From 0805f1fd90974f7ad2f3a54228368f88dcba261f Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 2 May 2024 09:56:50 -0400 Subject: [PATCH] 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= - 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 # After Submitting --- crates/nu-cli/src/prompt.rs | 37 +- crates/nu-cli/src/prompt_update.rs | 65 ++- crates/nu-cli/src/repl.rs | 488 ++++++++++++------ crates/nu-command/src/viewers/table.rs | 6 +- crates/nu-protocol/src/config/mod.rs | 68 ++- .../src/sample_config/default_config.nu | 29 +- 6 files changed, 521 insertions(+), 172 deletions(-) diff --git a/crates/nu-cli/src/prompt.rs b/crates/nu-cli/src/prompt.rs index 0ecdae1aaf..a2045a201c 100644 --- a/crates/nu-cli/src/prompt.rs +++ b/crates/nu-cli/src/prompt.rs @@ -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, right_prompt_string: Option, default_prompt_indicator: Option, @@ -18,12 +25,20 @@ pub struct NushellPrompt { default_vi_normal_prompt_indicator: Option, default_multiline_indicator: Option, 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() diff --git a/crates/nu-cli/src/prompt_update.rs b/crates/nu-cli/src/prompt_update.rs index 4c7cb7fcfe..0c5641378b 100644 --- a/crates/nu-cli/src/prompt_update.rs +++ b/crates/nu-cli/src/prompt_update.rs @@ -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><133 B><133 C> 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><133 B><133 C> - 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); diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 559611e265..0e5e24d46b 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -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><133 B><133 C> -// 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::::default()) // CLEAR STACK-REFERENCE 2 .with_completer(Box::::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 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 = diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index b032637275..11731e61b9 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -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(); diff --git a/crates/nu-protocol/src/config/mod.rs b/crates/nu-protocol/src/config/mod.rs index 312676038c..cd5ea69d7c 100644 --- a/crates/nu-protocol/src/config/mod.rs +++ b/crates/nu-protocol/src/config/mod.rs @@ -74,7 +74,14 @@ pub struct Config { pub menus: Vec, 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 { .. } => { diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 70e8a2ca7d..5681fb1a6d 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -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= - 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.