overhaul shell_integration to enable individual control over ansi escape sequences (#12629)

# Description

This PR overhauls the shell_integration system by allowing individual
control over which ansi escape sequences are used. As we continue to
broaden our support for more ansi escape sequences, we can't really have
an all-or-nothing strategy. Some ansi escapes cause problems in certain
operating systems or terminals. We should allow the user to choose which
escapes they want.

TODO:
* Gather feedback
* Should osc7, osc9_9 and osc633p be mutually exclusive?
* Is the naming convention for these settings too nerdy osc2, osc7, etc?

closes #11301

# User-Facing Changes
shell_integration is no longer a boolean value. This is what is
supported in the default_config.nu
```nushell
  shell_integration: {
    # osc2 abbreviates the path if in the home_dir, sets the tab/window title, shows the running command in the tab/window title
    osc2: true
    # osc7 is a way to communicate the path to the terminal, this is helpful for spawning new tabs in the same directory
    osc7: true
    # osc8 is also implemented as the deprecated setting ls.show_clickable_links, it shows clickable links in ls output if your terminal supports it
    osc8: true
    # osc9_9 is from ConEmu and is starting to get wider support. It's similar to osc7 in that it communicates the path to the terminal
    osc9_9: false
    # osc133 is several escapes invented by Final Term which include the supported ones below.
    # 133;A - Mark prompt start
    # 133;B - Mark prompt end
    # 133;C - Mark pre-execution
    # 133;D;exit - Mark execution finished with exit code
    # This is used to enable terminals to know where the prompt is, the command is, where the command finishes, and where the output of the command is
    osc133: true
    # osc633 is closely related to osc133 but only exists in visual studio code (vscode) and supports their shell integration features
    # 633;A - Mark prompt start
    # 633;B - Mark prompt end
    # 633;C - Mark pre-execution
    # 633;D;exit - Mark execution finished with exit code
    # 633;E - NOT IMPLEMENTED - Explicitly set the command line with an optional nonce
    # 633;P;Cwd=<path> - Mark the current working directory and communicate it to the terminal
    # and also helps with the run recent menu in vscode
    osc633: true
    # reset_application_mode is escape \x1b[?1l and was added to help ssh work better
    reset_application_mode: true
  }
```

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use std testing; testing run-tests --path
crates/nu-std"` to run the tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
This commit is contained in:
Darren Schroeder 2024-05-02 09:56:50 -04:00 committed by GitHub
parent 8ed0d84d6a
commit 0805f1fd90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 521 additions and 172 deletions

View File

@ -1,4 +1,10 @@
use crate::prompt_update::{POST_PROMPT_MARKER, PRE_PROMPT_MARKER};
use crate::prompt_update::{
POST_PROMPT_MARKER, PRE_PROMPT_MARKER, VSCODE_POST_PROMPT_MARKER, VSCODE_PRE_PROMPT_MARKER,
};
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
#[cfg(windows)]
use nu_utils::enable_vt_processing;
use reedline::{
@ -10,7 +16,8 @@ use std::borrow::Cow;
/// Nushell prompt definition
#[derive(Clone)]
pub struct NushellPrompt {
shell_integration: bool,
shell_integration_osc133: bool,
shell_integration_osc633: bool,
left_prompt_string: Option<String>,
right_prompt_string: Option<String>,
default_prompt_indicator: Option<String>,
@ -18,12 +25,20 @@ pub struct NushellPrompt {
default_vi_normal_prompt_indicator: Option<String>,
default_multiline_indicator: Option<String>,
render_right_prompt_on_last_line: bool,
engine_state: EngineState,
stack: Stack,
}
impl NushellPrompt {
pub fn new(shell_integration: bool) -> NushellPrompt {
pub fn new(
shell_integration_osc133: bool,
shell_integration_osc633: bool,
engine_state: EngineState,
stack: Stack,
) -> NushellPrompt {
NushellPrompt {
shell_integration,
shell_integration_osc133,
shell_integration_osc633,
left_prompt_string: None,
right_prompt_string: None,
default_prompt_indicator: None,
@ -31,6 +46,8 @@ impl NushellPrompt {
default_vi_normal_prompt_indicator: None,
default_multiline_indicator: None,
render_right_prompt_on_last_line: false,
engine_state,
stack,
}
}
@ -106,7 +123,17 @@ impl Prompt for NushellPrompt {
.to_string()
.replace('\n', "\r\n");
if self.shell_integration {
if self.shell_integration_osc633 {
if self.stack.get_env_var(&self.engine_state, "TERM_PROGRAM")
== Some(Value::test_string("vscode"))
{
// We're in vscode and we have osc633 enabled
format!("{VSCODE_PRE_PROMPT_MARKER}{prompt}{VSCODE_POST_PROMPT_MARKER}").into()
} else {
// If we're in VSCode but we don't find the env var, just return the regular markers
format!("{PRE_PROMPT_MARKER}{prompt}{POST_PROMPT_MARKER}").into()
}
} else if self.shell_integration_osc133 {
format!("{PRE_PROMPT_MARKER}{prompt}{POST_PROMPT_MARKER}").into()
} else {
prompt.into()

View File

@ -23,10 +23,37 @@ pub(crate) const TRANSIENT_PROMPT_INDICATOR_VI_NORMAL: &str =
"TRANSIENT_PROMPT_INDICATOR_VI_NORMAL";
pub(crate) const TRANSIENT_PROMPT_MULTILINE_INDICATOR: &str =
"TRANSIENT_PROMPT_MULTILINE_INDICATOR";
// Store all these Ansi Escape Markers here so they can be reused easily
// According to Daniel Imms @Tyriar, we need to do these this way:
// <133 A><prompt><133 B><command><133 C><command output>
pub(crate) const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\";
pub(crate) const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\";
pub(crate) const PRE_EXECUTION_MARKER: &str = "\x1b]133;C\x1b\\";
#[allow(dead_code)]
pub(crate) const POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]133;D;";
#[allow(dead_code)]
pub(crate) const POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
// OSC633 is the same as OSC133 but specifically for VSCode
pub(crate) const VSCODE_PRE_PROMPT_MARKER: &str = "\x1b]633;A\x1b\\";
pub(crate) const VSCODE_POST_PROMPT_MARKER: &str = "\x1b]633;B\x1b\\";
#[allow(dead_code)]
pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
#[allow(dead_code)]
//"\x1b]633;D;{}\x1b\\"
pub(crate) const VSCODE_POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]633;D;";
#[allow(dead_code)]
pub(crate) const VSCODE_POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
#[allow(dead_code)]
pub(crate) const VSCODE_COMMANDLINE_MARKER: &str = "\x1b]633;E\x1b\\";
#[allow(dead_code)]
// "\x1b]633;P;Cwd={}\x1b\\"
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_PREFIX: &str = "\x1b]633;P;Cwd=";
#[allow(dead_code)]
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_SUFFIX: &str = "\x1b\\";
pub(crate) const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
fn get_prompt_string(
prompt: &str,
@ -85,16 +112,46 @@ pub(crate) fn update_prompt(
// Now that we have the prompt string lets ansify it.
// <133 A><prompt><133 B><command><133 C><command output>
let left_prompt_string = if config.shell_integration {
if let Some(prompt_string) = left_prompt_string {
let left_prompt_string_133 = if config.shell_integration_osc133 {
if let Some(prompt_string) = left_prompt_string.clone() {
Some(format!(
"{PRE_PROMPT_MARKER}{prompt_string}{POST_PROMPT_MARKER}"
))
} else {
left_prompt_string
left_prompt_string.clone()
}
} else {
left_prompt_string
left_prompt_string.clone()
};
let left_prompt_string_633 = if config.shell_integration_osc633 {
if let Some(prompt_string) = left_prompt_string.clone() {
if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode"))
{
// If the user enabled osc633 and we're in vscode, use the vscode markers
Some(format!(
"{VSCODE_PRE_PROMPT_MARKER}{prompt_string}{VSCODE_POST_PROMPT_MARKER}"
))
} else {
// otherwise, use the regular osc133 markers
Some(format!(
"{PRE_PROMPT_MARKER}{prompt_string}{POST_PROMPT_MARKER}"
))
}
} else {
left_prompt_string.clone()
}
} else {
left_prompt_string.clone()
};
let left_prompt_string = match (left_prompt_string_133, left_prompt_string_633) {
(None, None) => left_prompt_string,
(None, Some(l633)) => Some(l633),
(Some(l133), None) => Some(l133),
// If both are set, it means we're in vscode, so use the vscode markers
// and even if we're not actually in vscode atm, the regular 133 markers are used
(Some(_l133), Some(l633)) => Some(l633),
};
let right_prompt_string = get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, stack);

View File

@ -1,3 +1,9 @@
use crate::prompt_update::{
POST_EXECUTION_MARKER_PREFIX, POST_EXECUTION_MARKER_SUFFIX, PRE_EXECUTION_MARKER,
RESET_APPLICATION_MODE, VSCODE_CWD_PROPERTY_MARKER_PREFIX, VSCODE_CWD_PROPERTY_MARKER_SUFFIX,
VSCODE_POST_EXECUTION_MARKER_PREFIX, VSCODE_POST_EXECUTION_MARKER_SUFFIX,
VSCODE_PRE_EXECUTION_MARKER,
};
use crate::{
completions::NuCompleter,
nu_highlight::NoOpHighlighter,
@ -42,16 +48,6 @@ use std::{
};
use sysinfo::System;
// According to Daniel Imms @Tyriar, we need to do these this way:
// <133 A><prompt><133 B><command><133 C><command output>
// These first two have been moved to prompt_update to get as close as possible to the prompt.
// const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\";
// const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\";
const PRE_EXECUTE_MARKER: &str = "\x1b]133;C\x1b\\";
// This one is in get_command_finished_marker() now so we can capture the exit codes properly.
// const CMD_FINISHED_MARKER: &str = "\x1b]133;D;{}\x1b\\";
const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
/// The main REPL loop, including spinning up the prompt itself.
pub fn evaluate_repl(
engine_state: &mut EngineState,
@ -66,7 +62,7 @@ pub fn evaluate_repl(
// so that it may be read by various reedline plugins. During this, we
// can't modify the stack, but at the end of the loop we take back ownership
// from the Arc. This lets us avoid copying stack variables needlessly
let mut unique_stack = stack;
let mut unique_stack = stack.clone();
let config = engine_state.get_config();
let use_color = config.use_ansi_coloring;
@ -74,8 +70,19 @@ pub fn evaluate_repl(
let mut entry_num = 0;
let shell_integration = config.shell_integration;
let nu_prompt = NushellPrompt::new(shell_integration);
// Let's grab the shell_integration configs
let shell_integration_osc2 = config.shell_integration_osc2;
let shell_integration_osc7 = config.shell_integration_osc7;
let shell_integration_osc9_9 = config.shell_integration_osc9_9;
let shell_integration_osc133 = config.shell_integration_osc133;
let shell_integration_osc633 = config.shell_integration_osc633;
let nu_prompt = NushellPrompt::new(
shell_integration_osc133,
shell_integration_osc633,
engine_state.clone(),
stack.clone(),
);
let start_time = std::time::Instant::now();
// Translate environment variables from Strings to Values
@ -116,8 +123,22 @@ pub fn evaluate_repl(
}
let hostname = System::host_name();
if shell_integration {
shell_integration_osc_7_633_2(hostname.as_deref(), engine_state, &mut unique_stack);
if shell_integration_osc2 {
run_shell_integration_osc2(None, engine_state, &mut unique_stack, use_color);
}
if shell_integration_osc7 {
run_shell_integration_osc7(
hostname.as_deref(),
engine_state,
&mut unique_stack,
use_color,
);
}
if shell_integration_osc9_9 {
run_shell_integration_osc9_9(engine_state, &mut unique_stack, use_color);
}
if shell_integration_osc633 {
run_shell_integration_osc633(engine_state, &mut unique_stack, use_color);
}
engine_state.set_startup_time(entire_start_time.elapsed().as_nanos() as i64);
@ -513,7 +534,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
.with_highlighter(Box::<NoOpHighlighter>::default())
// CLEAR STACK-REFERENCE 2
.with_completer(Box::<DefaultCompleter>::default());
let shell_integration = config.shell_integration;
// Let's grab the shell_integration configs
let shell_integration_osc2 = config.shell_integration_osc2;
let shell_integration_osc7 = config.shell_integration_osc7;
let shell_integration_osc9_9 = config.shell_integration_osc9_9;
let shell_integration_osc133 = config.shell_integration_osc133;
let shell_integration_osc633 = config.shell_integration_osc633;
let shell_integration_reset_application_mode = config.shell_integration_reset_application_mode;
let mut stack = Stack::unwrap_unique(stack_arc);
@ -575,10 +603,40 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
repl.buffer = line_editor.current_buffer_contents().to_string();
drop(repl);
if shell_integration {
if shell_integration_osc633 {
if stack.get_env_var(engine_state, "TERM_PROGRAM")
== Some(Value::test_string("vscode"))
{
start_time = Instant::now();
run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
perf(
"pre_execute_marker (633;C) ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
);
} else {
start_time = Instant::now();
run_ansi_sequence(PRE_EXECUTION_MARKER);
perf(
"pre_execute_marker (133;C) ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
} else if shell_integration_osc133 {
start_time = Instant::now();
run_ansi_sequence(PRE_EXECUTE_MARKER);
run_ansi_sequence(PRE_EXECUTION_MARKER);
perf(
"pre_execute_marker (133;C) ansi escape sequence",
@ -598,20 +656,13 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
ReplOperation::AutoCd { cwd, target, span } => {
do_auto_cd(target, cwd, &mut stack, engine_state, span);
if shell_integration {
start_time = Instant::now();
run_ansi_sequence(&get_command_finished_marker(&stack, engine_state));
perf(
"post_execute_marker (133;D) ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
run_finaliziation_ansi_sequence(
&stack,
engine_state,
shell_integration_osc633,
shell_integration_osc133,
use_color,
);
}
ReplOperation::RunCommand(cmd) => {
line_editor = do_run_cmd(
@ -619,25 +670,18 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
&mut stack,
engine_state,
line_editor,
shell_integration,
shell_integration_osc2,
*entry_num,
use_color,
);
if shell_integration {
start_time = Instant::now();
run_ansi_sequence(&get_command_finished_marker(&stack, engine_state));
perf(
"post_execute_marker (133;D) ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
run_finaliziation_ansi_sequence(
&stack,
engine_state,
shell_integration_osc633,
shell_integration_osc133,
use_color,
);
}
// as the name implies, we do nothing in this case
ReplOperation::DoNothing => {}
@ -663,56 +707,45 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
}
}
if shell_integration {
start_time = Instant::now();
shell_integration_osc_7_633_2(hostname, engine_state, &mut stack);
perf(
"shell_integration_finalize ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
);
if shell_integration_osc2 {
run_shell_integration_osc2(None, engine_state, &mut stack, use_color);
}
if shell_integration_osc7 {
run_shell_integration_osc7(hostname, engine_state, &mut stack, use_color);
}
if shell_integration_osc9_9 {
run_shell_integration_osc9_9(engine_state, &mut stack, use_color);
}
if shell_integration_osc633 {
run_shell_integration_osc633(engine_state, &mut stack, use_color);
}
if shell_integration_reset_application_mode {
run_shell_integration_reset_application_mode();
}
flush_engine_state_repl_buffer(engine_state, &mut line_editor);
}
Ok(Signal::CtrlC) => {
// `Reedline` clears the line content. New prompt is shown
if shell_integration {
start_time = Instant::now();
run_ansi_sequence(&get_command_finished_marker(&stack, engine_state));
perf(
"command_finished_marker ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
run_finaliziation_ansi_sequence(
&stack,
engine_state,
shell_integration_osc633,
shell_integration_osc133,
use_color,
);
}
Ok(Signal::CtrlD) => {
// When exiting clear to a new line
if shell_integration {
start_time = Instant::now();
run_ansi_sequence(&get_command_finished_marker(&stack, engine_state));
run_finaliziation_ansi_sequence(
&stack,
engine_state,
shell_integration_osc633,
shell_integration_osc133,
use_color,
);
perf(
"command_finished_marker ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
println!();
return (false, stack, line_editor);
}
@ -725,20 +758,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
// e.g. https://github.com/nushell/nushell/issues/6452
// Alternatively only allow that expected failures let the REPL loop
}
if shell_integration {
start_time = Instant::now();
run_ansi_sequence(&get_command_finished_marker(&stack, engine_state));
perf(
"command_finished_marker ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
run_finaliziation_ansi_sequence(
&stack,
engine_state,
shell_integration_osc633,
shell_integration_osc133,
use_color,
);
}
}
perf(
@ -946,7 +973,7 @@ fn do_run_cmd(
// we pass in the line editor so it can be dropped in the case of a process exit
// (in the normal case we don't want to drop it so return it as-is otherwise)
line_editor: Reedline,
shell_integration: bool,
shell_integration_osc2: bool,
entry_num: usize,
use_color: bool,
) -> Reedline {
@ -973,39 +1000,8 @@ fn do_run_cmd(
}
}
if shell_integration {
let start_time = Instant::now();
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
match cwd.coerce_into_string() {
Ok(path) => {
// Try to abbreviate string for windows title
let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
path.replace(&p.as_path().display().to_string(), "~")
} else {
path
};
let binary_name = s.split_whitespace().next();
if let Some(binary_name) = binary_name {
run_ansi_sequence(&format!(
"\x1b]2;{maybe_abbrev_path}> {binary_name}\x07"
));
}
}
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
perf(
"set title with command ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
);
if shell_integration_osc2 {
run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
}
eval_source(
@ -1025,34 +1021,16 @@ fn do_run_cmd(
/// can have more information about what is going on (both on startup and after we have
/// run a command)
///
fn shell_integration_osc_7_633_2(
hostname: Option<&str>,
fn run_shell_integration_osc2(
command_name: Option<&str>,
engine_state: &EngineState,
stack: &mut Stack,
use_color: bool,
) {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
match cwd.coerce_into_string() {
Ok(path) => {
// Supported escape sequences of Microsoft's Visual Studio Code (vscode)
// https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
if stack.get_env_var(engine_state, "TERM_PROGRAM")
== Some(Value::test_string("vscode"))
{
// If we're in vscode, run their specific ansi escape sequence.
// This is helpful for ctrl+g to change directories in the terminal.
run_ansi_sequence(&format!("\x1b]633;P;Cwd={}\x1b\\", path));
} else {
// Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir)
run_ansi_sequence(&format!(
"\x1b]7;file://{}{}{}\x1b\\",
percent_encoding::utf8_percent_encode(
hostname.unwrap_or("localhost"),
percent_encoding::CONTROLS
),
if path.starts_with('/') { "" } else { "/" },
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
));
}
let start_time = Instant::now();
// Try to abbreviate string for windows title
let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
@ -1061,18 +1039,144 @@ fn shell_integration_osc_7_633_2(
path
};
let title = match command_name {
Some(binary_name) => {
let split_binary_name = binary_name.split_whitespace().next();
if let Some(binary_name) = split_binary_name {
format!("{maybe_abbrev_path}> {binary_name}")
} else {
maybe_abbrev_path.to_string()
}
}
None => maybe_abbrev_path.to_string(),
};
// Set window title too
// https://tldp.org/HOWTO/Xterm-Title-3.html
// ESC]0;stringBEL -- Set icon name and window title to string
// ESC]1;stringBEL -- Set icon name to string
// ESC]2;stringBEL -- Set window title to string
run_ansi_sequence(&format!("\x1b]2;{maybe_abbrev_path}\x07"));
run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
perf(
"set title with command osc2",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
}
fn run_shell_integration_osc7(
hostname: Option<&str>,
engine_state: &EngineState,
stack: &mut Stack,
use_color: bool,
) {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
match cwd.coerce_into_string() {
Ok(path) => {
let start_time = Instant::now();
// Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir)
run_ansi_sequence(&format!(
"\x1b]7;file://{}{}{}\x1b\\",
percent_encoding::utf8_percent_encode(
hostname.unwrap_or("localhost"),
percent_encoding::CONTROLS
),
if path.starts_with('/') { "" } else { "/" },
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
));
perf(
"communicate path to terminal with osc7",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
}
fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
match cwd.coerce_into_string() {
Ok(path) => {
let start_time = Instant::now();
// Otherwise, communicate the path as OSC 9;9 from ConEmu (often used for spawning new tabs in the same dir)
run_ansi_sequence(&format!(
"\x1b]9;9;{}{}\x1b\\",
if path.starts_with('/') { "" } else { "/" },
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
));
perf(
"communicate path to terminal with osc9;9",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
}
fn run_shell_integration_osc633(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
match cwd.coerce_into_string() {
Ok(path) => {
// Supported escape sequences of Microsoft's Visual Studio Code (vscode)
// https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
if stack.get_env_var(engine_state, "TERM_PROGRAM")
== Some(Value::test_string("vscode"))
{
let start_time = Instant::now();
// If we're in vscode, run their specific ansi escape sequence.
// This is helpful for ctrl+g to change directories in the terminal.
run_ansi_sequence(&format!(
"{}{}{}",
VSCODE_CWD_PROPERTY_MARKER_PREFIX, path, VSCODE_CWD_PROPERTY_MARKER_SUFFIX
));
perf(
"communicate path to terminal with osc633;P",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
}
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
}
fn run_shell_integration_reset_application_mode() {
run_ansi_sequence(RESET_APPLICATION_MODE);
}
@ -1219,12 +1323,28 @@ fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorSty
}
}
fn get_command_finished_marker(stack: &Stack, engine_state: &EngineState) -> String {
fn get_command_finished_marker(stack: &Stack, engine_state: &EngineState, vscode: bool) -> String {
let exit_code = stack
.get_env_var(engine_state, "LAST_EXIT_CODE")
.and_then(|e| e.as_i64().ok());
format!("\x1b]133;D;{}\x1b\\", exit_code.unwrap_or(0))
if vscode {
// format!("\x1b]633;D;{}\x1b\\", exit_code.unwrap_or(0))
format!(
"{}{}{}",
VSCODE_POST_EXECUTION_MARKER_PREFIX,
exit_code.unwrap_or(0),
VSCODE_POST_EXECUTION_MARKER_SUFFIX
)
} else {
// format!("\x1b]133;D;{}\x1b\\", exit_code.unwrap_or(0))
format!(
"{}{}{}",
POST_EXECUTION_MARKER_PREFIX,
exit_code.unwrap_or(0),
POST_EXECUTION_MARKER_SUFFIX
)
}
}
fn run_ansi_sequence(seq: &str) {
@ -1235,6 +1355,58 @@ fn run_ansi_sequence(seq: &str) {
}
}
fn run_finaliziation_ansi_sequence(
stack: &Stack,
engine_state: &EngineState,
use_color: bool,
shell_integration_osc633: bool,
shell_integration_osc133: bool,
) {
if shell_integration_osc633 {
// Only run osc633 if we are in vscode
if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) {
let start_time = Instant::now();
run_ansi_sequence(&get_command_finished_marker(stack, engine_state, true));
perf(
"post_execute_marker (633;D) ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
);
} else {
let start_time = Instant::now();
run_ansi_sequence(&get_command_finished_marker(stack, engine_state, false));
perf(
"post_execute_marker (133;D) ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
} else if shell_integration_osc133 {
let start_time = Instant::now();
run_ansi_sequence(&get_command_finished_marker(stack, engine_state, false));
perf(
"post_execute_marker (133;D) ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
);
}
}
// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
#[cfg(windows)]
static DRIVE_PATH_REGEX: once_cell::sync::Lazy<fancy_regex::Regex> =

View File

@ -942,7 +942,11 @@ fn render_path_name(
// clickable links don't work in remote SSH sessions
let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();
let show_clickable_links = config.show_clickable_links_in_ls && !in_ssh_session && has_metadata;
//TODO: Deprecated show_clickable_links_in_ls in favor of shell_integration_osc8
let show_clickable_links = config.show_clickable_links_in_ls
&& !in_ssh_session
&& has_metadata
&& config.shell_integration_osc8;
let ansi_style = style.map(Style::to_nu_ansi_term_style).unwrap_or_default();

View File

@ -74,7 +74,14 @@ pub struct Config {
pub menus: Vec<ParsedMenu>,
pub hooks: Hooks,
pub rm_always_trash: bool,
pub shell_integration: bool,
// Shell integration OSC meaning is described in the default_config.nu
pub shell_integration_osc2: bool,
pub shell_integration_osc7: bool,
pub shell_integration_osc8: bool,
pub shell_integration_osc9_9: bool,
pub shell_integration_osc133: bool,
pub shell_integration_osc633: bool,
pub shell_integration_reset_application_mode: bool,
pub buffer_editor: Value,
pub table_index_mode: TableIndexMode,
pub case_sensitive_completions: bool,
@ -154,7 +161,15 @@ impl Default for Config {
use_ansi_coloring: true,
bracketed_paste: true,
edit_mode: EditBindings::default(),
shell_integration: false,
// shell_integration: false,
shell_integration_osc2: false,
shell_integration_osc7: false,
shell_integration_osc8: false,
shell_integration_osc9_9: false,
shell_integration_osc133: false,
shell_integration_osc633: false,
shell_integration_reset_application_mode: false,
render_right_prompt_on_last_line: false,
hooks: Hooks::new(),
@ -639,7 +654,54 @@ impl Value {
&mut errors);
}
"shell_integration" => {
process_bool_config(value, &mut errors, &mut config.shell_integration);
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"osc2" => {
process_bool_config(value, &mut errors, &mut config.shell_integration_osc2);
}
"osc7" => {
process_bool_config(value, &mut errors, &mut config.shell_integration_osc7);
}
"osc8" => {
process_bool_config(value, &mut errors, &mut config.shell_integration_osc8);
}
"osc9_9" => {
process_bool_config(value, &mut errors, &mut config.shell_integration_osc9_9);
}
"osc133" => {
process_bool_config(value, &mut errors, &mut config.shell_integration_osc133);
}
"osc633" => {
process_bool_config(value, &mut errors, &mut config.shell_integration_osc633);
}
"reset_application_mode" => {
process_bool_config(value, &mut errors, &mut config.shell_integration_reset_application_mode);
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
})
} else {
report_invalid_value("boolean value is deprecated, should be a record. see default_conifg.nu.", span, &mut errors);
// Reconstruct
*value = Value::record(
record! {
"osc2" => Value::bool(config.shell_integration_osc2, span),
"ocs7" => Value::bool(config.shell_integration_osc7, span),
"osc8" => Value::bool(config.shell_integration_osc8, span),
"osc9_9" => Value::bool(config.shell_integration_osc9_9, span),
"osc133" => Value::bool(config.shell_integration_osc133, span),
"osc633" => Value::bool(config.shell_integration_osc633, span),
"reset_application_mode" => Value::bool(config.shell_integration_reset_application_mode, span),
},
span,
);
}
}
"buffer_editor" => match value {
Value::Nothing { .. } | Value::String { .. } => {

View File

@ -236,7 +236,34 @@ $env.config = {
use_ansi_coloring: true
bracketed_paste: true # enable bracketed paste, currently useless on windows
edit_mode: emacs # emacs, vi
shell_integration: false # enables terminal shell integration. Off by default, as some terminals have issues with this.
shell_integration: {
# osc2 abbreviates the path if in the home_dir, sets the tab/window title, shows the running command in the tab/window title
osc2: true
# osc7 is a way to communicate the path to the terminal, this is helpful for spawning new tabs in the same directory
osc7: true
# osc8 is also implemented as the deprecated setting ls.show_clickable_links, it shows clickable links in ls output if your terminal supports it. show_clickable_links is deprecated in favor of osc8
osc8: true
# osc9_9 is from ConEmu and is starting to get wider support. It's similar to osc7 in that it communicates the path to the terminal
osc9_9: false
# osc133 is several escapes invented by Final Term which include the supported ones below.
# 133;A - Mark prompt start
# 133;B - Mark prompt end
# 133;C - Mark pre-execution
# 133;D;exit - Mark execution finished with exit code
# This is used to enable terminals to know where the prompt is, the command is, where the command finishes, and where the output of the command is
osc133: true
# osc633 is closely related to osc133 but only exists in visual studio code (vscode) and supports their shell integration features
# 633;A - Mark prompt start
# 633;B - Mark prompt end
# 633;C - Mark pre-execution
# 633;D;exit - Mark execution finished with exit code
# 633;E - NOT IMPLEMENTED - Explicitly set the command line with an optional nonce
# 633;P;Cwd=<path> - Mark the current working directory and communicate it to the terminal
# and also helps with the run recent menu in vscode
osc633: true
# reset_application_mode is escape \x1b[?1l and was added to help ssh work better
reset_application_mode: true
}
render_right_prompt_on_last_line: false # true or false to enable or disable right prompt to be rendered on last line of the prompt.
use_kitty_protocol: false # enables keyboard enhancement protocol implemented by kitty console, only if your terminal support this.
highlight_resolved_externals: false # true enables highlighting of external commands in the repl resolved by which.