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