use crate::input::{KeyboardHandler, MouseHandler};
use crate::render::buffer::Buffer;
use crate::{ColorPair, Error, Event, Result};
use crossterm::{
cursor,
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent},
execute,
style::{self, SetBackgroundColor, SetForegroundColor},
terminal::{self, disable_raw_mode, enable_raw_mode},
};
use std::io::{Write, stdout};
use std::time::Duration;
pub trait Window {
fn write_str(&mut self, y: u16, x: u16, s: &str) -> Result<()>;
fn write_str_colored(&mut self, y: u16, x: u16, s: &str, colors: ColorPair) -> Result<()>;
fn get_size(&self) -> (u16, u16);
fn clear_screen(&mut self) -> Result<()>;
fn clear_line(&mut self, y: u16) -> Result<()>;
fn clear_area(&mut self, y1: u16, x1: u16, y2: u16, x2: u16) -> Result<()>;
}
pub struct TerminalWindow {
width: u16,
height: u16,
buffer: Buffer,
auto_flush: bool,
keyboard: KeyboardHandler,
mouse: MouseHandler,
}
impl TerminalWindow {
pub fn new() -> Result<Self> {
enable_raw_mode()?;
let (cols, rows) = terminal::size()?;
execute!(
stdout(),
terminal::EnterAlternateScreen, terminal::Clear(terminal::ClearType::All),
cursor::Hide,
cursor::MoveTo(0, 0),
EnableMouseCapture )?;
Ok(Self {
width: cols,
height: rows,
buffer: Buffer::new(cols, rows),
auto_flush: true,
keyboard: KeyboardHandler::new(),
mouse: MouseHandler::new(),
})
}
pub fn clear(&self) -> Result<()> {
execute!(
stdout(),
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0)
)?;
Ok(())
}
pub fn get_input(&self) -> Result<Event> {
self.keyboard.get_input(Duration::from_millis(100))
}
pub fn get_input_timeout(&mut self, timeout: Duration) -> Result<Event> {
if event::poll(timeout)? {
match event::read()? {
CrosstermEvent::Key(key_event) => Ok(self.keyboard.process_key_event(key_event)),
CrosstermEvent::Mouse(mouse_event) => {
Ok(self.mouse.process_mouse_event(mouse_event))
}
CrosstermEvent::Resize(cols, rows) => Ok(Event::Resize {
width: cols,
height: rows,
}),
_ => Ok(Event::Unknown),
}
} else {
Ok(Event::Unknown)
}
}
pub fn wait_for_input(&mut self) -> Result<Event> {
loop {
match event::read()? {
CrosstermEvent::Key(key_event) => {
return Ok(self.keyboard.process_key_event(key_event));
}
CrosstermEvent::Mouse(mouse_event) => {
return Ok(self.mouse.process_mouse_event(mouse_event));
}
CrosstermEvent::Resize(cols, rows) => {
return Ok(Event::Resize {
width: cols,
height: rows,
});
}
_ => continue,
}
}
}
pub fn poll_input(&mut self) -> Result<Option<Event>> {
if let Some(event) = self.mouse.poll()? {
return Ok(Some(event));
}
self.keyboard.poll()
}
pub fn keyboard(&self) -> &KeyboardHandler {
&self.keyboard
}
pub fn keyboard_mut(&mut self) -> &mut KeyboardHandler {
&mut self.keyboard
}
pub fn mouse(&self) -> &MouseHandler {
&self.mouse
}
pub fn mouse_mut(&mut self) -> &mut MouseHandler {
&mut self.mouse
}
pub fn set_auto_flush(&mut self, enabled: bool) {
self.auto_flush = enabled;
}
pub fn flush(&mut self) -> Result<()> {
let changes = self.buffer.process_changes();
let mut last_colors = None;
for change in changes {
execute!(stdout(), cursor::MoveTo(change.x, change.y))?;
if change.colors != last_colors {
if let Some(colors) = change.colors {
execute!(
stdout(),
SetForegroundColor(colors.fg.to_crossterm()),
SetBackgroundColor(colors.bg.to_crossterm())
)?;
} else {
execute!(stdout(), style::ResetColor)?;
}
last_colors = change.colors;
}
execute!(stdout(), style::Print(&change.text))?;
}
execute!(stdout(), style::ResetColor)?;
stdout().flush()?;
Ok(())
}
}
impl Window for TerminalWindow {
fn write_str(&mut self, y: u16, x: u16, s: &str) -> Result<()> {
if y >= self.height || x >= self.width {
return Err(Error::OutOfBoundsError {
x,
y,
width: self.width,
height: self.height,
});
}
self.buffer.write_str(y, x, s, None)?;
if self.auto_flush {
self.flush()?;
}
Ok(())
}
fn write_str_colored(&mut self, y: u16, x: u16, s: &str, colors: ColorPair) -> Result<()> {
if y >= self.height || x >= self.width {
return Err(Error::OutOfBoundsError {
x,
y,
width: self.width,
height: self.height,
});
}
self.buffer.write_str(y, x, s, Some(colors))?;
if self.auto_flush {
self.flush()?;
}
Ok(())
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn clear_screen(&mut self) -> Result<()> {
self.buffer.clear();
if self.auto_flush {
self.flush()?;
}
Ok(())
}
fn clear_line(&mut self, y: u16) -> Result<()> {
if y >= self.height {
return Err(Error::LineOutOfBoundsError {
y,
height: self.height,
});
}
self.buffer.clear_line(y)?;
if self.auto_flush {
self.flush()?;
}
Ok(())
}
fn clear_area(&mut self, y1: u16, x1: u16, y2: u16, x2: u16) -> Result<()> {
if x1 >= self.width || x2 >= self.width || y1 >= self.height || y2 >= self.height {
return Err(Error::BoxOutOfBoundsError {
x1,
y1,
x2,
y2,
width: self.width,
height: self.height,
});
}
let (start_y, end_y) = if y1 <= y2 { (y1, y2) } else { (y2, y1) };
let (start_x, end_x) = if x1 <= x2 { (x1, x2) } else { (x2, x1) };
for y in start_y..=end_y {
let spaces = " ".repeat((end_x - start_x + 1) as usize);
self.buffer.write_str(y, start_x, &spaces, None)?;
}
if self.auto_flush {
self.flush()?;
}
Ok(())
}
}
impl Drop for TerminalWindow {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(
stdout(),
DisableMouseCapture, style::ResetColor,
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0),
cursor::Show,
terminal::LeaveAlternateScreen
);
let _ = self.flush();
}
}