use crate::core::buffer::Buffer;
use crate::core::rect::Rect;
use crate::sanitize::char_display_width;
use crate::widgets::Widget;
use crossterm::{
cursor,
event::{
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event,
},
execute, terminal,
};
use std::io::{self, Stdout, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalOptions {
pub mouse_capture: bool,
pub bracketed_paste: bool,
pub alternate_screen: bool,
pub hide_cursor: bool,
}
impl TerminalOptions {
pub const fn new() -> Self {
Self {
mouse_capture: false,
bracketed_paste: false,
alternate_screen: true,
hide_cursor: true,
}
}
}
impl Default for TerminalOptions {
fn default() -> Self {
Self::new()
}
}
pub struct Frame<'a> {
area: Rect,
buffer: &'a mut Buffer,
frame_count: u64,
cursor_position: Option<(u16, u16)>,
}
impl<'a> Frame<'a> {
pub fn area(&self) -> Rect {
self.area
}
pub fn buffer(&mut self) -> &mut Buffer {
self.buffer
}
pub fn frame_count(&self) -> u64 {
self.frame_count
}
pub fn set_cursor_position(&mut self, x: u16, y: u16) {
self.cursor_position = Some((x, y));
}
pub fn cursor_position(&self) -> Option<(u16, u16)> {
self.cursor_position
}
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.render(&mut *self.buffer, area);
}
}
#[derive(Debug)]
pub struct Terminal {
stdout: Stdout,
width: u16,
height: u16,
front_buffer: Buffer,
back_buffer: Buffer,
hidden_cursor: bool,
mouse_capture: bool,
bracketed_paste: bool,
alternate_screen: bool,
restored: bool,
frame_count: u64,
cursor_position: Option<(u16, u16)>,
}
impl Terminal {
pub fn init() -> io::Result<Self> {
Self::init_with(TerminalOptions::default())
}
pub fn init_with(options: TerminalOptions) -> io::Result<Self> {
let mut stdout = io::stdout();
terminal::enable_raw_mode()?;
if options.alternate_screen {
execute!(stdout, terminal::EnterAlternateScreen)?;
}
if options.hide_cursor {
execute!(stdout, cursor::Hide)?;
}
if options.mouse_capture {
execute!(stdout, EnableMouseCapture)?;
}
if options.bracketed_paste {
execute!(stdout, EnableBracketedPaste)?;
}
let (w, h) = terminal::size()?;
let front = Buffer::new(w as usize, h as usize);
let back = Buffer::new(w as usize, h as usize);
Ok(Self {
stdout,
width: w,
height: h,
front_buffer: front,
back_buffer: back,
hidden_cursor: options.hide_cursor,
mouse_capture: options.mouse_capture,
bracketed_paste: options.bracketed_paste,
alternate_screen: options.alternate_screen,
restored: false,
frame_count: 0,
cursor_position: None,
})
}
pub fn restore(&mut self) -> io::Result<()> {
if self.restored {
return Ok(());
}
if self.bracketed_paste {
execute!(self.stdout, DisableBracketedPaste)?;
self.bracketed_paste = false;
}
if self.mouse_capture {
execute!(self.stdout, DisableMouseCapture)?;
self.mouse_capture = false;
}
if self.hidden_cursor {
execute!(self.stdout, cursor::Show)?;
self.hidden_cursor = false;
}
if self.alternate_screen {
execute!(self.stdout, terminal::LeaveAlternateScreen)?;
self.alternate_screen = false;
}
terminal::disable_raw_mode()?;
self.restored = true;
Ok(())
}
pub fn size(&self) -> Rect {
Rect::new(0, 0, self.width, self.height)
}
pub fn width(&self) -> usize {
self.width as usize
}
pub fn height(&self) -> usize {
self.height as usize
}
pub fn back_buffer(&mut self) -> &mut Buffer {
&mut self.back_buffer
}
pub fn front_buffer(&self) -> &Buffer {
&self.front_buffer
}
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
where
F: FnOnce(&mut Frame),
{
self.size_changed()?;
self.clear();
let mut frame = Frame {
area: self.size(),
buffer: &mut self.back_buffer,
frame_count: self.frame_count,
cursor_position: None,
};
f(&mut frame);
self.cursor_position = frame.cursor_position;
self.present()?;
if let Some((x, y)) = self.cursor_position {
self.set_cursor(x, y)?;
}
self.frame_count = self.frame_count.wrapping_add(1);
Ok(())
}
pub fn present(&mut self) -> io::Result<()> {
let output = diff_ansi_string(
&self.front_buffer,
&self.back_buffer,
self.width,
self.height,
);
write!(self.stdout, "{}", output)?;
self.stdout.flush()?;
self.front_buffer = self.back_buffer.clone();
Ok(())
}
pub fn present_full(&mut self) -> io::Result<()> {
let output = self.back_buffer.to_ansi_string();
write!(self.stdout, "{}", output)?;
self.stdout.flush()?;
self.front_buffer = self.back_buffer.clone();
Ok(())
}
pub fn draw_full<F>(&mut self, f: F) -> io::Result<()>
where
F: FnOnce(&mut Frame),
{
self.size_changed()?;
self.clear();
let mut frame = Frame {
area: self.size(),
buffer: &mut self.back_buffer,
frame_count: self.frame_count,
cursor_position: None,
};
f(&mut frame);
self.cursor_position = frame.cursor_position;
self.present_full()?;
if let Some((x, y)) = self.cursor_position {
self.set_cursor(x, y)?;
}
self.frame_count = self.frame_count.wrapping_add(1);
Ok(())
}
pub fn clear(&mut self) {
self.back_buffer = Buffer::new(self.width as usize, self.height as usize);
}
pub fn clear_area(&mut self, area: Rect) {
self.back_buffer.clear(area);
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
execute!(self.stdout, cursor::Hide)?;
self.hidden_cursor = true;
Ok(())
}
pub fn show_cursor(&mut self) -> io::Result<()> {
execute!(self.stdout, cursor::Show)?;
self.hidden_cursor = false;
Ok(())
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
execute!(self.stdout, cursor::MoveTo(x, y))?;
Ok(())
}
pub fn set_mouse_capture(&mut self, enabled: bool) -> io::Result<()> {
if enabled == self.mouse_capture {
return Ok(());
}
if enabled {
execute!(self.stdout, EnableMouseCapture)?;
} else {
execute!(self.stdout, DisableMouseCapture)?;
}
self.mouse_capture = enabled;
Ok(())
}
pub fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()> {
if enabled == self.bracketed_paste {
return Ok(());
}
if enabled {
execute!(self.stdout, EnableBracketedPaste)?;
} else {
execute!(self.stdout, DisableBracketedPaste)?;
}
self.bracketed_paste = enabled;
Ok(())
}
pub fn poll_event(&self) -> io::Result<Option<Event>> {
if event::poll(std::time::Duration::from_millis(0))? {
Ok(Some(event::read()?))
} else {
Ok(None)
}
}
pub fn wait_event(&self) -> io::Result<Event> {
event::read()
}
pub fn flush(&mut self) -> io::Result<()> {
self.stdout.flush()
}
pub fn size_changed(&mut self) -> io::Result<bool> {
let (w, h) = terminal::size()?;
if w != self.width || h != self.height {
self.width = w;
self.height = h;
self.front_buffer.resize(w as usize, h as usize);
self.back_buffer.resize(w as usize, h as usize);
Ok(true)
} else {
Ok(false)
}
}
pub fn raw_output(&self) -> &Stdout {
&self.stdout
}
}
impl Drop for Terminal {
fn drop(&mut self) {
let _ = self.restore();
}
}
pub fn terminal_size() -> (u16, u16) {
terminal::size().unwrap_or((80, 24))
}
fn diff_ansi_string(front: &Buffer, back: &Buffer, width: u16, height: u16) -> String {
let mut output = String::with_capacity(width as usize * height as usize * 8);
let mut last_fg = crate::core::color::Color::BLACK;
let mut last_bg: Option<crate::core::color::Color> = None;
let mut last_bold = false;
let mut last_italic = false;
let mut last_underlined = false;
for y in 0..height as usize {
let mut cursor_col: Option<usize> = None;
for x in 0..width as usize {
if back.is_skip(x, y) {
continue;
}
let Some(back_cell) = back.get(x, y) else {
continue;
};
let front_cell = front.get(x, y);
let same_cell =
front_cell == Some(back_cell) && front.is_skip(x, y) == back.is_skip(x, y);
if same_cell {
continue;
}
if cursor_col != Some(x) {
output.push_str(&format!("\x1b[{};{}H", y + 1, x + 1));
}
if back_cell.fg != last_fg {
output.push_str(&back_cell.fg.to_ansi_fg());
last_fg = back_cell.fg;
}
if back_cell.bg != last_bg {
match back_cell.bg {
Some(c) => output.push_str(&c.to_ansi_bg()),
None => output.push_str("\x1b[49m"),
}
last_bg = back_cell.bg;
}
if back_cell.bold != last_bold {
output.push_str(if back_cell.bold {
"\x1b[1m"
} else {
"\x1b[22m"
});
last_bold = back_cell.bold;
}
if back_cell.italic != last_italic {
output.push_str(if back_cell.italic {
"\x1b[3m"
} else {
"\x1b[23m"
});
last_italic = back_cell.italic;
}
if back_cell.underlined != last_underlined {
output.push_str(if back_cell.underlined {
"\x1b[4m"
} else {
"\x1b[24m"
});
last_underlined = back_cell.underlined;
}
output.push(back_cell.ch);
cursor_col = Some(x + char_display_width(back_cell.ch).max(1));
}
}
output.push_str("\x1b[0m");
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminal_options_default_is_safe() {
let options = TerminalOptions::default();
assert!(options.alternate_screen);
assert!(options.hide_cursor);
assert!(!options.mouse_capture);
}
fn visible_text(ansi: &str) -> String {
let mut visible = String::new();
let mut chars = ansi.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
for code in chars.by_ref() {
if code.is_ascii_alphabetic() {
break;
}
}
}
continue;
}
visible.push(ch);
}
visible
}
#[test]
fn diff_repositions_non_contiguous_dirty_cells() {
let front = Buffer::new(12, 1);
let mut back = Buffer::new(12, 1);
back.set(
8,
0,
crate::core::buffer::Cell::new('A', crate::core::color::Color::WHITE, None),
);
back.set(
10,
0,
crate::core::buffer::Cell::new('B', crate::core::color::Color::WHITE, None),
);
let ansi = diff_ansi_string(&front, &back, 12, 1);
assert!(ansi.contains("\x1b[1;9H"));
assert!(ansi.contains("\x1b[1;11H"));
}
#[test]
fn diff_clears_old_wide_glyph_continuation_cell() {
let mut front = Buffer::new(4, 1);
let mut back = Buffer::new(4, 1);
front.set(
0,
0,
crate::core::buffer::Cell::new('δΈ', crate::core::color::Color::WHITE, None),
);
back.set(
0,
0,
crate::core::buffer::Cell::new('A', crate::core::color::Color::WHITE, None),
);
let ansi = diff_ansi_string(&front, &back, 4, 1);
assert!(ansi.contains("\x1b[1;1H"));
assert_eq!(visible_text(&ansi), "A ");
}
}