diff --git a/src/Session.cpp b/src/Session.cpp index 5e9cd062..92e37a48 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -1,1772 +1,1770 @@ /* This file is part of Konsole Copyright 2006-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "Session.h" // Standard #include #include #include // Qt #include #include #include #include #include #include // KDE #include #include #include #include #include #include // Konsole #include #include "ProcessInfo.h" #include "Pty.h" #include "TerminalDisplay.h" #include "ShellCommand.h" #include "Vt102Emulation.h" #include "ZModemDialog.h" #include "History.h" #include "konsoledebug.h" #include "SessionManager.h" #include "ProfileManager.h" #include "Profile.h" using namespace Konsole; int Session::lastSessionId = 0; static bool show_disallow_certain_dbus_methods_message = true; static const int ZMODEM_BUFFER_SIZE = 1048576; // 1 Mb Session::Session(QObject* parent) : QObject(parent) , _uniqueIdentifier(QUuid()) , _shellProcess(nullptr) , _emulation(nullptr) , _views(QList()) , _monitorActivity(false) , _monitorSilence(false) , _notifiedActivity(false) , _silenceSeconds(10) , _silenceTimer(nullptr) , _activityTimer(nullptr) , _autoClose(true) , _closePerUserRequest(false) , _nameTitle(QString()) , _displayTitle(QString()) , _userTitle(QString()) , _localTabTitleFormat(QString()) , _remoteTabTitleFormat(QString()) , _tabTitleSetByUser(false) , _iconName(QString()) , _iconText(QString()) , _addToUtmp(true) , _flowControlEnabled(true) , _program(QString()) , _arguments(QStringList()) , _environment(QStringList()) , _sessionId(0) , _initialWorkingDir(QString()) , _currentWorkingDir(QString()) , _reportedWorkingUrl(QUrl()) , _sessionProcessInfo(nullptr) , _foregroundProcessInfo(nullptr) , _foregroundPid(0) , _zmodemBusy(false) , _zmodemProc(nullptr) , _zmodemProgress(nullptr) , _hasDarkBackground(false) , _preferredSize(QSize()) , _readOnly(false) , _isPrimaryScreen(true) { _uniqueIdentifier = QUuid::createUuid(); //prepare DBus communication new SessionAdaptor(this); _sessionId = ++lastSessionId; QDBusConnection::sessionBus().registerObject(QLatin1String("/Sessions/") + QString::number(_sessionId), this); //create emulation backend _emulation = new Vt102Emulation(); connect(_emulation, &Konsole::Emulation::sessionAttributeChanged, this, &Konsole::Session::setSessionAttribute); connect(_emulation, &Konsole::Emulation::stateSet, this, &Konsole::Session::activityStateSet); connect(_emulation, &Konsole::Emulation::zmodemDownloadDetected, this, &Konsole::Session::fireZModemDownloadDetected); connect(_emulation, &Konsole::Emulation::zmodemUploadDetected, this, &Konsole::Session::fireZModemUploadDetected); connect(_emulation, &Konsole::Emulation::changeTabTextColorRequest, this, &Konsole::Session::changeTabTextColorRequest); connect(_emulation, &Konsole::Emulation::profileChangeCommandReceived, this, &Konsole::Session::profileChangeCommandReceived); connect(_emulation, &Konsole::Emulation::flowControlKeyPressed, this, &Konsole::Session::updateFlowControlState); connect(_emulation, &Konsole::Emulation::primaryScreenInUse, this, &Konsole::Session::onPrimaryScreenInUse); connect(_emulation, &Konsole::Emulation::selectionChanged, this, &Konsole::Session::selectionChanged); connect(_emulation, &Konsole::Emulation::imageResizeRequest, this, &Konsole::Session::resizeRequest); connect(_emulation, &Konsole::Emulation::sessionAttributeRequest, this, &Konsole::Session::sessionAttributeRequest); //create new teletype for I/O with shell process openTeletype(-1); //setup timer for monitoring session activity & silence _silenceTimer = new QTimer(this); _silenceTimer->setSingleShot(true); connect(_silenceTimer, &QTimer::timeout, this, &Konsole::Session::silenceTimerDone); _activityTimer = new QTimer(this); _activityTimer->setSingleShot(true); connect(_activityTimer, &QTimer::timeout, this, &Konsole::Session::activityTimerDone); - - connect(this, &Konsole::Session::tabRenamedByUser, this, &Konsole::Session::tabTitleSetByUser); } Session::~Session() { delete _foregroundProcessInfo; delete _sessionProcessInfo; delete _emulation; delete _shellProcess; delete _zmodemProc; } void Session::openTeletype(int fd) { if (isRunning()) { qWarning() << "Attempted to open teletype in a running session."; return; } delete _shellProcess; if (fd < 0) { _shellProcess = new Pty(); } else { _shellProcess = new Pty(fd); } _shellProcess->setUtf8Mode(_emulation->utf8()); // connect the I/O between emulator and pty process connect(_shellProcess, &Konsole::Pty::receivedData, this, &Konsole::Session::onReceiveBlock); connect(_emulation, &Konsole::Emulation::sendData, _shellProcess, &Konsole::Pty::sendData); // UTF8 mode connect(_emulation, &Konsole::Emulation::useUtf8Request, _shellProcess, &Konsole::Pty::setUtf8Mode); // get notified when the pty process is finished connect(_shellProcess, static_cast(&Konsole::Pty::finished), this, &Konsole::Session::done); // emulator size // Use a direct connection to ensure that the window size is set before it runs connect(_emulation, &Konsole::Emulation::imageSizeChanged, this, &Konsole::Session::updateWindowSize, Qt::DirectConnection); connect(_emulation, &Konsole::Emulation::imageSizeInitialized, this, &Konsole::Session::run); } WId Session::windowId() const { // Returns a window ID for this session which is used // to set the WINDOWID environment variable in the shell // process. // // Sessions can have multiple views or no views, which means // that a single ID is not always going to be accurate. // // If there are no views, the window ID is just 0. If // there are multiple views, then the window ID for the // top-level window which contains the first view is // returned if (_views.count() == 0) { return 0; } else { QWidget* window = _views.first(); Q_ASSERT(window); while (window->parentWidget() != nullptr) { window = window->parentWidget(); } return window->winId(); } } void Session::setDarkBackground(bool darkBackground) { _hasDarkBackground = darkBackground; } bool Session::isRunning() const { return (_shellProcess != nullptr) && (_shellProcess->state() == QProcess::Running); } void Session::setCodec(QTextCodec* codec) { if (isReadOnly()) { return; } emulation()->setCodec(codec); } bool Session::setCodec(const QByteArray& name) { QTextCodec* codec = QTextCodec::codecForName(name); if (codec != nullptr) { setCodec(codec); return true; } else { return false; } } QByteArray Session::codec() { return _emulation->codec()->name(); } void Session::setProgram(const QString& program) { _program = ShellCommand::expand(program); } void Session::setArguments(const QStringList& arguments) { _arguments = ShellCommand::expand(arguments); } void Session::setInitialWorkingDirectory(const QString& dir) { _initialWorkingDir = validDirectory(KShell::tildeExpand(ShellCommand::expand(dir))); } QString Session::currentWorkingDirectory() { if (_reportedWorkingUrl.isValid() && _reportedWorkingUrl.isLocalFile()) { return _reportedWorkingUrl.path(); } // only returned cached value if (_currentWorkingDir.isEmpty()) { updateWorkingDirectory(); } return _currentWorkingDir; } void Session::updateWorkingDirectory() { updateSessionProcessInfo(); const QString currentDir = _sessionProcessInfo->validCurrentDir(); if (currentDir != _currentWorkingDir) { _currentWorkingDir = currentDir; emit currentDirectoryChanged(_currentWorkingDir); } } QList Session::views() const { return _views; } void Session::addView(TerminalDisplay* widget) { Q_ASSERT(!_views.contains(widget)); _views.append(widget); // connect emulation - view signals and slots connect(widget, &Konsole::TerminalDisplay::keyPressedSignal, _emulation, &Konsole::Emulation::sendKeyEvent); connect(widget, &Konsole::TerminalDisplay::mouseSignal, _emulation, &Konsole::Emulation::sendMouseEvent); connect(widget, &Konsole::TerminalDisplay::sendStringToEmu, _emulation, &Konsole::Emulation::sendString); // allow emulation to notify the view when the foreground process // indicates whether or not it is interested in Mouse Tracking events connect(_emulation, &Konsole::Emulation::programRequestsMouseTracking, widget, &Konsole::TerminalDisplay::setUsesMouseTracking); widget->setUsesMouseTracking(_emulation->programUsesMouseTracking()); connect(_emulation, &Konsole::Emulation::enableAlternateScrolling, widget, &Konsole::TerminalDisplay::setAlternateScrolling); connect(_emulation, &Konsole::Emulation::programBracketedPasteModeChanged, widget, &Konsole::TerminalDisplay::setBracketedPasteMode); widget->setBracketedPasteMode(_emulation->programBracketedPasteMode()); widget->setScreenWindow(_emulation->createWindow()); //connect view signals and slots connect(widget, &Konsole::TerminalDisplay::changedContentSizeSignal, this, &Konsole::Session::onViewSizeChange); connect(widget, &Konsole::TerminalDisplay::destroyed, this, &Konsole::Session::viewDestroyed); connect(widget, &Konsole::TerminalDisplay::focusLost, _emulation, &Konsole::Emulation::focusLost); connect(widget, &Konsole::TerminalDisplay::focusGained, _emulation, &Konsole::Emulation::focusGained); connect(_emulation, &Konsole::Emulation::setCursorStyleRequest, widget, &Konsole::TerminalDisplay::setCursorStyle); connect(_emulation, &Konsole::Emulation::resetCursorStyleRequest, widget, &Konsole::TerminalDisplay::resetCursorStyle); } void Session::viewDestroyed(QObject* view) { TerminalDisplay* display = reinterpret_cast(view); Q_ASSERT(_views.contains(display)); removeView(display); } void Session::removeView(TerminalDisplay* widget) { _views.removeAll(widget); disconnect(widget, nullptr, this, nullptr); // disconnect // - key presses signals from widget // - mouse activity signals from widget // - string sending signals from widget // // ... and any other signals connected in addView() disconnect(widget, nullptr, _emulation, nullptr); // disconnect state change signals emitted by emulation disconnect(_emulation, nullptr, widget, nullptr); // close the session automatically when the last view is removed if (_views.count() == 0) { close(); } } // Upon a KPty error, there is no description on what that error was... // Check to see if the given program is executable. QString Session::checkProgram(const QString& program) { QString exec = program; if (exec.isEmpty()) { return QString(); } QFileInfo info(exec); if (info.isAbsolute() && info.exists() && info.isExecutable()) { return exec; } exec = KRun::binaryName(exec, false); exec = KShell::tildeExpand(exec); const QString pexec = QStandardPaths::findExecutable(exec); if (pexec.isEmpty()) { qCritical() << i18n("Could not find binary: ") << exec; return QString(); } return exec; } void Session::terminalWarning(const QString& message) { static const QByteArray warningText = i18nc("@info:shell Alert the user with red color text", "Warning: ").toLocal8Bit(); QByteArray messageText = message.toLocal8Bit(); static const char redPenOn[] = "\033[1m\033[31m"; static const char redPenOff[] = "\033[0m"; _emulation->receiveData(redPenOn, qstrlen(redPenOn)); _emulation->receiveData("\n\r\n\r", 4); _emulation->receiveData(warningText.constData(), qstrlen(warningText.constData())); _emulation->receiveData(messageText.constData(), qstrlen(messageText.constData())); _emulation->receiveData("\n\r\n\r", 4); _emulation->receiveData(redPenOff, qstrlen(redPenOff)); } QString Session::shellSessionId() const { QString friendlyUuid(_uniqueIdentifier.toString()); friendlyUuid.remove(QLatin1Char('-')).remove(QLatin1Char('{')).remove(QLatin1Char('}')); return friendlyUuid; } void Session::run() { // FIXME: run() is called twice in some instances if (isRunning()) { qCDebug(KonsoleDebug) << "Attempted to re-run an already running session (" << _shellProcess->pid() << ")"; return; } //check that everything is in place to run the session if (_program.isEmpty()) { qWarning() << "Program to run not set."; } if (_arguments.isEmpty()) { qWarning() << "No command line arguments specified."; } if (_uniqueIdentifier.isNull()) { _uniqueIdentifier = QUuid::createUuid(); } const int CHOICE_COUNT = 3; // if '_program' is empty , fall back to default shell. If that is not set // then fall back to /bin/sh QString programs[CHOICE_COUNT] = {_program, QString::fromUtf8(qgetenv("SHELL")), QStringLiteral("/bin/sh")}; QString exec; int choice = 0; while (choice < CHOICE_COUNT) { exec = checkProgram(programs[choice]); if (exec.isEmpty()) { choice++; } else { break; } } // if a program was specified via setProgram(), but it couldn't be found, print a warning if (choice != 0 && choice < CHOICE_COUNT && !_program.isEmpty()) { terminalWarning(i18n("Could not find '%1', starting '%2' instead. Please check your profile settings.", _program, exec)); // if none of the choices are available, print a warning } else if (choice == CHOICE_COUNT) { terminalWarning(i18n("Could not find an interactive shell to start.")); return; } // if no arguments are specified, fall back to program name QStringList arguments = _arguments.join(QLatin1Char(' ')).isEmpty() ? QStringList() << exec : _arguments; if (!_initialWorkingDir.isEmpty()) { _shellProcess->setInitialWorkingDirectory(_initialWorkingDir); } else { _shellProcess->setInitialWorkingDirectory(QDir::currentPath()); } _shellProcess->setFlowControlEnabled(_flowControlEnabled); _shellProcess->setEraseChar(_emulation->eraseChar()); _shellProcess->setUseUtmp(_addToUtmp); // this is not strictly accurate use of the COLORFGBG variable. This does not // tell the terminal exactly which colors are being used, but instead approximates // the color scheme as "black on white" or "white on black" depending on whether // the background color is deemed dark or not const QString backgroundColorHint = _hasDarkBackground ? QStringLiteral("COLORFGBG=15;0") : QStringLiteral("COLORFGBG=0;15"); addEnvironmentEntry(backgroundColorHint); addEnvironmentEntry(QStringLiteral("SHELL_SESSION_ID=%1").arg(shellSessionId())); addEnvironmentEntry(QStringLiteral("WINDOWID=%1").arg(QString::number(windowId()))); const QString dbusService = QDBusConnection::sessionBus().baseService(); addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_SERVICE=%1").arg(dbusService)); const QString dbusObject = QStringLiteral("/Sessions/%1").arg(QString::number(_sessionId)); addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_SESSION=%1").arg(dbusObject)); int result = _shellProcess->start(exec, arguments, _environment); if (result < 0) { terminalWarning(i18n("Could not start program '%1' with arguments '%2'.", exec, arguments.join(QLatin1String(" ")))); terminalWarning(_shellProcess->errorString()); return; } _shellProcess->setWriteable(false); // We are reachable via kwrited. emit started(); } void Session::setSessionAttribute(int what, const QString& caption) { // set to true if anything has actually changed // eg. old _nameTitle != new _nameTitle bool modified = false; if ((what == IconNameAndWindowTitle) || (what == WindowTitle)) { if (_userTitle != caption) { _userTitle = caption; modified = true; } } if ((what == IconNameAndWindowTitle) || (what == IconName)) { if (_iconText != caption) { _iconText = caption; modified = true; } } if (what == TextColor || what == BackgroundColor) { QString colorString = caption.section(QLatin1Char(';'), 0, 0); QColor color = QColor(colorString); if (color.isValid()) { if (what == TextColor) { emit changeForegroundColorRequest(color); } else { emit changeBackgroundColorRequest(color); } } } if (what == SessionName) { if (_localTabTitleFormat != caption) { _localTabTitleFormat = caption; setTitle(Session::DisplayedTitleRole, caption); modified = true; } } /* I don't believe this has ever worked in KDE 4.x if (what == 31) { QString cwd = caption; cwd = cwd.replace(QRegExp("^~"), QDir::homePath()); emit openUrlRequest(cwd); }*/ /* The below use of 32 works but appears to non-standard. It is from a commit from 2004 c20973eca8776f9b4f15bee5fdcb5a3205aa69de */ // change icon via \033]32;Icon\007 if (what == SessionIcon) { if (_iconName != caption) { _iconName = caption; modified = true; } } if (what == CurrentDirectory) { _reportedWorkingUrl = QUrl::fromUserInput(caption); emit currentDirectoryChanged(currentWorkingDirectory()); modified = true; } if (what == ProfileChange) { emit profileChangeCommandReceived(caption); return; } if (modified) { emit sessionAttributeChanged(); } } QString Session::userTitle() const { return _userTitle; } void Session::setTabTitleFormat(TabTitleContext context , const QString& format) { if (context == LocalTabTitle) { _localTabTitleFormat = format; ProcessInfo* process = getProcessInfo(); process->setUserNameRequired(format.contains(QLatin1String("%u"))); } else if (context == RemoteTabTitle) { _remoteTabTitleFormat = format; } } QString Session::tabTitleFormat(TabTitleContext context) const { if (context == LocalTabTitle) { return _localTabTitleFormat; } else if (context == RemoteTabTitle) { return _remoteTabTitleFormat; } return QString(); } void Session::tabTitleSetByUser(bool set) { _tabTitleSetByUser = set; } bool Session::isTabTitleSetByUser() const { return _tabTitleSetByUser; } void Session::silenceTimerDone() { //FIXME: The idea here is that the notification popup will appear to tell the user than output from //the terminal has stopped and the popup will disappear when the user activates the session. // //This breaks with the addition of multiple views of a session. The popup should disappear //when any of the views of the session becomes active //FIXME: Make message text for this notification and the activity notification more descriptive. if (!_monitorSilence) { emit stateChanged(NOTIFYNORMAL); return; } bool hasFocus = false; foreach(TerminalDisplay *display, _views) { if (display->hasFocus()) { hasFocus = true; break; } } KNotification::event(hasFocus ? QStringLiteral("Silence") : QStringLiteral("SilenceHidden"), i18n("Silence in session '%1'", _nameTitle), QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); emit stateChanged(NOTIFYSILENCE); } void Session::activityTimerDone() { _notifiedActivity = false; } void Session::updateFlowControlState(bool suspended) { if (suspended) { if (flowControlEnabled()) { foreach(TerminalDisplay * display, _views) { if (display->flowControlWarningEnabled()) { display->outputSuspended(true); } } } } else { foreach(TerminalDisplay * display, _views) { display->outputSuspended(false); } } } void Session::onPrimaryScreenInUse(bool use) { _isPrimaryScreen = use; emit primaryScreenInUse(use); } bool Session::isPrimaryScreen() { return _isPrimaryScreen; } void Session::sessionAttributeRequest(int id) { switch (id) { case BackgroundColor: // Get 'TerminalDisplay' (_view) background color emit getBackgroundColor(); break; } } void Session::activityStateSet(int state) { // TODO: should this hardcoded interval be user configurable? const int activityMaskInSeconds = 15; if (state == NOTIFYBELL) { emit bellRequest(i18n("Bell in session '%1'", _nameTitle)); } else if (state == NOTIFYACTIVITY) { // Don't notify if the terminal is active bool hasFocus = false; foreach(TerminalDisplay *display, _views) { if (display->hasFocus()) { hasFocus = true; break; } } if (_monitorActivity && !_notifiedActivity) { KNotification::event(hasFocus ? QStringLiteral("Activity") : QStringLiteral("ActivityHidden"), i18n("Activity in session '%1'", _nameTitle), QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); // mask activity notification for a while to avoid flooding _notifiedActivity = true; _activityTimer->start(activityMaskInSeconds * 1000); } // reset the counter for monitoring continuous silence since there is activity if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } } if (state == NOTIFYACTIVITY && !_monitorActivity) { state = NOTIFYNORMAL; } if (state == NOTIFYSILENCE && !_monitorSilence) { state = NOTIFYNORMAL; } emit stateChanged(state); } void Session::onViewSizeChange(int /*height*/, int /*width*/) { updateTerminalSize(); } void Session::updateTerminalSize() { int minLines = -1; int minColumns = -1; // minimum number of lines and columns that views require for // their size to be taken into consideration ( to avoid problems // with new view widgets which haven't yet been set to their correct size ) const int VIEW_LINES_THRESHOLD = 2; const int VIEW_COLUMNS_THRESHOLD = 2; //select largest number of lines and columns that will fit in all visible views foreach(TerminalDisplay* view, _views) { if (!view->isHidden() && view->lines() >= VIEW_LINES_THRESHOLD && view->columns() >= VIEW_COLUMNS_THRESHOLD) { minLines = (minLines == -1) ? view->lines() : qMin(minLines , view->lines()); minColumns = (minColumns == -1) ? view->columns() : qMin(minColumns , view->columns()); view->processFilters(); } } // backend emulation must have a _terminal of at least 1 column x 1 line in size if (minLines > 0 && minColumns > 0) { _emulation->setImageSize(minLines , minColumns); } } void Session::updateWindowSize(int lines, int columns) { Q_ASSERT(lines > 0 && columns > 0); _shellProcess->setWindowSize(columns, lines); } void Session::refresh() { // attempt to get the shell process to redraw the display // // this requires the program running in the shell // to cooperate by sending an update in response to // a window size change // // the window size is changed twice, first made slightly larger and then // resized back to its normal size so that there is actually a change // in the window size (some shells do nothing if the // new and old sizes are the same) // // if there is a more 'correct' way to do this, please // send an email with method or patches to konsole-devel@kde.org const QSize existingSize = _shellProcess->windowSize(); _shellProcess->setWindowSize(existingSize.width() + 1, existingSize.height()); // introduce small delay to avoid changing size too quickly QThread::usleep(500); _shellProcess->setWindowSize(existingSize.width(), existingSize.height()); } void Session::sendSignal(int signal) { const ProcessInfo* process = getProcessInfo(); bool ok = false; int pid; pid = process->foregroundPid(&ok); if (ok) { ::kill(pid, signal); } } void Session::reportBackgroundColor(const QColor& c) { #define to65k(a) (QStringLiteral("%1").arg(int(((a)*0xFFFF)), 4, 16, QLatin1Char('0'))) QString msg = QStringLiteral("\033]11;rgb:") + to65k(c.redF()) + QLatin1Char('/') + to65k(c.greenF()) + QLatin1Char('/') + to65k(c.blueF()) + QLatin1Char('\a'); _emulation->sendString(msg.toUtf8()); #undef to65k } bool Session::kill(int signal) { if (_shellProcess->pid() <= 0) { return false; } int result = ::kill(_shellProcess->pid(), signal); if (result == 0) { return _shellProcess->waitForFinished(1000); } else { return false; } } void Session::close() { if (isRunning()) { if (!closeInNormalWay()) { closeInForceWay(); } } else { // terminal process has finished, just close the session QTimer::singleShot(1, this, &Konsole::Session::finished); } } bool Session::closeInNormalWay() { _autoClose = true; _closePerUserRequest = true; // for the possible case where following events happen in sequence: // // 1). the terminal process crashes // 2). the tab stays open and displays warning message // 3). the user closes the tab explicitly // if (!isRunning()) { emit finished(); return true; } static QSet knownShells({QStringLiteral("ash"), QStringLiteral("bash"), QStringLiteral("csh"), QStringLiteral("dash"), QStringLiteral("fish"), QStringLiteral("hush"), QStringLiteral("ksh"), QStringLiteral("mksh"), QStringLiteral("pdksh"), QStringLiteral("tcsh"), QStringLiteral("zsh")}); // If only the session's shell is running, try sending an EOF for a clean exit if (!isForegroundProcessActive() && knownShells.contains(QFileInfo(_program).fileName())) { _shellProcess->sendEof(); if (_shellProcess->waitForFinished(1000)) { return true; } qWarning() << "shell did not close, sending SIGHUP"; } // We tried asking nicely, ask a bit less nicely if (kill(SIGHUP)) { return true; } else { qWarning() << "Process " << _shellProcess->pid() << " did not die with SIGHUP"; _shellProcess->closePty(); return (_shellProcess->waitForFinished(1000)); } } bool Session::closeInForceWay() { _autoClose = true; _closePerUserRequest = true; if (kill(SIGKILL)) { return true; } else { qWarning() << "Process " << _shellProcess->pid() << " did not die with SIGKILL"; return false; } } void Session::sendTextToTerminal(const QString& text, const QChar& eol) const { if (isReadOnly()) { return; } _emulation->sendText(text + eol); } // Only D-Bus calls this function (via SendText or runCommand) void Session::sendText(const QString& text) const { if (isReadOnly()) { return; } #if !defined(REMOVE_SENDTEXT_RUNCOMMAND_DBUS_METHODS) if (show_disallow_certain_dbus_methods_message) { KNotification::event(KNotification::Warning, QStringLiteral("Konsole D-Bus Warning"), i18n("The D-Bus methods sendText/runCommand were just used. There are security concerns about allowing these methods to be public. If desired, these methods can be changed to internal use only by re-compiling Konsole.

