use crate::input::{KeyboardHandler, MouseHandler};
use crate::render::buffer::Buffer;
use crate::term::TerminalCapabilities;
use crate::{ColorPair, Error, Event, Result};
use crossterm::{
cursor,
event::{
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event as CrosstermEvent,
},
execute,
style::{self, SetBackgroundColor, SetForegroundColor},
terminal::{self, disable_raw_mode, enable_raw_mode},
};
use std::io::{Stdout, Write, stdout};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CursorSpec {
pub x: u16,
pub y: u16,
pub visible: bool,
}
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 flush(&mut self) -> Result<()>;
fn request_cursor(&mut self, _cursor: CursorSpec) {}
fn clear_cursor_request(&mut self) {}
fn end_frame(&mut self) -> Result<()> {
self.flush()
}
fn set_cursor_position(&mut self, x: u16, y: u16) -> Result<()>;
fn show_cursor(&mut self, show: bool) -> 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,
capabilities: TerminalCapabilities,
pending_cursor: Option<CursorSpec>,
last_cursor: Option<CursorSpec>,
out: Stdout,
keyboard: KeyboardHandler,
mouse: MouseHandler,
}
impl TerminalWindow {
pub fn new() -> Result<Self> {
enable_raw_mode()?;
let (cols, rows) = terminal::size()?;
let mut out = stdout();
execute!(
out,
terminal::EnterAlternateScreen, terminal::Clear(terminal::ClearType::All),
cursor::Hide,
cursor::MoveTo(0, 0),
EnableMouseCapture, EnableBracketedPaste )?;
Ok(Self {
width: cols,
height: rows,
buffer: Buffer::new(cols, rows),
auto_flush: true,
capabilities: TerminalCapabilities::detect(),
pending_cursor: None,
last_cursor: None,
out,
keyboard: KeyboardHandler::new(),
mouse: MouseHandler::new(),
})
}
pub(crate) fn handle_resize(&mut self, width: u16, height: u16) {
if self.width == width && self.height == height {
return;
}
self.width = width;
self.height = height;
self.buffer = Buffer::new(width, height);
let _ = execute!(
self.out,
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0)
);
}
pub fn clear(&mut self) -> Result<()> {
execute!(
self.out,
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0)
)?;
self.out.flush()?;
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::Paste(text) => Ok(Event::Paste(text)),
CrosstermEvent::Resize(cols, rows) => {
self.handle_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::Paste(text) => {
return Ok(Event::Paste(text));
}
CrosstermEvent::Resize(cols, rows) => {
self.handle_resize(cols, rows);
return Ok(Event::Resize {
width: cols,
height: rows,
});
}
_ => continue,
}
}
}
pub fn poll_input(&mut self) -> Result<Option<Event>> {
if event::poll(Duration::from_millis(0))? {
match event::read()? {
CrosstermEvent::Key(key_event) => {
Ok(Some(self.keyboard.process_key_event(key_event)))
}
CrosstermEvent::Mouse(mouse_event) => {
Ok(Some(self.mouse.process_mouse_event(mouse_event)))
}
CrosstermEvent::Paste(text) => Ok(Some(Event::Paste(text))),
CrosstermEvent::Resize(cols, rows) => {
self.handle_resize(cols, rows);
Ok(Some(Event::Resize {
width: cols,
height: rows,
}))
}
_ => Ok(None),
}
} else {
Ok(None)
}
}
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 capabilities(&self) -> TerminalCapabilities {
self.capabilities
}
pub fn set_capabilities(&mut self, capabilities: TerminalCapabilities) {
self.capabilities = capabilities;
}
pub fn flush(&mut self) -> Result<()> {
let changes = self.buffer.process_changes();
let mut last_colors: Option<ColorPair> = None;
let hid_cursor_for_render = !changes.is_empty();
if hid_cursor_for_render {
execute!(self.out, cursor::Hide)?;
}
for change in changes {
execute!(self.out, cursor::MoveTo(change.x, change.y))?;
if change.colors != last_colors {
if let Some(colors) = change.colors {
let colors = self.capabilities.downgrade_pair(colors);
execute!(
self.out,
SetForegroundColor(colors.fg.to_crossterm()),
SetBackgroundColor(colors.bg.to_crossterm())
)?;
last_colors = Some(colors);
} else {
execute!(self.out, style::ResetColor)?;
last_colors = None;
}
}
execute!(self.out, style::Print(&change.text))?;
}
execute!(self.out, style::ResetColor)?;
let desired = match self.pending_cursor.take() {
Some(spec) => spec,
None => self.last_cursor.unwrap_or(CursorSpec {
x: 0,
y: 0,
visible: false,
}),
};
let prev = self.last_cursor;
if desired.visible {
let needs_move = match prev {
Some(p) => !p.visible || p.x != desired.x || p.y != desired.y,
None => true,
};
if needs_move || hid_cursor_for_render {
execute!(self.out, cursor::MoveTo(desired.x, desired.y))?;
}
}
let prev_visible = prev.map(|p| p.visible).unwrap_or(false);
if desired.visible {
if !prev_visible || hid_cursor_for_render {
execute!(self.out, cursor::Show)?;
}
} else if prev_visible {
execute!(self.out, cursor::Hide)?;
}
self.last_cursor = Some(desired);
self.out.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 Ok(());
}
self.buffer.write_str(y, x, s, None)?;
if self.auto_flush {
self.end_frame()?;
}
Ok(())
}
fn write_str_colored(&mut self, y: u16, x: u16, s: &str, colors: ColorPair) -> Result<()> {
if y >= self.height || x >= self.width {
return Ok(());
}
self.buffer.write_str(y, x, s, Some(colors))?;
if self.auto_flush {
self.end_frame()?;
}
Ok(())
}
fn flush(&mut self) -> Result<()> {
TerminalWindow::flush(self)
}
fn request_cursor(&mut self, cursor: CursorSpec) {
self.pending_cursor = Some(cursor);
}
fn clear_cursor_request(&mut self) {
self.pending_cursor = None;
}
fn end_frame(&mut self) -> Result<()> {
self.flush()
}
fn set_cursor_position(&mut self, x: u16, y: u16) -> Result<()> {
execute!(self.out, cursor::MoveTo(x, y))?;
Ok(())
}
fn show_cursor(&mut self, show: bool) -> Result<()> {
if show {
execute!(self.out, cursor::Show)?;
} else {
execute!(self.out, cursor::Hide)?;
}
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 _ = self.flush();
let _ = execute!(
self.out,
DisableBracketedPaste, DisableMouseCapture, style::ResetColor,
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0),
terminal::LeaveAlternateScreen,
cursor::Show
);
let _ = self.out.flush();
}
}