/*
    SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
    SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>
    SPDX-FileCopyrightText: 2009 Thomas Dreibholz <dreibh@iem.uni-due.de>
    SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

// Own
#include "Session.h"

// Standard
#include <csignal>
#include <cstdlib>

#ifndef Q_OS_WIN
#include <unistd.h>
#endif

// Qt
#include <QApplication>
#include <QByteArray>
#include <QColor>
#include <QDir>
#include <QFile>
#include <QKeyEvent>
#include <QRandomGenerator>
#include <QThread>

// KDE
#include <KActionCollection>
#include <KConfigGroup>
#include <KIO/DesktopExecParser>
#include <KLocalizedString>
#include <KNotification>
#include <KProcess>
#include <KSelectAction>
#include <KWindowSystem>

#ifndef Q_OS_WIN
#include <KPtyDevice>
#endif
#include <KShell>

// wayland window activation
#define HAVE_WAYLAND __has_include(<KWaylandExtras>)
#if HAVE_WAYLAND
#include <KWaylandExtras>
#endif

// Konsole
#if HAVE_DBUS
#include <sessionadaptor.h>
#endif

#include "KonsoleSettings.h"
#include "Pty.h"
#include "SSHProcessInfo.h"
#include "SessionController.h"
#include "SessionGroup.h"
#include "SessionManager.h"
#include "ShellCommand.h"
#include "Vt102Emulation.h"
#include "ZModemDialog.h"
#include "decoders/PlainTextDecoder.h"
#include "history/HistoryTypeFile.h"
#include "history/HistoryTypeNone.h"
#include "history/compact/CompactHistoryType.h"
#include "konsoledebug.h"
#include "profile/Profile.h"
#include "profile/ProfileManager.h"

#include "terminalDisplay/TerminalDisplay.h"
#include "terminalDisplay/TerminalScrollBar.h"

#ifndef Q_OS_WIN
// Linux
#if HAVE_GETPWUID
#include <pwd.h>
#include <sys/types.h>
#endif

#include <KSandbox>
#endif // Q_OS_WIN

using namespace Konsole;

static const int ZMODEM_BUFFER_SIZE = 1048576; // 1 Mb

// compute a securely random cookie used for activationToken
static QString computeRandomCookie()
{
    // get good random data
    quint32 array[8];
    QRandomGenerator::global()->fillRange(array);

    // convert to string usable for env var KONSOLE_DBUS_ACTIVATION_COOKIE
    return QString::fromUtf8(QByteArray(reinterpret_cast<const char *>(array), sizeof(array)).toBase64());
}

Session::Session(QObject *parent)
    : QObject(parent)
    , m_activationCookie(computeRandomCookie())
{
    _uniqueIdentifier = QUuid::createUuid();

    int maxSessionId = 0;
    auto allSessions = SessionManager::instance()->sessions();
    for (const auto &session : allSessions) {
        if (session->sessionId() > maxSessionId) {
            maxSessionId = session->sessionId();
        }
    }
    _sessionId = maxSessionId + 1;

#if HAVE_DBUS
    // prepare DBus communication
    new SessionAdaptor(this);
    QDBusConnection::sessionBus().registerObject(QLatin1String("/Sessions/") + QString::number(_sessionId), this);
#endif

    // create emulation backend
    _emulation = new Vt102Emulation();
    _emulation->reset();

    connect(_emulation, &Konsole::Emulation::sessionAttributeChanged, this, &Konsole::Session::setSessionAttribute);
    connect(_emulation, &Konsole::Emulation::bell, this, [this]() {
        Q_EMIT bellRequest(i18n("Bell in '%1' (Session '%2')", _displayTitle, _nameTitle));
        this->setPendingNotification(Notification::Bell);
    });
    connect(_emulation, &Konsole::Emulation::zmodemDownloadDetected, this, &Konsole::Session::fireZModemDownloadDetected);
    connect(_emulation, &Konsole::Emulation::zmodemUploadDetected, this, &Konsole::Session::fireZModemUploadDetected);
    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, true);

    // 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);
}

Session::~Session()
{
    delete _foregroundProcessInfo;
    delete _sessionProcessInfo;
    // kill process before emulation, e.g. QProcess::finished will use _emulation in some cases
    delete _shellProcess;
    delete _emulation;
    delete _zmodemProc;
}

void Session::openTeletype(int fd, bool runShell)
{
    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
#ifndef Q_OS_WIN
    connect(_shellProcess, &Konsole::Pty::finished, this, &Konsole::Session::done);
#else
    connect(_shellProcess, &Konsole::Pty::finished, this, &Konsole::Session::done);
#endif

    // emulator size
    connect(_emulation, &Konsole::Emulation::imageSizeChanged, this, &Konsole::Session::updateWindowSize);
    if (fd < 0 || runShell) {
        // Using a queued connection guarantees that starting the session
        // is delayed until all (both) image size updates at startup have
        // been processed. See #203185 and #412598.
        connect(_emulation, &Konsole::Emulation::imageSizeInitialized, this, &Konsole::Session::run, Qt::QueuedConnection);
    } else {
        // run needs to be disconnected, as it may be already connected by the constructor
        disconnect(_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 {
        /**
         * compute the windows id to use
         * doesn't call winId on some widget, as this might lead
         * to rendering artifacts as this will trigger the
         * creation of a native window, see https://doc.qt.io/qt-5/qwidget.html#winId
         * instead, use https://doc.qt.io/qt-5/qwidget.html#effectiveWinId
         */
        QWidget *widget = _views.first();
        Q_ASSERT(widget);
        return widget->effectiveWinId();
    }
}