This warning will only show once for this Konsole instance.

")); show_disallow_certain_dbus_methods_message = false; } #endif _emulation->sendText(text); } // Only D-Bus calls this function void Session::runCommand(const QString& command) const { sendText(command + QLatin1Char('\n')); } void Session::sendMouseEvent(int buttons, int column, int line, int eventType) { _emulation->sendMouseEvent(buttons, column, line, eventType); } void Session::done(int exitCode, QProcess::ExitStatus exitStatus) { // This slot should be triggered only one time disconnect(_shellProcess, static_cast(&Konsole::Pty::finished), this, &Konsole::Session::done); if (!_autoClose) { _userTitle = i18nc("@info:shell This session is done", "Finished"); emit sessionAttributeChanged(); return; } if (_closePerUserRequest) { emit finished(); return; } QString message; if (exitCode != 0) { if (exitStatus != QProcess::NormalExit) { message = i18n("Program '%1' crashed.", _program); } else { message = i18n("Program '%1' exited with status %2.", _program, exitCode); } //FIXME: See comments in Session::silenceTimerDone() KNotification::event(QStringLiteral("Finished"), message , QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); } if (exitStatus != QProcess::NormalExit) { // this seeming duplicated line is for the case when exitCode is 0 message = i18n("Program '%1' crashed.", _program); terminalWarning(message); } else { emit finished(); } } Emulation* Session::emulation() const { return _emulation; } QString Session::keyBindings() const { return _emulation->keyBindings(); } QStringList Session::environment() const { return _environment; } void Session::setEnvironment(const QStringList& environment) { if (isReadOnly()) { return; } _environment = environment; } void Session::addEnvironmentEntry(const QString& entry) { _environment << entry; } int Session::sessionId() const { return _sessionId; } void Session::setKeyBindings(const QString& name) { _emulation->setKeyBindings(name); } void Session::setTitle(TitleRole role , const QString& newTitle) { if (title(role) != newTitle) { if (role == NameRole) { _nameTitle = newTitle; } else if (role == DisplayedTitleRole) { _displayTitle = newTitle; } emit sessionAttributeChanged(); } } QString Session::title(TitleRole role) const { if (role == NameRole) { return _nameTitle; } else if (role == DisplayedTitleRole) { return _displayTitle; } else { return QString(); } } ProcessInfo* Session::getProcessInfo() { ProcessInfo* process = nullptr; if (isForegroundProcessActive() && updateForegroundProcessInfo()) { process = _foregroundProcessInfo; } else { updateSessionProcessInfo(); process = _sessionProcessInfo; } return process; } void Session::updateSessionProcessInfo() { Q_ASSERT(_shellProcess); bool ok; // The checking for pid changing looks stupid, but it is needed // at the moment to workaround the problem that processId() might // return 0 if ((_sessionProcessInfo == nullptr) || (processId() != 0 && processId() != _sessionProcessInfo->pid(&ok))) { delete _sessionProcessInfo; _sessionProcessInfo = ProcessInfo::newInstance(processId(), tabTitleFormat(Session::LocalTabTitle)); _sessionProcessInfo->setUserHomeDir(); } _sessionProcessInfo->update(); } bool Session::updateForegroundProcessInfo() { Q_ASSERT(_shellProcess); const int foregroundPid = _shellProcess->foregroundProcessGroup(); if (foregroundPid != _foregroundPid) { delete _foregroundProcessInfo; _foregroundProcessInfo = ProcessInfo::newInstance(foregroundPid, tabTitleFormat(Session::LocalTabTitle)); _foregroundPid = foregroundPid; } if (_foregroundProcessInfo != nullptr) { _foregroundProcessInfo->update(); return _foregroundProcessInfo->isValid(); } else { return false; } } bool Session::isRemote() { ProcessInfo* process = getProcessInfo(); bool ok = false; return (process->name(&ok) == QLatin1String("ssh") && ok); } QString Session::getDynamicTitle() { ProcessInfo* process = getProcessInfo(); // format tab titles using process info bool ok = false; if (process->name(&ok) == QLatin1String("ssh") && ok) { SSHProcessInfo sshInfo(*process); return sshInfo.format(tabTitleFormat(Session::RemoteTabTitle)); } /* * Parses an input string, looking for markers beginning with a '%' * character and returns a string with the markers replaced * with information from this process description. *
* The markers recognized are: *
    *
  • %u - Name of the user which owns the process.
  • *
  • %n - Replaced with the name of the process.
  • *
  • %d - Replaced with the last part of the path name of the * process' current working directory. * * (eg. if the current directory is '/home/bob' then * 'bob' would be returned) *
  • *
  • %D - Replaced with the current working directory of the process.
  • *
