use crate::prompt_update::{ POST_EXECUTION_MARKER_PREFIX, POST_EXECUTION_MARKER_SUFFIX, PRE_EXECUTION_MARKER, RESET_APPLICATION_MODE, VSCODE_COMMANDLINE_MARKER_PREFIX, VSCODE_COMMANDLINE_MARKER_SUFFIX, 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, prompt_update, reedline_config::{add_menus, create_keybindings, KeybindingsMode}, util::eval_source, NuHighlighter, NuValidator, NushellPrompt, }; use crossterm::cursor::SetCursorStyle; use log::{error, trace, warn}; use miette::{ErrReport, IntoDiagnostic, Result}; use nu_cmd_base::{hook::eval_hook, util::get_editor}; use nu_color_config::StyleComputer; #[allow(deprecated)] use nu_engine::{convert_env_values, current_dir_str, env_to_strings}; use nu_parser::{lex, parse, trim_quotes_str}; use nu_protocol::{ config::NuCursorShape, engine::{EngineState, Stack, StateWorkingSet}, report_shell_error, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value, }; use nu_utils::{ filesystem::{have_permission, PermissionResult}, perf, }; use reedline::{ CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory, HistorySessionId, Reedline, SqliteBackedHistory, Vi, }; use std::{ collections::HashMap, env::temp_dir, io::{self, IsTerminal, Write}, panic::{catch_unwind, AssertUnwindSafe}, path::{Path, PathBuf}, sync::Arc, time::{Duration, Instant}, }; use sysinfo::System; /// The main REPL loop, including spinning up the prompt itself. pub fn evaluate_repl( engine_state: &mut EngineState, stack: Stack, prerun_command: Option>, load_std_lib: Option>, entire_start_time: Instant, ) -> Result<()> { // throughout this code, we hold this stack uniquely. // During the main REPL loop, we hand ownership of this value to an Arc, // 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.clone(); let config = engine_state.get_config(); let use_color = config.use_ansi_coloring; confirm_stdin_is_terminal()?; let mut entry_num = 0; // 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 if let Err(e) = convert_env_values(engine_state, &unique_stack) { report_shell_error(engine_state, &e); } perf!("translate env vars", start_time, use_color); // seed env vars unique_stack.add_env_var( "CMD_DURATION_MS".into(), Value::string("0823", Span::unknown()), ); unique_stack.set_last_exit_code(0, Span::unknown()); let mut line_editor = get_line_editor(engine_state, use_color)?; let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4())); if let Some(s) = prerun_command { eval_source( engine_state, &mut unique_stack, s.item.as_bytes(), &format!("entry #{entry_num}"), PipelineData::empty(), false, ); engine_state.merge_env(&mut unique_stack)?; } let hostname = System::host_name(); 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 { // escape a few things because this says so // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st let cmd_text = line_editor.current_buffer_contents().to_string(); let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?; run_shell_integration_osc633( engine_state, &mut unique_stack, use_color, replaced_cmd_text, ); } engine_state.set_startup_time(entire_start_time.elapsed().as_nanos() as i64); // Regenerate the $nu constant to contain the startup time and any other potential updates engine_state.generate_nu_constant(); if load_std_lib.is_none() && engine_state.get_config().show_banner { eval_source( engine_state, &mut unique_stack, r#"banner"#.as_bytes(), "show_banner", PipelineData::empty(), false, ); } kitty_protocol_healthcheck(engine_state); // Setup initial engine_state and stack state let mut previous_engine_state = engine_state.clone(); let mut previous_stack_arc = Arc::new(unique_stack); loop { // clone these values so that they can be moved by AssertUnwindSafe // If there is a panic within this iteration the last engine_state and stack // will be used let mut current_engine_state = previous_engine_state.clone(); // for the stack, we are going to hold to create a child stack instead, // avoiding an expensive copy let current_stack = Stack::with_parent(previous_stack_arc.clone()); let temp_file_cloned = temp_file.clone(); let mut nu_prompt_cloned = nu_prompt.clone(); let iteration_panic_state = catch_unwind(AssertUnwindSafe(|| { let (continue_loop, current_stack, line_editor) = loop_iteration(LoopContext { engine_state: &mut current_engine_state, stack: current_stack, line_editor, nu_prompt: &mut nu_prompt_cloned, temp_file: &temp_file_cloned, use_color, entry_num: &mut entry_num, hostname: hostname.as_deref(), }); // pass the most recent version of the line_editor back ( continue_loop, current_engine_state, current_stack, line_editor, ) })); match iteration_panic_state { Ok((continue_loop, es, s, le)) => { // setup state for the next iteration of the repl loop previous_engine_state = es; // we apply the changes from the updated stack back onto our previous stack previous_stack_arc = Arc::new(Stack::with_changes_from_child(previous_stack_arc, s)); line_editor = le; if !continue_loop { break; } } Err(_) => { // line_editor is lost in the error case so reconstruct a new one line_editor = get_line_editor(engine_state, use_color)?; } } } Ok(()) } fn escape_special_vscode_bytes(input: &str) -> Result { let bytes = input .chars() .flat_map(|c| { let mut buf = [0; 4]; // Buffer to hold UTF-8 bytes of the character let c_bytes = c.encode_utf8(&mut buf); // Get UTF-8 bytes for the character if c_bytes.len() == 1 { let byte = c_bytes.as_bytes()[0]; match byte { // Escape bytes below 0x20 b if b < 0x20 => format!("\\x{:02X}", byte).into_bytes(), // Escape semicolon as \x3B b';' => "\\x3B".to_string().into_bytes(), // Escape backslash as \\ b'\\' => "\\\\".to_string().into_bytes(), // Otherwise, return the character unchanged _ => vec![byte], } } else { // pass through multi-byte characters unchanged c_bytes.bytes().collect() } }) .collect(); String::from_utf8(bytes).map_err(|err| ShellError::CantConvert { to_type: "string".to_string(), from_type: "bytes".to_string(), span: Span::unknown(), help: Some(format!( "Error {err}, Unable to convert {input} to escaped bytes" )), }) } fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result { let mut start_time = std::time::Instant::now(); let mut line_editor = Reedline::create(); // Now that reedline is created, get the history session id and store it in engine_state store_history_id_in_engine(engine_state, &line_editor); perf!("setup reedline", start_time, use_color); if let Some(history) = engine_state.history_config() { start_time = std::time::Instant::now(); line_editor = setup_history(engine_state, line_editor, history)?; perf!("setup history", start_time, use_color); } Ok(line_editor) } struct LoopContext<'a> { engine_state: &'a mut EngineState, stack: Stack, line_editor: Reedline, nu_prompt: &'a mut NushellPrompt, temp_file: &'a Path, use_color: bool, entry_num: &'a mut usize, hostname: Option<&'a str>, } /// Perform one iteration of the REPL loop /// Result is bool: continue loop, current reedline #[inline] fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { use nu_cmd_base::hook; use reedline::Signal; let loop_start_time = std::time::Instant::now(); let LoopContext { engine_state, mut stack, line_editor, nu_prompt, temp_file, use_color, entry_num, hostname, } = ctx; let mut start_time = std::time::Instant::now(); // Before doing anything, merge the environment from the previous REPL iteration into the // permanent state. if let Err(err) = engine_state.merge_env(&mut stack) { report_shell_error(engine_state, &err); } // Check whether $env.NU_DISABLE_IR is set, so that the user can change it in the REPL // Temporary while IR eval is optional stack.use_ir = !stack.has_env_var(engine_state, "NU_DISABLE_IR"); perf!("merge env", start_time, use_color); start_time = std::time::Instant::now(); engine_state.reset_signals(); perf!("reset signals", start_time, use_color); start_time = std::time::Instant::now(); // Right before we start our prompt and take input from the user, // fire the "pre_prompt" hook if let Some(hook) = engine_state.get_config().hooks.pre_prompt.clone() { if let Err(err) = eval_hook(engine_state, &mut stack, None, vec![], &hook, "pre_prompt") { report_shell_error(engine_state, &err); } } perf!("pre-prompt hook", start_time, use_color); start_time = std::time::Instant::now(); // Next, check all the environment variables they ask for // fire the "env_change" hook let env_change = engine_state.get_config().hooks.env_change.clone(); if let Err(error) = hook::eval_env_change_hook(env_change, engine_state, &mut stack) { report_shell_error(engine_state, &error) } perf!("env-change hook", start_time, use_color); let engine_reference = Arc::new(engine_state.clone()); let config = stack.get_config(engine_state); start_time = std::time::Instant::now(); // Find the configured cursor shapes for each mode let cursor_config = CursorConfig { vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert), vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal), emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs), }; perf!("get config/cursor config", start_time, use_color); start_time = std::time::Instant::now(); // at this line we have cloned the state for the completer and the transient prompt // until we drop those, we cannot use the stack in the REPL loop itself // See STACK-REFERENCE to see where we have taken a reference let stack_arc = Arc::new(stack); let mut line_editor = line_editor .use_kitty_keyboard_enhancement(config.use_kitty_protocol) // try to enable bracketed paste // It doesn't work on windows system: https://github.com/crossterm-rs/crossterm/issues/737 .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste) .with_highlighter(Box::new(NuHighlighter { engine_state: engine_reference.clone(), // STACK-REFERENCE 1 stack: stack_arc.clone(), })) .with_validator(Box::new(NuValidator { engine_state: engine_reference.clone(), })) .with_completer(Box::new(NuCompleter::new( engine_reference.clone(), // STACK-REFERENCE 2 stack_arc.clone(), ))) .with_quick_completions(config.completions.quick) .with_partial_completions(config.completions.partial) .with_ansi_colors(config.use_ansi_coloring) .with_cwd(Some( engine_state .cwd(None) .map(|cwd| cwd.into_std_path_buf()) .unwrap_or_default() .to_string_lossy() .to_string(), )) .with_cursor_config(cursor_config) .with_visual_selection_style(nu_ansi_term::Style { is_reverse: true, ..Default::default() }); perf!("reedline builder", start_time, use_color); let style_computer = StyleComputer::from_config(engine_state, &stack_arc); start_time = std::time::Instant::now(); line_editor = if config.use_ansi_coloring { line_editor.with_hinter(Box::new({ // As of Nov 2022, "hints" color_config closures only get `null` passed in. let style = style_computer.compute("hints", &Value::nothing(Span::unknown())); CwdAwareHinter::default().with_style(style) })) } else { line_editor.disable_hints() }; perf!("reedline coloring/style_computer", start_time, use_color); start_time = std::time::Instant::now(); trace!("adding menus"); line_editor = add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| { report_shell_error(engine_state, &e); Reedline::create() }); perf!("reedline adding menus", start_time, use_color); start_time = std::time::Instant::now(); let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown()); line_editor = if let Ok((cmd, args)) = buffer_editor { let mut command = std::process::Command::new(cmd); let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| { warn!("Couldn't convert environment variable values to strings: {e}"); HashMap::default() }); command.args(args).envs(envs); line_editor.with_buffer_editor(command, temp_file.to_path_buf()) } else { line_editor }; perf!("reedline buffer_editor", start_time, use_color); if let Some(history) = engine_state.history_config() { start_time = std::time::Instant::now(); if history.sync_on_enter { if let Err(e) = line_editor.sync_history() { warn!("Failed to sync history: {}", e); } } perf!("sync_history", start_time, use_color); } start_time = std::time::Instant::now(); // Changing the line editor based on the found keybindings line_editor = setup_keybindings(engine_state, line_editor); perf!("keybindings", start_time, use_color); start_time = std::time::Instant::now(); let config = &engine_state.get_config().clone(); prompt_update::update_prompt( config, engine_state, &mut Stack::with_parent(stack_arc.clone()), nu_prompt, ); let transient_prompt = prompt_update::make_transient_prompt( config, engine_state, &mut Stack::with_parent(stack_arc.clone()), nu_prompt, ); perf!("update_prompt", start_time, use_color); *entry_num += 1; start_time = std::time::Instant::now(); line_editor = line_editor.with_transient_prompt(transient_prompt); let input = line_editor.read_line(nu_prompt); // we got our inputs, we can now drop our stack references // This lists all of the stack references that we have cleaned up line_editor = line_editor // CLEAR STACK-REFERENCE 1 .with_highlighter(Box::::default()) // CLEAR STACK-REFERENCE 2 .with_completer(Box::::default()); // 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; // TODO: we may clone the stack, this can lead to major performance issues // so we should avoid it or making stack cheaper to clone. let mut stack = Arc::unwrap_or_clone(stack_arc); perf!("line_editor setup", start_time, use_color); let line_editor_input_time = std::time::Instant::now(); match input { Ok(Signal::Success(repl_cmd_line_text)) => { let history_supports_meta = matches!( engine_state.history_config().map(|h| h.file_format), Some(HistoryFileFormat::Sqlite) ); if history_supports_meta { prepare_history_metadata( &repl_cmd_line_text, hostname, engine_state, &mut line_editor, ); } // For pre_exec_hook start_time = Instant::now(); // Right before we start running the code the user gave us, fire the `pre_execution` // hook if let Some(hook) = config.hooks.pre_execution.clone() { // Set the REPL buffer to the current command for the "pre_execution" hook let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); repl.buffer = repl_cmd_line_text.to_string(); drop(repl); if let Err(err) = eval_hook( engine_state, &mut stack, None, vec![], &hook, "pre_execution", ) { report_shell_error(engine_state, &err); } } perf!("pre_execution_hook", start_time, use_color); let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); repl.cursor_pos = line_editor.current_insertion_point(); repl.buffer = line_editor.current_buffer_contents().to_string(); drop(repl); if shell_integration_osc633 { if stack .get_env_var(engine_state, "TERM_PROGRAM") .and_then(|v| v.as_str().ok()) == Some("vscode") { start_time = Instant::now(); run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER); perf!( "pre_execute_marker (633;C) ansi escape sequence", start_time, use_color ); } else if shell_integration_osc133 { start_time = Instant::now(); run_ansi_sequence(PRE_EXECUTION_MARKER); perf!( "pre_execute_marker (133;C) ansi escape sequence", start_time, use_color ); } } else if shell_integration_osc133 { start_time = Instant::now(); run_ansi_sequence(PRE_EXECUTION_MARKER); perf!( "pre_execute_marker (133;C) ansi escape sequence", start_time, use_color ); } // Actual command execution logic starts from here let cmd_execution_start_time = Instant::now(); match parse_operation(repl_cmd_line_text.clone(), engine_state, &stack) { Ok(operation) => match operation { ReplOperation::AutoCd { cwd, target, span } => { do_auto_cd(target, cwd, &mut stack, engine_state, span); run_finaliziation_ansi_sequence( &stack, engine_state, use_color, shell_integration_osc633, shell_integration_osc133, ); } ReplOperation::RunCommand(cmd) => { line_editor = do_run_cmd( &cmd, &mut stack, engine_state, line_editor, shell_integration_osc2, *entry_num, use_color, ); run_finaliziation_ansi_sequence( &stack, engine_state, use_color, shell_integration_osc633, shell_integration_osc133, ); } // as the name implies, we do nothing in this case ReplOperation::DoNothing => {} }, Err(ref e) => error!("Error parsing operation: {e}"), } let cmd_duration = cmd_execution_start_time.elapsed(); stack.add_env_var( "CMD_DURATION_MS".into(), Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()), ); if history_supports_meta { if let Err(e) = fill_in_result_related_history_metadata( &repl_cmd_line_text, engine_state, cmd_duration, &mut stack, &mut line_editor, ) { warn!("Could not fill in result related history metadata: {e}"); } } 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, repl_cmd_line_text, ); } 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 run_finaliziation_ansi_sequence( &stack, engine_state, use_color, shell_integration_osc633, shell_integration_osc133, ); } Ok(Signal::CtrlD) => { // When exiting clear to a new line run_finaliziation_ansi_sequence( &stack, engine_state, use_color, shell_integration_osc633, shell_integration_osc133, ); println!(); return (false, stack, line_editor); } Err(err) => { let message = err.to_string(); if !message.contains("duration") { eprintln!("Error: {err:?}"); // TODO: Identify possible error cases where a hard failure is preferable // Ignoring and reporting could hide bigger problems // e.g. https://github.com/nushell/nushell/issues/6452 // Alternatively only allow that expected failures let the REPL loop } run_finaliziation_ansi_sequence( &stack, engine_state, use_color, shell_integration_osc633, shell_integration_osc133, ); } } perf!( "processing line editor input", line_editor_input_time, use_color ); perf!( "time between prompts in line editor loop", loop_start_time, use_color ); (true, stack, line_editor) } /// /// Put in history metadata not related to the result of running the command /// fn prepare_history_metadata( s: &str, hostname: Option<&str>, engine_state: &EngineState, line_editor: &mut Reedline, ) { if !s.is_empty() && line_editor.has_last_command_context() { let result = line_editor .update_last_command_context(&|mut c| { c.start_timestamp = Some(chrono::Utc::now()); c.hostname = hostname.map(str::to_string); c.cwd = engine_state .cwd(None) .ok() .map(|path| path.to_string_lossy().to_string()); c }) .into_diagnostic(); if let Err(e) = result { warn!("Could not prepare history metadata: {e}"); } } } /// /// Fills in history item metadata based on the execution result (notably duration and exit code) /// fn fill_in_result_related_history_metadata( s: &str, engine_state: &EngineState, cmd_duration: Duration, stack: &mut Stack, line_editor: &mut Reedline, ) -> Result<()> { if !s.is_empty() && line_editor.has_last_command_context() { line_editor .update_last_command_context(&|mut c| { c.duration = Some(cmd_duration); c.exit_status = stack .get_env_var(engine_state, "LAST_EXIT_CODE") .and_then(|e| e.as_int().ok()); c }) .into_diagnostic()?; // todo: don't stop repl if error here? } Ok(()) } /// The kinds of operations you can do in a single loop iteration of the REPL enum ReplOperation { /// "auto-cd": change directory by typing it in directly AutoCd { /// the current working directory cwd: String, /// the target target: PathBuf, /// span information for debugging span: Span, }, /// run a command RunCommand(String), /// do nothing (usually through an empty string) DoNothing, } /// /// Parses one "REPL line" of input, to try and derive intent. /// Notably, this is where we detect whether the user is attempting an /// "auto-cd" (writing a relative path directly instead of `cd path`) /// /// Returns the ReplOperation we believe the user wants to do /// fn parse_operation( s: String, engine_state: &EngineState, stack: &Stack, ) -> Result { let tokens = lex(s.as_bytes(), 0, &[], &[], false); // Check if this is a single call to a directory, if so auto-cd #[allow(deprecated)] let cwd = nu_engine::env::current_dir_str(engine_state, stack).unwrap_or_default(); let mut orig = s.clone(); if orig.starts_with('`') { orig = trim_quotes_str(&orig).to_string() } let path = nu_path::expand_path_with(&orig, &cwd, true); if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 { Ok(ReplOperation::AutoCd { cwd, target: path, span: tokens.0[0].span, }) } else if !s.trim().is_empty() { Ok(ReplOperation::RunCommand(s)) } else { Ok(ReplOperation::DoNothing) } } /// /// Execute an "auto-cd" operation, changing the current working directory. /// fn do_auto_cd( path: PathBuf, cwd: String, stack: &mut Stack, engine_state: &mut EngineState, span: Span, ) { let path = { if !path.exists() { report_shell_error( engine_state, &ShellError::DirectoryNotFound { dir: path.to_string_lossy().to_string(), span, }, ); } path.to_string_lossy().to_string() }; if let PermissionResult::PermissionDenied(reason) = have_permission(path.clone()) { report_shell_error( engine_state, &ShellError::IOError { msg: format!("Cannot change directory to {path}: {reason}"), }, ); return; } stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), Span::unknown())); //FIXME: this only changes the current scope, but instead this environment variable //should probably be a block that loads the information from the state in the overlay if let Err(err) = stack.set_cwd(&path) { report_shell_error(engine_state, &err); return; }; let cwd = Value::string(cwd, span); let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); let mut shells = if let Some(v) = shells { v.clone().into_list().unwrap_or_else(|_| vec![cwd]) } else { vec![cwd] }; let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); let current_shell = if let Some(v) = current_shell { v.as_int().unwrap_or_default() as usize } else { 0 }; let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL"); let last_shell = if let Some(v) = last_shell { v.as_int().unwrap_or_default() as usize } else { 0 }; shells[current_shell] = Value::string(path, span); stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span)); stack.add_env_var( "NUSHELL_LAST_SHELL".into(), Value::int(last_shell as i64, span), ); stack.set_last_exit_code(0, Span::unknown()); } /// /// Run a command as received from reedline. This is where we are actually /// running a thing! /// fn do_run_cmd( s: &str, stack: &mut Stack, engine_state: &mut EngineState, // 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_osc2: bool, entry_num: usize, use_color: bool, ) -> Reedline { trace!("eval source: {}", s); let mut cmds = s.split_whitespace(); if let Some("exit") = cmds.next() { let mut working_set = StateWorkingSet::new(engine_state); let _ = parse(&mut working_set, None, s.as_bytes(), false); if working_set.parse_errors.is_empty() { match cmds.next() { Some(s) => { if let Ok(n) = s.parse::() { drop(line_editor); std::process::exit(n); } } None => { drop(line_editor); std::process::exit(0); } } } } if shell_integration_osc2 { run_shell_integration_osc2(Some(s), engine_state, stack, use_color); } eval_source( engine_state, stack, s.as_bytes(), &format!("entry #{entry_num}"), PipelineData::empty(), false, ); line_editor } /// /// Output some things and set environment variables so shells with the right integration /// can have more information about what is going on (both on startup and after we have /// run a command) /// fn run_shell_integration_osc2( command_name: Option<&str>, engine_state: &EngineState, stack: &mut Stack, use_color: bool, ) { #[allow(deprecated)] if let Ok(path) = current_dir_str(engine_state, stack) { let start_time = Instant::now(); // Try to abbreviate string for windows title let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() { let home_dir_str = p.as_path().display().to_string(); if path.starts_with(&home_dir_str) { path.replacen(&home_dir_str, "~", 1) } else { path } } else { 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;{title}\x07")); perf!("set title with command osc2", start_time, use_color); } } fn run_shell_integration_osc7( hostname: Option<&str>, engine_state: &EngineState, stack: &mut Stack, use_color: bool, ) { #[allow(deprecated)] if let Ok(path) = current_dir_str(engine_state, stack) { 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, use_color ); } } fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) { #[allow(deprecated)] if let Ok(path) = current_dir_str(engine_state, stack) { 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) // This is helpful in Windows Terminal with Duplicate Tab run_ansi_sequence(&format!( "\x1b]9;9;{}\x1b\\", percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS) )); perf!( "communicate path to terminal with osc9;9", start_time, use_color ); } } fn run_shell_integration_osc633( engine_state: &EngineState, stack: &mut Stack, use_color: bool, repl_cmd_line_text: String, ) { #[allow(deprecated)] if let Ok(path) = current_dir_str(engine_state, stack) { // 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") .and_then(|v| v.as_str().ok()) == Some("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, use_color ); // escape a few things because this says so // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st let replaced_cmd_text = escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text); //OSC 633 ; E ; [; Result { // Setup history_isolation aka "history per session" let history_session_id = if history.isolation { Reedline::create_history_session_id() } else { None }; if let Some(path) = history.file_path() { return update_line_editor_history( engine_state, path, history, line_editor, history_session_id, ); }; Ok(line_editor) } /// /// Setup Reedline keybindingds based on the provided config /// fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline { return match create_keybindings(engine_state.get_config()) { Ok(keybindings) => match keybindings { KeybindingsMode::Emacs(keybindings) => { let edit_mode = Box::new(Emacs::new(keybindings)); line_editor.with_edit_mode(edit_mode) } KeybindingsMode::Vi { insert_keybindings, normal_keybindings, } => { let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings)); line_editor.with_edit_mode(edit_mode) } }, Err(e) => { report_shell_error(engine_state, &e); line_editor } }; } /// /// Make sure that the terminal supports the kitty protocol if the config is asking for it /// fn kitty_protocol_healthcheck(engine_state: &EngineState) { if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() { warn!("Terminal doesn't support use_kitty_protocol config"); } } fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) { let session_id = line_editor .get_history_session_id() .map(i64::from) .unwrap_or(0); engine_state.history_session_id = session_id; } fn update_line_editor_history( engine_state: &mut EngineState, history_path: PathBuf, history: HistoryConfig, line_editor: Reedline, history_session_id: Option, ) -> Result { let history: Box = match history.file_format { HistoryFileFormat::Plaintext => Box::new( FileBackedHistory::with_file(history.max_size as usize, history_path) .into_diagnostic()?, ), HistoryFileFormat::Sqlite => Box::new( SqliteBackedHistory::with_file( history_path.to_path_buf(), history_session_id, Some(chrono::Utc::now()), ) .into_diagnostic()?, ), }; let line_editor = line_editor .with_history_session_id(history_session_id) .with_history_exclusion_prefix(Some(" ".into())) .with_history(history); store_history_id_in_engine(engine_state, &line_editor); Ok(line_editor) } fn confirm_stdin_is_terminal() -> Result<()> { // Guard against invocation without a connected terminal. // reedline / crossterm event polling will fail without a connected tty if !std::io::stdin().is_terminal() { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!", )) .into_diagnostic(); } Ok(()) } fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option { match shape { NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock), NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore), NuCursorShape::Line => Some(SetCursorStyle::SteadyBar), NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock), NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore), NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar), NuCursorShape::Inherit => None, } } fn get_command_finished_marker( stack: &Stack, engine_state: &EngineState, shell_integration_osc633: bool, shell_integration_osc133: bool, ) -> String { let exit_code = stack .get_env_var(engine_state, "LAST_EXIT_CODE") .and_then(|e| e.as_int().ok()); if shell_integration_osc633 { if stack .get_env_var(engine_state, "TERM_PROGRAM") .and_then(|v| v.as_str().ok()) == Some("vscode") { // We're in vscode and we have osc633 enabled format!( "{}{}{}", VSCODE_POST_EXECUTION_MARKER_PREFIX, exit_code.unwrap_or(0), VSCODE_POST_EXECUTION_MARKER_SUFFIX ) } else if shell_integration_osc133 { // If we're in VSCode but we don't find the env var, just return the regular markers format!( "{}{}{}", POST_EXECUTION_MARKER_PREFIX, exit_code.unwrap_or(0), POST_EXECUTION_MARKER_SUFFIX ) } else { // We're not in vscode, so we don't need to do anything special "\x1b[0m".to_string() } } else if shell_integration_osc133 { format!( "{}{}{}", POST_EXECUTION_MARKER_PREFIX, exit_code.unwrap_or(0), POST_EXECUTION_MARKER_SUFFIX ) } else { "\x1b[0m".to_string() } } fn run_ansi_sequence(seq: &str) { if let Err(e) = io::stdout().write_all(seq.as_bytes()) { warn!("Error writing ansi sequence {e}"); } else if let Err(e) = io::stdout().flush() { warn!("Error flushing stdio {e}"); } } 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") .and_then(|v| v.as_str().ok()) == Some("vscode") { let start_time = Instant::now(); run_ansi_sequence(&get_command_finished_marker( stack, engine_state, shell_integration_osc633, shell_integration_osc133, )); perf!( "post_execute_marker (633;D) ansi escape sequences", start_time, use_color ); } else if shell_integration_osc133 { let start_time = Instant::now(); run_ansi_sequence(&get_command_finished_marker( stack, engine_state, shell_integration_osc633, shell_integration_osc133, )); perf!( "post_execute_marker (133;D) ansi escape sequences", start_time, use_color ); } } else if shell_integration_osc133 { let start_time = Instant::now(); run_ansi_sequence(&get_command_finished_marker( stack, engine_state, shell_integration_osc633, shell_integration_osc133, )); perf!( "post_execute_marker (133;D) ansi escape sequences", start_time, use_color ); } } // Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo' #[cfg(windows)] static DRIVE_PATH_REGEX: std::sync::LazyLock = std::sync::LazyLock::new(|| { fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation") }); // A best-effort "does this string look kinda like a path?" function to determine whether to auto-cd fn looks_like_path(orig: &str) -> bool { #[cfg(windows)] { if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) { return true; } } orig.starts_with('.') || orig.starts_with('~') || orig.starts_with('/') || orig.starts_with('\\') || orig.ends_with(std::path::MAIN_SEPARATOR) } #[cfg(windows)] #[test] fn looks_like_path_windows_drive_path_works() { assert!(looks_like_path("C:")); assert!(looks_like_path("D:\\")); assert!(looks_like_path("E:/")); assert!(looks_like_path("F:\\some_dir")); assert!(looks_like_path("G:/some_dir")); } #[cfg(windows)] #[test] fn trailing_slash_looks_like_path() { assert!(looks_like_path("foo\\")) } #[cfg(not(windows))] #[test] fn trailing_slash_looks_like_path() { assert!(looks_like_path("foo/")) } #[test] fn are_session_ids_in_sync() { let engine_state = &mut EngineState::new(); let history = engine_state.history_config().unwrap(); let history_path = history.file_path().unwrap(); let line_editor = reedline::Reedline::create(); let history_session_id = reedline::Reedline::create_history_session_id(); let line_editor = update_line_editor_history( engine_state, history_path, history, line_editor, history_session_id, ); assert_eq!( i64::from(line_editor.unwrap().get_history_session_id().unwrap()), engine_state.history_session_id ); } #[cfg(test)] mod test_auto_cd { use super::{do_auto_cd, escape_special_vscode_bytes, parse_operation, ReplOperation}; use nu_path::AbsolutePath; use nu_protocol::engine::{EngineState, Stack}; use tempfile::tempdir; /// Create a symlink. Works on both Unix and Windows. #[cfg(any(unix, windows))] fn symlink( original: impl AsRef, link: impl AsRef, ) -> std::io::Result<()> { let original = original.as_ref(); let link = link.as_ref(); #[cfg(unix)] { std::os::unix::fs::symlink(original, link) } #[cfg(windows)] { if original.is_dir() { std::os::windows::fs::symlink_dir(original, link) } else { std::os::windows::fs::symlink_file(original, link) } } } /// Run one test case on the auto-cd feature. PWD is initially set to /// `before`, and after `input` is parsed and evaluated, PWD should be /// changed to `after`. #[track_caller] fn check(before: impl AsRef, input: &str, after: impl AsRef) { // Setup EngineState and Stack. let mut engine_state = EngineState::new(); let mut stack = Stack::new(); stack.set_cwd(before.as_ref()).unwrap(); // Parse the input. It must be an auto-cd operation. let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap(); let ReplOperation::AutoCd { cwd, target, span } = op else { panic!("'{}' was not parsed into an auto-cd operation", input) }; // Perform the auto-cd operation. do_auto_cd(target, cwd, &mut stack, &mut engine_state, span); let updated_cwd = engine_state.cwd(Some(&stack)).unwrap(); // Check that `updated_cwd` and `after` point to the same place. They // don't have to be byte-wise equal (on Windows, the 8.3 filename // conversion messes things up), let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap(); let after = std::fs::canonicalize(after.as_ref()).unwrap(); assert_eq!(updated_cwd, after); } #[test] fn auto_cd_root() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); let input = if cfg!(windows) { r"C:\" } else { "/" }; let root = AbsolutePath::try_new(input).unwrap(); check(tempdir, input, root); } #[test] fn auto_cd_tilde() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); let home = nu_path::home_dir().unwrap(); check(tempdir, "~", home); } #[test] fn auto_cd_dot() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); check(tempdir, ".", tempdir); } #[test] fn auto_cd_double_dot() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); let dir = tempdir.join("foo"); std::fs::create_dir_all(&dir).unwrap(); check(dir, "..", tempdir); } #[test] fn auto_cd_triple_dot() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); let dir = tempdir.join("foo").join("bar"); std::fs::create_dir_all(&dir).unwrap(); check(dir, "...", tempdir); } #[test] fn auto_cd_relative() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); let foo = tempdir.join("foo"); let bar = tempdir.join("bar"); std::fs::create_dir_all(&foo).unwrap(); std::fs::create_dir_all(&bar).unwrap(); let input = if cfg!(windows) { r"..\bar" } else { "../bar" }; check(foo, input, bar); } #[test] fn auto_cd_trailing_slash() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); let dir = tempdir.join("foo"); std::fs::create_dir_all(&dir).unwrap(); let input = if cfg!(windows) { r"foo\" } else { "foo/" }; check(tempdir, input, dir); } #[test] fn auto_cd_symlink() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); let dir = tempdir.join("foo"); std::fs::create_dir_all(&dir).unwrap(); let link = tempdir.join("link"); symlink(&dir, &link).unwrap(); let input = if cfg!(windows) { r".\link" } else { "./link" }; check(tempdir, input, link); } #[test] #[should_panic(expected = "was not parsed into an auto-cd operation")] fn auto_cd_nonexistent_directory() { let tempdir = tempdir().unwrap(); let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap(); let dir = tempdir.join("foo"); let input = if cfg!(windows) { r"foo\" } else { "foo/" }; check(tempdir, input, dir); } #[test] fn escape_vscode_semicolon_test() { let input = r#"now;is"#; let expected = r#"now\x3Bis"#; let actual = escape_special_vscode_bytes(input).unwrap(); assert_eq!(expected, actual); } #[test] fn escape_vscode_backslash_test() { let input = r#"now\is"#; let expected = r#"now\\is"#; let actual = escape_special_vscode_bytes(input).unwrap(); assert_eq!(expected, actual); } #[test] fn escape_vscode_linefeed_test() { let input = "now\nis"; let expected = r#"now\x0Ais"#; let actual = escape_special_vscode_bytes(input).unwrap(); assert_eq!(expected, actual); } #[test] fn escape_vscode_tab_null_cr_test() { let input = "now\t\0\ris"; let expected = r#"now\x09\x00\x0Dis"#; let actual = escape_special_vscode_bytes(input).unwrap(); assert_eq!(expected, actual); } #[test] fn escape_vscode_multibyte_ok() { let input = "now🍪is"; let actual = escape_special_vscode_bytes(input).unwrap(); assert_eq!(input, actual); } }