Skip to main content

nu_cli/
prompt.rs

1use nu_protocol::engine::{PromptContents, PromptState};
2#[cfg(windows)]
3use nu_utils::enable_vt_processing;
4#[cfg(feature = "helix")]
5use reedline::PromptHelixMode;
6use reedline::{
7    DefaultPrompt, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus,
8    PromptViMode,
9};
10use std::{borrow::Cow, sync::Arc};
11
12/// The reedline-facing view over some [`PromptContents`].
13pub struct NushellPrompt {
14    source: PromptSource,
15}
16
17/// Where a [`NushellPrompt`] reads its contents from.
18enum PromptSource {
19    /// The live, interactive prompt, shared with every background job.
20    Shared(Arc<PromptState>),
21
22    /// The transient prompt: live baseline with `TRANSIENT_PROMPT_*` overrides
23    /// layered on at render time, so late async pushes still show up.
24    Transient {
25        state: Arc<PromptState>,
26        overrides: PromptContents,
27    },
28}
29
30impl NushellPrompt {
31    /// A live prompt backed by the engine's shared [`PromptState`].
32    pub fn shared(state: Arc<PromptState>) -> Self {
33        Self {
34            source: PromptSource::Shared(state),
35        }
36    }
37
38    /// The transient prompt: reads the baseline live at render time, with the
39    /// resolved `TRANSIENT_PROMPT_*` `overrides` taking precedence per segment.
40    pub fn transient(state: Arc<PromptState>, overrides: PromptContents) -> Self {
41        Self {
42            source: PromptSource::Transient { state, overrides },
43        }
44    }
45
46    /// Read the current contents, taking the lock only for the shared variant.
47    fn with_contents<R>(&self, action: impl FnOnce(&PromptContents) -> R) -> R {
48        match &self.source {
49            PromptSource::Shared(state) => state.with_contents(action),
50            PromptSource::Transient { state, overrides } => {
51                action(&state.with_contents(|baseline| baseline.overridden_by(overrides)))
52            }
53        }
54    }
55}
56
57/// Render `content` for the terminal, or fall back to reedline's default via
58/// `default` when nothing has been set. reedline needs `\r\n` line breaks.
59fn render_or<'a>(content: Option<&str>, default: impl FnOnce() -> Cow<'a, str>) -> Cow<'a, str> {
60    const NEWLINE: char = '\n';
61    const LINEBREAK: &str = "\r\n";
62
63    match content {
64        Some(content) => content.replace(NEWLINE, LINEBREAK).into(),
65        None => default().replace(NEWLINE, LINEBREAK).into(),
66    }
67}
68
69impl Prompt for NushellPrompt {
70    fn render_prompt_left(&self) -> Cow<'_, str> {
71        #[cfg(windows)]
72        {
73            let _ = enable_vt_processing();
74        }
75
76        self.with_contents(|c| {
77            render_or(c.left.as_deref(), || {
78                DefaultPrompt::default()
79                    .render_prompt_left()
80                    .into_owned()
81                    .into()
82            })
83        })
84    }
85
86    fn render_prompt_right(&self) -> Cow<'_, str> {
87        self.with_contents(|c| {
88            render_or(c.right.as_deref(), || {
89                DefaultPrompt::default()
90                    .render_prompt_right()
91                    .into_owned()
92                    .into()
93            })
94        })
95    }
96
97    fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow<'_, str> {
98        self.with_contents(|c| indicator_for(c, edit_mode)).into()
99    }
100
101    fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
102        self.with_contents(|c| c.multiline.as_deref().unwrap_or("::: ").to_string())
103            .into()
104    }
105
106    fn render_prompt_history_search_indicator(
107        &self,
108        history_search: PromptHistorySearch,
109    ) -> Cow<'_, str> {
110        let prefix = match history_search.status {
111            PromptHistorySearchStatus::Passing => "",
112            PromptHistorySearchStatus::Failing => "failing ",
113        };
114
115        Cow::Owned(format!(
116            "({}reverse-search: {})",
117            prefix, history_search.term
118        ))
119    }
120
121    fn right_prompt_on_last_line(&self) -> bool {
122        self.with_contents(|c| c.render_right_on_last_line)
123    }
124}
125
126/// The indicator string for the given edit mode, with the built-in defaults.
127fn indicator_for(contents: &PromptContents, edit_mode: PromptEditMode) -> String {
128    match edit_mode {
129        PromptEditMode::Default | PromptEditMode::Emacs => {
130            contents.indicator.as_deref().unwrap_or("> ").to_string()
131        }
132        PromptEditMode::Vi(PromptViMode::Normal) => {
133            contents.vi_normal.as_deref().unwrap_or("> ").to_string()
134        }
135        PromptEditMode::Vi(PromptViMode::Insert) => {
136            contents.vi_insert.as_deref().unwrap_or(": ").to_string()
137        }
138        PromptEditMode::Vi(PromptViMode::Visual) => {
139            contents.vi_normal.as_deref().unwrap_or("v ").to_string()
140        }
141        // Helix reuses the vi indicators; normal and select share one, as they
142        // share a keybinding table.
143        #[cfg(feature = "helix")]
144        PromptEditMode::Helix(PromptHelixMode::Normal | PromptHelixMode::Select) => {
145            contents.vi_normal.as_deref().unwrap_or("> ").to_string()
146        }
147        #[cfg(feature = "helix")]
148        PromptEditMode::Helix(PromptHelixMode::Insert) => {
149            contents.vi_insert.as_deref().unwrap_or(": ").to_string()
150        }
151        PromptEditMode::Custom(str) => format!("({str})"),
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn default_prompt_does_not_embed_osc_markers() {
161        let prompt = NushellPrompt::shared(Arc::new(PromptState::new()));
162        let rendered = prompt.render_prompt_left().to_string();
163
164        assert!(!rendered.contains("\x1b]133;"));
165        assert!(!rendered.contains("\x1b]633;"));
166    }
167}