void Session::setDarkBackground(bool darkBackground)
{
    _hasDarkBackground = darkBackground;
}

bool Session::isRunning() const
{
#ifdef Q_OS_WIN
    return (_shellProcess != nullptr) && _shellProcess->isRunning();
#else
    return (_shellProcess != nullptr) && (_shellProcess->state() == QProcess::Running);
#endif
}

bool Session::hasFocus() const
{
    return std::any_of(_views.constBegin(), _views.constEnd(), [](const TerminalDisplay *display) {
        return display->hasFocus();
    });
}

bool Session::setCodec(const QByteArray &name)
{
    if (isReadOnly() || !emulation()->setCodec(name)) {
        return false;
    }

    Q_EMIT sessionCodecChanged(codec());
    return true;
}

QByteArray Session::codec()
{
    return _emulation->encoder().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()
        && (_reportedWorkingUrl.host().length() == 0 || _reportedWorkingUrl.host().compare(QSysInfo::machineHostName(), Qt::CaseInsensitive) == 0)) {
        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;
        Q_EMIT currentDirectoryChanged(_currentWorkingDir);
    }
}

QList<TerminalDisplay *> 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);
    connect(widget, &Konsole::TerminalDisplay::peekPrimaryRequested, _emulation, &Konsole::Emulation::setPeekPrimary);

    // 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->scrollBar(), &Konsole::TerminalScrollBar::setAlternateScrolling);

    connect(_emulation, &Konsole::Emulation::programBracketedPasteModeChanged, widget, &Konsole::TerminalDisplay::setBracketedPasteMode);

    widget->setBracketedPasteMode(_emulation->programBracketedPasteMode());

    widget->setScreenWindow(_emulation->createWindow());

    _emulation->setCurrentTerminalDisplay(widget);

    // 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::compositeFocusChanged, _emulation, &Konsole::Emulation::focusChanged);

    connect(_emulation, &Konsole::Emulation::setCursorStyleRequest, widget, &Konsole::TerminalDisplay::setCursorStyle);
    connect(_emulation, &Konsole::Emulation::resetCursorStyleRequest, widget, &Konsole::TerminalDisplay::resetCursorStyle);

    connect(widget, &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::Session::resetNotifications);
}

void Session::viewDestroyed(QObject *view)
{
    auto *display = reinterpret_cast<TerminalDisplay *>(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();
    }

#ifndef Q_OS_WIN
    if (KSandbox::isFlatpak()) {
        QProcess proc;
        // run "test -x exec" on the host to see if the shell is executable
        proc.setProgram(QStringLiteral("test"));
        proc.setArguments(QStringList{QStringLiteral("-x"), exec});
        KSandbox::startHostProcess(proc, QProcess::ReadOnly);
        if (proc.waitForStarted() && proc.waitForFinished(-1)) {
            return proc.exitCode() == 0 ? exec : QString();
        }
        return {};
    }
#endif // Q_OS_WIN

    QFileInfo info(exec);
    if (info.isAbsolute() && info.exists() && info.isExecutable()) {
        return exec;
    }

    exec = KIO::DesktopExecParser::executablePath(exec);
    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;
}

static QStringList postProcessArgs(const QStringList &contextArgs, const QStringList &args)
{
#ifndef Q_OS_WIN
    if (!KSandbox::isFlatpak()) {
        return contextArgs + args;
    }
    QStringList arguments;
    // last arg is the program
    arguments = contextArgs;
    QString program = arguments.back();
    arguments.pop_back();
    // Needs to be explicitly specified, KSandbox::makeHostContext
    // ignores any variables set in the system environment so this
    // never gets set.
    arguments.push_back(QStringLiteral("--env=TERM=xterm-256color"));
    arguments.push_back(program);
    arguments << args;
    return arguments;
#else
    qCritical() << "Should never get called on windows";
    return {};
#endif
}

