use std::io::{Stdout, Write, stdout};
use std::time::Duration;
use anyhow::Result;
use crossterm::ExecutableCommand;
use crossterm::cursor::{Hide, Show};
use crossterm::event::{
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
};
use crossterm::terminal::{
self, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::prelude::*;
pub struct TerminalGuard {
pub inline_mode: bool,
pub mouse_capture: bool,
}
impl TerminalGuard {
pub fn enter(inline_mode: bool, mouse_capture: bool) -> Result<Self> {
enable_raw_mode()?;
let mut out = stdout();
out.execute(Hide)?;
if !inline_mode {
out.execute(EnterAlternateScreen)?;
}
if mouse_capture {
out.execute(EnableMouseCapture)?;
}
out.execute(EnableBracketedPaste)?;
out.flush()?;
Ok(Self {
inline_mode,
mouse_capture,
})
}
pub fn leave(&self) -> Result<()> {
let mut out = stdout();
if self.mouse_capture {
let _ = out.execute(DisableMouseCapture);
}
let _ = out.execute(DisableBracketedPaste);
if !self.inline_mode {
let _ = out.execute(LeaveAlternateScreen);
}
let _ = out.execute(Show);
disable_raw_mode()?;
Ok(())
}
}
pub struct TuiTerminal {
pub terminal: Terminal<CrosstermBackend<Stdout>>,
pub guard: TerminalGuard,
_stderr_log: super::stderr_log::StderrLogGuard,
}
impl TuiTerminal {
pub fn new(inline_mode: bool, mouse_capture: bool) -> Result<Self> {
let _stderr_log = super::stderr_log::StderrLogGuard::install()?;
let guard = TerminalGuard::enter(inline_mode, mouse_capture)?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
Ok(Self {
terminal,
guard,
_stderr_log,
})
}
pub fn shutdown(mut self) -> Result<()> {
self.terminal.show_cursor()?;
self.guard.leave()
}
}
pub fn sync_terminal_geometry(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
layout: &mut super::layout::LayoutEngine,
) -> Result<()> {
let (cols, rows) = terminal::size()?;
layout.last_terminal_width = cols;
layout.apply_auto_collapse(cols);
terminal.resize(ratatui::layout::Rect::new(0, 0, cols, rows))?;
while event::poll(Duration::ZERO)? {
if let Event::Resize(w, h) = event::read()? {
layout.last_terminal_width = w;
layout.apply_auto_collapse(w);
terminal.resize(ratatui::layout::Rect::new(0, 0, w, h))?;
}
}
Ok(())
}
pub fn poll_event(timeout: Duration) -> Result<Option<Event>> {
if event::poll(timeout)? {
Ok(Some(event::read()?))
} else {
Ok(None)
}
}
pub fn is_key_press(key: &KeyEvent) -> bool {
matches!(key.kind, KeyEventKind::Press)
}
pub fn is_ctrl_c(event: &Event) -> bool {
matches!(
event,
Event::Key(key)
if key.code == KeyCode::Char('c')
&& key.modifiers.contains(KeyModifiers::CONTROL)
)
}
pub fn enter_inserts_newline(key: &KeyEvent) -> bool {
key.modifiers.contains(KeyModifiers::SHIFT)
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
fn press(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
#[test]
fn shift_enter_inserts_newline() {
let key = press(KeyCode::Enter, KeyModifiers::SHIFT);
assert!(enter_inserts_newline(&key));
}
#[test]
fn plain_enter_sends() {
let key = press(KeyCode::Enter, KeyModifiers::NONE);
assert!(!enter_inserts_newline(&key));
}
}