Skip to main content

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