void Session::run()
{
    // FIXME: run() is called twice in some instances
    if (isRunning()) {
        qCDebug(KonsoleDebug) << "Attempted to re-run an already running session (" << processId() << ")";
        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();
    }

    QStringList programs = {_program, QString::fromUtf8(qgetenv("SHELL")), QStringLiteral("/bin/sh")};

#if HAVE_GETPWUID
    auto pw = getpwuid(getuid());
    // pw: Do not pass the returned pointer to free.
    if (pw != nullptr) {
        if (KSandbox::isFlatpak()) {
            QProcess proc;
            proc.setProgram(QStringLiteral("getent"));
            proc.setArguments({QStringLiteral("passwd"), QString::number(pw->pw_uid)});
            KSandbox::startHostProcess(proc);
            proc.waitForFinished();
            const auto shell = proc.readAllStandardOutput().simplified().split(':').at(6);
            programs.insert(1, QString::fromUtf8(shell));
        } else {
            programs.insert(1, QString::fromLocal8Bit(pw->pw_shell));
        }
    }
#endif

    QString exec;
    for (const auto &choice : programs) {
        exec = checkProgram(choice);
        if (!exec.isEmpty()) {
            break;
        }
    }

    // if nothing could be found (not even the fallbacks), print a warning and do not run
    if (exec.isEmpty()) {
        terminalWarning(i18n("Could not find an interactive shell to start."));
        return;
    }

    // if a program was specified via setProgram(), but it couldn't be found (but a fallback was), print a warning
    if (exec != checkProgram(_program)) {
        terminalWarning(i18n("Could not find '%1', starting '%2' instead.  Please check your profile settings.", _program, exec));
        _arguments.clear(); // ignore args if program is invalid
    } else if (exec != checkProgram(exec)) {
        terminalWarning(i18n("Could not find '%1', starting '%2' instead.  Please check your profile settings.", exec, checkProgram(exec)));
        _arguments.clear(); // ignore args if program is invalid
    }

    // if no arguments are specified, fall back to program name
    QStringList arguments = _arguments.join(QLatin1Char(' ')).isEmpty() ? QStringList() << exec : _arguments;

    // For historical reasons, the first argument in _arguments is the
    // name of the program to execute, remove it in favor of the actual program name
    Q_ASSERT(arguments.count() >= 1);
    arguments = arguments.mid(1);

    if (!_initialWorkingDir.isEmpty()) {
        _shellProcess->setInitialWorkingDirectory(_initialWorkingDir);
    } else {
        _shellProcess->setInitialWorkingDirectory(QDir::currentPath());
    }

    _shellProcess->setFlowControlEnabled(_flowControlEnabled);
    _shellProcess->setEraseChar(_emulation->eraseChar());
#ifndef Q_OS_WIN
    _shellProcess->setUseUtmp(_addToUtmp);
#endif

#ifndef Q_OS_WIN
    if (KSandbox::isFlatpak()) {
        _shellProcess->pty()->setCTtyEnabled(false); // not possibly inside sandbox
    }
#endif

    // 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())));

    // env vars we shall not expose e.g. over dbus
    QStringList secretEnv;

#if HAVE_DBUS
    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));

    // secret cookie to trigger activationToken via dbus
    secretEnv << QStringLiteral("KONSOLE_DBUS_ACTIVATION_COOKIE=%1").arg(m_activationCookie);
#endif

    // stuff set via addEnvironmentEntry and the secret parts
    const QStringList fullEnv = _environment + secretEnv;

#ifndef Q_OS_WIN
    const auto originalEnvironment = _shellProcess->environment();
    _shellProcess->setProgram(exec);
    _shellProcess->setEnvironment(originalEnvironment + fullEnv);
    const auto context = KSandbox::makeHostContext(*_shellProcess);
    arguments = postProcessArgs(context.arguments, arguments);
    _shellProcess->setEnvironment(originalEnvironment);
    const auto result = _shellProcess->start(context.program, arguments, fullEnv);
#else // Q_OS_WIN
    const auto size = _emulation->imageSize();
    const int lines = size.height();
    const int cols = size.width();
    int result = _shellProcess->start(exec, arguments, _initialWorkingDir.isEmpty() ? QDir::currentPath() : _initialWorkingDir, fullEnv, cols, lines);
