Skip to main content

shannon_nu_cli/
repl.rs

1use crate::prompt_update::{
2    POST_EXECUTION_MARKER_PREFIX, POST_EXECUTION_MARKER_SUFFIX, PRE_EXECUTION_MARKER,
3    RESET_APPLICATION_MODE, VSCODE_COMMANDLINE_MARKER_PREFIX, VSCODE_COMMANDLINE_MARKER_SUFFIX,
4    VSCODE_CWD_PROPERTY_MARKER_PREFIX, VSCODE_CWD_PROPERTY_MARKER_SUFFIX,
5    VSCODE_POST_EXECUTION_MARKER_PREFIX, VSCODE_POST_EXECUTION_MARKER_SUFFIX,
6    VSCODE_PRE_EXECUTION_MARKER,
7};
8use crate::{
9    NuHighlighter, NuValidator, NushellPrompt,
10    completions::NuCompleter,
11    nu_highlight::NoOpHighlighter,
12    prompt_update,
13    reedline_config::{KeybindingsMode, add_menus, create_keybindings},
14    util::eval_source,
15};
16use crossterm::cursor::SetCursorStyle;
17use log::{error, trace, warn};
18use miette::{ErrReport, IntoDiagnostic, Result};
19use nu_cmd_base::util::get_editor;
20use nu_color_config::StyleComputer;
21#[allow(deprecated)]
22use nu_engine::env_to_strings;
23use nu_engine::exit::cleanup_exit;
24use nu_parser::{lex, parse, trim_quotes_str};
25use nu_protocol::shell_error::io::IoError;
26use nu_protocol::{BannerKind, shell_error};
27use nu_protocol::{
28    Config, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value,
29    config::NuCursorShape,
30    engine::{EngineState, Stack, StateWorkingSet},
31    report_shell_error,
32};
33use nu_utils::time::Instant;
34use nu_utils::{
35    filesystem::{PermissionResult, have_permission},
36    perf,
37};
38#[cfg(feature = "sqlite")]
39use reedline::SqliteBackedHistory;
40use reedline::{
41    CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory,
42    HistorySessionId, MouseClickMode, Osc133ClickEventsMarkers, Osc633Markers, Reedline,
43    SemanticPromptMarkers, Vi,
44};
45use std::sync::atomic::Ordering;
46use std::{
47    collections::HashMap,
48    env::temp_dir,
49    io::{self, IsTerminal, Write},
50    panic::{AssertUnwindSafe, catch_unwind},
51    path::{Path, PathBuf},
52    sync::Arc,
53    time::Duration,
54};
55use sysinfo::System;
56
57fn semantic_markers_from_config(
58    config: &Config,
59    term_program_is_vscode: bool,
60) -> Option<Box<dyn SemanticPromptMarkers>> {
61    if config.shell_integration.osc633 && term_program_is_vscode {
62        Some(Osc633Markers::boxed())
63    } else if config.shell_integration.osc133 {
64        Some(Osc133ClickEventsMarkers::boxed())
65    } else {
66        None
67    }
68}
69
70/// The main REPL loop, including spinning up the prompt itself.
71pub fn evaluate_repl(
72    engine_state: &mut EngineState,
73    stack: Stack,
74    prerun_command: Option<Spanned<String>>,
75    load_std_lib: Option<Spanned<String>>,
76    entire_start_time: Instant,
77    mode_dispatcher: Option<std::sync::Arc<std::sync::Mutex<Box<dyn crate::ModeDispatcher>>>>,
78) -> Result<()> {
79    // throughout this code, we hold this stack uniquely.
80    // During the main REPL loop, we hand ownership of this value to an Arc,
81    // so that it may be read by various reedline plugins. During this, we
82    // can't modify the stack, but at the end of the loop we take back ownership
83    // from the Arc. This lets us avoid copying stack variables needlessly
84    let mut unique_stack = stack.clone();
85    let config = engine_state.get_config();
86    let use_color = config.use_ansi_coloring.get(engine_state);
87
88    let mut entry_num = 0;
89
90    // Let's grab the shell_integration configs
91    let shell_integration_osc2 = config.shell_integration.osc2;
92    let shell_integration_osc7 = config.shell_integration.osc7;
93    let shell_integration_osc9_9 = config.shell_integration.osc9_9;
94    let shell_integration_osc633 = config.shell_integration.osc633;
95
96    let nu_prompt = NushellPrompt::new();
97
98    // Seed env vars — no source span exists at REPL startup
99    unique_stack.add_env_var(
100        "CMD_DURATION_MS".into(),
101        Value::string("0823", Span::unknown()),
102    );
103
104    unique_stack.set_last_exit_code(0, Span::unknown());
105
106    let mut line_editor = get_line_editor(engine_state, use_color)?;
107    let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
108
109    if let Some(s) = prerun_command {
110        eval_source(
111            engine_state,
112            &mut unique_stack,
113            s.item.as_bytes(),
114            &format!("repl_entry #{entry_num}"),
115            PipelineData::empty(),
116            false,
117        );
118        engine_state.merge_env(&mut unique_stack)?;
119    }
120
121    confirm_stdin_is_terminal()?;
122
123    let hostname = System::host_name();
124    if shell_integration_osc2 {
125        run_shell_integration_osc2(None, engine_state, &mut unique_stack, use_color);
126    }
127    if shell_integration_osc7 {
128        run_shell_integration_osc7(
129            hostname.as_deref(),
130            engine_state,
131            &mut unique_stack,
132            use_color,
133        );
134    }
135    if shell_integration_osc9_9 {
136        run_shell_integration_osc9_9(engine_state, &mut unique_stack, use_color);
137    }
138    if shell_integration_osc633 {
139        // escape a few things because this says so
140        // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
141        let cmd_text = line_editor.current_buffer_contents().to_string();
142
143        let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?;
144
145        run_shell_integration_osc633(
146            engine_state,
147            &mut unique_stack,
148            use_color,
149            replaced_cmd_text,
150        );
151    }
152
153    engine_state.set_startup_time(entire_start_time.elapsed().as_nanos() as i64);
154
155    // Regenerate the $nu constant to contain the startup time and any other potential updates
156    engine_state.generate_nu_constant();
157
158    if load_std_lib.is_none() {
159        match engine_state.get_config().show_banner {
160            BannerKind::None => {}
161            BannerKind::Short => {
162                eval_source(
163                    engine_state,
164                    &mut unique_stack,
165                    r#"banner --short"#.as_bytes(),
166                    "show short banner",
167                    PipelineData::empty(),
168                    false,
169                );
170            }
171            BannerKind::Full => {
172                eval_source(
173                    engine_state,
174                    &mut unique_stack,
175                    r#"banner"#.as_bytes(),
176                    "show_banner",
177                    PipelineData::empty(),
178                    false,
179                );
180            }
181        }
182    }
183
184    kitty_protocol_healthcheck(engine_state);
185
186    // Setup initial engine_state and stack state
187    let mut previous_engine_state = engine_state.clone();
188    let mut previous_stack_arc = Arc::new(unique_stack);
189    loop {
190        // clone these values so that they can be moved by AssertUnwindSafe
191        // If there is a panic within this iteration the last engine_state and stack
192        // will be used
193        let mut current_engine_state = previous_engine_state.clone();
194        // for the stack, we are going to hold to create a child stack instead,
195        // avoiding an expensive copy
196        let current_stack = Stack::with_parent(previous_stack_arc.clone());
197        let temp_file_cloned = temp_file.clone();
198        let mut nu_prompt_cloned = nu_prompt.clone();
199
200        let iteration_panic_state = catch_unwind(AssertUnwindSafe(|| {
201            let (continue_loop, current_stack, line_editor) = loop_iteration(LoopContext {
202                engine_state: &mut current_engine_state,
203                stack: current_stack,
204                line_editor,
205                nu_prompt: &mut nu_prompt_cloned,
206                temp_file: &temp_file_cloned,
207                use_color,
208                entry_num: &mut entry_num,
209                hostname: hostname.as_deref(),
210                mode_dispatcher: mode_dispatcher.clone(),
211            });
212
213            // pass the most recent version of the line_editor back
214            (
215                continue_loop,
216                current_engine_state,
217                current_stack,
218                line_editor,
219            )
220        }));
221        match iteration_panic_state {
222            Ok((continue_loop, mut es, s, le)) => {
223                // We apply the changes from the updated stack back onto our previous stack
224                let mut merged_stack = Stack::with_changes_from_child(previous_stack_arc, s);
225
226                // Check if new variables were created (indicating potential variable shadowing)
227                let prev_total_vars = previous_engine_state.num_vars();
228                let curr_total_vars = es.num_vars();
229                let new_variables_created = curr_total_vars > prev_total_vars;
230
231                if new_variables_created {
232                    // New variables created, clean up stack to prevent memory leaks
233                    es.cleanup_stack_variables(&mut merged_stack);
234                }
235
236                previous_stack_arc = Arc::new(merged_stack);
237                // setup state for the next iteration of the repl loop
238                previous_engine_state = es;
239                line_editor = le;
240                if !continue_loop {
241                    break;
242                }
243            }
244            Err(_) => {
245                // line_editor is lost in the error case so reconstruct a new one
246                line_editor = get_line_editor(engine_state, use_color)?;
247            }
248        }
249    }
250
251    Ok(())
252}
253
254fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
255    let bytes = input
256        .chars()
257        .flat_map(|c| {
258            let mut buf = [0; 4]; // Buffer to hold UTF-8 bytes of the character
259            let c_bytes = c.encode_utf8(&mut buf); // Get UTF-8 bytes for the character
260
261            if c_bytes.len() == 1 {
262                let byte = c_bytes.as_bytes()[0];
263
264                match byte {
265                    // Escape bytes below 0x20
266                    b if b < 0x20 => format!("\\x{byte:02X}").into_bytes(),
267                    // Escape semicolon as \x3B
268                    b';' => "\\x3B".to_string().into_bytes(),
269                    // Escape backslash as \\
270                    b'\\' => "\\\\".to_string().into_bytes(),
271                    // Otherwise, return the character unchanged
272                    _ => vec![byte],
273                }
274            } else {
275                // pass through multi-byte characters unchanged
276                c_bytes.bytes().collect()
277            }
278        })
279        .collect();
280
281    // No source span available — this is an internal REPL helper for vscode integration
282    String::from_utf8(bytes).map_err(|err| ShellError::CantConvert {
283        to_type: "string".to_string(),
284        from_type: "bytes".to_string(),
285        span: Span::unknown(),
286        help: Some(format!(
287            "Error {err}, Unable to convert {input} to escaped bytes"
288        )),
289    })
290}
291
292fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
293    let mut start_time = Instant::now();
294    let mut line_editor = Reedline::create();
295
296    // Now that reedline is created, get the history session id and store it in engine_state
297    store_history_id_in_engine(engine_state, &line_editor);
298    perf!("setup reedline", start_time, use_color);
299
300    if let Some(history) = engine_state.history_config() {
301        start_time = Instant::now();
302
303        line_editor = setup_history(engine_state, line_editor, history)?;
304
305        perf!("setup history", start_time, use_color);
306    }
307    Ok(line_editor)
308}
309
310struct LoopContext<'a> {
311    engine_state: &'a mut EngineState,
312    stack: Stack,
313    line_editor: Reedline,
314    nu_prompt: &'a mut NushellPrompt,
315    temp_file: &'a Path,
316    use_color: bool,
317    entry_num: &'a mut usize,
318    hostname: Option<&'a str>,
319    mode_dispatcher: Option<std::sync::Arc<std::sync::Mutex<Box<dyn crate::ModeDispatcher>>>>,
320}
321
322/// Perform one iteration of the REPL loop
323/// Result is bool: continue loop, current reedline
324#[inline]
325fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
326    use nu_cmd_base::hook;
327    use reedline::Signal;
328    let loop_start_time = Instant::now();
329
330    let LoopContext {
331        engine_state,
332        mut stack,
333        line_editor,
334        nu_prompt,
335        temp_file,
336        use_color,
337        entry_num,
338        hostname,
339        mode_dispatcher,
340    } = ctx;
341
342    let mut start_time = Instant::now();
343    // Before doing anything, merge the environment from the previous REPL iteration into the
344    // permanent state.
345    if let Err(err) = engine_state.merge_env(&mut stack) {
346        report_shell_error(None, engine_state, &err);
347    }
348    perf!("merge env", start_time, use_color);
349
350    start_time = Instant::now();
351    engine_state.reset_signals();
352    perf!("reset signals", start_time, use_color);
353
354    start_time = Instant::now();
355    // Check all the environment variables they ask for
356    // fire the "env_change" hook
357    if let Err(error) = hook::eval_env_change_hook(
358        &engine_state.get_config().hooks.env_change.clone(),
359        engine_state,
360        &mut stack,
361    ) {
362        report_shell_error(None, engine_state, &error)
363    }
364    perf!("env-change hook", start_time, use_color);
365
366    start_time = Instant::now();
367    // Next, right before we start our prompt and take input from the user, fire the "pre_prompt" hook
368    if let Err(err) = hook::eval_hooks(
369        engine_state,
370        &mut stack,
371        vec![],
372        &engine_state.get_config().hooks.pre_prompt.clone(),
373        "pre_prompt",
374    ) {
375        report_shell_error(None, engine_state, &err);
376    }
377    perf!("pre-prompt hook", start_time, use_color);
378
379    let engine_reference = Arc::new(engine_state.clone());
380    let config = stack.get_config(engine_state);
381
382    start_time = Instant::now();
383    // Find the configured cursor shapes for each mode
384    let cursor_config = CursorConfig {
385        vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert),
386        vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal),
387        emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs),
388    };
389    perf!("get config/cursor config", start_time, use_color);
390
391    start_time = Instant::now();
392    // at this line we have cloned the state for the completer and the transient prompt
393    // until we drop those, we cannot use the stack in the REPL loop itself
394    // See STACK-REFERENCE to see where we have taken a reference
395    let stack_arc = Arc::new(stack);
396    let term_program_is_vscode = engine_state
397        .get_env_var("TERM_PROGRAM")
398        .and_then(|v| v.as_str().ok())
399        == Some("vscode");
400    let mut line_editor = line_editor
401        .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
402        // try to enable bracketed paste
403        // It doesn't work on windows system: https://github.com/crossterm-rs/crossterm/issues/737
404        .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
405        // Shannon: use mode-aware highlighter
406        .with_highlighter({
407            let shannon_mode = stack_arc.get_env_var(engine_state, "SHANNON_MODE")
408                .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
409                .unwrap_or_else(|| "nu".to_string());
410            if shannon_mode == "bash" {
411                Box::new(crate::bash_highlight::BashHighlighter::new(&config))
412                    as Box<dyn reedline::Highlighter>
413            } else if shannon_mode != "nu" {
414                Box::<NoOpHighlighter>::default() as Box<dyn reedline::Highlighter>
415            } else {
416                Box::new(NuHighlighter {
417                    engine_state: engine_reference.clone(),
418                    // STACK-REFERENCE 1
419                    stack: stack_arc.clone(),
420                }) as Box<dyn reedline::Highlighter>
421            }
422        })
423        .with_validator(Box::new(NuValidator {
424            engine_state: engine_reference.clone(),
425        }))
426        .with_completer(Box::new(NuCompleter::new(
427            engine_reference.clone(),
428            // STACK-REFERENCE 2
429            stack_arc.clone(),
430        )))
431        .with_quick_completions(config.completions.quick)
432        .with_partial_completions(config.completions.partial)
433        .with_ansi_colors(config.use_ansi_coloring.get(engine_state))
434        .with_cwd(Some(
435            engine_state
436                .cwd(None)
437                .map(|cwd| cwd.into_std_path_buf())
438                .unwrap_or_default()
439                .to_string_lossy()
440                .to_string(),
441        ))
442        .with_cursor_config(cursor_config)
443        .with_visual_selection_style(nu_ansi_term::Style {
444            is_reverse: true,
445            ..Default::default()
446        })
447        .with_semantic_markers(semantic_markers_from_config(
448            &config,
449            term_program_is_vscode,
450        ))
451        .with_mouse_click(if config.shell_integration.osc133 {
452            MouseClickMode::Enabled
453        } else {
454            MouseClickMode::Disabled
455        });
456
457    perf!("reedline builder", start_time, use_color);
458
459    let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
460
461    start_time = Instant::now();
462    line_editor = if config.use_ansi_coloring.get(engine_state) && config.show_hints {
463        line_editor.with_hinter(Box::new({
464            // As of Nov 2022, "hints" color_config closures only get `null` passed in.
465            let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
466            CwdAwareHinter::default().with_style(style)
467        }))
468    } else {
469        line_editor.disable_hints()
470    };
471
472    perf!("reedline coloring/style_computer", start_time, use_color);
473
474    start_time = Instant::now();
475    trace!("adding menus");
476    line_editor =
477        add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
478            report_shell_error(None, engine_state, &e);
479            Reedline::create()
480        });
481
482    perf!("reedline adding menus", start_time, use_color);
483
484    start_time = Instant::now();
485    // No call span available in the REPL loop for editor lookup
486    let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
487
488    line_editor = if let Ok((cmd, args)) = buffer_editor {
489        let mut command = std::process::Command::new(cmd);
490        let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| {
491            warn!("Couldn't convert environment variable values to strings: {e}");
492            HashMap::default()
493        });
494        command.args(args).envs(envs);
495        line_editor.with_buffer_editor(command, temp_file.to_path_buf())
496    } else {
497        line_editor
498    };
499
500    perf!("reedline buffer_editor", start_time, use_color);
501
502    if let Some(history) = engine_state.history_config() {
503        start_time = Instant::now();
504
505        line_editor = line_editor
506            .with_history_exclusion_prefix(history.ignore_space_prefixed.then_some(" ".into()));
507
508        if history.sync_on_enter
509            && let Err(e) = line_editor.sync_history()
510        {
511            warn!("Failed to sync history: {e}");
512        }
513
514        perf!("sync_history", start_time, use_color);
515    }
516
517    start_time = Instant::now();
518    // Changing the line editor based on the found keybindings
519    line_editor = setup_keybindings(engine_state, line_editor);
520
521    perf!("keybindings", start_time, use_color);
522
523    start_time = Instant::now();
524    let config = &engine_state.get_config().clone();
525    prompt_update::update_prompt(
526        config,
527        engine_state,
528        &mut Stack::with_parent(stack_arc.clone()),
529        nu_prompt,
530    );
531    let transient_prompt = prompt_update::make_transient_prompt(
532        config,
533        engine_state,
534        &mut Stack::with_parent(stack_arc.clone()),
535        nu_prompt,
536    );
537
538    perf!("update_prompt", start_time, use_color);
539
540    *entry_num += 1;
541
542    start_time = Instant::now();
543    line_editor = line_editor.with_transient_prompt(transient_prompt);
544    let input = line_editor.read_line(nu_prompt);
545    // we got our inputs, we can now drop our stack references
546    // This lists all of the stack references that we have cleaned up
547    line_editor = line_editor
548        // CLEAR STACK-REFERENCE 1
549        .with_highlighter(Box::<NoOpHighlighter>::default())
550        // CLEAR STACK-REFERENCE 2
551        .with_completer(Box::<DefaultCompleter>::default())
552        // Ensure immediately accept is always cleared
553        .with_immediately_accept(false);
554
555    // Let's grab the shell_integration configs
556    let shell_integration_osc2 = config.shell_integration.osc2;
557    let shell_integration_osc7 = config.shell_integration.osc7;
558    let shell_integration_osc9_9 = config.shell_integration.osc9_9;
559    let shell_integration_osc133 = config.shell_integration.osc133;
560    let shell_integration_osc633 = config.shell_integration.osc633;
561    let shell_integration_reset_application_mode = config.shell_integration.reset_application_mode;
562
563    // TODO: we may clone the stack, this can lead to major performance issues
564    // so we should avoid it or making stack cheaper to clone.
565    let mut stack = Arc::unwrap_or_clone(stack_arc);
566
567    perf!("line_editor setup", start_time, use_color);
568
569    let line_editor_input_time = Instant::now();
570    match input {
571        Ok(Signal::Success(repl_cmd_line_text)) => {
572            // Shannon: handle mode switch command
573            if repl_cmd_line_text.trim() == "__shannon_switch" {
574                if let Some(ref _dispatcher) = mode_dispatcher {
575                    let current = stack
576                        .get_env_var(engine_state, "SHANNON_MODE")
577                        .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
578                        .unwrap_or_else(|| "nu".to_string());
579                    let modes = ["nu", "bash"];
580                    let next_idx = modes.iter().position(|m| *m == current)
581                        .map(|i| (i + 1) % modes.len())
582                        .unwrap_or(0);
583                    stack.add_env_var(
584                        "SHANNON_MODE".to_string(),
585                        Value::string(modes[next_idx], Span::unknown()),
586                    );
587                }
588                return (true, stack, line_editor);
589            }
590            // Shannon: handle exit from non-nu modes
591            if repl_cmd_line_text.trim() == "exit" {
592                if let Some(ref _dispatcher) = mode_dispatcher {
593                    let mode = stack
594                        .get_env_var(engine_state, "SHANNON_MODE")
595                        .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
596                        .unwrap_or_else(|| "nu".to_string());
597                    if mode != "nu" {
598                        return (false, stack, line_editor);
599                    }
600                }
601            }
602
603            let history_supports_meta = match engine_state.history_config().map(|h| h.file_format) {
604                #[cfg(feature = "sqlite")]
605                Some(HistoryFileFormat::Sqlite) => true,
606                _ => false,
607            };
608
609            if history_supports_meta {
610                prepare_history_metadata(
611                    &repl_cmd_line_text,
612                    hostname,
613                    engine_state,
614                    &mut line_editor,
615                );
616            }
617
618            // For pre_exec_hook
619            start_time = Instant::now();
620
621            // Right before we start running the code the user gave us, fire the `pre_execution`
622            // hook
623            {
624                // Set the REPL buffer to the current command for the "pre_execution" hook
625                let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
626                repl.buffer = repl_cmd_line_text.to_string();
627                drop(repl);
628
629                if let Err(err) = hook::eval_hooks(
630                    engine_state,
631                    &mut stack,
632                    vec![],
633                    &engine_state.get_config().hooks.pre_execution.clone(),
634                    "pre_execution",
635                ) {
636                    report_shell_error(None, engine_state, &err);
637                }
638            }
639
640            perf!("pre_execution_hook", start_time, use_color);
641
642            let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
643            repl.cursor_pos = line_editor.current_insertion_point();
644            repl.buffer = line_editor.current_buffer_contents().to_string();
645            drop(repl);
646
647            if shell_integration_osc633 {
648                if stack
649                    .get_env_var(engine_state, "TERM_PROGRAM")
650                    .and_then(|v| v.as_str().ok())
651                    == Some("vscode")
652                {
653                    start_time = Instant::now();
654
655                    run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
656
657                    perf!(
658                        "pre_execute_marker (633;C) ansi escape sequence",
659                        start_time,
660                        use_color
661                    );
662                } else if shell_integration_osc133 {
663                    start_time = Instant::now();
664
665                    run_ansi_sequence(PRE_EXECUTION_MARKER);
666
667                    perf!(
668                        "pre_execute_marker (133;C) ansi escape sequence",
669                        start_time,
670                        use_color
671                    );
672                }
673            } else if shell_integration_osc133 {
674                start_time = Instant::now();
675
676                run_ansi_sequence(PRE_EXECUTION_MARKER);
677
678                perf!(
679                    "pre_execute_marker (133;C) ansi escape sequence",
680                    start_time,
681                    use_color
682                );
683            }
684
685            // Actual command execution logic starts from here
686            let cmd_execution_start_time = Instant::now();
687
688            // Shannon mode dispatch — intercept non-nushell modes
689            let mut shannon_handled = false;
690            if let Some(ref dispatcher) = mode_dispatcher {
691                let mode = stack
692                    .get_env_var(engine_state, "SHANNON_MODE")
693                    .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
694                    .unwrap_or_else(|| "nu".to_string());
695                if mode != "nu" {
696                    #[allow(deprecated)]
697                    let env_strings = env_to_strings(engine_state, &stack).unwrap_or_default();
698                    let cwd = stack
699                        .get_env_var(engine_state, "PWD")
700                        .and_then(|v| v.as_str().ok())
701                        .map(std::path::PathBuf::from)
702                        .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
703                    let result = dispatcher.lock().unwrap().execute(
704                        &mode,
705                        &repl_cmd_line_text,
706                        env_strings,
707                        cwd,
708                    );
709                    for (key, value) in &result.env {
710                        stack.add_env_var(
711                            key.clone(),
712                            Value::string(value, Span::unknown()),
713                        );
714                    }
715                    let _ = stack.set_cwd(&result.cwd);
716                    let _ = std::env::set_current_dir(&result.cwd);
717                    shannon_handled = true;
718                }
719            }
720
721            if !shannon_handled {
722            match parse_operation(repl_cmd_line_text.clone(), engine_state, &stack) {
723                Ok(operation) => match operation {
724                    ReplOperation::AutoCd { cwd, target, span } => {
725                        do_auto_cd(target, cwd, &mut stack, engine_state, span);
726
727                        run_finaliziation_ansi_sequence(
728                            &stack,
729                            engine_state,
730                            use_color,
731                            shell_integration_osc633,
732                            shell_integration_osc133,
733                        );
734                    }
735                    ReplOperation::RunCommand(cmd) => {
736                        line_editor = do_run_cmd(
737                            &cmd,
738                            &mut stack,
739                            engine_state,
740                            line_editor,
741                            shell_integration_osc2,
742                            *entry_num,
743                            use_color,
744                        );
745
746                        run_finaliziation_ansi_sequence(
747                            &stack,
748                            engine_state,
749                            use_color,
750                            shell_integration_osc633,
751                            shell_integration_osc133,
752                        );
753                    }
754                    // as the name implies, we do nothing in this case
755                    ReplOperation::DoNothing => {}
756                },
757                Err(ref e) => error!("Error parsing operation: {e}"),
758            }
759            } // end if !shannon_handled
760            let cmd_duration = cmd_execution_start_time.elapsed();
761
762            // No source span for REPL-generated timing values
763            stack.add_env_var(
764                "CMD_DURATION_MS".into(),
765                Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
766            );
767
768            if history_supports_meta
769                && let Err(e) = fill_in_result_related_history_metadata(
770                    &repl_cmd_line_text,
771                    engine_state,
772                    cmd_duration,
773                    &mut stack,
774                    &mut line_editor,
775                )
776            {
777                warn!("Could not fill in result related history metadata: {e}");
778            }
779
780            if shell_integration_osc2 {
781                run_shell_integration_osc2(None, engine_state, &mut stack, use_color);
782            }
783            if shell_integration_osc7 {
784                run_shell_integration_osc7(hostname, engine_state, &mut stack, use_color);
785            }
786            if shell_integration_osc9_9 {
787                run_shell_integration_osc9_9(engine_state, &mut stack, use_color);
788            }
789            if shell_integration_osc633 {
790                run_shell_integration_osc633(
791                    engine_state,
792                    &mut stack,
793                    use_color,
794                    repl_cmd_line_text,
795                );
796            }
797            if shell_integration_reset_application_mode {
798                run_shell_integration_reset_application_mode();
799            }
800
801            line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
802        }
803        Ok(Signal::CtrlC) => {
804            // `Reedline` clears the line content. New prompt is shown
805            run_finaliziation_ansi_sequence(
806                &stack,
807                engine_state,
808                use_color,
809                shell_integration_osc633,
810                shell_integration_osc133,
811            );
812        }
813        Ok(Signal::CtrlD) => {
814            // When exiting clear to a new line
815
816            run_finaliziation_ansi_sequence(
817                &stack,
818                engine_state,
819                use_color,
820                shell_integration_osc633,
821                shell_integration_osc133,
822            );
823
824            println!();
825
826            cleanup_exit((), engine_state, 0);
827
828            // if cleanup_exit didn't exit, we should keep running
829            return (true, stack, line_editor);
830        }
831        Err(err) => {
832            let message = err.to_string();
833            if !message.contains("duration") {
834                eprintln!("Error: {err:?}");
835                // TODO: Identify possible error cases where a hard failure is preferable
836                // Ignoring and reporting could hide bigger problems
837                // e.g. https://github.com/nushell/nushell/issues/6452
838                // Alternatively only allow that expected failures let the REPL loop
839            }
840
841            run_finaliziation_ansi_sequence(
842                &stack,
843                engine_state,
844                use_color,
845                shell_integration_osc633,
846                shell_integration_osc133,
847            );
848        }
849        Ok(_) => {
850            // Handle any new reedline Signal variants
851        }
852    }
853    perf!(
854        "processing line editor input",
855        line_editor_input_time,
856        use_color
857    );
858
859    perf!(
860        "time between prompts in line editor loop",
861        loop_start_time,
862        use_color
863    );
864
865    (true, stack, line_editor)
866}
867
868///
869/// Put in history metadata not related to the result of running the command
870///
871fn prepare_history_metadata(
872    s: &str,
873    hostname: Option<&str>,
874    engine_state: &EngineState,
875    line_editor: &mut Reedline,
876) {
877    if !s.is_empty() && line_editor.has_last_command_context() {
878        let result = line_editor
879            .update_last_command_context(&|mut c| {
880                c.start_timestamp = Some(chrono::Utc::now());
881                c.hostname = hostname.map(str::to_string);
882                c.cwd = engine_state
883                    .cwd(None)
884                    .ok()
885                    .map(|path| path.to_string_lossy().to_string());
886                c
887            })
888            .into_diagnostic();
889        if let Err(e) = result {
890            warn!("Could not prepare history metadata: {e}");
891        }
892    }
893}
894
895///
896/// Fills in history item metadata based on the execution result (notably duration and exit code)
897///
898fn fill_in_result_related_history_metadata(
899    s: &str,
900    engine_state: &EngineState,
901    cmd_duration: Duration,
902    stack: &mut Stack,
903    line_editor: &mut Reedline,
904) -> Result<()> {
905    if !s.is_empty() && line_editor.has_last_command_context() {
906        line_editor
907            .update_last_command_context(&|mut c| {
908                c.duration = Some(cmd_duration);
909                c.exit_status = stack
910                    .get_env_var(engine_state, "LAST_EXIT_CODE")
911                    .and_then(|e| e.as_int().ok());
912                c
913            })
914            .into_diagnostic()?; // todo: don't stop repl if error here?
915    }
916    Ok(())
917}
918
919/// The kinds of operations you can do in a single loop iteration of the REPL
920enum ReplOperation {
921    /// "auto-cd": change directory by typing it in directly
922    AutoCd {
923        /// the current working directory
924        cwd: String,
925        /// the target
926        target: PathBuf,
927        /// span information for debugging
928        span: Span,
929    },
930    /// run a command
931    RunCommand(String),
932    /// do nothing (usually through an empty string)
933    DoNothing,
934}
935
936///
937/// Parses one "REPL line" of input, to try and derive intent.
938/// Notably, this is where we detect whether the user is attempting an
939/// "auto-cd" (writing a relative path directly instead of `cd path`)
940///
941/// Returns the ReplOperation we believe the user wants to do
942///
943fn parse_operation(
944    s: String,
945    engine_state: &EngineState,
946    stack: &Stack,
947) -> Result<ReplOperation, ErrReport> {
948    let tokens = lex(s.as_bytes(), 0, &[], &[], false);
949    // Check if this is a single call to a directory, if so auto-cd
950    let cwd = engine_state
951        .cwd(Some(stack))
952        .map(|p| p.to_string_lossy().to_string())
953        .unwrap_or_default();
954    let mut orig = s.clone();
955    if orig.starts_with('`') {
956        orig = trim_quotes_str(&orig).to_string()
957    }
958
959    let path = nu_path::expand_path_with(&orig, &cwd, true);
960    if (engine_state.get_config().auto_cd_implicit || looks_like_path(&orig))
961        && path.is_dir()
962        && tokens.0.len() == 1
963    {
964        Ok(ReplOperation::AutoCd {
965            cwd,
966            target: path,
967            span: tokens.0[0].span,
968        })
969    } else if !s.trim().is_empty() {
970        Ok(ReplOperation::RunCommand(s))
971    } else {
972        Ok(ReplOperation::DoNothing)
973    }
974}
975
976///
977/// Execute an "auto-cd" operation, changing the current working directory.
978///
979fn do_auto_cd(
980    path: PathBuf,
981    cwd: String,
982    stack: &mut Stack,
983    engine_state: &mut EngineState,
984    span: Span,
985) {
986    let path = {
987        if !path.exists() {
988            report_shell_error(
989                Some(stack),
990                engine_state,
991                &ShellError::Io(IoError::new_with_additional_context(
992                    shell_error::io::ErrorKind::DirectoryNotFound,
993                    span,
994                    PathBuf::from(&path),
995                    "Cannot change directory",
996                )),
997            );
998        }
999        path.to_string_lossy().to_string()
1000    };
1001
1002    if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
1003        report_shell_error(
1004            Some(stack),
1005            engine_state,
1006            &ShellError::Io(IoError::new_with_additional_context(
1007                shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
1008                span,
1009                PathBuf::from(path),
1010                "Cannot change directory",
1011            )),
1012        );
1013        return;
1014    }
1015
1016    stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), span));
1017
1018    //FIXME: this only changes the current scope, but instead this environment variable
1019    //should probably be a block that loads the information from the state in the overlay
1020    if let Err(err) = stack.set_cwd(&path) {
1021        report_shell_error(Some(stack), engine_state, &err);
1022        return;
1023    };
1024    let cwd = Value::string(cwd, span);
1025
1026    let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
1027    let mut shells = if let Some(v) = shells {
1028        v.clone().into_list().unwrap_or_else(|_| vec![cwd])
1029    } else {
1030        vec![cwd]
1031    };
1032
1033    let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
1034    let current_shell = if let Some(v) = current_shell {
1035        v.as_int().unwrap_or_default() as usize
1036    } else {
1037        0
1038    };
1039
1040    let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
1041    let last_shell = if let Some(v) = last_shell {
1042        v.as_int().unwrap_or_default() as usize
1043    } else {
1044        0
1045    };
1046
1047    shells[current_shell] = Value::string(path, span);
1048
1049    stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
1050    stack.add_env_var(
1051        "NUSHELL_LAST_SHELL".into(),
1052        Value::int(last_shell as i64, span),
1053    );
1054    stack.set_last_exit_code(0, span);
1055}
1056
1057///
1058/// Run a command as received from reedline. This is where we are actually
1059/// running a thing!
1060///
1061fn do_run_cmd(
1062    s: &str,
1063    stack: &mut Stack,
1064    engine_state: &mut EngineState,
1065    // we pass in the line editor so it can be dropped in the case of a process exit
1066    // (in the normal case we don't want to drop it so return it as-is otherwise)
1067    line_editor: Reedline,
1068    shell_integration_osc2: bool,
1069    entry_num: usize,
1070    use_color: bool,
1071) -> Reedline {
1072    trace!("eval source: {s}");
1073
1074    let mut cmds = s.split_whitespace();
1075
1076    let had_warning_before = engine_state.exit_warning_given.load(Ordering::SeqCst);
1077
1078    if let Some("exit") = cmds.next() {
1079        let mut working_set = StateWorkingSet::new(engine_state);
1080        let _ = parse(&mut working_set, None, s.as_bytes(), false);
1081
1082        if working_set.parse_errors.is_empty() {
1083            match cmds.next() {
1084                Some(s) => {
1085                    if let Ok(n) = s.parse::<i32>() {
1086                        return cleanup_exit(line_editor, engine_state, n);
1087                    }
1088                }
1089                None => {
1090                    return cleanup_exit(line_editor, engine_state, 0);
1091                }
1092            }
1093        }
1094    }
1095
1096    if shell_integration_osc2 {
1097        run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
1098    }
1099
1100    eval_source(
1101        engine_state,
1102        stack,
1103        s.as_bytes(),
1104        &format!("repl_entry #{entry_num}"),
1105        PipelineData::empty(),
1106        false,
1107    );
1108
1109    // if there was a warning before, and we got to this point, it means
1110    // the possible call to cleanup_exit did not occur.
1111    if had_warning_before && engine_state.is_interactive {
1112        engine_state
1113            .exit_warning_given
1114            .store(false, Ordering::SeqCst);
1115    }
1116
1117    line_editor
1118}
1119
1120///
1121/// Output some things and set environment variables so shells with the right integration
1122/// can have more information about what is going on (both on startup and after we have
1123/// run a command)
1124///
1125fn run_shell_integration_osc2(
1126    command_name: Option<&str>,
1127    engine_state: &EngineState,
1128    stack: &mut Stack,
1129    use_color: bool,
1130) {
1131    if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1132        let start_time = Instant::now();
1133
1134        // Try to abbreviate string for windows title
1135        let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
1136            let home_dir_str = p.as_path().display().to_string();
1137            if path.starts_with(&home_dir_str) {
1138                path.replacen(&home_dir_str, "~", 1)
1139            } else {
1140                path
1141            }
1142        } else {
1143            path
1144        };
1145
1146        let title = match command_name {
1147            Some(binary_name) => {
1148                let split_binary_name = binary_name.split_whitespace().next();
1149                if let Some(binary_name) = split_binary_name {
1150                    format!("{maybe_abbrev_path}> {binary_name}")
1151                } else {
1152                    maybe_abbrev_path.to_string()
1153                }
1154            }
1155            None => maybe_abbrev_path.to_string(),
1156        };
1157
1158        // Set window title too
1159        // https://tldp.org/HOWTO/Xterm-Title-3.html
1160        // ESC]0;stringBEL -- Set icon name and window title to string
1161        // ESC]1;stringBEL -- Set icon name to string
1162        // ESC]2;stringBEL -- Set window title to string
1163        run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
1164
1165        perf!("set title with command osc2", start_time, use_color);
1166    }
1167}
1168
1169fn run_shell_integration_osc7(
1170    hostname: Option<&str>,
1171    engine_state: &EngineState,
1172    stack: &mut Stack,
1173    use_color: bool,
1174) {
1175    if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1176        let start_time = Instant::now();
1177
1178        let path = if cfg!(windows) {
1179            path.replace('\\', "/")
1180        } else {
1181            path
1182        };
1183
1184        // Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir)
1185        run_ansi_sequence(&format!(
1186            "\x1b]7;file://{}{}{}\x1b\\",
1187            percent_encoding::utf8_percent_encode(
1188                hostname.unwrap_or("localhost"),
1189                percent_encoding::CONTROLS
1190            ),
1191            if path.starts_with('/') { "" } else { "/" },
1192            percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1193        ));
1194
1195        perf!(
1196            "communicate path to terminal with osc7",
1197            start_time,
1198            use_color
1199        );
1200    }
1201}
1202
1203fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
1204    if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1205        let start_time = Instant::now();
1206
1207        // Otherwise, communicate the path as OSC 9;9 from ConEmu (often used for spawning new tabs in the same dir)
1208        // This is helpful in Windows Terminal with Duplicate Tab
1209        run_ansi_sequence(&format!("\x1b]9;9;{}\x1b\\", path));
1210
1211        perf!(
1212            "communicate path to terminal with osc9;9",
1213            start_time,
1214            use_color
1215        );
1216    }
1217}
1218
1219fn run_shell_integration_osc633(
1220    engine_state: &EngineState,
1221    stack: &mut Stack,
1222    use_color: bool,
1223    repl_cmd_line_text: String,
1224) {
1225    if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1226        // Supported escape sequences of Microsoft's Visual Studio Code (vscode)
1227        // https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
1228        if stack
1229            .get_env_var(engine_state, "TERM_PROGRAM")
1230            .and_then(|v| v.as_str().ok())
1231            == Some("vscode")
1232        {
1233            let start_time = Instant::now();
1234
1235            // If we're in vscode, run their specific ansi escape sequence.
1236            // This is helpful for ctrl+g to change directories in the terminal.
1237            run_ansi_sequence(&format!(
1238                "{VSCODE_CWD_PROPERTY_MARKER_PREFIX}{path}{VSCODE_CWD_PROPERTY_MARKER_SUFFIX}"
1239            ));
1240
1241            perf!(
1242                "communicate path to terminal with osc633;P",
1243                start_time,
1244                use_color
1245            );
1246
1247            // escape a few things because this says so
1248            // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
1249            let replaced_cmd_text =
1250                escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
1251
1252            //OSC 633 ; E ; <commandline> [; <nonce] ST - Explicitly set the command line with an optional nonce.
1253            run_ansi_sequence(&format!(
1254                "{VSCODE_COMMANDLINE_MARKER_PREFIX}{replaced_cmd_text}{VSCODE_COMMANDLINE_MARKER_SUFFIX}"
1255            ));
1256        }
1257    }
1258}
1259
1260fn run_shell_integration_reset_application_mode() {
1261    run_ansi_sequence(RESET_APPLICATION_MODE);
1262}
1263
1264///
1265/// Clear the screen and output anything remaining in the EngineState buffer.
1266///
1267fn flush_engine_state_repl_buffer(
1268    engine_state: &mut EngineState,
1269    mut line_editor: Reedline,
1270) -> Reedline {
1271    let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
1272    line_editor.run_edit_commands(&[
1273        EditCommand::Clear,
1274        EditCommand::InsertString(repl.buffer.to_string()),
1275        EditCommand::MoveToPosition {
1276            position: repl.cursor_pos,
1277            select: false,
1278        },
1279    ]);
1280    if repl.accept {
1281        line_editor = line_editor.with_immediately_accept(true)
1282    }
1283    repl.accept = false;
1284    repl.buffer = "".to_string();
1285    repl.cursor_pos = 0;
1286    line_editor
1287}
1288
1289///
1290/// Setup history management for Reedline
1291///
1292fn setup_history(
1293    engine_state: &mut EngineState,
1294    line_editor: Reedline,
1295    history: HistoryConfig,
1296) -> Result<Reedline> {
1297    // Setup history_isolation aka "history per session"
1298    let history_session_id = if history.isolation {
1299        Reedline::create_history_session_id()
1300    } else {
1301        None
1302    };
1303
1304    if let Some(path) = history.file_path() {
1305        return update_line_editor_history(
1306            engine_state,
1307            path,
1308            history,
1309            line_editor,
1310            history_session_id,
1311        );
1312    };
1313    Ok(line_editor)
1314}
1315
1316///
1317/// Setup Reedline keybindingds based on the provided config
1318///
1319fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
1320    match create_keybindings(engine_state.get_config()) {
1321        Ok(keybindings) => match keybindings {
1322            KeybindingsMode::Emacs(mut keybindings) => {
1323                // Shannon: Shift+Tab to cycle shell modes
1324                keybindings.add_binding(
1325                    crossterm::event::KeyModifiers::SHIFT,
1326                    crossterm::event::KeyCode::BackTab,
1327                    reedline::ReedlineEvent::ExecuteHostCommand("__shannon_switch".into()),
1328                );
1329                let edit_mode = Box::new(Emacs::new(keybindings));
1330                line_editor.with_edit_mode(edit_mode)
1331            }
1332            KeybindingsMode::Vi {
1333                mut insert_keybindings,
1334                mut normal_keybindings,
1335            } => {
1336                // Shannon: Shift+Tab to cycle shell modes (both vi modes)
1337                for kb in [&mut insert_keybindings, &mut normal_keybindings] {
1338                    kb.add_binding(
1339                        crossterm::event::KeyModifiers::SHIFT,
1340                        crossterm::event::KeyCode::BackTab,
1341                        reedline::ReedlineEvent::ExecuteHostCommand("__shannon_switch".into()),
1342                    );
1343                }
1344                let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
1345                line_editor.with_edit_mode(edit_mode)
1346            }
1347        },
1348        Err(e) => {
1349            report_shell_error(None, engine_state, &e);
1350            line_editor
1351        }
1352    }
1353}
1354
1355///
1356/// Make sure that the terminal supports the kitty protocol if the config is asking for it
1357///
1358fn kitty_protocol_healthcheck(engine_state: &EngineState) {
1359    if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
1360        warn!("Terminal doesn't support use_kitty_protocol config");
1361    }
1362}
1363
1364fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
1365    let session_id = line_editor
1366        .get_history_session_id()
1367        .map(i64::from)
1368        .unwrap_or(0);
1369
1370    engine_state.history_session_id = session_id;
1371}
1372
1373fn update_line_editor_history(
1374    engine_state: &mut EngineState,
1375    history_path: PathBuf,
1376    history: HistoryConfig,
1377    line_editor: Reedline,
1378    history_session_id: Option<HistorySessionId>,
1379) -> Result<Reedline, ErrReport> {
1380    let ignore_space_prefixed = history.ignore_space_prefixed;
1381    let history: Box<dyn reedline::History> = match history.file_format {
1382        HistoryFileFormat::Plaintext => Box::new(
1383            FileBackedHistory::with_file(history.max_size as usize, history_path)
1384                .into_diagnostic()?,
1385        ),
1386        // this path should not happen as the config setting is captured by `nu-protocol` already
1387        #[cfg(not(feature = "sqlite"))]
1388        HistoryFileFormat::Sqlite => {
1389            return Err(miette::miette!(
1390                help = "compile Nushell with the `sqlite` feature to use this",
1391                "Unsupported history file format",
1392            ));
1393        }
1394        #[cfg(feature = "sqlite")]
1395        HistoryFileFormat::Sqlite => Box::new(
1396            SqliteBackedHistory::with_file(
1397                history_path.to_path_buf(),
1398                history_session_id,
1399                Some(chrono::Utc::now()),
1400            )
1401            .into_diagnostic()?,
1402        ),
1403    };
1404    let line_editor = line_editor
1405        .with_history_session_id(history_session_id)
1406        .with_history_exclusion_prefix(ignore_space_prefixed.then_some(" ".into()))
1407        .with_history(history);
1408
1409    store_history_id_in_engine(engine_state, &line_editor);
1410
1411    Ok(line_editor)
1412}
1413
1414fn confirm_stdin_is_terminal() -> Result<()> {
1415    // Guard against invocation without a connected terminal.
1416    // reedline / crossterm event polling will fail without a connected tty
1417    if !std::io::stdin().is_terminal() {
1418        return Err(std::io::Error::new(
1419            std::io::ErrorKind::NotFound,
1420            "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
1421        ))
1422        .into_diagnostic();
1423    }
1424    Ok(())
1425}
1426fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
1427    match shape {
1428        NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
1429        NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
1430        NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
1431        NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
1432        NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
1433        NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
1434        NuCursorShape::Inherit => None,
1435    }
1436}
1437
1438fn get_command_finished_marker(
1439    stack: &Stack,
1440    engine_state: &EngineState,
1441    shell_integration_osc633: bool,
1442    shell_integration_osc133: bool,
1443) -> String {
1444    let exit_code = stack
1445        .get_env_var(engine_state, "LAST_EXIT_CODE")
1446        .and_then(|e| e.as_int().ok());
1447
1448    if shell_integration_osc633 {
1449        if stack
1450            .get_env_var(engine_state, "TERM_PROGRAM")
1451            .and_then(|v| v.as_str().ok())
1452            == Some("vscode")
1453        {
1454            // We're in vscode and we have osc633 enabled
1455            format!(
1456                "{}{}{}",
1457                VSCODE_POST_EXECUTION_MARKER_PREFIX,
1458                exit_code.unwrap_or(0),
1459                VSCODE_POST_EXECUTION_MARKER_SUFFIX
1460            )
1461        } else if shell_integration_osc133 {
1462            // If we're in VSCode but we don't find the env var, just return the regular markers
1463            format!(
1464                "{}{}{}",
1465                POST_EXECUTION_MARKER_PREFIX,
1466                exit_code.unwrap_or(0),
1467                POST_EXECUTION_MARKER_SUFFIX
1468            )
1469        } else {
1470            // We're not in vscode, so we don't need to do anything special
1471            "\x1b[0m".to_string()
1472        }
1473    } else if shell_integration_osc133 {
1474        format!(
1475            "{}{}{}",
1476            POST_EXECUTION_MARKER_PREFIX,
1477            exit_code.unwrap_or(0),
1478            POST_EXECUTION_MARKER_SUFFIX
1479        )
1480    } else {
1481        "\x1b[0m".to_string()
1482    }
1483}
1484
1485fn run_ansi_sequence(seq: &str) {
1486    if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
1487        warn!("Error writing ansi sequence {e}");
1488    } else if let Err(e) = io::stdout().flush() {
1489        warn!("Error flushing stdio {e}");
1490    }
1491}
1492
1493fn run_finaliziation_ansi_sequence(
1494    stack: &Stack,
1495    engine_state: &EngineState,
1496    use_color: bool,
1497    shell_integration_osc633: bool,
1498    shell_integration_osc133: bool,
1499) {
1500    if shell_integration_osc633 {
1501        // Only run osc633 if we are in vscode
1502        if stack
1503            .get_env_var(engine_state, "TERM_PROGRAM")
1504            .and_then(|v| v.as_str().ok())
1505            == Some("vscode")
1506        {
1507            let start_time = Instant::now();
1508
1509            run_ansi_sequence(&get_command_finished_marker(
1510                stack,
1511                engine_state,
1512                shell_integration_osc633,
1513                shell_integration_osc133,
1514            ));
1515
1516            perf!(
1517                "post_execute_marker (633;D) ansi escape sequences",
1518                start_time,
1519                use_color
1520            );
1521        } else if shell_integration_osc133 {
1522            let start_time = Instant::now();
1523
1524            run_ansi_sequence(&get_command_finished_marker(
1525                stack,
1526                engine_state,
1527                shell_integration_osc633,
1528                shell_integration_osc133,
1529            ));
1530
1531            perf!(
1532                "post_execute_marker (133;D) ansi escape sequences",
1533                start_time,
1534                use_color
1535            );
1536        }
1537    } else if shell_integration_osc133 {
1538        let start_time = Instant::now();
1539
1540        run_ansi_sequence(&get_command_finished_marker(
1541            stack,
1542            engine_state,
1543            shell_integration_osc633,
1544            shell_integration_osc133,
1545        ));
1546
1547        perf!(
1548            "post_execute_marker (133;D) ansi escape sequences",
1549            start_time,
1550            use_color
1551        );
1552    }
1553}
1554
1555// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
1556#[cfg(windows)]
1557static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
1558    fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
1559});
1560
1561// A best-effort "does this string look kinda like a path?" function to determine whether to auto-cd
1562fn looks_like_path(orig: &str) -> bool {
1563    #[cfg(windows)]
1564    {
1565        if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
1566            return true;
1567        }
1568    }
1569
1570    orig.starts_with('.')
1571        || orig.starts_with('~')
1572        || orig.starts_with('/')
1573        || orig.starts_with('\\')
1574        || orig.ends_with(std::path::MAIN_SEPARATOR)
1575}
1576
1577#[cfg(test)]
1578mod semantic_marker_tests {
1579    use super::semantic_markers_from_config;
1580    use nu_protocol::Config;
1581    use reedline::PromptKind;
1582
1583    #[test]
1584    fn semantic_markers_use_osc633_in_vscode() {
1585        let mut config = Config::default();
1586        config.shell_integration.osc633 = true;
1587        config.shell_integration.osc133 = true;
1588
1589        let markers =
1590            semantic_markers_from_config(&config, true).expect("expected semantic markers");
1591
1592        assert_eq!(
1593            markers.prompt_start(PromptKind::Primary).as_ref(),
1594            "\x1b]633;A;k=i\x1b\\"
1595        );
1596    }
1597
1598    #[test]
1599    fn semantic_markers_use_osc133_click_events() {
1600        let mut config = Config::default();
1601        config.shell_integration.osc133 = true;
1602
1603        let markers =
1604            semantic_markers_from_config(&config, false).expect("expected semantic markers");
1605
1606        assert_eq!(
1607            markers.prompt_start(PromptKind::Primary).as_ref(),
1608            "\x1b]133;A;k=i;click_events=1\x1b\\"
1609        );
1610    }
1611
1612    #[test]
1613    fn semantic_markers_none_when_disabled() {
1614        let mut config = Config::default();
1615        config.shell_integration.osc133 = false;
1616        config.shell_integration.osc633 = false;
1617        assert!(semantic_markers_from_config(&config, false).is_none());
1618    }
1619}
1620
1621#[cfg(windows)]
1622#[test]
1623fn looks_like_path_windows_drive_path_works() {
1624    assert!(looks_like_path("C:"));
1625    assert!(looks_like_path("D:\\"));
1626    assert!(looks_like_path("E:/"));
1627    assert!(looks_like_path("F:\\some_dir"));
1628    assert!(looks_like_path("G:/some_dir"));
1629}
1630
1631#[cfg(windows)]
1632#[test]
1633fn trailing_slash_looks_like_path() {
1634    assert!(looks_like_path("foo\\"))
1635}
1636
1637#[cfg(not(windows))]
1638#[test]
1639fn trailing_slash_looks_like_path() {
1640    assert!(looks_like_path("foo/"))
1641}
1642
1643#[test]
1644fn are_session_ids_in_sync() {
1645    let engine_state = &mut EngineState::new();
1646    let history = engine_state.history_config().unwrap();
1647    let history_path = history.file_path().unwrap();
1648    let line_editor = reedline::Reedline::create();
1649    let history_session_id = reedline::Reedline::create_history_session_id();
1650    let line_editor = update_line_editor_history(
1651        engine_state,
1652        history_path,
1653        history,
1654        line_editor,
1655        history_session_id,
1656    );
1657    assert_eq!(
1658        i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
1659        engine_state.history_session_id
1660    );
1661}
1662
1663#[cfg(test)]
1664mod test_auto_cd {
1665    use super::{ReplOperation, do_auto_cd, escape_special_vscode_bytes, parse_operation};
1666    use nu_path::AbsolutePath;
1667    use nu_protocol::engine::{EngineState, Stack};
1668    use tempfile::tempdir;
1669
1670    /// Create a symlink. Works on both Unix and Windows.
1671    #[cfg(any(unix, windows))]
1672    fn symlink(
1673        original: impl AsRef<AbsolutePath>,
1674        link: impl AsRef<AbsolutePath>,
1675    ) -> std::io::Result<()> {
1676        let original = original.as_ref();
1677        let link = link.as_ref();
1678
1679        #[cfg(unix)]
1680        {
1681            std::os::unix::fs::symlink(original, link)
1682        }
1683        #[cfg(windows)]
1684        {
1685            if original.is_dir() {
1686                std::os::windows::fs::symlink_dir(original, link)
1687            } else {
1688                std::os::windows::fs::symlink_file(original, link)
1689            }
1690        }
1691    }
1692
1693    /// Run one test case on the auto-cd feature. PWD is initially set to
1694    /// `before`, and after `input` is parsed and evaluated, PWD should be
1695    /// changed to `after`.
1696    #[track_caller]
1697    fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
1698        // Setup EngineState and Stack.
1699        let mut engine_state = EngineState::new();
1700        let mut stack = Stack::new();
1701        stack.set_cwd(before.as_ref()).unwrap();
1702
1703        // Parse the input. It must be an auto-cd operation.
1704        let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
1705        let ReplOperation::AutoCd { cwd, target, span } = op else {
1706            panic!("'{input}' was not parsed into an auto-cd operation")
1707        };
1708
1709        // Perform the auto-cd operation.
1710        do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
1711        let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
1712
1713        // Check that `updated_cwd` and `after` point to the same place. They
1714        // don't have to be byte-wise equal (on Windows, the 8.3 filename
1715        // conversion messes things up),
1716        let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
1717        let after = std::fs::canonicalize(after.as_ref()).unwrap();
1718        assert_eq!(updated_cwd, after);
1719    }
1720
1721    #[test]
1722    fn auto_cd_root() {
1723        let tempdir = tempdir().unwrap();
1724        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1725
1726        let input = if cfg!(windows) { r"C:\" } else { "/" };
1727        let root = AbsolutePath::try_new(input).unwrap();
1728        check(tempdir, input, root);
1729    }
1730
1731    #[test]
1732    fn auto_cd_tilde() {
1733        let tempdir = tempdir().unwrap();
1734        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1735
1736        let home = nu_path::home_dir().unwrap();
1737        check(tempdir, "~", home);
1738    }
1739
1740    #[test]
1741    fn auto_cd_dot() {
1742        let tempdir = tempdir().unwrap();
1743        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1744
1745        check(tempdir, ".", tempdir);
1746    }
1747
1748    #[test]
1749    fn auto_cd_double_dot() {
1750        let tempdir = tempdir().unwrap();
1751        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1752
1753        let dir = tempdir.join("foo");
1754        std::fs::create_dir_all(&dir).unwrap();
1755        check(dir, "..", tempdir);
1756    }
1757
1758    #[test]
1759    fn auto_cd_triple_dot() {
1760        let tempdir = tempdir().unwrap();
1761        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1762
1763        let dir = tempdir.join("foo").join("bar");
1764        std::fs::create_dir_all(&dir).unwrap();
1765        check(dir, "...", tempdir);
1766    }
1767
1768    #[test]
1769    fn auto_cd_relative() {
1770        let tempdir = tempdir().unwrap();
1771        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1772
1773        let foo = tempdir.join("foo");
1774        let bar = tempdir.join("bar");
1775        std::fs::create_dir_all(&foo).unwrap();
1776        std::fs::create_dir_all(&bar).unwrap();
1777        let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
1778        check(foo, input, bar);
1779    }
1780
1781    #[test]
1782    fn auto_cd_trailing_slash() {
1783        let tempdir = tempdir().unwrap();
1784        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1785
1786        let dir = tempdir.join("foo");
1787        std::fs::create_dir_all(&dir).unwrap();
1788        let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1789        check(tempdir, input, dir);
1790    }
1791
1792    #[test]
1793    fn auto_cd_symlink() {
1794        let tempdir = tempdir().unwrap();
1795        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1796
1797        let dir = tempdir.join("foo");
1798        std::fs::create_dir_all(&dir).unwrap();
1799        let link = tempdir.join("link");
1800        symlink(&dir, &link).unwrap();
1801        let input = if cfg!(windows) { r".\link" } else { "./link" };
1802        check(tempdir, input, link);
1803
1804        let dir = tempdir.join("foo").join("bar");
1805        std::fs::create_dir_all(&dir).unwrap();
1806        let link = tempdir.join("link2");
1807        symlink(&dir, &link).unwrap();
1808        let input = "..";
1809        check(link, input, tempdir);
1810    }
1811
1812    #[test]
1813    #[should_panic(expected = "was not parsed into an auto-cd operation")]
1814    fn auto_cd_nonexistent_directory() {
1815        let tempdir = tempdir().unwrap();
1816        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1817
1818        let dir = tempdir.join("foo");
1819        let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1820        check(tempdir, input, dir);
1821    }
1822
1823    #[test]
1824    fn escape_vscode_semicolon_test() {
1825        let input = r#"now;is"#;
1826        let expected = r#"now\x3Bis"#;
1827        let actual = escape_special_vscode_bytes(input).unwrap();
1828        assert_eq!(expected, actual);
1829    }
1830
1831    #[test]
1832    fn escape_vscode_backslash_test() {
1833        let input = r#"now\is"#;
1834        let expected = r#"now\\is"#;
1835        let actual = escape_special_vscode_bytes(input).unwrap();
1836        assert_eq!(expected, actual);
1837    }
1838
1839    #[test]
1840    fn escape_vscode_linefeed_test() {
1841        let input = "now\nis";
1842        let expected = r#"now\x0Ais"#;
1843        let actual = escape_special_vscode_bytes(input).unwrap();
1844        assert_eq!(expected, actual);
1845    }
1846
1847    #[test]
1848    fn escape_vscode_tab_null_cr_test() {
1849        let input = "now\t\0\ris";
1850        let expected = r#"now\x09\x00\x0Dis"#;
1851        let actual = escape_special_vscode_bytes(input).unwrap();
1852        assert_eq!(expected, actual);
1853    }
1854
1855    #[test]
1856    fn escape_vscode_multibyte_ok() {
1857        let input = "now🍪is";
1858        let actual = escape_special_vscode_bytes(input).unwrap();
1859        assert_eq!(input, actual);
1860    }
1861}