use crossterm::cursor::MoveTo;
use crossterm::event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::execute;
use crossterm::terminal::{Clear, ClearType};
use ratatui::backend::CrosstermBackend;
use ratatui::{DefaultTerminal, Terminal, TerminalOptions, Viewport};
use std::io;
use std::thread;
pub const INLINE_SCROLLBACK_RESERVE: u16 = 2;
pub fn inline_viewport_height(terminal_height: u16) -> u16 {
if terminal_height == 0 { 0 } else { terminal_height.saturating_sub(INLINE_SCROLLBACK_RESERVE).max(1) }
}
pub fn inline_viewport_needs_resync(terminal_height: u16, current_viewport_height: u16) -> bool {
let height = inline_viewport_height(terminal_height);
height != 0 && height != current_viewport_height
}
pub(crate) struct TerminalSession {
terminal: DefaultTerminal,
keyboard_enhancement: bool,
bracketed_paste: bool,
mouse_capture: bool,
}
impl TerminalSession {
pub(crate) fn enter(viewport: Viewport) -> io::Result<Self> {
let terminal = match ratatui::try_init_with_options(TerminalOptions { viewport }) {
Ok(terminal) => terminal,
Err(error) => {
ratatui::restore();
return Err(error);
}
};
let mut session = Self { terminal, keyboard_enhancement: false, bracketed_paste: false, mouse_capture: false };
session.try_keyboard_enhancement();
session.apply_bracketed_paste()?;
Ok(session)
}
pub(crate) fn terminal_mut(&mut self) -> &mut DefaultTerminal {
&mut self.terminal
}
pub(crate) fn set_mouse_capture(&mut self, enabled: bool) {
if enabled == self.mouse_capture {
return;
}
if enabled {
let enabled_ok = execute!(io::stdout(), EnableMouseCapture).is_ok();
self.mouse_capture = enabled_ok || execute!(io::stdout(), DisableMouseCapture).is_err();
} else if execute!(io::stdout(), DisableMouseCapture).is_ok() {
self.mouse_capture = false;
}
}
pub(crate) fn resync_inline_viewport(&mut self) -> io::Result<()> {
let terminal_height = self.terminal.size()?.height;
let height = inline_viewport_height(terminal_height);
if !inline_viewport_needs_resync(terminal_height, self.terminal.get_frame().area().height) {
return Ok(());
}
execute!(io::stdout(), MoveTo(0, terminal_height - height), Clear(ClearType::FromCursorDown))?;
self.terminal = Terminal::with_options(
CrosstermBackend::new(io::stdout()),
TerminalOptions { viewport: Viewport::Inline(height) },
)?;
Ok(())
}
fn try_keyboard_enhancement(&mut self) {
let flags = KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
if execute!(io::stdout(), PushKeyboardEnhancementFlags(flags)).is_ok() {
self.keyboard_enhancement = true;
} else if execute!(io::stdout(), PopKeyboardEnhancementFlags).is_err() {
self.keyboard_enhancement = true;
}
}
fn apply_bracketed_paste(&mut self) -> io::Result<()> {
match execute!(io::stdout(), EnableBracketedPaste) {
Ok(()) => {
self.bracketed_paste = true;
Ok(())
}
Err(error) => {
if execute!(io::stdout(), DisableBracketedPaste).is_err() {
self.bracketed_paste = true;
}
Err(error)
}
}
}
fn undo_modes(&mut self) {
if self.mouse_capture {
let _ = execute!(io::stdout(), DisableMouseCapture);
self.mouse_capture = false;
}
if self.bracketed_paste {
let _ = execute!(io::stdout(), DisableBracketedPaste);
self.bracketed_paste = false;
}
if self.keyboard_enhancement {
let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags);
self.keyboard_enhancement = false;
}
}
}
impl Drop for TerminalSession {
fn drop(&mut self) {
self.undo_modes();
if !thread::panicking() {
ratatui::restore();
}
}
}