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    let mut output = match stack.get_env_var(engine_state, prompt)? {
58        Value::String { val, .. } => val.clone(),
59        Value::Closure { val, .. } => {
60            let result = ClosureEvalOnce::new(engine_state, stack, val.as_ref().clone())
61                .run_with_input(PipelineData::empty());
62
63            trace!(
64                "get_prompt_string (block) {}:{}:{}",
65                file!(),
66                line!(),
67                column!()
68            );
69
70            let result_string = result
71                .map_err(|err| report_shell_error(None, engine_state, &err))
72                .ok()
73                .and_then(|pd| pd.collect_string("", config).ok());
74
75            result_string?
76        }
77        _ => return None,
78    };
79
80    // Always reset the color at the start of the right prompt
81    // to ensure there is no ansi bleed over
82    if output.is_empty() && prompt == PROMPT_COMMAND_RIGHT {
83        output.insert_str(0, "\x1b[0m")
84    };
85
86    // Let's keep this for debugging purposes with nu --log-level warn
87    warn!("{}:{}:{} {:?}", file!(), line!(), column!(), output);
88
89    Some(output)
90}
91
92pub fn update_prompt(
93    config: &Config,
94    engine_state: &EngineState,
95    stack: &mut Stack,
96    nu_prompt: &mut NushellPrompt,
97) {
98    // Get the configured prompts - reedline now handles semantic markers
99    let left_prompt_string = get_prompt_string(PROMPT_COMMAND, config, engine_state, stack);
100
101    let right_prompt_string = get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, stack);
102
103    let prompt_indicator_string = get_prompt_string(PROMPT_INDICATOR, config, engine_state, stack);
104
105    let prompt_multiline_string =
106        get_prompt_string(PROMPT_MULTILINE_INDICATOR, config, engine_state, stack);
107
108    let prompt_vi_insert_string =
109        get_prompt_string(PROMPT_INDICATOR_VI_INSERT, config, engine_state, stack);
110
111    let prompt_vi_normal_string =
112        get_prompt_string(PROMPT_INDICATOR_VI_NORMAL, config, engine_state, stack);
113
114    // apply the other indicators
115    nu_prompt.update_all_prompt_strings(
116        left_prompt_string,
117        right_prompt_string,
118        prompt_indicator_string,
119        prompt_multiline_string,
120        (prompt_vi_insert_string, prompt_vi_normal_string),
121        config.render_right_prompt_on_last_line,
122    );
123    trace!("update_prompt {}:{}:{}", file!(), line!(), column!());
124}
125
126/// Construct the transient prompt based on the normal nu_prompt
127/// Note: Transient prompts do NOT emit semantic markers since they replace
128/// the actual prompt after command execution (which already has markers).
129pub(crate) fn make_transient_prompt(
130    config: &Config,
131    engine_state: &EngineState,
132    stack: &mut Stack,
133    nu_prompt: &NushellPrompt,
134) -> Box<dyn Prompt> {
135    let mut nu_prompt = nu_prompt.clone();
136
137    if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_COMMAND, config, engine_state, stack) {
138        nu_prompt.update_prompt_left(Some(s))
139    }
140
141    if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_COMMAND_RIGHT, config, engine_state, stack)
142    {
143        nu_prompt.update_prompt_right(Some(s), config.render_right_prompt_on_last_line)
144    }
145
146    if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_INDICATOR, config, engine_state, stack) {
147        nu_prompt.update_prompt_indicator(Some(s))
148    }
149    if let Some(s) = get_prompt_string(
150        TRANSIENT_PROMPT_INDICATOR_VI_INSERT,
151        config,
152        engine_state,
153        stack,
154    ) {
155        nu_prompt.update_prompt_vi_insert(Some(s))
156    }
157    if let Some(s) = get_prompt_string(
158        TRANSIENT_PROMPT_INDICATOR_VI_NORMAL,
159        config,
160        engine_state,
161        stack,
162    ) {
163        nu_prompt.update_prompt_vi_normal(Some(s))
164    }
165
166    if let Some(s) = get_prompt_string(
167        TRANSIENT_PROMPT_MULTILINE_INDICATOR,
168        config,
169        engine_state,
170        stack,
171    ) {
172        nu_prompt.update_prompt_multiline(Some(s))
173    }
174
175    Box::new(nu_prompt)
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use nu_protocol::Span;
182
183    #[test]
184    fn update_prompt_does_not_embed_osc_markers() {
185        let mut config = Config::default();
186        config.shell_integration.osc133 = true;
187
188        let engine_state = EngineState::new();
189        let mut stack = Stack::new();
190        stack.add_env_var(
191            PROMPT_COMMAND.into(),
192            Value::string("test", Span::test_data()),
193        );
194
195        let mut nu_prompt = NushellPrompt::new();
196
197        update_prompt(&config, &engine_state, &mut stack, &mut nu_prompt);
198
199        assert_eq!(nu_prompt.render_prompt_left(), "test");
200    }
201}