use crate::core::draw::Cell;
use crate::core::event::{Event, EventType, EscSequenceTracker, MB_LEFT_BUTTON, MB_MIDDLE_BUTTON, MB_RIGHT_BUTTON, KB_F12, KB_SHIFT_F12};
use crate::core::geometry::Point;
use crate::core::palette::Attr;
use crate::core::ansi_dump;
use crate::core::error::Result;
use crossterm::{
cursor, execute, queue, style,
terminal::{self},
event::{self, Event as CTEvent, KeyEventKind, MouseEventKind, MouseButton},
};
use std::io::{self, Write, stdout};
use std::time::{Duration, Instant};
pub struct Terminal {
buffer: Vec<Vec<Cell>>,
prev_buffer: Vec<Vec<Cell>>,
width: u16,
height: u16,
esc_tracker: EscSequenceTracker,
last_mouse_pos: Point,
last_mouse_buttons: u8,
last_click_time: Option<Instant>,
last_click_pos: Point,
clip_stack: Vec<crate::core::geometry::Rect>,
active_view_bounds: Option<crate::core::geometry::Rect>,
pending_event: Option<Event>, }
impl Terminal {
pub fn init() -> Result<Self> {
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(
stdout,
terminal::EnterAlternateScreen,
cursor::Hide,
event::EnableMouseCapture )?;
let (width, height) = terminal::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 {
buffer,
prev_buffer,
width,
height,
esc_tracker: EscSequenceTracker::new(),
last_mouse_pos: Point::zero(),
last_mouse_buttons: 0,
last_click_time: None,
last_click_pos: Point::zero(),
clip_stack: Vec::new(),
active_view_bounds: None,
pending_event: None,
})
}
pub fn shutdown(&mut self) -> Result<()> {
let mut stdout = stdout();
execute!(
stdout,
event::DisableMouseCapture, cursor::Show,
terminal::LeaveAlternateScreen
)?;
terminal::disable_raw_mode()?;
Ok(())
}
pub fn suspend(&mut self) -> Result<()> {
let mut stdout = stdout();
execute!(
stdout,
event::DisableMouseCapture,
cursor::Show,
terminal::LeaveAlternateScreen
)?;
terminal::disable_raw_mode()?;
Ok(())
}
pub fn resume(&mut self) -> Result<()> {
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(
stdout,
terminal::EnterAlternateScreen,
cursor::Hide,
event::EnableMouseCapture
)?;
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) -> (u16, u16) {
(self.width, self.height)
}
pub fn set_active_view_bounds(&mut self, bounds: crate::core::geometry::Rect) {
self.active_view_bounds = Some(bounds);
}
pub fn clear_active_view_bounds(&mut self) {
self.active_view_bounds = None;
}
pub fn push_clip(&mut self, rect: crate::core::geometry::Rect) {
self.clip_stack.push(rect);
}
pub fn pop_clip(&mut self) {
self.clip_stack.pop();
}
fn get_clip_rect(&self) -> Option<crate::core::geometry::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 stdout = stdout();
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;
}
queue!(
stdout,
cursor::MoveTo(start_x as u16, y as u16),
style::SetForegroundColor(current_attr.fg.to_crossterm()),
style::SetBackgroundColor(current_attr.bg.to_crossterm())
)?;
for i in start_x..x {
write!(stdout, "{}", self.buffer[y][i].ch)?;
}
}
}
stdout.flush()?;
self.prev_buffer.clone_from(&self.buffer);
Ok(())
}
pub fn show_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
let mut stdout = stdout();
execute!(
stdout,
cursor::MoveTo(x, y),
cursor::Show
)?;
Ok(())
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
let mut stdout = stdout();
execute!(stdout, cursor::Hide)?;
Ok(())
}
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));
}
if event::poll(timeout)? {
match event::read()? {
CTEvent::Key(key) => {
if key.kind != KeyEventKind::Press {
return Ok(None);
}
let key_code = self.esc_tracker.process_key(key);
if key_code == 0 {
return Ok(None);
}
if key_code == KB_F12 {
let _ = self.flash();
let _ = self.dump_screen("screen-dump.txt");
return Ok(None); }
if key_code == KB_SHIFT_F12 {
let _ = self.flash();
if let Some(bounds) = self.active_view_bounds {
let _ = self.dump_region(
bounds.a.x as u16,
bounds.a.y as u16,
(bounds.b.x - bounds.a.x) as u16,
(bounds.b.y - bounds.a.y) as u16,
"active-view-dump.txt"
);
}
return Ok(None); }
Ok(Some(Event {
what: EventType::Keyboard,
key_code,
key_modifiers: key.modifiers,
..Event::nothing()
}))
}
CTEvent::Mouse(mouse) => {
Ok(self.convert_mouse_event(mouse))
}
_ => Ok(None),
}
} else {
Ok(None)
}
}
pub fn read_event(&mut self) -> io::Result<Event> {
loop {
match event::read()? {
CTEvent::Key(key) => {
if key.kind != KeyEventKind::Press {
continue;
}
let key_code = self.esc_tracker.process_key(key);
if key_code == 0 {
continue;
}
if key_code == KB_F12 {
let _ = self.flash();
let _ = self.dump_screen("screen-dump.txt");
continue; }
if key_code == KB_SHIFT_F12 {
let _ = self.flash();
if let Some(bounds) = self.active_view_bounds {
let _ = self.dump_region(
bounds.a.x as u16,
bounds.a.y as u16,
(bounds.b.x - bounds.a.x) as u16,
(bounds.b.y - bounds.a.y) as u16,
"active-view-dump.txt"
);
}
continue; }
return Ok(Event::keyboard(key_code));
}
CTEvent::Mouse(mouse) => {
if let Some(event) = self.convert_mouse_event(mouse) {
return Ok(event);
}
}
_ => continue,
}
}
}
fn convert_mouse_event(&mut self, mouse: event::MouseEvent) -> Option<Event> {
let pos = Point::new(mouse.column as i16, mouse.row as i16);
match mouse.kind {
MouseEventKind::ScrollUp => {
return Some(Event::mouse(EventType::MouseWheelUp, pos, 0, false));
}
MouseEventKind::ScrollDown => {
return Some(Event::mouse(EventType::MouseWheelDown, pos, 0, false));
}
_ => {}
}
let buttons = match mouse.kind {
MouseEventKind::Down(MouseButton::Left) | MouseEventKind::Drag(MouseButton::Left) => MB_LEFT_BUTTON,
MouseEventKind::Down(MouseButton::Right) | MouseEventKind::Drag(MouseButton::Right) => MB_RIGHT_BUTTON,
MouseEventKind::Down(MouseButton::Middle) | MouseEventKind::Drag(MouseButton::Middle) => MB_MIDDLE_BUTTON,
MouseEventKind::Up(_) => 0, MouseEventKind::Moved => self.last_mouse_buttons, _ => return None,
};
let (event_type, is_double_click) = match mouse.kind {
MouseEventKind::Down(_) => {
let is_double = if let Some(last_time) = self.last_click_time {
let elapsed = last_time.elapsed();
elapsed.as_millis() <= 500 && pos == self.last_click_pos
} else {
false
};
self.last_click_time = Some(Instant::now());
self.last_click_pos = pos;
self.last_mouse_buttons = buttons;
self.last_mouse_pos = pos;
(EventType::MouseDown, is_double)
}
MouseEventKind::Up(_) => {
self.last_mouse_buttons = 0;
(EventType::MouseUp, false)
}
MouseEventKind::Drag(_) | MouseEventKind::Moved => {
self.last_mouse_pos = pos;
(EventType::MouseMove, false)
}
_ => return None,
};
Some(Event::mouse(event_type, pos, buttons, is_double_click))
}
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<()> {
let mut stdout = stdout();
write!(stdout, "\x07")?; stdout.flush()?;
Ok(())
}
}
impl Drop for Terminal {
fn drop(&mut self) {
let _ = self.shutdown();
}
}