*/ QString title = tabTitleFormat(Session::LocalTabTitle); // search for and replace known marker title.replace(QLatin1String("%u"), process->userName()); title.replace(QLatin1String("%h"), process->localHost()); title.replace(QLatin1String("%n"), process->name(&ok)); QString dir = _reportedWorkingUrl.toLocalFile(); if (dir.isEmpty()) { // update current directory from process updateWorkingDirectory(); dir = process->validCurrentDir(); } if (title.contains(QLatin1String("%D"))) { const QString homeDir = process->userHomeDir(); if (!homeDir.isEmpty()) { QString tempDir = dir; // Change User's Home Dir w/ ~ only at the beginning if (tempDir.startsWith(homeDir)) { tempDir.remove(0, homeDir.length()); tempDir.prepend(QLatin1Char('~')); } title.replace(QLatin1String("%D"), tempDir); } else { // Example: 'sudo top' We have to replace %D with something title.replace(QLatin1String("%D"), QStringLiteral("-")); } } title.replace(QLatin1String("%d"), process->formatShortDir(dir)); return title; } QUrl Session::getUrl() { if (_reportedWorkingUrl.isValid()) { return _reportedWorkingUrl; } QString path; updateSessionProcessInfo(); if (_sessionProcessInfo->isValid()) { bool ok = false; // check if foreground process is bookmark-able if (isForegroundProcessActive() && _foregroundProcessInfo->isValid()) { // for remote connections, save the user and host // bright ideas to get the directory at the other end are welcome :) if (_foregroundProcessInfo->name(&ok) == QLatin1String("ssh") && ok) { SSHProcessInfo sshInfo(*_foregroundProcessInfo); QUrl url; url.setScheme(QStringLiteral("ssh")); url.setUserName(sshInfo.userName()); url.setHost(sshInfo.host()); const QString port = sshInfo.port(); if (!port.isEmpty() && port != QLatin1String("22")) { url.setPort(port.toInt()); } return url; } else { path = _foregroundProcessInfo->currentDir(&ok); if (!ok) { path.clear(); } } } else { // otherwise use the current working directory of the shell process path = _sessionProcessInfo->currentDir(&ok); if (!ok) { path.clear(); } } } return QUrl::fromLocalFile(path); } void Session::setIconName(const QString& iconName) { if (iconName != _iconName) { _iconName = iconName; emit sessionAttributeChanged(); } } void Session::setIconText(const QString& iconText) { _iconText = iconText; } QString Session::iconName() const { return isReadOnly() ? QStringLiteral("object-locked") : _iconName; } QString Session::iconText() const { return _iconText; } void Session::setHistoryType(const HistoryType& hType) { _emulation->setHistory(hType); } const HistoryType& Session::historyType() const { return _emulation->history(); } void Session::clearHistory() { _emulation->clearHistory(); } QStringList Session::arguments() const { return _arguments; } QString Session::program() const { return _program; } bool Session::isMonitorActivity() const { return _monitorActivity; } bool Session::isMonitorSilence() const { return _monitorSilence; } void Session::setMonitorActivity(bool monitor) { if (_monitorActivity == monitor) { return; } _monitorActivity = monitor; _notifiedActivity = false; // This timer is meaningful only after activity has been notified _activityTimer->stop(); activityStateSet(NOTIFYNORMAL); } void Session::setMonitorSilence(bool monitor) { if (_monitorSilence == monitor) { return; } _monitorSilence = monitor; if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } else { _silenceTimer->stop(); } activityStateSet(NOTIFYNORMAL); } void Session::setMonitorSilenceSeconds(int seconds) { _silenceSeconds = seconds; if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } } void Session::setAddToUtmp(bool add) { _addToUtmp = add; } void Session::setAutoClose(bool close) { _autoClose = close; } bool Session::autoClose() const { return _autoClose; } void Session::setFlowControlEnabled(bool enabled) { if (isReadOnly()) { return; } _flowControlEnabled = enabled; if (_shellProcess != nullptr) { _shellProcess->setFlowControlEnabled(_flowControlEnabled); } emit flowControlEnabledChanged(enabled); } bool Session::flowControlEnabled() const { if (_shellProcess != nullptr) { return _shellProcess->flowControlEnabled(); } else { return _flowControlEnabled; } } void Session::fireZModemDownloadDetected() { if (!_zmodemBusy) { QTimer::singleShot(10, this, &Konsole::Session::zmodemDownloadDetected); _zmodemBusy = true; } } void Session::fireZModemUploadDetected() { if (!_zmodemBusy) { QTimer::singleShot(10, this, &Konsole::Session::zmodemUploadDetected); } } void Session::cancelZModem() { _shellProcess->sendData(QByteArrayLiteral("\030\030\030\030")); // Abort _zmodemBusy = false; } void Session::startZModem(const QString& zmodem, const QString& dir, const QStringList& list) { _zmodemBusy = true; _zmodemProc = new KProcess(); _zmodemProc->setOutputChannelMode(KProcess::SeparateChannels); *_zmodemProc << zmodem << QStringLiteral("-v") << QStringLiteral("-e") << list; if (!dir.isEmpty()) { _zmodemProc->setWorkingDirectory(dir); } connect(_zmodemProc, &KProcess::readyReadStandardOutput, this, &Konsole::Session::zmodemReadAndSendBlock); connect(_zmodemProc, &KProcess::readyReadStandardError, this, &Konsole::Session::zmodemReadStatus); connect(_zmodemProc, static_cast(&KProcess::finished), this, &Konsole::Session::zmodemFinished); _zmodemProc->start(); disconnect(_shellProcess, &Konsole::Pty::receivedData, this, &Konsole::Session::onReceiveBlock); connect(_shellProcess, &Konsole::Pty::receivedData, this, &Konsole::Session::zmodemReceiveBlock); _zmodemProgress = new ZModemDialog(QApplication::activeWindow(), false, i18n("ZModem Progress")); connect(_zmodemProgress, &Konsole::ZModemDialog::zmodemCancel, this, &Konsole::Session::zmodemFinished); _zmodemProgress->show(); } void Session::zmodemReadAndSendBlock() { _zmodemProc->setReadChannel(QProcess::StandardOutput); QByteArray data = _zmodemProc->read(ZMODEM_BUFFER_SIZE); while (data.count() != 0) { _shellProcess->sendData(data); data = _zmodemProc->read(ZMODEM_BUFFER_SIZE); } } void Session::zmodemReadStatus() { _zmodemProc->setReadChannel(QProcess::StandardError); QByteArray msg = _zmodemProc->readAll(); while (!msg.isEmpty()) { int i = msg.indexOf('\015'); int j = msg.indexOf('\012'); QByteArray txt; if ((i != -1) && ((j == -1) || (i < j))) { msg = msg.mid(i + 1); } else if (j != -1) { txt = msg.left(j); msg = msg.mid(j + 1); } else { txt = msg; msg.truncate(0); } if (!txt.isEmpty()) { _zmodemProgress->addText(QString::fromLocal8Bit(txt)); } } } void Session::zmodemReceiveBlock(const char* data, int len) { static int steps = 0; QByteArray bytes(data, len); _zmodemProc->write(bytes); // Provide some feedback to dialog if (steps > 100) { _zmodemProgress->addProgressText(QStringLiteral(".")); steps = 0; } steps++; } void Session::zmodemFinished() { /* zmodemFinished() is called by QProcess's finished() and ZModemDialog's user1Clicked(). Therefore, an invocation by user1Clicked() will recursively invoke this function again when the KProcess is deleted! */ if (_zmodemProc != nullptr) { KProcess* process = _zmodemProc; _zmodemProc = nullptr; // Set _zmodemProc to 0 avoid recursive invocations! _zmodemBusy = false; delete process; // Now, the KProcess may be disposed safely. disconnect(_shellProcess, &Konsole::Pty::receivedData, this , &Konsole::Session::zmodemReceiveBlock); connect(_shellProcess, &Konsole::Pty::receivedData, this, &Konsole::Session::onReceiveBlock); _shellProcess->sendData(QByteArrayLiteral("\030\030\030\030")); // Abort _shellProcess->sendData(QByteArrayLiteral("\001\013\n")); // Try to get prompt back _zmodemProgress->transferDone(); } } void Session::onReceiveBlock(const char* buf, int len) { _emulation->receiveData(buf, len); } QSize Session::size() { return _emulation->imageSize(); } void Session::setSize(const QSize& size) { if ((size.width() <= 1) || (size.height() <= 1)) { return; } emit resizeRequest(size); } QSize Session::preferredSize() const { return _preferredSize; } void Session::setPreferredSize(const QSize& size) { _preferredSize = size; } int Session::processId() const { return _shellProcess->pid(); } void Session::setTitle(int role , const QString& title) { switch (role) { case 0: setTitle(Session::NameRole, title); break; case 1: setTitle(Session::DisplayedTitleRole, title); // without these, that title will be overridden by the expansion of // title format shortly after, which will confuses users. _localTabTitleFormat = title; _remoteTabTitleFormat = title; break; } } QString Session::title(int role) const { switch (role) { case 0: return title(Session::NameRole); case 1: return title(Session::DisplayedTitleRole); default: return QString(); } } void Session::setTabTitleFormat(int context , const QString& format) { switch (context) { case 0: setTabTitleFormat(Session::LocalTabTitle, format); break; case 1: setTabTitleFormat(Session::RemoteTabTitle, format); break; } } QString Session::tabTitleFormat(int context) const { switch (context) { case 0: return tabTitleFormat(Session::LocalTabTitle); case 1: return tabTitleFormat(Session::RemoteTabTitle); default: return QString(); } } void Session::setHistorySize(int lines) { if (isReadOnly()) { return; } if (lines < 0) { setHistoryType(HistoryTypeFile()); } else if (lines == 0) { setHistoryType(HistoryTypeNone()); } else { setHistoryType(CompactHistoryType(lines)); } } int Session::historySize() const { const HistoryType& currentHistory = historyType(); if (currentHistory.isEnabled()) { if (currentHistory.isUnlimited()) { return -1; } else { return currentHistory.maximumLineCount(); } } else { return 0; } } void Session::setProfile(const QString &profileName) { const QList profiles = ProfileManager::instance()->allProfiles(); foreach (const Profile::Ptr &profile, profiles) { if (profile->name() == profileName) { SessionManager::instance()->setSessionProfile(this, profile); } } } int Session::foregroundProcessId() { int pid; bool ok = false; pid = getProcessInfo()->pid(&ok); if (!ok) { pid = -1; } return pid; } bool Session::isForegroundProcessActive() { // foreground process info is always updated after this return (_shellProcess->pid() != _shellProcess->foregroundProcessGroup()); } QString Session::foregroundProcessName() { QString name; if (updateForegroundProcessInfo()) { bool ok = false; name = _foregroundProcessInfo->name(&ok); if (!ok) { name.clear(); } } return name; } void Session::saveSession(KConfigGroup& group) { group.writePathEntry("WorkingDir", currentWorkingDirectory()); group.writeEntry("LocalTab", tabTitleFormat(LocalTabTitle)); group.writeEntry("RemoteTab", tabTitleFormat(RemoteTabTitle)); group.writeEntry("SessionGuid", _uniqueIdentifier.toString()); group.writeEntry("Encoding", QString::fromUtf8(codec())); } void Session::restoreSession(KConfigGroup& group) { QString value; value = group.readPathEntry("WorkingDir", QString()); if (!value.isEmpty()) { setInitialWorkingDirectory(value); } value = group.readEntry("LocalTab"); if (!value.isEmpty()) { setTabTitleFormat(LocalTabTitle, value); } value = group.readEntry("RemoteTab"); if (!value.isEmpty()) { setTabTitleFormat(RemoteTabTitle, value); } value = group.readEntry("SessionGuid"); if (!value.isEmpty()) { _uniqueIdentifier = QUuid(value); } value = group.readEntry("Encoding"); if (!value.isEmpty()) { setCodec(value.toUtf8()); } } QString Session::validDirectory(const QString& dir) const { QString validDir = dir; if (validDir.isEmpty()) { validDir = QDir::currentPath(); } const QFileInfo fi(validDir); if (!fi.exists() || !fi.isDir()) { validDir = QDir::homePath(); } return validDir; } bool Session::isReadOnly() const { return _readOnly; } void Session::setReadOnly(bool readOnly) { if (_readOnly != readOnly) { _readOnly = readOnly; // Needed to update the tab icons and all // attached views. emit readOnlyChanged(); } } SessionGroup::SessionGroup(QObject* parent) : QObject(parent), _masterMode(0) { } SessionGroup::~SessionGroup() = default; int SessionGroup::masterMode() const { return _masterMode; } QList SessionGroup::sessions() const { return _sessions.keys(); } bool SessionGroup::masterStatus(Session* session) const { return _sessions[session]; } void SessionGroup::addSession(Session* session) { connect(session, &Konsole::Session::finished, this, &Konsole::SessionGroup::sessionFinished); _sessions.insert(session, false); } void SessionGroup::removeSession(Session* session) { disconnect(session, &Konsole::Session::finished, this, &Konsole::SessionGroup::sessionFinished); setMasterStatus(session, false); _sessions.remove(session); } void SessionGroup::sessionFinished() { Session* session = qobject_cast(sender()); Q_ASSERT(session); removeSession(session); } void SessionGroup::setMasterMode(int mode) { _masterMode = mode; } QList SessionGroup::masters() const { return _sessions.keys(true); } void SessionGroup::setMasterStatus(Session* session , bool master) { const bool wasMaster = _sessions[session]; if (wasMaster == master) { // No status change -> nothing to do. return; } _sessions[session] = master; if (master) { connect(session->emulation(), &Konsole::Emulation::sendData, this, &Konsole::SessionGroup::forwardData); } else { disconnect(session->emulation(), &Konsole::Emulation::sendData, this, &Konsole::SessionGroup::forwardData); } } void SessionGroup::forwardData(const QByteArray& data) { static bool _inForwardData = false; if (_inForwardData) { // Avoid recursive calls among session groups! // A recursive call happens when a master in group A calls forwardData() // in group B. If one of the destination sessions in group B is also a // master of a group including the master session of group A, this would // again call forwardData() in group A, and so on. return; } _inForwardData = true; const QList sessionsKeys = _sessions.keys(); foreach(Session* other, sessionsKeys) { if (!_sessions[other]) { other->emulation()->sendString(data); } } _inForwardData = false; } diff --git a/src/Session.h b/src/Session.h index 024b0690..07f97f3d 100644 --- a/src/Session.h +++ b/src/Session.h @@ -1,904 +1,898 @@ /* This file is part of Konsole, an X terminal. Copyright 2007-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef SESSION_H #define SESSION_H // Qt #include #include #include #include #include #include #include // Konsole #include "konsoleprivate_export.h" #include "config-konsole.h" //krazy:exclude=includes #include "Shortcut_p.h" class QColor; class KConfigGroup; class KProcess; namespace Konsole { class Emulation; class Pty; class ProcessInfo; class TerminalDisplay; class ZModemDialog; class HistoryType; /** * Represents a terminal session consisting of a pseudo-teletype and a terminal emulation. * The pseudo-teletype (or PTY) handles I/O between the terminal process and Konsole. * The terminal emulation ( Emulation and subclasses ) processes the output stream from the * PTY and produces a character image which is then shown on views connected to the session. * * Each Session can be connected to one or more views by using the addView() method. * The attached views can then display output from the program running in the terminal * or send input to the program in the terminal in the form of keypresses and mouse * activity. */ class KONSOLEPRIVATE_EXPORT Session : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.konsole.Session") public: Q_PROPERTY(QString name READ nameTitle) Q_PROPERTY(int processId READ processId) Q_PROPERTY(QString keyBindings READ keyBindings WRITE setKeyBindings) Q_PROPERTY(QSize size READ size WRITE setSize) /** * Constructs a new session. * * To start the terminal process, call the run() method, * after specifying the program and arguments * using setProgram() and setArguments() * * If no program or arguments are specified explicitly, the Session * falls back to using the program specified in the SHELL environment * variable. */ explicit Session(QObject *parent = nullptr); ~Session() Q_DECL_OVERRIDE; /** * Connect to an existing terminal. When a new Session() is constructed it * automatically searches for and opens a new teletype. If you want to * use an existing teletype (given its file descriptor) call this after * constructing the session. * * Calling openTeletype() while a session is running has no effect. * * @param fd The file descriptor of the pseudo-teletype master (See KPtyProcess::KPtyProcess()) */ void openTeletype(int fd); /** * Returns true if the session is currently running. This will be true * after run() has been called successfully. */ bool isRunning() const; /** * Adds a new view for this session. * * The viewing widget will display the output from the terminal and * input from the viewing widget (key presses, mouse activity etc.) * will be sent to the terminal. * * Views can be removed using removeView(). The session is automatically * closed when the last view is removed. */ void addView(TerminalDisplay *widget); /** * Removes a view from this session. When the last view is removed, * the session will be closed automatically. * * @p widget will no longer display output from or send input * to the terminal */ void removeView(TerminalDisplay *widget); /** * Returns the views connected to this session */ QList views() const; /** * Returns the terminal emulation instance being used to encode / decode * characters to / from the process. */ Emulation *emulation() const; /** Returns the unique ID for this session. */ int sessionId() const; /** * This enum describes the contexts for which separate * tab title formats may be specified. */ enum TabTitleContext { /** Default tab title format */ LocalTabTitle, /** * Tab title format used session currently contains * a connection to a remote computer (via SSH) */ RemoteTabTitle }; /** * Returns true if the session currently contains a connection to a * remote computer. It currently supports ssh. */ bool isRemote(); /** * Sets the format used by this session for tab titles. * * @param context The context whose format should be set. * @param format The tab title format. This may be a mixture * of plain text and dynamic elements denoted by a '%' character * followed by a letter. (eg. %d for directory). The dynamic * elements available depend on the @p context */ void setTabTitleFormat(TabTitleContext context, const QString &format); /** Returns the format used by this session for tab titles. */ QString tabTitleFormat(TabTitleContext context) const; /** * Returns true if the tab title has been changed by the user via the * rename-tab dialog. */ bool isTabTitleSetByUser() const; /** Returns the arguments passed to the shell process when run() is called. */ QStringList arguments() const; /** Returns the program name of the shell process started when run() is called. */ QString program() const; /** * Sets the command line arguments which the session's program will be passed when * run() is called. */ void setArguments(const QStringList &arguments); /** Sets the program to be executed when run() is called. */ void setProgram(const QString &program); /** Returns the session's current working directory. */ QString initialWorkingDirectory() { return _initialWorkingDir; } /** * Sets the initial working directory for the session when it is run * This has no effect once the session has been started. */ void setInitialWorkingDirectory(const QString &dir); /** * Returns the current directory of the foreground process in the session */ QString currentWorkingDirectory(); /** * Sets the type of history store used by this session. * Lines of output produced by the terminal are added * to the history store. The type of history store * used affects the number of lines which can be * remembered before they are lost and the storage * (in memory, on-disk etc.) used. */ void setHistoryType(const HistoryType &hType); /** * Returns the type of history store used by this session. */ const HistoryType &historyType() const; /** * Clears the history store used by this session. */ void clearHistory(); /** * Sets the key bindings used by this session. The bindings * specify how input key sequences are translated into * the character stream which is sent to the terminal. * * @param name The name of the key bindings to use. The * names of available key bindings can be determined using the * KeyboardTranslatorManager class. */ void setKeyBindings(const QString &name); /** Returns the name of the key bindings used by this session. */ QString keyBindings() const; /** * This enum describes the available title roles. */ enum TitleRole { /** The name of the session. */ NameRole, /** The title of the session which is displayed in tabs etc. */ DisplayedTitleRole }; /** * Return the session title set by the user (ie. the program running * in the terminal), or an empty string if the user has not set a custom title */ QString userTitle() const; /** Convenience method used to read the name property. Returns title(Session::NameRole). */ QString nameTitle() const { return title(Session::NameRole); } /** Returns a title generated from tab format and process information. */ QString getDynamicTitle(); /** Sets the name of the icon associated with this session. */ void setIconName(const QString &iconName); /** Returns the name of the icon associated with this session. */ QString iconName() const; /** Return URL for the session. */ QUrl getUrl(); /** Sets the text of the icon associated with this session. */ void setIconText(const QString &iconText); /** Returns the text of the icon associated with this session. */ QString iconText() const; /** Sets the session's title for the specified @p role to @p title. */ void setTitle(TitleRole role, const QString &newTitle); /** Returns the session's title for the specified @p role. */ QString title(TitleRole role) const; /** * Specifies whether a utmp entry should be created for the pty used by this session. * If true, KPty::login() is called when the session is started. */ void setAddToUtmp(bool); /** * Specifies whether to close the session automatically when the terminal * process terminates. */ void setAutoClose(bool close); /** See setAutoClose() */ bool autoClose() const; /** Returns true if the user has started a program in the session. */ bool isForegroundProcessActive(); /** Returns the name of the current foreground process. */ QString foregroundProcessName(); /** Returns the terminal session's window size in lines and columns. */ QSize size(); /** * Emits a request to resize the session to accommodate * the specified window size. * * @param size The size in lines and columns to request. */ void setSize(const QSize &size); QSize preferredSize() const; void setPreferredSize(const QSize &size); /** * Sets whether the session has a dark background or not. The session * uses this information to set the COLORFGBG variable in the process's * environment, which allows the programs running in the terminal to determine * whether the background is light or dark and use appropriate colors by default. * * This has no effect once the session is running. */ void setDarkBackground(bool darkBackground); /** * Attempts to get the shell program to redraw the current display area. * This can be used after clearing the screen, for example, to get the * shell to redraw the prompt line. */ void refresh(); void startZModem(const QString &zmodem, const QString &dir, const QStringList &list); void cancelZModem(); bool isZModemBusy() { return _zmodemBusy; } void setZModemBusy(bool busy) { _zmodemBusy = busy; } /** * Possible values of the @p what parameter for setSessionAttribute(). * See the "Operating System Commands" section at: * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands */ enum SessionAttributes { IconNameAndWindowTitle = 0, IconName = 1, WindowTitle = 2, CurrentDirectory = 7, // From VTE (supposedly 6 was for dir, 7 for file, but whatever) TextColor = 10, BackgroundColor = 11, SessionName = 30, // Non-standard SessionIcon = 32, // Non-standard ProfileChange = 50 // this clashes with Xterm's font change command }; // Sets the text codec used by this sessions terminal emulation. void setCodec(QTextCodec *codec); // session management void saveSession(KConfigGroup &group); void restoreSession(KConfigGroup &group); void sendSignal(int signal); void reportBackgroundColor(const QColor &c); bool isReadOnly() const; void setReadOnly(bool readOnly); // Returns true if the current screen is the secondary/alternate one // or false if it's the primary/normal buffer bool isPrimaryScreen(); + void tabTitleSetByUser(bool set); public Q_SLOTS: /** * Starts the terminal session. * * This creates the terminal process and connects the teletype to it. */ void run(); /** * Returns the environment of this session as a list of strings like * VARIABLE=VALUE */ Q_SCRIPTABLE QStringList environment() const; /** * Sets the environment for this session. * @p environment should be a list of strings like * VARIABLE=VALUE */ Q_SCRIPTABLE void setEnvironment(const QStringList &environment); /** * Adds one entry for the environment of this session * @p entry should be like VARIABLE=VALUE */ void addEnvironmentEntry(const QString &entry); /** * Closes the terminal session. It kills the terminal process by calling * closeInNormalWay() and, optionally, closeInForceWay(). */ //Q_SCRIPTABLE void close(); // This cause the menu issues bko 185466 void close(); /** * Kill the terminal process in normal way. This sends a hangup signal * (SIGHUP) to the terminal process and causes the finished() signal to * be emitted. If the process does not respond to the SIGHUP signal then * the terminal connection (the pty) is closed and Konsole waits for the * process to exit. This method works most of the time, but fails with some * programs which respond to SIGHUP signal in special way, such as autossh * and irssi. */ bool closeInNormalWay(); /** * kill terminal process in force way. This send a SIGKILL signal to the * terminal process. It should be called only after closeInNormalWay() has * failed. Take it as last resort. */ bool closeInForceWay(); /** * Changes one of certain session attributes in the terminal emulation * display. For a list of what may be changed see the * Emulation::sessionAttributeChanged() signal. * * @param what The session attribute being changed, it is one of the * SessionAttributes enum * @param caption The text part of the terminal command */ void setSessionAttribute(int what, const QString &caption); /** * Enables monitoring for activity in the session. * This will cause notifySessionState() to be emitted * with the NOTIFYACTIVITY state flag when output is * received from the terminal. */ Q_SCRIPTABLE void setMonitorActivity(bool); /** Returns true if monitoring for activity is enabled. */ Q_SCRIPTABLE bool isMonitorActivity() const; /** * Enables monitoring for silence in the session. * This will cause notifySessionState() to be emitted * with the NOTIFYSILENCE state flag when output is not * received from the terminal for a certain period of * time, specified with setMonitorSilenceSeconds() */ Q_SCRIPTABLE void setMonitorSilence(bool); /** * Returns true if monitoring for inactivity (silence) * in the session is enabled. */ Q_SCRIPTABLE bool isMonitorSilence() const; /** See setMonitorSilence() */ Q_SCRIPTABLE void setMonitorSilenceSeconds(int seconds); /** * Sets whether flow control is enabled for this terminal * session. */ Q_SCRIPTABLE void setFlowControlEnabled(bool enabled); /** Returns whether flow control is enabled for this terminal session. */ Q_SCRIPTABLE bool flowControlEnabled() const; /** * @param text to send to the current foreground terminal program. * @param eol send this after @p text */ void sendTextToTerminal(const QString &text, const QChar &eol = QChar()) const; #if defined(REMOVE_SENDTEXT_RUNCOMMAND_DBUS_METHODS) void sendText(const QString &text) const; #else Q_SCRIPTABLE void sendText(const QString &text) const; #endif /** * Sends @p command to the current foreground terminal program. */ #if defined(REMOVE_SENDTEXT_RUNCOMMAND_DBUS_METHODS) void runCommand(const QString &command) const; #else Q_SCRIPTABLE void runCommand(const QString &command) const; #endif /** * Sends a mouse event of type @p eventType emitted by button * @p buttons on @p column/@p line to the current foreground * terminal program */ Q_SCRIPTABLE void sendMouseEvent(int buttons, int column, int line, int eventType); /** * Returns the process id of the terminal process. * This is the id used by the system API to refer to the process. */ Q_SCRIPTABLE int processId() const; /** * Returns the process id of the terminal's foreground process. * This is initially the same as processId() but can change * as the user starts other programs inside the terminal. */ Q_SCRIPTABLE int foregroundProcessId(); /** Sets the text codec used by this sessions terminal emulation. * Overloaded to accept a QByteArray for convenience since DBus * does not accept QTextCodec directly. */ Q_SCRIPTABLE bool setCodec(const QByteArray &name); /** Returns the codec used to decode incoming characters in this * terminal emulation */ Q_SCRIPTABLE QByteArray codec(); /** Sets the session's title for the specified @p role to @p title. * This is an overloaded member function for setTitle(TitleRole, QString) * provided for convenience since enum data types may not be * exported directly through DBus */ Q_SCRIPTABLE void setTitle(int role, const QString &title); /** Returns the session's title for the specified @p role. * This is an overloaded member function for setTitle(TitleRole) * provided for convenience since enum data types may not be * exported directly through DBus */ Q_SCRIPTABLE QString title(int role) const; /** Returns the "friendly" version of the QUuid of this session. * This is a QUuid with the braces and dashes removed, so it cannot be * used to construct a new QUuid. The same text appears in the * SHELL_SESSION_ID environment variable. */ Q_SCRIPTABLE QString shellSessionId() const; /** Sets the session's tab title format for the specified @p context to @p format. * This is an overloaded member function for setTabTitleFormat(TabTitleContext, QString) * provided for convenience since enum data types may not be * exported directly through DBus */ Q_SCRIPTABLE void setTabTitleFormat(int context, const QString &format); /** Returns the session's tab title format for the specified @p context. * This is an overloaded member function for tabTitleFormat(TitleRole) * provided for convenience since enum data types may not be * exported directly through DBus */ Q_SCRIPTABLE QString tabTitleFormat(int context) const; /** * Sets the history capacity of this session. * * @param lines The history capacity in unit of lines. Its value can be: *
    *
  • positive integer - fixed size history
  • *
  • 0 - no history
  • *
  • negative integer - unlimited history
  • *
