Skip to main content

nu_cli/
prompt_update.rs

1use crate::NushellPrompt;
2use log::{trace, warn};
3use nu_engine::ClosureEvalOnce;
4use nu_protocol::{
5    Config, PipelineData, Value,
6    engine::{EngineState, Stack},
7    report_shell_error,
8};
9use reedline::Prompt;
10
11// Name of environment variable where the prompt could be stored
12pub(crate) const PROMPT_COMMAND: &str = "PROMPT_COMMAND";
13pub(crate) const PROMPT_COMMAND_RIGHT: &str = "PROMPT_COMMAND_RIGHT";
14pub(crate) const PROMPT_INDICATOR: &str = "PROMPT_INDICATOR";
15pub(crate) const PROMPT_INDICATOR_VI_INSERT: &str = "PROMPT_INDICATOR_VI_INSERT";
16pub(crate) const PROMPT_INDICATOR_VI_NORMAL: &str = "PROMPT_INDICATOR_VI_NORMAL";
17pub(crate) const PROMPT_MULTILINE_INDICATOR: &str = "PROMPT_MULTILINE_INDICATOR";
18pub(crate) const TRANSIENT_PROMPT_COMMAND: &str = "TRANSIENT_PROMPT_COMMAND";
19pub(crate) const TRANSIENT_PROMPT_COMMAND_RIGHT: &str = "TRANSIENT_PROMPT_COMMAND_RIGHT";
20pub(crate) const TRANSIENT_PROMPT_INDICATOR: &str = "TRANSIENT_PROMPT_INDICATOR";
21pub(crate) const TRANSIENT_PROMPT_INDICATOR_VI_INSERT: &str =
22    "TRANSIENT_PROMPT_INDICATOR_VI_INSERT";
23pub(crate) const TRANSIENT_PROMPT_INDICATOR_VI_NORMAL: &str =
24    "TRANSIENT_PROMPT_INDICATOR_VI_NORMAL";
25pub(crate) const TRANSIENT_PROMPT_MULTILINE_INDICATOR: &str =
26    "TRANSIENT_PROMPT_MULTILINE_INDICATOR";
27
28// ────────────────────────────────────────────────────────────────────────────────
29// OSC 133 / OSC 633 COMMAND EXECUTION MARKERS
30// ────────────────────────────────────────────────────────────────────────────────
31// These escape sequences are used by the shell to mark command execution boundaries.
32// Note: A/B/P markers for prompts are now handled by reedline.
33
34// Command execution markers (C = pre-exec, D = post-exec with exit code)
35pub(crate) const PRE_EXECUTION_MARKER: &str = "\x1b]133;C\x1b\\";
36pub(crate) const POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]133;D;";
37pub(crate) const POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
38
39// VS Code specific markers (OSC 633)
40pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
41pub(crate) const VSCODE_POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]633;D;";
42pub(crate) const VSCODE_POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
43pub(crate) const VSCODE_COMMANDLINE_MARKER_PREFIX: &str = "\x1b]633;E;";
44pub(crate) const VSCODE_COMMANDLINE_MARKER_SUFFIX: &str = "\x1b\\";
45pub(crate) const VSCODE_CWD_PROPERTY_MARKER_PREFIX: &str = "\x1b]633;P;Cwd=";
46pub(crate) const VSCODE_CWD_PROPERTY_MARKER_SUFFIX: &str = "\x1b\\";
47
48// Reset terminal application mode sequence
49pub(crate) const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
50
51fn get_prompt_string(
52    prompt: &str,
53    config: &Config,
54    engine_state: &EngineState,
55    stack: &mut Stack,
56) -> Option<String> {
57    stack
58        .get_env_var(engine_state, prompt)
59        .and_then(|v| match v {
60            Value::Closure { val, .. } => {
61                let result = ClosureEvalOnce::new(engine_state, stack, val.as_ref().clone())
62                    .run_with_input(PipelineData::empty());
63
64                trace!(
65                    "get_prompt_string (block) {}:{}:{}",
66                    file!(),
67                    line!(),
68                    column!()
69                );
70
71                result
72                    .map_err(|err| {
73                        report_shell_error(None, engine_state, &err);
74                    })
75                    .ok()
76            }
77            Value::String { .. } => Some(PipelineData::value(v.clone(), None)),
78            _ => None,
79        })
80        .and_then(|pipeline_data| {
81            let output = pipeline_data.collect_string("", config).ok();
82            let ansi_output = output.map(|mut x| {
83                // Always reset the color at the start of the right prompt
84                // to ensure there is no ansi bleed over
85                if x.is_empty() && prompt == PROMPT_COMMAND_RIGHT {
86                    x.insert_str(0, "\x1b[0m")
87                };
88
89                x
90            });
91            // Let's keep this for debugging purposes with nu --log-level warn
92            warn!("{}:{}:{} {:?}", file!(), line!(), column!(), ansi_output);
93
94            ansi_output
95        })
96}
97
98pub fn update_prompt(
99    config: &Config,
100    engine_state: &EngineState,
101    stack: &mut Stack,
102    nu_prompt: &mut NushellPrompt,
103) {
104    // Get the configured prompts - reedline now handles semantic markers
105    let left_prompt_string = get_prompt_string(PROMPT_COMMAND, config, engine_state, stack);
106
107    let right_prompt_string = get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, stack);
108
109    let prompt_indicator_string = get_prompt_string(PROMPT_INDICATOR, config, engine_state, stack);
110
111    let prompt_multiline_string =
112        get_prompt_string(PROMPT_MULTILINE_INDICATOR, config, engine_state, stack);
113
114    let prompt_vi_insert_string =
115        get_prompt_string(PROMPT_INDICATOR_VI_INSERT, config, engine_state, stack);
116
117    let prompt_vi_normal_string =
118        get_prompt_string(PROMPT_INDICATOR_VI_NORMAL, config, engine_state, stack);
119
120    // apply the other indicators
121    nu_prompt.update_all_prompt_strings(
122        left_prompt_string,
123        right_prompt_string,
124        prompt_indicator_string,
125        prompt_multiline_string,
126        (prompt_vi_insert_string, prompt_vi_normal_string),
127        config.render_right_prompt_on_last_line,
128    );
129    trace!("update_prompt {}:{}:{}", file!(), line!(), column!());
130}
131
132/// Construct the transient prompt based on the normal nu_prompt
133/// Note: Transient prompts do NOT emit semantic markers since they replace
134/// the actual prompt after command execution (which already has markers).
135pub(crate) fn make_transient_prompt(
136    config: &Config,
137    engine_state: &EngineState,
138    stack: &mut Stack,
139    nu_prompt: &NushellPrompt,
140) -> Box<dyn Prompt> {
141    let mut nu_prompt = nu_prompt.clone();
142
143    if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_COMMAND, config, engine_state, stack) {
144        nu_prompt.update_prompt_left(Some(s))
145    }
146
147    if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_COMMAND_RIGHT, config, engine_state, stack)
148    {
149        nu_prompt.update_prompt_right(Some(s), config.render_right_prompt_on_last_line)
150    }
151
152    if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_INDICATOR, config, engine_state, stack) {
153        nu_prompt.update_prompt_indicator(Some(s))
154    }
155    if let Some(s) = get_prompt_string(
156        TRANSIENT_PROMPT_INDICATOR_VI_INSERT,
157        config,
158        engine_state,
159        stack,
160    ) {
161        nu_prompt.update_prompt_vi_insert(Some(s))
162    }
163    if let Some(s) = get_prompt_string(
164        TRANSIENT_PROMPT_INDICATOR_VI_NORMAL,
165        config,
166        engine_state,
167        stack,
168    ) {
169        nu_prompt.update_prompt_vi_normal(Some(s))
170    }
171
172    if let Some(s) = get_prompt_string(
173        TRANSIENT_PROMPT_MULTILINE_INDICATOR,
174        config,
175        engine_state,
176        stack,
177    ) {
178        nu_prompt.update_prompt_multiline(Some(s))
179    }
180
181    Box::new(nu_prompt)
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use nu_protocol::Span;
188
189    #[test]
190    fn update_prompt_does_not_embed_osc_markers() {
191        let mut config = Config::default();
192        config.shell_integration.osc133 = true;
193
194        let engine_state = EngineState::new();
195        let mut stack = Stack::new();
196        stack.add_env_var(
197            PROMPT_COMMAND.into(),
198            Value::string("test", Span::unknown()),
199        );
200
201        let mut nu_prompt = NushellPrompt::new();
202
203        update_prompt(&config, &engine_state, &mut stack, &mut nu_prompt);
204
205        assert_eq!(nu_prompt.render_prompt_left(), "test");
206    }
207}