rskit_cli/prompt/terminal/
scripted.rs1use std::collections::VecDeque;
12
13use rskit_errors::{AppError, AppResult};
14
15use super::{Capabilities, Terminal};
16use crate::prompt::key::Key;
17
18#[derive(Debug, Clone)]
20enum Input {
21 Line(String),
22 Key(Key),
23}
24
25#[derive(Debug, Default)]
27pub struct ScriptedTerminal {
28 capabilities: Capabilities,
29 inputs: VecDeque<Input>,
30 output: String,
31 raw: bool,
32}
33
34impl ScriptedTerminal {
35 #[must_use]
37 pub fn key_driven() -> Self {
38 Self {
39 capabilities: Capabilities::key_driven(),
40 ..Self::default()
41 }
42 }
43
44 #[must_use]
46 pub fn line_driven() -> Self {
47 Self {
48 capabilities: Capabilities::line_driven(),
49 ..Self::default()
50 }
51 }
52
53 #[must_use]
55 pub fn with_line(mut self, line: impl Into<String>) -> Self {
56 self.inputs.push_back(Input::Line(line.into()));
57 self
58 }
59
60 #[must_use]
62 pub fn with_lines<I, S>(mut self, lines: I) -> Self
63 where
64 I: IntoIterator<Item = S>,
65 S: Into<String>,
66 {
67 for line in lines {
68 self.inputs.push_back(Input::Line(line.into()));
69 }
70 self
71 }
72
73 #[must_use]
75 pub fn with_key(mut self, key: Key) -> Self {
76 self.inputs.push_back(Input::Key(key));
77 self
78 }
79
80 #[must_use]
82 pub fn with_keys<I>(mut self, keys: I) -> Self
83 where
84 I: IntoIterator<Item = Key>,
85 {
86 for key in keys {
87 self.inputs.push_back(Input::Key(key));
88 }
89 self
90 }
91
92 #[must_use]
94 pub fn output(&self) -> &str {
95 &self.output
96 }
97
98 #[must_use]
100 pub const fn is_interactive(&self) -> bool {
101 self.raw
102 }
103}
104
105impl Terminal for ScriptedTerminal {
106 fn capabilities(&self) -> Capabilities {
107 self.capabilities
108 }
109
110 fn read_line(&mut self) -> AppResult<Option<String>> {
111 match self.inputs.pop_front() {
112 Some(Input::Line(line)) => Ok(Some(line)),
113 Some(Input::Key(_)) => Err(AppError::invalid_input(
114 "prompt",
115 "scripted terminal expected a line but the next input is a key",
116 )),
117 None => Ok(None),
118 }
119 }
120
121 fn read_key(&mut self) -> AppResult<Key> {
122 match self.inputs.pop_front() {
123 Some(Input::Key(key)) => Ok(key),
124 Some(Input::Line(_)) => Err(AppError::invalid_input(
125 "prompt",
126 "scripted terminal expected a key but the next input is a line",
127 )),
128 None => Err(AppError::invalid_input(
129 "prompt",
130 "scripted terminal ran out of keys",
131 )),
132 }
133 }
134
135 fn write(&mut self, text: &str) -> AppResult<()> {
136 self.output.push_str(text);
137 Ok(())
138 }
139
140 fn write_line(&mut self, text: &str) -> AppResult<()> {
141 self.output.push_str(text);
142 self.output.push('\n');
143 Ok(())
144 }
145
146 fn flush(&mut self) -> AppResult<()> {
147 Ok(())
148 }
149
150 fn clear_last_lines(&mut self, _count: u16) -> AppResult<()> {
151 Ok(())
152 }
153
154 fn begin_interactive(&mut self) -> AppResult<()> {
155 self.raw = true;
156 Ok(())
157 }
158
159 fn end_interactive(&mut self) -> AppResult<()> {
160 self.raw = false;
161 Ok(())
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{Key, ScriptedTerminal, Terminal};
168
169 #[test]
170 fn replays_lines_then_eof() {
171 let mut term = ScriptedTerminal::line_driven().with_lines(["one", "two"]);
172 assert_eq!(term.read_line().expect("line"), Some("one".to_string()));
173 assert_eq!(term.read_line().expect("line"), Some("two".to_string()));
174 assert_eq!(term.read_line().expect("eof"), None);
175 }
176
177 #[test]
178 fn replays_keys_and_captures_output() {
179 let mut term = ScriptedTerminal::key_driven().with_keys([Key::Down, Key::Enter]);
180 term.write_line("frame").expect("write");
181 assert_eq!(term.read_key().expect("key"), Key::Down);
182 assert_eq!(term.read_key().expect("key"), Key::Enter);
183 assert!(term.read_key().is_err());
184 assert_eq!(term.output(), "frame\n");
185 }
186
187 #[test]
188 fn interactive_lifecycle_toggles() {
189 let mut term = ScriptedTerminal::key_driven();
190 assert!(!term.is_interactive());
191 term.begin_interactive().expect("begin");
192 assert!(term.is_interactive());
193 term.end_interactive().expect("end");
194 assert!(!term.is_interactive());
195 }
196
197 #[test]
198 fn reading_a_line_when_a_key_is_queued_is_an_error() {
199 let mut term = ScriptedTerminal::line_driven().with_key(Key::Enter);
200 let err = term.read_line().expect_err("mismatched input");
201 assert!(err.message().contains("expected a line"));
202 }
203
204 #[test]
205 fn reading_a_key_when_a_line_is_queued_is_an_error() {
206 let mut term = ScriptedTerminal::key_driven().with_line("typed");
207 let err = term.read_key().expect_err("mismatched input");
208 assert!(err.message().contains("expected a key"));
209 }
210}