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