diff options
author | Marcus Tillmanns <marcus.tillmanns@qt.io> | 2023-03-10 13:55:17 +0100 |
---|---|---|
committer | Marcus Tillmanns <marcus.tillmanns@qt.io> | 2023-03-16 06:08:09 +0000 |
commit | bd52e53dbfa7641098313edd56d885df69916f9f (patch) | |
tree | 161502368e1ebabb785bd9db668d028e65005fcd /src | |
parent | 3507229a007e3fd5f004a94f4c3857518ab1c9df (diff) | |
download | qt-creator-bd52e53dbfa7641098313edd56d885df69916f9f.tar.gz |
Terminal: Add shell integration
Change-Id: Ic1e226b56f0103e5a6e7764073ab7ab241b67baa
Reviewed-by: Cristian Adam <cristian.adam@qt.io>
Diffstat (limited to 'src')
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 ¤tDir) { + 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 + |