*/ Q_SCRIPTABLE void setHistorySize(int lines); /** * Returns the history capacity of this session. */ Q_SCRIPTABLE int historySize() const; /** * Sets the current session's profile */ Q_SCRIPTABLE void setProfile(const QString &profile); Q_SIGNALS: /** Emitted when the terminal process starts. */ void started(); /** * Emitted when the terminal process exits. */ void finished(); /** * Emitted when one of certain session attributes has been changed. * See setSessionAttribute(). */ void sessionAttributeChanged(); /** Emitted when the session gets locked / unlocked. */ void readOnlyChanged(); /** * Emitted when the activity state of this session changes. * * @param state The new state of the session. This may be one * of NOTIFYNORMAL, NOTIFYSILENCE or NOTIFYACTIVITY */ void stateChanged(int state); /** * Emitted when the current working directory of this session changes. * * @param dir The new current working directory of the session. */ void currentDirectoryChanged(const QString &dir); /** Emitted when a bell event occurs in the session. */ void bellRequest(const QString &message); /** * Requests that the color the text for any tabs associated with * this session should be changed; * * TODO: Document what the parameter does */ void changeTabTextColorRequest(int); /** * Requests that the background color of views on this session * should be changed. */ void changeBackgroundColorRequest(const QColor &); /** * Requests that the text color of views on this session should * be changed to @p color. */ void changeForegroundColorRequest(const QColor &); /** TODO: Document me. */ void openUrlRequest(const QString &url); /** * Emitted when the request for data transmission through ZModem * protocol is detected. */ void zmodemDownloadDetected(); void zmodemUploadDetected(); /** * Emitted when the terminal process requests a change * in the size of the terminal window. * * @param size The requested window size in terms of lines and columns. */ void resizeRequest(const QSize &size); /** * Emitted when a profile change command is received from the terminal. * * @param text The text of the command. This is a string of the form * "PropertyName=Value;PropertyName=Value ..." */ void profileChangeCommandReceived(const QString &text); /** * Emitted when the flow control state changes. * * @param enabled True if flow control is enabled or false otherwise. */ void flowControlEnabledChanged(bool enabled); /** * Emitted when the active screen is switched, to indicate whether the primary * screen is in use. * * This signal serves as a relayer of Emulation::priamyScreenInUse(bool), * making it usable for higher level component. */ void primaryScreenInUse(bool use); /** * Emitted when the text selection is changed. * * This signal serves as a relayer of Emulation::selectedText(QString), * making it usable for higher level component. */ void selectionChanged(const QString &text); /** * Emitted when background request ("\033]11;?\a") terminal code received. * Terminal is expected send "\033]11;rgb:RRRR/GGGG/BBBB\a" response. * * Originally implemented to support vim's background detection feature * (without explictly setting 'bg=dark' within local/remote vimrc) */ void getBackgroundColor(); - /** - * Relays the tabRenamedByUser signal from SessionController - */ - void tabRenamedByUser(bool renamed) const; - private Q_SLOTS: void done(int, QProcess::ExitStatus); void fireZModemDownloadDetected(); void fireZModemUploadDetected(); void onReceiveBlock(const char *buf, int len); void silenceTimerDone(); void activityTimerDone(); void onViewSizeChange(int height, int width); void activityStateSet(int); //automatically detach views from sessions when view is destroyed void viewDestroyed(QObject *view); void zmodemReadStatus(); void zmodemReadAndSendBlock(); void zmodemReceiveBlock(const char *data, int len); void zmodemFinished(); void updateFlowControlState(bool suspended); void updateWindowSize(int lines, int columns); // Relays the signal from Emulation and sets _isPrimaryScreen void onPrimaryScreenInUse(bool use); void sessionAttributeRequest(int id); - void tabTitleSetByUser(bool set); - private: Q_DISABLE_COPY(Session) // checks that the binary 'program' is available and can be executed // returns the binary name if available or an empty string otherwise static QString checkProgram(const QString &program); void updateTerminalSize(); WId windowId() const; bool kill(int signal); // print a warning message in the terminal. This is used // if the program fails to start, or if the shell exits in // an unsuccessful manner void terminalWarning(const QString &message); ProcessInfo *getProcessInfo(); void updateSessionProcessInfo(); bool updateForegroundProcessInfo(); void updateWorkingDirectory(); QString validDirectory(const QString &dir) const; QUuid _uniqueIdentifier; // SHELL_SESSION_ID Pty *_shellProcess; Emulation *_emulation; QList _views; // monitor activity & silence bool _monitorActivity; bool _monitorSilence; bool _notifiedActivity; int _silenceSeconds; QTimer *_silenceTimer; QTimer *_activityTimer; bool _autoClose; bool _closePerUserRequest; QString _nameTitle; QString _displayTitle; QString _userTitle; QString _localTabTitleFormat; QString _remoteTabTitleFormat; bool _tabTitleSetByUser; QString _iconName; QString _iconText; // not actually used bool _addToUtmp; bool _flowControlEnabled; QString _program; QStringList _arguments; QStringList _environment; int _sessionId; QString _initialWorkingDir; QString _currentWorkingDir; QUrl _reportedWorkingUrl; ProcessInfo *_sessionProcessInfo; ProcessInfo *_foregroundProcessInfo; int _foregroundPid; // ZModem bool _zmodemBusy; KProcess *_zmodemProc; ZModemDialog *_zmodemProgress; bool _hasDarkBackground; QSize _preferredSize; bool _readOnly; static int lastSessionId; bool _isPrimaryScreen; }; /** * Provides a group of sessions which is divided into master and slave sessions. * Activity in master sessions can be propagated to all sessions within the group. * The type of activity which is propagated and method of propagation is controlled * by the masterMode() flags. */ class SessionGroup : public QObject { Q_OBJECT public: /** Constructs an empty session group. */ explicit SessionGroup(QObject *parent); /** Destroys the session group and removes all connections between master and slave sessions. */ ~SessionGroup() Q_DECL_OVERRIDE; /** Adds a session to the group. */ void addSession(Session *session); /** Removes a session from the group. */ void removeSession(Session *session); /** Returns the list of sessions currently in the group. */ QList sessions() const; /** * Sets whether a particular session is a master within the group. * Changes or activity in the group's master sessions may be propagated * to all the sessions in the group, depending on the current masterMode() * * @param session The session whose master status should be changed. * @param master True to make this session a master or false otherwise */ void setMasterStatus(Session *session, bool master); /** Returns the master status of a session. See setMasterStatus() */ bool masterStatus(Session *session) const; /** * This enum describes the options for propagating certain activity or * changes in the group's master sessions to all sessions in the group. */ enum MasterMode { /** * Any input key presses in the master sessions are sent to all * sessions in the group. */ CopyInputToAll = 1 }; /** * Specifies which activity in the group's master sessions is propagated * to all sessions in the group. * * @param mode A bitwise OR of MasterMode flags. */ void setMasterMode(int mode); /** * Returns a bitwise OR of the active MasterMode flags for this group. * See setMasterMode() */ int masterMode() const; private Q_SLOTS: void sessionFinished(); void forwardData(const QByteArray &data); private: QList masters() const; // maps sessions to their master status QHash _sessions; int _masterMode; }; } #endif diff --git a/src/SessionController.cpp b/src/SessionController.cpp index b0bb53bf..5cff9c4a 100644 --- a/src/SessionController.cpp +++ b/src/SessionController.cpp @@ -1,2118 +1,2118 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "SessionController.h" #include "ProfileManager.h" #include "konsoledebug.h" // Qt #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Konsole #include "EditProfileDialog.h" #include "CopyInputDialog.h" #include "Emulation.h" #include "Filter.h" #include "History.h" #include "HistorySizeDialog.h" #include "IncrementalSearchBar.h" #include "RenameTabDialog.h" #include "ScreenWindow.h" #include "Session.h" #include "ProfileList.h" #include "TerminalDisplay.h" #include "SessionManager.h" #include "Enumeration.h" #include "PrintOptions.h" // for SaveHistoryTask #include #include #include "TerminalCharacterDecoder.h" // For Unix signal names #include using namespace Konsole; // TODO - Replace the icon choices below when suitable icons for silence and // activity are available Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _activityIcon, (QIcon::fromTheme(QLatin1String("dialog-information")))) Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _silenceIcon, (QIcon::fromTheme(QLatin1String("dialog-information")))) Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _broadcastIcon, (QIcon::fromTheme(QLatin1String("emblem-important")))) QSet SessionController::_allControllers; int SessionController::_lastControllerId; SessionController::SessionController(Session* session , TerminalDisplay* view, QObject* parent) : ViewProperties(parent) , KXMLGUIClient() , _session(session) , _view(view) , _copyToGroup(nullptr) , _profileList(nullptr) , _sessionIcon(QIcon()) , _sessionIconName(QString()) , _previousState(-1) , _searchFilter(nullptr) , _urlFilter(nullptr) , _fileFilter(nullptr) , _copyInputToAllTabsAction(nullptr) , _findAction(nullptr) , _findNextAction(nullptr) , _findPreviousAction(nullptr) , _interactionTimer(nullptr) , _searchStartLine(0) , _prevSearchResultLine(0) , _codecAction(nullptr) , _switchProfileMenu(nullptr) , _webSearchMenu(nullptr) , _listenForScreenWindowUpdates(false) , _preventClose(false) , _keepIconUntilInteraction(false) , _selectedText(QString()) , _showMenuAction(nullptr) , _bookmarkValidProgramsToClear(QStringList()) , _isSearchBarEnabled(false) , _editProfileDialog(nullptr) , _searchBar(view->searchBar()) { Q_ASSERT(session); Q_ASSERT(view); // handle user interface related to session (menus etc.) if (isKonsolePart()) { setComponentName(QStringLiteral("konsole"), i18n("Konsole")); setXMLFile(QStringLiteral("partui.rc")); setupCommonActions(); } else { setXMLFile(QStringLiteral("sessionui.rc")); setupCommonActions(); setupExtraActions(); } actionCollection()->addAssociatedWidget(view); foreach(QAction * action, actionCollection()->actions()) { action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } setIdentifier(++_lastControllerId); sessionAttributeChanged(); view->installEventFilter(this); view->setSessionController(this); // install filter on the view to highlight URLs and files updateFilterList(SessionManager::instance()->sessionProfile(_session)); // listen for changes in session, we might need to change the enabled filters connect(ProfileManager::instance(), &Konsole::ProfileManager::profileChanged, this, &Konsole::SessionController::updateFilterList); // listen for session resize requests connect(_session.data(), &Konsole::Session::resizeRequest, this, &Konsole::SessionController::sessionResizeRequest); // listen for popup menu requests connect(_view.data(), &Konsole::TerminalDisplay::configureRequest, this, &Konsole::SessionController::showDisplayContextMenu); // move view to newest output when keystrokes occur connect(_view.data(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::trackOutput); // listen to activity / silence notifications from session connect(_session.data(), &Konsole::Session::stateChanged, this, &Konsole::SessionController::sessionStateChanged); // listen to title and icon changes connect(_session.data(), &Konsole::Session::sessionAttributeChanged, this, &Konsole::SessionController::sessionAttributeChanged); connect(_session.data(), &Konsole::Session::readOnlyChanged, this, &Konsole::SessionController::sessionReadOnlyChanged); - connect(this, &Konsole::SessionController::tabRenamedByUser, _session, &Konsole::Session::tabRenamedByUser); + connect(this, &Konsole::SessionController::tabRenamedByUser, _session, &Konsole::Session::tabTitleSetByUser); connect(_session.data() , &Konsole::Session::currentDirectoryChanged , this , &Konsole::SessionController::currentDirectoryChanged); // listen for color changes connect(_session.data(), &Konsole::Session::changeBackgroundColorRequest, _view.data(), &Konsole::TerminalDisplay::setBackgroundColor); connect(_session.data(), &Konsole::Session::changeForegroundColorRequest, _view.data(), &Konsole::TerminalDisplay::setForegroundColor); // update the title when the session starts connect(_session.data(), &Konsole::Session::started, this, &Konsole::SessionController::snapshot); // listen for output changes to set activity flag connect(_session->emulation(), &Konsole::Emulation::outputChanged, this, &Konsole::SessionController::fireActivity); // listen for detection of ZModem transfer connect(_session.data(), &Konsole::Session::zmodemDownloadDetected, this, &Konsole::SessionController::zmodemDownload); connect(_session.data(), &Konsole::Session::zmodemUploadDetected, this, &Konsole::SessionController::zmodemUpload); // listen for flow control status changes connect(_session.data(), &Konsole::Session::flowControlEnabledChanged, _view.data(), &Konsole::TerminalDisplay::setFlowControlWarningEnabled); _view->setFlowControlWarningEnabled(_session->flowControlEnabled()); // take a snapshot of the session state every so often when // user activity occurs // // the timer is owned by the session so that it will be destroyed along // with the session _interactionTimer = new QTimer(_session); _interactionTimer->setSingleShot(true); _interactionTimer->setInterval(500); connect(_interactionTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot); connect(_view.data(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::interactionHandler); // take a snapshot of the session state periodically in the background auto backgroundTimer = new QTimer(_session); backgroundTimer->setSingleShot(false); backgroundTimer->setInterval(2000); connect(backgroundTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot); backgroundTimer->start(); // xterm '11;?' request connect(_session.data(), &Konsole::Session::getBackgroundColor, this, &Konsole::SessionController::sendBackgroundColor); _allControllers.insert(this); // A list of programs that accept Ctrl+C to clear command line used // before outputting bookmark. _bookmarkValidProgramsToClear << QStringLiteral("bash") << QStringLiteral("fish") << QStringLiteral("sh"); _bookmarkValidProgramsToClear << QStringLiteral("tcsh") << QStringLiteral("zsh"); setupSearchBar(); _searchBar->setVisible(_isSearchBarEnabled); } SessionController::~SessionController() { if (!_view.isNull()) { _view->setScreenWindow(nullptr); } _allControllers.remove(this); if (!_editProfileDialog.isNull()) { delete _editProfileDialog.data(); } } void SessionController::trackOutput(QKeyEvent* event) { Q_ASSERT(_view->screenWindow()); // Only jump to the bottom if the user actually typed something in, // not if the user e. g. just pressed a modifier. if (event->text().isEmpty() && (event->modifiers() != 0u)) { return; } _view->screenWindow()->setTrackOutput(true); } void SessionController::interactionHandler() { // This flag is used to make sure those special icons indicating interest // events (activity/silence/bell?) remain in the tab until user interaction // happens. Otherwise, those special icons will quickly be replaced by // normal icon when ::snapshot() is triggered _keepIconUntilInteraction = false; _interactionTimer->start(); } void SessionController::snapshot() { Q_ASSERT(!_session.isNull()); QString title = _session->getDynamicTitle(); title = title.simplified(); // Visualize that the session is broadcasting to others if ((_copyToGroup != nullptr) && _copyToGroup->sessions().count() > 1) { title.append(QLatin1Char('*')); } // use the fallback title if needed if (title.isEmpty()) { title = _session->title(Session::NameRole); } // apply new title _session->setTitle(Session::DisplayedTitleRole, title); // do not forget icon updateSessionIcon(); } QString SessionController::currentDir() const { return _session->currentWorkingDirectory(); } QUrl SessionController::url() const { return _session->getUrl(); } void SessionController::rename() { renameSession(); } void SessionController::openUrl(const QUrl& url) { // Clear shell's command line if (!_session->isForegroundProcessActive() && _bookmarkValidProgramsToClear.contains(_session->foregroundProcessName())) { _session->sendTextToTerminal(QChar(0x03), QLatin1Char('\n')); // Ctrl+C } // handle local paths if (url.isLocalFile()) { QString path = url.toLocalFile(); _session->sendTextToTerminal(QStringLiteral("cd ") + KShell::quoteArg(path), QLatin1Char('\r')); } else if (url.scheme().isEmpty()) { // QUrl couldn't parse what the user entered into the URL field // so just dump it to the shell QString command = url.toDisplayString(); if (!command.isEmpty()) { _session->sendTextToTerminal(command, QLatin1Char('\r')); } } else if (url.scheme() == QLatin1String("ssh")) { QString sshCommand = QStringLiteral("ssh "); if (url.port() > -1) { sshCommand += QStringLiteral("-p %1 ").arg(url.port()); } if (!url.userName().isEmpty()) { sshCommand += (url.userName() + QLatin1Char('@')); } if (!url.host().isEmpty()) { sshCommand += url.host(); } _session->sendTextToTerminal(sshCommand, QLatin1Char('\r')); } else if (url.scheme() == QLatin1String("telnet")) { QString telnetCommand = QStringLiteral("telnet "); if (!url.userName().isEmpty()) { telnetCommand += QStringLiteral("-l %1 ").arg(url.userName()); } if (!url.host().isEmpty()) { telnetCommand += (url.host() + QLatin1Char(' ')); } if (url.port() > -1) { telnetCommand += QString::number(url.port()); } _session->sendTextToTerminal(telnetCommand, QLatin1Char('\r')); } else { //TODO Implement handling for other Url types KMessageBox::sorry(_view->window(), i18n("Konsole does not know how to open the bookmark: ") + url.toDisplayString()); qCDebug(KonsoleDebug) << "Unable to open bookmark at url" << url << ", I do not know" << " how to handle the protocol " << url.scheme(); } } void SessionController::setupPrimaryScreenSpecificActions(bool use) { KActionCollection* collection = actionCollection(); QAction* clearAction = collection->action(QStringLiteral("clear-history")); QAction* resetAction = collection->action(QStringLiteral("clear-history-and-reset")); QAction* selectAllAction = collection->action(QStringLiteral("select-all")); QAction* selectLineAction = collection->action(QStringLiteral("select-line")); // these actions are meaningful only when primary screen is used. clearAction->setEnabled(use); resetAction->setEnabled(use); selectAllAction->setEnabled(use); selectLineAction->setEnabled(use); } void SessionController::selectionChanged(const QString& selectedText) { _selectedText = selectedText; updateCopyAction(selectedText); } void SessionController::updateCopyAction(const QString& selectedText) { QAction* copyAction = actionCollection()->action(QStringLiteral("edit_copy")); // copy action is meaningful only when some text is selected. copyAction->setEnabled(!selectedText.isEmpty()); } void SessionController::updateWebSearchMenu() { // reset _webSearchMenu->setVisible(false); _webSearchMenu->menu()->clear(); if (_selectedText.isEmpty()) { return; } QString searchText = _selectedText; searchText = searchText.replace(QLatin1Char('\n'), QLatin1Char(' ')).replace(QLatin1Char('\r'), QLatin1Char(' ')).simplified(); if (searchText.isEmpty()) { return; } KUriFilterData filterData(searchText); filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly); if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) { const QStringList searchProviders = filterData.preferredSearchProviders(); if (!searchProviders.isEmpty()) { _webSearchMenu->setText(i18n("Search for '%1' with", KStringHandler::rsqueeze(searchText, 16))); QAction* action = nullptr; foreach(const QString& searchProvider, searchProviders) { action = new QAction(searchProvider, _webSearchMenu); action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider))); action->setData(filterData.queryForPreferredSearchProvider(searchProvider)); connect(action, &QAction::triggered, this, &Konsole::SessionController::handleWebShortcutAction); _webSearchMenu->addAction(action); } _webSearchMenu->addSeparator(); action = new QAction(i18n("Configure Web Shortcuts..."), _webSearchMenu); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect(action, &QAction::triggered, this, &Konsole::SessionController::configureWebShortcuts); _webSearchMenu->addAction(action); _webSearchMenu->setVisible(true); } } } void SessionController::handleWebShortcutAction() { QAction * action = qobject_cast(sender()); if (action == nullptr) { return; } KUriFilterData filterData(action->data().toString()); if (KUriFilter::self()->filterUri(filterData, QStringList() << QStringLiteral("kurisearchfilter"))) { const QUrl& url = filterData.uri(); new KRun(url, QApplication::activeWindow()); } } void SessionController::configureWebShortcuts() { KToolInvocation::kdeinitExec(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts")); } void SessionController::sendSignal(QAction* action) { const int signal = action->data().value(); _session->sendSignal(signal); } void SessionController::sendBackgroundColor() { const QColor c = _view->getBackgroundColor(); _session->reportBackgroundColor(c); } void SessionController::toggleReadOnly() { QAction *action = qobject_cast(sender()); if (action != nullptr) { bool readonly = !isReadOnly(); _session->setReadOnly(readonly); } } bool SessionController::eventFilter(QObject* watched , QEvent* event) { if (event->type() == QEvent::FocusIn && watched == _view) { // notify the world that the view associated with this session has been focused // used by the view manager to update the title of the MainWindow widget containing the view emit focused(this); // when the view is focused, set bell events from the associated session to be delivered // by the focused view // first, disconnect any other views which are listening for bell signals from the session disconnect(_session.data(), &Konsole::Session::bellRequest, nullptr, nullptr); // second, connect the newly focused view to listen for the session's bell signal connect(_session.data(), &Konsole::Session::bellRequest, _view.data(), &Konsole::TerminalDisplay::bell); if ((_copyInputToAllTabsAction != nullptr) && _copyInputToAllTabsAction->isChecked()) { // A session with "Copy To All Tabs" has come into focus: // Ensure that newly created sessions are included in _copyToGroup! copyInputToAllTabs(); } } return Konsole::ViewProperties::eventFilter(watched, event); } void SessionController::removeSearchFilter() { if (_searchFilter == nullptr) { return; } _view->filterChain()->removeFilter(_searchFilter); delete _searchFilter; _searchFilter = nullptr; } void SessionController::setupSearchBar() { connect(_searchBar.data(), &Konsole::IncrementalSearchBar::unhandledMovementKeyPressed, this, &Konsole::SessionController::movementKeyFromSearchBarReceived); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::closeClicked, this, &Konsole::SessionController::searchClosed); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchFromClicked, this, &Konsole::SessionController::searchFrom); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::findNextClicked, this, &Konsole::SessionController::findNextInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::findPreviousClicked, this, &Konsole::SessionController::findPreviousInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::highlightMatchesToggled , this , &Konsole::SessionController::highlightMatches); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::matchCaseToggled, this, &Konsole::SessionController::changeSearchMatch); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::matchRegExpToggled, this, &Konsole::SessionController::changeSearchMatch); } void SessionController::setShowMenuAction(QAction* action) { _showMenuAction = action; } void SessionController::setupCommonActions() { KActionCollection* collection = actionCollection(); // Close Session QAction* action = collection->addAction(QStringLiteral("close-session"), this, SLOT(closeSession())); if (isKonsolePart()) { action->setText(i18n("&Close Session")); } else { action->setText(i18n("&Close Tab")); } action->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_W); // Open Browser action = collection->addAction(QStringLiteral("open-browser"), this, SLOT(openBrowser())); action->setText(i18n("Open File Manager")); action->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); // Copy and Paste action = KStandardAction::copy(this, SLOT(copy()), collection); #ifdef Q_OS_MACOS // Don't use the Konsole::ACCEL const here, we really want the Command key (Qt::META) // TODO: check what happens if we leave it to Qt to assign the default? collection->setDefaultShortcut(action, Qt::META + Qt::Key_C); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_C); #endif // disabled at first, since nothing has been selected now action->setEnabled(false); action = KStandardAction::paste(this, SLOT(paste()), collection); QList pasteShortcut; #ifdef Q_OS_MACOS pasteShortcut.append(QKeySequence(Qt::META + Qt::Key_V)); // No Insert key on Mac keyboards #else pasteShortcut.append(QKeySequence(Konsole::ACCEL + Qt::SHIFT + Qt::Key_V)); pasteShortcut.append(QKeySequence(Qt::SHIFT + Qt::Key_Insert)); #endif collection->setDefaultShortcuts(action, pasteShortcut); action = collection->addAction(QStringLiteral("paste-selection"), this, SLOT(pasteFromX11Selection())); action->setText(i18n("Paste Selection")); #ifdef Q_OS_MACOS collection->setDefaultShortcut(action, Qt::META + Qt::SHIFT + Qt::Key_V); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Insert); #endif _webSearchMenu = new KActionMenu(i18n("Web Search"), this); _webSearchMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts"))); _webSearchMenu->setVisible(false); collection->addAction(QStringLiteral("web-search"), _webSearchMenu); action = collection->addAction(QStringLiteral("select-all"), this, SLOT(selectAll())); action->setText(i18n("&Select All")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); action = collection->addAction(QStringLiteral("select-line"), this, SLOT(selectLine())); action->setText(i18n("Select &Line")); action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection); action->setText(i18n("Save Output &As...")); #ifdef Q_OS_MACOS action->setShortcut(QKeySequence(Qt::META + Qt::Key_S)); #endif action = KStandardAction::print(this, SLOT(print_screen()), collection); action->setText(i18n("&Print Screen...")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_P); action = collection->addAction(QStringLiteral("adjust-history"), this, SLOT(showHistoryOptions())); action->setText(i18n("Adjust Scrollback...")); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); action = collection->addAction(QStringLiteral("clear-history"), this, SLOT(clearHistory())); action->setText(i18n("Clear Scrollback")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); action = collection->addAction(QStringLiteral("clear-history-and-reset"), this, SLOT(clearHistoryAndReset())); action->setText(i18n("Clear Scrollback and Reset")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_K); // Profile Options action = collection->addAction(QStringLiteral("edit-current-profile"), this, SLOT(editCurrentProfile())); action->setText(i18n("Edit Current Profile...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); _switchProfileMenu = new KActionMenu(i18n("Switch Profile"), this); collection->addAction(QStringLiteral("switch-profile"), _switchProfileMenu); connect(_switchProfileMenu->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::prepareSwitchProfileMenu); // History _findAction = KStandardAction::find(this, SLOT(searchBarEvent()), collection); collection->setDefaultShortcut(_findAction, QKeySequence()); _findNextAction = KStandardAction::findNext(this, SLOT(findNextInHistory()), collection); collection->setDefaultShortcut(_findNextAction, QKeySequence()); _findNextAction->setEnabled(false); _findPreviousAction = KStandardAction::findPrev(this, SLOT(findPreviousInHistory()), collection); collection->setDefaultShortcut(_findPreviousAction, QKeySequence()); _findPreviousAction->setEnabled(false); // Character Encoding _codecAction = new KCodecAction(i18n("Set &Encoding"), this); _codecAction->setIcon(QIcon::fromTheme(QStringLiteral("character-set"))); collection->addAction(QStringLiteral("set-encoding"), _codecAction); connect(_codecAction->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::updateCodecAction); connect(_codecAction, static_cast(&KCodecAction::triggered), this, &Konsole::SessionController::changeCodec); // Read-only action = collection->addAction(QStringLiteral("view-readonly"), this, SLOT(toggleReadOnly())); action->setText(i18nc("@item:inmenu A read only (locked) session", "Read-only")); action->setCheckable(true); updateReadOnlyActionStates(); } void SessionController::setupExtraActions() { KActionCollection* collection = actionCollection(); // Rename Session QAction* action = collection->addAction(QStringLiteral("rename-session"), this, SLOT(renameSession())); action->setText(i18n("&Rename Tab...")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_S); // Copy input to ==> all tabs KToggleAction* copyInputToAllTabsAction = collection->add(QStringLiteral("copy-input-to-all-tabs")); copyInputToAllTabsAction->setText(i18n("&All Tabs in Current Window")); copyInputToAllTabsAction->setData(CopyInputToAllTabsMode); // this action is also used in other place, so remember it _copyInputToAllTabsAction = copyInputToAllTabsAction; // Copy input to ==> selected tabs KToggleAction* copyInputToSelectedTabsAction = collection->add(QStringLiteral("copy-input-to-selected-tabs")); copyInputToSelectedTabsAction->setText(i18n("&Select Tabs...")); collection->setDefaultShortcut(copyInputToSelectedTabsAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Period); copyInputToSelectedTabsAction->setData(CopyInputToSelectedTabsMode); // Copy input to ==> none KToggleAction* copyInputToNoneAction = collection->add(QStringLiteral("copy-input-to-none")); copyInputToNoneAction->setText(i18nc("@action:inmenu Do not select any tabs", "&None")); collection->setDefaultShortcut(copyInputToNoneAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Slash); copyInputToNoneAction->setData(CopyInputToNoneMode); copyInputToNoneAction->setChecked(true); // the default state // The "Copy Input To" submenu // The above three choices are represented as combo boxes KSelectAction* copyInputActions = collection->add(QStringLiteral("copy-input-to")); copyInputActions->setText(i18n("Copy Input To")); copyInputActions->addAction(copyInputToAllTabsAction); copyInputActions->addAction(copyInputToSelectedTabsAction); copyInputActions->addAction(copyInputToNoneAction); connect(copyInputActions, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::copyInputActionsTriggered); action = collection->addAction(QStringLiteral("zmodem-upload"), this, SLOT(zmodemUpload())); action->setText(i18n("&ZModem Upload...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_U); // Monitor KToggleAction* toggleAction = new KToggleAction(i18n("Monitor for &Activity"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_A); action = collection->addAction(QStringLiteral("monitor-activity"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorActivity); toggleAction = new KToggleAction(i18n("Monitor for &Silence"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_I); action = collection->addAction(QStringLiteral("monitor-silence"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorSilence); // Text Size action = collection->addAction(QStringLiteral("enlarge-font"), this, SLOT(increaseFontSize())); action->setText(i18n("Enlarge Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-more"))); QList enlargeFontShortcut; enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Plus)); enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Equal)); collection->setDefaultShortcuts(action, enlargeFontShortcut); action = collection->addAction(QStringLiteral("shrink-font"), this, SLOT(decreaseFontSize())); action->setText(i18n("Shrink Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-less"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_Minus); // Send signal KSelectAction* sendSignalActions = collection->add(QStringLiteral("send-signal")); sendSignalActions->setText(i18n("Send Signal")); connect(sendSignalActions, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::sendSignal); action = collection->addAction(QStringLiteral("sigstop-signal")); action->setText(i18n("&Suspend Task") + QStringLiteral(" (STOP)")); action->setData(SIGSTOP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigcont-signal")); action->setText(i18n("&Continue Task") + QStringLiteral(" (CONT)")); action->setData(SIGCONT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sighup-signal")); action->setText(i18n("&Hangup") + QStringLiteral(" (HUP)")); action->setData(SIGHUP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigint-signal")); action->setText(i18n("&Interrupt Task") + QStringLiteral(" (INT)")); action->setData(SIGINT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigterm-signal")); action->setText(i18n("&Terminate Task") + QStringLiteral(" (TERM)")); action->setData(SIGTERM); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigkill-signal")); action->setText(i18n("&Kill Task") + QStringLiteral(" (KILL)")); action->setData(SIGKILL); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr1-signal")); action->setText(i18n("User Signal &1") + QStringLiteral(" (USR1)")); action->setData(SIGUSR1); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr2-signal")); action->setText(i18n("User Signal &2") + QStringLiteral(" (USR2)")); action->setData(SIGUSR2); sendSignalActions->addAction(action); #ifdef Q_OS_MACOS collection->setDefaultShortcut(_findAction, Qt::META + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::META + Qt::Key_G); collection->setDefaultShortcut(_findPreviousAction, Qt::META + Qt::SHIFT + Qt::Key_G); #else collection->setDefaultShortcut(_findAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::Key_F3); collection->setDefaultShortcut(_findPreviousAction, Qt::SHIFT + Qt::Key_F3); #endif } void SessionController::switchProfile(Profile::Ptr profile) { SessionManager::instance()->setSessionProfile(_session, profile); updateFilterList(profile); } void SessionController::prepareSwitchProfileMenu() { if (_switchProfileMenu->menu()->isEmpty()) { _profileList = new ProfileList(false, this); connect(_profileList, &Konsole::ProfileList::profileSelected, this, &Konsole::SessionController::switchProfile); } _switchProfileMenu->menu()->clear(); _switchProfileMenu->menu()->addActions(_profileList->actions()); } void SessionController::updateCodecAction() { _codecAction->setCurrentCodec(QString::fromUtf8(_session->codec())); } void SessionController::changeCodec(QTextCodec* codec) { _session->setCodec(codec); } EditProfileDialog* SessionController::profileDialogPointer() { return _editProfileDialog.data(); } void SessionController::editCurrentProfile() { // Searching for Edit profile dialog opened with the same profile const QList allSessionsControllers = _allControllers.values(); foreach (SessionController* session, allSessionsControllers) { if ((session->profileDialogPointer() != nullptr) && session->profileDialogPointer()->isVisible() && session->profileDialogPointer()->lookupProfile() == SessionManager::instance()->sessionProfile(_session)) { session->profileDialogPointer()->close(); } } // NOTE bug311270: For to prevent the crash, the profile must be reset. if (!_editProfileDialog.isNull()) { // exists but not visible delete _editProfileDialog.data(); } _editProfileDialog = new EditProfileDialog(QApplication::activeWindow()); _editProfileDialog.data()->setProfile(SessionManager::instance()->sessionProfile(_session)); _editProfileDialog.data()->show(); } void SessionController::renameSession() { const QString &sessionLocalTabTitleFormat = _session->tabTitleFormat(Session::LocalTabTitle); const QString &sessionRemoteTabTitleFormat = _session->tabTitleFormat(Session::RemoteTabTitle); QScopedPointer dialog(new RenameTabDialog(QApplication::activeWindow())); dialog->setTabTitleText(sessionLocalTabTitleFormat); dialog->setRemoteTabTitleText(sessionRemoteTabTitleFormat); if (_session->isRemote()) { dialog->focusRemoteTabTitleText(); } else { dialog->focusTabTitleText(); } QPointer guard(_session); int result = dialog->exec(); if (guard.isNull()) { return; } if (result != 0) { const QString &tabTitle = dialog->tabTitleText(); const QString &remoteTabTitle = dialog->remoteTabTitleText(); if (tabTitle != sessionLocalTabTitleFormat) { _session->setTabTitleFormat(Session::LocalTabTitle, tabTitle); emit tabRenamedByUser(true); // trigger an update of the tab text snapshot(); } if(remoteTabTitle != sessionRemoteTabTitleFormat) { _session->setTabTitleFormat(Session::RemoteTabTitle, remoteTabTitle); emit tabRenamedByUser(true); snapshot(); } } } bool SessionController::confirmClose() const { if (_session->isForegroundProcessActive()) { QString title = _session->foregroundProcessName(); // hard coded for now. In future make it possible for the user to specify which programs // are ignored when considering whether to display a confirmation QStringList ignoreList; ignoreList << QString::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -1); if (ignoreList.contains(title)) { return true; } QString question; if (title.isEmpty()) { question = i18n("A program is currently running in this session." " Are you sure you want to close it?"); } else { question = i18n("The program '%1' is currently running in this session." " Are you sure you want to close it?", title); } int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close")); return result == KMessageBox::Yes; } return true; } bool SessionController::confirmForceClose() const { if (_session->isRunning()) { QString title = _session->program(); // hard coded for now. In future make it possible for the user to specify which programs // are ignored when considering whether to display a confirmation QStringList ignoreList; ignoreList << QString::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -1); if (ignoreList.contains(title)) { return true; } QString question; if (title.isEmpty()) { question = i18n("A program in this session would not die." " Are you sure you want to kill it by force?"); } else { question = i18n("The program '%1' is in this session would not die." " Are you sure you want to kill it by force?", title); } int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close")); return result == KMessageBox::Yes; } return true; } void SessionController::closeSession() { if (_preventClose) { return; } if (confirmClose()) { if (_session->closeInNormalWay()) { return; } else if (confirmForceClose()) { if (_session->closeInForceWay()) { return; } else { qCDebug(KonsoleDebug) << "Konsole failed to close a session in any way."; } } } } // Trying to open a remote Url may produce unexpected results. // Therefore, if a remote url, open the user's home path. // TODO consider: 1) disable menu upon remote session // 2) transform url to get the desired result (ssh -> sftp, etc) void SessionController::openBrowser() { const QUrl currentUrl = url(); if (currentUrl.isLocalFile()) { new KRun(currentUrl, QApplication::activeWindow(), true); } else { new KRun(QUrl::fromLocalFile(QDir::homePath()), QApplication::activeWindow(), true); } } void SessionController::copy() { _view->copyToClipboard(); } void SessionController::paste() { _view->pasteFromClipboard(); } void SessionController::pasteFromX11Selection() { _view->pasteFromX11Selection(); } void SessionController::selectAll() { _view->selectAll(); } void SessionController::selectLine() { _view->selectCurrentLine(); } static const KXmlGuiWindow* findWindow(const QObject* object) { // Walk up the QObject hierarchy to find a KXmlGuiWindow. while (object != nullptr) { const KXmlGuiWindow* window = qobject_cast(object); if (window != nullptr) { return(window); } object = object->parent(); } return(nullptr); } static bool hasTerminalDisplayInSameWindow(const Session* session, const KXmlGuiWindow* window) { // Iterate all TerminalDisplays of this Session ... foreach(const TerminalDisplay* terminalDisplay, session->views()) { // ... and check whether a TerminalDisplay has the same // window as given in the parameter if (window == findWindow(terminalDisplay)) { return(true); } } return(false); } void SessionController::copyInputActionsTriggered(QAction* action) { const int mode = action->data().value(); switch (mode) { case CopyInputToAllTabsMode: copyInputToAllTabs(); break; case CopyInputToSelectedTabsMode: copyInputToSelectedTabs(); break; case CopyInputToNoneMode: copyInputToNone(); break; default: Q_ASSERT(false); } } void SessionController::copyInputToAllTabs() { if (_copyToGroup == nullptr) { _copyToGroup = new SessionGroup(this); } // Find our window ... const KXmlGuiWindow* myWindow = findWindow(_view); QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto session : group) { // First, ensure that the session is removed // (necessary to avoid duplicates on addSession()!) _copyToGroup->removeSession(session); // Add current session if it is displayed our window if (hasTerminalDisplayInSameWindow(session, myWindow)) { _copyToGroup->addSession(session); } } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } void SessionController::copyInputToSelectedTabs() { if (_copyToGroup == nullptr) { _copyToGroup = new SessionGroup(this); _copyToGroup->addSession(_session); _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); } QPointer dialog = new CopyInputDialog(_view); dialog->setMasterSession(_session); QSet currentGroup = QSet::fromList(_copyToGroup->sessions()); currentGroup.remove(_session); dialog->setChosenSessions(currentGroup); QPointer guard(_session); int result = dialog->exec(); if (guard.isNull()) { return; } if (result == QDialog::Accepted) { QSet newGroup = dialog->chosenSessions(); newGroup.remove(_session); QSet completeGroup = newGroup | currentGroup; foreach(Session * session, completeGroup) { if (newGroup.contains(session) && !currentGroup.contains(session)) { _copyToGroup->addSession(session); } else if (!newGroup.contains(session) && currentGroup.contains(session)) { _copyToGroup->removeSession(session); } } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } } void SessionController::copyInputToNone() { if (_copyToGroup == nullptr) { // No 'Copy To' is active return; } QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto iterator : group) { Session* session = iterator; if (session != _session) { _copyToGroup->removeSession(iterator); } } delete _copyToGroup; _copyToGroup = nullptr; snapshot(); } void SessionController::searchClosed() { _isSearchBarEnabled = false; searchHistory(false); } void SessionController::updateFilterList(Profile::Ptr profile) { if (profile != SessionManager::instance()->sessionProfile(_session)) { return; } bool underlineFiles = profile->underlineFilesEnabled(); if (!underlineFiles && (_fileFilter != nullptr)) { _view->filterChain()->removeFilter(_fileFilter); delete _fileFilter; _fileFilter = nullptr; } else if (underlineFiles && (_fileFilter == nullptr)) { _fileFilter = new FileFilter(_session); _view->filterChain()->addFilter(_fileFilter); } bool underlineLinks = profile->underlineLinksEnabled(); if (!underlineLinks && (_urlFilter != nullptr)) { _view->filterChain()->removeFilter(_urlFilter); delete _urlFilter; _urlFilter = nullptr; } else if (underlineLinks && (_urlFilter == nullptr)) { _urlFilter = new UrlFilter(); _view->filterChain()->addFilter(_urlFilter); } } void SessionController::setSearchStartToWindowCurrentLine() { setSearchStartTo(-1); } void SessionController::setSearchStartTo(int line) { _searchStartLine = line; _prevSearchResultLine = line; } void SessionController::listenForScreenWindowUpdates() { if (_listenForScreenWindowUpdates) { return; } connect(_view->screenWindow(), &Konsole::ScreenWindow::outputChanged, this, &Konsole::SessionController::updateSearchFilter); connect(_view->screenWindow(), &Konsole::ScreenWindow::scrolled, this, &Konsole::SessionController::updateSearchFilter); connect(_view->screenWindow(), &Konsole::ScreenWindow::currentResultLineChanged, _view.data(), static_cast(&Konsole::TerminalDisplay::update)); _listenForScreenWindowUpdates = true; } void SessionController::updateSearchFilter() { if ((_searchFilter != nullptr) && (!_searchBar.isNull())) { _view->processFilters(); } } void SessionController::searchBarEvent() { QString selectedText = _view->screenWindow()->selectedText(Screen::PreserveLineBreaks | Screen::TrimLeadingWhitespace | Screen::TrimTrailingWhitespace); if (!selectedText.isEmpty()) { _searchBar->setSearchText(selectedText); } if (_searchBar->isVisible()) { _searchBar->focusLineEdit(); } else { searchHistory(true); _isSearchBarEnabled = true; } } void SessionController::enableSearchBar(bool showSearchBar) { if (_searchBar.isNull()) { return; } if (showSearchBar && !_searchBar->isVisible()) { setSearchStartToWindowCurrentLine(); } _searchBar->setVisible(showSearchBar); if (showSearchBar) { connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchChanged, this, &Konsole::SessionController::searchTextChanged); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchReturnPressed, this, &Konsole::SessionController::findPreviousInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchShiftPlusReturnPressed, this, &Konsole::SessionController::findNextInHistory); } else { disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchChanged, this, &Konsole::SessionController::searchTextChanged); disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchReturnPressed, this, &Konsole::SessionController::findPreviousInHistory); disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchShiftPlusReturnPressed, this, &Konsole::SessionController::findNextInHistory); if ((!_view.isNull()) && (_view->screenWindow() != nullptr)) { _view->screenWindow()->setCurrentResultLine(-1); } } } bool SessionController::reverseSearchChecked() const { Q_ASSERT(_searchBar); QBitArray options = _searchBar->optionsChecked(); return options.at(IncrementalSearchBar::ReverseSearch); } QRegularExpression SessionController::regexpFromSearchBarOptions() const { QBitArray options = _searchBar->optionsChecked(); QString text(_searchBar->searchText()); QRegularExpression regExp; if (options.at(IncrementalSearchBar::RegExp)) { regExp.setPattern(text); } else { regExp.setPattern(QRegularExpression::escape(text)); } if (!options.at(IncrementalSearchBar::MatchCase)) { regExp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); } return regExp; } // searchHistory() may be called either as a result of clicking a menu item or // as a result of changing the search bar widget void SessionController::searchHistory(bool showSearchBar) { enableSearchBar(showSearchBar); if (!_searchBar.isNull()) { if (showSearchBar) { removeSearchFilter(); listenForScreenWindowUpdates(); _searchFilter = new RegExpFilter(); _searchFilter->setRegExp(regexpFromSearchBarOptions()); _view->filterChain()->addFilter(_searchFilter); _view->processFilters(); setFindNextPrevEnabled(true); } else { setFindNextPrevEnabled(false); removeSearchFilter(); _view->setFocus(Qt::ActiveWindowFocusReason); } } } void SessionController::setFindNextPrevEnabled(bool enabled) { _findNextAction->setEnabled(enabled); _findPreviousAction->setEnabled(enabled); } void SessionController::searchTextChanged(const QString& text) { Q_ASSERT(_view->screenWindow()); if (_searchText == text) { return; } _searchText = text; if (text.isEmpty()) { _view->screenWindow()->clearSelection(); _view->screenWindow()->scrollTo(_searchStartLine); } // update search. this is called even when the text is // empty to clear the view's filters beginSearch(text , reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::searchCompleted(bool success) { _prevSearchResultLine = _view->screenWindow()->currentResultLine(); if (!_searchBar.isNull()) { _searchBar->setFoundMatch(success); } } void SessionController::beginSearch(const QString& text, Enum::SearchDirection direction) { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); QRegularExpression regExp = regexpFromSearchBarOptions(); _searchFilter->setRegExp(regExp); if (_searchStartLine < 0 || _searchStartLine > _view->screenWindow()->lineCount()) { if (direction == Enum::ForwardsSearch) { setSearchStartTo(_view->screenWindow()->currentLine()); } else { setSearchStartTo(_view->screenWindow()->currentLine() + _view->screenWindow()->windowLines()); } } if (!regExp.pattern().isEmpty()) { _view->screenWindow()->setCurrentResultLine(-1); auto task = new SearchHistoryTask(this); connect(task, &Konsole::SearchHistoryTask::completed, this, &Konsole::SessionController::searchCompleted); task->setRegExp(regExp); task->setSearchDirection(direction); task->setAutoDelete(true); task->setStartLine(_searchStartLine); task->addScreenWindow(_session , _view->screenWindow()); task->execute(); } else if (text.isEmpty()) { searchCompleted(false); } _view->processFilters(); } void SessionController::highlightMatches(bool highlight) { if (highlight) { _view->filterChain()->addFilter(_searchFilter); _view->processFilters(); } else { _view->filterChain()->removeFilter(_searchFilter); } _view->update(); } void SessionController::searchFrom() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); if (reverseSearchChecked()) { setSearchStartTo(_view->screenWindow()->lineCount()); } else { setSearchStartTo(0); } beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::findNextInHistory() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); setSearchStartTo(_prevSearchResultLine); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::findPreviousInHistory() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); setSearchStartTo(_prevSearchResultLine); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::ForwardsSearch : Enum::BackwardsSearch); } void SessionController::changeSearchMatch() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); // reset Selection for new case match _view->screenWindow()->clearSelection(); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::showHistoryOptions() { QScopedPointer dialog(new HistorySizeDialog(QApplication::activeWindow())); const HistoryType& currentHistory = _session->historyType(); if (currentHistory.isEnabled()) { if (currentHistory.isUnlimited()) { dialog->setMode(Enum::UnlimitedHistory); } else { dialog->setMode(Enum::FixedSizeHistory); dialog->setLineCount(currentHistory.maximumLineCount()); } } else { dialog->setMode(Enum::NoHistory); } QPointer guard(_session); int result = dialog->exec(); if (guard.isNull()) { return; } if (result != 0) { scrollBackOptionsChanged(dialog->mode(), dialog->lineCount()); } } void SessionController::sessionResizeRequest(const QSize& size) { ////qDebug() << "View resize requested to " << size; _view->setSize(size.width(), size.height()); } void SessionController::scrollBackOptionsChanged(int mode, int lines) { switch (mode) { case Enum::NoHistory: _session->setHistoryType(HistoryTypeNone()); break; case Enum::FixedSizeHistory: _session->setHistoryType(CompactHistoryType(lines)); break; case Enum::UnlimitedHistory: _session->setHistoryType(HistoryTypeFile()); break; } } void SessionController::print_screen() { QPrinter printer; QPointer dialog = new QPrintDialog(&printer, _view); auto options = new PrintOptions(); dialog->setOptionTabs(QList() << options); dialog->setWindowTitle(i18n("Print Shell")); connect(dialog.data(), static_cast(&QPrintDialog::accepted), options, &Konsole::PrintOptions::saveSettings); if (dialog->exec() != QDialog::Accepted) { return; } QPainter painter; painter.begin(&printer); KConfigGroup configGroup(KSharedConfig::openConfig(), "PrintOptions"); if (configGroup.readEntry("ScaleOutput", true)) { double scale = qMin(printer.pageRect().width() / static_cast(_view->width()), printer.pageRect().height() / static_cast(_view->height())); painter.scale(scale, scale); } _view->printContent(painter, configGroup.readEntry("PrinterFriendly", true)); } void SessionController::saveHistory() { SessionTask* task = new SaveHistoryTask(this); task->setAutoDelete(true); task->addSession(_session); task->execute(); } void SessionController::clearHistory() { _session->clearHistory(); _view->updateImage(); // To reset view scrollbar _view->repaint(); } void SessionController::clearHistoryAndReset() { Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session); QByteArray name = profile->defaultEncoding().toUtf8(); Emulation* emulation = _session->emulation(); emulation->reset(); _session->refresh(); _session->setCodec(QTextCodec::codecForName(name)); clearHistory(); } void SessionController::increaseFontSize() { _view->increaseFontSize(); } void SessionController::decreaseFontSize() { _view->decreaseFontSize(); } void SessionController::monitorActivity(bool monitor) { _session->setMonitorActivity(monitor); } void SessionController::monitorSilence(bool monitor) { _session->setMonitorSilence(monitor); } void SessionController::updateSessionIcon() { // Visualize that the session is broadcasting to others if ((_copyToGroup != nullptr) && _copyToGroup->sessions().count() > 1) { // Master Mode: set different icon, to warn the user to be careful setIcon(*_broadcastIcon); } else { if (!_keepIconUntilInteraction) { // Not in Master Mode: use normal icon setIcon(_sessionIcon); } } } void SessionController::updateReadOnlyActionStates() { bool readonly = isReadOnly(); QAction *readonlyAction = actionCollection()->action(QStringLiteral("view-readonly")); Q_ASSERT(readonlyAction != nullptr); readonlyAction->setIcon(QIcon::fromTheme(readonly ? QStringLiteral("object-locked") : QStringLiteral("object-unlocked"))); readonlyAction->setChecked(readonly); auto updateActionState = [this, readonly](const QString &name) { QAction *action = actionCollection()->action(name); if (action != nullptr) { action->setEnabled(!readonly); } }; updateActionState(QStringLiteral("edit_paste")); updateActionState(QStringLiteral("clear-history")); updateActionState(QStringLiteral("clear-history-and-reset")); updateActionState(QStringLiteral("edit-current-profile")); updateActionState(QStringLiteral("switch-profile")); updateActionState(QStringLiteral("adjust-history")); updateActionState(QStringLiteral("send-signal")); updateActionState(QStringLiteral("zmodem-upload")); _codecAction->setEnabled(!readonly); // Without the timer, when detaching a tab while the message widget is visible, // the size of the terminal becomes really small... QTimer::singleShot(0, this, [this, readonly]() { _view->updateReadOnlyState(readonly); }); } bool SessionController::isReadOnly() const { if (!_session.isNull()) { return _session->isReadOnly(); } else { return false; } } void SessionController::sessionAttributeChanged() { if (_sessionIconName != _session->iconName()) { _sessionIconName = _session->iconName(); _sessionIcon = QIcon::fromTheme(_sessionIconName); updateSessionIcon(); } QString title = _session->title(Session::DisplayedTitleRole); // special handling for the "%w" marker which is replaced with the // window title set by the shell title.replace(QLatin1String("%w"), _session->userTitle()); // special handling for the "%#" marker which is replaced with the // number of the shell title.replace(QLatin1String("%#"), QString::number(_session->sessionId())); if (title.isEmpty()) { title = _session->title(Session::NameRole); } setTitle(title); emit rawTitleChanged(); } void SessionController::sessionReadOnlyChanged() { // Trigger icon update sessionAttributeChanged(); updateReadOnlyActionStates(); // Update all views for (TerminalDisplay* view : session()->views()) { if (view != _view.data()) { view->updateReadOnlyState(isReadOnly()); } } } void SessionController::showDisplayContextMenu(const QPoint& position) { // needed to make sure the popup menu is available, even if a hosting // application did not merge our GUI. if (factory() == nullptr) { if (clientBuilder() == nullptr) { setClientBuilder(new KXMLGUIBuilder(_view)); } auto factory = new KXMLGUIFactory(clientBuilder(), this); factory->addClient(this); ////qDebug() << "Created xmlgui factory" << factory; } QPointer popup = qobject_cast(factory()->container(QStringLiteral("session-popup-menu"), this)); if (!popup.isNull()) { updateReadOnlyActionStates(); // prepend content-specific actions such as "Open Link", "Copy Email Address" etc. QList contentActions = _view->filterActions(position); auto contentSeparator = new QAction(popup); contentSeparator->setSeparator(true); contentActions << contentSeparator; popup->insertActions(popup->actions().value(0, nullptr), contentActions); // always update this submenu before showing the context menu, // because the available search services might have changed // since the context menu is shown last time updateWebSearchMenu(); _preventClose = true; if (_showMenuAction != nullptr) { if ( _showMenuAction->isChecked() ) { popup->removeAction( _showMenuAction); } else { popup->insertAction(_switchProfileMenu, _showMenuAction); } } QAction* chosen = popup->exec(_view->mapToGlobal(position)); // check for validity of the pointer to the popup menu if (!popup.isNull()) { // Remove content-specific actions // // If the close action was chosen, the popup menu will be partially // destroyed at this point, and the rest will be destroyed later by // 'chosen->trigger()' foreach(QAction * action, contentActions) { popup->removeAction(action); } delete contentSeparator; } _preventClose = false; if ((chosen != nullptr) && chosen->objectName() == QLatin1String("close-session")) { chosen->trigger(); } } else { qCDebug(KonsoleDebug) << "Unable to display popup menu for session" << _session->title(Session::NameRole) << ", no GUI factory available to build the popup."; } } void SessionController::movementKeyFromSearchBarReceived(QKeyEvent *event) { QCoreApplication::sendEvent(_view, event); setSearchStartToWindowCurrentLine(); } void SessionController::sessionStateChanged(int state) { if (state == _previousState) { return; } if (state == NOTIFYACTIVITY) { setIcon(*_activityIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYSILENCE) { setIcon(*_silenceIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYNORMAL) { if (_sessionIconName != _session->iconName()) { _sessionIconName = _session->iconName(); _sessionIcon = QIcon::fromTheme(_sessionIconName); } updateSessionIcon(); } _previousState = state; } void SessionController::zmodemDownload() { QString zmodem = QStandardPaths::findExecutable(QStringLiteral("rz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("lrz")); } if (!zmodem.isEmpty()) { const QString path = QFileDialog::getExistingDirectory(_view, i18n("Save ZModem Download to..."), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!path.isEmpty()) { _session->startZModem(zmodem, path, QStringList()); return; } } else { KMessageBox::error(_view, i18n("

