1 /*
2 SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 // Own
8 #include "Application.h"
9
10 // Qt
11 #include <QApplication>
12 #include <QCommandLineParser>
13 #include <QDir>
14 #include <QFileInfo>
15 #include <QHash>
16 #include <QMenuBar>
17 #include <QStandardPaths>
18 #include <QTimer>
19
20 // KDE
21 #include <KActionCollection>
22 #include <KGlobalAccel>
23 #include <KLocalizedString>
24
25 // Konsole
26 #include "KonsoleSettings.h"
27 #include "MainWindow.h"
28 #include "ShellCommand.h"
29 #include "ViewManager.h"
30 #include "WindowSystemInfo.h"
31 #include "profile/ProfileCommandParser.h"
32 #include "profile/ProfileManager.h"
33 #include "session/Session.h"
34 #include "session/SessionManager.h"
35 #include "terminalDisplay/TerminalDisplay.h"
36 #include "widgets/ViewContainer.h"
37
38 #include "pluginsystem/IKonsolePlugin.h"
39
40 using namespace Konsole;
41
Application(QSharedPointer<QCommandLineParser> parser,const QStringList & customCommand)42 Application::Application(QSharedPointer<QCommandLineParser> parser, const QStringList &customCommand)
43 : _backgroundInstance(nullptr)
44 , m_parser(parser)
45 , m_customCommand(customCommand)
46 {
47 m_pluginManager.loadAllPlugins();
48 }
49
populateCommandLineParser(QCommandLineParser * parser)50 void Application::populateCommandLineParser(QCommandLineParser *parser)
51 {
52 const auto options = QVector<QCommandLineOption>{
53 {{QStringLiteral("profile")}, i18nc("@info:shell", "Name of profile to use for new Konsole instance"), QStringLiteral("name")},
54 {{QStringLiteral("layout")}, i18nc("@info:shell", "json layoutfile to be loaded to use for new Konsole instance"), QStringLiteral("file")},
55 {{QStringLiteral("fallback-profile")}, i18nc("@info:shell", "Use the internal FALLBACK profile")},
56 {{QStringLiteral("workdir")}, i18nc("@info:shell", "Set the initial working directory of the new tab or window to 'dir'"), QStringLiteral("dir")},
57 {{QStringLiteral("hold"), QStringLiteral("noclose")}, i18nc("@info:shell", "Do not close the initial session automatically when it ends.")},
58 // BR: 373440
59 {{QStringLiteral("new-tab")}, i18nc("@info:shell", "Create a new tab in an existing window rather than creating a new window ('Run all Konsole windows in a single process' must be enabled)")},
60 {{QStringLiteral("tabs-from-file")}, i18nc("@info:shell", "Create tabs as specified in given tabs configuration file"), QStringLiteral("file")},
61 {{QStringLiteral("background-mode")},
62 i18nc("@info:shell", "Start Konsole in the background and bring to the front when Ctrl+Shift+F12 (by default) is pressed")},
63 {{QStringLiteral("separate"), QStringLiteral("nofork")}, i18nc("@info:shell", "Run in a separate process")},
64 {{QStringLiteral("show-menubar")}, i18nc("@info:shell", "Show the menubar, overriding the default setting")},
65 {{QStringLiteral("hide-menubar")}, i18nc("@info:shell", "Hide the menubar, overriding the default setting")},
66 {{QStringLiteral("show-tabbar")}, i18nc("@info:shell", "Show the tabbar, overriding the default setting")},
67 {{QStringLiteral("hide-tabbar")}, i18nc("@info:shell", "Hide the tabbar, overriding the default setting")},
68 {{QStringLiteral("fullscreen")}, i18nc("@info:shell", "Start Konsole in fullscreen mode")},
69 {{QStringLiteral("notransparency")}, i18nc("@info:shell", "Disable transparent backgrounds, even if the system supports them.")},
70 {{QStringLiteral("list-profiles")}, i18nc("@info:shell", "List the available profiles")},
71 {{QStringLiteral("list-profile-properties")}, i18nc("@info:shell", "List all the profile properties names and their type (for use with -p)")},
72 {{QStringLiteral("p")}, i18nc("@info:shell", "Change the value of a profile property."), QStringLiteral("property=value")},
73 {{QStringLiteral("e")},
74 i18nc("@info:shell", "Command to execute. This option will catch all following arguments, so use it as the last option."),
75 QStringLiteral("cmd")}};
76 for (const auto &option : options) {
77 parser->addOption(option);
78 }
79
80 parser->addPositionalArgument(QStringLiteral("[args]"), i18nc("@info:shell", "Arguments passed to command"));
81
82 // Add a no-op compatibility option to make Konsole compatible with
83 // Debian's policy on X terminal emulators.
84 // -T is technically meant to set a title, that is not really meaningful
85 // for Konsole as we have multiple user-facing options controlling
86 // the title and overriding whatever is set elsewhere.
87 // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=532029
88 // https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s11.8.3
89 auto titleOption = QCommandLineOption({QStringLiteral("T")}, QStringLiteral("Debian policy compatibility, not used"), QStringLiteral("value"));
90 titleOption.setFlags(QCommandLineOption::HiddenFromHelp);
91 parser->addOption(titleOption);
92 }
93
getCustomCommand(QStringList & args)94 QStringList Application::getCustomCommand(QStringList &args)
95 {
96 int i = args.indexOf(QStringLiteral("-e"));
97 QStringList customCommand;
98 if ((0 < i) && (i < (args.size() - 1))) {
99 // -e was specified with at least one extra argument
100 // if -e was specified without arguments, QCommandLineParser will deal
101 // with that
102 args.removeAt(i);
103 while (args.size() > i) {
104 customCommand << args.takeAt(i);
105 }
106 }
107 return customCommand;
108 }
109
~Application()110 Application::~Application()
111 {
112 SessionManager::instance()->closeAllSessions();
113 }
114
newMainWindow()115 MainWindow *Application::newMainWindow()
116 {
117 WindowSystemInfo::HAVE_TRANSPARENCY = !m_parser->isSet(QStringLiteral("notransparency"));
118
119 auto window = new MainWindow();
120
121 connect(window, &Konsole::MainWindow::newWindowRequest, this, &Konsole::Application::createWindow);
122 connect(window, &Konsole::MainWindow::terminalsDetached, this, &Konsole::Application::detachTerminals);
123
124 m_pluginManager.registerMainWindow(window);
125
126 return window;
127 }
128
createWindow(const Profile::Ptr & profile,const QString & directory)129 void Application::createWindow(const Profile::Ptr &profile, const QString &directory)
130 {
131 MainWindow *window = newMainWindow();
132 window->createSession(profile, directory);
133 window->show();
134 }
135
detachTerminals(ViewSplitter * splitter,const QHash<TerminalDisplay *,Session * > & sessionsMap)136 void Application::detachTerminals(ViewSplitter *splitter, const QHash<TerminalDisplay *, Session *> &sessionsMap)
137 {
138 auto *currentWindow = qobject_cast<MainWindow *>(sender());
139 MainWindow *window = newMainWindow();
140 ViewManager *manager = window->viewManager();
141
142 const QList<TerminalDisplay *> displays = splitter->findChildren<TerminalDisplay *>();
143 for (TerminalDisplay *terminal : displays) {
144 manager->attachView(terminal, sessionsMap[terminal]);
145 }
146 manager->activeContainer()->addSplitter(splitter);
147
148 window->show();
149 window->resize(currentWindow->width(), currentWindow->height());
150 window->move(QCursor::pos());
151 }
152
newInstance()153 int Application::newInstance()
154 {
155 // handle session management
156
157 // returns from processWindowArgs(args, createdNewMainWindow)
158 // if a new window was created
159 bool createdNewMainWindow = false;
160
161 // check for arguments to print help or other information to the
162 // terminal, quit if such an argument was found
163 if (processHelpArgs()) {
164 return 0;
165 }
166
167 // create a new window or use an existing one
168 MainWindow *window = processWindowArgs(createdNewMainWindow);
169
170 if (m_parser->isSet(QStringLiteral("tabs-from-file"))) {
171 // create new session(s) as described in file
172 if (!processTabsFromFileArgs(window)) {
173 return 0;
174 }
175 }
176 // select profile to use
177 Profile::Ptr baseProfile = processProfileSelectArgs();
178
179 // process various command-line options which cause a property of the
180 // selected profile to be changed
181 Profile::Ptr newProfile = processProfileChangeArgs(baseProfile);
182
183 // if layout file is enable load it and create session from definitions,
184 // else create new session
185 if (m_parser->isSet(QStringLiteral("layout"))) {
186 window->viewManager()->loadLayout(m_parser->value(QStringLiteral("layout")));
187 } else {
188 Session *session = window->createSession(newProfile, QString());
189
190 const QString workingDir = m_parser->value(QStringLiteral("workdir"));
191 if (!workingDir.isEmpty()) {
192 session->setInitialWorkingDirectory(workingDir);
193 }
194
195 if (m_parser->isSet(QStringLiteral("noclose"))) {
196 session->setAutoClose(false);
197 }
198 }
199
200 // if the background-mode argument is supplied, start the background
201 // session ( or bring to the front if it already exists )
202 if (m_parser->isSet(QStringLiteral("background-mode"))) {
203 startBackgroundMode(window);
204 } else {
205 // Qt constrains top-level windows which have not been manually
206 // resized (via QWidget::resize()) to a maximum of 2/3rds of the
207 // screen size.
208 //
209 // This means that the terminal display might not get the width/
210 // height it asks for. To work around this, the widget must be
211 // manually resized to its sizeHint().
212 //
213 // This problem only affects the first time the application is run.
214 // run. After that KMainWindow will have manually resized the
215 // window to its saved size at this point (so the Qt::WA_Resized
216 // attribute will be set)
217
218 // If not restoring size from last time or only adding new tab,
219 // resize window to chosen profile size (see Bug:345403)
220 if (createdNewMainWindow) {
221 QTimer::singleShot(0, window, &MainWindow::show);
222 } else {
223 window->setWindowState(window->windowState() & (~Qt::WindowMinimized | Qt::WindowActive));
224 window->show();
225 window->activateWindow();
226 }
227 }
228
229 return 1;
230 }
231
232 /* Documentation for tab file:
233 *
234 * ;; is the token separator
235 * # at the beginning of line results in line being ignored.
236 * supported tokens: title, command, profile and workdir
237 *
238 * Note that the title is static and the tab will close when the
239 * command is complete (do not use --noclose). You can start new tabs.
240 *
241 * Example below will create 6 tabs as listed and a 7th default tab
242 title: This is the title;; command: ssh localhost
243 title: This is the title;; command: ssh localhost;; profile: Shell
244 title: Top this!;; command: top
245 title: mc this!;; command: mc;; workdir: /tmp
246 #this line is comment
247 command: ssh localhost
248 profile: Shell
249 */
processTabsFromFileArgs(MainWindow * window)250 bool Application::processTabsFromFileArgs(MainWindow *window)
251 {
252 // Open tab configuration file
253 const QString tabsFileName(m_parser->value(QStringLiteral("tabs-from-file")));
254 QFile tabsFile(tabsFileName);
255 if (!tabsFile.open(QFile::ReadOnly)) {
256 qWarning() << "ERROR: Cannot open tabs file " << tabsFileName.toLocal8Bit().data();
257 return false;
258 }
259
260 unsigned int sessions = 0;
261 while (!tabsFile.atEnd()) {
262 QString lineString(QString::fromUtf8(tabsFile.readLine()).trimmed());
263 if ((lineString.isEmpty()) || (lineString[0] == QLatin1Char('#'))) {
264 continue;
265 }
266
267 QHash<QString, QString> lineTokens;
268 QStringList lineParts = lineString.split(QStringLiteral(";;"), Qt::SkipEmptyParts);
269
270 for (int i = 0; i < lineParts.size(); ++i) {
271 QString key = lineParts.at(i).section(QLatin1Char(':'), 0, 0).trimmed().toLower();
272 QString value = lineParts.at(i).section(QLatin1Char(':'), 1, -1).trimmed();
273 lineTokens[key] = value;
274 }
275 // should contain at least one of 'command' and 'profile'
276 if (lineTokens.contains(QStringLiteral("command")) || lineTokens.contains(QStringLiteral("profile"))) {
277 createTabFromArgs(window, lineTokens);
278 sessions++;
279 } else {
280 qWarning() << "Each line should contain at least one of 'command' and 'profile'.";
281 }
282 }
283 tabsFile.close();
284
285 if (sessions < 1) {
286 qWarning() << "No valid lines found in " << tabsFileName.toLocal8Bit().data();
287 return false;
288 }
289
290 return true;
291 }
292
createTabFromArgs(MainWindow * window,const QHash<QString,QString> & tokens)293 void Application::createTabFromArgs(MainWindow *window, const QHash<QString, QString> &tokens)
294 {
295 const QString &title = tokens[QStringLiteral("title")];
296 const QString &command = tokens[QStringLiteral("command")];
297 const QString &profile = tokens[QStringLiteral("profile")];
298 const QColor &color = tokens[QStringLiteral("tabcolor")];
299
300 Profile::Ptr baseProfile;
301 if (!profile.isEmpty()) {
302 baseProfile = ProfileManager::instance()->loadProfile(profile);
303 }
304 if (!baseProfile) {
305 // fallback to default profile
306 baseProfile = ProfileManager::instance()->defaultProfile();
307 }
308
309 Profile::Ptr newProfile = Profile::Ptr(new Profile(baseProfile));
310 newProfile->setHidden(true);
311
312 // FIXME: the method of determining whether to use newProfile does not
313 // scale well when we support more fields in the future
314 bool shouldUseNewProfile = false;
315
316 if (!command.isEmpty()) {
317 newProfile->setProperty(Profile::Command, command);
318 newProfile->setProperty(Profile::Arguments, command.split(QLatin1Char(' ')));
319 shouldUseNewProfile = true;
320 }
321
322 if (!title.isEmpty()) {
323 newProfile->setProperty(Profile::LocalTabTitleFormat, title);
324 newProfile->setProperty(Profile::RemoteTabTitleFormat, title);
325 shouldUseNewProfile = true;
326 }
327
328 // For tab color support
329 if (color.isValid()) {
330 newProfile->setProperty(Profile::TabColor, color);
331 shouldUseNewProfile = true;
332 }
333
334 // Create the new session
335 Profile::Ptr theProfile = shouldUseNewProfile ? newProfile : baseProfile;
336 Session *session = window->createSession(theProfile, QString());
337
338 const QString wdirOptionName(QStringLiteral("workdir"));
339 auto it = tokens.constFind(wdirOptionName);
340 const QString workingDirectory = it != tokens.cend() ? it.value() : m_parser->value(wdirOptionName);
341
342 if (!workingDirectory.isEmpty()) {
343 session->setInitialWorkingDirectory(workingDirectory);
344 }
345
346 if (m_parser->isSet(QStringLiteral("noclose"))) {
347 session->setAutoClose(false);
348 }
349
350 if (!window->testAttribute(Qt::WA_Resized)) {
351 window->resize(window->sizeHint());
352 }
353
354 // FIXME: this ugly hack here is to make the session start running, so that
355 // its tab title is displayed as expected.
356 //
357 // This is another side effect of the commit fixing BKO 176902.
358 window->show();
359 window->hide();
360 }
361
362 // Creates a new Konsole window.
363 // If --new-tab is given, use existing window.
processWindowArgs(bool & createdNewMainWindow)364 MainWindow *Application::processWindowArgs(bool &createdNewMainWindow)
365 {
366 MainWindow *window = nullptr;
367
368 if (m_parser->isSet(QStringLiteral("new-tab"))) {
369 const QList<QWidget *> list = QApplication::topLevelWidgets();
370 for (auto it = list.crbegin(), endIt = list.crend(); it != endIt; ++it) {
371 window = qobject_cast<MainWindow *>(*it);
372 if (window) {
373 break;
374 }
375 }
376 }
377
378 if (window == nullptr) {
379 createdNewMainWindow = true;
380 window = newMainWindow();
381
382 // override default menubar visibility
383 if (m_parser->isSet(QStringLiteral("show-menubar"))) {
384 window->setMenuBarInitialVisibility(true);
385 }
386 if (m_parser->isSet(QStringLiteral("hide-menubar"))) {
387 window->setMenuBarInitialVisibility(false);
388 }
389 if (m_parser->isSet(QStringLiteral("fullscreen"))) {
390 window->viewFullScreen(true);
391 }
392 if (m_parser->isSet(QStringLiteral("show-tabbar"))) {
393 window->viewManager()->setNavigationVisibility(ViewManager::AlwaysShowNavigation);
394 } else if (m_parser->isSet(QStringLiteral("hide-tabbar"))) {
395 window->viewManager()->setNavigationVisibility(ViewManager::AlwaysHideNavigation);
396 }
397 }
398 return window;
399 }
400
401 // Loads a profile.
402 // If --profile <name> is given, loads profile <name>.
403 // If --fallback-profile is given, loads profile FALLBACK/.
404 // Else loads the default profile.
processProfileSelectArgs()405 Profile::Ptr Application::processProfileSelectArgs()
406 {
407 Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile();
408
409 if (m_parser->isSet(QStringLiteral("profile"))) {
410 Profile::Ptr profile = ProfileManager::instance()->loadProfile(m_parser->value(QStringLiteral("profile")));
411 if (profile) {
412 return profile;
413 }
414 } else if (m_parser->isSet(QStringLiteral("fallback-profile"))) {
415 Profile::Ptr profile = ProfileManager::instance()->loadProfile(QStringLiteral("FALLBACK/"));
416 if (profile) {
417 return profile;
418 }
419 }
420
421 return defaultProfile;
422 }
423
processHelpArgs()424 bool Application::processHelpArgs()
425 {
426 if (m_parser->isSet(QStringLiteral("list-profiles"))) {
427 listAvailableProfiles();
428 return true;
429 } else if (m_parser->isSet(QStringLiteral("list-profile-properties"))) {
430 listProfilePropertyInfo();
431 return true;
432 }
433 return false;
434 }
435
listAvailableProfiles()436 void Application::listAvailableProfiles()
437 {
438 const QStringList paths = ProfileManager::instance()->availableProfilePaths();
439
440 for (const QString &path : paths) {
441 QFileInfo info(path);
442 printf("%s\n", info.completeBaseName().toLocal8Bit().constData());
443 }
444 }
445
listProfilePropertyInfo()446 void Application::listProfilePropertyInfo()
447 {
448 Profile::Ptr tempProfile = ProfileManager::instance()->defaultProfile();
449 const QStringList names = tempProfile->propertiesInfoList();
450
451 for (const QString &name : names) {
452 printf("%s\n", name.toLocal8Bit().constData());
453 }
454 }
455
processProfileChangeArgs(Profile::Ptr baseProfile)456 Profile::Ptr Application::processProfileChangeArgs(Profile::Ptr baseProfile)
457 {
458 bool shouldUseNewProfile = false;
459
460 Profile::Ptr newProfile = Profile::Ptr(new Profile(baseProfile));
461 newProfile->setHidden(true);
462
463 // temporary changes to profile options specified on the command line
464 const QStringList profileProperties = m_parser->values(QStringLiteral("p"));
465 for (const QString &value : profileProperties) {
466 ProfileCommandParser parser;
467
468 QHashIterator<Profile::Property, QVariant> iter(parser.parse(value));
469 while (iter.hasNext()) {
470 iter.next();
471 newProfile->setProperty(iter.key(), iter.value());
472 }
473
474 shouldUseNewProfile = true;
475 }
476
477 // run a custom command
478 if (!m_customCommand.isEmpty()) {
479 // Example: konsole -e man ls
480 QString commandExec = m_customCommand[0];
481 QStringList commandArguments(m_customCommand);
482 if ((m_customCommand.size() == 1) && (QStandardPaths::findExecutable(commandExec).isEmpty())) {
483 // Example: konsole -e "man ls"
484 ShellCommand shellCommand(commandExec);
485 commandExec = shellCommand.command();
486 commandArguments = shellCommand.arguments();
487 }
488
489 if (commandExec.startsWith(QLatin1String("./"))) {
490 commandExec = QDir::currentPath() + commandExec.mid(1);
491 }
492
493 newProfile->setProperty(Profile::Command, commandExec);
494 newProfile->setProperty(Profile::Arguments, commandArguments);
495
496 shouldUseNewProfile = true;
497 }
498
499 if (shouldUseNewProfile) {
500 return newProfile;
501 }
502 return baseProfile;
503 }
504
startBackgroundMode(MainWindow * window)505 void Application::startBackgroundMode(MainWindow *window)
506 {
507 if (_backgroundInstance != nullptr) {
508 return;
509 }
510
511 KActionCollection *collection = window->actionCollection();
512 QAction *action = collection->addAction(QStringLiteral("toggle-background-window"));
513 action->setObjectName(QStringLiteral("Konsole Background Mode"));
514 action->setText(i18nc("@item", "Toggle Background Window"));
515 KGlobalAccel::self()->setGlobalShortcut(action, QKeySequence(Konsole::ACCEL | Qt::SHIFT | Qt::Key_F12));
516 connect(action, &QAction::triggered, this, &Application::toggleBackgroundInstance);
517
518 _backgroundInstance = window;
519 }
520
toggleBackgroundInstance()521 void Application::toggleBackgroundInstance()
522 {
523 Q_ASSERT(_backgroundInstance);
524
525 if (!_backgroundInstance->isVisible()) {
526 _backgroundInstance->show();
527 // ensure that the active terminal display has the focus. Without
528 // this, an odd problem occurred where the focus widget would change
529 // each time the background instance was shown
530 _backgroundInstance->setFocus();
531 } else {
532 _backgroundInstance->hide();
533 }
534 }
535
slotActivateRequested(QStringList args,const QString &)536 void Application::slotActivateRequested(QStringList args, const QString & /*workingDir*/)
537 {
538 // QCommandLineParser expects the first argument to be the executable name
539 // In the current version it just strips it away
540 args.prepend(qApp->applicationFilePath());
541
542 m_customCommand = getCustomCommand(args);
543
544 // We can't re-use QCommandLineParser instances, it preserves earlier parsed values
545 auto parser = new QCommandLineParser;
546 populateCommandLineParser(parser);
547 parser->parse(args);
548 m_parser.reset(parser);
549
550 newInstance();
551 }
552