#endif

    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.

    Q_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) {
                Q_EMIT changeForegroundColorRequest(color);
            } else {
                Q_EMIT changeBackgroundColorRequest(color);
            }
        }
    }

    if (what == SessionName) {
        if (_localTabTitleFormat != caption) {
            _localTabTitleFormat = caption;
            setTitle(Session::DisplayedTitleRole, caption);
            modified = true;
        }
    }

    /* 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 == SessionColor) {
        QString colorString = caption.section(QLatin1Char(';'), 0, 0);
        QColor color = QColor(colorString);
        if (color.isValid()) {
            setColor(color);
            tabColorSetByUser(true);
        }
    }

    if (what == CurrentDirectory) {
        _reportedWorkingUrl = QUrl::fromUserInput(caption);
        Q_EMIT currentDirectoryChanged(currentWorkingDirectory());
        modified = true;
    }

    if (what == ProfileChange) {
        Q_EMIT profileChangeCommandReceived(caption);
        return;
    }

    if (what == ResetTextColor || what == ResetBackgroundColor) {
        QColor color;
        if (what == ResetTextColor) {
            Q_EMIT changeForegroundColorRequest(color);
        } else {
            Q_EMIT changeBackgroundColorRequest(color);
        }
    }

    if (modified) {
        Q_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::tabColorSetByUser(bool set)
{
    _tabColorSetByUser = set;
}

bool Session::isTabColorSetByUser() const
{
    return _tabColorSetByUser;
}

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) {
        setPendingNotification(Notification::Silence, false);
        return;
    }

    TerminalDisplay *view = nullptr;
    if (!_views.isEmpty()) {
        view = _views.first();
    }

    KNotification *notification =
        new KNotification(hasFocus() ? QStringLiteral("Silence") : QStringLiteral("SilenceHidden"), KNotification::CloseWhenWindowActivated);
    notification->setWindow(view->windowHandle());

    notification->setText(i18n("Silence in '%1' (Session '%2')", _displayTitle, _nameTitle));
    auto action = notification->addDefaultAction(i18n("Show session"));
    connect(action, &KNotificationAction::activated, this, [view, notification]() {
        view->notificationClicked(notification->xdgActivationToken());
    });
    if (view->sessionController()->isMonitorOnce()) {
        view->sessionController()->actionCollection()->action(QStringLiteral("monitor-silence"))->setChecked(false);
    }
    setPendingNotification(Notification::Silence);
}

void Session::activityTimerDone()
{
    _notifiedActivity = false;
}

void Session::resetNotifications()
{
    static const Notification availableNotifications[] = {Activity, Silence, Bell};
    for (auto notification : availableNotifications) {
        setPendingNotification(notification, false);
    }
}

void Session::updateFlowControlState(bool suspended)
{
    if (suspended) {
        if (flowControlEnabled()) {
            for (TerminalDisplay *display : std::as_const(_views)) {
                if (display->flowControlWarningEnabled()) {
                    display->outputSuspended(true);
                }
            }
        }
    } else {
        for (TerminalDisplay *display : std::as_const(_views)) {
            display->outputSuspended(false);
        }
    }
}

void Session::onPrimaryScreenInUse(bool use)
{
    _isPrimaryScreen = use;
    Q_EMIT primaryScreenInUse(use);
}

bool Session::isPrimaryScreen()
{
    return _isPrimaryScreen;
}

void Session::sessionAttributeRequest(int id, uint terminator)
{
    switch (id) {
    case TextColor:
        // Get 'TerminalDisplay' (_view) foreground color
        Q_EMIT getForegroundColor(terminator);
        break;
    case BackgroundColor:
        // Get 'TerminalDisplay' (_view) background color
        Q_EMIT getBackgroundColor(terminator);
        break;
    }
}

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
    for (TerminalDisplay *view : std::as_const(_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);

    int width = 0;
    int height = 0;
    if (!_views.isEmpty()) {
        // This is somewhat arbitrary. Views having potentially different font sizes is
        // irreconcilable with the PTY user having accurate knowledge of the geometry.
        QSize cr = _views.at(0)->contentRect().size();
        width = cr.width();
        height = cr.height();
    }
    _shellProcess->setWindowSize(columns, lines, width, height);
}
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();
    const QSize existingPxSize = _shellProcess->pixelSize();
    _shellProcess->setWindowSize(existingSize.width() + 1, existingSize.height(), existingPxSize.width() + 1, existingPxSize.height());
    // introduce small delay to avoid changing size too quickly
    QThread::usleep(500);
    _shellProcess->setWindowSize(existingSize.width(), existingSize.height(), existingPxSize.width(), existingPxSize.height());
}

void Session::sendSignal(int signal)
{
#ifndef Q_OS_WIN
    const ProcessInfo *process = getProcessInfo();
    bool ok = false;
    int pid;
    pid = process->foregroundPid(&ok);

    if (ok) {
        ::kill(pid, signal);
    } else {
        qWarning() << "foreground process id not set, unable to send signal " << signal;
    }
#else
    // FIXME: Can we do this on windows?
#endif
}

void Session::reportColor(SessionAttributes r, const QColor &c, uint terminator)
{
#define to65k(a) (QStringLiteral("%1").arg(int(((a) * 0xFFFF)), 4, 16, QLatin1Char('0')))
    QString msg = QStringLiteral("\033]%1;rgb:").arg(r) + to65k(c.redF()) + QLatin1Char('/') + to65k(c.greenF()) + QLatin1Char('/') + to65k(c.blueF());

    // Match termination of OSC reply to termination of OSC request.
    if (terminator == '\a') { // non standard BEL terminator
        msg += QLatin1Char('\a');
    } else { // standard 7-bit ST terminator
        msg += QStringLiteral("\033\\");
    }
    _emulation->sendString(msg.toUtf8());
#undef to65k
}

void Session::reportForegroundColor(const QColor &c, uint terminator)
{
    reportColor(SessionAttributes::TextColor, c, terminator);
}

void Session::reportBackgroundColor(const QColor &c, uint terminator)
{
    reportColor(SessionAttributes::BackgroundColor, c, terminator);
}

bool Session::kill(int signal)
{
#ifndef Q_OS_WIN
    const int pid = KSandbox::isFlatpak() ? _shellProcess->flatpakSpawnProcessId() : processId();
    if (pid <= 0) {
        return false;
    }

    int result = ::kill(pid, signal);

    if (result == 0) {
        return _shellProcess->waitForFinished(1000);
    } else {
        return false;
    }
#else
    return false;
#endif
}

void Session::close()
{
    if (isRunning()) {
        if (!closeInNormalWay()) {
            closeInForceWay();
        }
    } else {
        // terminal process has finished, just close the session
        QTimer::singleShot(1, this, [this]() {
            Q_EMIT finished(this);
        });
    }
}

bool Session::closeInNormalWay()
{
#ifdef Q_OS_WIN
    _shellProcess->closePty();
    return true;
#else
    _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()) {
        Q_EMIT finished(this);
        return true;
    }

    // try SIGHUP, afterwards do hard kill
    // this is the sequence used by most other terminal emulators like xterm, gnome-terminal, ...
    // see bug 401898 for details about tries to have some "soft-terminate" via EOF character
    if (kill(SIGHUP)) {
        return true;
    }

    qWarning() << "Process " << processId() << " did not die with SIGHUP";
    _shellProcess->closePty();
    return (_shellProcess->waitForFinished(1000));
#endif
}

bool Session::closeInForceWay()
{
    _autoClose = true;
    _closePerUserRequest = true;

#ifdef Q_OS_WIN
    return _shellProcess->kill();
#else
    if (kill(SIGKILL)) {
        return true;
    } else {
        qWarning() << "Process " << processId() << " did not die with SIGKILL";
        return false;
    }
#endif
}

void Session::sendTextToTerminal(const QString &text, const QChar &eol) const
{
    if (isReadOnly()) {
        return;
    }

    if (eol.isNull()) {
        _emulation->sendText(text);
    } else {
        _emulation->sendText(text + eol);
    }
}

// Only D-Bus calls this function (via SendText or runCommand)
void Session::sendText(const QString &text) const
{
    // avoid that we expose this if not wanted by the user
    if (isCalledViaDbusAndForbidden() || isReadOnly()) {
        return;
    }

    _emulation->sendText(text);
}

// Only D-Bus calls this function
void Session::runCommand(const QString &command) const
{
    // avoid that we expose this if not wanted by the user
    if (isCalledViaDbusAndForbidden() || isReadOnly()) {
        return;
    }

    sendText(command + QLatin1Char('\n'));
}

void Session::sendMouseEvent(int buttons, int column, int line, int eventType)
{
    // avoid that we expose this if not wanted by the user
    if (isCalledViaDbusAndForbidden() || isReadOnly()) {
        return;
    }

    _emulation->sendMouseEvent(buttons, column, line, eventType);
}

void Session::done(int exitCode, QProcess::ExitStatus exitStatus)
{
#ifndef Q_OS_WIN
    // This slot should be triggered only one time
    disconnect(_shellProcess, &Konsole::Pty::finished, this, &Konsole::Session::done);
#else
    disconnect(_shellProcess, &Konsole::Pty::finished, this, &Konsole::Session::done);
#endif

    if (!_autoClose) {
        _userTitle = i18nc("@info:shell This session is done", "Finished");
        Q_EMIT sessionAttributeChanged();
        return;
    }

    if (_closePerUserRequest) {
        Q_EMIT finished(this);
        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 *notification = new KNotification(QStringLiteral("Finished"), KNotification::CloseWhenWindowActivated);
        if (QApplication::activeWindow()) {
            notification->setWindow(QApplication::activeWindow()->windowHandle());
        }
        notification->setText(message);
        notification->sendEvent();
    }

    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 {
        Q_EMIT finished(this);
    }
}

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)
{
    // avoid that we expose this if not wanted by the user
    if (isCalledViaDbusAndForbidden() || 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;
        }

        Q_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;

        int sessionPid = processId();
#ifndef Q_OS_WIN
        _sessionProcessInfo = ProcessInfo::newInstance(sessionPid, -1, _shellProcess->pty()->ttyName());
#else
        _sessionProcessInfo = ProcessInfo::newInstance(sessionPid, -1);
#endif

        _sessionProcessInfo->setUserHomeDir();
    }
    _sessionProcessInfo->update();
}

bool Session::updateForegroundProcessInfo()
{
    Q_ASSERT(_shellProcess);

    const int foregroundPid = _shellProcess->foregroundProcessGroup();
    if (foregroundPid != _foregroundPid) {
        delete _foregroundProcessInfo;
#ifndef Q_OS_WIN
        _foregroundProcessInfo = ProcessInfo::newInstance(foregroundPid, processId(), _shellProcess->pty()->ttyName());
#else
        _foregroundProcessInfo = ProcessInfo::newInstance(foregroundPid, processId());
#endif
        _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();
    std::unique_ptr<SSHProcessInfo> sshProcess;

    // format tab titles using process info
    bool ok = false;
    if (process->name(&ok) == QLatin1String("ssh") && ok) {
        process->refreshArguments();
        sshProcess = std::make_unique<SSHProcessInfo>(*process);
    }

    QString currHostName = sshProcess ? sshProcess->host() : process->localHost();

    if (_currentHostName != currHostName) {
        _currentHostName = currHostName;
        Q_EMIT hostnameChanged(currHostName);
    }

    if (sshProcess) {
        QString title = tabTitleFormat(Session::RemoteTabTitle);
        title.replace(QLatin1String("%w"), userTitle());
        title.replace(QLatin1String("%#"), QString::number(sessionId()));
        return sshProcess->format(title);
    }

    /*
     * 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.
     * <br>
     * The markers recognized are:
     * <ul>
     * <li> %B - User's Bourne prompt sigil ($, or # for superuser). </li>
     * <li> %u - Name of the user which owns the process. </li>
     * <li> %n - Replaced with the name of the process.   </li>
     * <li> %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)
     * </li>
     * <li> %D - Replaced with the current working directory of the process. </li>
     * <li> %h - Replaced with the local host name. <li>
     * <li> %w - Replaced with the window title set by the shell. </li>
     * <li> %# - Replaced with the number of the session. <li>
     * </ul>
     */
    QString title = tabTitleFormat(Session::LocalTabTitle);
    // search for and replace known marker

    QString dir = _reportedWorkingUrl.toLocalFile();
    bool dirOk = true;
    if (dir.isEmpty()) {
        // update current directory from process
        updateWorkingDirectory();
        // Previous process may have been freed in updateSessionProcessInfo()
        process = getProcessInfo();
        dir = process->currentDir(&dirOk);
    }

    int pos = 0;
    while ((pos = title.indexOf(QLatin1Char('%'), pos)) != -1) {
        if (pos >= title.size() - 1) {
            break;
        }

        switch (title.at(pos + 1).toLatin1()) {
        case 'B': {
            int UID = process->userId(&ok);
            if (!ok) {
                title.replace(pos, 2, QStringLiteral("-"));
                pos++;
            } else {
                // title.replace(QLatin1String("%I"), QString::number(UID));
                if (UID == 0) {
                    title.replace(pos, 2, QStringLiteral("#"));
                    pos++;
                } else {
                    title.replace(pos, 2, QStringLiteral("$"));
                    pos++;
                }
            }
        } break;
        case 'u': {
            QString replacement = process->userName();
            title.replace(pos, 2, replacement);
            pos += replacement.size();
        } break;
        case 'h': {
            QString replacement = Konsole::ProcessInfo::localHost();
            title.replace(pos, 2, replacement);
            pos += replacement.size();
        } break;
        case 'n': {
            QString replacement = process->name(&ok);
            title.replace(pos, 2, replacement);
            pos += replacement.size();
        } break;
        case 'w': {
            QString replacement = userTitle();
            title.replace(pos, 2, replacement);
            pos += replacement.size();
        } break;
        case '#': {
            QString replacement = QString::number(sessionId());
            title.replace(pos, 2, replacement);
            pos += replacement.size();
        } break;
        case 'd':
            if (!dirOk) {
                title.replace(pos, 2, QStringLiteral("-"));
                pos++;
            } else {
                // allow for shortname to have the ~ as homeDir
                const QString homeDir = process->userHomeDir();
                if (!homeDir.isEmpty()) {
                    if (dir.startsWith(homeDir)) {
                        dir.remove(0, homeDir.length());
                        dir.prepend(QLatin1Char('~'));
                    }
                }
                const QString replacement = process->formatShortDir(dir);
                title.replace(pos, 2, replacement);
                pos += replacement.size();
            }
            break;
        case 'D':
            if (!dirOk) {
                title.replace(pos, 2, QStringLiteral("-"));
                pos++;
            } else {
                // allow for shortname to have the ~ as homeDir
                const QString homeDir = process->userHomeDir();
                if (!homeDir.isEmpty()) {
                    if (dir.startsWith(homeDir)) {
                        dir.remove(0, homeDir.length());
                        dir.prepend(QLatin1Char('~'));
                    }
                }
                title.replace(pos, 2, dir);
                pos += dir.size();
            }
            break;
        default:
            pos++;
        }
    }

    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() && updateForegroundProcessInfo() && _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;
        Q_EMIT sessionAttributeChanged();
    }
}

