1use core::fmt;
2use std::{
3 fs::File,
4 io::{Result, Write},
5};
6
7use termion::{
8 color::{self, Color},
9 cursor,
10 event::Key,
11 input::{Keys, TermRead},
12 raw::{IntoRawMode, RawTerminal},
13 terminal_size,
14};
15
16use crate::{
17 error::InquireResult,
18 ui::{Attributes, InputReader, Styled},
19};
20
21use super::Terminal;
22
23#[allow(clippy::upper_case_acronyms)]
24enum IO<'a> {
25 TTY(RawTerminal<File>),
26 #[allow(unused)]
27 Custom(&'a mut (dyn Write)),
28}
29
30pub struct TermionKeyReader {
31 keys: Keys<File>,
32}
33
34impl TermionKeyReader {
35 #[allow(unused)]
36 pub fn new() -> InquireResult<Self> {
37 Ok(Self {
38 keys: termion::get_tty()?.keys(),
39 })
40 }
41}
42
43impl InputReader for TermionKeyReader {
44 fn read_key(&mut self) -> InquireResult<crate::ui::Key> {
45 loop {
46 if let Some(key) = self.keys.next() {
47 let key = key?;
48 return Ok(key.into());
49 }
50 }
51 }
52}
53
54pub struct TermionTerminal<'a> {
55 io: IO<'a>,
56}
57
58impl<'a> TermionTerminal<'a> {
59 #[allow(unused)]
60 pub fn new() -> InquireResult<Self> {
61 let tty = termion::get_tty()?;
62 let raw_terminal = tty.into_raw_mode()?;
63 let keys = raw_terminal.try_clone()?.keys();
64
65 Ok(Self {
66 io: IO::TTY(raw_terminal),
67 })
68 }
69
70 #[cfg(test)]
74 pub fn new_with_writer<W: 'a + Write>(writer: &'a mut W) -> Self {
75 Self {
76 io: IO::Custom(writer),
77 }
78 }
79
80 fn get_writer(&mut self) -> &mut dyn Write {
81 match &mut self.io {
82 IO::TTY(w) => w,
83 IO::Custom(w) => w,
84 }
85 }
86
87 fn set_attributes(&mut self, attributes: Attributes) -> Result<()> {
88 if attributes.contains(Attributes::BOLD) {
89 write!(self.get_writer(), "{}", termion::style::Bold)?;
90 }
91 if attributes.contains(Attributes::ITALIC) {
92 write!(self.get_writer(), "{}", termion::style::Italic)?;
93 }
94
95 Ok(())
96 }
97
98 fn reset_attributes(&mut self) -> Result<()> {
99 write!(self.get_writer(), "{}", termion::style::Reset)
100 }
101
102 fn set_fg_color(&mut self, color: crate::ui::Color) -> Result<()> {
103 write!(self.get_writer(), "{}", color::Fg(color))
104 }
105
106 fn reset_fg_color(&mut self) -> Result<()> {
107 write!(self.get_writer(), "{}", color::Fg(color::Reset))
108 }
109
110 fn set_bg_color(&mut self, color: crate::ui::Color) -> Result<()> {
111 write!(self.get_writer(), "{}", color::Bg(color))
112 }
113
114 fn reset_bg_color(&mut self) -> Result<()> {
115 write!(self.get_writer(), "{}", color::Bg(color::Reset))
116 }
117}
118
119impl<'a> Terminal for TermionTerminal<'a> {
120 fn cursor_up(&mut self, cnt: u16) -> Result<()> {
121 match cnt {
122 0 => Ok(()),
123 cnt => write!(self.get_writer(), "{}", cursor::Up(cnt)),
124 }
125 }
126
127 fn cursor_down(&mut self, cnt: u16) -> Result<()> {
128 match cnt {
129 0 => Ok(()),
130 cnt => write!(self.get_writer(), "{}", cursor::Down(cnt)),
131 }
132 }
133
134 fn cursor_left(&mut self, cnt: u16) -> Result<()> {
135 match cnt {
136 0 => Ok(()),
137 cnt => write!(self.get_writer(), "{}", cursor::Left(cnt)),
138 }
139 }
140
141 fn cursor_right(&mut self, cnt: u16) -> Result<()> {
142 match cnt {
143 0 => Ok(()),
144 cnt => write!(self.get_writer(), "{}", cursor::Right(cnt)),
145 }
146 }
147
148 fn cursor_move_to_column(&mut self, idx: u16) -> Result<()> {
149 write!(self.get_writer(), "\x1b[{}G", idx.saturating_add(1))
150 }
151
152 fn flush(&mut self) -> Result<()> {
153 self.get_writer().flush()
154 }
155
156 fn get_size(&self) -> Result<Option<super::TerminalSize>> {
157 terminal_size().map(|(width, height)| super::TerminalSize::new(width, height))
158 }
159
160 fn write<T: std::fmt::Display>(&mut self, val: T) -> Result<()> {
161 write!(self.get_writer(), "{}", val)
162 }
163
164 fn write_styled<T: std::fmt::Display>(&mut self, val: &Styled<T>) -> Result<()> {
165 if let Some(color) = val.style.fg {
166 self.set_fg_color(color)?;
167 }
168 if let Some(color) = val.style.bg {
169 self.set_bg_color(color)?;
170 }
171 if !val.style.att.is_empty() {
172 self.set_attributes(val.style.att)?;
173 }
174
175 self.write(&val.content)?;
176
177 if val.style.fg.as_ref().is_some() {
178 self.reset_fg_color()?;
179 }
180 if val.style.bg.as_ref().is_some() {
181 self.reset_bg_color()?;
182 }
183 if !val.style.att.is_empty() {
184 self.reset_attributes()?;
185 }
186
187 Ok(())
188 }
189
190 fn clear_line(&mut self) -> Result<()> {
191 write!(self.get_writer(), "{}", termion::clear::CurrentLine)
192 }
193
194 fn clear_until_new_line(&mut self) -> Result<()> {
195 write!(self.get_writer(), "{}", termion::clear::UntilNewline)
196 }
197
198 fn cursor_hide(&mut self) -> Result<()> {
199 write!(self.get_writer(), "{}", cursor::Hide)
200 }
201
202 fn cursor_show(&mut self) -> Result<()> {
203 write!(self.get_writer(), "{}", cursor::Show)
204 }
205}
206
207impl<'a> Drop for TermionTerminal<'a> {
208 fn drop(&mut self) {
209 let _unused = self.flush();
210 }
211}
212
213macro_rules! into_termion_color {
214 ($self:expr, $fn_name:ident, $f:expr) => {{
215 use $crate::ui::Color as C;
216 match $self {
217 C::Black => color::Black.$fn_name($f),
218 C::LightRed => color::LightRed.$fn_name($f),
219 C::DarkRed => color::Red.$fn_name($f),
220 C::LightGreen => color::LightGreen.$fn_name($f),
221 C::DarkGreen => color::Green.$fn_name($f),
222 C::LightYellow => color::LightYellow.$fn_name($f),
223 C::DarkYellow => color::Yellow.$fn_name($f),
224 C::LightBlue => color::LightBlue.$fn_name($f),
225 C::DarkBlue => color::Blue.$fn_name($f),
226 C::LightMagenta => color::LightMagenta.$fn_name($f),
227 C::DarkMagenta => color::Magenta.$fn_name($f),
228 C::LightCyan => color::LightCyan.$fn_name($f),
229 C::DarkCyan => color::Cyan.$fn_name($f),
230 C::White => color::LightWhite.$fn_name($f),
231 C::Grey => color::White.$fn_name($f),
232 C::DarkGrey => color::LightBlack.$fn_name($f),
233 C::Rgb { r, g, b } => color::Rgb(*r, *g, *b).$fn_name($f),
234 C::AnsiValue(b) => color::AnsiValue(*b).$fn_name($f),
235 }
236 }};
237}
238
239impl Color for crate::ui::Color {
240 fn write_fg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 into_termion_color!(self, write_fg, f)
242 }
243
244 fn write_bg(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 into_termion_color!(self, write_bg, f)
246 }
247}
248
249impl From<Key> for crate::ui::Key {
250 fn from(key: Key) -> Self {
251 use crate::ui::KeyModifiers;
252
253 match key {
254 Key::Esc => Self::Escape,
255 Key::Char('\n' | '\r') => Self::Enter,
256 Key::Char('\t') => Self::Tab,
257 Key::Backspace => Self::Backspace,
258 Key::Delete => Self::Delete(KeyModifiers::empty()),
259 Key::Home => Self::Home,
260 Key::End => Self::End,
261 Key::PageUp => Self::PageUp(KeyModifiers::empty()),
262 Key::PageDown => Self::PageDown(KeyModifiers::empty()),
263 Key::Up => Self::Up(KeyModifiers::empty()),
264 Key::Down => Self::Down(KeyModifiers::empty()),
265 Key::Left => Self::Left(KeyModifiers::empty()),
266 Key::Right => Self::Right(KeyModifiers::empty()),
267 Key::Char(c) => Self::Char(c, KeyModifiers::empty()),
268 Key::Ctrl(c) => Self::Char(c, KeyModifiers::CONTROL),
269 Key::Alt(c) => Self::Char(c, KeyModifiers::ALT),
270 #[allow(deprecated)]
271 _ => Self::Any,
272 }
273 }
274}
275
276#[cfg(test)]
277mod test {
278 use crate::terminal::Terminal;
279 use crate::ui::Color;
280
281 use super::Attributes;
282 use super::TermionTerminal;
283
284 #[test]
285 fn writer() {
286 let mut write: Vec<u8> = Vec::new();
287
288 {
289 let mut terminal = TermionTerminal::new_with_writer(&mut write);
290
291 terminal.write("testing ").unwrap();
292 terminal.write("writing ").unwrap();
293 terminal.flush().unwrap();
294 terminal.write("wow").unwrap();
295 }
296
297 #[cfg(unix)]
298 assert_eq!("testing writing wow", std::str::from_utf8(&write).unwrap());
299 }
300
301 #[test]
302 fn style_management() {
303 let mut write: Vec<u8> = Vec::new();
304
305 {
306 let mut terminal = TermionTerminal::new_with_writer(&mut write);
307
308 terminal.set_attributes(Attributes::BOLD).unwrap();
309 terminal.set_attributes(Attributes::ITALIC).unwrap();
310 terminal.set_attributes(Attributes::BOLD).unwrap();
311 terminal.reset_attributes().unwrap();
312 }
313
314 #[cfg(unix)]
315 assert_eq!(
316 "\x1B[1m\x1B[3m\x1B[1m\x1B[m",
317 std::str::from_utf8(&write).unwrap()
318 );
319 }
320
321 #[test]
322 fn style_management_with_flags() {
323 let mut write: Vec<u8> = Vec::new();
324
325 {
326 let mut terminal = TermionTerminal::new_with_writer(&mut write);
327
328 terminal
329 .set_attributes(Attributes::BOLD | Attributes::ITALIC | Attributes::BOLD)
330 .unwrap();
331 terminal.reset_attributes().unwrap();
332 }
333
334 #[cfg(unix)]
335 assert_eq!("\x1B[1m\x1B[3m\x1B[m", std::str::from_utf8(&write).unwrap());
336 }
337
338 #[test]
339 fn fg_color_management() {
340 let mut write: Vec<u8> = Vec::new();
341
342 {
343 let mut terminal = TermionTerminal::new_with_writer(&mut write);
344
345 terminal.set_fg_color(Color::LightRed).unwrap();
346 terminal.reset_fg_color().unwrap();
347 terminal.set_fg_color(Color::Black).unwrap();
348 terminal.set_fg_color(Color::LightGreen).unwrap();
349 }
350
351 #[cfg(unix)]
352 assert_eq!(
353 "\x1B[38;5;9m\x1B[39m\x1B[38;5;0m\x1B[38;5;10m",
354 std::str::from_utf8(&write).unwrap()
355 );
356 }
357
358 #[test]
359 fn bg_color_management() {
360 let mut write: Vec<u8> = Vec::new();
361
362 {
363 let mut terminal = TermionTerminal::new_with_writer(&mut write);
364
365 terminal.set_bg_color(Color::LightRed).unwrap();
366 terminal.reset_bg_color().unwrap();
367 terminal.set_bg_color(Color::Black).unwrap();
368 terminal.set_bg_color(Color::LightGreen).unwrap();
369 }
370
371 #[cfg(unix)]
372 assert_eq!(
373 "\x1B[48;5;9m\x1B[49m\x1B[48;5;0m\x1B[48;5;10m",
374 std::str::from_utf8(&write).unwrap()
375 );
376 }
377}