1use std::io::{self, Stdout, Write};
2
3use crossterm::{
4 cursor::{Hide, MoveTo, Show},
5 event::{self, poll, Event},
6 execute, queue,
7 style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor},
8 terminal::{
9 self, disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
10 LeaveAlternateScreen,
11 },
12};
13
14use crate::buffer::Buffer;
15use crate::canvas::{encode_kitty_graphics, supports_kitty_graphics, PendingCanvas};
16use crate::image::{encode_kitty_image, PendingImage};
17use crate::render::{render_view, RenderContext};
18use crate::theme::current_theme;
19use crate::View;
20
21pub struct Terminal {
23 stdout: Stdout,
24 buffer: Buffer,
25 prev_buffer: Buffer,
26}
27
28impl Terminal {
29 pub fn new() -> io::Result<Self> {
32 enable_raw_mode()?;
33 let mut stdout = io::stdout();
34 execute!(stdout, EnterAlternateScreen, Hide, Clear(ClearType::All))?;
35
36 let (width, height) = terminal::size()?;
37 let buffer = Buffer::new(width, height);
38 let prev_buffer = Buffer::new(width, height);
39
40 Ok(Self {
41 stdout,
42 buffer,
43 prev_buffer,
44 })
45 }
46
47 pub fn draw(
49 &mut self,
50 view: &View,
51 focus_index: usize,
52 focus_visible: bool,
53 scroll_offsets: Vec<(u16, u16)>,
54 cursor_offsets: Vec<usize>,
55 modal_visible: bool,
56 ) -> io::Result<Vec<(u16, u16)>> {
57 let (width, height) = terminal::size()?;
59 if width != self.buffer.width || height != self.buffer.height {
60 self.buffer = Buffer::new(width, height);
61 self.prev_buffer = Buffer::new(width, height);
62 execute!(self.stdout, Clear(ClearType::All))?;
64 }
65
66 let theme = current_theme();
68 self.buffer.fill(theme.foreground, theme.background);
69
70 let area = self.buffer.rect();
72 let mut ctx = RenderContext::new(focus_index, focus_visible, scroll_offsets, cursor_offsets, area);
73 ctx.set_modal_visible(modal_visible);
74 render_view(&mut self.buffer, view, area, &mut ctx);
75
76 ctx.render_pending_dropdowns(&mut self.buffer);
78
79 let pending_canvases = ctx.take_pending_canvases();
81 let pending_images = ctx.take_pending_images();
82
83 self.flush_diff()?;
85
86 if !pending_canvases.is_empty() {
88 self.flush_canvases(&pending_canvases)?;
89 }
90
91 if !pending_images.is_empty() {
93 self.flush_images(&pending_images)?;
94 }
95
96 std::mem::swap(&mut self.buffer, &mut self.prev_buffer);
98
99 Ok(ctx.scroll_offsets().to_vec())
101 }
102
103 pub fn height(&self) -> u16 {
105 self.buffer.height
106 }
107
108 fn flush_diff(&mut self) -> io::Result<()> {
110 let changes = self.buffer.diff(&self.prev_buffer);
111
112 for (x, y, cell) in changes {
113 if cell.wide_continuation {
116 continue;
117 }
118
119 queue!(self.stdout, MoveTo(x, y))?;
120
121 queue!(self.stdout, SetAttribute(Attribute::Reset))?;
123
124 if cell.bold {
126 queue!(self.stdout, SetAttribute(Attribute::Bold))?;
127 }
128 if cell.italic {
129 queue!(self.stdout, SetAttribute(Attribute::Italic))?;
130 }
131 if cell.underline {
132 queue!(self.stdout, SetAttribute(Attribute::Underlined))?;
133 }
134 if cell.dim {
135 queue!(self.stdout, SetAttribute(Attribute::Dim))?;
136 }
137
138 queue!(self.stdout, SetForegroundColor(cell.fg))?;
140 queue!(self.stdout, SetBackgroundColor(cell.bg))?;
141
142 queue!(self.stdout, Print(cell.ch))?;
143 }
144
145 queue!(self.stdout, SetAttribute(Attribute::Reset))?;
147 self.stdout.flush()?;
148 Ok(())
149 }
150
151 fn flush_canvases(&mut self, canvases: &[PendingCanvas]) -> io::Result<()> {
153 if !supports_kitty_graphics() {
154 return Ok(());
155 }
156
157 for canvas in canvases {
158 let escape_seq =
159 encode_kitty_graphics(&canvas.pixels, canvas.cell_x, canvas.cell_y, canvas.id);
160 self.stdout.write_all(escape_seq.as_bytes())?;
161 }
162
163 self.stdout.flush()?;
164 Ok(())
165 }
166
167 fn flush_images(&mut self, images: &[PendingImage]) -> io::Result<()> {
169 if !supports_kitty_graphics() {
170 return Ok(());
171 }
172
173 for image in images {
174 let escape_seq = encode_kitty_image(&image.data, image.cell_x, image.cell_y, image.id);
175 self.stdout.write_all(escape_seq.as_bytes())?;
176 }
177
178 self.stdout.flush()?;
179 Ok(())
180 }
181
182 pub fn poll_event(&self) -> io::Result<Option<Event>> {
184 if poll(std::time::Duration::from_millis(16))? {
186 Ok(Some(event::read()?))
187 } else {
188 Ok(None)
189 }
190 }
191
192 pub fn draw_debug(
194 &mut self,
195 frame: u64,
196 render_us: u64,
197 focus_idx: usize,
198 focusable_count: usize,
199 ) -> io::Result<()> {
200 let _ = (frame, render_us); let debug_text = format!(" Focus: {}/{} ", focus_idx, focusable_count);
202
203 let x = self
205 .buffer
206 .width
207 .saturating_sub(debug_text.len() as u16 + 1);
208 let y = 0; queue!(
211 self.stdout,
212 MoveTo(x, y),
213 SetForegroundColor(Color::Black),
214 SetBackgroundColor(Color::Yellow),
215 Print(&debug_text),
216 SetForegroundColor(Color::Reset),
217 SetBackgroundColor(Color::Reset)
218 )?;
219 self.stdout.flush()?;
220 Ok(())
221 }
222
223 pub fn cleanup(&mut self) -> io::Result<()> {
225 if supports_kitty_graphics() {
227 let delete_cmd = crate::canvas::delete_all_kitty_images();
228 let _ = self.stdout.write_all(delete_cmd.as_bytes());
229 }
230
231 execute!(
232 self.stdout,
233 Clear(ClearType::All),
234 SetForegroundColor(Color::Reset),
235 SetBackgroundColor(Color::Reset),
236 Show,
237 LeaveAlternateScreen
238 )?;
239 disable_raw_mode()?;
240 Ok(())
241 }
242}
243
244impl Drop for Terminal {
245 fn drop(&mut self) {
246 if supports_kitty_graphics() {
249 let delete_cmd = crate::canvas::delete_all_kitty_images();
250 let _ = self.stdout.write_all(delete_cmd.as_bytes());
251 }
252
253 let _ = execute!(
254 self.stdout,
255 Clear(ClearType::All),
256 SetForegroundColor(Color::Reset),
257 SetBackgroundColor(Color::Reset),
258 Show,
259 LeaveAlternateScreen
260 );
261 let _ = disable_raw_mode();
262 }
263}