osp_cli/repl/engine/config.rs
1//! Host-facing REPL configuration and outcome types.
2//!
3//! These types are the stable semantic surface around the editor engine:
4//! callers describe prompts, history, completion, and restart behavior here,
5//! while the neighboring editor modules keep the lower-level reedline
6//! integration private.
7
8use std::collections::BTreeSet;
9use std::sync::Arc;
10
11use crate::completion::CompletionTree;
12
13use super::super::history_store::HistoryConfig;
14
15pub(crate) const DEFAULT_HISTORY_MENU_ROWS: u16 = 5;
16
17/// Static prompt text shown by the interactive editor.
18///
19/// The right-hand prompt is configured separately through
20/// [`PromptRightRenderer`] because it is often dynamic.
21#[derive(Debug, Clone)]
22pub struct ReplPrompt {
23 /// Left prompt text shown before the input buffer.
24 pub left: String,
25 /// Prompt indicator rendered after `left`.
26 pub indicator: String,
27}
28
29/// Lazily renders the right-hand prompt for a REPL frame.
30pub type PromptRightRenderer = Arc<dyn Fn() -> String + Send + Sync>;
31
32/// Pre-processed editor input used for completion and highlighting.
33///
34/// REPL input can contain host-level flags or aliases that should not
35/// participate in command completion. A [`LineProjector`] can blank those
36/// spans while also hiding corresponding suggestions.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct LineProjection {
39 /// Projected line passed to completion and highlighting.
40 pub line: String,
41 /// Suggestion values that should be hidden for this projection.
42 pub hidden_suggestions: BTreeSet<String>,
43}
44
45impl LineProjection {
46 /// Returns a projection that leaves the line untouched.
47 pub fn passthrough(line: impl Into<String>) -> Self {
48 Self {
49 line: line.into(),
50 hidden_suggestions: BTreeSet::new(),
51 }
52 }
53
54 /// Marks suggestion values that should be suppressed for this projection.
55 pub fn with_hidden_suggestions(mut self, hidden_suggestions: BTreeSet<String>) -> Self {
56 self.hidden_suggestions = hidden_suggestions;
57 self
58 }
59}
60
61/// Projects a raw editor line into the view used by completion/highlighting.
62pub type LineProjector = Arc<dyn Fn(&str) -> LineProjection + Send + Sync>;
63
64/// Selects how aggressively the REPL should use the interactive line editor.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ReplInputMode {
67 /// Use the interactive editor when the terminal supports it, else fall back.
68 Auto,
69 /// Require the interactive editor even when capability probes would skip it.
70 Interactive,
71 /// Use plain stdin line reading instead of `reedline`.
72 Basic,
73}
74
75/// Controls how a command-triggered REPL restart should be presented.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum ReplReloadKind {
78 /// Rebuild the REPL and continue without reprinting the intro surface.
79 Default,
80 /// Rebuild the REPL and re-render the intro/help chrome.
81 WithIntro,
82}
83
84/// Outcome of executing one submitted REPL line.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum ReplLineResult {
87 /// Print output and continue the current session.
88 Continue(String),
89 /// Replace the current input buffer instead of printing output.
90 ReplaceInput(String),
91 /// Exit the REPL with the given process status.
92 Exit(i32),
93 /// Rebuild the REPL runtime, optionally showing intro chrome again.
94 Restart {
95 /// Output to print before restarting.
96 output: String,
97 /// Restart presentation mode to use.
98 reload: ReplReloadKind,
99 },
100}
101
102/// Outcome of one `run_repl` session.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum ReplRunResult {
105 /// Exit the editor loop and return a process status.
106 Exit(i32),
107 /// Restart the surrounding REPL host loop with refreshed state.
108 Restart {
109 /// Output to print before restarting.
110 output: String,
111 /// Restart presentation mode to use.
112 reload: ReplReloadKind,
113 },
114}
115
116/// Editor-host configuration for one REPL run.
117///
118/// This is the semantic boundary the app host should configure. The engine
119/// implementation may change, but callers should still only describe prompt,
120/// completion, history, and input-mode intent here.
121#[non_exhaustive]
122pub struct ReplRunConfig {
123 /// Left prompt and indicator strings.
124 pub prompt: ReplPrompt,
125 /// Legacy root words used when no structured completion tree is provided.
126 pub completion_words: Vec<String>,
127 /// Structured completion tree for commands, flags, and pipe verbs.
128 pub completion_tree: Option<CompletionTree>,
129 /// Visual configuration for completion menus and command highlighting.
130 pub appearance: ReplAppearance,
131 /// History backend configuration for the session.
132 pub history_config: HistoryConfig,
133 /// Chooses between interactive and basic input handling.
134 pub input_mode: ReplInputMode,
135 /// Optional renderer for the right-hand prompt.
136 pub prompt_right: Option<PromptRightRenderer>,
137 /// Optional projector used before completion/highlighting analysis.
138 pub line_projector: Option<LineProjector>,
139}
140
141impl ReplRunConfig {
142 /// Creates the exact REPL runtime baseline for one run.
143 pub fn new(prompt: ReplPrompt, history_config: HistoryConfig) -> Self {
144 Self {
145 prompt,
146 completion_words: Vec::new(),
147 completion_tree: None,
148 appearance: ReplAppearance::default(),
149 history_config,
150 input_mode: ReplInputMode::Auto,
151 prompt_right: None,
152 line_projector: None,
153 }
154 }
155
156 /// Starts guided construction for a REPL run.
157 ///
158 /// # Examples
159 ///
160 /// ```
161 /// use osp_cli::repl::{HistoryConfig, ReplInputMode, ReplPrompt, ReplRunConfig};
162 ///
163 /// let config = ReplRunConfig::builder(
164 /// ReplPrompt::simple("osp> "),
165 /// HistoryConfig::builder().build(),
166 /// )
167 /// .with_completion_words(["help", "exit"])
168 /// .with_input_mode(ReplInputMode::Basic)
169 /// .build();
170 ///
171 /// assert_eq!(config.prompt.left, "osp> ");
172 /// assert_eq!(config.input_mode, ReplInputMode::Basic);
173 /// assert_eq!(
174 /// config.completion_words,
175 /// vec!["help".to_string(), "exit".to_string()]
176 /// );
177 /// ```
178 pub fn builder(prompt: ReplPrompt, history_config: HistoryConfig) -> ReplRunConfigBuilder {
179 ReplRunConfigBuilder::new(prompt, history_config)
180 }
181}
182
183/// Builder for [`ReplRunConfig`].
184pub struct ReplRunConfigBuilder {
185 config: ReplRunConfig,
186}
187
188impl ReplRunConfigBuilder {
189 /// Starts a builder from the required prompt and history settings.
190 pub fn new(prompt: ReplPrompt, history_config: HistoryConfig) -> Self {
191 Self {
192 config: ReplRunConfig::new(prompt, history_config),
193 }
194 }
195
196 /// Replaces the legacy fallback completion words.
197 pub fn with_completion_words<I, S>(mut self, completion_words: I) -> Self
198 where
199 I: IntoIterator<Item = S>,
200 S: Into<String>,
201 {
202 self.config.completion_words = completion_words.into_iter().map(Into::into).collect();
203 self
204 }
205
206 /// Replaces the structured completion tree.
207 pub fn with_completion_tree(mut self, completion_tree: Option<CompletionTree>) -> Self {
208 self.config.completion_tree = completion_tree;
209 self
210 }
211
212 /// Replaces the REPL appearance overrides.
213 pub fn with_appearance(mut self, appearance: ReplAppearance) -> Self {
214 self.config.appearance = appearance;
215 self
216 }
217
218 /// Replaces the history configuration.
219 pub fn with_history_config(mut self, history_config: HistoryConfig) -> Self {
220 self.config.history_config = history_config;
221 self
222 }
223
224 /// Replaces the input-mode policy.
225 pub fn with_input_mode(mut self, input_mode: ReplInputMode) -> Self {
226 self.config.input_mode = input_mode;
227 self
228 }
229
230 /// Replaces the optional right-prompt renderer.
231 pub fn with_prompt_right(mut self, prompt_right: Option<PromptRightRenderer>) -> Self {
232 self.config.prompt_right = prompt_right;
233 self
234 }
235
236 /// Replaces the optional completion/highlighting line projector.
237 pub fn with_line_projector(mut self, line_projector: Option<LineProjector>) -> Self {
238 self.config.line_projector = line_projector;
239 self
240 }
241
242 /// Builds the configured [`ReplRunConfig`].
243 pub fn build(self) -> ReplRunConfig {
244 self.config
245 }
246}
247
248impl ReplPrompt {
249 /// Builds a prompt with no indicator suffix.
250 ///
251 /// # Examples
252 ///
253 /// ```
254 /// use osp_cli::repl::ReplPrompt;
255 ///
256 /// let prompt = ReplPrompt::simple("osp> ");
257 ///
258 /// assert_eq!(prompt.left, "osp> ");
259 /// assert!(prompt.indicator.is_empty());
260 /// ```
261 pub fn simple(left: impl Into<String>) -> Self {
262 Self {
263 left: left.into(),
264 indicator: String::new(),
265 }
266 }
267}
268
269/// Style overrides for REPL-only completion and highlighting chrome.
270#[derive(Debug, Clone)]
271#[non_exhaustive]
272pub struct ReplAppearance {
273 /// Style applied to non-selected completion text.
274 pub completion_text_style: Option<String>,
275 /// Background style applied to the completion menu.
276 pub completion_background_style: Option<String>,
277 /// Style applied to the selected completion entry.
278 pub completion_highlight_style: Option<String>,
279 /// Style applied to recognized command segments in the input line.
280 pub command_highlight_style: Option<String>,
281 /// Maximum number of visible rows in the history search menu.
282 pub history_menu_rows: u16,
283}
284
285impl ReplAppearance {
286 /// Starts guided construction for REPL-only appearance overrides.
287 ///
288 /// # Examples
289 ///
290 /// ```
291 /// use osp_cli::repl::ReplAppearance;
292 ///
293 /// let appearance = ReplAppearance::builder()
294 /// .with_history_menu_rows(8)
295 /// .with_command_highlight_style(Some("green".to_string()))
296 /// .build();
297 ///
298 /// assert_eq!(appearance.history_menu_rows, 8);
299 /// assert_eq!(appearance.command_highlight_style.as_deref(), Some("green"));
300 /// ```
301 pub fn builder() -> ReplAppearanceBuilder {
302 ReplAppearanceBuilder::new()
303 }
304}
305
306impl Default for ReplAppearance {
307 fn default() -> Self {
308 Self {
309 completion_text_style: None,
310 completion_background_style: None,
311 completion_highlight_style: None,
312 command_highlight_style: None,
313 history_menu_rows: DEFAULT_HISTORY_MENU_ROWS,
314 }
315 }
316}
317
318/// Builder for [`ReplAppearance`].
319#[derive(Debug, Clone, Default)]
320pub struct ReplAppearanceBuilder {
321 appearance: ReplAppearance,
322}
323
324impl ReplAppearanceBuilder {
325 /// Starts a builder from the default REPL appearance baseline.
326 pub fn new() -> Self {
327 Self {
328 appearance: ReplAppearance::default(),
329 }
330 }
331
332 /// Replaces the style applied to non-selected completion text.
333 pub fn with_completion_text_style(mut self, completion_text_style: Option<String>) -> Self {
334 self.appearance.completion_text_style = completion_text_style;
335 self
336 }
337
338 /// Replaces the menu background style.
339 pub fn with_completion_background_style(
340 mut self,
341 completion_background_style: Option<String>,
342 ) -> Self {
343 self.appearance.completion_background_style = completion_background_style;
344 self
345 }
346
347 /// Replaces the style applied to the selected completion entry.
348 pub fn with_completion_highlight_style(
349 mut self,
350 completion_highlight_style: Option<String>,
351 ) -> Self {
352 self.appearance.completion_highlight_style = completion_highlight_style;
353 self
354 }
355
356 /// Replaces the style applied to recognized command segments.
357 pub fn with_command_highlight_style(mut self, command_highlight_style: Option<String>) -> Self {
358 self.appearance.command_highlight_style = command_highlight_style;
359 self
360 }
361
362 /// Replaces the maximum number of visible history-menu rows.
363 pub fn with_history_menu_rows(mut self, history_menu_rows: u16) -> Self {
364 self.appearance.history_menu_rows = history_menu_rows;
365 self
366 }
367
368 /// Builds the configured [`ReplAppearance`].
369 pub fn build(self) -> ReplAppearance {
370 self.appearance
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::{
377 ReplAppearance, ReplInputMode, ReplPrompt, ReplReloadKind, ReplRunConfig, ReplRunResult,
378 };
379 use crate::repl::HistoryConfig;
380
381 #[test]
382 fn run_config_builder_captures_host_surface_choices() {
383 let appearance = ReplAppearance::builder()
384 .with_history_menu_rows(8)
385 .with_command_highlight_style(Some("green".to_string()))
386 .build();
387 let config = ReplRunConfig::builder(
388 ReplPrompt::simple("osp> "),
389 HistoryConfig::builder().build(),
390 )
391 .with_completion_words(["help", "exit"])
392 .with_appearance(appearance.clone())
393 .with_input_mode(ReplInputMode::Basic)
394 .build();
395
396 assert_eq!(config.prompt.left, "osp> ");
397 assert_eq!(config.input_mode, ReplInputMode::Basic);
398 assert_eq!(
399 config.completion_words,
400 vec!["help".to_string(), "exit".to_string()]
401 );
402 assert_eq!(config.appearance.history_menu_rows, 8);
403 assert_eq!(
404 config.appearance.command_highlight_style.as_deref(),
405 Some("green")
406 );
407 }
408
409 #[test]
410 fn prompt_and_restart_outcomes_stay_plain_semantic_payloads() {
411 let prompt = ReplPrompt::simple("osp");
412 assert_eq!(prompt.left, "osp");
413 assert!(prompt.indicator.is_empty());
414
415 let restart = ReplRunResult::Restart {
416 output: "reloading".to_string(),
417 reload: ReplReloadKind::WithIntro,
418 };
419 assert!(matches!(
420 restart,
421 ReplRunResult::Restart {
422 output,
423 reload: ReplReloadKind::WithIntro
424 } if output == "reloading"
425 ));
426 }
427}