gnostr_asyncgit/gitui/term/
mod.rs

1use crate::{gitui::gitui_error::Error, gitui::Res};
2use crossterm::{
3    event::Event,
4    terminal::{
5        disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, EnterAlternateScreen,
6        LeaveAlternateScreen,
7    },
8    ExecutableCommand,
9};
10use ratatui::{
11    backend::{Backend, CrosstermBackend, TestBackend},
12    layout::Size,
13    prelude::{backend::WindowSize, buffer::Cell, Position},
14    Terminal,
15};
16use std::io::{self, stderr, Stderr};
17use std::{fmt::Display, time::Duration};
18
19pub type Term = Terminal<TermBackend>;
20
21// TODO It would be more logical if the following top-level functions also were in 'TermBackend'.
22//      However left here for now.
23
24pub fn alternate_screen<T, F: Fn() -> Res<T>>(fun: F) -> Res<T> {
25    stderr()
26        .execute(EnterAlternateScreen)
27        .map_err(Error::Term)?;
28    let result = fun();
29    stderr()
30        .execute(LeaveAlternateScreen)
31        .map_err(Error::Term)?;
32    result
33}
34
35pub fn raw_mode<T, F: Fn() -> Res<T>>(fun: F) -> Res<T> {
36    let was_raw_mode_enabled = is_raw_mode_enabled().map_err(Error::Term)?;
37
38    if !was_raw_mode_enabled {
39        enable_raw_mode().map_err(Error::Term)?;
40    }
41
42    let result = fun();
43
44    if !was_raw_mode_enabled {
45        disable_raw_mode().map_err(Error::Term)?;
46    }
47
48    result
49}
50
51pub fn cleanup_alternate_screen() {
52    print_err(stderr().execute(LeaveAlternateScreen));
53}
54
55pub fn cleanup_raw_mode() {
56    print_err(disable_raw_mode());
57}
58
59fn print_err<T, E: Display>(result: Result<T, E>) {
60    match result {
61        Ok(_) => (),
62        Err(error) => eprintln!("Error: {}", error),
63    };
64}
65
66pub fn backend() -> TermBackend {
67    TermBackend::Crossterm(CrosstermBackend::new(stderr()))
68}
69
70pub enum TermBackend {
71    Crossterm(CrosstermBackend<Stderr>),
72    #[allow(dead_code)]
73    Test {
74        backend: TestBackend,
75        events: Vec<Event>,
76    },
77}
78
79impl Backend for TermBackend {
80    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
81    where
82        I: Iterator<Item = (u16, u16, &'a Cell)>,
83    {
84        match self {
85            TermBackend::Crossterm(t) => t.draw(content),
86            TermBackend::Test { backend, .. } => backend.draw(content),
87        }
88    }
89
90    fn hide_cursor(&mut self) -> io::Result<()> {
91        match self {
92            TermBackend::Crossterm(t) => t.hide_cursor(),
93            TermBackend::Test { backend, .. } => backend.hide_cursor(),
94        }
95    }
96
97    fn show_cursor(&mut self) -> io::Result<()> {
98        match self {
99            TermBackend::Crossterm(t) => t.show_cursor(),
100            TermBackend::Test { backend, .. } => backend.show_cursor(),
101        }
102    }
103
104    fn get_cursor_position(&mut self) -> io::Result<Position> {
105        match self {
106            TermBackend::Crossterm(t) => t.get_cursor_position(),
107            TermBackend::Test { backend, .. } => backend.get_cursor_position(),
108        }
109    }
110
111    fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
112        match self {
113            TermBackend::Crossterm(t) => t.set_cursor_position(position),
114            TermBackend::Test { backend, .. } => backend.set_cursor_position(position),
115        }
116    }
117
118    fn clear(&mut self) -> io::Result<()> {
119        match self {
120            TermBackend::Crossterm(t) => t.clear(),
121            TermBackend::Test { backend, .. } => backend.clear(),
122        }
123    }
124
125    fn size(&self) -> io::Result<Size> {
126        match self {
127            TermBackend::Crossterm(t) => t.size(),
128            TermBackend::Test { backend, .. } => backend.size(),
129        }
130    }
131
132    fn window_size(&mut self) -> io::Result<WindowSize> {
133        match self {
134            TermBackend::Crossterm(t) => t.window_size(),
135            TermBackend::Test { backend, .. } => backend.window_size(),
136        }
137    }
138
139    fn flush(&mut self) -> io::Result<()> {
140        match self {
141            TermBackend::Crossterm(t) => t.flush(),
142            TermBackend::Test { backend, .. } => backend.flush(),
143        }
144    }
145}
146
147impl TermBackend {
148    pub fn enter_alternate_screen(&mut self) -> Res<()> {
149        match self {
150            TermBackend::Crossterm(c) => c
151                .execute(EnterAlternateScreen)
152                .map_err(Error::Term)
153                .map(|_| ()),
154            TermBackend::Test { .. } => Ok(()),
155        }
156    }
157
158    pub fn enable_raw_mode(&self) -> Res<()> {
159        match self {
160            TermBackend::Crossterm(_) => enable_raw_mode().map_err(Error::Term),
161            TermBackend::Test { .. } => Ok(()),
162        }
163    }
164
165    pub fn disable_raw_mode(&self) -> Res<()> {
166        match self {
167            TermBackend::Crossterm(_) => disable_raw_mode().map_err(Error::Term),
168            TermBackend::Test { .. } => Ok(()),
169        }
170    }
171
172    pub fn poll_event(&self, timeout: Duration) -> Res<bool> {
173        match self {
174            TermBackend::Crossterm(_) => crossterm::event::poll(timeout).map_err(Error::Term),
175            TermBackend::Test { events, .. } => {
176                if events.is_empty() {
177                    Err(Error::NoMoreEvents)
178                } else {
179                    Ok(true)
180                }
181            }
182        }
183    }
184
185    pub fn read_event(&mut self) -> Res<Event> {
186        match self {
187            TermBackend::Crossterm(_) => crossterm::event::read().map_err(Error::Term),
188            TermBackend::Test { events, .. } => events.pop().ok_or(Error::NoMoreEvents),
189        }
190    }
191}