Skip to main content

flywheel/widget/
terminal.rs

1//! Terminal Widget: Embedded terminal emulator for Flywheel.
2//!
3//! This widget uses `vt100` to provide a full terminal emulation
4//! within a Flywheel widget. It handles ANSI escape sequences,
5//! colors, and scrolling.
6
7use crate::buffer::{Buffer, Cell, Rgb};
8use crate::layout::Rect;
9use crate::actor::InputEvent;
10use crate::widget::Widget;
11use std::sync::{Arc, Mutex};
12
13/// A terminal emulator widget.
14pub struct Terminal {
15    bounds: Rect,
16    parser: Arc<Mutex<vt100::Parser>>,
17    needs_redraw: bool,
18}
19
20impl Terminal {
21    /// Create a new terminal widget with the given bounds.
22    pub fn new(bounds: Rect) -> Self {
23        Self {
24            bounds,
25            parser: Arc::new(Mutex::new(vt100::Parser::new(bounds.height, bounds.width, 0))),
26            needs_redraw: true,
27        }
28    }
29
30    /// Process a chunk of bytes through the terminal emulator.
31    pub fn write(&mut self, data: &[u8]) {
32        if let Ok(mut parser) = self.parser.lock() {
33            parser.process(data);
34            self.needs_redraw = true;
35        }
36    }
37
38    /// Clear the terminal content.
39    pub fn clear(&mut self) {
40        if let Ok(mut parser) = self.parser.lock() {
41            *parser = vt100::Parser::new(self.bounds.height, self.bounds.width, 0);
42            self.needs_redraw = true;
43        }
44    }
45}
46
47impl Widget for Terminal {
48    fn bounds(&self) -> Rect {
49        self.bounds
50    }
51
52    fn set_bounds(&mut self, bounds: Rect) {
53        if bounds != self.bounds {
54            self.bounds = bounds;
55            if let Ok(mut parser) = self.parser.lock() {
56                parser.set_size(bounds.height, bounds.width);
57            }
58            self.needs_redraw = true;
59        }
60    }
61
62    #[allow(clippy::cast_possible_truncation)]
63    fn render(&self, buffer: &mut Buffer) {
64        let Ok(parser) = self.parser.lock() else { return };
65        let screen = parser.screen();
66
67        for y in 0..self.bounds.height {
68            for x in 0..self.bounds.width {
69                if let Some(cell) = screen.cell(y, x) {
70                    let mut fl_cell = Cell::from_char(cell.contents().chars().next().unwrap_or(' '));
71                    
72                    // FG Color
73                    match cell.fgcolor() {
74                        vt100::Color::Rgb(r, g, b) => {
75                            fl_cell.set_fg(Rgb::new(r, g, b));
76                        }
77                        vt100::Color::Idx(i) => {
78                            fl_cell.set_fg(ansi_to_rgb(i));
79                        }
80                        vt100::Color::Default => {}
81                    }
82                    
83                    // BG Color
84                    match cell.bgcolor() {
85                        vt100::Color::Rgb(r, g, b) => {
86                            fl_cell.set_bg(Rgb::new(r, g, b));
87                        }
88                        vt100::Color::Idx(i) => {
89                            fl_cell.set_bg(ansi_to_rgb(i));
90                        }
91                        vt100::Color::Default => {}
92                    }
93                    
94                    buffer.set(self.bounds.x + x, self.bounds.y + y, fl_cell);
95                }
96            }
97        }
98    }
99
100    fn handle_input(&mut self, _event: &InputEvent) -> bool {
101        // Terminal doesn't handle input locally by default, 
102        // it just consumes it if it's targeted?
103        // Actually, the caller usually maps keys to bytes and writes to the PTY.
104        false
105    }
106
107    fn needs_redraw(&self) -> bool {
108        self.needs_redraw
109    }
110
111    fn clear_redraw(&mut self) {
112        self.needs_redraw = false;
113    }
114}
115
116/// Convert ANSI color index to RGB.
117const fn ansi_to_rgb(idx: u8) -> Rgb {
118    match idx {
119        0 => Rgb::new(0, 0, 0),
120        1 => Rgb::new(128, 0, 0),
121        2 => Rgb::new(0, 128, 0),
122        3 => Rgb::new(128, 128, 0),
123        4 => Rgb::new(0, 0, 128),
124        5 => Rgb::new(128, 0, 128),
125        6 => Rgb::new(0, 128, 128),
126        7 => Rgb::new(192, 192, 192),
127        8 => Rgb::new(128, 128, 128),
128        9 => Rgb::new(255, 0, 0),
129        10 => Rgb::new(0, 255, 0),
130        11 => Rgb::new(255, 255, 0),
131        12 => Rgb::new(0, 0, 255),
132        13 => Rgb::new(255, 0, 255),
133        14 => Rgb::new(0, 255, 255),
134        15 => Rgb::new(255, 255, 255),
135        16..=231 => {
136            let i = idx - 16;
137            let r = (i / 36) % 6;
138            let g = (i / 6) % 6;
139            let b = i % 6;
140            Rgb::new(
141                if r == 0 { 0 } else { r * 40 + 55 },
142                if g == 0 { 0 } else { g * 40 + 55 },
143                if b == 0 { 0 } else { b * 40 + 55 },
144            )
145        }
146        232..=255 => {
147            let v = (idx - 232) * 10 + 8;
148            Rgb::new(v, v, v)
149        }
150    }
151}