Skip to main content

nu_cli/
prompt_update.rs

1use crate::NushellPrompt;
2use log::{info, trace};
3use nu_engine::ClosureEvalOnce;
4use nu_protocol::{
5    Config, PipelineData, Value,
6    engine::{EngineState, PromptContents, Stack},
7    report_shell_error,
8};
9use reedline::Prompt;
10use std::sync::Arc;
11
12// Name of environment variable where the prompt could be stored
13pub(crate) const PROMPT_COMMAND: &str = "PROMPT_COMMAND";
14pub(crate) const PROMPT_COMMAND_RIGHT: &str = "PROMPT_COMMAND_RIGHT";
15pub(crate) const PROMPT_INDICATOR: &str = "PROMPT_INDICATOR";
16pub(crate) const PROMPT_INDICATOR_VI_INSERT: &str = "PROMPT_INDICATOR_VI_INSERT";
17pub(crate) const PROMPT_INDICATOR_VI_NORMAL: &str = "PROMPT_INDICATOR_VI_NORMAL";
18pub(crate) const PROMPT_MULTILINE_INDICATOR: &str = "PROMPT_MULTILINE_INDICATOR";
19pub(crate) const TRANSIENT_PROMPT_COMMAND: &str = "TRANSIENT_PROMPT_COMMAND";
20pub(crate) const TRANSIENT_PROMPT_COMMAND_RIGHT: &str = "TRANSIENT_PROMPT_COMMAND_RIGHT";
21pub(crate) const TRANSIENT_PROMPT_INDICATOR: &str = "TRANSIENT_PROMPT_INDICATOR";
22pub(crate) const TRANSIENT_PROMPT_INDICATOR_VI_INSERT: &str =
23    "TRANSIENT_PROMPT_INDICATOR_VI_INSERT";
24pub(crate) const TRANSIENT_PROMPT_INDICATOR_VI_NORMAL: &str =
25    "TRANSIENT_PROMPT_INDICATOR_VI_NORMAL";
26pub(crate) const TRANSIENT_PROMPT_MULTILINE_INDICATOR: &str =
27    "TRANSIENT_PROMPT_MULTILINE_INDICATOR";
28
29// ────────────────────────────────────────────────────────────────────────────────
30// OSC 133 / OSC 633 COMMAND EXECUTION MARKERS
31// ────────────────────────────────────────────────────────────────────────────────
32// These escape sequences are used by the shell to mark command execution boundaries.
33// Note: A/B/P markers for prompts are now handled by reedline.
34
35// Command execution markers (C = pre-exec, D = post-exec with exit code)
36pub(crate) const PRE_EXECUTION_MARKER: &str = "\x1b]133;C\x1b\\";
37pub(crate) const POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]133;D;";
38pub(crate) const POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
39
40// VS Code specific markers (OSC 633)
41pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
42pub(crate) const VSCODE_POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]633;D;";
43pub(crate) const VSCODE_POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
44pub(crate) const VSCODE_COMMANDLINE_MARKER_PREFIX: &str = "\x1b]633;E;";
45pub(crate) const VSCODE_COMMANDLINE_MARKER_SUFFIX: &str = "\x1b\\";
46pub(crate) const VSCODE_CWD_PROPERTY_MARKER_PREFIX: &str = "\x1b]633;P;Cwd=";
47pub(crate) const VSCODE_CWD_PROPERTY_MARKER_SUFFIX: &str = "\x1b\\";
48
49// Reset terminal application mode sequence
50pub(crate) const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
51
52fn get_prompt_string(
53    prompt: &str,
54    config: &Config,
55    engine_state: &EngineState,
56    stack: &mut Stack,
57) -> Option<String> {
58    let mut output = match stack.get_env_var(engine_state, prompt)? {
59        Value::String { val, .. } => val.clone(),
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            let result_string = result
72                .map_err(|err| report_shell_error(None, engine_state, &err))
73                .ok()
74                .and_then(|pd| pd.collect_string("", config).ok());
75
76            result_string?
77        }
78        _ => return None,
79    };
80
81    // Always reset the color at the start of the right prompt
82    // to ensure there is no ansi bleed over
83    if output.is_empty() && prompt == PROMPT_COMMAND_RIGHT {
84        output.insert_str(0, "\x1b[0m")
85    };
86
87    // Let's keep this for debugging purposes with nu --log-level warn
88    info!("{}:{}:{} {:?}", file!(), line!(), column!(), output);
89
90    Some(output)
91}
92
93/// Re-evaluate `$env.PROMPT_COMMAND` and friends and install the result as the
94/// prompt's per-cycle baseline. This overwrites anything a background job pushed
95/// during the previous cycle, resetting the prompt for the next line.
96pub fn update_prompt(config: &Config, engine_state: &EngineState, stack: &mut Stack) {
97    let new_contents = build_prompt_contents(config, engine_state, stack);
98
99    // reedline handles semantic markers itself.
100    engine_state.prompt_state.set_contents(new_contents);
101
102    trace!("update_prompt {}:{}:{}", file!(), line!(), column!());
103}
104
105fn build_prompt_contents(
106    config: &Config,
107    engine_state: &EngineState,
108    stack: &mut Stack,
109) -> PromptContents {
110    let mut fetch_prompt =
111        |prompt_type| get_prompt_string(prompt_type, config, engine_state, stack).map(Arc::from);
112
113    PromptContents {
114        left: fetch_prompt(PROMPT_COMMAND),
115        right: fetch_prompt(PROMPT_COMMAND_RIGHT),
116        indicator: fetch_prompt(PROMPT_INDICATOR),
117        vi_insert: fetch_prompt(PROMPT_INDICATOR_VI_INSERT),
118        vi_normal: fetch_prompt(PROMPT_INDICATOR_VI_NORMAL),
119        multiline: fetch_prompt(PROMPT_MULTILINE_INDICATOR),
120        render_right_on_last_line: config.render_right_prompt_on_last_line,
121    }
122}
123
124/// Construct the transient prompt based on the normal nu_prompt.
125/// Note: Transient prompts do NOT emit semantic markers since they replace
126/// the actual prompt after command execution (which already has markers).
127///
128/// The transient prompt is drawn only once the line is submitted, which can
129/// be long after this function runs. Rather than freezing a snapshot of the
130/// baseline now, we resolve only the `TRANSIENT_PROMPT_*` overrides here and
131/// read the baseline live at render time, so a background job's late
132/// `commandline set-prompt` pushes still show up.
133pub(crate) fn make_transient_prompt(
134    config: &Config,
135    engine_state: &EngineState,
136    stack: &mut Stack,
137) -> Box<dyn Prompt> {
138    let mut fetch_transient =
139        |env_var| get_prompt_string(env_var, config, engine_state, stack).map(Arc::from);
140
141    let overrides = PromptContents {
142        left: fetch_transient(TRANSIENT_PROMPT_COMMAND),
143        right: fetch_transient(TRANSIENT_PROMPT_COMMAND_RIGHT),
144        indicator: fetch_transient(TRANSIENT_PROMPT_INDICATOR),
145        vi_insert: fetch_transient(TRANSIENT_PROMPT_INDICATOR_VI_INSERT),
146        vi_normal: fetch_transient(TRANSIENT_PROMPT_INDICATOR_VI_NORMAL),
147        multiline: fetch_transient(TRANSIENT_PROMPT_MULTILINE_INDICATOR),
148        // Not overridable by a `TRANSIENT_PROMPT_*` var; falls back to the
149        // live baseline via `PromptContents::overridden_by`.
150        render_right_on_last_line: false,
151    };
152
153    Box::new(NushellPrompt::transient(
154        engine_state.prompt_state.clone(),
155        overrides,
156    ))
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use nu_protocol::Span;
163
164    #[test]
165    fn update_prompt_does_not_embed_osc_markers() {
166        let mut config = Config::default();
167        config.shell_integration.osc133 = true;
168
169        let engine_state = EngineState::new();
170        let mut stack = Stack::new();
171        stack.add_env_var(
172            PROMPT_COMMAND.into(),
173            Value::string("test", Span::test_data()),
174        );
175
176        update_prompt(&config, &engine_state, &mut stack);
177
178        let nu_prompt = NushellPrompt::shared(engine_state.prompt_state.clone());
179        assert_eq!(nu_prompt.render_prompt_left(), "test");
180    }
181
182    #[test]
183    fn transient_prompt_override_still_wins_over_the_live_baseline() {
184        use nu_protocol::engine::PromptSegment;
185
186        let config = Config::default();
187        let engine_state = EngineState::new();
188        let mut stack = Stack::new();
189        stack.add_env_var(
190            TRANSIENT_PROMPT_INDICATOR.into(),
191            Value::string("transient> ", Span::test_data()),
192        );
193
194        let transient_prompt = make_transient_prompt(&config, &engine_state, &mut stack);
195
196        // The configured TRANSIENT_PROMPT_INDICATOR beats a later live change.
197        engine_state
198            .prompt_state
199            .set(PromptSegment::Indicator, "live> ");
200
201        assert_eq!(
202            transient_prompt.render_prompt_indicator(reedline::PromptEditMode::Emacs),
203            "transient> "
204        );
205    }
206}