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
52void 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(sender: subjob, signal: &KJob::result, context: this, slot: [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
76void KTerminalLauncherJob::emitDelayedResult()
77{
78 // Use delayed invocation so the caller has time to connect to the signal
79 QMetaObject::invokeMethod(object: this, function: &KTerminalLauncherJob::emitResult, type: Qt::QueuedConnection);
80}
81
82#ifndef Q_OS_WIN
83// helper function to help scope service so that not the entire determineFullCommand has access to it (it is not
84// always not null!)
85static KServicePtr serviceFromConfig(bool fallbackToKonsoleService)
86{
87 const KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
88 const QString terminalExec = confGroup.readEntry(key: "TerminalApplication");
89 const QString terminalService = confGroup.readEntry(key: "TerminalService");
90 KServicePtr service;
91 if (!terminalService.isEmpty()) {
92 service = KService::serviceByStorageId(storageId: terminalService);
93 } else if (!terminalExec.isEmpty()) {
94 service = new KService(QStringLiteral("terminal"), terminalExec, QStringLiteral("utilities-terminal"));
95 }
96 if (!service && fallbackToKonsoleService) {
97 service = KService::serviceByStorageId(QStringLiteral("org.kde.konsole"));
98 }
99 return service;
100}
101#endif
102
103// This sets m_fullCommand, but also (when possible) m_desktopName
104void KTerminalLauncherJob::determineFullCommand(bool fallbackToKonsoleService /* allow synthesizing no konsole */)
105{
106 const QString workingDir = d->m_workingDirectory;
107#ifndef Q_OS_WIN
108
109 QString exec;
110 if (KServicePtr service = serviceFromConfig(fallbackToKonsoleService); service) {
111 d->m_desktopName = service->desktopEntryName();
112 exec = service->exec();
113 } else {
114 // konsole not found by desktop file, let's see what PATH has for us
115 auto useIfAvailable = [&exec](const QString &terminalApp) {
116 const bool found = !QStandardPaths::findExecutable(executableName: terminalApp).isEmpty();
117 if (found) {
118 exec = terminalApp;
119 }
120 return found;
121 };
122 if (!useIfAvailable(QStringLiteral("konsole")) && !useIfAvailable(QStringLiteral("xterm"))) {
123 setError(KJob::UserDefinedError);
124 setErrorText(i18n("No terminal emulator found"));
125 return;
126 }
127 }
128 if (!d->m_command.isEmpty()) {
129 if (exec == QLatin1String("konsole")) {
130 exec += QLatin1String(" --noclose");
131 } else if (exec == QLatin1String("xterm")) {
132 exec += QLatin1String(" -hold");
133 }
134 }
135 if (exec.startsWith(s: QLatin1String("konsole")) && !workingDir.isEmpty()) {
136 exec += QLatin1String(" --workdir %1").arg(args: KShell::quoteArg(arg: workingDir));
137 }
138 if (!d->m_command.isEmpty()) {
139 exec += QLatin1String(" -e ") + d->m_command;
140 }
141#else
142 const QString windowsTerminal = QStringLiteral("wt.exe");
143 const QString pwsh = QStringLiteral("pwsh.exe");
144 const QString powershell = QStringLiteral("powershell.exe"); // Powershell is used as fallback
145 const bool hasWindowsTerminal = !QStandardPaths::findExecutable(windowsTerminal).isEmpty();
146 const bool hasPwsh = !QStandardPaths::findExecutable(pwsh).isEmpty();
147
148 QString exec;
149 if (hasWindowsTerminal) {
150 exec = windowsTerminal;
151 if (!workingDir.isEmpty()) {
152 exec += QLatin1String(" --startingDirectory %1").arg(KShell::quoteArg(workingDir));
153 }
154 if (!d->m_command.isEmpty()) {
155 // Command and NoExit flag will be added later
156 exec += QLatin1Char(' ') + (hasPwsh ? pwsh : powershell);
157 }
158 } else {
159 exec = hasPwsh ? pwsh : powershell;
160 }
161 if (!d->m_command.isEmpty()) {
162 exec += QLatin1String(" -NoExit -Command ") + d->m_command;
163 }
164#endif
165 d->m_fullCommand = exec;
166}
167
168QString KTerminalLauncherJob::fullCommand() const
169{
170 return d->m_fullCommand;
171}
172
173#include "moc_kterminallauncherjob.cpp"
174

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