File indexing completed on 2024-04-28 16:44:28

0001 /*
0002  * kstart.C. Part of the KDE project.
0003  *
0004  * SPDX-FileCopyrightText: 1997-2000 Matthias Ettrich <ettrich@kde.org>
0005  * SPDX-FileCopyrightText: David Faure <faure@kde.org>
0006  * SPDX-FileCopyrightText: Richard Moore <rich@kde.org>
0007  *
0008  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0009  */
0010 
0011 #include <config-kde-cli-tools.h>
0012 
0013 #include "kstart.h"
0014 #include <ktoolinvocation.h>
0015 
0016 #include <fcntl.h>
0017 #include <iostream>
0018 #include <stdlib.h>
0019 
0020 #include <QApplication>
0021 #include <QCommandLineOption>
0022 #include <QCommandLineParser>
0023 #include <QDebug>
0024 #include <QDesktopWidget>
0025 #include <QRegExp>
0026 #include <QScreen>
0027 #include <QTimer>
0028 #include <QUrl>
0029 
0030 #include <kaboutdata.h>
0031 #include <klocalizedstring.h>
0032 #include <kprocess.h>
0033 #include <kwindowsystem.h>
0034 
0035 #include <kxmessages.h>
0036 
0037 #include <KIO/ApplicationLauncherJob>
0038 #include <KIO/CommandLauncherJob>
0039 
0040 #include <QX11Info>
0041 #include <X11/Xlib.h>
0042 #include <X11/Xutil.h>
0043 #include <netwm.h>
0044 
0045 // some globals
0046 
0047 static QString servicePath; // TODO KF6 remove
0048 static QString serviceName;
0049 static QString exe;
0050 static QStringList exeArgs;
0051 static QString url;
0052 static QString windowtitle;
0053 static QString windowclass;
0054 static int desktop = 0;
0055 static bool activate = false;
0056 static bool iconify = false;
0057 static bool fullscreen = false;
0058 static NET::States state = {};
0059 static NET::States mask = {};
0060 static NET::WindowType windowtype = NET::Unknown;
0061 
0062 KStart::KStart()
0063     : QObject()
0064 {
0065     bool useRule = false;
0066 
0067 #ifdef HAVE_X11
0068     if (QX11Info::isPlatformX11()) {
0069         NETRootInfo i(QX11Info::connection(), NET::Supported);
0070         useRule = i.isSupported(NET::WM2KDETemporaryRules);
0071     }
0072 #endif
0073 
0074     if (useRule) {
0075         sendRule();
0076     } else {
0077         // connect to window add to get the NEW windows
0078         connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &KStart::windowAdded);
0079     }
0080 
0081     // finally execute the comand
0082     if (!servicePath.isEmpty()) { // TODO KF6 remove
0083         QString error;
0084         QString dbusService;
0085         int pid;
0086         if (KToolInvocation::startServiceByDesktopPath(exe, url, &error, &dbusService, &pid) == 0) {
0087             printf("%s\n", qPrintable(dbusService));
0088         } else {
0089             qCritical() << error;
0090         }
0091     } else if (!serviceName.isEmpty()) {
0092         KService::Ptr service = KService::serviceByDesktopName(serviceName);
0093         if (!service) {
0094             qCritical() << "No such service" << exe;
0095         } else {
0096             auto *job = new KIO::ApplicationLauncherJob(service);
0097             if (!url.isEmpty()) {
0098                 job->setUrls({QUrl(url)}); // TODO use QUrl::fromUserInput(PreferLocalFile)?
0099             }
0100             job->exec();
0101             if (job->error()) {
0102                 qCritical() << job->errorString();
0103             }
0104         }
0105     } else {
0106         auto *job = new KIO::CommandLauncherJob(exe, exeArgs);
0107         job->exec();
0108     }
0109 
0110     QTimer::singleShot(useRule ? 0 : 120 * 1000, qApp, SLOT(quit()));
0111 }
0112 
0113 void KStart::sendRule()
0114 {
0115     KXMessages msg;
0116     QString message;
0117     if (!windowtitle.isEmpty()) {
0118         message += QStringLiteral("title=") + windowtitle + QStringLiteral("\ntitlematch=3\n"); // 3 = regexp match
0119     }
0120     if (!windowclass.isEmpty()) {
0121         message += QStringLiteral("wmclass=") + windowclass + QStringLiteral("\nwmclassmatch=1\n") // 1 = exact match
0122             + QStringLiteral("wmclasscomplete=")
0123             // if windowclass contains a space (i.e. 2 words, use whole WM_CLASS)
0124             + (windowclass.contains(QLatin1Char(' ')) ? QStringLiteral("true") : QStringLiteral("false")) + QLatin1Char('\n');
0125     }
0126     if ((!windowtitle.isEmpty()) || (!windowclass.isEmpty())) {
0127         // always ignore these window types
0128         message += QStringLiteral("types=")
0129             + QString().setNum(-1U & ~(NET::TopMenuMask | NET::ToolbarMask | NET::DesktopMask | NET::SplashMask | NET::MenuMask)) + QLatin1Char('\n');
0130     } else {
0131         // accept only "normal" windows
0132         message += QStringLiteral("types=") + QString().setNum(NET::NormalMask | NET::DialogMask) + QLatin1Char('\n');
0133     }
0134     if ((desktop > 0 && desktop <= KWindowSystem::numberOfDesktops()) || desktop == NETWinInfo::OnAllDesktops) {
0135         message += QStringLiteral("desktop=") + QString().setNum(desktop) + QStringLiteral("\ndesktoprule=3\n");
0136     }
0137     if (activate) {
0138         message += QStringLiteral("fsplevel=0\nfsplevelrule=2\n");
0139     }
0140     if (iconify) {
0141         message += QStringLiteral("minimize=true\nminimizerule=3\n");
0142     }
0143     if (windowtype != NET::Unknown) {
0144         message += QStringLiteral("type=") + QString().setNum(windowtype) + QStringLiteral("\ntyperule=2");
0145     }
0146     if (state) {
0147         if (state & NET::KeepAbove) {
0148             message += QStringLiteral("above=true\naboverule=3\n");
0149         }
0150         if (state & NET::KeepBelow) {
0151             message += QStringLiteral("below=true\nbelowrule=3\n");
0152         }
0153         if (state & NET::SkipTaskbar) {
0154             message += QStringLiteral("skiptaskbar=true\nskiptaskbarrule=3\n");
0155         }
0156         if (state & NET::SkipPager) {
0157             message += QStringLiteral("skippager=true\nskippagerrule=3\n");
0158         }
0159         if (state & NET::MaxVert) {
0160             message += QStringLiteral("maximizevert=true\nmaximizevertrule=3\n");
0161         }
0162         if (state & NET::MaxHoriz) {
0163             message += QStringLiteral("maximizehoriz=true\nmaximizehorizrule=3\n");
0164         }
0165         if (state & NET::FullScreen) {
0166             message += QStringLiteral("fullscreen=true\nfullscreenrule=3\n");
0167         }
0168     }
0169 
0170     msg.broadcastMessage("_KDE_NET_WM_TEMPORARY_RULES", message, -1);
0171 }
0172 
0173 const NET::WindowTypes SUPPORTED_WINDOW_TYPES_MASK = NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask
0174     | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask;
0175 
0176 void KStart::windowAdded(WId w)
0177 {
0178     KWindowInfo info(w, NET::WMWindowType | NET::WMName);
0179 
0180     // always ignore these window types
0181     if (info.windowType(SUPPORTED_WINDOW_TYPES_MASK) == NET::TopMenu || info.windowType(SUPPORTED_WINDOW_TYPES_MASK) == NET::Toolbar
0182         || info.windowType(SUPPORTED_WINDOW_TYPES_MASK) == NET::Desktop) {
0183         return;
0184     }
0185 
0186     if (!windowtitle.isEmpty()) {
0187         QString title = info.name().toLower();
0188         QRegExp r(windowtitle.toLower());
0189         if (!r.exactMatch(title)) {
0190             return; // no match
0191         }
0192     }
0193     if (windowtitle.isEmpty() && windowclass.isEmpty()) {
0194         // accept only "normal" windows
0195         if (info.windowType(SUPPORTED_WINDOW_TYPES_MASK) != NET::Unknown && info.windowType(SUPPORTED_WINDOW_TYPES_MASK) != NET::Normal
0196             && info.windowType(SUPPORTED_WINDOW_TYPES_MASK) != NET::Dialog) {
0197             return;
0198         }
0199     }
0200     applyStyle(w);
0201     QApplication::exit();
0202 }
0203 
0204 void KStart::applyStyle(WId w)
0205 {
0206     if (state || iconify || windowtype != NET::Unknown || desktop >= 1) {
0207         XWithdrawWindow(QX11Info::display(), w, QX11Info::appScreen());
0208     }
0209 
0210     NETWinInfo info(QX11Info::connection(), w, QX11Info::appRootWindow(), NET::WMState, NET::Properties2());
0211 
0212     if ((desktop > 0 && desktop <= KWindowSystem::numberOfDesktops()) || desktop == NETWinInfo::OnAllDesktops) {
0213         info.setDesktop(desktop);
0214     }
0215 
0216     if (iconify) {
0217         XWMHints *hints = XGetWMHints(QX11Info::display(), w);
0218         if (hints) {
0219             hints->flags |= StateHint;
0220             hints->initial_state = IconicState;
0221             XSetWMHints(QX11Info::display(), w, hints);
0222             XFree(hints);
0223         }
0224     }
0225 
0226     if (windowtype != NET::Unknown) {
0227         info.setWindowType(windowtype);
0228     }
0229 
0230     if (state) {
0231         info.setState(state, mask);
0232     }
0233 
0234     if (fullscreen) {
0235         QRect r = QGuiApplication::primaryScreen()->geometry();
0236         XMoveResizeWindow(QX11Info::display(), w, r.x(), r.y(), r.width(), r.height());
0237     }
0238 
0239     XSync(QX11Info::display(), False);
0240 
0241     XMapWindow(QX11Info::display(), w);
0242     XSync(QX11Info::display(), False);
0243 
0244     if (activate) {
0245         KWindowSystem::forceActiveWindow(w);
0246     }
0247 }
0248 
0249 int main(int argc, char *argv[])
0250 {
0251     QApplication app(argc, argv);
0252     KLocalizedString::setApplicationDomain("kstart5");
0253 
0254     KAboutData aboutData(QStringLiteral("kstart"),
0255                          i18n("KStart"),
0256                          QString::fromLatin1(PROJECT_VERSION),
0257                          i18n(""
0258                               "Utility to launch applications with special window properties \n"
0259                               "such as iconified, maximized, a certain virtual desktop, a special decoration\n"
0260                               "and so on."),
0261                          KAboutLicense::GPL,
0262                          i18n("(C) 1997-2000 Matthias Ettrich (ettrich@kde.org)"));
0263 
0264     aboutData.addAuthor(i18n("Matthias Ettrich"), QString(), QStringLiteral("ettrich@kde.org"));
0265     aboutData.addAuthor(i18n("David Faure"), QString(), QStringLiteral("faure@kde.org"));
0266     aboutData.addAuthor(i18n("Richard J. Moore"), QString(), QStringLiteral("rich@kde.org"));
0267     KAboutData::setApplicationData(aboutData);
0268 
0269     QCommandLineParser parser;
0270     aboutData.setupCommandLine(&parser);
0271     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("!+command"), i18n("Command to execute")));
0272     // TODO KF6 remove
0273     parser.addOption(
0274         QCommandLineOption(QStringList() << QLatin1String("service"),
0275                            i18n("Alternative to <command>: desktop file path to start. D-Bus service will be printed to stdout. Deprecated: use --application"),
0276                            QLatin1String("desktopfile")));
0277     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("application"),
0278                                         i18n("Alternative to <command>: desktop file to start."),
0279                                         QLatin1String("desktopfile")));
0280     parser.addOption(
0281         QCommandLineOption(QStringList() << QLatin1String("url"), i18n("Optional URL to pass <desktopfile>, when using --service"), QLatin1String("url")));
0282     // "!" means: all options after command are treated as arguments to the command
0283     parser.addOption(
0284         QCommandLineOption(QStringList() << QLatin1String("window"), i18n("A regular expression matching the window title"), QLatin1String("regexp")));
0285     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("windowclass"),
0286                                         i18n("A string matching the window class (WM_CLASS property)\n"
0287                                              "The window class can be found out by running\n"
0288                                              "'xprop | grep WM_CLASS' and clicking on a window\n"
0289                                              "(use either both parts separated by a space or only the right part).\n"
0290                                              "NOTE: If you specify neither window title nor window class,\n"
0291                                              "then the very first window to appear will be taken;\n"
0292                                              "omitting both options is NOT recommended."),
0293                                         QLatin1String("class")));
0294     parser.addOption(
0295         QCommandLineOption(QStringList() << QLatin1String("desktop"), i18n("Desktop on which to make the window appear"), QLatin1String("number")));
0296     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("currentdesktop"),
0297                                         i18n("Make the window appear on the desktop that was active\nwhen starting the application")));
0298     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("alldesktops"), i18n("Make the window appear on all desktops")));
0299     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("iconify"), i18n("Iconify the window")));
0300     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("maximize"), i18n("Maximize the window")));
0301     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("maximize-vertically"), i18n("Maximize the window vertically")));
0302     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("maximize-horizontally"), i18n("Maximize the window horizontally")));
0303     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("fullscreen"), i18n("Show window fullscreen")));
0304     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("type"),
0305                                         i18n("The window type: Normal, Desktop, Dock, Toolbar, \nMenu, Dialog, TopMenu or Override"),
0306                                         QLatin1String("type")));
0307     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("activate"),
0308                                         i18n("Jump to the window even if it is started on a \n"
0309                                              "different virtual desktop")));
0310     parser.addOption(
0311         QCommandLineOption(QStringList() << QLatin1String("ontop") << QLatin1String("keepabove"), i18n("Try to keep the window above other windows")));
0312     parser.addOption(
0313         QCommandLineOption(QStringList() << QLatin1String("onbottom") << QLatin1String("keepbelow"), i18n("Try to keep the window below other windows")));
0314     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("skiptaskbar"), i18n("The window does not get an entry in the taskbar")));
0315     parser.addOption(QCommandLineOption(QStringList() << QLatin1String("skippager"), i18n("The window does not get an entry on the pager")));
0316 
0317     parser.process(app);
0318     aboutData.processCommandLine(&parser);
0319 
0320     if (parser.isSet(QStringLiteral("service"))) {
0321         servicePath = parser.value(QStringLiteral("service"));
0322         url = parser.value(QStringLiteral("url"));
0323     } else if (parser.isSet(QStringLiteral("application"))) {
0324         serviceName = parser.value(QStringLiteral("application"));
0325         url = parser.value(QStringLiteral("url"));
0326     } else {
0327         QStringList positionalArgs = parser.positionalArguments();
0328         if (positionalArgs.isEmpty()) {
0329             qCritical() << i18n("No command specified");
0330             parser.showHelp(1);
0331         }
0332 
0333         exe = positionalArgs.takeFirst();
0334         exeArgs = positionalArgs;
0335     }
0336 
0337     desktop = parser.value(QStringLiteral("desktop")).toInt();
0338     if (parser.isSet(QStringLiteral("alldesktops"))) {
0339         desktop = NETWinInfo::OnAllDesktops;
0340     }
0341     if (parser.isSet(QStringLiteral("currentdesktop"))) {
0342         desktop = KWindowSystem::currentDesktop();
0343     }
0344 
0345     windowtitle = parser.value(QStringLiteral("window"));
0346     windowclass = parser.value(QStringLiteral("windowclass"));
0347     if (!windowclass.isEmpty()) {
0348         windowclass = windowclass.toLower();
0349     }
0350 
0351     if (windowtitle.isEmpty() && windowclass.isEmpty()) {
0352         qWarning() << "Omitting both --window and --windowclass arguments is not recommended";
0353     }
0354 
0355     QString s = parser.value(QStringLiteral("type"));
0356     if (!s.isEmpty()) {
0357         s = s.toLower();
0358         if (s == QLatin1String("desktop")) {
0359             windowtype = NET::Desktop;
0360         } else if (s == QLatin1String("dock")) {
0361             windowtype = NET::Dock;
0362         } else if (s == QLatin1String("toolbar")) {
0363             windowtype = NET::Toolbar;
0364         } else if (s == QLatin1String("menu")) {
0365             windowtype = NET::Menu;
0366         } else if (s == QLatin1String("dialog")) {
0367             windowtype = NET::Dialog;
0368         } else if (s == QLatin1String("override")) {
0369             windowtype = NET::Override;
0370         } else if (s == QLatin1String("topmenu")) {
0371             windowtype = NET::TopMenu;
0372         } else {
0373             windowtype = NET::Normal;
0374         }
0375     }
0376 
0377     if (parser.isSet(QStringLiteral("keepabove"))) {
0378         state |= NET::KeepAbove;
0379         mask |= NET::KeepAbove;
0380     } else if (parser.isSet(QStringLiteral("keepbelow"))) {
0381         state |= NET::KeepBelow;
0382         mask |= NET::KeepBelow;
0383     }
0384 
0385     if (parser.isSet(QStringLiteral("skiptaskbar"))) {
0386         state |= NET::SkipTaskbar;
0387         mask |= NET::SkipTaskbar;
0388     }
0389 
0390     if (parser.isSet(QStringLiteral("skippager"))) {
0391         state |= NET::SkipPager;
0392         mask |= NET::SkipPager;
0393     }
0394 
0395     activate = parser.isSet(QStringLiteral("activate"));
0396 
0397     if (parser.isSet(QStringLiteral("maximize"))) {
0398         state |= NET::Max;
0399         mask |= NET::Max;
0400     }
0401     if (parser.isSet(QStringLiteral("maximize-vertically"))) {
0402         state |= NET::MaxVert;
0403         mask |= NET::MaxVert;
0404     }
0405     if (parser.isSet(QStringLiteral("maximize-horizontally"))) {
0406         state |= NET::MaxHoriz;
0407         mask |= NET::MaxHoriz;
0408     }
0409 
0410     iconify = parser.isSet(QStringLiteral("iconify"));
0411     if (parser.isSet(QStringLiteral("fullscreen"))) {
0412         NETRootInfo i(QX11Info::connection(), NET::Supported);
0413         if (i.isSupported(NET::FullScreen)) {
0414             state |= NET::FullScreen;
0415             mask |= NET::FullScreen;
0416         } else {
0417             windowtype = NET::Override;
0418             fullscreen = true;
0419         }
0420     }
0421 
0422     fcntl(XConnectionNumber(QX11Info::display()), F_SETFD, 1);
0423 
0424     KStart start;
0425 
0426     return app.exec();
0427 }