use std::io::{self, Stdout, Write};
use crossterm::{
cursor::{Hide, MoveTo, Show},
event::{self, poll, Event},
execute, queue,
style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor},
terminal::{
self, disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
LeaveAlternateScreen,
},
};
use crate::buffer::Buffer;
use crate::canvas::{encode_kitty_graphics, supports_kitty_graphics, PendingCanvas};
use crate::image::{encode_kitty_image, PendingImage};
use crate::render::{render_view, RenderContext};
use crate::theme::current_theme;
use crate::View;
pub struct Terminal {
stdout: Stdout,
buffer: Buffer,
prev_buffer: Buffer,
}
impl Terminal {
pub fn new() -> io::Result<Self> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, Hide, Clear(ClearType::All))?;
let (width, height) = terminal::size()?;
let buffer = Buffer::new(width, height);
let prev_buffer = Buffer::new(width, height);
Ok(Self {
stdout,
buffer,
prev_buffer,
})
}
pub fn draw(
&mut self,
view: &View,
focus_index: usize,
focus_visible: bool,
scroll_offsets: Vec<(u16, u16)>,
cursor_offsets: Vec<usize>,
modal_visible: bool,
) -> io::Result<Vec<(u16, u16)>> {
let (width, height) = terminal::size()?;
if width != self.buffer.width || height != self.buffer.height {
self.buffer = Buffer::new(width, height);
self.prev_buffer = Buffer::new(width, height);
execute!(self.stdout, Clear(ClearType::All))?;
}
let theme = current_theme();
self.buffer.fill(theme.foreground, theme.background);
let area = self.buffer.rect();
let mut ctx = RenderContext::new(focus_index, focus_visible, scroll_offsets, cursor_offsets, area);
ctx.set_modal_visible(modal_visible);
render_view(&mut self.buffer, view, area, &mut ctx);
ctx.render_pending_dropdowns(&mut self.buffer);
let pending_canvases = ctx.take_pending_canvases();
let pending_images = ctx.take_pending_images();
self.flush_diff()?;
if !pending_canvases.is_empty() {
self.flush_canvases(&pending_canvases)?;
}
if !pending_images.is_empty() {
self.flush_images(&pending_images)?;
}
std::mem::swap(&mut self.buffer, &mut self.prev_buffer);
Ok(ctx.scroll_offsets().to_vec())
}
pub fn height(&self) -> u16 {
self.buffer.height
}
fn flush_diff(&mut self) -> io::Result<()> {
let changes = self.buffer.diff(&self.prev_buffer);
for (x, y, cell) in changes {
if cell.wide_continuation {
continue;
}
queue!(self.stdout, MoveTo(x, y))?;
queue!(self.stdout, SetAttribute(Attribute::Reset))?;
if cell.bold {
queue!(self.stdout, SetAttribute(Attribute::Bold))?;
}
if cell.italic {
queue!(self.stdout, SetAttribute(Attribute::Italic))?;
}
if cell.underline {
queue!(self.stdout, SetAttribute(Attribute::Underlined))?;
}
if cell.dim {
queue!(self.stdout, SetAttribute(Attribute::Dim))?;
}
queue!(self.stdout, SetForegroundColor(cell.fg))?;
queue!(self.stdout, SetBackgroundColor(cell.bg))?;
queue!(self.stdout, Print(cell.ch))?;
}
queue!(self.stdout, SetAttribute(Attribute::Reset))?;
self.stdout.flush()?;
Ok(())
}
fn flush_canvases(&mut self, canvases: &[PendingCanvas]) -> io::Result<()> {
if !supports_kitty_graphics() {
return Ok(());
}
for canvas in canvases {
let escape_seq =
encode_kitty_graphics(&canvas.pixels, canvas.cell_x, canvas.cell_y, canvas.id);
self.stdout.write_all(escape_seq.as_bytes())?;
}
self.stdout.flush()?;
Ok(())
}
fn flush_images(&mut self, images: &[PendingImage]) -> io::Result<()> {
if !supports_kitty_graphics() {
return Ok(());
}
for image in images {
let escape_seq = encode_kitty_image(&image.data, image.cell_x, image.cell_y, image.id);
self.stdout.write_all(escape_seq.as_bytes())?;
}
self.stdout.flush()?;
Ok(())
}
pub fn poll_event(&self) -> io::Result<Option<Event>> {
if poll(std::time::Duration::from_millis(16))? {
Ok(Some(event::read()?))
} else {
Ok(None)
}
}
pub fn draw_debug(
&mut self,
frame: u64,
render_us: u64,
focus_idx: usize,
focusable_count: usize,
) -> io::Result<()> {
let _ = (frame, render_us); let debug_text = format!(" Focus: {}/{} ", focus_idx, focusable_count);
let x = self
.buffer
.width
.saturating_sub(debug_text.len() as u16 + 1);
let y = 0;
queue!(
self.stdout,
MoveTo(x, y),
SetForegroundColor(Color::Black),
SetBackgroundColor(Color::Yellow),
Print(&debug_text),
SetForegroundColor(Color::Reset),
SetBackgroundColor(Color::Reset)
)?;
self.stdout.flush()?;
Ok(())
}
pub fn cleanup(&mut self) -> io::Result<()> {
if supports_kitty_graphics() {
let delete_cmd = crate::canvas::delete_all_kitty_images();
let _ = self.stdout.write_all(delete_cmd.as_bytes());
}
execute!(
self.stdout,
Clear(ClearType::All),
SetForegroundColor(Color::Reset),
SetBackgroundColor(Color::Reset),
Show,
LeaveAlternateScreen
)?;
disable_raw_mode()?;
Ok(())
}
}
impl Drop for Terminal {
fn drop(&mut self) {
if supports_kitty_graphics() {
let delete_cmd = crate::canvas::delete_all_kitty_images();
let _ = self.stdout.write_all(delete_cmd.as_bytes());
}
let _ = execute!(
self.stdout,
Clear(ClearType::All),
SetForegroundColor(Color::Reset),
SetBackgroundColor(Color::Reset),
Show,
LeaveAlternateScreen
);
let _ = disable_raw_mode();
}
}