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"