mod backend;
mod crossterm_backend;
#[cfg(feature = "ssh")]
mod input_parser;
#[cfg(feature = "ssh")]
mod ssh_backend;
pub use backend::{Backend, Capabilities};
pub use crossterm_backend::CrosstermBackend;
#[cfg(feature = "ssh")]
pub use input_parser::InputParser;
#[cfg(feature = "ssh")]
pub use ssh_backend::{SshBackend, SshSessionBuilder, SshSessionHandle};
use crate::core::draw::Cell;
use crate::core::event::Event;
use crate::core::geometry::{Point, Rect};
use crate::core::palette::Attr;
use crate::core::ansi_dump;
use crate::core::error::Result;
use std::io::{self, Write};
use std::time::Duration;
pub struct Terminal {
backend: Box<dyn Backend>,
buffer: Vec<Vec<Cell>>,
prev_buffer: Vec<Vec<Cell>>,
width: u16,
height: u16,
clip_stack: Vec<Rect>,
active_view_bounds: Option<Rect>,
pending_event: Option<Event>,
}
impl Terminal {
pub fn init() -> Result<Self> {
let backend = CrosstermBackend::new()?;
Self::with_backend(Box::new(backend))
}
pub fn with_backend(mut backend: Box<dyn Backend>) -> Result<Self> {
backend.init()?;
let (width, height) = backend.size()?;
let empty_cell = Cell::new(' ', Attr::from_u8(0x07));
let buffer = vec![vec![empty_cell; width as usize]; height as usize];
let prev_buffer = vec![vec![empty_cell; width as usize]; height as usize];
Ok(Self {
backend,
buffer,
prev_buffer,
width,
height,
clip_stack: Vec::new(),
active_view_bounds: None,
pending_event: None,
})
}
pub fn shutdown(&mut self) -> Result<()> {
self.backend.cleanup()?;
Ok(())
}
pub fn suspend(&mut self) -> Result<()> {
self.backend.suspend()?;
Ok(())
}
pub fn resume(&mut self) -> Result<()> {
self.backend.resume()?;
let empty_cell = Cell::new(' ', Attr::from_u8(0x07));
for row in &mut self.prev_buffer {
for cell in row {
*cell = empty_cell;
}
}
Ok(())
}
pub fn size(&self) -> (i16, i16) {
(self.width as i16, self.height as i16)
}
pub fn query_size() -> io::Result<(i16, i16)> {
let (width, height) = crossterm::terminal::size()?;
Ok((width as i16, height as i16))
}
pub fn query_cell_aspect_ratio() -> (i16, i16) {
use crossterm::terminal::window_size;
if let Ok(ws) = window_size() {
if ws.width > 0 && ws.height > 0 && ws.columns > 0 && ws.rows > 0 {
let cell_width = ws.width as f32 / ws.columns as f32;
let cell_height = ws.height as f32 / ws.rows as f32;
if cell_width > 0.0 {
let ratio = (cell_height / cell_width).round() as i16;
return (ratio.max(1), 1);
}
}
}
(2, 1)
}
pub fn cell_aspect_ratio(&self) -> (i16, i16) {
self.backend.cell_aspect_ratio()
}
pub fn resize(&mut self, new_width: u16, new_height: u16) {
self.width = new_width;
self.height = new_height;
let empty_cell = Cell::new(' ', Attr::from_u8(0x07));
self.buffer = vec![vec![empty_cell; new_width as usize]; new_height as usize];
let force_redraw_cell = Cell::new('\0', Attr::from_u8(0xFF));
self.prev_buffer = vec![vec![force_redraw_cell; new_width as usize]; new_height as usize];
let _ = self.backend.clear_screen();
}
pub fn set_esc_timeout(&mut self, timeout_ms: u64) {
if let Some(ct_backend) = self.backend_as_crossterm_mut() {
ct_backend.set_esc_timeout(timeout_ms);
}
}
fn backend_as_crossterm_mut(&mut self) -> Option<&mut CrosstermBackend> {
None }
pub fn set_active_view_bounds(&mut self, bounds: Rect) {
self.active_view_bounds = Some(bounds);
}
pub fn clear_active_view_bounds(&mut self) {
self.active_view_bounds = None;
}
pub fn force_full_redraw(&mut self) {
let force_cell = Cell::new('\0', Attr::from_u8(0xFF));
for row in &mut self.prev_buffer {
for cell in row {
*cell = force_cell;
}
}
}
pub fn push_clip(&mut self, rect: Rect) {
self.clip_stack.push(rect);
}
pub fn pop_clip(&mut self) {
self.clip_stack.pop();
}
fn get_clip_rect(&self) -> Option<Rect> {
if self.clip_stack.is_empty() {
None
} else {
let mut result = self.clip_stack[0];
for clip in &self.clip_stack[1..] {
result = result.intersect(clip);
}
Some(result)
}
}
fn is_clipped(&self, x: i16, y: i16) -> bool {
if let Some(clip) = self.get_clip_rect() {
!clip.contains(Point::new(x, y))
} else {
false
}
}
pub fn write_cell(&mut self, x: u16, y: u16, cell: Cell) {
let x_i16 = x as i16;
let y_i16 = y as i16;
if (x as usize) >= self.width as usize || (y as usize) >= self.height as usize {
return;
}
if self.is_clipped(x_i16, y_i16) {
return;
}
self.buffer[y as usize][x as usize] = cell;
}
pub fn write_line(&mut self, x: u16, y: u16, cells: &[Cell]) {
let y_i16 = y as i16;
if (y as usize) >= self.height as usize {
return;
}
let max_width = (self.width as usize).saturating_sub(x as usize);
let len = cells.len().min(max_width);
for (i, cell) in cells.iter().enumerate().take(len) {
let cell_x = (x as usize) + i;
let cell_x_i16 = cell_x as i16;
if !self.is_clipped(cell_x_i16, y_i16) {
self.buffer[y as usize][cell_x] = *cell;
}
}
}
pub fn read_cell(&self, x: i16, y: i16) -> Option<Cell> {
if x < 0 || y < 0 || x >= self.width as i16 || y >= self.height as i16 {
return None;
}
Some(self.buffer[y as usize][x as usize])
}
pub fn clear(&mut self) {
let empty_cell = Cell::new(' ', Attr::from_u8(0x07));
for row in &mut self.buffer {
for cell in row {
*cell = empty_cell;
}
}
}
pub fn flush(&mut self) -> io::Result<()> {
let mut output = Vec::new();
for y in 0..self.height as usize {
let mut x = 0;
while x < self.width as usize {
if self.buffer[y][x] == self.prev_buffer[y][x] {
x += 1;
continue;
}
let start_x = x;
let current_attr = self.buffer[y][x].attr;
while x < self.width as usize
&& self.buffer[y][x] != self.prev_buffer[y][x]
&& self.buffer[y][x].attr == current_attr
{
x += 1;
}
write!(output, "\x1b[{};{}H", y + 1, start_x + 1)?;
let (fg_r, fg_g, fg_b) = current_attr.fg.to_rgb();
let (bg_r, bg_g, bg_b) = current_attr.bg.to_rgb();
write!(output, "\x1b[38;2;{};{};{};48;2;{};{};{}m", fg_r, fg_g, fg_b, bg_r, bg_g, bg_b)?;
for i in start_x..x {
let ch = self.buffer[y][i].ch;
let mut buf = [0u8; 4];
let encoded = ch.encode_utf8(&mut buf);
output.extend_from_slice(encoded.as_bytes());
}
}
}
if !output.is_empty() {
self.backend.write_raw(&output)?;
}
self.backend.flush()?;
self.prev_buffer.clone_from(&self.buffer);
Ok(())
}
pub fn show_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.show_cursor(x, y)
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()
}
pub fn put_event(&mut self, event: Event) {
self.pending_event = Some(event);
}
pub fn poll_event(&mut self, timeout: Duration) -> io::Result<Option<Event>> {
if let Some(event) = self.pending_event.take() {
return Ok(Some(event));
}
self.backend.poll_event(timeout)
}
pub fn read_event(&mut self) -> io::Result<Event> {
loop {
if let Some(event) = self.poll_event(Duration::from_secs(60))? {
return Ok(event);
}
}
}
pub fn dump_screen(&self, path: &str) -> io::Result<()> {
ansi_dump::dump_buffer_to_file(&self.buffer, self.width as usize, self.height as usize, path)
}
pub fn dump_region(&self, x: u16, y: u16, width: u16, height: u16, path: &str) -> io::Result<()> {
let mut file = std::fs::File::create(path)?;
ansi_dump::dump_buffer_region(
&mut file,
&self.buffer,
x as usize,
y as usize,
width as usize,
height as usize,
)
}
pub fn buffer(&self) -> &[Vec<Cell>] {
&self.buffer
}
pub fn flash(&mut self) -> io::Result<()> {
use std::thread;
let saved_buffer = self.buffer.clone();
for row in &mut self.buffer {
for cell in row {
let temp_fg = cell.attr.fg;
cell.attr.fg = cell.attr.bg;
cell.attr.bg = temp_fg;
}
}
self.flush()?;
thread::sleep(Duration::from_millis(50));
self.buffer = saved_buffer;
self.flush()?;
Ok(())
}
pub fn beep(&mut self) -> io::Result<()> {
self.backend.bell()
}
pub fn capabilities(&self) -> Capabilities {
self.backend.capabilities()
}
pub fn write_kitty_graphics(&mut self, data: &[u8]) -> io::Result<()> {
self.backend.write_raw(data)?;
self.backend.flush()
}
pub fn supports_kitty_graphics(&self) -> bool {
if let Ok(term) = std::env::var("TERM") {
let term_lower = term.to_lowercase();
if term_lower.contains("kitty")
|| term_lower.contains("wezterm")
|| term_lower.contains("ghostty")
{
return true;
}
}
if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
let prog_lower = term_program.to_lowercase();
if prog_lower.contains("kitty")
|| prog_lower.contains("wezterm")
|| prog_lower.contains("ghostty")
{
return true;
}
}
if std::env::var("KITTY_WINDOW_ID").is_ok() {
return true;
}
false
}
pub fn delete_kitty_image(&mut self, image_id: u32) -> io::Result<()> {
let cmd = format!("\x1b_Ga=d,d=I,i={},q=2;\x1b\\", image_id);
self.write_kitty_graphics(cmd.as_bytes())
}
pub fn clear_kitty_images(&mut self) -> io::Result<()> {
self.write_kitty_graphics(b"\x1b_Ga=d,d=A,q=2;\x1b\\")
}
}
impl Drop for Terminal {
fn drop(&mut self) {
let _ = self.shutdown();
}
}