Skip to main content

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}