void Session::setIconText(const QString &iconText)
{
    _iconText = iconText;
}

QString Session::iconName() const
{
    return _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::isMonitorPrompt() const
{
    return _monitorPrompt;
}
bool Session::isMonitorActivity() const
{
    return _monitorActivity;
}
bool Session::isMonitorSilence() const
{
    return _monitorSilence;
}

void Session::setMonitorPrompt(bool monitor)
{
    if (_monitorPrompt == monitor) {
        return;
    }
    _monitorPrompt = monitor;
}

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();

    setPendingNotification(Notification::Activity, false);
}

void Session::setMonitorSilence(bool monitor)
{
    if (_monitorSilence == monitor) {
        return;
    }

    _monitorSilence = monitor;
    if (_monitorSilence) {
        _silenceTimer->start(_silenceSeconds * 1000);
    } else {
        _silenceTimer->stop();
    }

    setPendingNotification(Notification::Silence, false);
}

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);
    }

    Q_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, &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.isEmpty()) {
        _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)
{
    handleActivity();
    _emulation->receiveData(buf, len);
}

QSize Session::size()
{
    return _emulation->imageSize();
}

void Session::setSize(const QSize &size)
{
    if ((size.width() <= 1) || (size.height() <= 1)) {
        return;
    }

    Q_EMIT resizeRequest(size);
}