A ZModem file transfer attempt has been detected, " "but no suitable ZModem software was found on this system.

" "

You may wish to install the 'rzsz' or 'lrzsz' package.

")); } _session->cancelZModem(); return; } void SessionController::zmodemUpload() { if (_session->isZModemBusy()) { KMessageBox::sorry(_view, i18n("

The current session already has a ZModem file transfer in progress.

")); return; } _session->setZModemBusy(true); QString zmodem = QStandardPaths::findExecutable(QStringLiteral("sz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("lsz")); } if (zmodem.isEmpty()) { KMessageBox::sorry(_view, i18n("

No suitable ZModem software was found on this system.

" "

You may wish to install the 'rzsz' or 'lrzsz' package.

")); return; } QStringList files = QFileDialog::getOpenFileNames(_view, i18n("Select Files for ZModem Upload"), QDir::homePath()); if (!files.isEmpty()) { _session->startZModem(zmodem, QString(), files); } } bool SessionController::isKonsolePart() const { // Check to see if we are being called from Konsole or a KPart return !(qApp->applicationName() == QLatin1String("konsole")); } SessionTask::SessionTask(QObject* parent) : QObject(parent) , _autoDelete(false) { } void SessionTask::setAutoDelete(bool enable) { _autoDelete = enable; } bool SessionTask::autoDelete() const { return _autoDelete; } void SessionTask::addSession(Session* session) { _sessions << session; } QList SessionTask::sessions() const { return _sessions; } SaveHistoryTask::SaveHistoryTask(QObject* parent) : SessionTask(parent) { } SaveHistoryTask::~SaveHistoryTask() = default; void SaveHistoryTask::execute() { // TODO - think about the UI when saving multiple history sessions, if there are more than two or // three then providing a URL for each one will be tedious // TODO - show a warning ( preferably passive ) if saving the history output fails QFileDialog* dialog = new QFileDialog(QApplication::activeWindow(), QString(), QDir::homePath()); dialog->setAcceptMode(QFileDialog::AcceptSave); QStringList mimeTypes; mimeTypes << QStringLiteral("text/plain"); mimeTypes << QStringLiteral("text/html"); dialog->setMimeTypeFilters(mimeTypes); // iterate over each session in the task and display a dialog to allow the user to choose where // to save that session's history. // then start a KIO job to transfer the data from the history to the chosen URL foreach(const SessionPtr& session, sessions()) { dialog->setWindowTitle(i18n("Save Output From %1", session->title(Session::NameRole))); int result = dialog->exec(); if (result != QDialog::Accepted) { continue; } QUrl url = (dialog->selectedUrls()).at(0); if (!url.isValid()) { // UI: Can we make this friendlier? KMessageBox::sorry(nullptr , i18n("%1 is an invalid URL, the output could not be saved.", url.url())); continue; } KIO::TransferJob* job = KIO::put(url, -1, // no special permissions // overwrite existing files // do not resume an existing transfer // show progress information only for remote // URLs KIO::Overwrite | (url.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags) // a better solution would be to show progress // information after a certain period of time // instead, since the overall speed of transfer // depends on factors other than just the protocol // used ); SaveJob jobInfo; jobInfo.session = session; jobInfo.lastLineFetched = -1; // when each request for data comes in from the KIO subsystem // lastLineFetched is used to keep track of how much of the history // has already been sent, and where the next request should continue // from. // this is set to -1 to indicate the job has just been started if (((dialog->selectedNameFilter()).contains(QLatin1String("html"), Qt::CaseInsensitive)) || ((dialog->selectedFiles()).at(0).endsWith(QLatin1String("html"), Qt::CaseInsensitive))) { jobInfo.decoder = new HTMLDecoder(); } else { jobInfo.decoder = new PlainTextDecoder(); } _jobSession.insert(job, jobInfo); connect(job, &KIO::TransferJob::dataReq, this, &Konsole::SaveHistoryTask::jobDataRequested); connect(job, &KIO::TransferJob::result, this, &Konsole::SaveHistoryTask::jobResult); } dialog->deleteLater(); } void SaveHistoryTask::jobDataRequested(KIO::Job* job , QByteArray& data) { // TODO - Report progress information for the job // PERFORMANCE: Do some tests and tweak this value to get faster saving const int LINES_PER_REQUEST = 500; SaveJob& info = _jobSession[job]; // transfer LINES_PER_REQUEST lines from the session's history // to the save location if (!info.session.isNull()) { // note: when retrieving lines from the emulation, // the first line is at index 0. int sessionLines = info.session->emulation()->lineCount(); if (sessionLines - 1 == info.lastLineFetched) { return; // if there is no more data to transfer then stop the job } int copyUpToLine = qMin(info.lastLineFetched + LINES_PER_REQUEST , sessionLines - 1); QTextStream stream(&data, QIODevice::ReadWrite); info.decoder->begin(&stream); info.session->emulation()->writeToStream(info.decoder , info.lastLineFetched + 1 , copyUpToLine); info.decoder->end(); info.lastLineFetched = copyUpToLine; } } void SaveHistoryTask::jobResult(KJob* job) { if (job->error() != 0) { KMessageBox::sorry(nullptr , i18n("A problem occurred when saving the output.\n%1", job->errorString())); } TerminalCharacterDecoder * decoder = _jobSession[job].decoder; _jobSession.remove(job); delete decoder; // notify the world that the task is done emit completed(true); if (autoDelete()) { deleteLater(); } } void SearchHistoryTask::addScreenWindow(Session* session , ScreenWindow* searchWindow) { _windows.insert(session, searchWindow); } void SearchHistoryTask::execute() { QMapIterator< SessionPtr , ScreenWindowPtr > iter(_windows); while (iter.hasNext()) { iter.next(); executeOnScreenWindow(iter.key() , iter.value()); } } void SearchHistoryTask::executeOnScreenWindow(SessionPtr session , ScreenWindowPtr window) { Q_ASSERT(session); Q_ASSERT(window); Emulation* emulation = session->emulation(); if (!_regExp.pattern().isEmpty()) { int pos = -1; const bool forwards = (_direction == Enum::ForwardsSearch); const int lastLine = window->lineCount() - 1; int startLine; if (forwards && (_startLine == lastLine)) { startLine = 0; } else if (!forwards && (_startLine == 0)) { startLine = lastLine; } else { startLine = _startLine + (forwards ? 1 : -1); } QString string; //text stream to read history into string for pattern or regular expression searching QTextStream searchStream(&string); PlainTextDecoder decoder; decoder.setRecordLinePositions(true); //setup first and last lines depending on search direction int line = startLine; //read through and search history in blocks of 10K lines. //this balances the need to retrieve lots of data from the history each time //(for efficient searching) //without using silly amounts of memory if the history is very large. const int maxDelta = qMin(window->lineCount(), 10000); int delta = forwards ? maxDelta : -maxDelta; int endLine = line; bool hasWrapped = false; // set to true when we reach the top/bottom // of the output and continue from the other // end //loop through history in blocks of lines. do { // ensure that application does not appear to hang // if searching through a lengthy output QApplication::processEvents(); // calculate lines to search in this iteration if (hasWrapped) { if (endLine == lastLine) { line = 0; } else if (endLine == 0) { line = lastLine; } endLine += delta; if (forwards) { endLine = qMin(startLine , endLine); } else { endLine = qMax(startLine , endLine); } } else { endLine += delta; if (endLine > lastLine) { hasWrapped = true; endLine = lastLine; } else if (endLine < 0) { hasWrapped = true; endLine = 0; } } decoder.begin(&searchStream); emulation->writeToStream(&decoder, qMin(endLine, line) , qMax(endLine, line)); decoder.end(); // line number search below assumes that the buffer ends with a new-line string.append(QLatin1Char('\n')); if (forwards) { pos = string.indexOf(_regExp); } else { pos = string.lastIndexOf(_regExp); } //if a match is found, position the cursor on that line and update the screen if (pos != -1) { int newLines = 0; QList linePositions = decoder.linePositions(); while (newLines < linePositions.count() && linePositions[newLines] <= pos) { newLines++; } // ignore the new line at the start of the buffer newLines--; int findPos = qMin(line, endLine) + newLines; highlightResult(window, findPos); emit completed(true); return; } //clear the current block of text and move to the next one string.clear(); line = endLine; } while (startLine != endLine); // if no match was found, clear selection to indicate this window->clearSelection(); window->notifyOutputChanged(); } emit completed(false); } void SearchHistoryTask::highlightResult(ScreenWindowPtr window , int findPos) { //work out how many lines into the current block of text the search result was found //- looks a little painful, but it only has to be done once per search. ////qDebug() << "Found result at line " << findPos; //update display to show area of history containing selection if ((findPos < window->currentLine()) || (findPos >= (window->currentLine() + window->windowLines()))) { int centeredScrollPos = findPos - window->windowLines() / 2; if (centeredScrollPos < 0) { centeredScrollPos = 0; } window->scrollTo(centeredScrollPos); } window->setTrackOutput(false); window->notifyOutputChanged(); window->setCurrentResultLine(findPos); } SearchHistoryTask::SearchHistoryTask(QObject* parent) : SessionTask(parent) , _direction(Enum::BackwardsSearch) , _startLine(0) { } void SearchHistoryTask::setSearchDirection(Enum::SearchDirection direction) { _direction = direction; } void SearchHistoryTask::setStartLine(int line) { _startLine = line; } Enum::SearchDirection SearchHistoryTask::searchDirection() const { return _direction; } void SearchHistoryTask::setRegExp(const QRegularExpression &expression) { _regExp = expression; } QRegularExpression SearchHistoryTask::regExp() const { return _regExp; } QString SessionController::userTitle() const { if (!_session.isNull()) { return _session->userTitle(); } else { return QString(); } }