summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMarcus Tillmanns <marcus.tillmanns@qt.io>2023-03-10 13:55:17 +0100
committerMarcus Tillmanns <marcus.tillmanns@qt.io>2023-03-16 06:08:09 +0000
commitbd52e53dbfa7641098313edd56d885df69916f9f (patch)
tree161502368e1ebabb785bd9db668d028e65005fcd /src
parent3507229a007e3fd5f004a94f4c3857518ab1c9df (diff)
downloadqt-creator-bd52e53dbfa7641098313edd56d885df69916f9f.tar.gz
Terminal: Add shell integration
Change-Id: Ic1e226b56f0103e5a6e7764073ab7ab241b67baa Reviewed-by: Cristian Adam <cristian.adam@qt.io>
Diffstat (limited to 'src')
-rw-r--r--src/libs/utils/fileutils.cpp1
-rw-r--r--src/libs/utils/fileutils.h1
-rw-r--r--src/libs/utils/qtcprocess.cpp4
-rw-r--r--src/plugins/terminal/CMakeLists.txt1
-rw-r--r--src/plugins/terminal/shellintegration.cpp143
-rw-r--r--src/plugins/terminal/shellintegration.h34
-rwxr-xr-xsrc/plugins/terminal/shellintegrations/shellintegration-bash.sh252
-rw-r--r--src/plugins/terminal/shellintegrations/shellintegration-env.zsh15
-rw-r--r--src/plugins/terminal/shellintegrations/shellintegration-login.zsh7
-rw-r--r--src/plugins/terminal/shellintegrations/shellintegration-profile.zsh15
-rw-r--r--src/plugins/terminal/shellintegrations/shellintegration-rc.zsh160
-rw-r--r--src/plugins/terminal/shellintegrations/shellintegration.fish122
-rw-r--r--src/plugins/terminal/shellintegrations/shellintegration.ps1158
-rw-r--r--src/plugins/terminal/terminal.qrc15
-rw-r--r--src/plugins/terminal/terminalpane.cpp18
-rw-r--r--src/plugins/terminal/terminalsurface.cpp34
-rw-r--r--src/plugins/terminal/terminalsurface.h5
-rw-r--r--src/plugins/terminal/terminalwidget.cpp37
-rw-r--r--src/plugins/terminal/terminalwidget.h9
-rwxr-xr-xsrc/plugins/terminal/tests/integration70
20 files changed, 1087 insertions, 14 deletions
diff --git a/src/libs/utils/fileutils.cpp b/src/libs/utils/fileutils.cpp
index c0a574f9c3..4bd0692e13 100644
--- a/src/libs/utils/fileutils.cpp
+++ b/src/libs/utils/fileutils.cpp
@@ -5,7 +5,6 @@
#include "savefile.h"
#include "algorithm.h"
-#include "hostosinfo.h"
#include "qtcassert.h"
#include "utilstr.h"
diff --git a/src/libs/utils/fileutils.h b/src/libs/utils/fileutils.h
index 19f17a6d71..a1a7ffef97 100644
--- a/src/libs/utils/fileutils.h
+++ b/src/libs/utils/fileutils.h
@@ -121,7 +121,6 @@ public:
QString *selectedFilter = nullptr,
QFileDialog::Options options = {});
#endif
-
};
// for actually finding out if e.g. directories are writable on Windows
diff --git a/src/libs/utils/qtcprocess.cpp b/src/libs/utils/qtcprocess.cpp
index a5de95d29d..21b1684242 100644
--- a/src/libs/utils/qtcprocess.cpp
+++ b/src/libs/utils/qtcprocess.cpp
@@ -355,7 +355,9 @@ public:
bool startResult
= m_ptyProcess->startProcess(program,
- arguments,
+ HostOsInfo::isWindowsHost()
+ ? QStringList{m_setup.m_nativeArguments} << arguments
+ : arguments,
m_setup.m_workingDirectory.path(),
m_setup.m_environment.toProcessEnvironment().toStringList(),
m_setup.m_ptyData->size().width(),
diff --git a/src/plugins/terminal/CMakeLists.txt b/src/plugins/terminal/CMakeLists.txt
index c0972eccf3..f68a0b2ce4 100644
--- a/src/plugins/terminal/CMakeLists.txt
+++ b/src/plugins/terminal/CMakeLists.txt
@@ -7,6 +7,7 @@ add_qtc_plugin(Terminal
glyphcache.cpp glyphcache.h
keys.cpp keys.h
scrollback.cpp scrollback.h
+ shellintegration.cpp shellintegration.h
shellmodel.cpp shellmodel.h
terminal.qrc
terminalpane.cpp terminalpane.h
diff --git a/src/plugins/terminal/shellintegration.cpp b/src/plugins/terminal/shellintegration.cpp
new file mode 100644
index 0000000000..fd4c136469
--- /dev/null
+++ b/src/plugins/terminal/shellintegration.cpp
@@ -0,0 +1,143 @@
+// 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 "shellintegration.h"
+
+#include <utils/environment.h>
+#include <utils/filepath.h>
+#include <utils/stringutils.h>
+
+#include <QLoggingCategory>
+
+Q_LOGGING_CATEGORY(integrationLog, "qtc.terminal.shellintegration", QtWarningMsg)
+
+using namespace Utils;
+
+namespace Terminal {
+
+struct FileToCopy
+{
+ FilePath source;
+ QString destName;
+};
+
+// clang-format off
+struct
+{
+ struct
+ {
+ FilePath rcFile{":/terminal/shellintegrations/shellintegration-bash.sh"};
+ } bash;
+ struct
+ {
+ QList<FileToCopy> files{
+ {":/terminal/shellintegrations/shellintegration-env.zsh", ".zshenv"},
+ {":/terminal/shellintegrations/shellintegration-login.zsh", ".zlogin"},
+ {":/terminal/shellintegrations/shellintegration-profile.zsh", ".zprofile"},
+ {":/terminal/shellintegrations/shellintegration-rc.zsh", ".zshrc"}
+ };
+ } zsh;
+ struct
+ {
+ FilePath script{":/terminal/shellintegrations/shellintegration.ps1"};
+ } pwsh;
+
+} filesToCopy;
+// clang-format on
+
+bool ShellIntegration::canIntegrate(const Utils::CommandLine &cmdLine)
+{
+ if (cmdLine.executable().needsDevice())
+ return false; // TODO: Allow integration for remote shells
+
+ if (!cmdLine.arguments().isEmpty())
+ return false;
+
+ if (cmdLine.executable().baseName() == "bash")
+ return true;
+
+ if (cmdLine.executable().baseName() == "zsh")
+ return true;
+
+ if (cmdLine.executable().baseName() == "pwsh"
+ || cmdLine.executable().baseName() == "powershell") {
+ return true;
+ }
+
+ return false;
+}
+
+void ShellIntegration::onOsc(int cmd, const VTermStringFragment &fragment)
+{
+ QString d = QString::fromLocal8Bit(fragment.str, fragment.len);
+ const auto [command, data] = Utils::splitAtFirst(d, ';');
+
+ if (cmd == 1337) {
+ const auto [key, value] = Utils::splitAtFirst(command, '=');
+ if (key == QStringView(u"CurrentDir"))
+ emit currentDirChanged(FilePath::fromUserInput(value.toString()).path());
+
+ } else if (cmd == 7) {
+ emit currentDirChanged(FilePath::fromUserInput(d).path());
+ } else if (cmd == 133) {
+ qCDebug(integrationLog) << "OSC 133:" << data;
+ } else if (cmd == 633 && command.length() == 1) {
+ if (command[0] == 'E') {
+ CommandLine cmdLine = CommandLine::fromUserInput(data.toString());
+ emit commandChanged(cmdLine);
+ } else if (command[0] == 'D') {
+ emit commandChanged({});
+ } else if (command[0] == 'P') {
+ const auto [key, value] = Utils::splitAtFirst(data, '=');
+ if (key == QStringView(u"Cwd"))
+ emit currentDirChanged(value.toString());
+ }
+ }
+}
+
+void ShellIntegration::prepareProcess(Utils::QtcProcess &process)
+{
+ Environment env = process.environment().hasChanges() ? process.environment()
+ : Environment::systemEnvironment();
+ CommandLine cmd = process.commandLine();
+
+ if (!canIntegrate(cmd))
+ return;
+
+ env.set("VSCODE_INJECTION", "1");
+
+ if (cmd.executable().baseName() == "bash") {
+ const FilePath rcPath = filesToCopy.bash.rcFile;
+ const FilePath tmpRc = FilePath::fromUserInput(
+ m_tempDir.filePath(filesToCopy.bash.rcFile.fileName()));
+ rcPath.copyFile(tmpRc);
+
+ cmd.addArgs({"--init-file", tmpRc.nativePath()});
+ } else if (cmd.executable().baseName() == "zsh") {
+ for (const FileToCopy &file : filesToCopy.zsh.files) {
+ const auto copyResult = file.source.copyFile(
+ FilePath::fromUserInput(m_tempDir.filePath(file.destName)));
+ QTC_ASSERT_EXPECTED(copyResult, return);
+ }
+
+ const Utils::FilePath originalZdotDir = FilePath::fromUserInput(
+ env.value_or("ZDOTDIR", QDir::homePath()));
+
+ env.set("ZDOTDIR", m_tempDir.path());
+ env.set("USER_ZDOTDIR", originalZdotDir.nativePath());
+ } else if (cmd.executable().baseName() == "pwsh"
+ || cmd.executable().baseName() == "powershell") {
+ const FilePath rcPath = filesToCopy.pwsh.script;
+ const FilePath tmpRc = FilePath::fromUserInput(
+ m_tempDir.filePath(filesToCopy.pwsh.script.fileName()));
+ rcPath.copyFile(tmpRc);
+
+ cmd.addArgs(QString("-noexit -command try { . \"%1\" } catch {}{1}").arg(tmpRc.nativePath()),
+ CommandLine::Raw);
+ }
+
+ process.setCommand(cmd);
+ process.setEnvironment(env);
+}
+
+} // namespace Terminal
diff --git a/src/plugins/terminal/shellintegration.h b/src/plugins/terminal/shellintegration.h
new file mode 100644
index 0000000000..264b5a4d67
--- /dev/null
+++ b/src/plugins/terminal/shellintegration.h
@@ -0,0 +1,34 @@
+// 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 <utils/commandline.h>
+#include <utils/qtcprocess.h>
+
+#include <vterm.h>
+
+#include <QTemporaryDir>
+
+namespace Terminal {
+
+class ShellIntegration : public QObject
+{
+ Q_OBJECT
+public:
+ static bool canIntegrate(const Utils::CommandLine &cmdLine);
+
+ void onOsc(int cmd, const VTermStringFragment &fragment);
+
+ void prepareProcess(Utils::QtcProcess &process);
+
+signals:
+ void commandChanged(const Utils::CommandLine &command);
+ void currentDirChanged(const QString &dir);
+
+private:
+ QTemporaryDir m_tempDir;
+};
+
+} // namespace Terminal
diff --git a/src/plugins/terminal/shellintegrations/shellintegration-bash.sh b/src/plugins/terminal/shellintegrations/shellintegration-bash.sh
new file mode 100755
index 0000000000..7db188be08
--- /dev/null
+++ b/src/plugins/terminal/shellintegrations/shellintegration-bash.sh
@@ -0,0 +1,252 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# SPDX-License-Identifier: MIT
+
+
+# Prevent the script recursing when setting up
+if [[ -n "$VSCODE_SHELL_INTEGRATION" ]]; then
+ builtin return
+fi
+
+VSCODE_SHELL_INTEGRATION=1
+
+# Run relevant rc/profile only if shell integration has been injected, not when run manually
+if [ "$VSCODE_INJECTION" == "1" ]; then
+ if [ -z "$VSCODE_SHELL_LOGIN" ]; then
+ if [ -r ~/.bashrc ]; then
+ . ~/.bashrc
+ fi
+ else
+ # Imitate -l because --init-file doesn't support it:
+ # run the first of these files that exists
+ if [ -r /etc/profile ]; then
+ . /etc/profile
+ fi
+ # exceute the first that exists
+ if [ -r ~/.bash_profile ]; then
+ . ~/.bash_profile
+ elif [ -r ~/.bash_login ]; then
+ . ~/.bash_login
+ elif [ -r ~/.profile ]; then
+ . ~/.profile
+ fi
+ builtin unset VSCODE_SHELL_LOGIN
+
+ # Apply any explicit path prefix (see #99878)
+ if [ -n "$VSCODE_PATH_PREFIX" ]; then
+ export PATH=$VSCODE_PATH_PREFIX$PATH
+ builtin unset VSCODE_PATH_PREFIX
+ fi
+ fi
+ builtin unset VSCODE_INJECTION
+fi
+
+if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then
+ builtin return
+fi
+
+__vsc_get_trap() {
+ # 'trap -p DEBUG' outputs a shell command like `trap -- '…shellcode…' DEBUG`.
+ # The terms are quoted literals, but are not guaranteed to be on a single line.
+ # (Consider a trap like $'echo foo\necho \'bar\'').
+ # To parse, we splice those terms into an expression capturing them into an array.
+ # This preserves the quoting of those terms: when we `eval` that expression, they are preserved exactly.
+ # This is different than simply exploding the string, which would split everything on IFS, oblivious to quoting.
+ builtin local -a terms
+ builtin eval "terms=( $(trap -p "${1:-DEBUG}") )"
+ # |________________________|
+ # |
+ # \-------------------*--------------------/
+ # terms=( trap -- '…arbitrary shellcode…' DEBUG )
+ # |____||__| |_____________________| |_____|
+ # | | | |
+ # 0 1 2 3
+ # |
+ # \--------*----/
+ builtin printf '%s' "${terms[2]:-}"
+}
+
+# The property (P) and command (E) codes embed values which require escaping.
+# Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex.
+__vsc_escape_value() {
+ # Process text byte by byte, not by codepoint.
+ builtin local LC_ALL=C str="${1}" i byte token out=''
+
+ for (( i=0; i < "${#str}"; ++i )); do
+ byte="${str:$i:1}"
+
+ # Escape backslashes and semi-colons
+ if [ "$byte" = "\\" ]; then
+ token="\\\\"
+ elif [ "$byte" = ";" ]; then
+ token="\\x3b"
+ else
+ token="$byte"
+ fi
+
+ out+="$token"
+ done
+
+ builtin printf '%s\n' "${out}"
+}
+
+# Send the IsWindows property if the environment looks like Windows
+if [[ "$(uname -s)" =~ ^CYGWIN*|MINGW*|MSYS* ]]; then
+ builtin printf '\e]633;P;IsWindows=True\a'
+fi
+
+# Allow verifying $BASH_COMMAND doesn't have aliases resolved via history when the right HISTCONTROL
+# configuration is used
+if [[ "$HISTCONTROL" =~ .*(erasedups|ignoreboth|ignoredups).* ]]; then
+ __vsc_history_verify=0
+else
+ __vsc_history_verify=1
+fi
+
+__vsc_initialized=0
+__vsc_original_PS1="$PS1"
+__vsc_original_PS2="$PS2"
+__vsc_custom_PS1=""
+__vsc_custom_PS2=""
+__vsc_in_command_execution="1"
+__vsc_current_command=""
+
+__vsc_prompt_start() {
+ builtin printf '\e]633;A\a'
+}
+
+__vsc_prompt_end() {
+ builtin printf '\e]633;B\a'
+}
+
+__vsc_update_cwd() {
+ builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "$PWD")"
+}
+
+__vsc_command_output_start() {
+ builtin printf '\e]633;C\a'
+ builtin printf '\e]633;E;%s\a' "$(__vsc_escape_value "${__vsc_current_command}")"
+}
+
+__vsc_continuation_start() {
+ builtin printf '\e]633;F\a'
+}
+
+__vsc_continuation_end() {
+ builtin printf '\e]633;G\a'
+}
+
+__vsc_command_complete() {
+ if [ "$__vsc_current_command" = "" ]; then
+ builtin printf '\e]633;D\a'
+ else
+ builtin printf '\e]633;D;%s\a' "$__vsc_status"
+ fi
+ __vsc_update_cwd
+}
+__vsc_update_prompt() {
+ # in command execution
+ if [ "$__vsc_in_command_execution" = "1" ]; then
+ # Wrap the prompt if it is not yet wrapped, if the PS1 changed this this was last set it
+ # means the user re-exported the PS1 so we should re-wrap it
+ if [[ "$__vsc_custom_PS1" == "" || "$__vsc_custom_PS1" != "$PS1" ]]; then
+ __vsc_original_PS1=$PS1
+ __vsc_custom_PS1="\[$(__vsc_prompt_start)\]$__vsc_original_PS1\[$(__vsc_prompt_end)\]"
+ PS1="$__vsc_custom_PS1"
+ fi
+ if [[ "$__vsc_custom_PS2" == "" || "$__vsc_custom_PS2" != "$PS2" ]]; then
+ __vsc_original_PS2=$PS2
+ __vsc_custom_PS2="\[$(__vsc_continuation_start)\]$__vsc_original_PS2\[$(__vsc_continuation_end)\]"
+ PS2="$__vsc_custom_PS2"
+ fi
+ __vsc_in_command_execution="0"
+ fi
+}
+
+__vsc_precmd() {
+ __vsc_command_complete "$__vsc_status"
+ __vsc_current_command=""
+ __vsc_update_prompt
+}
+
+__vsc_preexec() {
+ __vsc_initialized=1
+ if [[ ! "$BASH_COMMAND" =~ ^__vsc_prompt* ]]; then
+ # Use history if it's available to verify the command as BASH_COMMAND comes in with aliases
+ # resolved
+ if [ "$__vsc_history_verify" = "1" ]; then
+ __vsc_current_command="$(builtin history 1 | sed 's/ *[0-9]* *//')"
+ else
+ __vsc_current_command=$BASH_COMMAND
+ fi
+ else
+ __vsc_current_command=""
+ fi
+ __vsc_command_output_start
+}
+
+# Debug trapping/preexec inspired by starship (ISC)
+if [[ -n "${bash_preexec_imported:-}" ]]; then
+ __vsc_preexec_only() {
+ if [ "$__vsc_in_command_execution" = "0" ]; then
+ __vsc_in_command_execution="1"
+ __vsc_preexec
+ fi
+ }
+ precmd_functions+=(__vsc_prompt_cmd)
+ preexec_functions+=(__vsc_preexec_only)
+else
+ __vsc_dbg_trap="$(__vsc_get_trap DEBUG)"
+
+ if [[ -z "$__vsc_dbg_trap" ]]; then
+ __vsc_preexec_only() {
+ if [ "$__vsc_in_command_execution" = "0" ]; then
+ __vsc_in_command_execution="1"
+ __vsc_preexec
+ fi
+ }
+ trap '__vsc_preexec_only "$_"' DEBUG
+ elif [[ "$__vsc_dbg_trap" != '__vsc_preexec "$_"' && "$__vsc_dbg_trap" != '__vsc_preexec_all "$_"' ]]; then
+ __vsc_preexec_all() {
+ if [ "$__vsc_in_command_execution" = "0" ]; then
+ __vsc_in_command_execution="1"
+ builtin eval "${__vsc_dbg_trap}"
+ __vsc_preexec
+ fi
+ }
+ trap '__vsc_preexec_all "$_"' DEBUG
+ fi
+fi
+
+__vsc_update_prompt
+
+__vsc_restore_exit_code() {
+ return "$1"
+}
+
+__vsc_prompt_cmd_original() {
+ __vsc_status="$?"
+ __vsc_restore_exit_code "${__vsc_status}"
+ # Evaluate the original PROMPT_COMMAND similarly to how bash would normally
+ # See https://unix.stackexchange.com/a/672843 for technique
+ for cmd in "${__vsc_original_prompt_command[@]}"; do
+ eval "${cmd:-}"
+ done
+ __vsc_precmd
+}
+
+__vsc_prompt_cmd() {
+ __vsc_status="$?"
+ __vsc_precmd
+}
+
+# PROMPT_COMMAND arrays and strings seem to be handled the same (handling only the first entry of
+# the array?)
+__vsc_original_prompt_command=$PROMPT_COMMAND
+
+if [[ -z "${bash_preexec_imported:-}" ]]; then
+ if [[ -n "$__vsc_original_prompt_command" && "$__vsc_original_prompt_command" != "__vsc_prompt_cmd" ]]; then
+ PROMPT_COMMAND=__vsc_prompt_cmd_original
+ else
+ PROMPT_COMMAND=__vsc_prompt_cmd
+ fi
+fi
diff --git a/src/plugins/terminal/shellintegrations/shellintegration-env.zsh b/src/plugins/terminal/shellintegrations/shellintegration-env.zsh
new file mode 100644
index 0000000000..3c890539ae
--- /dev/null
+++ b/src/plugins/terminal/shellintegrations/shellintegration-env.zsh
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# SPDX-License-Identifier: MIT
+
+if [[ -f $USER_ZDOTDIR/.zshenv ]]; then
+ VSCODE_ZDOTDIR=$ZDOTDIR
+ ZDOTDIR=$USER_ZDOTDIR
+
+ # prevent recursion
+ if [[ $USER_ZDOTDIR != $VSCODE_ZDOTDIR ]]; then
+ . $USER_ZDOTDIR/.zshenv
+ fi
+
+ USER_ZDOTDIR=$ZDOTDIR
+ ZDOTDIR=$VSCODE_ZDOTDIR
+fi
diff --git a/src/plugins/terminal/shellintegrations/shellintegration-login.zsh b/src/plugins/terminal/shellintegrations/shellintegration-login.zsh
new file mode 100644
index 0000000000..37ff543979
--- /dev/null
+++ b/src/plugins/terminal/shellintegrations/shellintegration-login.zsh
@@ -0,0 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# SPDX-License-Identifier: MIT
+
+ZDOTDIR=$USER_ZDOTDIR
+if [[ $options[norcs] = off && -o "login" && -f $ZDOTDIR/.zlogin ]]; then
+ . $ZDOTDIR/.zlogin
+fi
diff --git a/src/plugins/terminal/shellintegrations/shellintegration-profile.zsh b/src/plugins/terminal/shellintegrations/shellintegration-profile.zsh
new file mode 100644
index 0000000000..724e1f2879
--- /dev/null
+++ b/src/plugins/terminal/shellintegrations/shellintegration-profile.zsh
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# SPDX-License-Identifier: MIT
+
+if [[ $options[norcs] = off && -o "login" && -f $USER_ZDOTDIR/.zprofile ]]; then
+ VSCODE_ZDOTDIR=$ZDOTDIR
+ ZDOTDIR=$USER_ZDOTDIR
+ . $USER_ZDOTDIR/.zprofile
+ ZDOTDIR=$VSCODE_ZDOTDIR
+
+ # Apply any explicit path prefix (see #99878)
+ if (( ${+VSCODE_PATH_PREFIX} )); then
+ export PATH=$VSCODE_PATH_PREFIX$PATH
+ fi
+ builtin unset VSCODE_PATH_PREFIX
+fi
diff --git a/src/plugins/terminal/shellintegrations/shellintegration-rc.zsh b/src/plugins/terminal/shellintegrations/shellintegration-rc.zsh
new file mode 100644
index 0000000000..df4109131a
--- /dev/null
+++ b/src/plugins/terminal/shellintegrations/shellintegration-rc.zsh
@@ -0,0 +1,160 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# SPDX-License-Identifier: MIT
+
+builtin autoload -Uz add-zsh-hook
+
+# Prevent the script recursing when setting up
+if [ -n "$VSCODE_SHELL_INTEGRATION" ]; then
+ ZDOTDIR=$USER_ZDOTDIR
+ builtin return
+fi
+
+# This variable allows the shell to both detect that VS Code's shell integration is enabled as well
+# as disable it by unsetting the variable.
+VSCODE_SHELL_INTEGRATION=1
+
+# By default, zsh will set the $HISTFILE to the $ZDOTDIR location automatically. In the case of the
+# shell integration being injected, this means that the terminal will use a different history file
+# to other terminals. To fix this issue, set $HISTFILE back to the default location before ~/.zshrc
+# is called as that may depend upon the value.
+if [[ "$VSCODE_INJECTION" == "1" ]]; then
+ HISTFILE=$USER_ZDOTDIR/.zsh_history
+fi
+
+# Only fix up ZDOTDIR if shell integration was injected (not manually installed) and has not been called yet
+if [[ "$VSCODE_INJECTION" == "1" ]]; then
+ if [[ $options[norcs] = off && -f $USER_ZDOTDIR/.zshrc ]]; then
+ VSCODE_ZDOTDIR=$ZDOTDIR
+ ZDOTDIR=$USER_ZDOTDIR
+ # A user's custom HISTFILE location might be set when their .zshrc file is sourced below
+ . $USER_ZDOTDIR/.zshrc
+ fi
+fi
+
+# Shell integration was disabled by the shell, exit without warning assuming either the shell has
+# explicitly disabled shell integration as it's incompatible or it implements the protocol.
+if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then
+ builtin return
+fi
+
+# The property (P) and command (E) codes embed values which require escaping.
+# Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex.
+__vsc_escape_value() {
+ builtin emulate -L zsh
+
+ # Process text byte by byte, not by codepoint.
+ builtin local LC_ALL=C str="$1" i byte token out=''
+
+ for (( i = 0; i < ${#str}; ++i )); do
+ byte="${str:$i:1}"
+
+ # Escape backslashes and semi-colons
+ if [ "$byte" = "\\" ]; then
+ token="\\\\"
+ elif [ "$byte" = ";" ]; then
+ token="\\x3b"
+ else
+ token="$byte"
+ fi
+
+ out+="$token"
+ done
+
+ builtin print -r "$out"
+}
+
+__vsc_in_command_execution="1"
+__vsc_current_command=""
+
+__vsc_prompt_start() {
+ builtin printf '\e]633;A\a'
+}
+
+__vsc_prompt_end() {
+ builtin printf '\e]633;B\a'
+}
+
+__vsc_update_cwd() {
+ builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "${PWD}")"
+}
+
+__vsc_command_output_start() {
+ builtin printf '\e]633;C\a'
+ builtin printf '\e]633;E;%s\a' "${__vsc_current_command}"
+}
+
+__vsc_continuation_start() {
+ builtin printf '\e]633;F\a'
+}
+
+__vsc_continuation_end() {
+ builtin printf '\e]633;G\a'
+}
+
+__vsc_right_prompt_start() {
+ builtin printf '\e]633;H\a'
+}
+
+__vsc_right_prompt_end() {
+ builtin printf '\e]633;I\a'
+}
+
+__vsc_command_complete() {
+ if [[ "$__vsc_current_command" == "" ]]; then
+ builtin printf '\e]633;D\a'
+ else
+ builtin printf '\e]633;D;%s\a' "$__vsc_status"
+ fi
+ __vsc_update_cwd
+}
+
+if [[ -o NOUNSET ]]; then
+ if [ -z "${RPROMPT-}" ]; then
+ RPROMPT=""
+ fi
+fi
+__vsc_update_prompt() {
+ __vsc_prior_prompt="$PS1"
+ __vsc_prior_prompt2="$PS2"
+ __vsc_in_command_execution=""
+ PS1="%{$(__vsc_prompt_start)%}$PS1%{$(__vsc_prompt_end)%}"
+ PS2="%{$(__vsc_continuation_start)%}$PS2%{$(__vsc_continuation_end)%}"
+ if [ -n "$RPROMPT" ]; then
+ __vsc_prior_rprompt="$RPROMPT"
+ RPROMPT="%{$(__vsc_right_prompt_start)%}$RPROMPT%{$(__vsc_right_prompt_end)%}"
+ fi
+}
+
+__vsc_precmd() {
+ local __vsc_status="$?"
+ if [ -z "${__vsc_in_command_execution-}" ]; then
+ # not in command execution
+ __vsc_command_output_start
+ fi
+
+ __vsc_command_complete "$__vsc_status"
+ __vsc_current_command=""
+
+ # in command execution
+ if [ -n "$__vsc_in_command_execution" ]; then
+ # non null
+ __vsc_update_prompt
+ fi
+}
+
+__vsc_preexec() {
+ PS1="$__vsc_prior_prompt"
+ PS2="$__vsc_prior_prompt2"
+ if [ -n "$RPROMPT" ]; then
+ RPROMPT="$__vsc_prior_rprompt"
+ fi
+ __vsc_in_command_execution="1"
+ __vsc_current_command=$2
+ __vsc_command_output_start
+}
+add-zsh-hook precmd __vsc_precmd
+add-zsh-hook preexec __vsc_preexec
+
+if [[ $options[login] = off && $USER_ZDOTDIR != $VSCODE_ZDOTDIR ]]; then
+ ZDOTDIR=$USER_ZDOTDIR
+fi
diff --git a/src/plugins/terminal/shellintegrations/shellintegration.fish b/src/plugins/terminal/shellintegrations/shellintegration.fish
new file mode 100644
index 0000000000..7495bab3f4
--- /dev/null
+++ b/src/plugins/terminal/shellintegrations/shellintegration.fish
@@ -0,0 +1,122 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# SPDX-License-Identifier: MIT
+#
+# Visual Studio Code terminal integration for fish
+#
+# Manual installation:
+#
+# (1) Add the following to the end of `$__fish_config_dir/config.fish`:
+#
+# string match -q "$TERM_PROGRAM" "vscode"
+# and . (code --locate-shell-integration-path fish)
+#
+# (2) Restart fish.
+
+# Don't run in scripts, other terminals, or more than once per session.
+status is-interactive
+and string match --quiet "$TERM_PROGRAM" "vscode"
+and ! set --query VSCODE_SHELL_INTEGRATION
+or exit
+
+set --global VSCODE_SHELL_INTEGRATION 1
+
+# Apply any explicit path prefix (see #99878)
+if status --is-login; and set -q VSCODE_PATH_PREFIX
+ fish_add_path -p $VSCODE_PATH_PREFIX
+end
+set -e VSCODE_PATH_PREFIX
+
+# Helper function
+function __vsc_esc -d "Emit escape sequences for VS Code shell integration"
+ builtin printf "\e]633;%s\a" (string join ";" $argv)
+end
+
+# Sent right before executing an interactive command.
+# Marks the beginning of command output.
+function __vsc_cmd_executed --on-event fish_preexec
+ __vsc_esc C
+ __vsc_esc E (__vsc_escape_value "$argv")
+
+ # Creates a marker to indicate a command was run.
+ set --global _vsc_has_cmd
+end
+
+
+# Escape a value for use in the 'P' ("Property") or 'E' ("Command Line") sequences.
+# Backslashes are doubled and non-alphanumeric characters are hex encoded.
+function __vsc_escape_value
+ # Escape backslashes and semi-colons
+ echo $argv \
+ | string replace --all '\\' '\\\\' \
+ | string replace --all ';' '\\x3b' \
+ ;
+end
+
+# Sent right after an interactive command has finished executing.
+# Marks the end of command output.
+function __vsc_cmd_finished --on-event fish_postexec
+ __vsc_esc D $status
+end
+
+# Sent when a command line is cleared or reset, but no command was run.
+# Marks the cleared line with neither success nor failure.
+function __vsc_cmd_clear --on-event fish_cancel
+ __vsc_esc D
+end
+
+# Sent whenever a new fish prompt is about to be displayed.
+# Updates the current working directory.
+function __vsc_update_cwd --on-event fish_prompt
+ __vsc_esc P Cwd=(__vsc_escape_value "$PWD")
+
+ # If a command marker exists, remove it.
+ # Otherwise, the commandline is empty and no command was run.
+ if set --query _vsc_has_cmd
+ set --erase _vsc_has_cmd
+ else
+ __vsc_cmd_clear
+ end
+end
+
+# Sent at the start of the prompt.
+# Marks the beginning of the prompt (and, implicitly, a new line).
+function __vsc_fish_prompt_start
+ __vsc_esc A
+end
+
+# Sent at the end of the prompt.
+# Marks the beginning of the user's command input.
+function __vsc_fish_cmd_start
+ __vsc_esc B
+end
+
+function __vsc_fish_has_mode_prompt -d "Returns true if fish_mode_prompt is defined and not empty"
+ functions fish_mode_prompt | string match -rvq '^ *(#|function |end$|$)'
+end
+
+# Preserve the user's existing prompt, to wrap in our escape sequences.
+functions --copy fish_prompt __vsc_fish_prompt
+
+# Preserve and wrap fish_mode_prompt (which appears to the left of the regular
+# prompt), but only if it's not defined as an empty function (which is the
+# officially documented way to disable that feature).
+if __vsc_fish_has_mode_prompt
+ functions --copy fish_mode_prompt __vsc_fish_mode_prompt
+
+ function fish_mode_prompt
+ __vsc_fish_prompt_start
+ __vsc_fish_mode_prompt
+ end
+
+ function fish_prompt
+ __vsc_fish_prompt
+ __vsc_fish_cmd_start
+ end
+else
+ # No fish_mode_prompt, so put everything in fish_prompt.
+ function fish_prompt
+ __vsc_fish_prompt_start
+ __vsc_fish_prompt
+ __vsc_fish_cmd_start
+ end
+end
diff --git a/src/plugins/terminal/shellintegrations/shellintegration.ps1 b/src/plugins/terminal/shellintegrations/shellintegration.ps1
new file mode 100644
index 0000000000..4fd978a884
--- /dev/null
+++ b/src/plugins/terminal/shellintegrations/shellintegration.ps1
@@ -0,0 +1,158 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# SPDX-License-Identifier: MIT
+
+# Prevent installing more than once per session
+if (Test-Path variable:global:__VSCodeOriginalPrompt) {
+ return;
+}
+
+# Disable shell integration when the language mode is restricted
+if ($ExecutionContext.SessionState.LanguageMode -ne "FullLanguage") {
+ return;
+}
+
+$Global:__VSCodeOriginalPrompt = $function:Prompt
+
+$Global:__LastHistoryId = -1
+
+function Global:__VSCode-Escape-Value([string]$value) {
+ # NOTE: In PowerShell v6.1+, this can be written `$value -replace '…', { … }` instead of `[regex]::Replace`.
+ # Replace any non-alphanumeric characters.
+ [regex]::Replace($value, '[\\\n;]', { param($match)
+ # Encode the (ascii) matches as `\x<hex>`
+ -Join (
+ [System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ }
+ )
+ })
+}
+
+function Global:Prompt() {
+ $FakeCode = [int]!$global:?
+ # NOTE: We disable strict mode for the scope of this function because it unhelpfully throws an
+ # error when $LastHistoryEntry is null, and is not otherwise useful.
+ Set-StrictMode -Off
+ $LastHistoryEntry = Get-History -Count 1
+ # Skip finishing the command if the first command has not yet started
+ if ($Global:__LastHistoryId -ne -1) {
+ if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) {
+ # Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command)
+ $Result = "$([char]0x1b)]633;E`a"
+ $Result += "$([char]0x1b)]633;D`a"
+ } else {
+ # Command finished command line
+ # OSC 633 ; A ; <CommandLine?> ST
+ $Result = "$([char]0x1b)]633;E;"
+ # Sanitize the command line to ensure it can get transferred to the terminal and can be parsed
+ # correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter
+ # to only be composed of _printable_ characters as per the spec.
+ if ($LastHistoryEntry.CommandLine) {
+ $CommandLine = $LastHistoryEntry.CommandLine
+ } else {
+ $CommandLine = ""
+ }
+ $Result += $(__VSCode-Escape-Value $CommandLine)
+ $Result += "`a"
+ # Command finished exit code
+ # OSC 633 ; D [; <ExitCode>] ST
+ $Result += "$([char]0x1b)]633;D;$FakeCode`a"
+ }
+ }
+ # Prompt started
+ # OSC 633 ; A ST
+ $Result += "$([char]0x1b)]633;A`a"
+ # Current working directory
+ # OSC 633 ; <Property>=<Value> ST
+ $Result += if($pwd.Provider.Name -eq 'FileSystem'){"$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a"}
+ # Before running the original prompt, put $? back to what it was:
+ if ($FakeCode -ne 0) {
+ Write-Error "failure" -ea ignore
+ }
+ # Run the original prompt
+ $Result += $Global:__VSCodeOriginalPrompt.Invoke()
+ # Write command started
+ $Result += "$([char]0x1b)]633;B`a"
+ $Global:__LastHistoryId = $LastHistoryEntry.Id
+ return $Result
+}
+
+# Only send the command executed sequence when PSReadLine is loaded, if not shell integration should
+# still work thanks to the command line sequence
+if (Get-Module -Name PSReadLine) {
+ $__VSCodeOriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine
+ function Global:PSConsoleHostReadLine {
+ $tmp = $__VSCodeOriginalPSConsoleHostReadLine.Invoke()
+ # Write command executed sequence directly to Console to avoid the new line from Write-Host
+ [Console]::Write("$([char]0x1b)]633;C`a")
+ $tmp
+ }
+}
+
+# Set IsWindows property
+[Console]::Write("$([char]0x1b)]633;P;IsWindows=$($IsWindows)`a")
+
+# Set always on key handlers which map to default VS Code keybindings
+function Set-MappedKeyHandler {
+ param ([string[]] $Chord, [string[]]$Sequence)
+ try {
+ $Handler = Get-PSReadLineKeyHandler -Chord $Chord | Select-Object -First 1
+ } catch [System.Management.Automation.ParameterBindingException] {
+ # PowerShell 5.1 ships with PSReadLine 2.0.0 which does not have -Chord,
+ # so we check what's bound and filter it.
+ $Handler = Get-PSReadLineKeyHandler -Bound | Where-Object -FilterScript { $_.Key -eq $Chord } | Select-Object -First 1
+ }
+ if ($Handler) {
+ Set-PSReadLineKeyHandler -Chord $Sequence -Function $Handler.Function
+ }
+}
+
+function Set-MappedKeyHandlers {
+ Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a'
+ Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b'
+ Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c'
+ Set-MappedKeyHandler -Chord Shift+End -Sequence 'F12,d'
+
+ # Conditionally enable suggestions
+ if ($env:VSCODE_SUGGEST -eq '1') {
+ Remove-Item Env:VSCODE_SUGGEST
+
+ # VS Code send completions request (may override Ctrl+Spacebar)
+ Set-PSReadLineKeyHandler -Chord 'F12,e' -ScriptBlock {
+ Send-Completions
+ }
+
+ # Suggest trigger characters
+ Set-PSReadLineKeyHandler -Chord "-" -ScriptBlock {
+ [Microsoft.PowerShell.PSConsoleReadLine]::Insert("-")
+ Send-Completions
+ }
+ }
+}
+
+function Send-Completions {
+ $commandLine = ""
+ $cursorIndex = 0
+ # TODO: Since fuzzy matching exists, should completions be provided only for character after the
+ # last space and then filter on the client side? That would let you trigger ctrl+space
+ # anywhere on a word and have full completions available
+ [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$commandLine, [ref]$cursorIndex)
+ $completionPrefix = $commandLine
+
+ # Get completions
+ $result = "`e]633;Completions"
+ if ($completionPrefix.Length -gt 0) {
+ # Get and send completions
+ $completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex
+ if ($null -ne $completions.CompletionMatches) {
+ $result += ";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);"
+ $result += $completions.CompletionMatches | ConvertTo-Json -Compress
+ }
+ }
+ $result += "`a"
+
+ Write-Host -NoNewLine $result
+}
+
+# Register key handlers if PSReadLine is available
+if (Get-Module -Name PSReadLine) {
+ Set-MappedKeyHandlers
+}
diff --git a/src/plugins/terminal/terminal.qrc b/src/plugins/terminal/terminal.qrc
index a4fece92fc..32d717d0f4 100644
--- a/src/plugins/terminal/terminal.qrc
+++ b/src/plugins/terminal/terminal.qrc
@@ -1,6 +1,13 @@
<RCC>
- <qresource prefix="/terminal">
- <file>images/settingscategory_terminal.png</file>
- <file>images/settingscategory_terminal@2x.png</file>
- </qresource>
+ <qresource prefix="/terminal">
+ <file>images/settingscategory_terminal.png</file>
+ <file>images/settingscategory_terminal@2x.png</file>
+ <file>shellintegrations/shellintegration-bash.sh</file>
+ <file>shellintegrations/shellintegration-env.zsh</file>
+ <file>shellintegrations/shellintegration-login.zsh</file>
+ <file>shellintegrations/shellintegration-profile.zsh</file>
+ <file>shellintegrations/shellintegration-rc.zsh</file>
+ <file>shellintegrations/shellintegration.fish</file>
+ <file>shellintegrations/shellintegration.ps1</file>
+ </qresource>
</RCC>
diff --git a/src/plugins/terminal/terminalpane.cpp b/src/plugins/terminal/terminalpane.cpp
index 3ad8fc713a..215a3883c4 100644
--- a/src/plugins/terminal/terminalpane.cpp
+++ b/src/plugins/terminal/terminalpane.cpp
@@ -188,13 +188,29 @@ void TerminalPane::setupTerminalWidget(TerminalWidget *terminal)
auto setTabText = [this](TerminalWidget * terminal) {
auto index = m_tabWidget->indexOf(terminal);
- m_tabWidget->setTabText(index, terminal->shellName());
+ const FilePath cwd = terminal->cwd();
+
+ const QString exe = terminal->currentCommand().isEmpty() ? terminal->shellName()
+ : terminal->currentCommand().executable().fileName();
+
+ if (cwd.isEmpty())
+ m_tabWidget->setTabText(index, exe);
+ else
+ m_tabWidget->setTabText(index, exe + " - " + cwd.fileName());
};
connect(terminal, &TerminalWidget::started, [setTabText, terminal](qint64 /*pid*/) {
setTabText(terminal);
});
+ connect(terminal, &TerminalWidget::cwdChanged, [setTabText, terminal]() {
+ setTabText(terminal);
+ });
+
+ connect(terminal, &TerminalWidget::commandChanged, [setTabText, terminal]() {
+ setTabText(terminal);
+ });
+
if (!terminal->shellName().isEmpty())
setTabText(terminal);
diff --git a/src/plugins/terminal/terminalsurface.cpp b/src/plugins/terminal/terminalsurface.cpp
index ad47fd8b15..128c05213d 100644
--- a/src/plugins/terminal/terminalsurface.cpp
+++ b/src/plugins/terminal/terminalsurface.cpp
@@ -23,10 +23,13 @@ QColor toQColor(const VTermColor &c)
struct TerminalSurfacePrivate
{
- TerminalSurfacePrivate(TerminalSurface *surface, const QSize &initialGridSize)
+ TerminalSurfacePrivate(TerminalSurface *surface,
+ const QSize &initialGridSize,
+ ShellIntegration *shellIntegration)
: m_vterm(vterm_new(initialGridSize.height(), initialGridSize.width()), vterm_free)
, m_vtermScreen(vterm_obtain_screen(m_vterm.get()))
, m_scrollback(std::make_unique<Internal::Scrollback>(5000))
+ , m_shellIntegration(shellIntegration)
, q(surface)
{}
@@ -75,7 +78,15 @@ struct TerminalSurfacePrivate
vterm_screen_set_damage_merge(m_vtermScreen, VTERM_DAMAGE_SCROLL);
vterm_screen_enable_altscreen(m_vtermScreen, true);
+ memset(&m_vtermStateFallbacks, 0, sizeof(m_vtermStateFallbacks));
+
+ m_vtermStateFallbacks.osc = [](int cmd, VTermStringFragment fragment, void *user) {
+ auto p = static_cast<TerminalSurfacePrivate *>(user);
+ return p->osc(cmd, fragment);
+ };
+
VTermState *vts = vterm_obtain_state(m_vterm.get());
+ vterm_state_set_unrecognised_fallbacks(vts, &m_vtermStateFallbacks, this);
vterm_state_set_bold_highbright(vts, true);
vterm_screen_reset(m_vtermScreen, 1);
@@ -196,6 +207,14 @@ struct TerminalSurfacePrivate
return 1;
}
+ int osc(int cmd, const VTermStringFragment &fragment)
+ {
+ if (m_shellIntegration)
+ m_shellIntegration->onOsc(cmd, fragment);
+
+ return 1;
+ }
+
int setTerminalProperties(VTermProp prop, VTermValue *val)
{
switch (prop) {
@@ -274,19 +293,23 @@ struct TerminalSurfacePrivate
std::unique_ptr<VTerm, void (*)(VTerm *)> m_vterm;
VTermScreen *m_vtermScreen;
VTermScreenCallbacks m_vtermScreenCallbacks;
+ VTermStateFallbacks m_vtermStateFallbacks;
QColor m_defaultBgColor;
Cursor m_cursor;
+ QString m_currentCommand;
bool m_altscreen{false};
std::unique_ptr<Internal::Scrollback> m_scrollback;
+ ShellIntegration *m_shellIntegration{nullptr};
+
TerminalSurface *q;
};
-TerminalSurface::TerminalSurface(QSize initialGridSize)
- : d(std::make_unique<TerminalSurfacePrivate>(this, initialGridSize))
+TerminalSurface::TerminalSurface(QSize initialGridSize, ShellIntegration *shellIntegration)
+ : d(std::make_unique<TerminalSurfacePrivate>(this, initialGridSize, shellIntegration))
{
d->init();
}
@@ -478,6 +501,11 @@ QColor TerminalSurface::defaultBgColor() const
return toQColor(d->defaultBgColor());
}
+ShellIntegration *TerminalSurface::shellIntegration() const
+{
+ return d->m_shellIntegration;
+}
+
CellIterator TerminalSurface::begin() const
{
auto res = CellIterator(this, {0, 0});
diff --git a/src/plugins/terminal/terminalsurface.h b/src/plugins/terminal/terminalsurface.h
index 354a3a028d..04958582c2 100644
--- a/src/plugins/terminal/terminalsurface.h
+++ b/src/plugins/terminal/terminalsurface.h
@@ -4,6 +4,7 @@
#pragma once
#include "celliterator.h"
+#include "shellintegration.h"
#include <QKeyEvent>
#include <QSize>
@@ -47,7 +48,7 @@ class TerminalSurface : public QObject
Q_OBJECT;
public:
- TerminalSurface(QSize initialGridSize);
+ TerminalSurface(QSize initialGridSize, ShellIntegration *shellIntegration);
~TerminalSurface();
public:
@@ -95,6 +96,8 @@ public:
QColor defaultBgColor() const;
+ ShellIntegration *shellIntegration() const;
+
signals:
void writeToPty(const QByteArray &data);
void invalidated(QRect grid);
diff --git a/src/plugins/terminal/terminalwidget.cpp b/src/plugins/terminal/terminalwidget.cpp
index 23da5088ce..b168320dce 100644
--- a/src/plugins/terminal/terminalwidget.cpp
+++ b/src/plugins/terminal/terminalwidget.cpp
@@ -12,6 +12,7 @@
#include <utils/algorithm.h>
#include <utils/environment.h>
+#include <utils/fileutils.h>
#include <utils/hostosinfo.h>
#include <utils/processinterface.h>
#include <utils/stringutils.h>
@@ -132,6 +133,10 @@ void TerminalWidget::setupPty()
m_process->setWorkingDirectory(*m_openParameters.workingDirectory);
m_process->setEnvironment(env);
+ if (m_surface->shellIntegration()) {
+ m_surface->shellIntegration()->prepareProcess(*m_process.get());
+ }
+
connect(m_process.get(), &QtcProcess::readyReadStandardOutput, this, [this]() {
onReadyRead(false);
});
@@ -242,7 +247,8 @@ void TerminalWidget::setupActions()
connect(&m_zoomInAction, &QAction::triggered, this, &TerminalWidget::zoomIn);
connect(&m_zoomOutAction, &QAction::triggered, this, &TerminalWidget::zoomOut);
- addActions({&m_copyAction, &m_pasteAction, &m_clearSelectionAction, &m_zoomInAction, &m_zoomOutAction});
+ addActions(
+ {&m_copyAction, &m_pasteAction, &m_clearSelectionAction, &m_zoomInAction, &m_zoomOutAction});
}
void TerminalWidget::writeToPty(const QByteArray &data)
@@ -253,7 +259,8 @@ void TerminalWidget::writeToPty(const QByteArray &data)
void TerminalWidget::setupSurface()
{
- m_surface = std::make_unique<Internal::TerminalSurface>(QSize{80, 60});
+ m_shellIntegration.reset(new ShellIntegration());
+ m_surface = std::make_unique<Internal::TerminalSurface>(QSize{80, 60}, m_shellIntegration.get());
connect(m_surface.get(),
&Internal::TerminalSurface::writeToPty,
@@ -299,6 +306,22 @@ void TerminalWidget::setupSurface()
connect(m_surface.get(), &Internal::TerminalSurface::unscroll, this, [this] {
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
});
+ if (m_shellIntegration) {
+ connect(m_shellIntegration.get(),
+ &ShellIntegration::commandChanged,
+ this,
+ [this](const CommandLine &command) {
+ m_currentCommand = command;
+ emit commandChanged(m_currentCommand);
+ });
+ connect(m_shellIntegration.get(),
+ &ShellIntegration::currentDirChanged,
+ this,
+ [this](const QString &currentDir) {
+ m_cwd = FilePath::fromUserInput(currentDir);
+ emit cwdChanged(m_cwd);
+ });
+ }
}
void TerminalWidget::configBlinkTimer()
@@ -470,6 +493,16 @@ QString TerminalWidget::shellName() const
return m_shellName;
}
+FilePath TerminalWidget::cwd() const
+{
+ return m_cwd;
+}
+
+CommandLine TerminalWidget::currentCommand() const
+{
+ return m_currentCommand;
+}
+
QPoint TerminalWidget::viewportToGlobal(QPoint p) const
{
int y = p.y() - topMargin();
diff --git a/src/plugins/terminal/terminalwidget.h b/src/plugins/terminal/terminalwidget.h
index 53d82b3899..60cff4183f 100644
--- a/src/plugins/terminal/terminalwidget.h
+++ b/src/plugins/terminal/terminalwidget.h
@@ -72,8 +72,13 @@ public:
QString shellName() const;
+ Utils::FilePath cwd() const;
+ Utils::CommandLine currentCommand() const;
+
signals:
void started(qint64 pid);
+ void cwdChanged(const Utils::FilePath &cwd);
+ void commandChanged(const Utils::CommandLine &cmd);
protected:
void paintEvent(QPaintEvent *event) override;
@@ -158,6 +163,7 @@ protected:
private:
std::unique_ptr<Utils::QtcProcess> m_process;
std::unique_ptr<Internal::TerminalSurface> m_surface;
+ std::unique_ptr<ShellIntegration> m_shellIntegration;
QString m_shellName;
@@ -201,6 +207,9 @@ private:
Internal::Cursor m_cursor;
QTimer m_cursorBlinkTimer;
bool m_cursorBlinkState{true};
+
+ Utils::FilePath m_cwd;
+ Utils::CommandLine m_currentCommand;
};
} // namespace Terminal
diff --git a/src/plugins/terminal/tests/integration b/src/plugins/terminal/tests/integration
new file mode 100755
index 0000000000..ac17432cb6
--- /dev/null
+++ b/src/plugins/terminal/tests/integration
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+echo "Testing integration response, best start this from a terminal that has no builtin integration"
+echo "e.g. 'sh'"
+echo
+
+echo -e "\033[1m ⎆ Current dir should have changed to '/Some/Dir/Here'\033[0m"
+printf "\033]7;file:///Some/Dir/Here\033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+
+echo -e "\033[1m ⎆ Current dir should have changed to '/Some/Other/Dir/Here'\033[0m"
+printf "\033]1337;CurrentDir=/Some/Other/Dir/Here\033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+
+
+echo -e "\033[1m ⎆ Current dir should have changed to '/VSCode/dir/with space'\033[0m"
+printf "\033]633P;Cwd=/VSCode/dir/with space\033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+
+echo -e "\033[1m ⎆ The current process should have changed to 'test'\033[0m"
+printf "\033]633E;test with arguments\033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+
+echo -e "\033[1m ⎆ The current process should have changed to 'test with space'\033[0m"
+printf "\033]633E;'test with space'\033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+
+echo -e "\033[1m ⎆ The current process should have changed to 'test with space v2'\033[0m"
+printf "\033]633E;\"test with space v2\"\033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+
+echo -e "\033[1m ⎆ The current process should have changed to 'test with space v3'\033[0m"
+printf "\033]633E;\"./test/test with space v3\" -argument\033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+
+echo -e "\033[1m ⎆ The current process should have changed to 'cat'\033[0m"
+printf "\033]633E;cat /dev/random | base64 -argument\033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+
+echo -e "\033[1m ⎆ The current process should have changed to 'cat me'\033[0m"
+printf "\033]633E;cat\\ me args \033\\"
+
+read -p " ⎆ Press enter to continue " -n1 -s
+echo
+echo
+