QSize Session::preferredSize() const
{
    return _preferredSize;
}

void Session::setPreferredSize(const QSize &size)
{
    _preferredSize = size;
}

int Session::processId() const
{
    return _shellProcess->shellProcessId();
}

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;
    }
}

QString Session::profile()
{
    return SessionManager::instance()->sessionProfile(this)->name();
}

void Session::setProfile(const QString &profileName)
{
    const QList<Profile::Ptr> profiles = ProfileManager::instance()->allProfiles();
    for (const Profile::Ptr &profile : profiles) {
        if (profile->name() == profileName) {
            SessionManager::instance()->setSessionProfile(this, profile);
        }
    }
}

bool Session::copyInputToAllSessions()
{
    if (auto c = controller()) {
        c->copyInputActions()->setCurrentItem(SessionController::CopyInputToAllTabsMode);
        c->copyInputToAllTabs();
        return true;
    }

    return false;
}

bool Session::copyInputToSessions(QList<int> sessionIds)
{
    if (auto c = controller()) {
        auto sessions = new QList<Session *>();
        c->copyInputActions()->setCurrentItem(SessionController::CopyInputToSelectedTabsMode);

        for (auto sessionId : sessionIds) {
            if (auto session = SessionManager::instance()->idToSession(sessionId))
                sessions->append(session);
            else
                return false;
        }

        c->copyInputToSelectedTabs(sessions);
        return true;
    }

    return false;
}

