1 /*
2     Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3 
4     This file is part of CopyQ.
5 
6     CopyQ is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     CopyQ is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "x11platform.h"
21 
22 #include "app/applicationexceptionhandler.h"
23 #include "common/log.h"
24 #include "common/textdata.h"
25 
26 #include <QApplication>
27 #include <QCoreApplication>
28 #include <QDir>
29 #include <QRegularExpression>
30 #include <QStringList>
31 #include <QVariant>
32 #include <QWidget>
33 #include <QX11Info>
34 
35 #include "x11platformwindow.h"
36 #include "x11platformclipboard.h"
37 
38 #include <X11/Xatom.h>
39 
40 #include <memory>
41 
42 namespace {
43 
44 int (*old_xio_errhandler)(Display *) = nullptr;
45 
46 const char *defaultDesktopFileContent =
47 R"([Desktop Entry]
48 Name=CopyQ
49 Icon=copyq
50 GenericName=Clipboard Manager
51 Type=Application
52 Terminal=false
53 X-KDE-autostart-after=panel
54 X-KDE-StartupNotify=false
55 X-KDE-UniqueApplet=true
56 )";
57 
58 // Try to handle X11 fatal error gracefully.
copyq_xio_errhandler(Display * display)59 int copyq_xio_errhandler(Display *display)
60 {
61     // Try to call MainWindow::saveTabs().
62     if ( QCoreApplication::instance() ) {
63         for ( auto obj : qApp->topLevelWidgets() ) {
64             if (obj->objectName() == "MainWindow") {
65                 QMetaObject::invokeMethod(obj, "saveTabs");
66                 break;
67             }
68         }
69     }
70 
71     // Call the old handler (possibly for Qt).
72     if (old_xio_errhandler)
73         old_xio_errhandler(display);
74 
75     // As documentation for XSetIOErrorHandler states, this function should not return.
76     exit(1);
77 }
78 
79 #ifdef COPYQ_DESKTOP_FILE
getDesktopFilename()80 QString getDesktopFilename()
81 {
82     const char *path = getenv("XDG_CONFIG_HOME");
83     QString filename = path ? getTextData(path) : QDir::homePath() + "/.config";
84     filename.append("/autostart/" + QCoreApplication::applicationName() + ".desktop");
85     return filename;
86 }
87 #endif
88 
printFileError(const QFile & file,const char * message,LogLevel logLevel=LogError)89 void printFileError(const QFile &file, const char *message, LogLevel logLevel = LogError)
90 {
91     log( QString("%1 \"%2\": %3")
92          .arg( QString::fromLatin1(message), file.fileName(), file.errorString()),
93          logLevel );
94 }
95 
maybePrintFileError(const QFile & file,const char * message)96 void maybePrintFileError(const QFile &file, const char *message)
97 {
98     if (file.error() != QFile::NoError)
99         printFileError(file, message);
100 }
101 
102 } // namespace
103 
platformNativeInterface()104 PlatformNativeInterface *platformNativeInterface()
105 {
106     static X11Platform platform;
107     return &platform;
108 }
109 
110 X11Platform::~X11Platform() = default;
111 
getWindow(WId winId)112 PlatformWindowPtr X11Platform::getWindow(WId winId)
113 {
114     if (!QX11Info::isPlatformX11())
115         return PlatformWindowPtr();
116 
117     std::unique_ptr<X11PlatformWindow> window(new X11PlatformWindow(winId));
118     return PlatformWindowPtr(window->isValid() ? window.release() : nullptr);
119 }
120 
getCurrentWindow()121 PlatformWindowPtr X11Platform::getCurrentWindow()
122 {
123     if (!QX11Info::isPlatformX11())
124         return PlatformWindowPtr();
125 
126     std::unique_ptr<X11PlatformWindow> window(new X11PlatformWindow());
127     return PlatformWindowPtr(window->isValid() ? window.release() : nullptr);
128 }
129 
canAutostart()130 bool X11Platform::canAutostart()
131 {
132 #ifdef COPYQ_DESKTOP_FILE
133     return true;
134 #else
135     return false;
136 #endif
137 }
138 
isAutostartEnabled()139 bool X11Platform::isAutostartEnabled()
140 {
141 #ifdef COPYQ_DESKTOP_FILE
142     const QString filename = getDesktopFilename();
143 
144     QFile desktopFile(filename);
145 
146     if ( !desktopFile.exists() )
147         return false;
148 
149     if ( !desktopFile.open(QIODevice::ReadOnly | QIODevice::Text) )
150         return false;
151 
152     const QRegularExpression re("^Hidden\\s*=\\s*([a-zA-Z01]+)");
153 
154     while ( !desktopFile.atEnd() ) {
155         const QString line = getTextData(desktopFile.readLine());
156         const auto m = re.match(line);
157         if (m.hasMatch()) {
158             const QString value = m.captured(1);
159             return !(value.startsWith("True") || value.startsWith("true") || value.startsWith("0"));
160         }
161     }
162 
163     return true;
164 #else
165     return false;
166 #endif
167 }
168 
setAutostartEnabled(bool enable)169 void X11Platform::setAutostartEnabled(bool enable)
170 {
171 #ifdef COPYQ_DESKTOP_FILE
172     if ( isAutostartEnabled() == enable )
173         return;
174 
175     const QString filename = getDesktopFilename();
176 
177     const auto autostartPath = QDir::cleanPath(filename + "/..");
178     QDir autostartDir(autostartPath);
179     if ( !autostartDir.mkpath(".") ) {
180         log( QString("Failed to create autostart path \"%1\"").arg(autostartPath) );
181         return;
182     }
183 
184     QFile desktopFile2(filename + ".new");
185     if ( !desktopFile2.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text) ) {
186         printFileError(desktopFile2, "Failed to create new desktop file");
187         return;
188     }
189 
190     const QRegularExpression re("^(Hidden|X-GNOME-Autostart-enabled|Exec)\\s*=\\s*");
191 
192     QFile desktopFile(filename);
193     bool createUserDesktopFile = !desktopFile.exists();
194     if (createUserDesktopFile)
195         desktopFile.setFileName(COPYQ_DESKTOP_FILE);
196 
197     if ( desktopFile.open(QIODevice::ReadOnly | QIODevice::Text) ) {
198         while ( !desktopFile.atEnd() ) {
199             const QString line = getTextData(desktopFile.readLine());
200             if ( !line.contains(re) )
201                 desktopFile2.write(line.toUtf8());
202         }
203         desktopFile.close();
204         maybePrintFileError(desktopFile, "Failed to read desktop file");
205     } else {
206         // Installed desktop file not found (can happen when running tests).
207         printFileError(desktopFile, "Failed to open desktop file", LogNote);
208         desktopFile2.write(defaultDesktopFileContent);
209     }
210 
211 #ifdef COPYQ_AUTOSTART_COMMAND
212     QString cmd = COPYQ_AUTOSTART_COMMAND;
213 #else
214     QString cmd = "\"" + QApplication::applicationFilePath() + "\"";
215 #endif
216     const QString sessionName = qApp->property("CopyQ_session_name").toString();
217     if ( !sessionName.isEmpty() )
218         cmd.append(" -s " + sessionName);
219     desktopFile2.write("Exec=" + cmd.toUtf8() + "\n");
220 
221     desktopFile2.write("Hidden=");
222     desktopFile2.write(enable ? "false" : "true");
223     desktopFile2.write("\n");
224 
225     desktopFile2.write("X-GNOME-Autostart-enabled=");
226     desktopFile2.write(enable ? "true" : "false");
227     desktopFile2.write("\n");
228 
229     QFile::remove(filename);
230     desktopFile2.rename(filename);
231 
232     maybePrintFileError(desktopFile2, "Failed to write desktop file");
233 #else
234     Q_UNUSED(enable)
235 #endif
236 }
237 
createConsoleApplication(int & argc,char ** argv)238 QCoreApplication *X11Platform::createConsoleApplication(int &argc, char **argv)
239 {
240     return new ApplicationExceptionHandler<QCoreApplication>(argc, argv);
241 }
242 
createServerApplication(int & argc,char ** argv)243 QApplication *X11Platform::createServerApplication(int &argc, char **argv)
244 {
245     if (QX11Info::isPlatformX11())
246         old_xio_errhandler = XSetIOErrorHandler(copyq_xio_errhandler);
247     return new ApplicationExceptionHandler<QApplication>(argc, argv);
248 }
249 
createMonitorApplication(int & argc,char ** argv)250 QGuiApplication *X11Platform::createMonitorApplication(int &argc, char **argv)
251 {
252     return new ApplicationExceptionHandler<QGuiApplication>(argc, argv);
253 }
254 
createClipboardProviderApplication(int & argc,char ** argv)255 QGuiApplication *X11Platform::createClipboardProviderApplication(int &argc, char **argv)
256 {
257     return new ApplicationExceptionHandler<QGuiApplication>(argc, argv);
258 }
259 
createClientApplication(int & argc,char ** argv)260 QCoreApplication *X11Platform::createClientApplication(int &argc, char **argv)
261 {
262     return new ApplicationExceptionHandler<QCoreApplication>(argc, argv);
263 }
264 
createTestApplication(int & argc,char ** argv)265 QGuiApplication *X11Platform::createTestApplication(int &argc, char **argv)
266 {
267     return new ApplicationExceptionHandler<QGuiApplication>(argc, argv);
268 }
269 
clipboard()270 PlatformClipboardPtr X11Platform::clipboard()
271 {
272     return PlatformClipboardPtr(new X11PlatformClipboard());
273 }
274 
getCommandLineArguments(int argc,char ** argv)275 QStringList X11Platform::getCommandLineArguments(int argc, char **argv)
276 {
277     if (argc == 0)
278         return QStringList();
279 
280     QStringList arguments;
281     arguments.reserve(argc - 1);
282 
283     for (int i = 1; i < argc; ++i)
284         arguments.append( QString::fromUtf8(argv[i]) );
285 
286     return arguments;
287 }
288 
findPluginDir(QDir * pluginsDir)289 bool X11Platform::findPluginDir(QDir *pluginsDir)
290 {
291     pluginsDir->setPath( qApp->applicationDirPath() );
292 
293     if ( pluginsDir->dirName() == QLatin1String("bin")
294          && pluginsDir->cdUp()
295          && (pluginsDir->cd(QLatin1String("lib64")) || pluginsDir->cd(QLatin1String("lib")))
296          && pluginsDir->cd(QLatin1String("copyq")) )
297     {
298         // OK, installed in /usr/local/bin or /usr/bin.
299         return true;
300     }
301 
302     pluginsDir->setPath( qApp->applicationDirPath() );
303 
304     if ( pluginsDir->cd(QLatin1String("plugins")) ) {
305         // OK, plugins in same directory as executable.
306         pluginsDir->cd(QLatin1String("copyq"));
307         return true;
308     }
309 
310     return false;
311 }
312 
defaultEditorCommand()313 QString X11Platform::defaultEditorCommand()
314 {
315     return QLatin1String("gedit --standalone -- %1");
316 }
317 
translationPrefix()318 QString X11Platform::translationPrefix()
319 {
320     return QString();
321 }
322 
sendDummyX11Event()323 void sendDummyX11Event()
324 {
325     if (!QX11Info::isPlatformX11())
326         return;
327 
328     auto display = QX11Info::display();
329     auto black = BlackPixel(display, 0);
330     Window window = XCreateSimpleWindow(
331         display, RootWindow(display, 0), -100000, -100000, 1, 1, 0, black, black);
332     XDestroyWindow(display, window);
333     XFlush(display);
334 }
335