1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2021 David Faure <faure@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7 
8 #include "kterminallauncherjob.h"
9 
10 #include <KConfigGroup>
11 #include <KLocalizedString>
12 #include <KService>
13 #include <KSharedConfig>
14 #include <KShell>
15 #include <QProcessEnvironment>
16 
17 class KTerminalLauncherJobPrivate
18 {
19 public:
20     QString m_workingDirectory;
21     QString m_command; // "ls"
22     QString m_fullCommand; // "xterm -e ls"
23     QString m_desktopName;
24     QByteArray m_startupId;
25     QProcessEnvironment m_environment;
26 };
27 
KTerminalLauncherJob(const QString & command,QObject * parent)28 KTerminalLauncherJob::KTerminalLauncherJob(const QString &command, QObject *parent)
29     : KJob(parent)
30     , d(new KTerminalLauncherJobPrivate)
31 {
32     d->m_command = command;
33 }
34 
35 KTerminalLauncherJob::~KTerminalLauncherJob() = default;
36 
setWorkingDirectory(const QString & workingDirectory)37 void KTerminalLauncherJob::setWorkingDirectory(const QString &workingDirectory)
38 {
39     d->m_workingDirectory = workingDirectory;
40 }
41 
setStartupId(const QByteArray & startupId)42 void KTerminalLauncherJob::setStartupId(const QByteArray &startupId)
43 {
44     d->m_startupId = startupId;
45 }
46 
setProcessEnvironment(const QProcessEnvironment & environment)47 void KTerminalLauncherJob::setProcessEnvironment(const QProcessEnvironment &environment)
48 {
49     d->m_environment = environment;
50 }
51 
start()52 void KTerminalLauncherJob::start()
53 {
54     determineFullCommand();
55     if (error()) {
56         emitDelayedResult();
57     } else {
58         auto *subjob = new KIO::CommandLauncherJob(d->m_fullCommand, this);
59         subjob->setDesktopName(d->m_desktopName);
60         subjob->setWorkingDirectory(d->m_workingDirectory);
61         subjob->setStartupId(d->m_startupId);
62         subjob->setProcessEnvironment(d->m_environment);
63         connect(subjob, &KJob::result, this, [this, subjob] {
64             // NB: must go through emitResult otherwise we don't get correctly finished!
65             // TODO KF6: maybe change the base to KCompositeJob so we can get rid of this nonesense
66             if (subjob->error()) {
67                 setError(subjob->error());
68                 setErrorText(subjob->errorText());
69             }
70             emitResult();
71         });
72         subjob->start();
73     }
74 }
75 
emitDelayedResult()76 void KTerminalLauncherJob::emitDelayedResult()
77 {
78     // Use delayed invocation so the caller has time to connect to the signal
79     QMetaObject::invokeMethod(this, &KTerminalLauncherJob::emitResult, Qt::QueuedConnection);
80 }
81 
82 // This sets m_fullCommand, but also (when possible) m_desktopName
determineFullCommand()83 void KTerminalLauncherJob::determineFullCommand()
84 {
85     const QString workingDir = d->m_workingDirectory;
86 #ifndef Q_OS_WIN
87     const KConfigGroup confGroup(KSharedConfig::openConfig(), "General");
88     const QString terminalExec = confGroup.readEntry("TerminalApplication");
89     const QString terminalService = confGroup.readEntry("TerminalService");
90     KServicePtr service;
91     if (!terminalService.isEmpty()) {
92         service = KService::serviceByStorageId(terminalService);
93     } else if (!terminalExec.isEmpty()) {
94         service = new KService(QStringLiteral("terminal"), terminalExec, QStringLiteral("utilities-terminal"));
95     }
96     if (!service) {
97         service = KService::serviceByStorageId(QStringLiteral("org.kde.konsole"));
98     }
99     QString exec;
100     if (service) {
101         d->m_desktopName = service->desktopEntryName();
102         exec = service->exec();
103     } else {
104         // konsole not found by desktop file, let's see what PATH has for us
105         auto useIfAvailable = [&exec](const QString &terminalApp) {
106             const bool found = !QStandardPaths::findExecutable(terminalApp).isEmpty();
107             if (found) {
108                 exec = terminalApp;
109             }
110             return found;
111         };
112         if (!useIfAvailable(QStringLiteral("konsole")) && !useIfAvailable(QStringLiteral("xterm"))) {
113             setError(KJob::UserDefinedError);
114             setErrorText(i18n("No terminal emulator found"));
115             return;
116         }
117     }
118     if (!d->m_command.isEmpty()) {
119         if (exec == QLatin1String("konsole")) {
120             exec += QLatin1String(" --noclose");
121         } else if (exec == QLatin1String("xterm")) {
122             exec += QLatin1String(" -hold");
123         }
124     }
125     if (service->exec() == QLatin1String("konsole") && !workingDir.isEmpty()) {
126         exec += QLatin1String(" --workdir %1").arg(KShell::quoteArg(workingDir));
127     }
128     if (!d->m_command.isEmpty()) {
129         exec += QLatin1String(" -e ") + d->m_command;
130     }
131 #else
132     const QString windowsTerminal = QStringLiteral("wt.exe");
133     const QString pwsh = QStringLiteral("pwsh.exe");
134     const QString powershell = QStringLiteral("powershell.exe"); // Powershell is used as fallback
135     const bool hasWindowsTerminal = !QStandardPaths::findExecutable(windowsTerminal).isEmpty();
136     const bool hasPwsh = !QStandardPaths::findExecutable(pwsh).isEmpty();
137 
138     QString exec;
139     if (hasWindowsTerminal) {
140         exec = windowsTerminal;
141         if (!workingDir.isEmpty()) {
142             exec += QLatin1String(" --startingDirectory %1").arg(KShell::quoteArg(workingDir));
143         }
144         if (!d->m_command.isEmpty()) {
145             // Command and NoExit flag will be added later
146             exec += QLatin1Char(' ') + (hasPwsh ? pwsh : powershell);
147         }
148     } else {
149         exec = hasPwsh ? pwsh : powershell;
150     }
151     if (!d->m_command.isEmpty()) {
152         exec += QLatin1String(" -NoExit -Command ") + d->m_command;
153     }
154 #endif
155     d->m_fullCommand = exec;
156 }
157 
fullCommand() const158 QString KTerminalLauncherJob::fullCommand() const
159 {
160     return d->m_fullCommand;
161 }
162