use std::io::{self, Write};
use std::time::Duration;
use crossterm::event::{self, Event, KeyEvent, KeyboardEnhancementFlags};
#[derive(Debug, Clone)]
pub enum TerminalEvent {
Key(KeyEvent),
Paste(String),
Resize(u16, u16),
}
pub trait TerminalTrait {
fn start(&mut self, writer: &mut dyn Write) -> io::Result<()>;
fn stop(&mut self, writer: &mut dyn Write) -> io::Result<()>;
fn drain_input(&mut self, max_ms: u64) -> io::Result<()>;
fn write(&self, writer: &mut dyn Write, data: &str) -> io::Result<()>;
fn size(&self) -> io::Result<(u16, u16)>;
fn kitty_protocol_active(&self) -> bool;
fn move_by(&self, writer: &mut dyn Write, lines: i32) -> io::Result<()>;
fn hide_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
fn show_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
fn clear_line(&self, writer: &mut dyn Write) -> io::Result<()>;
fn clear_from_cursor(&self, writer: &mut dyn Write) -> io::Result<()>;
fn clear_screen(&self, writer: &mut dyn Write) -> io::Result<()>;
fn set_title(&self, writer: &mut dyn Write, title: &str) -> io::Result<()>;
fn set_progress(&self, writer: &mut dyn Write, active: bool) -> io::Result<()>;
fn set_color_scheme_notifications(
&self,
writer: &mut dyn Write,
enabled: bool,
) -> io::Result<()>;
}
pub fn poll_terminal_event(timeout: Option<Duration>) -> io::Result<Option<TerminalEvent>> {
if event::poll(timeout.unwrap_or(Duration::ZERO))? {
match event::read()? {
Event::Key(key) => Ok(Some(TerminalEvent::Key(key))),
Event::Paste(content) => Ok(Some(TerminalEvent::Paste(content))),
Event::Resize(w, h) => Ok(Some(TerminalEvent::Resize(w, h))),
_ => Ok(None),
}
} else {
Ok(None)
}
}
pub fn poll_key_event(timeout: Option<Duration>) -> io::Result<Option<KeyEvent>> {
match poll_terminal_event(timeout)? {
Some(TerminalEvent::Key(key)) => Ok(Some(key)),
_ => Ok(None),
}
}
pub fn read_key_event() -> io::Result<KeyEvent> {
loop {
match event::read()? {
Event::Key(key) => return Ok(key),
Event::Paste(_) => continue,
Event::Resize(_, _) => continue,
_ => continue,
}
}
}
pub struct ProcessTerminal {
was_raw: bool,
kitty_active: bool,
}
impl ProcessTerminal {
pub fn new() -> Self {
Self {
was_raw: false,
kitty_active: false,
}
}
fn enable_bracketed_paste(&self, writer: &mut dyn Write) -> io::Result<()> {
write!(writer, "\x1b[?2004h")?;
writer.flush()
}
fn disable_bracketed_paste(&self, writer: &mut dyn Write) -> io::Result<()> {
write!(writer, "\x1b[?2004l")?;
writer.flush()
}
fn enable_kitty_protocol(&mut self, writer: &mut dyn Write) -> io::Result<()> {
let flags = KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
write!(writer, "\x1b[>{}u", flags.bits())?;
writer.flush()?;
self.kitty_active = true;
Ok(())
}
fn disable_kitty_protocol(&mut self, writer: &mut dyn Write) -> io::Result<()> {
if self.kitty_active {
write!(writer, "\x1b[<u")?;
writer.flush()?;
self.kitty_active = false;
}
Ok(())
}
}
impl Default for ProcessTerminal {
fn default() -> Self {
Self::new()
}
}
impl Drop for ProcessTerminal {
fn drop(&mut self) {
if self.was_raw {
let _ = crossterm::terminal::disable_raw_mode();
}
}
}
impl TerminalTrait for ProcessTerminal {
fn start(&mut self, writer: &mut dyn Write) -> io::Result<()> {
crossterm::terminal::enable_raw_mode()?;
self.was_raw = true;
self.enable_bracketed_paste(writer)?;
self.enable_kitty_protocol(writer)?;
let _ = crossterm::terminal::size();
Ok(())
}
fn stop(&mut self, writer: &mut dyn Write) -> io::Result<()> {
self.disable_kitty_protocol(writer)?;
self.disable_bracketed_paste(writer)?;
if self.was_raw {
crossterm::terminal::disable_raw_mode()?;
self.was_raw = false;
}
Ok(())
}
fn drain_input(&mut self, max_ms: u64) -> io::Result<()> {
let mut buf = Vec::new();
self.disable_kitty_protocol(&mut buf)?;
if !buf.is_empty() {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
handle.write_all(&buf)?;
handle.flush()?;
}
let start = std::time::Instant::now();
let mut last_data = start;
loop {
if start.elapsed().as_millis() as u64 >= max_ms {
break;
}
if event::poll(Duration::from_millis(10))? {
let _ = event::read()?;
last_data = std::time::Instant::now();
} else if last_data.elapsed().as_millis() > 50 {
break;
}
}
Ok(())
}
fn write(&self, writer: &mut dyn Write, data: &str) -> io::Result<()> {
write!(writer, "{}", data)?;
writer.flush()
}
fn size(&self) -> io::Result<(u16, u16)> {
crossterm::terminal::size()
}
fn kitty_protocol_active(&self) -> bool {
self.kitty_active
}
fn move_by(&self, writer: &mut dyn Write, lines: i32) -> io::Result<()> {
if lines > 0 {
write!(writer, "\x1b[{}B", lines)?;
} else if lines < 0 {
write!(writer, "\x1b[{}A", -lines)?;
}
writer.flush()
}
fn hide_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
write!(writer, "\x1b[?25l")?;
writer.flush()
}
fn show_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
write!(writer, "\x1b[?25h")?;
writer.flush()
}
fn clear_line(&self, writer: &mut dyn Write) -> io::Result<()> {
write!(writer, "\x1b[2K")?;
writer.flush()
}
fn clear_from_cursor(&self, writer: &mut dyn Write) -> io::Result<()> {
write!(writer, "\x1b[J")?;
writer.flush()
}
fn clear_screen(&self, writer: &mut dyn Write) -> io::Result<()> {
write!(writer, "\x1b[2J\x1b[H")?;
writer.flush()
}
fn set_title(&self, writer: &mut dyn Write, title: &str) -> io::Result<()> {
write!(writer, "\x1b]0;{}\x07", title)?;
writer.flush()
}
fn set_progress(&self, writer: &mut dyn Write, active: bool) -> io::Result<()> {
if active {
write!(writer, "\x1b]9;4;3\x07")?;
} else {
write!(writer, "\x1b]9;4;0;\x07")?;
}
writer.flush()
}
fn set_color_scheme_notifications(
&self,
writer: &mut dyn Write,
enabled: bool,
) -> io::Result<()> {
if enabled {
write!(writer, "\x1b[?2031h")?;
} else {
write!(writer, "\x1b[?2031l")?;
}
writer.flush()
}
}
use crossterm::{cursor, execute, terminal::ClearType};
pub struct Terminal {
inner: ProcessTerminal,
}
impl Terminal {
pub fn new() -> Self {
Self {
inner: ProcessTerminal::new(),
}
}
pub fn enter_raw_mode(&mut self) -> io::Result<()> {
let mut buf = Vec::new();
self.inner.start(&mut buf)?;
if !buf.is_empty() {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
handle.write_all(&buf)?;
handle.flush()?;
}
Ok(())
}
pub fn leave_raw_mode(&mut self) -> io::Result<()> {
let mut buf = Vec::new();
self.inner.stop(&mut buf)?;
if !buf.is_empty() {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
handle.write_all(&buf)?;
handle.flush()?;
}
Ok(())
}
pub fn show_cursor(writer: &mut impl Write) -> io::Result<()> {
execute!(writer, cursor::Show)
}
pub fn hide_cursor(writer: &mut impl Write) -> io::Result<()> {
execute!(writer, cursor::Hide)
}
pub fn move_cursor_to(writer: &mut impl Write, row: u16, col: u16) -> io::Result<()> {
execute!(writer, cursor::MoveTo(col, row))
}
pub fn clear_line(writer: &mut impl Write) -> io::Result<()> {
execute!(writer, crossterm::terminal::Clear(ClearType::CurrentLine))
}
pub fn clear_screen(writer: &mut impl Write) -> io::Result<()> {
execute!(writer, crossterm::terminal::Clear(ClearType::All))
}
pub fn size() -> io::Result<(u16, u16)> {
crossterm::terminal::size()
}
pub fn write(writer: &mut impl Write, data: &str) -> io::Result<()> {
write!(writer, "{}", data)?;
writer.flush()
}
pub fn begin_sync(writer: &mut impl Write) -> io::Result<()> {
write!(writer, "\x1b[?2026h")?;
writer.flush()
}
pub fn end_sync(writer: &mut impl Write) -> io::Result<()> {
write!(writer, "\x1b[?2026l")?;
writer.flush()
}
pub fn set_color_scheme_notifications(
writer: &mut impl Write,
enabled: bool,
) -> io::Result<()> {
if enabled {
write!(writer, "\x1b[?2031h")?;
} else {
write!(writer, "\x1b[?2031l")?;
}
writer.flush()
}
}
impl Default for Terminal {
fn default() -> Self {
Self::new()
}
}
impl Drop for Terminal {
fn drop(&mut self) {
let _ = self.inner.stop(&mut std::io::sink());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_terminal() {
let term = ProcessTerminal::new();
assert!(!term.kitty_protocol_active());
}
#[test]
fn test_drain_input_timeout() {
let mut term = ProcessTerminal::new();
let _ = term.drain_input(10);
}
}