line_editor/config.rs
1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// Default maximum number of history entries retained in memory.
6pub const DEFAULT_MAX_HISTORY_LEN: usize = 100;
7
8/// Default maximum length of an input line in bytes.
9pub const DEFAULT_MAX_LINE_LEN: usize = 4096;
10
11/// Default terminal column width when auto-detection fails.
12pub const DEFAULT_COLUMN_COUNT: usize = 80;
13
14/// Controls how TTY terminal capability detection is performed.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum TerminalMode {
17 /// Automatically check `std::io::stdin().is_terminal()` at runtime.
18 #[default]
19 Auto,
20 /// Force TTY mode regardless of ambient `is_terminal()` check.
21 Tty,
22 /// Force non-TTY mode regardless of ambient `is_terminal()` check.
23 NonTty,
24}
25
26/// Controls how the terminal window column width is determined.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum ColumnWidth {
29 /// Automatically determine width by querying `TIOCGWINSZ` via ioctl, falling back to ANSI
30 /// cursor position queries.
31 #[default]
32 Auto,
33 /// Determine width using only ANSI cursor position queries (`\x1b[6n`), bypassing `TIOCGWINSZ`
34 /// ioctl.
35 AnsiCursor,
36 /// Use an explicitly specified fixed column width without querying the terminal.
37 Fixed(usize),
38}
39
40/// Represents the capability level of a terminal type name (e.g., `TERM` variable or explicit
41/// configuration).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum TerminalCapability {
44 /// Fully supported terminal with ANSI/VT100 capabilities.
45 #[default]
46 Supported,
47 /// Limited UART terminal that requires manual character echoing.
48 UartEcho,
49 /// Unsupported terminal (e.g., `dumb`, `cons25`, `emacs`) that only supports line reading with
50 /// a prompt.
51 PromptOnly,
52}
53
54/// Represents the resolved concrete operating mode of the line editor.
55///
56/// `OperatingMode` determines how `line-editor` handles input streams, character echoing, escape
57/// sequences, line editing keybindings, and prompt rendering.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum OperatingMode {
60 /// Non-TTY mode (e.g., input redirected from a file, pipe, or non-interactive script).
61 ///
62 /// - **When used**: Resolved when standard input is not a terminal stream, or when
63 /// `TerminalMode::NonTty` is explicitly configured.
64 /// - **Consequences**: No prompt is printed to the output stream. Escape sequences, raw mode,
65 /// completion, hints, and line-editing keybindings are completely disabled. Input bytes are
66 /// read sequentially until a newline or EOF is reached.
67 NonTty,
68
69 /// Prompt-only mode for unsupported or dumb terminals (e.g., `TERM=dumb`, `cons25`, `emacs`).
70 ///
71 /// - **When used**: Resolved when the terminal capability check determines the terminal does
72 /// not support ANSI/VT100 escape sequences.
73 /// - **Consequences**: The prompt is written to the output stream before reading input, but
74 /// ANSI/VT100 control sequences (cursor positioning, line clearing, colorized hints) and raw
75 /// mode editing are disabled. Standard stream line buffering applies.
76 PromptOnly,
77
78 /// UART character echo mode (e.g., `TERM=uart` or serial console).
79 ///
80 /// - **When used**: Resolved when connected to a serial UART console that lacks full ANSI/VT100
81 /// screen manipulation but requires character-by-character echoing.
82 /// - **Consequences**: The prompt is printed, and typed characters are echoed back to the
83 /// output stream byte-by-byte. Basic backspace erasing (`\x08 \x08`) is handled, but complex
84 /// VT100 cursor movement and multi-line editing are disabled.
85 UartEcho,
86
87 /// Full interactive VT100/ANSI terminal mode.
88 ///
89 /// - **When used**: Resolved when connected to an interactive TTY terminal supporting
90 /// ANSI/VT100 control sequences (e.g., `xterm`, `vt100`, `screen`, `tmux`).
91 /// - **Consequences**: Full raw-mode line editing with VT100 escape sequence handling, cursor
92 /// movement, history navigation, tab autocompletion, inline hints, and multi-line refresh.
93 /// Writes a trailing newline upon line submission.
94 Interactive,
95}
96
97/// Configuration settings for an [`Editor`](crate::Editor) instance.
98#[derive(Debug, Clone)]
99pub struct Config {
100 /// Maximum number of history entries to keep in memory.
101 pub max_history_len: usize,
102 /// Maximum line length in bytes.
103 pub max_line_len: usize,
104 /// Enable multi-line editing mode.
105 pub multiline_mode: bool,
106 /// Mode for TTY terminal detection.
107 pub terminal_mode: TerminalMode,
108 /// Mode for determining column width.
109 pub column_width: ColumnWidth,
110 /// Explicit terminal name override (or `None` to check `TERM` env var).
111 pub term_name: Option<String>,
112}
113
114impl Default for Config {
115 fn default() -> Self {
116 Self {
117 max_history_len: DEFAULT_MAX_HISTORY_LEN,
118 max_line_len: DEFAULT_MAX_LINE_LEN,
119 multiline_mode: false,
120 terminal_mode: TerminalMode::default(),
121 column_width: ColumnWidth::default(),
122 term_name: None,
123 }
124 }
125}
126
127impl Config {
128 /// Sets the terminal detection mode.
129 pub fn with_terminal_mode(mut self, mode: TerminalMode) -> Self {
130 self.terminal_mode = mode;
131 self
132 }
133
134 /// Sets the column width determination mode.
135 pub fn with_column_width(mut self, column_width: ColumnWidth) -> Self {
136 self.column_width = column_width;
137 self
138 }
139
140 /// Sets an explicit terminal name override.
141 pub fn with_term_name(mut self, name: Option<impl Into<String>>) -> Self {
142 self.term_name = name.map(Into::into);
143 self
144 }
145
146 /// Resolves this configuration specification into a `TerminalCapability` level.
147 pub fn resolve_terminal_capability(&self) -> TerminalCapability {
148 terminal_capability_from_name(self.term_name.as_deref())
149 }
150
151 /// Resolves this configuration specification into a concrete `OperatingMode` for the editor.
152 ///
153 /// `is_terminal` is invoked only when `self.terminal_mode == TerminalMode::Auto` to resolve
154 /// whether the input stream is a TTY.
155 pub fn resolve_operating_mode(&self, is_terminal: impl FnOnce() -> bool) -> OperatingMode {
156 let is_tty = match self.terminal_mode {
157 TerminalMode::Auto => is_terminal(),
158 TerminalMode::Tty => true,
159 TerminalMode::NonTty => false,
160 };
161
162 if !is_tty {
163 return OperatingMode::NonTty;
164 }
165
166 match self.resolve_terminal_capability() {
167 TerminalCapability::PromptOnly => OperatingMode::PromptOnly,
168 TerminalCapability::UartEcho => OperatingMode::UartEcho,
169 TerminalCapability::Supported => OperatingMode::Interactive,
170 }
171 }
172}
173
174/// Classifies a terminal type name into a `TerminalCapability`.
175pub fn terminal_capability_from_name(term: Option<&str>) -> TerminalCapability {
176 let term_val = match term {
177 Some(t) => t.to_string(),
178 None => std::env::var("TERM").unwrap_or_default(),
179 };
180
181 if term_val.is_empty() {
182 return TerminalCapability::Supported;
183 }
184
185 let unsupported = ["dumb", "cons25", "emacs"];
186 for name in &unsupported {
187 if term_val.eq_ignore_ascii_case(name) {
188 return TerminalCapability::PromptOnly;
189 }
190 }
191
192 if term_val.eq_ignore_ascii_case("uart") {
193 return TerminalCapability::UartEcho;
194 }
195
196 TerminalCapability::Supported
197}