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