fresh/services/
terminal_modes.rs1use anyhow::Result;
17use crossterm::{
18 cursor::SetCursorStyle,
19 event::{
20 DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
21 KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
22 },
23 terminal::{
24 disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, EnterAlternateScreen,
25 LeaveAlternateScreen,
26 },
27 ExecutableCommand,
28};
29use std::io::{stdout, Write};
30
31pub mod sequences {
37 pub const ENTER_ALTERNATE_SCREEN: &[u8] = b"\x1b[?1049h";
39 pub const LEAVE_ALTERNATE_SCREEN: &[u8] = b"\x1b[?1049l";
40
41 pub const ENABLE_MOUSE_CLICK: &[u8] = b"\x1b[?1000h";
43 pub const ENABLE_MOUSE_DRAG: &[u8] = b"\x1b[?1002h";
44 pub const ENABLE_MOUSE_MOTION: &[u8] = b"\x1b[?1003h";
45 pub const ENABLE_SGR_MOUSE: &[u8] = b"\x1b[?1006h";
46 pub const DISABLE_MOUSE_CLICK: &[u8] = b"\x1b[?1000l";
47 pub const DISABLE_MOUSE_DRAG: &[u8] = b"\x1b[?1002l";
48 pub const DISABLE_MOUSE_MOTION: &[u8] = b"\x1b[?1003l";
49 pub const DISABLE_SGR_MOUSE: &[u8] = b"\x1b[?1006l";
50
51 pub const ENABLE_FOCUS_EVENTS: &[u8] = b"\x1b[?1004h";
53 pub const DISABLE_FOCUS_EVENTS: &[u8] = b"\x1b[?1004l";
54
55 pub const ENABLE_BRACKETED_PASTE: &[u8] = b"\x1b[?2004h";
57 pub const DISABLE_BRACKETED_PASTE: &[u8] = b"\x1b[?2004l";
58
59 pub const SHOW_CURSOR: &[u8] = b"\x1b[?25h";
61 pub const HIDE_CURSOR: &[u8] = b"\x1b[?25l";
62 pub const RESET_CURSOR_STYLE: &[u8] = b"\x1b[0 q";
63
64 pub const RESET_ATTRIBUTES: &[u8] = b"\x1b[0m";
66}
67
68#[derive(Debug, Clone)]
70pub struct KeyboardConfig {
71 pub disambiguate_escape_codes: bool,
73 pub report_event_types: bool,
75 pub report_alternate_keys: bool,
77 pub report_all_keys_as_escape_codes: bool,
79}
80
81impl Default for KeyboardConfig {
82 fn default() -> Self {
83 Self {
84 disambiguate_escape_codes: true,
85 report_event_types: false,
86 report_alternate_keys: true,
87 report_all_keys_as_escape_codes: false,
88 }
89 }
90}
91
92impl KeyboardConfig {
93 pub fn to_flags(&self) -> KeyboardEnhancementFlags {
95 let mut flags = KeyboardEnhancementFlags::empty();
96 if self.disambiguate_escape_codes {
97 flags |= KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES;
98 }
99 if self.report_event_types {
100 flags |= KeyboardEnhancementFlags::REPORT_EVENT_TYPES;
101 }
102 if self.report_alternate_keys {
103 flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
104 }
105 if self.report_all_keys_as_escape_codes {
106 flags |= KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
107 }
108 flags
109 }
110
111 pub fn any_enabled(&self) -> bool {
113 self.disambiguate_escape_codes
114 || self.report_event_types
115 || self.report_alternate_keys
116 || self.report_all_keys_as_escape_codes
117 }
118}
119
120#[derive(Debug, Default)]
125pub struct TerminalModes {
126 raw_mode: bool,
127 alternate_screen: bool,
128 mouse_capture: bool,
129 keyboard_enhancement: bool,
130 bracketed_paste: bool,
131}
132
133impl TerminalModes {
134 pub fn new() -> Self {
136 Self::default()
137 }
138
139 pub fn enable(keyboard_config: Option<&KeyboardConfig>) -> Result<Self> {
147 let mut modes = Self::new();
148 let keyboard_config = keyboard_config.cloned().unwrap_or_default();
149
150 if let Err(e) = enable_raw_mode() {
152 tracing::error!("Failed to enable raw mode: {}", e);
153 return Err(e.into());
154 }
155 modes.raw_mode = true;
156 tracing::debug!("Enabled raw mode");
157
158 if let Err(e) = stdout().execute(EnterAlternateScreen) {
166 tracing::error!("Failed to enter alternate screen: {}", e);
167 modes.undo();
168 return Err(e.into());
169 }
170 modes.alternate_screen = true;
171 tracing::debug!("Entered alternate screen");
172
173 if keyboard_config.any_enabled() {
177 match supports_keyboard_enhancement() {
178 Ok(true) => {
179 let flags = keyboard_config.to_flags();
180 if let Err(e) = stdout().execute(PushKeyboardEnhancementFlags(flags)) {
181 tracing::warn!("Failed to enable keyboard enhancement: {}", e);
182 } else {
184 modes.keyboard_enhancement = true;
185 tracing::debug!("Enabled keyboard enhancement flags: {:?}", flags);
186 }
187 }
188 Ok(false) => {
189 tracing::info!("Keyboard enhancement not supported by terminal");
190 }
191 Err(e) => {
192 tracing::warn!("Failed to query keyboard enhancement support: {}", e);
193 }
194 }
195 } else {
196 tracing::debug!("Keyboard enhancement disabled by config");
197 }
198
199 #[cfg(not(windows))]
205 {
206 if let Err(e) = stdout().execute(EnableMouseCapture) {
207 tracing::warn!("Failed to enable mouse capture: {}", e);
208 } else {
210 modes.mouse_capture = true;
211 tracing::debug!("Enabled mouse capture");
212 }
213 }
214 #[cfg(windows)]
215 {
216 modes.mouse_capture = true;
217 tracing::debug!(
218 "Skipped crossterm EnableMouseCapture on Windows (handled by win_vt_input)"
219 );
220 }
221
222 if let Err(e) = stdout().execute(EnableBracketedPaste) {
224 tracing::warn!("Failed to enable bracketed paste: {}", e);
225 } else {
227 modes.bracketed_paste = true;
228 tracing::debug!("Enabled bracketed paste mode");
229 }
230
231 Ok(modes)
232 }
233
234 #[allow(clippy::let_underscore_must_use)]
239 pub fn undo(&mut self) {
240 if self.mouse_capture {
246 #[cfg(not(windows))]
247 let _ = stdout().execute(DisableMouseCapture);
248 self.mouse_capture = false;
249 tracing::debug!("Disabled mouse capture");
250 }
251
252 if self.bracketed_paste {
254 let _ = stdout().execute(DisableBracketedPaste);
255 self.bracketed_paste = false;
256 tracing::debug!("Disabled bracketed paste");
257 }
258
259 let _ = stdout().execute(SetCursorStyle::DefaultUserShape);
261
262 crate::view::theme::Theme::reset_terminal_cursor_color();
264
265 if self.keyboard_enhancement {
267 let _ = stdout().execute(PopKeyboardEnhancementFlags);
268 self.keyboard_enhancement = false;
269 tracing::debug!("Popped keyboard enhancement flags");
270 }
271
272 if self.raw_mode {
274 let _ = disable_raw_mode();
275 self.raw_mode = false;
276 tracing::debug!("Disabled raw mode");
277 }
278
279 if self.alternate_screen {
281 let _ = stdout().execute(LeaveAlternateScreen);
282 self.alternate_screen = false;
283 tracing::debug!("Left alternate screen");
284 }
285
286 let _ = stdout().flush();
288 }
289
290 pub fn raw_mode_enabled(&self) -> bool {
292 self.raw_mode
293 }
294
295 pub fn keyboard_enhancement_enabled(&self) -> bool {
297 self.keyboard_enhancement
298 }
299
300 pub fn mouse_capture_enabled(&self) -> bool {
302 self.mouse_capture
303 }
304
305 pub fn bracketed_paste_enabled(&self) -> bool {
307 self.bracketed_paste
308 }
309
310 pub fn alternate_screen_enabled(&self) -> bool {
312 self.alternate_screen
313 }
314}
315
316impl Drop for TerminalModes {
317 fn drop(&mut self) {
318 self.undo();
319 }
320}
321
322#[allow(clippy::let_underscore_must_use)]
328pub fn emergency_cleanup() {
329 let _ = stdout().execute(DisableMouseCapture);
332
333 let _ = stdout().execute(DisableBracketedPaste);
335
336 let _ = stdout().execute(SetCursorStyle::DefaultUserShape);
338
339 crate::view::theme::Theme::reset_terminal_cursor_color();
341
342 let _ = stdout().execute(PopKeyboardEnhancementFlags);
344
345 let _ = disable_raw_mode();
347
348 let _ = stdout().execute(LeaveAlternateScreen);
350
351 let _ = stdout().flush();
353}