use crate::input::{KeyboardHandler, MouseHandler};
use crate::render::buffer::Buffer;
use crate::term::TerminalCapabilities;
use crate::text::{TabPolicy, cell_width};
use crate::{ColorPair, Error, Event, Result};
#[cfg(not(windows))]
use crossterm::event::{
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::{
cursor,
event::{
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event as CrosstermEvent,
},
execute, queue,
style::{self, SetBackgroundColor, SetForegroundColor},
terminal::{self, disable_raw_mode, enable_raw_mode},
};
use std::io::{Stdout, Write, stdout};
use std::time::Duration;
#[cfg(not(windows))]
fn keyboard_enhancement_flags() -> KeyboardEnhancementFlags {
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
}
#[derive(Debug, Default)]
struct TerminalSession {
active: bool,
}
impl TerminalSession {
fn enter(out: &mut Stdout) -> Result<Self> {
enable_raw_mode()?;
let mut session = Self { active: true };
if let Err(err) = Self::write_enter_commands(out) {
session.restore(out);
return Err(err);
}
Ok(session)
}
fn write_enter_commands(out: &mut impl Write) -> Result<()> {
execute!(
out,
terminal::EnterAlternateScreen,
terminal::Clear(terminal::ClearType::All),
cursor::Hide,
cursor::MoveTo(0, 0),
EnableMouseCapture,
EnableBracketedPaste
)?;
#[cfg(not(windows))]
execute!(
out,
PushKeyboardEnhancementFlags(keyboard_enhancement_flags())
)?;
Ok(())
}
fn write_exit_commands(out: &mut impl Write) -> Result<()> {
#[cfg(not(windows))]
execute!(out, PopKeyboardEnhancementFlags)?;
execute!(
out,
DisableBracketedPaste,
DisableMouseCapture,
style::ResetColor,
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0),
terminal::LeaveAlternateScreen,
cursor::Show
)?;
Ok(())
}
fn restore(&mut self, out: &mut Stdout) {
if !self.active {
return;
}
self.active = false;
let _ = Self::write_exit_commands(out);
let _ = disable_raw_mode();
let _ = out.flush();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CursorSpec {
pub x: u16,
pub y: u16,
pub visible: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ColoredSpan<'a> {
pub text: &'a str,
pub colors: ColorPair,
}
impl<'a> ColoredSpan<'a> {
pub const fn new(text: &'a str, colors: ColorPair) -> Self {
Self { text, colors }
}
}
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 write_spans_colored(&mut self, y: u16, x: u16, spans: &[ColoredSpan<'_>]) -> Result<()> {
let mut cursor_x = x;
for span in spans {
if span.text.is_empty() {
continue;
}
self.write_str_colored(y, cursor_x, span.text, span.colors)?;
let span_w = cell_width(span.text, TabPolicy::SingleCell);
cursor_x = cursor_x.saturating_add(span_w);
}
Ok(())
}
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,
session: TerminalSession,
keyboard: KeyboardHandler,
mouse: MouseHandler,
}
impl TerminalWindow {
pub fn new() -> Result<Self> {
let (cols, rows) = terminal::size()?;
let mut out = stdout();
let session = TerminalSession::enter(&mut out)?;
Ok(Self {
width: cols,
height: rows,
buffer: Buffer::new(cols, rows),
auto_flush: true,
capabilities: TerminalCapabilities::detect(),
pending_cursor: None,
last_cursor: None,
out,
session,
keyboard: KeyboardHandler::new(),
mouse: MouseHandler::new(),
})
}
pub 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 change_count = self.buffer.process_changes();
let mut last_colors: Option<ColorPair> = None;
let mut cursor_after_write: Option<(u16, u16)> = None;
let hid_cursor_for_render = change_count > 0;
if hid_cursor_for_render {
queue!(self.out, cursor::Hide)?;
}
for change_idx in 0..change_count {
let change = self.buffer.change(change_idx);
if cursor_after_write != Some((change.x, change.y)) {
queue!(self.out, cursor::MoveTo(change.x, change.y))?;
}
let applied_colors = change
.colors
.map(|colors| self.capabilities.downgrade_pair(colors));
if applied_colors != last_colors {
if let Some(colors) = applied_colors {
queue!(
self.out,
SetForegroundColor(colors.fg.to_crossterm()),
SetBackgroundColor(colors.bg.to_crossterm())
)?;
last_colors = Some(colors);
} else {
queue!(self.out, style::ResetColor)?;
last_colors = None;
}
}
for ch in self.buffer.change_chars(change) {
write!(self.out, "{ch}")?;
}
cursor_after_write = Some((change.x.saturating_add(change.len as u16), change.y));
}
if last_colors.is_some() {
queue!(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;
let prev_visible = prev.map(|p| p.visible).unwrap_or(false);
if desired.visible {
if !prev_visible || hid_cursor_for_render {
queue!(self.out, cursor::Show)?;
}
} else if prev_visible {
queue!(self.out, cursor::Hide)?;
}
if desired.visible {
let needs_move = match prev {
Some(p) => p.x != desired.x || p.y != desired.y,
None => true,
};
if needs_move || hid_cursor_for_render {
queue!(self.out, cursor::MoveTo(desired.x, desired.y))?;
}
}
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 _ = self.flush();
self.session.restore(&mut self.out);
}
}
#[cfg(test)]
mod tests {
use super::TerminalSession;
#[test]
fn shutdown_restores_terminal_modes() {
let mut bytes = Vec::new();
TerminalSession::write_enter_commands(&mut bytes).unwrap();
TerminalSession::write_exit_commands(&mut bytes).unwrap();
let output = String::from_utf8(bytes).unwrap();
#[cfg(not(windows))]
{
assert!(output.contains("\u{1b}[>9u"));
assert!(output.contains("\u{1b}[<1u"));
}
assert!(output.contains("\u{1b}[?1049h"));
assert!(output.contains("\u{1b}[?1049l"));
assert!(output.contains("\u{1b}[?1000h"));
assert!(output.contains("\u{1b}[?1000l"));
assert!(output.contains("\u{1b}[?2004h"));
assert!(output.contains("\u{1b}[?2004l"));
}
}