dreg_crossterm/
lib.rs

1//! Crossterm Platform
2
3
4use std::io::{stdout, Write};
5
6use crossterm::{
7    cursor::{Hide, MoveTo, Show},
8    event::{
9        DisableMouseCapture, EnableMouseCapture,
10        Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, KeyboardEnhancementFlags,
11        ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind,
12        PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
13    },
14    execute,
15    queue,
16    style::{
17        Attribute as CtAttribute,
18        Attributes as CtAttributes,
19        Color as CColor, Colors, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor
20    },
21    terminal::{
22        disable_raw_mode, enable_raw_mode, Clear, EnterAlternateScreen, LeaveAlternateScreen
23    },
24    ExecutableCommand as _,
25};
26use dreg_core::prelude::*;
27
28
29
30pub mod prelude {
31    pub extern crate crossterm;
32    pub use crate::CrosstermPlatform;
33}
34
35
36
37pub struct CrosstermPlatform {
38    ctx: Context,
39
40    /// Holds the results of the current and previous draw calls. The two are compared at the end
41    /// of each draw pass to output the necessary updates to the terminal
42    buffers: [Buffer; 2],
43    /// Index of the current buffer in the previous array
44    current: usize,
45
46    viewport_area: Rect,
47    last_known_size: Rect,
48}
49
50impl Platform for CrosstermPlatform {
51    fn run(mut self, mut program: impl Program) -> Result<()> {
52        bind_terminal()?;
53        while !program.should_exit() {
54            if crossterm::event::poll(std::time::Duration::from_millis(31))? {
55                let event = crossterm::event::read()?;
56                handle_crossterm_event(&mut self.ctx, event);
57            }
58
59            self.autoresize()?;
60
61            let size = self.size()?;
62            let frame = Frame {
63                context: &mut self.ctx,
64                area: size,
65                buffer: &mut self.buffers[self.current],
66            };
67
68            program.update(frame);
69            self.flush()?;
70            self.swap_buffers();
71            stdout().flush()?;
72        }
73        release_terminal()?;
74
75        Ok(())
76    }
77}
78
79impl CrosstermPlatform {
80    pub fn new() -> Result<Self> {
81        let (width, height) = crossterm::terminal::size()?;
82        let size = Rect::new(0, 0, width, height);
83
84        Ok(Self {
85            ctx: Context::default(),
86            buffers: [Buffer::empty(size), Buffer::empty(size)],
87            current: 0,
88            viewport_area: size,
89            last_known_size: size,
90        })
91    }
92
93    pub fn size(&self) -> std::io::Result<Rect> {
94        let (width, height) = crossterm::terminal::size()?;
95        Ok(Rect::new(0, 0, width, height))
96    }
97
98    fn resize(&mut self, size: Rect) -> std::io::Result<()> {
99        self.set_viewport_area(size);
100        execute!(stdout(), Clear(crossterm::terminal::ClearType::All))?;
101
102        self.last_known_size = size;
103        Ok(())
104    }
105
106    fn set_viewport_area(&mut self, area: Rect) {
107        self.buffers[self.current].resize(area);
108        self.buffers[1 - self.current].resize(area);
109        self.viewport_area = area;
110    }
111
112    /// Resizes if the terminal's size doesn't match the previous size.
113    fn autoresize(&mut self) -> std::io::Result<()> {
114        let size = self.size()?;
115        if size != self.last_known_size {
116            self.resize(size)?;
117        }
118        Ok(())
119    }
120
121    /// Clear the inactive buffer and swap it with the current buffer.
122    fn swap_buffers(&mut self) {
123        self.buffers[1 - self.current].reset();
124        self.current = 1 - self.current;
125    }
126
127    fn flush(&mut self) -> std::io::Result<()> {
128        let previous_buffer = &self.buffers[1 - self.current];
129        let current_buffer = &self.buffers[self.current];
130        let updates = previous_buffer.diff(current_buffer);
131        if let Some((col, row, _)) = updates.last() {
132            // self.last_known_cursor_pos = (*col, *row);
133        }
134        let content = updates.into_iter();
135
136        let mut writer = stdout();
137        let mut fg = Color::Reset;
138        let mut bg = Color::Reset;
139        #[cfg(feature = "underline-color")]
140        let mut underline_color = Color::Reset;
141        let mut modifier = Modifier::empty();
142        let mut last_pos: Option<(u16, u16)> = None;
143        for (x, y, cell) in content {
144            // Move the cursor if the previous location was not (x - 1, y)
145            if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
146                queue!(writer, MoveTo(x, y))?;
147            }
148            last_pos = Some((x, y));
149            if cell.modifier != modifier {
150                let diff = ModifierDiff {
151                    from: modifier,
152                    to: cell.modifier,
153                };
154                diff.queue(&mut writer)?;
155                modifier = cell.modifier;
156            }
157            if cell.fg != fg || cell.bg != bg {
158                queue!(
159                    writer,
160                    SetColors(Colors::new(
161                        dreg_color_to_crossterm_color(cell.fg),
162                        dreg_color_to_crossterm_color(cell.bg),
163                    ))
164                )?;
165                fg = cell.fg;
166                bg = cell.bg;
167            }
168            #[cfg(feature = "underline-color")]
169            if cell.underline_color != underline_color {
170                let color = dreg_color_to_crossterm_color(cell.underline_color);
171                queue!(writer, SetUnderlineColor(color))?;
172                underline_color = cell.underline_color;
173            }
174
175            queue!(writer, Print(cell.symbol()))?;
176        }
177
178        #[cfg(feature = "underline-color")]
179        return queue!(
180            writer,
181            SetForegroundColor(CColor::Reset),
182            SetBackgroundColor(CColor::Reset),
183            SetUnderlineColor(CColor::Reset),
184            SetAttribute(CtAttribute::Reset),
185        );
186        #[cfg(not(feature = "underline-color"))]
187        return queue!(
188            writer,
189            SetForegroundColor(CColor::Reset),
190            SetBackgroundColor(CColor::Reset),
191            SetAttribute(CtAttribute::Reset),
192        );
193    }
194}
195
196
197
198fn bind_terminal() -> Result<()> {
199    let mut writer = stdout();
200    enable_raw_mode()?;
201    writer.execute(EnableMouseCapture)?;
202    writer.execute(EnterAlternateScreen)?;
203    writer.execute(PushKeyboardEnhancementFlags(
204        KeyboardEnhancementFlags::REPORT_EVENT_TYPES
205    ))?;
206    writer.execute(Hide)?;
207    let original_hook = std::panic::take_hook();
208    std::panic::set_hook(Box::new(move |panic| {
209        release_terminal().unwrap();
210        original_hook(panic);
211    }));
212
213    Ok(())
214}
215
216fn release_terminal() -> Result<()> {
217    let mut writer = stdout();
218    disable_raw_mode()?;
219    writer.execute(DisableMouseCapture)?;
220    writer.execute(LeaveAlternateScreen)?;
221    writer.execute(PopKeyboardEnhancementFlags)?;
222    writer.execute(Show)?;
223
224    Ok(())
225}
226
227
228
229fn handle_crossterm_event(ctx: &mut Context, event: Event) {
230    match event {
231        Event::Key(KeyEvent { code, modifiers, kind, state }) => {
232            let mut scancodes = vec![];
233            if modifiers != KeyModifiers::NONE {
234                for m in modifiers.iter() {
235                    match m {
236                        KeyModifiers::SHIFT => scancodes.push(Scancode::L_SHIFT),
237                        KeyModifiers::ALT => scancodes.push(Scancode::L_ALT),
238                        KeyModifiers::CONTROL => scancodes.push(Scancode::L_CTRL),
239                        _ => {} // TODO: Handle other modifiers.
240                    }
241                }
242            }
243            scancodes.extend(translate_keycode(code));
244            match kind {
245                KeyEventKind::Press => {
246                    for scancode in scancodes {
247                        ctx.handle_key_down(scancode);
248                    }
249                }
250                KeyEventKind::Release => {
251                    for scancode in scancodes {
252                        ctx.handle_key_up(&scancode);
253                    }
254                }
255                _ => {} // Do nothing.
256            }
257        }
258        Event::Mouse(MouseEvent { kind, column, row, .. }) => {
259            match kind {
260                MouseEventKind::Moved | MouseEventKind::Drag(_) => {
261                    ctx.handle_input(Input::MouseMove(column, row));
262                }
263                MouseEventKind::Down(btn) => {
264                    let code = match btn {
265                        MouseButton::Left => Scancode::LMB,
266                        MouseButton::Right => Scancode::RMB,
267                        MouseButton::Middle => Scancode::MMB,
268                    };
269                    ctx.handle_key_down(code);
270                }
271                MouseEventKind::Up(btn) => {
272                    let code = match btn {
273                        MouseButton::Left => Scancode::LMB,
274                        MouseButton::Right => Scancode::RMB,
275                        MouseButton::Middle => Scancode::MMB,
276                    };
277                    ctx.handle_key_up(&code);
278                }
279                _ => {} // TODO: Handle scroll wheel events.
280            }
281        }
282        Event::FocusGained => {
283            ctx.handle_input(Input::FocusChange(true));
284        }
285        Event::FocusLost => {
286            ctx.handle_input(Input::FocusChange(false));
287        }
288        Event::Resize(new_cols, new_rows) => {
289            ctx.handle_input(Input::Resize(new_cols, new_rows));
290        }
291        _ => {}
292    }
293}
294
295fn translate_keycode(code: KeyCode) -> Vec<Scancode> {
296    // All of `crossterm`'s keycodes translate to 2 or less scancodes.
297    let mut scancodes = Vec::with_capacity(2);
298    match code {
299        KeyCode::Char(c) => {
300            let (modifier, scancode) = Scancode::from_char(c);
301            if let Some(mod_code) = modifier {
302                scancodes.push(mod_code);
303            }
304            scancodes.push(scancode);
305        }
306        KeyCode::F(n) => {
307            scancodes.push(match n {
308                1 => Scancode::F1,
309                2 => Scancode::F2,
310                3 => Scancode::F3,
311                4 => Scancode::F4,
312                5 => Scancode::F5,
313                6 => Scancode::F6,
314                7 => Scancode::F7,
315                8 => Scancode::F8,
316                9 => Scancode::F9,
317                10 => Scancode::F10,
318                _ => Scancode::NULL,
319            })
320        }
321        KeyCode::Modifier(mod_keycode) => match mod_keycode {
322            ModifierKeyCode::LeftShift => { scancodes.push(Scancode::L_SHIFT); },
323            ModifierKeyCode::LeftAlt => { scancodes.push(Scancode::L_ALT); },
324            ModifierKeyCode::LeftControl => { scancodes.push(Scancode::L_CTRL); },
325
326            ModifierKeyCode::RightShift => { scancodes.push(Scancode::R_SHIFT); },
327            ModifierKeyCode::RightAlt => { scancodes.push(Scancode::R_ALT); },
328            ModifierKeyCode::RightControl => { scancodes.push(Scancode::R_CTRL); },
329
330            _ => {} // TODO: Handle other modifiers.
331        }
332
333        KeyCode::Esc => { scancodes.push(Scancode::ESC); },
334        KeyCode::Backspace => { scancodes.push(Scancode::BACKSPACE); }
335        KeyCode::Tab => { scancodes.push(Scancode::TAB); },
336        KeyCode::BackTab => { scancodes.extend([Scancode::L_SHIFT, Scancode::TAB]); }
337        KeyCode::Enter => { scancodes.push(Scancode::ENTER); },
338        KeyCode::Delete => { scancodes.push(Scancode::DELETE); },
339        KeyCode::Insert => { scancodes.push(Scancode::INSERT); },
340        KeyCode::CapsLock => { scancodes.push(Scancode::CAPSLOCK); },
341
342        KeyCode::Left => { scancodes.push(Scancode::LEFT); },
343        KeyCode::Right => { scancodes.push(Scancode::RIGHT); },
344        KeyCode::Up => { scancodes.push(Scancode::UP); },
345        KeyCode::Down => { scancodes.push(Scancode::DOWN); },
346
347        KeyCode::Home => { scancodes.push(Scancode::HOME); },
348        KeyCode::End => { scancodes.push(Scancode::END); },
349        KeyCode::PageUp => { scancodes.push(Scancode::PAGEDOWN); },
350        KeyCode::PageDown => { scancodes.push(Scancode::PAGEUP); },
351
352        _ => {}
353    }
354
355    scancodes
356}
357
358
359
360/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier` values.
361/// This is useful when updating the terminal display, as it allows for more efficient updates by
362/// only sending the necessary changes.
363struct ModifierDiff {
364    pub from: Modifier,
365    pub to: Modifier,
366}
367
368impl ModifierDiff {
369    fn queue<W: std::io::Write>(self, mut w: W) -> std::io::Result<()> {
370        //use crossterm::Attribute;
371        let removed = self.from - self.to;
372        if removed.contains(Modifier::REVERSED) {
373            queue!(w, SetAttribute(CtAttribute::NoReverse))?;
374        }
375        if removed.contains(Modifier::BOLD) {
376            queue!(w, SetAttribute(CtAttribute::NormalIntensity))?;
377            if self.to.contains(Modifier::DIM) {
378                queue!(w, SetAttribute(CtAttribute::Dim))?;
379            }
380        }
381        if removed.contains(Modifier::ITALIC) {
382            queue!(w, SetAttribute(CtAttribute::NoItalic))?;
383        }
384        if removed.contains(Modifier::UNDERLINED) {
385            queue!(w, SetAttribute(CtAttribute::NoUnderline))?;
386        }
387        if removed.contains(Modifier::DIM) {
388            queue!(w, SetAttribute(CtAttribute::NormalIntensity))?;
389        }
390        if removed.contains(Modifier::CROSSED_OUT) {
391            queue!(w, SetAttribute(CtAttribute::NotCrossedOut))?;
392        }
393        if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
394            queue!(w, SetAttribute(CtAttribute::NoBlink))?;
395        }
396
397        let added = self.to - self.from;
398        if added.contains(Modifier::REVERSED) {
399            queue!(w, SetAttribute(CtAttribute::Reverse))?;
400        }
401        if added.contains(Modifier::BOLD) {
402            queue!(w, SetAttribute(CtAttribute::Bold))?;
403        }
404        if added.contains(Modifier::ITALIC) {
405            queue!(w, SetAttribute(CtAttribute::Italic))?;
406        }
407        if added.contains(Modifier::UNDERLINED) {
408            queue!(w, SetAttribute(CtAttribute::Underlined))?;
409        }
410        if added.contains(Modifier::DIM) {
411            queue!(w, SetAttribute(CtAttribute::Dim))?;
412        }
413        if added.contains(Modifier::CROSSED_OUT) {
414            queue!(w, SetAttribute(CtAttribute::CrossedOut))?;
415        }
416        if added.contains(Modifier::SLOW_BLINK) {
417            queue!(w, SetAttribute(CtAttribute::SlowBlink))?;
418        }
419        if added.contains(Modifier::RAPID_BLINK) {
420            queue!(w, SetAttribute(CtAttribute::RapidBlink))?;
421        }
422
423        Ok(())
424    }
425}
426
427fn translate_attribute(value: CtAttribute) -> Modifier {
428    // `Attribute*s*` (note the *s*) contains multiple `Attribute`
429    // We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
430    // the conversion again
431    translate_attributes(CtAttributes::from(value))
432}
433
434fn translate_attributes(value: CtAttributes) -> Modifier {
435    let mut res = Modifier::empty();
436
437    if value.has(CtAttribute::Bold) {
438        res |= Modifier::BOLD;
439    }
440    if value.has(CtAttribute::Dim) {
441        res |= Modifier::DIM;
442    }
443    if value.has(CtAttribute::Italic) {
444        res |= Modifier::ITALIC;
445    }
446    if value.has(CtAttribute::Underlined)
447        || value.has(CtAttribute::DoubleUnderlined)
448        || value.has(CtAttribute::Undercurled)
449        || value.has(CtAttribute::Underdotted)
450        || value.has(CtAttribute::Underdashed)
451    {
452        res |= Modifier::UNDERLINED;
453    }
454    if value.has(CtAttribute::SlowBlink) {
455        res |= Modifier::SLOW_BLINK;
456    }
457    if value.has(CtAttribute::RapidBlink) {
458        res |= Modifier::RAPID_BLINK;
459    }
460    if value.has(CtAttribute::Reverse) {
461        res |= Modifier::REVERSED;
462    }
463    if value.has(CtAttribute::Hidden) {
464        res |= Modifier::HIDDEN;
465    }
466    if value.has(CtAttribute::CrossedOut) {
467        res |= Modifier::CROSSED_OUT;
468    }
469
470    res
471}
472
473
474
475fn dreg_color_to_crossterm_color(color: Color) -> CColor {
476    match color {
477        Color::Reset => CColor::Reset,
478        Color::Black => CColor::Black,
479        Color::Red => CColor::DarkRed,
480        Color::Green => CColor::DarkGreen,
481        Color::Yellow => CColor::DarkYellow,
482        Color::Blue => CColor::DarkBlue,
483        Color::Magenta => CColor::DarkMagenta,
484        Color::Cyan => CColor::DarkCyan,
485        Color::Gray => CColor::Grey,
486        Color::DarkGray => CColor::DarkGrey,
487        Color::LightRed => CColor::Red,
488        Color::LightGreen => CColor::Green,
489        Color::LightBlue => CColor::Blue,
490        Color::LightYellow => CColor::Yellow,
491        Color::LightMagenta => CColor::Magenta,
492        Color::LightCyan => CColor::Cyan,
493        Color::White => CColor::White,
494        Color::Indexed(i) => CColor::AnsiValue(i),
495        Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
496    }
497}
498
499fn crossterm_color_to_dreg_color(value: CColor) -> Color {
500    match value {
501        CColor::Reset => Color::Reset,
502        CColor::Black => Color::Black,
503        CColor::DarkRed => Color::Red,
504        CColor::DarkGreen => Color::Green,
505        CColor::DarkYellow => Color::Yellow,
506        CColor::DarkBlue => Color::Blue,
507        CColor::DarkMagenta => Color::Magenta,
508        CColor::DarkCyan => Color::Cyan,
509        CColor::Grey => Color::Gray,
510        CColor::DarkGrey => Color::DarkGray,
511        CColor::Red => Color::LightRed,
512        CColor::Green => Color::LightGreen,
513        CColor::Blue => Color::LightBlue,
514        CColor::Yellow => Color::LightYellow,
515        CColor::Magenta => Color::LightMagenta,
516        CColor::Cyan => Color::LightCyan,
517        CColor::White => Color::White,
518        CColor::Rgb { r, g, b } => Color::Rgb(r, g, b),
519        CColor::AnsiValue(v) => Color::Indexed(v),
520    }
521}