rskit_cli/prompt/terminal/
line.rs1use std::io::{self, BufRead, Write};
4
5use rskit_errors::{AppError, AppResult};
6
7use super::{Capabilities, Terminal};
8use crate::prompt::key::Key;
9
10pub struct LineTerminal<R, W> {
17 reader: R,
18 writer: W,
19}
20
21impl LineTerminal<io::BufReader<io::Stdin>, io::Stderr> {
22 #[must_use]
24 pub fn stdio() -> Self {
25 Self::new(io::BufReader::new(io::stdin()), io::stderr())
26 }
27}
28
29impl<R: BufRead, W: Write> LineTerminal<R, W> {
30 #[must_use]
32 pub const fn new(reader: R, writer: W) -> Self {
33 Self { reader, writer }
34 }
35}
36
37impl<R: BufRead, W: Write> Terminal for LineTerminal<R, W> {
38 fn capabilities(&self) -> Capabilities {
39 Capabilities::line_driven()
40 }
41
42 fn read_line(&mut self) -> AppResult<Option<String>> {
43 let mut buffer = String::new();
44 let read = self
45 .reader
46 .read_line(&mut buffer)
47 .map_err(AppError::internal)?;
48 if read == 0 {
49 return Ok(None);
50 }
51 Ok(Some(buffer.trim_end_matches(['\n', '\r']).to_string()))
52 }
53
54 fn read_key(&mut self) -> AppResult<Key> {
55 Err(AppError::invalid_input(
56 "prompt",
57 "line terminal does not support key input",
58 ))
59 }
60
61 fn write(&mut self, text: &str) -> AppResult<()> {
62 write!(self.writer, "{text}").map_err(AppError::internal)
63 }
64
65 fn write_line(&mut self, text: &str) -> AppResult<()> {
66 writeln!(self.writer, "{text}").map_err(AppError::internal)
67 }
68
69 fn flush(&mut self) -> AppResult<()> {
70 self.writer.flush().map_err(AppError::internal)
71 }
72
73 fn clear_last_lines(&mut self, _count: u16) -> AppResult<()> {
74 Ok(())
75 }
76
77 fn begin_interactive(&mut self) -> AppResult<()> {
78 Ok(())
79 }
80
81 fn end_interactive(&mut self) -> AppResult<()> {
82 Ok(())
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::{Capabilities, LineTerminal, Terminal};
89
90 #[test]
91 fn reads_trimmed_lines_then_eof() {
92 let input = b"hello\nworld\n";
93 let mut out = Vec::new();
94 let mut term = LineTerminal::new(&input[..], &mut out);
95 assert_eq!(term.capabilities(), Capabilities::line_driven());
96 assert_eq!(term.read_line().expect("line"), Some("hello".to_string()));
97 assert_eq!(term.read_line().expect("line"), Some("world".to_string()));
98 assert_eq!(term.read_line().expect("eof"), None);
99 }
100
101 #[test]
102 fn key_input_is_unsupported() {
103 let mut out = Vec::new();
104 let mut term = LineTerminal::new(&b""[..], &mut out);
105 assert!(term.read_key().is_err());
106 }
107
108 #[test]
109 fn writes_pass_through() {
110 let mut out = Vec::new();
111 {
112 let mut term = LineTerminal::new(&b""[..], &mut out);
113 term.write("a").expect("write");
114 term.write_line("b").expect("writeln");
115 }
116 assert_eq!(String::from_utf8(out).expect("utf8"), "ab\n");
117 }
118
119 #[test]
120 fn line_control_operations_are_no_ops_over_cooked_stdio() {
121 let mut out = Vec::new();
122 let mut term = LineTerminal::new(&b""[..], &mut out);
123 term.flush().expect("flush");
124 term.clear_last_lines(3).expect("clear is a no-op");
125 term.begin_interactive().expect("begin is a no-op");
126 term.end_interactive().expect("end is a no-op");
127 }
128
129 #[test]
130 fn stdio_binds_to_process_streams() {
131 let term = LineTerminal::stdio();
134 assert_eq!(term.capabilities(), Capabilities::line_driven());
135 }
136}