File indexing completed on 2024-04-21 05:51:21

0001 /*
0002     SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 // Own
0008 #include "Application.h"
0009 
0010 // Qt
0011 #include <QApplication>
0012 #include <QCommandLineParser>
0013 #include <QDir>
0014 #include <QFileInfo>
0015 #include <QHash>
0016 #include <QStandardPaths>
0017 #include <QTimer>
0018 
0019 // KDE
0020 #include <KActionCollection>
0021 #ifndef Q_OS_WIN
0022 #include <KGlobalAccel>
0023 #endif
0024 #include <KLocalizedString>
0025 
0026 // Konsole
0027 #include "KonsoleSettings.h"
0028 #include "MainWindow.h"
0029 #include "ShellCommand.h"
0030 #include "ViewManager.h"
0031 #include "WindowSystemInfo.h"
0032 #include "profile/ProfileCommandParser.h"
0033 #include "profile/ProfileManager.h"
0034 #include "session/Session.h"
0035 #include "session/SessionManager.h"
0036 #include "widgets/ViewContainer.h"
0037 
0038 #include "pluginsystem/IKonsolePlugin.h"
0039 
0040 using namespace Konsole;
0041 
0042 Application::Application(QSharedPointer<QCommandLineParser> parser, const QStringList &customCommand)
0043     : _backgroundInstance(nullptr)
0044     , m_parser(parser)
0045     , m_customCommand(customCommand)
0046 {
0047     m_pluginManager.loadAllPlugins();
0048 }
0049 
0050 void Application::populateCommandLineParser(QCommandLineParser *parser)
0051 {
0052     const auto options = QVector<QCommandLineOption>{
0053         {{QStringLiteral("profile")}, i18nc("@info:shell", "Name of profile to use for new Konsole instance"), QStringLiteral("name")},
0054         {{QStringLiteral("layout")}, i18nc("@info:shell", "json layoutfile to be loaded to use for new Konsole instance"), QStringLiteral("file")},
0055         {{QStringLiteral("builtin-profile")}, i18nc("@info:shell", "Use the built-in profile instead of the default profile")},
0056         {{QStringLiteral("workdir")}, i18nc("@info:shell", "Set the initial working directory of the new tab or window to 'dir'"), QStringLiteral("dir")},
0057         {{QStringLiteral("hold"), QStringLiteral("noclose")}, i18nc("@info:shell", "Do not close the initial session automatically when it ends.")},
0058         // BR: 373440
0059         {{QStringLiteral("new-tab")},
0060          i18nc("@info:shell",
0061                "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)")},
0062         {{QStringLiteral("tabs-from-file")}, i18nc("@info:shell", "Create tabs as specified in given tabs configuration file"), QStringLiteral("file")},
0063         {{QStringLiteral("background-mode")},
0064          i18nc("@info:shell", "Start Konsole in the background and bring to the front when Ctrl+Shift+F12 (by default) is pressed")},
0065         {{QStringLiteral("separate"), QStringLiteral("nofork")}, i18nc("@info:shell", "Run in a separate process")},
0066         {{QStringLiteral("show-menubar")}, i18nc("@info:shell", "Show the menubar, overriding the default setting")},
0067         {{QStringLiteral("hide-menubar")}, i18nc("@info:shell", "Hide the menubar, overriding the default setting")},
0068         {{QStringLiteral("show-tabbar")}, i18nc("@info:shell", "Show the tabbar, overriding the default setting")},
0069         {{QStringLiteral("hide-tabbar")}, i18nc("@info:shell", "Hide the tabbar, overriding the default setting")},
0070         {{QStringLiteral("fullscreen")}, i18nc("@info:shell", "Start Konsole in fullscreen mode")},
0071         {{QStringLiteral("notransparency")}, i18nc("@info:shell", "Disable transparent backgrounds, even if the system supports them.")},
0072         {{QStringLiteral("list-profiles")}, i18nc("@info:shell", "List the available profiles")},
0073         {{QStringLiteral("list-profile-properties")}, i18nc("@info:shell", "List all the profile properties names and their type (for use with -p)")},
0074         {{QStringLiteral("p")}, i18nc("@info:shell", "Change the value of a profile property."), QStringLiteral("property=value")},
0075         {{QStringLiteral("e")},
0076          i18nc("@info:shell", "Command to execute. This option will catch all following arguments, so use it as the last option."),
0077          QStringLiteral("cmd")},
0078         {{QStringLiteral("force-reuse")},
0079          i18nc("@info:shell", "Force re-using the existing instance even if it breaks functionality, e. g. --new-tab. Mostly for debugging.")},
0080     };
0081 
0082     for (const auto &option : options) {
0083         parser->addOption(option);
0084     }
0085 
0086     parser->addPositionalArgument(QStringLiteral("[args]"), i18nc("@info:shell", "Arguments passed to command"));
0087 
0088     // Add a no-op compatibility option to make Konsole compatible with
0089     // Debian's policy on X terminal emulators.
0090     // -T is technically meant to set a title, that is not really meaningful
0091     // for Konsole as we have multiple user-facing options controlling
0092     // the title and overriding whatever is set elsewhere.
0093     // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=532029
0094     // https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s11.8.3
0095     auto titleOption = QCommandLineOption({QStringLiteral("T")}, QStringLiteral("Debian policy compatibility, not used"), QStringLiteral("value"));
0096     titleOption.setFlags(QCommandLineOption::HiddenFromHelp);
0097     parser->addOption(titleOption);
0098 }
0099 
0100 QStringList Application::getCustomCommand(QStringList &args)
0101 {
0102     int i = args.indexOf(QStringLiteral("-e"));
0103     QStringList customCommand;
0104     if ((0 < i) && (i < (args.size() - 1))) {
0105         // -e was specified with at least one extra argument
0106         // if -e was specified without arguments, QCommandLineParser will deal
0107         // with that
0108         args.removeAt(i);
0109         while (args.size() > i) {
0110             customCommand << args.takeAt(i);
0111         }
0112     }
0113     return customCommand;
0114 }
0115 
0116 Application::~Application()
0117 {
0118     SessionManager::instance()->closeAllSessions();
0119 }
0120 
0121 MainWindow *Application::newMainWindow()
0122 {
0123     WindowSystemInfo::HAVE_TRANSPARENCY = !m_parser->isSet(QStringLiteral("notransparency"));
0124 
0125     auto window = new MainWindow();
0126 
0127     connect(window, &Konsole::MainWindow::newWindowRequest, this, &Konsole::Application::createWindow);
0128 
0129     connect(window,
0130             &Konsole::MainWindow::terminalsDetached,
0131             this,
0132             [this, window](ViewSplitter *splitter, const QHash<TerminalDisplay *, Session *> &sessionsMap) {
0133                 detachTerminals(window, splitter, sessionsMap);
0134             });
0135 
0136     m_pluginManager.registerMainWindow(window);
0137 
0138     return window;
0139 }
0140 
0141 void Application::createWindow(const Profile::Ptr &profile, const QString &directory)
0142 {
0143     MainWindow *window = newMainWindow();
0144     window->createSession(profile, directory);
0145     window->show();
0146 }
0147 
0148 void Application::detachTerminals(MainWindow *currentWindow, ViewSplitter *splitter, const QHash<TerminalDisplay *, Session *> &sessionsMap)
0149 {
0150     MainWindow *window = newMainWindow();
0151     ViewManager *manager = window->viewManager();
0152 
0153     const QList<TerminalDisplay *> displays = splitter->findChildren<TerminalDisplay *>();
0154     for (TerminalDisplay *terminal : displays) {
0155         manager->attachView(terminal, sessionsMap[terminal]);
0156     }
0157     manager->activeContainer()->addSplitter(splitter);
0158 
0159     window->show();
0160     window->resize(currentWindow->width(), currentWindow->height());
0161     window->move(QCursor::pos());
0162 }
0163 
0164 int Application::newInstance()
0165 {
0166     // handle session management
0167 
0168     // returns from processWindowArgs(args, createdNewMainWindow)
0169     // if a new window was created
0170     bool createdNewMainWindow = false;
0171 
0172     // check for arguments to print help or other information to the
0173     // terminal, quit if such an argument was found
0174     if (processHelpArgs()) {
0175         return 0;
0176     }
0177 
0178     // create a new window or use an existing one
0179     MainWindow *window = processWindowArgs(createdNewMainWindow);
0180 
0181     if (m_parser->isSet(QStringLiteral("tabs-from-file"))) {
0182         // create new session(s) as described in file
0183         if (!processTabsFromFileArgs(window)) {
0184             return 0;
0185         }
0186     }
0187     // select profile to use
0188     Profile::Ptr baseProfile = processProfileSelectArgs();
0189 
0190     // process various command-line options which cause a property of the
0191     // selected profile to be changed
0192     Profile::Ptr newProfile = processProfileChangeArgs(baseProfile);
0193 
0194     // if layout file is enable load it and create session from definitions,
0195     // else create new session
0196     if (m_parser->isSet(QStringLiteral("layout"))) {
0197         window->viewManager()->loadLayout(m_parser->value(QStringLiteral("layout")));
0198     } else {
0199         Session *session = window->createSession(newProfile, QString());
0200 
0201         const QString workingDir = m_parser->value(QStringLiteral("workdir"));
0202         if (!workingDir.isEmpty()) {
0203             session->setInitialWorkingDirectory(workingDir);
0204         }
0205 
0206         if (m_parser->isSet(QStringLiteral("noclose"))) {
0207             session->setAutoClose(false);
0208         }
0209     }
0210 
0211     // if the background-mode argument is supplied, start the background
0212     // session ( or bring to the front if it already exists )
0213     if (m_parser->isSet(QStringLiteral("background-mode"))) {
0214         startBackgroundMode(window);
0215     } else {
0216         // Qt constrains top-level windows which have not been manually
0217         // resized (via QWidget::resize()) to a maximum of 2/3rds of the
0218         //  screen size.
0219         //
0220         // This means that the terminal display might not get the width/
0221         // height it asks for.  To work around this, the widget must be
0222         // manually resized to its sizeHint().
0223         //
0224         // This problem only affects the first time the application is run.
0225         // run. After that KMainWindow will have manually resized the
0226         // window to its saved size at this point (so the Qt::WA_Resized
0227         // attribute will be set)
0228 
0229         // If not restoring size from last time or only adding new tab,
0230         // resize window to chosen profile size (see Bug:345403)
0231         if (createdNewMainWindow) {
0232             QTimer::singleShot(0, window, &MainWindow::show);
0233         } else {
0234             window->setWindowState(window->windowState() & (~Qt::WindowMinimized | Qt::WindowActive));
0235             window->show();
0236             window->activateWindow();
0237         }
0238     }
0239 
0240     return 1;
0241 }
0242 
0243 /* Documentation for tab file:
0244  *
0245  * ;; is the token separator
0246  * # at the beginning of line results in line being ignored.
0247  * supported tokens: title, command, profile and workdir
0248  *
0249  * Note that the title is static and the tab will close when the
0250  * command is complete (do not use --noclose).  You can start new tabs.
0251  *
0252  * Example below will create 6 tabs as listed and a 7th default tab
0253 title: This is the title;; command: ssh localhost
0254 title: This is the title;; command: ssh localhost;; profile: Shell
0255 title: Top this!;; command: top
0256 title: mc this!;; command: mc;; workdir: /tmp
0257 #this line is comment
0258 command: ssh localhost
0259 profile: Shell
0260 */
0261 bool Application::processTabsFromFileArgs(MainWindow *window)
0262 {
0263     // Open tab configuration file
0264     const QString tabsFileName(m_parser->value(QStringLiteral("tabs-from-file")));
0265     QFile tabsFile(tabsFileName);
0266     if (!tabsFile.open(QFile::ReadOnly)) {
0267         qWarning() << "ERROR: Cannot open tabs file " << tabsFileName.toLocal8Bit().data();
0268         return false;
0269     }
0270 
0271     unsigned int sessions = 0;
0272     while (!tabsFile.atEnd()) {
0273         QString lineString(QString::fromUtf8(tabsFile.readLine()).trimmed());
0274         if ((lineString.isEmpty()) || (lineString[0] == QLatin1Char('#'))) {
0275             continue;
0276         }
0277 
0278         QHash<QString, QString> lineTokens;
0279         QStringList lineParts = lineString.split(QStringLiteral(";;"), Qt::SkipEmptyParts);
0280 
0281         for (int i = 0; i < lineParts.size(); ++i) {
0282             QString key = lineParts.at(i).section(QLatin1Char(':'), 0, 0).trimmed().toLower();
0283             QString value = lineParts.at(i).section(QLatin1Char(':'), 1, -1).trimmed();
0284             lineTokens[key] = value;
0285         }
0286         // should contain at least one of 'command' and 'profile'
0287         if (lineTokens.contains(QStringLiteral("command")) || lineTokens.contains(QStringLiteral("profile"))) {
0288             createTabFromArgs(window, lineTokens);
0289             sessions++;
0290         } else {
0291             qWarning() << "Each line should contain at least one of 'command' and 'profile'.";
0292         }
0293     }
0294     tabsFile.close();
0295 
0296     if (sessions < 1) {
0297         qWarning() << "No valid lines found in " << tabsFileName.toLocal8Bit().data();
0298         return false;
0299     }
0300 
0301     return true;
0302 }
0303 
0304 void Application::createTabFromArgs(MainWindow *window, const QHash<QString, QString> &tokens)
0305 {
0306     const QString &title = tokens[QStringLiteral("title")];
0307     const QString &command = tokens[QStringLiteral("command")];
0308     const QString &profile = tokens[QStringLiteral("profile")];
0309     const QColor &color = tokens[QStringLiteral("tabcolor")];
0310 
0311     Profile::Ptr baseProfile;
0312     if (!profile.isEmpty()) {
0313         baseProfile = ProfileManager::instance()->loadProfile(profile);
0314     }
0315     if (!baseProfile) {
0316         // fallback to default profile
0317         baseProfile = ProfileManager::instance()->defaultProfile();
0318     }
0319 
0320     Profile::Ptr newProfile = Profile::Ptr(new Profile(baseProfile));
0321     newProfile->setHidden(true);
0322 
0323     // FIXME: the method of determining whether to use newProfile does not
0324     // scale well when we support more fields in the future
0325     bool shouldUseNewProfile = false;
0326 
0327     if (!command.isEmpty()) {
0328         newProfile->setProperty(Profile::Command, command);
0329         newProfile->setProperty(Profile::Arguments, command.split(QLatin1Char(' ')));
0330         shouldUseNewProfile = true;
0331     }
0332 
0333     if (!title.isEmpty()) {
0334         newProfile->setProperty(Profile::LocalTabTitleFormat, title);
0335         newProfile->setProperty(Profile::RemoteTabTitleFormat, title);
0336         shouldUseNewProfile = true;
0337     }
0338 
0339     // For tab color support
0340     if (color.isValid()) {
0341         newProfile->setProperty(Profile::TabColor, color);
0342         shouldUseNewProfile = true;
0343     }
0344 
0345     // Create the new session
0346     Profile::Ptr theProfile = shouldUseNewProfile ? newProfile : baseProfile;
0347     Session *session = window->createSession(theProfile, QString());
0348 
0349     const QString wdirOptionName(QStringLiteral("workdir"));
0350     auto it = tokens.constFind(wdirOptionName);
0351     const QString workingDirectory = it != tokens.cend() ? it.value() : m_parser->value(wdirOptionName);
0352 
0353     if (!workingDirectory.isEmpty()) {
0354         session->setInitialWorkingDirectory(workingDirectory);
0355     }
0356 
0357     if (m_parser->isSet(QStringLiteral("noclose"))) {
0358         session->setAutoClose(false);
0359     }
0360 
0361     if (!window->testAttribute(Qt::WA_Resized)) {
0362         window->resize(window->sizeHint());
0363     }
0364 
0365     // FIXME: this ugly hack here is to make the session start running, so that
0366     // its tab title is displayed as expected.
0367     //
0368     // This is another side effect of the commit fixing BKO 176902.
0369     window->show();
0370     window->hide();
0371 }
0372 
0373 // Creates a new Konsole window.
0374 // If --new-tab is given, use existing window.
0375 MainWindow *Application::processWindowArgs(bool &createdNewMainWindow)
0376 {
0377     MainWindow *window = nullptr;
0378 
0379     if (m_parser->isSet(QStringLiteral("new-tab"))) {
0380         const QList<QWidget *> list = QApplication::topLevelWidgets();
0381         for (auto it = list.crbegin(), endIt = list.crend(); it != endIt; ++it) {
0382             window = qobject_cast<MainWindow *>(*it);
0383             if (window) {
0384                 break;
0385             }
0386         }
0387     }
0388 
0389     if (window == nullptr) {
0390         createdNewMainWindow = true;
0391         window = newMainWindow();
0392 
0393         // override default menubar visibility
0394         if (m_parser->isSet(QStringLiteral("show-menubar"))) {
0395             window->setMenuBarInitialVisibility(true);
0396         }
0397         if (m_parser->isSet(QStringLiteral("hide-menubar"))) {
0398             window->setMenuBarInitialVisibility(false);
0399         }
0400         if (m_parser->isSet(QStringLiteral("fullscreen"))) {
0401             window->viewFullScreen(true);
0402         }
0403         if (m_parser->isSet(QStringLiteral("show-tabbar"))) {
0404             window->viewManager()->setNavigationVisibility(ViewManager::AlwaysShowNavigation);
0405         } else if (m_parser->isSet(QStringLiteral("hide-tabbar"))) {
0406             window->viewManager()->setNavigationVisibility(ViewManager::AlwaysHideNavigation);
0407         }
0408     }
0409     return window;
0410 }
0411 
0412 // Loads a profile.
0413 // If --profile <name> is given, loads profile <name>.
0414 // If --builtin-profile is given, loads built-in profile.
0415 // Else loads the default profile.
0416 Profile::Ptr Application::processProfileSelectArgs()
0417 {
0418     if (m_parser->isSet(QStringLiteral("profile"))) {
0419         Profile::Ptr profile = ProfileManager::instance()->loadProfile(m_parser->value(QStringLiteral("profile")));
0420         if (profile) {
0421             return profile;
0422         }
0423     } else if (m_parser->isSet(QStringLiteral("builtin-profile"))) {
0424         // no need to check twice: built-in and default profiles are always available
0425         return ProfileManager::instance()->builtinProfile();
0426     }
0427     return ProfileManager::instance()->defaultProfile();
0428 }
0429 
0430 bool Application::processHelpArgs()
0431 {
0432     if (m_parser->isSet(QStringLiteral("list-profiles"))) {
0433         listAvailableProfiles();
0434         return true;
0435     } else if (m_parser->isSet(QStringLiteral("list-profile-properties"))) {
0436         listProfilePropertyInfo();
0437         return true;
0438     }
0439     return false;
0440 }
0441 
0442 void Application::listAvailableProfiles()
0443 {
0444     const QStringList paths = ProfileManager::instance()->availableProfilePaths();
0445 
0446     for (const QString &path : paths) {
0447         QFileInfo info(path);
0448         printf("%s\n", info.completeBaseName().toLocal8Bit().constData());
0449     }
0450 }
0451 
0452 void Application::listProfilePropertyInfo()
0453 {
0454     const std::vector<std::string> &properties = Profile::propertiesInfoList();
0455 
0456     for (const auto &prop : properties) {
0457         printf("%s\n", prop.c_str());
0458     }
0459 }
0460 
0461 Profile::Ptr Application::processProfileChangeArgs(Profile::Ptr baseProfile)
0462 {
0463     bool shouldUseNewProfile = false;
0464 
0465     Profile::Ptr newProfile = Profile::Ptr(new Profile(baseProfile));
0466     newProfile->setHidden(true);
0467 
0468     // temporary changes to profile options specified on the command line
0469     const QStringList profileProperties = m_parser->values(QStringLiteral("p"));
0470     for (const QString &value : profileProperties) {
0471         ProfileCommandParser parser;
0472         newProfile->assignProperties(parser.parse(value));
0473         shouldUseNewProfile = true;
0474     }
0475 
0476     // run a custom command
0477     if (!m_customCommand.isEmpty()) {
0478         // Example: konsole -e man ls
0479         QString commandExec = m_customCommand[0];
0480         QStringList commandArguments(m_customCommand);
0481         if ((m_customCommand.size() == 1) && (QStandardPaths::findExecutable(commandExec).isEmpty())) {
0482             // Example: konsole -e "man ls"
0483             ShellCommand shellCommand(commandExec);
0484             commandExec = shellCommand.command();
0485             commandArguments = shellCommand.arguments();
0486         }
0487 
0488         if (commandExec.startsWith(QLatin1String("./"))) {
0489             commandExec = QDir::currentPath() + commandExec.mid(1);
0490         }
0491 
0492         newProfile->setProperty(Profile::Command, commandExec);
0493         newProfile->setProperty(Profile::Arguments, commandArguments);
0494 
0495         shouldUseNewProfile = true;
0496     }
0497 
0498     if (shouldUseNewProfile) {
0499         return newProfile;
0500     }
0501     return baseProfile;
0502 }
0503 
0504 void Application::startBackgroundMode(MainWindow *window)
0505 {
0506     if (_backgroundInstance != nullptr) {
0507         return;
0508     }
0509 
0510 #ifndef Q_OS_WIN
0511     KActionCollection *collection = window->actionCollection();
0512     QAction *action = collection->addAction(QStringLiteral("toggle-background-window"));
0513     action->setObjectName(QStringLiteral("Konsole Background Mode"));
0514     action->setText(i18nc("@item", "Toggle Background Window"));
0515     KGlobalAccel::self()->setGlobalShortcut(action, QKeySequence(Konsole::ACCEL | Qt::Key_F12));
0516     connect(action, &QAction::triggered, this, &Application::toggleBackgroundInstance);
0517 #endif
0518     _backgroundInstance = window;
0519 }
0520 
0521 void Application::toggleBackgroundInstance()
0522 {
0523     Q_ASSERT(_backgroundInstance);
0524 
0525     if (!_backgroundInstance->isVisible()) {
0526         _backgroundInstance->show();
0527         // ensure that the active terminal display has the focus. Without
0528         // this, an odd problem occurred where the focus widget would change
0529         // each time the background instance was shown
0530         _backgroundInstance->setFocus();
0531     } else {
0532         _backgroundInstance->hide();
0533     }
0534 }
0535 
0536 void Application::slotActivateRequested(QStringList args, const QString & /*workingDir*/)
0537 {
0538     // QCommandLineParser expects the first argument to be the executable name
0539     // In the current version it just strips it away
0540     args.prepend(qApp->applicationFilePath());
0541 
0542     m_customCommand = getCustomCommand(args);
0543 
0544     // We can't re-use QCommandLineParser instances, it preserves earlier parsed values
0545     auto parser = new QCommandLineParser;
0546     populateCommandLineParser(parser);
0547     parser->parse(args);
0548     m_parser.reset(parser);
0549 
0550     newInstance();
0551 }
0552 
0553 #include "moc_Application.cpp"