1use crossterm::cursor::MoveTo;
5use crossterm::event::{
6 DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, KeyboardEnhancementFlags,
7 PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
8};
9use crossterm::execute;
10use crossterm::terminal::{Clear, ClearType};
11use ratatui::backend::CrosstermBackend;
12use ratatui::{DefaultTerminal, Terminal, TerminalOptions, Viewport};
13use std::io;
14use std::thread;
15
16pub const INLINE_SCROLLBACK_RESERVE: u16 = 2;
19
20pub fn inline_viewport_height(terminal_height: u16) -> u16 {
21 if terminal_height == 0 { 0 } else { terminal_height.saturating_sub(INLINE_SCROLLBACK_RESERVE).max(1) }
22}
23
24pub fn inline_viewport_needs_resync(terminal_height: u16, current_viewport_height: u16) -> bool {
32 let height = inline_viewport_height(terminal_height);
33 height != 0 && height != current_viewport_height
34}
35
36pub(crate) struct TerminalSession {
40 terminal: DefaultTerminal,
41 keyboard_enhancement: bool,
42 bracketed_paste: bool,
43 mouse_capture: bool,
44}
45
46impl TerminalSession {
47 pub(crate) fn enter(viewport: Viewport) -> io::Result<Self> {
53 let terminal = match ratatui::try_init_with_options(TerminalOptions { viewport }) {
54 Ok(terminal) => terminal,
55 Err(error) => {
56 ratatui::restore();
59 return Err(error);
60 }
61 };
62 let mut session = Self { terminal, keyboard_enhancement: false, bracketed_paste: false, mouse_capture: false };
63 session.try_keyboard_enhancement();
64 session.apply_bracketed_paste()?;
67 Ok(session)
68 }
69
70 pub(crate) fn terminal_mut(&mut self) -> &mut DefaultTerminal {
71 &mut self.terminal
72 }
73
74 pub(crate) fn set_mouse_capture(&mut self, enabled: bool) {
77 if enabled == self.mouse_capture {
78 return;
79 }
80 if enabled {
81 let enabled_ok = execute!(io::stdout(), EnableMouseCapture).is_ok();
84 self.mouse_capture = enabled_ok || execute!(io::stdout(), DisableMouseCapture).is_err();
85 } else if execute!(io::stdout(), DisableMouseCapture).is_ok() {
86 self.mouse_capture = false;
87 }
88 }
89
90 pub(crate) fn resync_inline_viewport(&mut self) -> io::Result<()> {
94 let terminal_height = self.terminal.size()?.height;
95 let height = inline_viewport_height(terminal_height);
96 if !inline_viewport_needs_resync(terminal_height, self.terminal.get_frame().area().height) {
97 return Ok(());
98 }
99
100 execute!(io::stdout(), MoveTo(0, terminal_height - height), Clear(ClearType::FromCursorDown))?;
101 self.terminal = Terminal::with_options(
102 CrosstermBackend::new(io::stdout()),
103 TerminalOptions { viewport: Viewport::Inline(height) },
104 )?;
105 Ok(())
106 }
107
108 fn try_keyboard_enhancement(&mut self) {
109 let flags = KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
110 | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
111 | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
112 if execute!(io::stdout(), PushKeyboardEnhancementFlags(flags)).is_ok() {
113 self.keyboard_enhancement = true;
114 } else if execute!(io::stdout(), PopKeyboardEnhancementFlags).is_err() {
115 self.keyboard_enhancement = true;
118 }
119 }
120
121 fn apply_bracketed_paste(&mut self) -> io::Result<()> {
122 match execute!(io::stdout(), EnableBracketedPaste) {
123 Ok(()) => {
124 self.bracketed_paste = true;
125 Ok(())
126 }
127 Err(error) => {
128 if execute!(io::stdout(), DisableBracketedPaste).is_err() {
130 self.bracketed_paste = true;
131 }
132 Err(error)
133 }
134 }
135 }
136
137 fn undo_modes(&mut self) {
138 if self.mouse_capture {
139 let _ = execute!(io::stdout(), DisableMouseCapture);
140 self.mouse_capture = false;
141 }
142 if self.bracketed_paste {
143 let _ = execute!(io::stdout(), DisableBracketedPaste);
144 self.bracketed_paste = false;
145 }
146 if self.keyboard_enhancement {
147 let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags);
148 self.keyboard_enhancement = false;
149 }
150 }
151}
152
153impl Drop for TerminalSession {
154 fn drop(&mut self) {
155 self.undo_modes();
158 if !thread::panicking() {
162 ratatui::restore();
163 }
164 }
165}