1use crate::style::{Color, Modifier, Style};
2use crate::{Res, config::Config, error::Error};
3use crossterm::{
4 QueueableCommand,
5 cursor::{self, MoveTo},
6 event::{DisableMouseCapture, EnableMouseCapture, Event},
7 style::{Attribute, Colors, Print, SetAttribute, SetColors},
8 terminal::{
9 Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
10 enable_raw_mode,
11 },
12};
13use std::io::{self, Stdout, Write, stdout};
14use std::time::Duration;
15use unicode_segmentation::UnicodeSegmentation;
16use unicode_width::UnicodeWidthStr;
17
18pub type Term = TermBackend;
19
20pub fn backend() -> TermBackend {
21 TermBackend::Crossterm(stdout())
22}
23
24pub enum TermBackend {
25 Crossterm(Stdout),
26 #[allow(dead_code)]
27 Test {
28 buffer: TestBuffer,
29 events: Vec<Event>,
30 },
31}
32
33#[derive(Clone, PartialEq)]
34pub struct TestCell {
35 pub symbol: String,
36 pub link: Option<String>,
38 pub fg: Color,
39 pub bg: Color,
40 pub modifier: Modifier,
41}
42
43impl Default for TestCell {
44 fn default() -> Self {
45 TestCell {
46 symbol: " ".to_string(),
47 link: None,
48 fg: Color::Reset,
49 bg: Color::Reset,
50 modifier: Modifier::empty(),
51 }
52 }
53}
54
55impl TestCell {
56 fn set(&mut self, symbol: &str, style: &Style) {
57 self.symbol.clear();
58 self.symbol.push_str(symbol);
59
60 if let Some(fg) = style.fg {
61 self.fg = fg;
62 }
63 if let Some(bg) = style.bg {
64 self.bg = bg;
65 }
66 self.modifier.insert(style.add_modifier);
67 self.modifier.remove(style.sub_modifier);
68 }
69}
70
71pub struct TestBuffer {
72 pub cells: Vec<TestCell>,
73 pub width: u16,
74 pub height: u16,
75 cursor: (u16, u16),
76 link: Option<String>,
78}
79
80impl TestBuffer {
81 pub fn new(width: u16, height: u16) -> Self {
82 TestBuffer {
83 cells: vec![TestCell::default(); width as usize * height as usize],
84 width,
85 height,
86 cursor: (0, 0),
87 link: None,
88 }
89 }
90
91 fn clear(&mut self) {
92 self.cells.fill(TestCell::default());
93 }
94
95 fn cell_mut(&mut self, x: u16, y: u16) -> Option<&mut TestCell> {
96 if x >= self.width || y >= self.height {
97 return None;
98 }
99
100 self.cells
101 .get_mut(y as usize * self.width as usize + x as usize)
102 }
103
104 fn print(&mut self, text: &str, style: &Style) {
105 let (mut x, y) = self.cursor;
106
107 for grapheme in text.graphemes(true) {
108 let grapheme_width = grapheme.width() as u16;
109 if grapheme_width == 0 || x >= self.width {
110 continue;
111 }
112
113 let link = self.link.clone();
114 if let Some(cell) = self.cell_mut(x, y) {
115 *cell = TestCell::default();
116 cell.set(grapheme, style);
117 cell.link = link;
118 }
119 x += 1;
120
121 for _ in 1..grapheme_width {
122 if x >= self.width {
123 break;
124 }
125
126 if let Some(cell) = self.cell_mut(x, y) {
127 *cell = TestCell::default();
128 }
129 x += 1;
130 }
131 }
132
133 self.cursor = (x, y);
134 }
135}
136
137impl TermBackend {
138 pub(crate) fn queue_move_cursor(&mut self, x: u16, y: u16) -> Res<()> {
139 match self {
140 TermBackend::Crossterm(t) => crossterm::queue!(t, MoveTo(x, y)).map_err(Error::Term),
141 TermBackend::Test { buffer, .. } => {
142 buffer.cursor = (x, y);
143 Ok(())
144 }
145 }
146 }
147
148 pub(crate) fn queue_link(&mut self, uri: Option<&str>) -> Res<()> {
152 match self {
153 TermBackend::Crossterm(t) => {
154 let uri = uri.unwrap_or("");
155 crossterm::queue!(t, Print(format!("\x1b]8;;{uri}\x1b\\"))).map_err(Error::Term)
156 }
157 TermBackend::Test { buffer, .. } => {
158 buffer.link = uri.map(str::to_owned);
159 Ok(())
160 }
161 }
162 }
163
164 pub fn queue_print(&mut self, text: &str, style: &Style) -> Res<()> {
165 match self {
166 TermBackend::Crossterm(t) => {
167 print_crossterm_span(text, style, t).map_err(Error::Term)?;
168
169 Ok(())
170 }
171 TermBackend::Test { buffer, .. } => {
172 buffer.print(text, style);
173 Ok(())
174 }
175 }
176 }
177}
178
179const ATTRS: &[(Modifier, Attribute)] = &[
180 (Modifier::BOLD, Attribute::Bold),
181 (Modifier::DIM, Attribute::Dim),
182 (Modifier::ITALIC, Attribute::Italic),
183 (Modifier::UNDERLINED, Attribute::Underlined),
184 (Modifier::SLOW_BLINK, Attribute::SlowBlink),
185 (Modifier::RAPID_BLINK, Attribute::RapidBlink),
186 (Modifier::REVERSED, Attribute::Reverse),
187 (Modifier::HIDDEN, Attribute::Hidden),
188 (Modifier::CROSSED_OUT, Attribute::CrossedOut),
189];
190
191fn print_crossterm_span(text: &str, style: &Style, t: &mut Stdout) -> Result<(), io::Error> {
192 let fg = style.fg.unwrap_or(Color::Reset);
193 let bg = style.bg.unwrap_or(Color::Reset);
194
195 crossterm::queue!(t, SetAttribute(Attribute::Reset))?;
196
197 for (modifier, attribute) in ATTRS {
198 if style.add_modifier.contains(*modifier) {
199 crossterm::queue!(t, SetAttribute(*attribute))?;
200 }
201 }
202
203 crossterm::queue!(t, SetColors(Colors::new(fg.into(), bg.into())))?;
204 crossterm::queue!(t, Print(text))?;
205 Ok(())
206}
207
208impl TermBackend {
209 pub(crate) fn size(&self) -> io::Result<(u16, u16)> {
210 match self {
211 TermBackend::Crossterm(_) => crossterm::terminal::size(),
212 TermBackend::Test { buffer, .. } => Ok((buffer.width, buffer.height)),
213 }
214 }
215
216 pub(crate) fn clear(&mut self) -> io::Result<()> {
217 match self {
218 TermBackend::Crossterm(t) => crossterm::queue!(t, Clear(ClearType::All)),
219 TermBackend::Test { buffer, .. } => {
220 buffer.clear();
221 Ok(())
222 }
223 }
224 }
225
226 pub(crate) fn flush(&mut self) -> io::Result<()> {
227 match self {
228 TermBackend::Crossterm(t) => t.flush(),
229 TermBackend::Test { .. } => Ok(()),
230 }
231 }
232}
233
234impl TermBackend {
235 pub fn setup_term(&mut self, config: &Config) -> io::Result<()> {
236 match self {
237 TermBackend::Crossterm(crossterm_backend) => {
238 enable_raw_mode()?;
239 crossterm_backend.queue(EnterAlternateScreen)?;
240 crossterm_backend.queue(cursor::Hide)?;
241 if config.general.mouse_support {
242 crossterm_backend.queue(EnableMouseCapture)?;
243 }
244 }
245 TermBackend::Test { .. } => {}
246 }
247
248 self.flush()
249 }
250
251 pub fn reset_term(&mut self, config: &Config) -> io::Result<()> {
252 match self {
253 TermBackend::Crossterm(crossterm_backend) => {
254 if config.general.mouse_support {
255 crossterm_backend.queue(DisableMouseCapture)?;
256 }
257 crossterm_backend.queue(cursor::Show)?;
258 crossterm_backend.queue(LeaveAlternateScreen)?;
259 disable_raw_mode()?;
260 }
261 TermBackend::Test { .. } => {}
262 }
263
264 self.flush()
265 }
266
267 pub(crate) fn reset_term_stay_on_alt_screeen(
268 &mut self,
269 config: &Config,
270 ) -> Result<(), io::Error> {
271 match self {
272 TermBackend::Crossterm(crossterm_backend) => {
273 if config.general.mouse_support {
274 crossterm_backend.queue(DisableMouseCapture)?;
275 }
276 crossterm_backend.queue(cursor::Show)?;
277 disable_raw_mode()?;
278 }
279 TermBackend::Test { .. } => {}
280 }
281
282 self.flush()
283 }
284
285 pub fn poll_event(&self, timeout: Duration) -> Res<bool> {
286 match self {
287 TermBackend::Crossterm(_) => crossterm::event::poll(timeout).map_err(Error::Term),
288 TermBackend::Test { events, .. } => {
289 if events.is_empty() {
290 Err(Error::NoMoreEvents)
291 } else {
292 Ok(true)
293 }
294 }
295 }
296 }
297
298 pub fn read_event(&mut self) -> Res<Event> {
299 match self {
300 TermBackend::Crossterm(_) => crossterm::event::read().map_err(Error::Term),
301 TermBackend::Test { events, .. } => events.pop().ok_or(Error::NoMoreEvents),
302 }
303 }
304}