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
12pub struct NushellPrompt {
14 source: PromptSource,
15}
16
17enum PromptSource {
19 Shared(Arc<PromptState>),
21
22 Transient {
25 state: Arc<PromptState>,
26 overrides: PromptContents,
27 },
28}
29
30impl NushellPrompt {
31 pub fn shared(state: Arc<PromptState>) -> Self {
33 Self {
34 source: PromptSource::Shared(state),
35 }
36 }
37
38 pub fn transient(state: Arc<PromptState>, overrides: PromptContents) -> Self {
41 Self {
42 source: PromptSource::Transient { state, overrides },
43 }
44 }
45
46 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
57fn 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
126fn 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 #[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}