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