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