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.