Skip to main content

shannon_nu_cli/
repl.rs

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