bool Session::copyInputToNone()
{
    if (auto c = controller()) {
        c->copyInputActions()->setCurrentItem(SessionController::CopyInputToNoneMode);
        c->copyInputToNone();
        return true;
    } else
        return false;
}

QList<int> Session::copyingSessions()
{
    if (auto c = controller()) {
        if (auto copyToGroup = c->copyToGroup()) {
            QList<int> sessionIds;

            const auto sessions = copyToGroup->sessions();
            for (auto session : sessions) {
                sessionIds.append(session->sessionId());
            }

            sessionIds.removeAll(sessionId());
            return sessionIds;
        }
    }

    return QList<int>();
}

QList<int> Session::feederSessions()
{
    QList<int> feeders;

    for (auto session : SessionManager::instance()->sessions()) {
        if (session->copyingSessions().contains(sessionId()) && !feeders.contains(session->sessionId())) {
            feeders.append(session->sessionId());
        }
    }

    feeders.removeAll(sessionId());
    return feeders;
}

QString Session::getAllDisplayedText(bool removeTrailingEmptyLines)
{
    return getAllDisplayedTextList(removeTrailingEmptyLines).join(QLatin1Char('\n'));
}

QStringList Session::getAllDisplayedTextList(bool removeTrailingEmptyLines)
{
    auto screenWindow = _views.at(0)->screenWindow();
    if (removeTrailingEmptyLines) {
        auto lineproperties = screenWindow->getLineProperties();
        int lastNonemptyLine = screenWindow->windowLines();

        for (int i = lineproperties.size() - 1; i >= 0; --i) {
            if (lineproperties[i].length > 0) {
                lastNonemptyLine = i;
                break;
            }
        }

        return getDisplayedTextList(0, lastNonemptyLine);
    } else {
        return getDisplayedTextList(0, screenWindow->windowLines() - 1);
    }
}

QString Session::getDisplayedText(int startLineOffset, int endLineOffset)
{
    return getDisplayedTextList(startLineOffset, endLineOffset).join(QLatin1Char('\n'));
}

QStringList Session::getDisplayedTextList(int startLineOffset, int endLineOffset)
{
    auto screenWindow = _views.at(0)->screenWindow();

    if (startLineOffset < 0 || endLineOffset >= screenWindow->windowLines() || startLineOffset > endLineOffset) {
        return QStringList();
    }

    QStringList list;
    QTextStream stream;
    PlainTextDecoder decoder;
    int screenTopLineIndex = screenWindow->currentLine();
    int startLine = startLineOffset + screenTopLineIndex;
    int endLine = endLineOffset + screenTopLineIndex;

    for (int currLine = startLine; currLine <= endLine; ++currLine) {
        QString lineContent;
        stream.setString(&lineContent, QIODevice::ReadWrite);
        decoder.begin(&stream);
        screenWindow->screen()->writeLinesToStream(&decoder, currLine, currLine);
        decoder.end();

        if (lineContent.back() == QLatin1Char('\n')) {
            lineContent.removeLast();
        }

        list.append(lineContent);
    }

    return list;
}

int Session::foregroundProcessId()
{
    int pid;

    bool ok = false;
    pid = getProcessInfo()->pid(&ok);
    if (!ok) {
        pid = -1;
    }

    return pid;
}

