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
17class KTerminalLauncherJobPrivate
18{
19public:
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{QProcessEnvironment::InheritFromParent};
26};
27
28KTerminalLauncherJob::KTerminalLauncherJob(const QString &command, QObject *parent)
29 : KJob(parent)
30 , d(new KTerminalLauncherJobPrivate)
31{
32 d->m_command = command;
33}
34
35KTerminalLauncherJob::~KTerminalLauncherJob() = default;
36
37void KTerminalLauncherJob::setWorkingDirectory(const QString &workingDirectory)
38{
39 d->m_workingDirectory = workingDirectory;
40}
41
42void KTerminalLauncherJob::setStartupId(const QByteArray &startupId)
43{
44 d->m_startupId = startupId;
45}
46
47void KTerminalLauncherJob::setProcessEnvironment(const QProcessEnvironment &environment)
48{
49 d->m_environment = environment;
50}
51
52// prepares for launching but doesn't actually launch yet. sets error on error
53bool KTerminalLauncherJob::prepare()
54{
55 determineFullCommand(); // checks and sets m_fullCommand
56 return error() == KJob::NoError;
57}
58
59void KTerminalLauncherJob::start()
60{
61 if (!prepare()) {
62 emitDelayedResult();
63 } else {
64 auto *subjob = new KIO::CommandLauncherJob(d->m_fullCommand, this);
65 subjob->setDesktopName(d->m_desktopName);
66 subjob->setWorkingDirectory(d->m_workingDirectory);
67 subjob->setStartupId(d->m_startupId);
68 subjob->setProcessEnvironment(d->m_environment);
69 connect(sender: subjob, signal: &KJob::result, context: this, slot: [this, subjob] {
70 // NB: must go through emitResult otherwise we don't get correctly finished!
71 // TODO KF6: maybe change the base to KCompositeJob so we can get rid of this nonesense
72 if (subjob->error()) {
73 setError(subjob->error());
74 setErrorText(subjob->errorText());
75 }
76 emitResult();
77 });
78 subjob->start();
79 }
80}
81
82void KTerminalLauncherJob::emitDelayedResult()
83{
84 // Use delayed invocation so the caller has time to connect to the signal
85 QMetaObject::invokeMethod(object: this, function: &KTerminalLauncherJob::emitResult, type: Qt::QueuedConnection);
86}
87
88#ifndef Q_OS_WIN
89// helper function to help scope service so that not the entire determineFullCommand has access to it (it is not
90// always not null!)
91static KServicePtr serviceFromConfig(bool fallbackToKonsoleService)
92{
93 const KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
94 const QString terminalExec = confGroup.readEntry(key: "TerminalApplication");
95 const QString terminalService = confGroup.readEntry(key: "TerminalService");
96 KServicePtr service;
97 if (!terminalService.isEmpty()) {
98 service = KService::serviceByStorageId(storageId: terminalService);
99 } else if (!terminalExec.isEmpty()) {
100 service = new KService(QStringLiteral("terminal"), terminalExec, QStringLiteral("utilities-terminal"));
101 }
102 if (!service && fallbackToKonsoleService) {
103 service = KService::serviceByStorageId(QStringLiteral("org.kde.konsole"));
104 }
105 return service;
106}
107#endif
108
109// This sets m_fullCommand, but also (when possible) m_desktopName
110void KTerminalLauncherJob::determineFullCommand(bool fallbackToKonsoleService /* allow synthesizing no konsole */)
111{
112 const QString workingDir = d->m_workingDirectory;
113#ifndef Q_OS_WIN
114
115 QString exec;
116 if (KServicePtr service = serviceFromConfig(fallbackToKonsoleService); service) {
117 d->m_desktopName = service->desktopEntryName();
118 exec = service->exec();
119 } else {
120 // konsole not found by desktop file, let's see what PATH has for us
121 auto useIfAvailable = [&exec](const QString &terminalApp) {
122 const bool found = !QStandardPaths::findExecutable(executableName: terminalApp).isEmpty();
123 if (found) {
124 exec = terminalApp;
125 }
126 return found;
127 };
128 if (!useIfAvailable(QStringLiteral("konsole")) && !useIfAvailable(QStringLiteral("xterm"))) {
129 setError(KJob::UserDefinedError);
130 setErrorText(i18n("No terminal emulator found"));
131 return;
132 }
133 }
134 if (!d->m_command.isEmpty()) {
135 if (exec == QLatin1String("konsole")) {
136 exec += QLatin1String(" --noclose");
137 } else if (exec == QLatin1String("xterm")) {
138 exec += QLatin1String(" -hold");
139 }
140 }
141 if (exec.startsWith(s: QLatin1String("konsole")) && !workingDir.isEmpty()) {
142 exec += QLatin1String(" --workdir %1").arg(args: KShell::quoteArg(arg: workingDir));
143 }
144 if (!d->m_command.isEmpty()) {
145 exec += QLatin1String(" -e ") + d->m_command;
146 }
147#else
148 const QString windowsTerminal = QStringLiteral("wt.exe");
149 const QString pwsh = QStringLiteral("pwsh.exe");
150 const QString powershell = QStringLiteral("powershell.exe"); // Powershell is used as fallback
151 const bool hasWindowsTerminal = !QStandardPaths::findExecutable(windowsTerminal).isEmpty();
152 const bool hasPwsh = !QStandardPaths::findExecutable(pwsh).isEmpty();
153
154 QString exec;
155 if (hasWindowsTerminal) {
156 exec = windowsTerminal;
157 if (!workingDir.isEmpty()) {
158 exec += QLatin1String(" --startingDirectory %1").arg(KShell::quoteArg(workingDir));
159 }
160 if (!d->m_command.isEmpty()) {
161 // Command and NoExit flag will be added later
162 exec += QLatin1Char(' ') + (hasPwsh ? pwsh : powershell);
163 }
164 } else {
165 exec = hasPwsh ? pwsh : powershell;
166 }
167 if (!d->m_command.isEmpty()) {
168 exec += QLatin1String(" -NoExit -Command ") + d->m_command;
169 }
170#endif
171 d->m_fullCommand = exec;
172}
173
174QString KTerminalLauncherJob::fullCommand() const
175{
176 return d->m_fullCommand;
177}
178
179#include "moc_kterminallauncherjob.cpp"
180

source code of kio/src/gui/kterminallauncherjob.cpp