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