diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 34b24ff78c7..04f475afb7c 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -170,6 +170,7 @@ add_qtc_library(Utils temporarydirectory.cpp temporarydirectory.h temporaryfile.cpp temporaryfile.h terminalcommand.cpp terminalcommand.h + terminalhooks.cpp terminalhooks.h terminalprocess.cpp terminalprocess_p.h textfieldcheckbox.cpp textfieldcheckbox.h textfieldcombobox.cpp textfieldcombobox.h diff --git a/src/libs/utils/aspects.cpp b/src/libs/utils/aspects.cpp index 72e667d76a6..51be951a8fe 100644 --- a/src/libs/utils/aspects.cpp +++ b/src/libs/utils/aspects.cpp @@ -9,6 +9,7 @@ #include "layoutbuilder.h" #include "pathchooser.h" #include "qtcassert.h" +#include "qtcolorbutton.h" #include "qtcsettings.h" #include "utilstr.h" #include "variablechooser.h" @@ -579,6 +580,12 @@ public: QPointer m_groupBox; // For BoolAspects handling GroupBox check boxes }; +class ColorAspectPrivate +{ +public: + QPointer m_colorButton; // Owned by configuration widget +}; + class SelectionAspectPrivate { public: @@ -1287,6 +1294,70 @@ void StringAspect::makeCheckable(CheckBoxPlacement checkBoxPlacement, update(); } +/*! + \class Utils::ColorAspect + \inmodule QtCreator + + \brief A color aspect is a color property of some object, together with + a description of its behavior for common operations like visualizing or + persisting. + + The color aspect is displayed using a QtColorButton. +*/ + +ColorAspect::ColorAspect(const QString &settingsKey) + : d(new Internal::ColorAspectPrivate) +{ + setDefaultValue(QColor::fromRgb(0, 0, 0)); + setSettingsKey(settingsKey); + setSpan(1, 1); + + addDataExtractor(this, &ColorAspect::value, &Data::value); +} + +ColorAspect::~ColorAspect() = default; + +void ColorAspect::addToLayout(Layouting::LayoutBuilder &builder) +{ + QTC_CHECK(!d->m_colorButton); + d->m_colorButton = createSubWidget(); + builder.addItem(d->m_colorButton.data()); + d->m_colorButton->setColor(value()); + if (isAutoApply()) { + connect(d->m_colorButton.data(), + &QtColorButton::colorChanged, + this, + [this](const QColor &color) { setValue(color); }); + } +} + +QColor ColorAspect::value() const +{ + return BaseAspect::value().value(); +} + +void ColorAspect::setValue(const QColor &value) +{ + if (BaseAspect::setValueQuietly(value)) + emit changed(); +} + +QVariant ColorAspect::volatileValue() const +{ + QTC_CHECK(!isAutoApply()); + if (d->m_colorButton) + return d->m_colorButton->color(); + QTC_CHECK(false); + return {}; +} + +void ColorAspect::setVolatileValue(const QVariant &val) +{ + QTC_CHECK(!isAutoApply()); + if (d->m_colorButton) + d->m_colorButton->setColor(val.value()); +} + /*! \class Utils::BoolAspect \inmodule QtCreator diff --git a/src/libs/utils/aspects.h b/src/libs/utils/aspects.h index 413a17e6245..1aaaca3f299 100644 --- a/src/libs/utils/aspects.h +++ b/src/libs/utils/aspects.h @@ -31,6 +31,7 @@ namespace Internal { class AspectContainerPrivate; class BaseAspectPrivate; class BoolAspectPrivate; +class ColorAspectPrivate; class DoubleAspectPrivate; class IntegerAspectPrivate; class MultiSelectionAspectPrivate; @@ -245,6 +246,31 @@ private: std::unique_ptr d; }; +class QTCREATOR_UTILS_EXPORT ColorAspect : public BaseAspect +{ + Q_OBJECT + +public: + explicit ColorAspect(const QString &settingsKey = QString()); + ~ColorAspect() override; + + struct Data : BaseAspect::Data + { + QColor value; + }; + + void addToLayout(Layouting::LayoutBuilder &builder) override; + + QColor value() const; + void setValue(const QColor &val); + + QVariant volatileValue() const override; + void setVolatileValue(const QVariant &val) override; + +private: + std::unique_ptr d; +}; + class QTCREATOR_UTILS_EXPORT SelectionAspect : public BaseAspect { Q_OBJECT diff --git a/src/libs/utils/qtcprocess.cpp b/src/libs/utils/qtcprocess.cpp index 74bd28560b3..096f6445617 100644 --- a/src/libs/utils/qtcprocess.cpp +++ b/src/libs/utils/qtcprocess.cpp @@ -12,6 +12,7 @@ #include "processreaper.h" #include "processutils.h" #include "stringutils.h" +#include "terminalhooks.h" #include "terminalprocess_p.h" #include "threadutils.h" #include "utilstr.h" @@ -630,7 +631,7 @@ public: ProcessInterface *createProcessInterface() { if (m_setup.m_terminalMode != TerminalMode::Off) - return new TerminalImpl(); + return Terminal::Hooks::instance().createTerminalProcessInterfaceHook()(); const ProcessImpl impl = m_setup.m_processImpl == ProcessImpl::Default ? defaultProcessImpl() : m_setup.m_processImpl; diff --git a/src/libs/utils/terminalhooks.cpp b/src/libs/utils/terminalhooks.cpp new file mode 100644 index 00000000000..d0b48075683 --- /dev/null +++ b/src/libs/utils/terminalhooks.cpp @@ -0,0 +1,48 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "terminalhooks.h" + +#include "filepath.h" +#include "terminalprocess_p.h" + +namespace Utils::Terminal { + +struct HooksPrivate +{ + HooksPrivate() + : m_openTerminalHook([](const OpenTerminalParameters ¶meters) { + DeviceFileHooks::instance().openTerminal(parameters.workingDirectory.value_or( + FilePath{}), + parameters.environment.value_or(Environment{})); + }) + , m_createTerminalProcessInterfaceHook( + []() -> ProcessInterface * { return new Internal::TerminalImpl(); }) + {} + + Hooks::OpenTerminalHook m_openTerminalHook; + Hooks::CreateTerminalProcessInterfaceHook m_createTerminalProcessInterfaceHook; +}; + +Hooks &Hooks::instance() +{ + static Hooks manager; + return manager; +} + +Hooks::Hooks() + : d(new HooksPrivate()) +{} + +Hooks::~Hooks() = default; + +Hooks::OpenTerminalHook &Hooks::openTerminalHook() +{ + return d->m_openTerminalHook; +} +Hooks::CreateTerminalProcessInterfaceHook &Hooks::createTerminalProcessInterfaceHook() +{ + return d->m_createTerminalProcessInterfaceHook; +} + +} // namespace Utils::Terminal diff --git a/src/libs/utils/terminalhooks.h b/src/libs/utils/terminalhooks.h new file mode 100644 index 00000000000..57012afc903 --- /dev/null +++ b/src/libs/utils/terminalhooks.h @@ -0,0 +1,70 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "commandline.h" +#include "environment.h" +#include "filepath.h" + +#include +#include + +namespace Utils { +class ProcessInterface; + +template +class Hook +{ +public: + using Callback = std::function; + +public: + Hook() = delete; + Hook(const Hook &other) = delete; + Hook(Hook &&other) = delete; + Hook &operator=(const Hook &other) = delete; + Hook &operator=(Hook &&other) = delete; + + explicit Hook(Callback defaultCallback) { set(defaultCallback); } + + void set(Callback cb) { m_callback = cb; } + R operator()(Params &&...params) { return m_callback(std::forward(params)...); } + +private: + Callback m_callback; +}; + +namespace Terminal { +struct HooksPrivate; + +enum class ExitBehavior { Close, Restart, Keep }; + +struct OpenTerminalParameters +{ + std::optional shellCommand; + std::optional workingDirectory; + std::optional environment; + ExitBehavior m_exitBehavior{ExitBehavior::Close}; +}; + +class QTCREATOR_UTILS_EXPORT Hooks +{ +public: + using OpenTerminalHook = Hook; + using CreateTerminalProcessInterfaceHook = Hook; + +public: + static Hooks &instance(); + + OpenTerminalHook &openTerminalHook(); + CreateTerminalProcessInterfaceHook &createTerminalProcessInterfaceHook(); + + ~Hooks(); +private: + Hooks(); + std::unique_ptr d; +}; + +} // namespace Terminal +} // namespace Utils diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index 11c0e342826..a1b3415e719 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -307,6 +307,8 @@ Project { "temporaryfile.h", "terminalcommand.cpp", "terminalcommand.h", + "terminalhooks.cpp", + "terminalhooks.h", "terminalprocess.cpp", "terminalprocess_p.h", "textfieldcheckbox.cpp", diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index dc92077f0c1..5e3ff9bb18a 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -101,3 +101,4 @@ add_subdirectory(webassembly) add_subdirectory(mcusupport) add_subdirectory(saferenderer) add_subdirectory(copilot) +add_subdirectory(terminal) diff --git a/src/plugins/coreplugin/fileutils.cpp b/src/plugins/coreplugin/fileutils.cpp index 6c783488971..116fe244f2f 100644 --- a/src/plugins/coreplugin/fileutils.cpp +++ b/src/plugins/coreplugin/fileutils.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -104,8 +105,7 @@ void FileUtils::showInFileSystemView(const FilePath &path) void FileUtils::openTerminal(const FilePath &path, const Environment &env) { - QTC_ASSERT(DeviceFileHooks::instance().openTerminal, return); - DeviceFileHooks::instance().openTerminal(path, env); + Terminal::Hooks::instance().openTerminalHook()({std::nullopt, path, env}); } QString FileUtils::msgFindInDirectory() diff --git a/src/plugins/docker/dockerdevice.cpp b/src/plugins/docker/dockerdevice.cpp index 2a955f131fe..d9ec143a9e3 100644 --- a/src/plugins/docker/dockerdevice.cpp +++ b/src/plugins/docker/dockerdevice.cpp @@ -160,9 +160,11 @@ public: Environment environment(); CommandLine withDockerExecCmd(const CommandLine &cmd, - Environment *env = nullptr, - FilePath *workDir = nullptr, - bool interactive = false); + const std::optional &env = std::nullopt, + const std::optional &workDir = std::nullopt, + bool interactive = false, + bool includeMarker = true, + bool withPty = false); bool prepareForBuild(const Target *target); Tasks validateMounts() const; @@ -294,11 +296,11 @@ void DockerProcessImpl::start() const bool interactive = m_setup.m_processMode == ProcessMode::Writer || !m_setup.m_writeData.isEmpty(); - const CommandLine fullCommandLine = m_devicePrivate - ->withDockerExecCmd(m_setup.m_commandLine, - &m_setup.m_environment, - &m_setup.m_workingDirectory, - interactive); + const CommandLine fullCommandLine + = m_devicePrivate->withDockerExecCmd(m_setup.m_commandLine, + m_setup.m_environment, + m_setup.m_workingDirectory, + interactive); m_process.setCommand(fullCommandLine); m_process.start(); @@ -446,9 +448,11 @@ void DockerDevice::updateContainerAccess() const } CommandLine DockerDevicePrivate::withDockerExecCmd(const CommandLine &cmd, - Environment *env, - FilePath *workDir, - bool interactive) + const std::optional &env, + const std::optional &workDir, + bool interactive, + bool includeMarker, + bool withPty) { if (!m_settings) return {}; @@ -460,6 +464,9 @@ CommandLine DockerDevicePrivate::withDockerExecCmd(const CommandLine &cmd, if (interactive) dockerCmd.addArg("-i"); + if (withPty) + dockerCmd.addArg("-t"); + if (env) { for (auto it = env->constBegin(); it != env->constEnd(); ++it) { dockerCmd.addArg("-e"); @@ -468,19 +475,24 @@ CommandLine DockerDevicePrivate::withDockerExecCmd(const CommandLine &cmd, } if (workDir && !workDir->isEmpty()) - dockerCmd.addArgs({"-w", workDir->path()}); + dockerCmd.addArgs({"-w", workDir->onDevice(q->rootPath()).nativePath()}); dockerCmd.addArg(m_container); - dockerCmd.addArgs({"/bin/sh", "-c"}); - CommandLine exec("exec"); - exec.addCommandLineAsArgs(cmd); + if (includeMarker) { + dockerCmd.addArgs({"/bin/sh", "-c"}); - CommandLine echo("echo"); - echo.addArgs("__qtc$$qtc__", CommandLine::Raw); - echo.addCommandLineWithAnd(exec); + CommandLine exec("exec"); + exec.addCommandLineAsArgs(cmd); - dockerCmd.addCommandLineAsSingleArg(echo); + CommandLine echo("echo"); + echo.addArgs("__qtc$$qtc__", CommandLine::Raw); + echo.addCommandLineWithAnd(exec); + + dockerCmd.addCommandLineAsSingleArg(echo); + } else { + dockerCmd.addCommandLineAsArgs(cmd); + } return dockerCmd; } @@ -1222,4 +1234,16 @@ std::optional DockerDevice::clangdExecutable() const return d->clangdExecutable(); } +std::optional DockerDevice::terminalCommand(const FilePath &workDir, + const Environment &env) const +{ + const QString shell = d->environment().value_or("SHELL", "/bin/sh"); + return d->withDockerExecCmd({FilePath::fromUserInput(shell), {}}, + std::nullopt, + workDir, + true, + false, + true); +} + } // namespace Docker::Internal diff --git a/src/plugins/docker/dockerdevice.h b/src/plugins/docker/dockerdevice.h index 3ecc0118d49..0a1a77f947b 100644 --- a/src/plugins/docker/dockerdevice.h +++ b/src/plugins/docker/dockerdevice.h @@ -101,6 +101,9 @@ public: bool prepareForBuild(const ProjectExplorer::Target *target) override; std::optional clangdExecutable() const override; + std::optional terminalCommand(const Utils::FilePath &workDir, + const Utils::Environment &env) const override; + protected: void fromMap(const QVariantMap &map) final; QVariantMap toMap() const final; diff --git a/src/plugins/plugins.qbs b/src/plugins/plugins.qbs index 5ce3a8fa9a1..d28aeb05f17 100644 --- a/src/plugins/plugins.qbs +++ b/src/plugins/plugins.qbs @@ -79,6 +79,7 @@ Project { "squish/squish.qbs", "studiowelcome/studiowelcome.qbs", "subversion/subversion.qbs", + "terminal/terminal.qbs", "texteditor/texteditor.qbs", "todo/todo.qbs", "updateinfo/updateinfo.qbs", diff --git a/src/plugins/projectexplorer/devicesupport/idevice.cpp b/src/plugins/projectexplorer/devicesupport/idevice.cpp index b884237b3b2..c615a89badc 100644 --- a/src/plugins/projectexplorer/devicesupport/idevice.cpp +++ b/src/plugins/projectexplorer/devicesupport/idevice.cpp @@ -15,6 +15,7 @@ #include +#include #include #include #include @@ -180,6 +181,14 @@ void IDevice::openTerminal(const Environment &env, const FilePath &workingDir) c d->openTerminal(env, workingDir); } +std::optional IDevice::terminalCommand(const FilePath &workDir, const Environment &env) const +{ + Q_UNUSED(workDir); + Q_UNUSED(env); + + return std::nullopt; +} + bool IDevice::isEmptyCommandAllowed() const { return d->emptyCommandAllowed; diff --git a/src/plugins/projectexplorer/devicesupport/idevice.h b/src/plugins/projectexplorer/devicesupport/idevice.h index 6323b8589bb..c4759a7d327 100644 --- a/src/plugins/projectexplorer/devicesupport/idevice.h +++ b/src/plugins/projectexplorer/devicesupport/idevice.h @@ -193,6 +193,9 @@ public: bool canOpenTerminal() const; void openTerminal(const Utils::Environment &env, const Utils::FilePath &workingDir) const; + virtual std::optional terminalCommand(const Utils::FilePath &workDir, + const Utils::Environment &env) const; + bool isEmptyCommandAllowed() const; void setAllowEmptyCommand(bool allow); diff --git a/src/plugins/projectexplorer/projectexplorer.cpp b/src/plugins/projectexplorer/projectexplorer.cpp index d2fee3f2bda..86f93307147 100644 --- a/src/plugins/projectexplorer/projectexplorer.cpp +++ b/src/plugins/projectexplorer/projectexplorer.cpp @@ -127,6 +127,7 @@ #include #include #include +#include #include #include @@ -3794,6 +3795,13 @@ void ProjectExplorerPluginPrivate::showInFileSystemPane() Core::FileUtils::showInFileSystemView(currentNode->filePath()); } +static BuildConfiguration *activeBuildConfiguration(Project *project) +{ + if (!project || !project->activeTarget() || !project->activeTarget()->activeBuildConfiguration()) + return {}; + return project->activeTarget()->activeBuildConfiguration(); +} + void ProjectExplorerPluginPrivate::openTerminalHere(const EnvironmentGetter &env) { const Node *currentNode = ProjectTree::currentNode(); @@ -3803,7 +3811,21 @@ void ProjectExplorerPluginPrivate::openTerminalHere(const EnvironmentGetter &env if (!environment) return; - Core::FileUtils::openTerminal(currentNode->directory(), environment.value()); + BuildConfiguration *bc = activeBuildConfiguration(ProjectTree::projectForNode(currentNode)); + if (!bc) + Terminal::Hooks::instance().openTerminalHook()({{}, currentNode->directory(), environment}); + + IDeviceConstPtr buildDevice = BuildDeviceKitAspect::device(bc->target()->kit()); + + if (!buildDevice) + return; + + FilePath workingDir = currentNode->directory(); + if (!buildDevice->ensureReachable(workingDir)) + workingDir.clear(); + + const auto cmd = buildDevice->terminalCommand(workingDir, *environment); + Terminal::Hooks::instance().openTerminalHook()({cmd, workingDir, environment}); } void ProjectExplorerPluginPrivate::openTerminalHereWithRunEnv() @@ -3824,9 +3846,12 @@ void ProjectExplorerPluginPrivate::openTerminalHereWithRunEnv() if (!device) device = DeviceKitAspect::device(target->kit()); QTC_ASSERT(device && device->canOpenTerminal(), return); + const FilePath workingDir = device->type() == Constants::DESKTOP_DEVICE_TYPE ? currentNode->directory() : runnable.workingDirectory; - device->openTerminal(runnable.environment, workingDir); + + const auto cmd = device->terminalCommand(workingDir, runnable.environment); + Terminal::Hooks::instance().openTerminalHook()({cmd, workingDir, runnable.environment}); } void ProjectExplorerPluginPrivate::removeFile() diff --git a/src/plugins/terminal/CMakeLists.txt b/src/plugins/terminal/CMakeLists.txt new file mode 100644 index 00000000000..14f6d844b36 --- /dev/null +++ b/src/plugins/terminal/CMakeLists.txt @@ -0,0 +1,17 @@ + +add_qtc_plugin(Terminal + SKIP_TRANSLATION + PLUGIN_DEPENDS Core + DEPENDS libvterm ptyqt + SOURCES + celllayout.cpp celllayout.h + terminalplugin.cpp terminalplugin.h + terminaltr.h + terminalpane.cpp terminalpane.h + terminalwidget.cpp terminalwidget.h + terminalprocessinterface.cpp terminalprocessinterface.h + terminalsettings.cpp terminalsettings.h + terminalsettingspage.cpp terminalsettingspage.h + scrollback.h scrollback.cpp + keys.cpp keys.h +) diff --git a/src/plugins/terminal/Terminal.json.in b/src/plugins/terminal/Terminal.json.in new file mode 100644 index 00000000000..3715f77fa31 --- /dev/null +++ b/src/plugins/terminal/Terminal.json.in @@ -0,0 +1,19 @@ +{ + \"Name\" : \"Terminal\", + \"Version\" : \"$$QTCREATOR_VERSION\", + \"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\", + \"DisabledByDefault\" : true, + \"Vendor\" : \"The Qt Company Ltd\", + \"Copyright\" : \"(C) $$QTCREATOR_COPYRIGHT_YEAR The Qt Company Ltd\", + \"License\" : [ \"Commercial Usage\", + \"\", + \"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and The Qt Company.\", + \"\", + \"GNU General Public License Usage\", + \"\", + \"Alternatively, this plugin may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this plugin. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html.\" + ], + \"Description\" : \"Terminal window.\", + \"Url\" : \"http://www.qt.io\", + $$dependencyList +} diff --git a/src/plugins/terminal/celllayout.cpp b/src/plugins/terminal/celllayout.cpp new file mode 100644 index 00000000000..f9034dccce6 --- /dev/null +++ b/src/plugins/terminal/celllayout.cpp @@ -0,0 +1,120 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "celllayout.h" + +#include + +namespace Terminal::Internal { + +QColor toQColor(const VTermColor &c) +{ + return QColor(qRgb(c.rgb.red, c.rgb.green, c.rgb.blue)); +}; + +void createTextLayout(QTextLayout &textLayout, + QString &resultText, + VTermColor defaultBg, + QRect cellRect, + qreal lineSpacing, + std::function fetchCell) +{ + QList formats; + + QTextCharFormat currentFormat; + int currentFormatStart = 0; + currentFormat.setForeground(QColor(0xff, 0xff, 0xff)); + currentFormat.clearBackground(); + + resultText.clear(); + + for (int y = cellRect.y(); y < cellRect.bottom() + 1; y++) { + QTextCharFormat format; + + const auto setNewFormat = [&formats, ¤tFormatStart, &resultText, ¤tFormat]( + const QTextCharFormat &format) { + if (resultText.size() != currentFormatStart) { + QTextLayout::FormatRange fr; + fr.start = currentFormatStart; + fr.length = resultText.size() - currentFormatStart; + fr.format = currentFormat; + formats.append(fr); + + currentFormat = format; + currentFormatStart = resultText.size(); + } else { + currentFormat = format; + } + }; + + for (int x = cellRect.x(); x < cellRect.right() + 1; x++) { + const VTermScreenCell *cell = fetchCell(x, y); + + const VTermColor *bg = &cell->bg; + const VTermColor *fg = &cell->fg; + + if (static_cast(cell->attrs.reverse)) { + bg = &cell->fg; + fg = &cell->bg; + } + + format = QTextCharFormat(); + format.setForeground(toQColor(*fg)); + + if (!vterm_color_is_equal(bg, &defaultBg)) + format.setBackground(toQColor(*bg)); + else + format.clearBackground(); + + if (cell->attrs.bold) + format.setFontWeight(QFont::Bold); + if (cell->attrs.underline) + format.setFontUnderline(true); + if (cell->attrs.italic) + format.setFontItalic(true); + if (cell->attrs.strike) + format.setFontStrikeOut(true); + + if (format != currentFormat) + setNewFormat(format); + + if (cell->chars[0] != 0xFFFFFFFF) { + QString ch = QString::fromUcs4(cell->chars); + if (ch.size() > 0) { + resultText += ch; + } else { + resultText += QChar::Nbsp; + } + } + } // for x + setNewFormat(format); + if (y != cellRect.bottom()) + resultText.append(QChar::LineSeparator); + } // for y + + QTextLayout::FormatRange fr; + fr.start = currentFormatStart; + fr.length = (resultText.size() - 1) - currentFormatStart; + fr.format = currentFormat; + formats.append(fr); + + textLayout.setText(resultText); + textLayout.setFormats(formats); + + qreal height = 0; + textLayout.beginLayout(); + while (1) { + QTextLine line = textLayout.createLine(); + if (!line.isValid()) + break; + + // Just give it a number that is definitely larger than + // the number of columns in a line. + line.setNumColumns(std::numeric_limits::max()); + line.setPosition(QPointF(0, height)); + height += lineSpacing; + } + textLayout.endLayout(); +} + +} // namespace Terminal::Internal diff --git a/src/plugins/terminal/celllayout.h b/src/plugins/terminal/celllayout.h new file mode 100644 index 00000000000..286ed8944fb --- /dev/null +++ b/src/plugins/terminal/celllayout.h @@ -0,0 +1,27 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include + +namespace Terminal::Internal { + +QColor toQColor(const VTermColor &c); + +void createTextLayout(QTextLayout &textLayout, + QString &resultText, + VTermColor defaultBg, + QRect cellRect, + qreal lineSpacing, + std::function fetchCell); + +} // namespace Terminal::Internal diff --git a/src/plugins/terminal/keys.cpp b/src/plugins/terminal/keys.cpp new file mode 100644 index 00000000000..f6a7a91b13d --- /dev/null +++ b/src/plugins/terminal/keys.cpp @@ -0,0 +1,83 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "keys.h" + +namespace Terminal::Internal { + +VTermModifier qtModifierToVTerm(Qt::KeyboardModifiers mod) +{ + int ret = VTERM_MOD_NONE; + + if (mod & Qt::ShiftModifier) + ret |= VTERM_MOD_SHIFT; + + if (mod & Qt::AltModifier) + ret |= VTERM_MOD_ALT; + +#ifdef Q_OS_DARWIN + if (mod & Qt::MetaModifier) + ret |= VTERM_MOD_CTRL; +#else + if (mod & Qt::ControlModifier) + ret |= VTERM_MOD_CTRL; +#endif + + return static_cast(ret); +} + +VTermKey qtKeyToVTerm(Qt::Key key, bool keypad) +{ + if (key >= Qt::Key_F1 && key <= Qt::Key_F35) + return static_cast(VTERM_KEY_FUNCTION_0 + key - Qt::Key_F1 + 1); + + switch (key) { + case Qt::Key_Return: + return VTERM_KEY_ENTER; + case Qt::Key_Tab: + return VTERM_KEY_TAB; + case Qt::Key_Backspace: + return VTERM_KEY_BACKSPACE; + case Qt::Key_Escape: + return VTERM_KEY_ESCAPE; + case Qt::Key_Up: + return VTERM_KEY_UP; + case Qt::Key_Down: + return VTERM_KEY_DOWN; + case Qt::Key_Left: + return VTERM_KEY_LEFT; + case Qt::Key_Right: + return VTERM_KEY_RIGHT; + case Qt::Key_Insert: + return VTERM_KEY_INS; + case Qt::Key_Delete: + return VTERM_KEY_DEL; + case Qt::Key_Home: + return VTERM_KEY_HOME; + case Qt::Key_End: + return VTERM_KEY_END; + case Qt::Key_PageUp: + return VTERM_KEY_PAGEUP; + case Qt::Key_PageDown: + return VTERM_KEY_PAGEDOWN; + case Qt::Key_multiply: + return keypad ? VTERM_KEY_KP_MULT : VTERM_KEY_NONE; + case Qt::Key_Plus: + return keypad ? VTERM_KEY_KP_PLUS : VTERM_KEY_NONE; + case Qt::Key_Comma: + return keypad ? VTERM_KEY_KP_COMMA : VTERM_KEY_NONE; + case Qt::Key_Minus: + return keypad ? VTERM_KEY_KP_MINUS : VTERM_KEY_NONE; + case Qt::Key_Period: + return keypad ? VTERM_KEY_KP_PERIOD : VTERM_KEY_NONE; + case Qt::Key_Slash: + return keypad ? VTERM_KEY_KP_DIVIDE : VTERM_KEY_NONE; + case Qt::Key_Enter: + return keypad ? VTERM_KEY_KP_ENTER : VTERM_KEY_NONE; + case Qt::Key_Equal: + return keypad ? VTERM_KEY_KP_EQUAL : VTERM_KEY_NONE; + default: + return VTERM_KEY_NONE; + } +} +} // namespace Terminal::Internal diff --git a/src/plugins/terminal/keys.h b/src/plugins/terminal/keys.h new file mode 100644 index 00000000000..f3df9330013 --- /dev/null +++ b/src/plugins/terminal/keys.h @@ -0,0 +1,15 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +#include + +namespace Terminal::Internal { + +VTermKey qtKeyToVTerm(Qt::Key key, bool keypad); +VTermModifier qtModifierToVTerm(Qt::KeyboardModifiers mod); + +} // namespace Terminal::Internal diff --git a/src/plugins/terminal/scrollback.cpp b/src/plugins/terminal/scrollback.cpp new file mode 100644 index 00000000000..8a07c700f8d --- /dev/null +++ b/src/plugins/terminal/scrollback.cpp @@ -0,0 +1,93 @@ +// Copyright (c) 2020, Justin Bronder +// Copied and modified from: https://github.com/jsbronder/sff +// SPDX-License-Identifier: BSD-3-Clause + +#include "scrollback.h" +#include "celllayout.h" + +#include +#include + +namespace Terminal::Internal { + +Scrollback::Line::Line(int cols, const VTermScreenCell *cells, VTermState *vts) + : m_cols(cols) + , m_cells(std::make_unique(cols)) + +{ + memcpy(m_cells.get(), cells, cols * sizeof(cells[0])); + for (int i = 0; i < cols; ++i) { + vterm_state_convert_color_to_rgb(vts, &m_cells[i].fg); + vterm_state_convert_color_to_rgb(vts, &m_cells[i].bg); + } + m_layout = std::make_unique(); +} + +const VTermScreenCell *Scrollback::Line::cell(int i) const +{ + assert(i >= 0 && i < m_cols); + return &m_cells[i]; +} + +const QTextLayout &Scrollback::Line::layout(int version, const QFont &font, qreal lineSpacing) const +{ + if (m_layoutVersion != version) { + QString text; + VTermColor defaultBg; + defaultBg.type = VTERM_COLOR_DEFAULT_BG; + m_layout->clearLayout(); + m_layout->setFont(font); + createTextLayout(*m_layout, + text, + defaultBg, + QRect(0, 0, m_cols, 1), + lineSpacing, + [this](int x, int) { return &m_cells[x]; }); + m_layoutVersion = version; + } + return *m_layout; +} + +Scrollback::Scrollback(size_t capacity) + : m_capacity(capacity) +{} + +void Scrollback::emplace(int cols, const VTermScreenCell *cells, VTermState *vts) +{ + m_deque.emplace_front(cols, cells, vts); + while (m_deque.size() > m_capacity) + m_deque.pop_back(); +} + +void Scrollback::popto(int cols, VTermScreenCell *cells) +{ + const Line &sbl = m_deque.front(); + + int ncells = cols; + if (ncells > sbl.cols()) + ncells = sbl.cols(); + + memcpy(cells, sbl.cells(), sizeof(cells[0]) * ncells); + for (size_t i = ncells; i < static_cast(cols); ++i) { + cells[i].chars[0] = '\0'; + cells[i].width = 1; + cells[i].bg = cells[ncells - 1].bg; + } + + m_deque.pop_front(); +} + +size_t Scrollback::scroll(int delta) +{ + m_offset = std::min(std::max(0, static_cast(m_offset) + delta), + static_cast(m_deque.size())); + return m_offset; +} + +void Scrollback::clear() +{ + m_offset = 0; + m_deque.clear(); +} + +} // namespace Terminal::Internal diff --git a/src/plugins/terminal/scrollback.h b/src/plugins/terminal/scrollback.h new file mode 100644 index 00000000000..63c18772b93 --- /dev/null +++ b/src/plugins/terminal/scrollback.h @@ -0,0 +1,66 @@ +// Copyright (c) 2020, Justin Bronder +// Copied and modified from: https://github.com/jsbronder/sff +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include + +#include +#include + +#include +#include + +namespace Terminal::Internal { + +class Scrollback +{ +public: + class Line + { + public: + Line(int cols, const VTermScreenCell *cells, VTermState *vts); + Line(Line &&other) = default; + Line() = delete; + + int cols() const { return m_cols; }; + const VTermScreenCell *cell(int i) const; + const VTermScreenCell *cells() const { return &m_cells[0]; }; + + const QTextLayout &layout(int version, const QFont &font, qreal lineSpacing) const; + + private: + int m_cols; + std::unique_ptr m_cells; + std::unique_ptr m_layout; + mutable int m_layoutVersion{-1}; + }; + +public: + Scrollback(size_t capacity); + Scrollback() = delete; + + size_t capacity() const { return m_capacity; }; + size_t size() const { return m_deque.size(); }; + size_t offset() const { return m_offset; }; + + const Line &line(size_t index) const { return m_deque.at(index); }; + const std::deque &lines() const { return m_deque; }; + + void emplace(int cols, + const VTermScreenCell *cells, + VTermState *vts); + void popto(int cols, VTermScreenCell *cells); + size_t scroll(int delta); + void unscroll() { m_offset = 0; }; + + void clear(); + +private: + size_t m_capacity; + size_t m_offset{0}; + std::deque m_deque; +}; + +} // namespace Terminal::Internal diff --git a/src/plugins/terminal/terminal.qbs b/src/plugins/terminal/terminal.qbs new file mode 100644 index 00000000000..54743641107 --- /dev/null +++ b/src/plugins/terminal/terminal.qbs @@ -0,0 +1,32 @@ +import qbs 1.0 + +QtcPlugin { + name: "Terminal" + + Depends { name: "Core" } + Depends { name: "vterm" } + Depends { name: "ptyqt" } + + files: [ + "celllayout.cpp", + "celllayout.h", + "keys.cpp", + "keys.h", + "scrollback.cpp", + "scrollback.h", + "terminalpane.cpp", + "terminalpane.h", + "terminalplugin.cpp", + "terminalplugin.h", + "terminalprocessinterface.cpp", + "terminalprocessinterface.h", + "terminalsettings.cpp", + "terminalsettings.h", + "terminalsettingspage.cpp", + "terminalsettingspage.h", + "terminaltr.h", + "terminalwidget.cpp", + "terminalwidget.h", + ] +} + diff --git a/src/plugins/terminal/terminalpane.cpp b/src/plugins/terminal/terminalpane.cpp new file mode 100644 index 00000000000..5e57317c0fc --- /dev/null +++ b/src/plugins/terminal/terminalpane.cpp @@ -0,0 +1,176 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "terminalpane.h" + +#include "terminaltr.h" +#include "terminalwidget.h" + +#include +#include + +#include + +namespace Terminal { + +TerminalPane::TerminalPane(QObject *parent) + : Core::IOutputPane(parent) +{ + Core::Context context("Terminal.Window"); + + m_newTerminal.setIcon(Utils::Icons::PLUS_TOOLBAR.icon()); + m_newTerminal.setToolTip(Tr::tr("Create a new Terminal.")); + + connect(&m_newTerminal, &QAction::triggered, this, [this] { + m_tabWidget->setCurrentIndex( + m_tabWidget->addTab(new TerminalWidget(m_tabWidget), Tr::tr("Terminal"))); + + m_closeTerminal.setEnabled(m_tabWidget->count() > 1); + emit navigateStateUpdate(); + }); + + m_closeTerminal.setIcon(Utils::Icons::CLOSE_TOOLBAR.icon()); + m_closeTerminal.setToolTip(Tr::tr("Close the current Terminal.")); + m_closeTerminal.setEnabled(false); + + connect(&m_closeTerminal, &QAction::triggered, this, [this] { + removeTab(m_tabWidget->currentIndex()); + }); + + //Core::Command *cmd = Core::ActionManager::registerAction(m_newTerminal, Constants::STOP); + //cmd->setDescription(m_newTerminal->toolTip()); + + m_newTerminalButton = new QToolButton(); + m_newTerminalButton->setDefaultAction(&m_newTerminal); + + m_closeTerminalButton = new QToolButton(); + m_closeTerminalButton->setDefaultAction(&m_closeTerminal); +} + +void TerminalPane::openTerminal(const Utils::Terminal::OpenTerminalParameters ¶meters) +{ + showPage(0); + m_tabWidget->setCurrentIndex( + m_tabWidget->addTab(new TerminalWidget(m_tabWidget, parameters), Tr::tr("Terminal"))); + + m_closeTerminal.setEnabled(m_tabWidget->count() > 1); + emit navigateStateUpdate(); +} + +void TerminalPane::addTerminal(TerminalWidget *terminal, const QString &title) +{ + showPage(0); + m_tabWidget->setCurrentIndex(m_tabWidget->addTab(terminal, title)); + + m_closeTerminal.setEnabled(m_tabWidget->count() > 1); + emit navigateStateUpdate(); +} + +QWidget *TerminalPane::outputWidget(QWidget *parent) +{ + if (!m_tabWidget) { + m_tabWidget = new QTabWidget(parent); + + m_tabWidget->setTabBarAutoHide(true); + m_tabWidget->setDocumentMode(true); + m_tabWidget->setTabsClosable(true); + m_tabWidget->setMovable(true); + + connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) { + removeTab(index); + }); + + m_tabWidget->addTab(new TerminalWidget(parent), Tr::tr("Terminal")); + } + + return m_tabWidget; +} + +TerminalWidget *TerminalPane::currentTerminal() const +{ + QWidget *activeWidget = m_tabWidget->currentWidget(); + return static_cast(activeWidget); +} + +void TerminalPane::removeTab(int index) +{ + if (m_tabWidget->count() > 1) + delete m_tabWidget->widget(index); + + m_closeTerminal.setEnabled(m_tabWidget->count() > 1); + emit navigateStateUpdate(); +} + +QList TerminalPane::toolBarWidgets() const +{ + return {m_newTerminalButton, m_closeTerminalButton}; +} + +QString TerminalPane::displayName() const +{ + return Tr::tr("Terminal"); +} + +int TerminalPane::priorityInStatusBar() const +{ + return 50; +} + +void TerminalPane::clearContents() +{ + if (const auto t = currentTerminal()) + t->clearContents(); +} + +void TerminalPane::visibilityChanged(bool visible) +{ + Q_UNUSED(visible); +} + +void TerminalPane::setFocus() +{ + if (const auto t = currentTerminal()) + t->setFocus(); +} + +bool TerminalPane::hasFocus() const +{ + if (const auto t = currentTerminal()) + t->hasFocus(); + + return false; +} + +bool TerminalPane::canFocus() const +{ + return true; +} + +bool TerminalPane::canNavigate() const +{ + return true; +} + +bool TerminalPane::canNext() const +{ + return m_tabWidget->count() > 1 && m_tabWidget->currentIndex() < m_tabWidget->count() - 1; +} + +bool TerminalPane::canPrevious() const +{ + return m_tabWidget->count() > 1 && m_tabWidget->currentIndex() > 0; +} + +void TerminalPane::goToNext() +{ + m_tabWidget->setCurrentIndex(m_tabWidget->currentIndex() + 1); + emit navigateStateUpdate(); +} + +void TerminalPane::goToPrev() +{ + m_tabWidget->setCurrentIndex(m_tabWidget->currentIndex() - 1); + emit navigateStateUpdate(); +} + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalpane.h b/src/plugins/terminal/terminalpane.h new file mode 100644 index 00000000000..3dea1c84142 --- /dev/null +++ b/src/plugins/terminal/terminalpane.h @@ -0,0 +1,57 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +#include + +#include +#include +#include + +namespace Terminal { + +class TerminalWidget; + +class TerminalPane : public Core::IOutputPane +{ + Q_OBJECT +public: + TerminalPane(QObject *parent = nullptr); + + virtual QWidget *outputWidget(QWidget *parent); + virtual QList toolBarWidgets() const; + virtual QString displayName() const; + virtual int priorityInStatusBar() const; + virtual void clearContents(); + virtual void visibilityChanged(bool visible); + virtual void setFocus(); + virtual bool hasFocus() const; + virtual bool canFocus() const; + virtual bool canNavigate() const; + virtual bool canNext() const; + virtual bool canPrevious() const; + virtual void goToNext(); + virtual void goToPrev(); + + void openTerminal(const Utils::Terminal::OpenTerminalParameters ¶meters); + void addTerminal(TerminalWidget *terminal, const QString &title); + +private: + TerminalWidget *currentTerminal() const; + + void removeTab(int index); + +private: + QTabWidget *m_tabWidget{nullptr}; + + QToolButton *m_newTerminalButton{nullptr}; + QToolButton *m_closeTerminalButton{nullptr}; + + QAction m_newTerminal; + QAction m_closeTerminal; +}; + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalplugin.cpp b/src/plugins/terminal/terminalplugin.cpp new file mode 100644 index 00000000000..ce196441182 --- /dev/null +++ b/src/plugins/terminal/terminalplugin.cpp @@ -0,0 +1,58 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "terminalplugin.h" + +#include "terminalpane.h" +#include "terminalprocessinterface.h" +#include "terminalsettings.h" +#include "terminalsettingspage.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace Terminal { +namespace Internal { + +TerminalPlugin::TerminalPlugin() {} + +TerminalPlugin::~TerminalPlugin() +{ + ExtensionSystem::PluginManager::instance()->removeObject(m_terminalPane); + delete m_terminalPane; + m_terminalPane = nullptr; +} + +void TerminalPlugin::extensionsInitialized() +{ + TerminalSettingsPage::instance().init(); + TerminalSettings::instance().readSettings(Core::ICore::settings()); + + m_terminalPane = new TerminalPane(); + ExtensionSystem::PluginManager::instance()->addObject(m_terminalPane); + + Utils::Terminal::Hooks::instance().openTerminalHook().set( + [this](const Utils::Terminal::OpenTerminalParameters &p) { + m_terminalPane->openTerminal(p); + }); + + /*Utils::Terminal::Hooks::instance().createTerminalProcessInterfaceHook().set( + [this]() -> Utils::ProcessInterface * { + return new TerminalProcessInterface(m_terminalPane); + });*/ +} + +} // namespace Internal +} // namespace Terminal diff --git a/src/plugins/terminal/terminalplugin.h b/src/plugins/terminal/terminalplugin.h new file mode 100644 index 00000000000..5e39dded711 --- /dev/null +++ b/src/plugins/terminal/terminalplugin.h @@ -0,0 +1,29 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace Terminal { + +class TerminalPane; +namespace Internal { + +class TerminalPlugin : public ExtensionSystem::IPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Terminal.json") + +public: + TerminalPlugin(); + ~TerminalPlugin() override; + + void extensionsInitialized() override; + +private: + TerminalPane *m_terminalPane{nullptr}; +}; + +} // namespace Internal +} // namespace Terminal diff --git a/src/plugins/terminal/terminalprocessinterface.cpp b/src/plugins/terminal/terminalprocessinterface.cpp new file mode 100644 index 00000000000..75f56338146 --- /dev/null +++ b/src/plugins/terminal/terminalprocessinterface.cpp @@ -0,0 +1,47 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "terminalprocessinterface.h" +#include "terminalwidget.h" + +namespace Terminal { + +TerminalProcessInterface::TerminalProcessInterface(TerminalPane *terminalPane) + : m_terminalPane(terminalPane) +{} + +// It's being called only in Starting state. Just before this method is being called, +// the process transitions from NotRunning into Starting state. +void TerminalProcessInterface::start() +{ + QTC_ASSERT(!m_setup.m_commandLine.executable().needsDevice(), return); + + TerminalWidget *terminal = new TerminalWidget(nullptr, + {m_setup.m_commandLine, + m_setup.m_workingDirectory, + m_setup.m_environment, + Utils::Terminal::ExitBehavior::Keep}); + + connect(terminal, &TerminalWidget::started, this, [this](qint64 pid) { emit started(pid); }); + + connect(terminal, &QObject::destroyed, this, [this]() { + emit done(Utils::ProcessResultData{}); + }); + + m_terminalPane->addTerminal(terminal, "App"); +} + +// It's being called only in Running state. +qint64 TerminalProcessInterface::write(const QByteArray &data) +{ + Q_UNUSED(data); + return 0; +} + +// It's being called in Starting or Running state. +void TerminalProcessInterface::sendControlSignal(Utils::ControlSignal controlSignal) +{ + Q_UNUSED(controlSignal); +} + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalprocessinterface.h b/src/plugins/terminal/terminalprocessinterface.h new file mode 100644 index 00000000000..55dc7be3f80 --- /dev/null +++ b/src/plugins/terminal/terminalprocessinterface.h @@ -0,0 +1,49 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "terminalpane.h" + +#include + +namespace Terminal { + +class TerminalProcessInterface : public Utils::ProcessInterface +{ + Q_OBJECT + +public: + TerminalProcessInterface(TerminalPane *terminalPane); + + /* + // This should be emitted when being in Starting state only. + // After emitting this signal the process enters Running state. + void started(qint64 processId, qint64 applicationMainThreadId = 0); + + // This should be emitted when being in Running state only. + void readyRead(const QByteArray &outputData, const QByteArray &errorData); + + // This should be emitted when being in Starting or Running state. + // When being in Starting state, the resultData should set error to FailedToStart. + // After emitting this signal the process enters NotRunning state. + void done(const Utils::ProcessResultData &resultData); +*/ +private: + // It's being called only in Starting state. Just before this method is being called, + // the process transitions from NotRunning into Starting state. + void start() override; + + // It's being called only in Running state. + qint64 write(const QByteArray &data) override; + + // It's being called in Starting or Running state. + void sendControlSignal(Utils::ControlSignal controlSignal) override; + + //Utils::ProcessBlockingInterface *processBlockingInterface() const { return nullptr; } + +private: + TerminalPane *m_terminalPane; +}; + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalsettings.cpp b/src/plugins/terminal/terminalsettings.cpp new file mode 100644 index 00000000000..a69a1e56810 --- /dev/null +++ b/src/plugins/terminal/terminalsettings.cpp @@ -0,0 +1,120 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "terminalsettings.h" + +#include "terminaltr.h" + +#include +#include + +using namespace Utils; + +namespace Terminal { + +static QString defaultFontFamily() +{ + if (HostOsInfo::isMacHost()) + return QLatin1String("Menlo"); + + if (Utils::HostOsInfo::isAnyUnixHost()) + return QLatin1String("Monospace"); + + return QLatin1String("Consolas"); +} + +static int defaultFontSize() +{ + if (Utils::HostOsInfo::isMacHost()) + return 12; + if (Utils::HostOsInfo::isAnyUnixHost()) + return 9; + return 10; +} + +static QString defaultShell() +{ + if (Utils::HostOsInfo::isMacHost()) + return "/bin/zsh"; + if (Utils::HostOsInfo::isAnyUnixHost()) + return "/bin/bash"; + return qtcEnvironmentVariable("COMSPEC"); +} + +TerminalSettings &TerminalSettings::instance() +{ + static TerminalSettings settings; + return settings; +} + +void setupColor(ColorAspect &color, const QString &label, const QColor &defaultColor) +{ + color.setSettingsKey(label); + color.setDefaultValue(defaultColor); + color.setToolTip(Tr::tr("The color used for %1.").arg(label)); +} + +TerminalSettings::TerminalSettings() +{ + setAutoApply(false); + setSettingsGroup("Terminal"); + + font.setSettingsKey("FontFamily"); + font.setLabelText(Tr::tr("Family:")); + font.setHistoryCompleter("Terminal.Fonts.History"); + font.setToolTip(Tr::tr("The font family used in the terminal.")); + font.setDefaultValue(defaultFontFamily()); + + fontSize.setSettingsKey("FontSize"); + fontSize.setLabelText(Tr::tr("Size:")); + fontSize.setToolTip(Tr::tr("The font size used in the terminal. (in points)")); + fontSize.setDefaultValue(defaultFontSize()); + fontSize.setRange(1, 100); + + shell.setSettingsKey("ShellPath"); + shell.setLabelText(Tr::tr("Shell path:")); + shell.setExpectedKind(PathChooser::ExistingCommand); + shell.setDisplayStyle(StringAspect::PathChooserDisplay); + shell.setHistoryCompleter("Terminal.Shell.History"); + shell.setToolTip(Tr::tr("The shell executable to be started as terminal")); + shell.setDefaultValue(defaultShell()); + + setupColor(foregroundColor, "Foreground", QColor::fromRgb(0xff, 0xff, 0xff)); + setupColor(backgroundColor, "Background", QColor::fromRgb(0x0, 0x0, 0x0)); + + setupColor(colors[0], "0", QColor::fromRgb(0x00, 0x00, 0x00)); + setupColor(colors[8], "8", QColor::fromRgb(102, 102, 102)); + + setupColor(colors[1], "1", QColor::fromRgb(139, 27, 16)); + setupColor(colors[9], "9", QColor::fromRgb(210, 45, 31)); + + setupColor(colors[2], "2", QColor::fromRgb(74, 163, 46)); + setupColor(colors[10], "10", QColor::fromRgb(98, 214, 63)); + + setupColor(colors[3], "3", QColor::fromRgb(154, 154, 47)); + setupColor(colors[11], "11", QColor::fromRgb(229, 229, 75)); + + setupColor(colors[4], "4", QColor::fromRgb(0, 0, 171)); + setupColor(colors[12], "12", QColor::fromRgb(0, 0, 254)); + + setupColor(colors[5], "5", QColor::fromRgb(163, 32, 172)); + setupColor(colors[13], "13", QColor::fromRgb(210, 45, 222)); + + setupColor(colors[6], "6", QColor::fromRgb(73, 163, 176)); + setupColor(colors[14], "14", QColor::fromRgb(105, 226, 228)); + + setupColor(colors[7], "7", QColor::fromRgb(191, 191, 191)); + setupColor(colors[15], "15", QColor::fromRgb(229, 229, 230)); + + registerAspect(&font); + registerAspect(&fontSize); + registerAspect(&shell); + + registerAspect(&foregroundColor); + registerAspect(&backgroundColor); + + for (auto &color : colors) + registerAspect(&color); +} + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalsettings.h b/src/plugins/terminal/terminalsettings.h new file mode 100644 index 00000000000..82557b207c1 --- /dev/null +++ b/src/plugins/terminal/terminalsettings.h @@ -0,0 +1,26 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace Terminal { +class TerminalSettings : public Utils::AspectContainer +{ +public: + TerminalSettings(); + + static TerminalSettings &instance(); + + Utils::StringAspect font; + Utils::IntegerAspect fontSize; + Utils::StringAspect shell; + + Utils::ColorAspect foregroundColor; + Utils::ColorAspect backgroundColor; + + Utils::ColorAspect colors[16]; +}; + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalsettingspage.cpp b/src/plugins/terminal/terminalsettingspage.cpp new file mode 100644 index 00000000000..b85b4a1321b --- /dev/null +++ b/src/plugins/terminal/terminalsettingspage.cpp @@ -0,0 +1,96 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "terminalsettingspage.h" + +#include "terminalsettings.h" +#include "terminaltr.h" + +#include + +#include +#include + +#include + +using namespace Utils; + +namespace Terminal { + +TerminalSettingsPage::TerminalSettingsPage() +{ + setId("Terminal.General"); + setDisplayName("Terminal"); + setCategory("ZY.Terminal"); + setDisplayCategory("Terminal"); + setSettings(&TerminalSettings::instance()); + + // TODO: Add real icon! + setCategoryIconPath(":/texteditor/images/settingscategory_texteditor.png"); +} + +void TerminalSettingsPage::init() {} + +QWidget *TerminalSettingsPage::widget() +{ + QWidget *widget = new QWidget; + + using namespace Layouting; + + QFontComboBox *fontComboBox = new QFontComboBox(widget); + fontComboBox->setFontFilters(QFontComboBox::MonospacedFonts); + fontComboBox->setCurrentFont(TerminalSettings::instance().font.value()); + + connect(fontComboBox, &QFontComboBox::currentFontChanged, this, [](const QFont &f) { + TerminalSettings::instance().font.setValue(f.family()); + }); + + TerminalSettings &settings = TerminalSettings::instance(); + + // clang-format off + Column { + Group { + title(Tr::tr("Font")), + Row { + settings.font.labelText(), fontComboBox, Space(20), + settings.fontSize, st, + }, + }, + Group { + title(Tr::tr("Colors")), + Column { + Row { + Tr::tr("Foreground"), settings.foregroundColor, st, + Tr::tr("Background"), settings.backgroundColor, st, + }, + Row { + settings.colors[0], settings.colors[1], + settings.colors[2], settings.colors[3], + settings.colors[4], settings.colors[5], + settings.colors[6], settings.colors[7] + }, + Row { + settings.colors[8], settings.colors[9], + settings.colors[10], settings.colors[11], + settings.colors[12], settings.colors[13], + settings.colors[14], settings.colors[15] + }, + } + }, + Row { + settings.shell, + }, + st, + }.attachTo(widget); + // clang-format on + + return widget; +} + +TerminalSettingsPage &TerminalSettingsPage::instance() +{ + static TerminalSettingsPage settingsPage; + return settingsPage; +} + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalsettingspage.h b/src/plugins/terminal/terminalsettingspage.h new file mode 100644 index 00000000000..4c12697b13f --- /dev/null +++ b/src/plugins/terminal/terminalsettingspage.h @@ -0,0 +1,22 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace Terminal { + +class TerminalSettingsPage : public Core::IOptionsPage +{ +public: + TerminalSettingsPage(); + + static TerminalSettingsPage &instance(); + + void init(); + + QWidget *widget() override; +}; + +} // namespace Terminal diff --git a/src/plugins/terminal/terminaltr.h b/src/plugins/terminal/terminaltr.h new file mode 100644 index 00000000000..711e0e82000 --- /dev/null +++ b/src/plugins/terminal/terminaltr.h @@ -0,0 +1,15 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace Terminal { + +struct Tr +{ + Q_DECLARE_TR_FUNCTIONS(::Terminal) +}; + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalwidget.cpp b/src/plugins/terminal/terminalwidget.cpp new file mode 100644 index 00000000000..a6169846291 --- /dev/null +++ b/src/plugins/terminal/terminalwidget.cpp @@ -0,0 +1,942 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "terminalwidget.h" +#include "celllayout.h" +#include "keys.h" +#include "terminalsettings.h" +#include "terminaltr.h" + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(terminalLog, "qtc.terminal", QtWarningMsg) + +using namespace Utils; +using namespace Utils::Terminal; + +namespace Terminal { + +TerminalWidget::TerminalWidget(QWidget *parent, const OpenTerminalParameters &openParameters) + : QAbstractScrollArea(parent) + , m_vterm(vterm_new(size().height(), size().width()), vterm_free) + , m_vtermScreen(vterm_obtain_screen(m_vterm.get())) + , m_scrollback(std::make_unique(5000)) + , m_copyAction(Tr::tr("Copy")) + , m_pasteAction(Tr::tr("Paste")) + , m_clearSelectionAction(Tr::tr("Clear Selection")) + , m_zoomInAction(Tr::tr("Zoom In")) + , m_zoomOutAction(Tr::tr("Zoom Out")) + , m_openParameters(openParameters) +{ + setupVTerm(); + setupFont(); + setupColors(); + + setAttribute(Qt::WA_InputMethodEnabled); + setAttribute(Qt::WA_MouseTracking); + + setCursor(Qt::IBeamCursor); + + m_textLayout.setCacheEnabled(true); + + setFocus(); + setFocusPolicy(Qt::StrongFocus); + + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + // setFrameStyle(QFrame::NoFrame); + setAttribute(Qt::WA_OpaquePaintEvent); + + m_readDelayTimer.setSingleShot(true); + m_readDelayTimer.setInterval(10); + + connect(&m_readDelayTimer, &QTimer::timeout, this, [this]() { + m_readDelayRestarts = 0; + onReadyRead(); + }); + + connect(&m_copyAction, &QAction::triggered, this, &TerminalWidget::copyToClipboard); + connect(&m_pasteAction, &QAction::triggered, this, &TerminalWidget::pasteFromClipboard); + connect(&m_clearSelectionAction, &QAction::triggered, this, &TerminalWidget::clearSelection); + connect(&m_zoomInAction, &QAction::triggered, this, &TerminalWidget::zoomIn); + connect(&m_zoomOutAction, &QAction::triggered, this, &TerminalWidget::zoomOut); + + connect(&TerminalSettings::instance(), &AspectContainer::applied, this, [this]() { + m_layoutVersion++; + // Setup colors first, as setupFont will redraw the screen. + setupColors(); + setupFont(); + }); +} + +void TerminalWidget::setupPty() +{ + m_ptyProcess.reset(PtyQt::createPtyProcess(IPtyProcess::PtyType::AutoPty)); + + Environment env = m_openParameters.environment.value_or(Environment::systemEnvironment()); + // Why? + env.appendOrSetPath(TerminalSettings::instance().shell.filePath().parentDir()); + + CommandLine shellCommand = m_openParameters.shellCommand.value_or( + CommandLine{TerminalSettings::instance().shell.filePath(), {}}); + + QStringList envList = filtered(env.toStringList(), [](const QString &envPair) { + return envPair != "CLINK_NOAUTORUN=1"; + }); + + m_ptyProcess->startProcess(shellCommand.executable().nativePath(), + shellCommand.splitArguments(), + m_openParameters.workingDirectory + .value_or(FilePath::fromString(QDir::homePath())) + .nativePath(), + envList, + m_vtermSize.width(), + m_vtermSize.height()); + + emit started(m_ptyProcess->pid()); + + if (!m_ptyProcess->lastError().isEmpty()) { + qCWarning(terminalLog) << m_ptyProcess->lastError(); + m_ptyProcess.reset(); + return; + } + + connect(m_ptyProcess->notifier(), &QIODevice::readyRead, this, [this]() { + if (m_readDelayTimer.isActive()) + m_readDelayRestarts++; + + if (m_readDelayRestarts > 100) + return; + + m_readDelayTimer.start(); + }); + + connect(m_ptyProcess->notifier(), &QIODevice::aboutToClose, this, [this]() { + m_cursor.visible = false; + if (m_ptyProcess) { + onReadyRead(); + + if (m_ptyProcess->exitCode() != 0) { + QByteArray msg = QString("\r\n\033[31mProcess exited with code: %1") + .arg(m_ptyProcess->exitCode()) + .toUtf8(); + + vterm_input_write(m_vterm.get(), msg.constData(), msg.size()); + vterm_screen_flush_damage(m_vtermScreen); + + return; + } + } + + if (m_openParameters.m_exitBehavior == ExitBehavior::Restart) { + QMetaObject::invokeMethod( + this, + [this]() { + m_ptyProcess.reset(); + setupPty(); + }, + Qt::QueuedConnection); + } + + if (m_openParameters.m_exitBehavior == ExitBehavior::Close) + deleteLater(); + + if (m_openParameters.m_exitBehavior == ExitBehavior::Keep) { + QByteArray msg = QString("\r\nProcess exited with code: %1") + .arg(m_ptyProcess ? m_ptyProcess->exitCode() : -1) + .toUtf8(); + + vterm_input_write(m_vterm.get(), msg.constData(), msg.size()); + vterm_screen_flush_damage(m_vtermScreen); + } + }); +} + +void TerminalWidget::setupFont() +{ + QFont f; + f.setFixedPitch(true); + f.setFamily(TerminalSettings::instance().font.value()); + f.setPointSize(TerminalSettings::instance().fontSize.value()); + + setFont(f); +} + +void TerminalWidget::setupColors() +{ + // Check if the colors have changed. + std::array newColors; + for (int i = 0; i < 16; ++i) { + newColors[i] = TerminalSettings::instance().colors[i].value(); + } + newColors[16] = TerminalSettings::instance().foregroundColor.value(); + newColors[17] = TerminalSettings::instance().backgroundColor.value(); + + if (m_currentColors == newColors) + return; + + m_currentColors = newColors; + + VTermState *vts = vterm_obtain_state(m_vterm.get()); + + auto setColor = [vts](int index, uint8_t r, uint8_t g, uint8_t b) { + VTermColor col; + vterm_color_rgb(&col, r, g, b); + vterm_state_set_palette_color(vts, index, &col); + }; + + for (int i = 0; i < 16; ++i) { + QColor c = TerminalSettings::instance().colors[i].value(); + setColor(i, c.red(), c.green(), c.blue()); + } + + VTermColor fg; + VTermColor bg; + + vterm_color_rgb(&fg, + TerminalSettings::instance().foregroundColor.value().red(), + TerminalSettings::instance().foregroundColor.value().green(), + TerminalSettings::instance().foregroundColor.value().blue()); + vterm_color_rgb(&bg, + TerminalSettings::instance().backgroundColor.value().red(), + TerminalSettings::instance().backgroundColor.value().green(), + TerminalSettings::instance().backgroundColor.value().blue()); + + vterm_state_set_default_colors(vts, &fg, &bg); + + clearContents(); +} + +void TerminalWidget::writeToPty(const QByteArray &data) +{ + if (m_ptyProcess) + m_ptyProcess->write(data); +} + +void TerminalWidget::setupVTerm() +{ + vterm_set_utf8(m_vterm.get(), true); + + static auto writeToPty = [](const char *s, size_t len, void *user) { + auto p = static_cast(user); + p->writeToPty(QByteArray(s, static_cast(len))); + }; + + vterm_output_set_callback(m_vterm.get(), writeToPty, this); + + memset(&m_vtermScreenCallbacks, 0, sizeof(m_vtermScreenCallbacks)); + + m_vtermScreenCallbacks.damage = [](VTermRect rect, void *user) { + auto p = static_cast(user); + p->invalidate(rect); + return 1; + }; + m_vtermScreenCallbacks.sb_pushline = [](int cols, const VTermScreenCell *cells, void *user) { + auto p = static_cast(user); + return p->sb_pushline(cols, cells); + }; + m_vtermScreenCallbacks.sb_popline = [](int cols, VTermScreenCell *cells, void *user) { + auto p = static_cast(user); + return p->sb_popline(cols, cells); + }; + m_vtermScreenCallbacks.settermprop = [](VTermProp prop, VTermValue *val, void *user) { + auto p = static_cast(user); + return p->setTerminalProperties(prop, val); + }; + m_vtermScreenCallbacks.movecursor = [](VTermPos pos, VTermPos oldpos, int visible, void *user) { + auto p = static_cast(user); + return p->movecursor(pos, oldpos, visible); + }; + + m_vtermScreenCallbacks.sb_clear = [](void *user) { + auto p = static_cast(user); + return p->sb_clear(); + }; + + vterm_screen_set_callbacks(m_vtermScreen, &m_vtermScreenCallbacks, this); + vterm_screen_set_damage_merge(m_vtermScreen, VTERM_DAMAGE_SCROLL); + vterm_screen_enable_altscreen(m_vtermScreen, true); + + VTermState *vts = vterm_obtain_state(m_vterm.get()); + vterm_state_set_bold_highbright(vts, true); + + vterm_screen_reset(m_vtermScreen, 1); +} + +void TerminalWidget::setFont(const QFont &font) +{ + m_font = font; + + //QRawFont rawFont = QRawFont::fromFont(m_font); + m_textLayout.setFont(m_font); + + QFontMetricsF qfm{m_font}; + const auto w = [qfm]() -> qreal { + if (HostOsInfo::isMacHost()) + return qfm.maxWidth(); + return qfm.averageCharWidth(); + }(); + + qCInfo(terminalLog) << font.family() << font.pointSize() << w << size(); + + m_cellSize = {w, qfm.height()}; + m_cellBaseline = qfm.ascent(); + m_lineSpacing = qfm.height(); + + QAbstractScrollArea::setFont(m_font); + + if (m_ptyProcess) { + applySizeChange(); + } +} + +QAction &TerminalWidget::copyAction() +{ + return m_copyAction; +} + +QAction &TerminalWidget::pasteAction() +{ + return m_pasteAction; +} + +QAction &TerminalWidget::clearSelectionAction() +{ + return m_clearSelectionAction; +} + +QAction &TerminalWidget::zoomInAction() +{ + return m_zoomInAction; +} + +QAction &TerminalWidget::zoomOutAction() +{ + return m_zoomOutAction; +} + +void TerminalWidget::copyToClipboard() const +{ + if (m_selection) { + const size_t startLine = qFloor(m_selection->start.y() / m_lineSpacing); + const size_t endLine = qFloor(m_selection->end.y() / m_lineSpacing); + + QString selectedText; + size_t row = startLine; + for (; row < m_scrollback->size(); row++) { + const Internal::Scrollback::Line &line = m_scrollback->line((m_scrollback->size() - 1) + - row); + if (row > endLine) + break; + + const QTextLayout &layout = line.layout(m_layoutVersion, m_font, m_lineSpacing); + const std::optional range + = selectionToFormatRange(*m_selection, layout, row); + if (range) + selectedText.append(line.layout(m_layoutVersion, m_font, m_lineSpacing) + .text() + .mid(range->start, range->length) + .trimmed()); + + if (endLine > row) + selectedText.append(QChar::LineFeed); + } + + if (row <= endLine) { + const std::optional range + = selectionToFormatRange(*m_selection, m_textLayout, m_scrollback->size()); + if (range) + selectedText.append(m_textLayout.text() + .mid(range->start, range->length) + .replace(QChar::LineSeparator, QChar::LineFeed) + .trimmed()); + } + + setClipboardAndSelection(selectedText); + } +} +void TerminalWidget::pasteFromClipboard() +{ + QClipboard *clipboard = QApplication::clipboard(); + const QString clipboardText = clipboard->text(QClipboard::Clipboard); + + if (clipboardText.isEmpty()) + return; + + vterm_keyboard_start_paste(m_vterm.get()); + for (unsigned int ch : clipboardText.toUcs4()) + vterm_keyboard_unichar(m_vterm.get(), ch, VTERM_MOD_NONE); + vterm_keyboard_end_paste(m_vterm.get()); + + if (!m_altscreen && m_scrollback->offset()) { + m_scrollback->unscroll(); + viewport()->update(); + } +} + +void TerminalWidget::clearSelection() +{ + m_selection.reset(); + update(); +} +void TerminalWidget::zoomIn() +{ + m_layoutVersion++; + m_font.setPointSize(m_font.pointSize() + 1); + setFont(m_font); +} +void TerminalWidget::zoomOut() +{ + m_layoutVersion++; + m_font.setPointSize(qMax(m_font.pointSize() - 1, 1)); + setFont(m_font); +} + +void TerminalWidget::clearContents() +{ + // Fake a scrollback clearing + QByteArray data{"\x1b[3J"}; + vterm_input_write(m_vterm.get(), data.constData(), data.size()); + vterm_screen_flush_damage(m_vtermScreen); + + // Send Ctrl+L which will clear the screen + writeToPty(QByteArray("\f")); +} + +void TerminalWidget::onReadyRead() +{ + QByteArray data = m_ptyProcess->readAll(); + vterm_input_write(m_vterm.get(), data.constData(), data.size()); + vterm_screen_flush_damage(m_vtermScreen); +} + +const VTermScreenCell *TerminalWidget::fetchCell(int x, int y) const +{ + QTC_ASSERT(y >= 0, return nullptr); + QTC_ASSERT(y < m_vtermSize.height(), return nullptr); + + static VTermScreenCell refCell{}; + VTermPos vtp{y, x}; + vterm_screen_get_cell(m_vtermScreen, vtp, &refCell); + vterm_screen_convert_color_to_rgb(m_vtermScreen, &refCell.fg); + vterm_screen_convert_color_to_rgb(m_vtermScreen, &refCell.bg); + return &refCell; +}; + +QPoint TerminalWidget::viewportToGlobal(QPoint p) const +{ + int y = p.y() - topMargin(); + const double offset = (m_scrollback->size() - m_scrollback->offset()) * m_lineSpacing; + y += offset; + + return {p.x(), y}; +} + +void TerminalWidget::createTextLayout() +{ + QElapsedTimer t; + t.start(); + + VTermColor defaultBg; + if (!m_altscreen) { + VTermColor defaultFg; + vterm_state_get_default_colors(vterm_obtain_state(m_vterm.get()), &defaultFg, &defaultBg); + // We want to compare the cell bg against this later and cells don't + // set DEFAULT_BG + defaultBg.type = VTERM_COLOR_RGB; + } else { + // This is a slightly better guess when in an altscreen + const VTermScreenCell *cell = fetchCell(0, 0); + defaultBg = cell->bg; + } + + m_textLayout.clearLayout(); + + QString allText; + + Internal::createTextLayout(m_textLayout, + allText, + defaultBg, + QRect({0, 0}, m_vtermSize), + m_lineSpacing, + [this](int x, int y) { return fetchCell(x, y); }); + + qCInfo(terminalLog) << "createTextLayout took:" << t.elapsed() << "ms"; +} + +qreal TerminalWidget::topMargin() const +{ + return size().height() - (m_vtermSize.height() * m_lineSpacing); +} + +std::optional TerminalWidget::selectionToFormatRange( + TerminalWidget::Selection selection, const QTextLayout &layout, int rowOffset) const +{ + int selectionStartLine = qFloor(selection.start.y() / m_lineSpacing) - rowOffset; + int selectionEndLine = qFloor(selection.end.y() / m_lineSpacing) - rowOffset; + + int nRows = layout.lineCount(); + + if (selectionStartLine < nRows && selectionEndLine >= 0) { + QTextLine lStart = layout.lineAt(qMax(0, qMin(selectionStartLine, nRows))); + QTextLine lEnd = layout.lineAt(qMin(nRows - 1, qMax(0, selectionEndLine))); + + int startPos = 0; + int endPos = lEnd.textLength(); + + if (selectionStartLine >= 0) + startPos = lStart.xToCursor(selection.start.x()); + if (selectionEndLine < (nRows)) + endPos = lEnd.xToCursor(selection.end.x()); + + QTextLayout::FormatRange range; + range.start = startPos; + range.length = endPos - startPos; + range.format.setBackground(QColor::fromRgbF(1.0, 1.0, 1.0, 0.5)); + return range; + } + + return {}; +} + +void TerminalWidget::paintEvent(QPaintEvent *event) +{ + event->accept(); + QPainter p(viewport()); + + p.setCompositionMode(QPainter::CompositionMode_Source); + + VTermColor defaultBg; + if (!m_altscreen) { + VTermColor defaultFg; + vterm_state_get_default_colors(vterm_obtain_state(m_vterm.get()), &defaultFg, &defaultBg); + // We want to compare the cell bg against this later and cells don't + // set DEFAULT_BG + defaultBg.type = VTERM_COLOR_RGB; + } else { + // This is a slightly better guess when in an altscreen + const VTermScreenCell *cell = fetchCell(0, 0); + defaultBg = cell->bg; + } + + p.fillRect(event->rect(), Internal::toQColor(defaultBg)); + + unsigned long off = m_scrollback->size() - m_scrollback->offset(); + + // transform painter according to scroll offsets + QPointF offset{0, -(off * m_lineSpacing)}; + + qreal margin = topMargin(); + qreal y = offset.y() + margin; + + size_t row = qFloor((offset.y() * -1) / m_lineSpacing); + y += row * m_lineSpacing; + for (; row < m_scrollback->size(); row++) { + if (y >= 0 && y < viewport()->height()) { + const Internal::Scrollback::Line &line = m_scrollback->line((m_scrollback->size() - 1) + - row); + + QList selections; + + if (m_selection) { + const std::optional range + = selectionToFormatRange(m_selection.value(), + line.layout(m_layoutVersion, m_font, m_lineSpacing), + row); + if (range) { + selections.append(range.value()); + } + } + line.layout(m_layoutVersion, m_font, m_lineSpacing).draw(&p, {0.0, y}, selections); + } + + y += m_lineSpacing; + } + + // Draw the live part + if (y < m_vtermSize.height() * m_lineSpacing) { + QList selections; + + if (m_selection) { + const std::optional range + = selectionToFormatRange(m_selection.value(), m_textLayout, row); + if (range) { + selections.append(range.value()); + } + } + + m_textLayout.draw(&p, {0.0, y}, selections); + + if (m_cursor.visible && m_preEditString.isEmpty()) { + p.setPen(QColor::fromRgb(0xFF, 0xFF, 0xFF)); + if (m_textLayout.lineCount() > m_cursor.row) { + QTextLine cursorLine = m_textLayout.lineAt(m_cursor.row); + if (cursorLine.isValid()) { + QFontMetricsF fm(m_font); + const QString text = m_textLayout.text(); + const QList asUcs4 = text.toUcs4(); + const int textStart = cursorLine.textStart(); + const int cPos = textStart + m_cursor.col; + if (cPos >= 0 && cPos < asUcs4.size()) { + const unsigned int ch = asUcs4.at(cPos); + const qreal br = fm.horizontalAdvance(QString::fromUcs4(&ch, 1)); + const qreal xCursor = cursorLine.cursorToX(cPos); + const double yCursor = cursorLine.y() + y; + const QRectF cursorRect = QRectF{xCursor, yCursor, br, m_lineSpacing}; + if (hasFocus()) { + QPainter::CompositionMode oldMode = p.compositionMode(); + p.setCompositionMode(QPainter::RasterOp_NotDestination); + p.fillRect(cursorRect, p.pen().brush()); + p.setCompositionMode(oldMode); + } else { + p.drawRect(cursorRect); + } + } + } + } + } + + if (!m_preEditString.isEmpty()) { + // TODO: Use QTextLayout::setPreeditArea() instead ? + QTextLine cursorLine = m_textLayout.lineAt(m_cursor.row); + if (cursorLine.isValid()) { + int pos = cursorLine.textStart() + m_cursor.col; + QPointF displayPos = QPointF{cursorLine.cursorToX(pos), cursorLine.y()}; + + p.fillRect(QRectF{displayPos.toPoint(), m_cellSize}, QColor::fromRgb(0, 0, 0)); + p.setPen(Qt::white); + displayPos.setY(displayPos.y() + m_cellBaseline); + p.drawText(displayPos, m_preEditString); + } + } + } + + p.fillRect(QRectF{{0, 0}, QSizeF{(qreal) width(), margin}}, Internal::toQColor(defaultBg)); +} + +void TerminalWidget::keyPressEvent(QKeyEvent *event) +{ + event->accept(); + + if (event->modifiers() == Qt::NoModifier && event->key() == Qt::Key_Escape && m_selection) { + clearSelectionAction().trigger(); + return; + } + + if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_Plus) { + zoomInAction().trigger(); + return; + } + + if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_Minus) { + zoomOutAction().trigger(); + return; + } + + if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_C) { + copyAction().trigger(); + return; + } + + if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_V) { + pasteAction().trigger(); + return; + } + + bool keypad = event->modifiers() & Qt::KeypadModifier; + VTermModifier mod = Internal::qtModifierToVTerm(event->modifiers()); + VTermKey key = Internal::qtKeyToVTerm(Qt::Key(event->key()), keypad); + + if (key != VTERM_KEY_NONE) { + if (mod == VTERM_MOD_SHIFT && (key == VTERM_KEY_ESCAPE || key == VTERM_KEY_BACKSPACE)) + mod = VTERM_MOD_NONE; + + vterm_keyboard_key(m_vterm.get(), key, mod); + } else if (event->text().length() == 1) { + // This maps to delete word and is way to easy to mistakenly type + // if (event->key() == Qt::Key_Space && mod == VTERM_MOD_SHIFT) + // mod = VTERM_MOD_NONE; + + // Per https://github.com/justinmk/neovim/commit/317d5ca7b0f92ef42de989b3556ca9503f0a3bf6 + // libvterm prefers we send the full keycode rather than sending the + // ctrl modifier. This helps with ncurses applications which otherwise + // do not recognize ctrl+ and in the shell for getting common control characters + // like ctrl+i for tab or ctrl+j for newline. + vterm_keyboard_unichar(m_vterm.get(), + event->text().toUcs4()[0], + static_cast(mod & ~VTERM_MOD_CTRL)); + } else if (mod != VTERM_MOD_NONE && event->key() == Qt::Key_C) { + vterm_keyboard_unichar(m_vterm.get(), 'c', mod); + } +} + +void TerminalWidget::applySizeChange() +{ + m_vtermSize = { + qFloor((qreal) size().width() / (qreal) m_cellSize.width()), + qFloor((qreal) size().height() / m_lineSpacing), + }; + + if (m_vtermSize.height() <= 0) + m_vtermSize.setHeight(1); + + if (m_vtermSize.width() <= 0) + m_vtermSize.setWidth(1); + + if (m_ptyProcess) + m_ptyProcess->resize(m_vtermSize.width(), m_vtermSize.height()); + + vterm_set_size(m_vterm.get(), m_vtermSize.height(), m_vtermSize.width()); + vterm_screen_flush_damage(m_vtermScreen); +} + +void TerminalWidget::updateScrollBars() +{ + verticalScrollBar()->setRange(0, static_cast(m_scrollback->size())); + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); +} + +void TerminalWidget::resizeEvent(QResizeEvent *event) +{ + event->accept(); + + // If increasing in size, we'll trigger libvterm to call sb_popline in + // order to pull lines out of the history. This will cause the scrollback + // to decrease in size which reduces the size of the verticalScrollBar. + // That will trigger a scroll offset increase which we want to ignore. + m_ignoreScroll = true; + + applySizeChange(); + + m_selection.reset(); + m_ignoreScroll = false; +} + +void TerminalWidget::invalidate(VTermRect rect) +{ + Q_UNUSED(rect); + createTextLayout(); + + viewport()->update(); +} + +int TerminalWidget::sb_pushline(int cols, const VTermScreenCell *cells) +{ + m_scrollback->emplace(cols, cells, vterm_obtain_state(m_vterm.get())); + + updateScrollBars(); + + return 1; +} + +int TerminalWidget::sb_popline(int cols, VTermScreenCell *cells) +{ + if (m_scrollback->size() == 0) + return 0; + + m_scrollback->popto(cols, cells); + + updateScrollBars(); + + return 1; +} + +int TerminalWidget::sb_clear() +{ + m_scrollback->clear(); + updateScrollBars(); + + return 1; +} + +void TerminalWidget::wheelEvent(QWheelEvent *event) +{ + event->accept(); + + QPoint delta = event->angleDelta(); + scrollContentsBy(0, delta.y() / 24); +} + +void TerminalWidget::focusInEvent(QFocusEvent *) +{ + viewport()->update(); +} +void TerminalWidget::focusOutEvent(QFocusEvent *) +{ + viewport()->update(); +} + +void TerminalWidget::inputMethodEvent(QInputMethodEvent *event) +{ + m_preEditString = event->preeditString(); + + if (event->commitString().isEmpty()) { + viewport()->update(); + return; + } + + for (const unsigned int ch : event->commitString().toUcs4()) { + vterm_keyboard_unichar(m_vterm.get(), ch, VTERM_MOD_NONE); + } +} + +void TerminalWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + m_selectionStartPos = event->pos(); + + QPoint pos = viewportToGlobal(event->pos()); + m_selection = Selection{pos, pos}; + viewport()->update(); + } +} +void TerminalWidget::mouseMoveEvent(QMouseEvent *event) +{ + if (m_selection && event->buttons() & Qt::LeftButton) { + QPoint start = viewportToGlobal(m_selectionStartPos); + QPoint newEnd = viewportToGlobal(event->pos()); + + if (start.y() > newEnd.y() || (start.y() == newEnd.y() && start.x() > newEnd.x())) + std::swap(start, newEnd); + + m_selection->start = start; + m_selection->end = newEnd; + + viewport()->update(); + } +} + +void TerminalWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_selection && event->button() != Qt::LeftButton) { + if ((m_selectionStartPos - event->pos()).manhattanLength() < 2) { + m_selection.reset(); + viewport()->update(); + } + } +} + +void TerminalWidget::mouseDoubleClickEvent(QMouseEvent *event) +{ + // TODO :( + Q_UNUSED(event); + viewport()->update(); +} + +void TerminalWidget::scrollContentsBy(int dx, int dy) +{ + Q_UNUSED(dx); + + if (m_ignoreScroll) + return; + + if (m_altscreen) + return; + + size_t orig = m_scrollback->offset(); + size_t offset = m_scrollback->scroll(dy); + if (orig == offset) + return; + + m_cursor.visible = (offset == 0); + viewport()->update(); +} + +void TerminalWidget::showEvent(QShowEvent *event) +{ + Q_UNUSED(event); + + if (!m_ptyProcess) + setupPty(); + + QAbstractScrollArea::showEvent(event); +} + +bool TerminalWidget::event(QEvent *event) +{ + if (event->type() == QEvent::ShortcutOverride) { + if (hasFocus()) { + event->accept(); + return true; + } + } + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *k = (QKeyEvent *) event; + keyPressEvent(k); + return true; + } + if (event->type() == QEvent::KeyRelease) { + QKeyEvent *k = (QKeyEvent *) event; + keyReleaseEvent(k); + return true; + } + + return QAbstractScrollArea::event(event); +} + +int TerminalWidget::setTerminalProperties(VTermProp prop, VTermValue *val) +{ + switch (prop) { + case VTERM_PROP_CURSORVISIBLE: + m_cursor.visible = val->boolean; + break; + case VTERM_PROP_CURSORBLINK: + qCDebug(terminalLog) << "Ignoring VTERM_PROP_CURSORBLINK" << val->boolean; + break; + case VTERM_PROP_CURSORSHAPE: + qCDebug(terminalLog) << "Ignoring VTERM_PROP_CURSORSHAPE" << val->number; + break; + case VTERM_PROP_ICONNAME: + //emit iconTextChanged(val->string); + break; + case VTERM_PROP_TITLE: + //emit titleChanged(val->string); + setWindowTitle(QString::fromUtf8(val->string.str, val->string.len)); + break; + case VTERM_PROP_ALTSCREEN: + m_altscreen = val->boolean; + m_selection.reset(); + break; + case VTERM_PROP_MOUSE: + qCDebug(terminalLog) << "Ignoring VTERM_PROP_MOUSE" << val->number; + break; + case VTERM_PROP_REVERSE: + qCDebug(terminalLog) << "Ignoring VTERM_PROP_REVERSE" << val->boolean; + break; + case VTERM_N_PROPS: + break; + } + return 1; +} + +int TerminalWidget::movecursor(VTermPos pos, VTermPos oldpos, int visible) +{ + Q_UNUSED(oldpos); + viewport()->update(); + m_cursor.row = pos.row; + m_cursor.col = pos.col; + m_cursor.visible = visible; + + return 1; +} + +} // namespace Terminal diff --git a/src/plugins/terminal/terminalwidget.h b/src/plugins/terminal/terminalwidget.h new file mode 100644 index 00000000000..3f1590dfaaf --- /dev/null +++ b/src/plugins/terminal/terminalwidget.h @@ -0,0 +1,164 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "scrollback.h" + +#include + +#include +#include +#include +#include + +#include +#include + +#include + +namespace Terminal { + +class TerminalWidget : public QAbstractScrollArea +{ + Q_OBJECT +public: + TerminalWidget(QWidget *parent = nullptr, + const Utils::Terminal::OpenTerminalParameters &openParameters = {}); + + void setFont(const QFont &font); + + QAction ©Action(); + QAction &pasteAction(); + + QAction &clearSelectionAction(); + + QAction &zoomInAction(); + QAction &zoomOutAction(); + + void copyToClipboard() const; + void pasteFromClipboard(); + + void clearSelection(); + + void zoomIn(); + void zoomOut(); + + void clearContents(); + + struct Selection + { + QPoint start; + QPoint end; + }; + +signals: + void started(qint64 pid); + +protected: + void paintEvent(QPaintEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void focusInEvent(QFocusEvent *event) override; + void focusOutEvent(QFocusEvent *event) override; + void inputMethodEvent(QInputMethodEvent *event) override; + + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + + void scrollContentsBy(int dx, int dy) override; + + void showEvent(QShowEvent *event) override; + + bool event(QEvent *event) override; + +protected: + void onReadyRead(); + void setupVTerm(); + void setupFont(); + void setupPty(); + void setupColors(); + + void writeToPty(const QByteArray &data); + + void createTextLayout(); + + // Callbacks from vterm + void invalidate(VTermRect rect); + int sb_pushline(int cols, const VTermScreenCell *cells); + int sb_popline(int cols, VTermScreenCell *cells); + int sb_clear(); + int setTerminalProperties(VTermProp prop, VTermValue *val); + int movecursor(VTermPos pos, VTermPos oldpos, int visible); + + const VTermScreenCell *fetchCell(int x, int y) const; + + qreal topMargin() const; + + QPoint viewportToGlobal(QPoint p) const; + + int textLineFromPixel(int y) const; + std::optional textPosFromPoint(const QTextLayout &textLayout, QPoint p) const; + + std::optional selectionToFormatRange( + TerminalWidget::Selection selection, const QTextLayout &layout, int rowOffset) const; + + void applySizeChange(); + + void updateScrollBars(); + +private: + std::unique_ptr m_ptyProcess; + + std::unique_ptr m_vterm; + VTermScreen *m_vtermScreen; + QSize m_vtermSize; + + QFont m_font; + QSizeF m_cellSize; + qreal m_cellBaseline; + qreal m_lineSpacing; + + bool m_altscreen{false}; + bool m_ignoreScroll{false}; + + QString m_preEditString; + + std::optional m_selection; + QPoint m_selectionStartPos; + + std::unique_ptr m_scrollback; + + QTextLayout m_textLayout; + + struct + { + int row{0}; + int col{0}; + bool visible{false}; + } m_cursor; + + VTermScreenCallbacks m_vtermScreenCallbacks; + + QAction m_copyAction; + QAction m_pasteAction; + + QAction m_clearSelectionAction; + + QAction m_zoomInAction; + QAction m_zoomOutAction; + + QTimer m_readDelayTimer; + int m_readDelayRestarts{0}; + + int m_layoutVersion{0}; + + std::array m_currentColors; + + Utils::Terminal::OpenTerminalParameters m_openParameters; +}; + +} // namespace Terminal