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 headless: bool,
27}
28
29impl Terminal {
30 pub fn new() -> io::Result<Self> {
33 enable_raw_mode()?;
34 let mut stdout = io::stdout();
35 execute!(stdout, EnterAlternateScreen, Hide, Clear(ClearType::All))?;
36
37 let (width, height) = terminal::size()?;
38 let buffer = Buffer::new(width, height);
39 let prev_buffer = Buffer::new(width, height);
40
41 Ok(Self {
42 stdout,
43 buffer,
44 prev_buffer,
45 headless: false,
46 })
47 }
48
49 pub fn new_headless(width: u16, height: u16) -> Self {
52 let stdout = io::stdout();
53 let buffer = Buffer::new(width, height);
54 let prev_buffer = Buffer::new(width, height);
55
56 Self {
57 stdout,
58 buffer,
59 prev_buffer,
60 headless: true,
61 }
62 }
63
64 pub fn draw(
66 &mut self,
67 view: &View,
68 focus_index: usize,
69 focus_visible: bool,
70 scroll_offsets: Vec<(u16, u16)>,
71 cursor_offsets: Vec<usize>,
72 modal_visible: bool,
73 ) -> io::Result<Vec<(u16, u16)>> {
74 if !self.headless {
75 let (width, height) = terminal::size()?;
77 if width != self.buffer.width || height != self.buffer.height {
78 self.buffer = Buffer::new(width, height);
79 self.prev_buffer = Buffer::new(width, height);
80 execute!(self.stdout, Clear(ClearType::All))?;
82 }
83 }
84
85 let theme = current_theme();
87 self.buffer.fill(theme.foreground, theme.background);
88
89 let area = self.buffer.rect();
91 let mut ctx = RenderContext::new(focus_index, focus_visible, scroll_offsets, cursor_offsets, area);
92 ctx.set_modal_visible(modal_visible);
93 render_view(&mut self.buffer, view, area, &mut ctx);
94
95 ctx.render_pending_dropdowns(&mut self.buffer);
97
98 if !self.headless {
99 let pending_canvases = ctx.take_pending_canvases();
101 let pending_images = ctx.take_pending_images();
102
103 self.flush_diff()?;
105
106 if !pending_canvases.is_empty() {
108 self.flush_canvases(&pending_canvases)?;
109 }
110
111 if !pending_images.is_empty() {
113 self.flush_images(&pending_images)?;
114 }
115 }
116
117 std::mem::swap(&mut self.buffer, &mut self.prev_buffer);
119
120 Ok(ctx.scroll_offsets().to_vec())
122 }
123
124 pub fn height(&self) -> u16 {
126 self.buffer.height
127 }
128
129 pub fn width(&self) -> u16 {
131 self.buffer.width
132 }
133
134 pub fn buffer_string(&self) -> String {
137 self.prev_buffer.to_string()
138 }
139
140 fn flush_diff(&mut self) -> io::Result<()> {
142 let changes = self.buffer.diff(&self.prev_buffer);
143
144 for (x, y, cell) in changes {
145 if cell.wide_continuation {
148 continue;
149 }
150
151 queue!(self.stdout, MoveTo(x, y))?;
152
153 queue!(self.stdout, SetAttribute(Attribute::Reset))?;
155
156 if cell.bold {
158 queue!(self.stdout, SetAttribute(Attribute::Bold))?;
159 }
160 if cell.italic {
161 queue!(self.stdout, SetAttribute(Attribute::Italic))?;
162 }
163 if cell.underline {
164 queue!(self.stdout, SetAttribute(Attribute::Underlined))?;
165 }
166 if cell.dim {
167 queue!(self.stdout, SetAttribute(Attribute::Dim))?;
168 }
169
170 queue!(self.stdout, SetForegroundColor(cell.fg))?;
172 queue!(self.stdout, SetBackgroundColor(cell.bg))?;
173
174 queue!(self.stdout, Print(cell.ch))?;
175 }
176
177 queue!(self.stdout, SetAttribute(Attribute::Reset))?;
179 self.stdout.flush()?;
180 Ok(())
181 }
182
183 fn flush_canvases(&mut self, canvases: &[PendingCanvas]) -> io::Result<()> {
185 if !supports_kitty_graphics() {
186 return Ok(());
187 }
188
189 for canvas in canvases {
190 let escape_seq =
191 encode_kitty_graphics(&canvas.pixels, canvas.cell_x, canvas.cell_y, canvas.id);
192 self.stdout.write_all(escape_seq.as_bytes())?;
193 }
194
195 self.stdout.flush()?;
196 Ok(())
197 }
198
199 fn flush_images(&mut self, images: &[PendingImage]) -> io::Result<()> {
201 if !supports_kitty_graphics() {
202 return Ok(());
203 }
204
205 for image in images {
206 let escape_seq = encode_kitty_image(&image.data, image.cell_x, image.cell_y, image.id);
207 self.stdout.write_all(escape_seq.as_bytes())?;
208 }
209
210 self.stdout.flush()?;
211 Ok(())
212 }
213
214 pub fn poll_event(&self, timeout: std::time::Duration) -> io::Result<Option<Event>> {
216 if poll(timeout)? {
217 Ok(Some(event::read()?))
218 } else {
219 Ok(None)
220 }
221 }
222
223 pub fn draw_debug(
225 &mut self,
226 frame: u64,
227 render_us: u64,
228 focus_idx: usize,
229 focusable_count: usize,
230 ) -> io::Result<()> {
231 let _ = (frame, render_us); let debug_text = format!(" Focus: {}/{} ", focus_idx, focusable_count);
233
234 let x = self
236 .buffer
237 .width
238 .saturating_sub(debug_text.len() as u16 + 1);
239 let y = 0; queue!(
242 self.stdout,
243 MoveTo(x, y),
244 SetForegroundColor(Color::Black),
245 SetBackgroundColor(Color::Yellow),
246 Print(&debug_text),
247 SetForegroundColor(Color::Reset),
248 SetBackgroundColor(Color::Reset)
249 )?;
250 self.stdout.flush()?;
251 Ok(())
252 }
253
254 pub fn cleanup(&mut self) -> io::Result<()> {
256 if self.headless {
257 return Ok(());
258 }
259
260 if supports_kitty_graphics() {
262 let delete_cmd = crate::canvas::delete_all_kitty_images();
263 let _ = self.stdout.write_all(delete_cmd.as_bytes());
264 }
265
266 execute!(
267 self.stdout,
268 Clear(ClearType::All),
269 SetForegroundColor(Color::Reset),
270 SetBackgroundColor(Color::Reset),
271 Show,
272 LeaveAlternateScreen
273 )?;
274 disable_raw_mode()?;
275 Ok(())
276 }
277}
278
279impl Drop for Terminal {
280 fn drop(&mut self) {
281 if self.headless {
282 return;
283 }
284
285 if supports_kitty_graphics() {
288 let delete_cmd = crate::canvas::delete_all_kitty_images();
289 let _ = self.stdout.write_all(delete_cmd.as_bytes());
290 }
291
292 let _ = execute!(
293 self.stdout,
294 Clear(ClearType::All),
295 SetForegroundColor(Color::Reset),
296 SetBackgroundColor(Color::Reset),
297 Show,
298 LeaveAlternateScreen
299 );
300 let _ = disable_raw_mode();
301 }
302}