bool Session::isForegroundProcessActive()
{
    const auto pid = processId();
    const auto fgid = _shellProcess->foregroundProcessGroup();

    // On FreeBSD, after exiting the shell, the foreground GID is
    // an invalid value, and the "shell" PID is 0. Those are not equal,
    // so the check below would return true.
    if (pid == 0) {
        return false;
    }

    // This check is wrong when Konsole is started with '-e cmd'
    // as there will only be one process.
    // See BKO 134581 for no popup when closing session
    return (pid != fgid);
}

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("TabColor", color().isValid() ? color().name(QColor::HexArgb) : QString());
    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("TabColor");
    if (!value.isEmpty()) {
        setColor(QColor(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 the directory does not exist, is not a directory, or is not accessible,
    // use the home directory as a fallback.
    if (!fi.exists() || !fi.isDir() || !fi.isReadable() || !fi.isExecutable()) {
        validDir = QDir::homePath();
    }

    return validDir;
}

void Session::setPendingNotification(Session::Notification notification, bool enable)
{
    if (enable != _activeNotifications.testFlag(notification)) {
        _activeNotifications.setFlag(notification, enable);
        Q_EMIT notificationsChanged(notification, enable);
    }
}

void Session::handleActivity()
{
    // TODO: should this hardcoded interval be user configurable?
    const int activityMaskInSeconds = 15;

    TerminalDisplay *view = nullptr;
    if (!_views.isEmpty()) {
        view = _views.first();
    }

    if (_monitorActivity && !_notifiedActivity) {
        KNotification *notification =
            new KNotification(hasFocus() ? QStringLiteral("Activity") : QStringLiteral("ActivityHidden"), KNotification::CloseWhenWindowActivated);
        notification->setWindow(view->windowHandle());
        notification->setText(i18n("Activity in '%1' (Session '%2')", _displayTitle, _nameTitle));
        auto action = notification->addDefaultAction(i18n("Show session"));
        connect(action, &KNotificationAction::activated, this, [view, notification]() {
            view->notificationClicked(notification->xdgActivationToken());
        });
        notification->sendEvent();

        if (view->sessionController()->isMonitorOnce()) {
            view->sessionController()->actionCollection()->action(QStringLiteral("monitor-activity"))->setChecked(false);
        }

        // 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 (_monitorActivity) {
        setPendingNotification(Notification::Activity);
    }
}

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.
        Q_EMIT readOnlyChanged();
    }
}
bool Session::getSelectMode() const
{
    return _selectMode;
}

void Session::setSelectMode(bool mode)
{
    if (_selectMode != mode) {
        _selectMode = mode;
    }
}

void Session::setColor(const QColor &color)
{
    if (_tabColor == color) {
        return;
    }

    _tabColor = color;
    Q_EMIT sessionAttributeChanged();
}

QColor Session::color() const
{
    return _tabColor;
}

SessionController *Session::controller()
{
    if (!_views.isEmpty())
        return _views.first()->sessionController();

    return nullptr;
}

// Only called during LoadLayout
void Session::runCommandFromLayout(const QString &command) const
{
    if (isReadOnly()) {
        return;
    }

    _emulation->sendText(command + QLatin1Char('\n'));
}

QString Session::activationToken(const QString &cookieForRequest) const
{
    // safety check, only work if the caller knows our id
    // they will read it from the SHELL_SESSION_ID env var inside this session
    if (cookieForRequest != m_activationCookie) {
        return {};
    }

#if HAVE_DBUS && HAVE_WAYLAND
    // no window active, no token
    // same if we don't run wayland
    const auto window = qApp->activeWindow();
    if (!window || !window->window() || !KWindowSystem::isPlatformWayland()) {
        return {};
    }

    // we will respond delayed, as the token needs to arrive
    Q_ASSERT(calledFromDBus());
    const auto msg = message();
    setDelayedReply(true);

    // we need to filter the response with the request serial
    const int launchedSerial = KWaylandExtras::self()->lastInputSerial(window->window()->windowHandle());
    connect(
        KWaylandExtras::self(),
        &KWaylandExtras::xdgActivationTokenArrived,
        this,
        [msg, launchedSerial](int tokenSerial, QString token) {
            // if wrong token, ignore it, but we must always reply to not stall the caller
            // we use here a SingleShotConnection, we will just be called once!
            if (tokenSerial != launchedSerial) {
                token.clear();
            }
            auto reply = msg.createReply(token);
            QDBusConnection::sessionBus().send(reply);
        },
        Qt::SingleShotConnection);

    KWaylandExtras::requestXdgActivationToken(window->window()->windowHandle(), launchedSerial, {});

#endif

    return {};
}

bool Session::isCalledViaDbusAndForbidden() const
{
#if HAVE_DBUS
    // we can check if this is called via dbus and then consult the config
    if (calledFromDBus() && !Konsole::KonsoleSettings::enableSecuritySensitiveDBusAPI()) {
        // trigger error reply
        setDelayedReply(true);
        auto reply = message().createErrorReply(QDBusError::AccessDenied, i18n("Security sensitive DBus API is disabled in the settings."));
        QDBusConnection::sessionBus().send(reply);

        // forbid call
        return true;
    }
    return false;
#else
    // no dbus, just allow it
    return false;
#endif
}

#include "moc_Session.cpp"
