requestty_ui/backend/
termion.rs

1use std::{
2    cmp::Ordering,
3    fmt,
4    io::{self, Write},
5    ops::{Deref, DerefMut},
6    os::fd::AsFd,
7};
8
9use termion::{
10    clear, color, cursor,
11    raw::{IntoRawMode, RawTerminal},
12    scroll, style,
13};
14
15use super::{Attributes, Backend, ClearType, Color, DisplayBackend, MoveDirection, Size};
16
17enum Terminal<W: Write + AsFd> {
18    Raw(RawTerminal<W>),
19    Normal(W),
20    TemporaryNone,
21}
22
23impl<W: Write + AsFd> Deref for Terminal<W> {
24    type Target = W;
25
26    fn deref(&self) -> &Self::Target {
27        match self {
28            Terminal::Raw(w) => w,
29            Terminal::Normal(w) => w,
30            Terminal::TemporaryNone => unreachable!("TemporaryNone is only used during swap"),
31        }
32    }
33}
34
35impl<W: Write + AsFd> DerefMut for Terminal<W> {
36    fn deref_mut(&mut self) -> &mut Self::Target {
37        match self {
38            Terminal::Raw(w) => w,
39            Terminal::Normal(w) => w,
40            Terminal::TemporaryNone => unreachable!("TemporaryNone is only used during swap"),
41        }
42    }
43}
44
45/// A display backend that uses the `termion` library.
46///
47/// This is separate from [`TermionBackend`] to allow use without [`AsFd`] types.
48#[allow(missing_debug_implementations)]
49#[cfg_attr(docsrs, doc(cfg(feature = "termion")))]
50pub struct TermionDisplayBackend<W: Write> {
51    attributes: Attributes,
52    buffer: W,
53}
54
55impl<W: Write> TermionDisplayBackend<W> {
56    /// Creates a new [`TermionDisplayBackend`]
57    pub fn new(buffer: W) -> Self {
58        Self {
59            buffer,
60            attributes: Attributes::empty(),
61        }
62    }
63}
64
65impl<W: Write> Write for TermionDisplayBackend<W> {
66    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
67        self.buffer.write(buf)
68    }
69
70    fn flush(&mut self) -> io::Result<()> {
71        self.buffer.flush()
72    }
73}
74
75impl<W: Write + AsFd> AsFd for TermionDisplayBackend<W> {
76    fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
77        self.buffer.as_fd()
78    }
79}
80
81impl<W: Write> DisplayBackend for TermionDisplayBackend<W> {
82    fn set_attributes(&mut self, attributes: Attributes) -> io::Result<()> {
83        set_attributes(self.attributes, attributes, &mut self.buffer)?;
84        self.attributes = attributes;
85        Ok(())
86    }
87
88    fn set_fg(&mut self, color: Color) -> io::Result<()> {
89        write!(self.buffer, "{}", Fg(color))
90    }
91
92    fn set_bg(&mut self, color: Color) -> io::Result<()> {
93        write!(self.buffer, "{}", Bg(color))
94    }
95}
96
97/// A backend that uses the `termion` library.
98#[allow(missing_debug_implementations)]
99#[cfg_attr(docsrs, doc(cfg(feature = "termion")))]
100pub struct TermionBackend<W: Write + AsFd> {
101    buffer: Terminal<TermionDisplayBackend<W>>,
102}
103
104impl<W: Write + AsFd> TermionBackend<W> {
105    /// Creates a new [`TermionBackend`]
106    pub fn new(buffer: W) -> Self {
107        Self {
108            buffer: Terminal::Normal(TermionDisplayBackend::new(buffer)),
109        }
110    }
111}
112
113impl<W: Write + AsFd> Write for TermionBackend<W> {
114    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
115        self.buffer.write(buf)
116    }
117
118    fn flush(&mut self) -> io::Result<()> {
119        self.buffer.flush()
120    }
121}
122
123impl<W: Write + AsFd> DisplayBackend for TermionBackend<W> {
124    fn set_attributes(&mut self, attributes: Attributes) -> io::Result<()> {
125        match self.buffer {
126            Terminal::Raw(ref mut buf) => buf.set_attributes(attributes),
127            Terminal::Normal(ref mut buf) => buf.set_attributes(attributes),
128            Terminal::TemporaryNone => unreachable!("TemporaryNone is only used during swap"),
129        }
130    }
131
132    fn set_fg(&mut self, color: Color) -> io::Result<()> {
133        match self.buffer {
134            Terminal::Raw(ref mut buf) => buf.set_fg(color),
135            Terminal::Normal(ref mut buf) => buf.set_fg(color),
136            Terminal::TemporaryNone => unreachable!("TemporaryNone is only used during swap"),
137        }
138    }
139
140    fn set_bg(&mut self, color: Color) -> io::Result<()> {
141        match self.buffer {
142            Terminal::Raw(ref mut buf) => buf.set_bg(color),
143            Terminal::Normal(ref mut buf) => buf.set_bg(color),
144            Terminal::TemporaryNone => unreachable!("TemporaryNone is only used during swap"),
145        }
146    }
147
148    fn write_styled(
149        &mut self,
150        styled: &crate::style::Styled<dyn fmt::Display + '_>,
151    ) -> io::Result<()> {
152        match self.buffer {
153            Terminal::Raw(ref mut buf) => buf.write_styled(styled),
154            Terminal::Normal(ref mut buf) => buf.write_styled(styled),
155            Terminal::TemporaryNone => unreachable!("TemporaryNone is only used during swap"),
156        }
157    }
158}
159
160impl<W: Write + AsFd> Backend for TermionBackend<W> {
161    fn enable_raw_mode(&mut self) -> io::Result<()> {
162        match self.buffer {
163            Terminal::Raw(ref mut buf) => buf.activate_raw_mode(),
164            Terminal::Normal(_) => {
165                let buf = match std::mem::replace(&mut self.buffer, Terminal::TemporaryNone) {
166                    Terminal::Normal(buf) => buf,
167                    _ => unreachable!(),
168                };
169
170                self.buffer = Terminal::Raw(buf.into_raw_mode()?);
171
172                Ok(())
173            }
174            Terminal::TemporaryNone => unreachable!("TemporaryNone is only used during swap"),
175        }
176    }
177
178    fn disable_raw_mode(&mut self) -> io::Result<()> {
179        match self.buffer {
180            Terminal::Raw(ref buf) => buf.suspend_raw_mode(),
181            Terminal::Normal(_) => {
182                if cfg!(debug_assertions) {
183                    panic!("Called disable_raw_mode without enable_raw_mode");
184                }
185
186                Ok(())
187            }
188            Terminal::TemporaryNone => unreachable!("TemporaryNone is only used during swap"),
189        }
190    }
191
192    fn hide_cursor(&mut self) -> io::Result<()> {
193        write!(self.buffer, "{}", cursor::Hide)
194    }
195
196    fn show_cursor(&mut self) -> io::Result<()> {
197        write!(self.buffer, "{}", cursor::Show)
198    }
199
200    fn get_cursor_pos(&mut self) -> io::Result<(u16, u16)> {
201        cursor::DetectCursorPos::cursor_pos(&mut *self.buffer)
202            // 0 index the position
203            .map(|(x, y)| (x - 1, y - 1))
204    }
205
206    fn move_cursor_to(&mut self, x: u16, y: u16) -> io::Result<()> {
207        write!(self.buffer, "{}", cursor::Goto(x + 1, y + 1))
208    }
209
210    fn move_cursor(&mut self, direction: MoveDirection) -> io::Result<()> {
211        match direction {
212            MoveDirection::Up(n) => write!(self.buffer, "{}", cursor::Up(n))?,
213            MoveDirection::Down(n) => write!(self.buffer, "{}", cursor::Down(n))?,
214            MoveDirection::Left(n) => write!(self.buffer, "{}", cursor::Left(n))?,
215            MoveDirection::Right(n) => write!(self.buffer, "{}", cursor::Right(n))?,
216            _ => super::default_move_cursor(self, direction)?,
217        }
218
219        Ok(())
220    }
221
222    fn scroll(&mut self, dist: i16) -> io::Result<()> {
223        match dist.cmp(&0) {
224            Ordering::Greater => {
225                write!(self.buffer, "{}", scroll::Down(dist as u16))
226            }
227            Ordering::Less => {
228                write!(self.buffer, "{}", scroll::Up(-dist as u16))
229            }
230            Ordering::Equal => Ok(()),
231        }
232    }
233
234    fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
235        match clear_type {
236            ClearType::All => write!(self.buffer, "{}", clear::All),
237            ClearType::FromCursorDown => {
238                write!(self.buffer, "{}", clear::AfterCursor)
239            }
240            ClearType::FromCursorUp => {
241                write!(self.buffer, "{}", clear::BeforeCursor)
242            }
243            ClearType::CurrentLine => write!(self.buffer, "{}", clear::CurrentLine),
244            ClearType::UntilNewLine => {
245                write!(self.buffer, "{}", clear::UntilNewline)
246            }
247        }
248    }
249
250    fn size(&self) -> io::Result<Size> {
251        termion::terminal_size().map(Into::into)
252    }
253}
254
255pub(super) struct Fg(pub(super) Color);
256
257pub(super) struct Bg(pub(super) Color);
258
259impl fmt::Display for Fg {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        use color::Color as TermionColor;
262        match self.0 {
263            Color::Reset => color::Reset.write_fg(f),
264            Color::Black => color::Black.write_fg(f),
265            Color::Red => color::Red.write_fg(f),
266            Color::Green => color::Green.write_fg(f),
267            Color::Yellow => color::Yellow.write_fg(f),
268            Color::Blue => color::Blue.write_fg(f),
269            Color::Magenta => color::Magenta.write_fg(f),
270            Color::Cyan => color::Cyan.write_fg(f),
271            Color::Grey => color::White.write_fg(f),
272            Color::DarkGrey => color::LightBlack.write_fg(f),
273            Color::LightRed => color::LightRed.write_fg(f),
274            Color::LightGreen => color::LightGreen.write_fg(f),
275            Color::LightBlue => color::LightBlue.write_fg(f),
276            Color::LightYellow => color::LightYellow.write_fg(f),
277            Color::LightMagenta => color::LightMagenta.write_fg(f),
278            Color::LightCyan => color::LightCyan.write_fg(f),
279            Color::White => color::LightWhite.write_fg(f),
280            Color::Ansi(i) => color::AnsiValue(i).write_fg(f),
281            Color::Rgb(r, g, b) => color::Rgb(r, g, b).write_fg(f),
282        }
283    }
284}
285impl fmt::Display for Bg {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        use color::Color as TermionColor;
288        match self.0 {
289            Color::Reset => color::Reset.write_bg(f),
290            Color::Black => color::Black.write_bg(f),
291            Color::Red => color::Red.write_bg(f),
292            Color::Green => color::Green.write_bg(f),
293            Color::Yellow => color::Yellow.write_bg(f),
294            Color::Blue => color::Blue.write_bg(f),
295            Color::Magenta => color::Magenta.write_bg(f),
296            Color::Cyan => color::Cyan.write_bg(f),
297            Color::Grey => color::White.write_bg(f),
298            Color::DarkGrey => color::LightBlack.write_bg(f),
299            Color::LightRed => color::LightRed.write_bg(f),
300            Color::LightGreen => color::LightGreen.write_bg(f),
301            Color::LightBlue => color::LightBlue.write_bg(f),
302            Color::LightYellow => color::LightYellow.write_bg(f),
303            Color::LightMagenta => color::LightMagenta.write_bg(f),
304            Color::LightCyan => color::LightCyan.write_bg(f),
305            Color::White => color::LightWhite.write_bg(f),
306            Color::Ansi(i) => color::AnsiValue(i).write_bg(f),
307            Color::Rgb(r, g, b) => color::Rgb(r, g, b).write_bg(f),
308        }
309    }
310}
311
312pub(super) fn set_attributes<W: Write>(
313    from: Attributes,
314    to: Attributes,
315    mut w: W,
316) -> io::Result<()> {
317    let diff = from.diff(to);
318
319    if diff.to_remove.contains(Attributes::REVERSED) {
320        write!(w, "{}", style::NoInvert)?;
321    }
322    if diff.to_remove.contains(Attributes::BOLD) {
323        // XXX: the termion NoBold flag actually enables double-underline on ECMA-48 compliant
324        // terminals, and NoFaint additionally disables bold... so we use this trick to get
325        // the right semantics.
326        write!(w, "{}", style::NoFaint)?;
327
328        if to.contains(Attributes::DIM) {
329            write!(w, "{}", style::Faint)?;
330        }
331    }
332    if diff.to_remove.contains(Attributes::ITALIC) {
333        write!(w, "{}", style::NoItalic)?;
334    }
335    if diff.to_remove.contains(Attributes::UNDERLINED) {
336        write!(w, "{}", style::NoUnderline)?;
337    }
338    if diff.to_remove.contains(Attributes::DIM) {
339        write!(w, "{}", style::NoFaint)?;
340
341        // XXX: the NoFaint flag additionally disables bold as well, so we need to re-enable it
342        // here if we want it.
343        if to.contains(Attributes::BOLD) {
344            write!(w, "{}", style::Bold)?;
345        }
346    }
347    if diff.to_remove.contains(Attributes::CROSSED_OUT) {
348        write!(w, "{}", style::NoCrossedOut)?;
349    }
350    if diff.to_remove.contains(Attributes::SLOW_BLINK)
351        || diff.to_remove.contains(Attributes::RAPID_BLINK)
352    {
353        write!(w, "{}", style::NoBlink)?;
354    }
355
356    if diff.to_add.contains(Attributes::REVERSED) {
357        write!(w, "{}", style::Invert)?;
358    }
359    if diff.to_add.contains(Attributes::BOLD) {
360        write!(w, "{}", style::Bold)?;
361    }
362    if diff.to_add.contains(Attributes::ITALIC) {
363        write!(w, "{}", style::Italic)?;
364    }
365    if diff.to_add.contains(Attributes::UNDERLINED) {
366        write!(w, "{}", style::Underline)?;
367    }
368    if diff.to_add.contains(Attributes::DIM) {
369        write!(w, "{}", style::Faint)?;
370    }
371    if diff.to_add.contains(Attributes::CROSSED_OUT) {
372        write!(w, "{}", style::CrossedOut)?;
373    }
374    if diff.to_add.contains(Attributes::SLOW_BLINK) || diff.to_add.contains(Attributes::RAPID_BLINK)
375    {
376        write!(w, "{}", style::Blink)?;
377    }
378
379    Ok(())
380}