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 if let Err(e) = stdout().execute(EnableMouseCapture) {
201 tracing::warn!("Failed to enable mouse capture: {}", e);
202 } else {
204 modes.mouse_capture = true;
205 tracing::debug!("Enabled mouse capture");
206 }
207
208 if let Err(e) = stdout().execute(EnableBracketedPaste) {
210 tracing::warn!("Failed to enable bracketed paste: {}", e);
211 } else {
213 modes.bracketed_paste = true;
214 tracing::debug!("Enabled bracketed paste mode");
215 }
216
217 Ok(modes)
218 }
219
220 #[allow(clippy::let_underscore_must_use)]
225 pub fn undo(&mut self) {
226 if self.mouse_capture {
229 let _ = stdout().execute(DisableMouseCapture);
230 self.mouse_capture = false;
231 tracing::debug!("Disabled mouse capture");
232 }
233
234 if self.bracketed_paste {
236 let _ = stdout().execute(DisableBracketedPaste);
237 self.bracketed_paste = false;
238 tracing::debug!("Disabled bracketed paste");
239 }
240
241 let _ = stdout().execute(SetCursorStyle::DefaultUserShape);
243
244 crate::view::theme::Theme::reset_terminal_cursor_color();
246
247 if self.keyboard_enhancement {
249 let _ = stdout().execute(PopKeyboardEnhancementFlags);
250 self.keyboard_enhancement = false;
251 tracing::debug!("Popped keyboard enhancement flags");
252 }
253
254 if self.raw_mode {
256 let _ = disable_raw_mode();
257 self.raw_mode = false;
258 tracing::debug!("Disabled raw mode");
259 }
260
261 if self.alternate_screen {
263 let _ = stdout().execute(LeaveAlternateScreen);
264 self.alternate_screen = false;
265 tracing::debug!("Left alternate screen");
266 }
267
268 let _ = stdout().flush();
270 }
271
272 pub fn raw_mode_enabled(&self) -> bool {
274 self.raw_mode
275 }
276
277 pub fn keyboard_enhancement_enabled(&self) -> bool {
279 self.keyboard_enhancement
280 }
281
282 pub fn mouse_capture_enabled(&self) -> bool {
284 self.mouse_capture
285 }
286
287 pub fn bracketed_paste_enabled(&self) -> bool {
289 self.bracketed_paste
290 }
291
292 pub fn alternate_screen_enabled(&self) -> bool {
294 self.alternate_screen
295 }
296}
297
298impl Drop for TerminalModes {
299 fn drop(&mut self) {
300 self.undo();
301 }
302}
303
304#[allow(clippy::let_underscore_must_use)]
310pub fn emergency_cleanup() {
311 let _ = stdout().execute(DisableMouseCapture);
314
315 let _ = stdout().execute(DisableBracketedPaste);
317
318 let _ = stdout().execute(SetCursorStyle::DefaultUserShape);
320
321 crate::view::theme::Theme::reset_terminal_cursor_color();
323
324 let _ = stdout().execute(PopKeyboardEnhancementFlags);
326
327 let _ = disable_raw_mode();
329
330 let _ = stdout().execute(LeaveAlternateScreen);
332
333 let _ = stdout().flush();
335}