Skip to main content

shannon_nu_cli/
repl.rs

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