1use std::io::{self, IsTerminal, Write};
7use std::sync::{Arc, Mutex};
8use std::time::Duration;
9
10use crossterm::{
11 event::{self, Event, KeyEvent, MouseEvent},
12 terminal as cterm,
13};
14use thiserror::Error;
15
16const fn mouse_tracking_enabled_for(is_macos: bool) -> bool {
19 !is_macos
20}
21
22const fn mouse_tracking_enabled_by_default() -> bool {
23 mouse_tracking_enabled_for(cfg!(target_os = "macos"))
24}
25
26fn keyboard_protocol_enabled() -> bool {
31 if let Some(value) = std::env::var_os("RPI_KITTY_KEYBOARD") {
32 return value != "0";
33 }
34 !cfg!(target_os = "macos")
35}
36
37#[derive(Debug, Error)]
39pub enum TerminalError {
40 #[error("IO error: {0}")]
41 Io(#[from] io::Error),
42 #[error("Terminal error: {0}")]
43 Terminal(String),
44}
45
46#[derive(Debug, Clone, Default)]
48pub struct TerminalInfo {
49 pub columns: usize,
50 pub rows: usize,
51 pub supports_true_color: bool,
52 pub supports_mouse: bool,
53 pub supports_kitty_keyboard: bool,
54}
55
56#[derive(Debug, Clone)]
58pub enum InputEvent {
59 Key(KeyEvent),
60 Mouse(MouseEvent),
61 Resize(usize, usize),
62 FocusGained,
63 FocusLost,
64}
65
66pub trait Terminal: Send + Sync {
70 fn info(&self) -> TerminalInfo;
72
73 fn columns(&self) -> usize {
75 self.info().columns
76 }
77
78 fn rows(&self) -> usize {
80 self.info().rows
81 }
82
83 fn write(&self, data: &str);
85
86 fn hide_cursor(&self);
88
89 fn show_cursor(&self);
91
92 fn move_cursor(&self, row: usize, col: usize);
94
95 fn clear_screen(&self);
97
98 fn set_title(&self, title: &str);
100
101 fn enable_mouse(&self);
103
104 fn disable_mouse(&self);
106
107 fn enter_raw_mode(&self);
115
116 fn exit_raw_mode(&self);
120
121 fn refresh_size(&self);
124
125 fn start(
127 &self,
128 on_input: Box<dyn Fn(InputEvent) + Send + Sync>,
129 on_resize: Box<dyn Fn() + Send + Sync>,
130 );
131
132 fn stop(&self);
134
135 fn is_tty(&self) -> bool;
137
138 fn set_progress(&self, active: bool);
140
141 fn flush(&self);
143}
144
145pub struct ProcessTerminal {
147 info: Mutex<TerminalInfo>,
148 running: Mutex<bool>,
149 writer: Mutex<Box<dyn Write + Send + Sync>>,
150 #[cfg(any(unix, test))]
151 keyboard: Mutex<KeyboardProtocolState>,
152}
153
154#[cfg(any(unix, test))]
155#[derive(Clone, Copy)]
156enum KeyboardProtocol {
157 Kitty,
158 Legacy,
159}
160
161#[cfg(any(unix, test))]
162#[derive(Default)]
163struct KeyboardProtocolState {
164 detected: Option<bool>,
165 active: Option<KeyboardProtocol>,
166}
167
168impl ProcessTerminal {
169 pub fn new() -> Self {
171 let (cols, rows) = cterm::size().unwrap_or((80, 24));
172 let info = TerminalInfo {
173 columns: cols as usize,
174 rows: rows as usize,
175 supports_true_color: true, supports_mouse: true,
177 supports_kitty_keyboard: false, };
179
180 Self {
181 info: Mutex::new(info),
182 running: Mutex::new(false),
183 writer: Mutex::new(Box::new(io::stdout())),
184 #[cfg(any(unix, test))]
185 keyboard: Mutex::new(KeyboardProtocolState::default()),
186 }
187 }
188
189 fn update_size(&self) {
191 if let Ok(size) = cterm::size() {
192 if let Ok(mut info) = self.info.lock() {
193 info.columns = size.0 as usize;
194 info.rows = size.1 as usize;
195 }
196 }
197 }
198
199 fn configure_mouse_tracking(&self) {
200 if mouse_tracking_enabled_by_default() {
201 self.enable_mouse();
202 } else {
203 self.disable_mouse();
204 }
205 }
206
207 #[cfg(any(unix, test))]
210 fn enable_keyboard_protocol(&self, detect: impl FnOnce() -> bool) {
211 let mut keyboard = self.keyboard.lock().unwrap_or_else(|p| p.into_inner());
212 if keyboard.active.is_some() {
213 return;
214 }
215 let supported = match keyboard.detected {
216 Some(supported) => supported,
217 None => {
218 self.write("\x1b[>7u");
222 self.flush();
223 let supported = detect();
224 keyboard.detected = Some(supported);
225 if !supported {
226 self.write("\x1b[<u");
227 }
228 if let Ok(mut info) = self.info.lock() {
229 info.supports_kitty_keyboard = supported;
230 }
231 if supported {
232 keyboard.active = Some(KeyboardProtocol::Kitty);
233 return;
234 }
235 false
236 }
237 };
238 let protocol = if supported {
239 self.write("\x1b[>7u");
240 KeyboardProtocol::Kitty
241 } else {
242 KeyboardProtocol::Legacy
246 };
247 keyboard.active = Some(protocol);
248 }
249
250 #[cfg(any(unix, test))]
251 fn disable_keyboard_protocol(&self) {
252 let mut keyboard = self.keyboard.lock().unwrap_or_else(|p| p.into_inner());
253 match keyboard.active.take() {
254 Some(KeyboardProtocol::Kitty) => self.write("\x1b[<u"),
255 Some(KeyboardProtocol::Legacy) | None => {}
256 }
257 }
258}
259
260impl Default for ProcessTerminal {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266impl Terminal for ProcessTerminal {
267 fn info(&self) -> TerminalInfo {
268 self.info.lock().map(|i| i.clone()).unwrap_or_default()
269 }
270
271 fn write(&self, data: &str) {
272 if let Ok(mut writer) = self.writer.lock() {
273 let _ = writer.write_all(data.as_bytes());
274 }
275 }
276
277 fn hide_cursor(&self) {
278 self.write("\x1b[?25l");
279 }
280
281 fn show_cursor(&self) {
282 self.write("\x1b[?25h");
283 }
284
285 fn move_cursor(&self, row: usize, col: usize) {
286 self.write(&format!("\x1b[{};{}H", row + 1, col + 1));
288 }
289
290 fn clear_screen(&self) {
291 self.write("\x1b[2J\x1b[H");
292 }
293
294 fn set_title(&self, title: &str) {
295 self.write(&format!("\x1b]2;{}\x07", title));
296 }
297
298 fn enable_mouse(&self) {
299 self.write("\x1b[?1000h\x1b[?1002h\x1b[?1006h");
301 }
302
303 fn disable_mouse(&self) {
304 self.write("\x1b[?1006l\x1b[?1002l\x1b[?1000l");
305 }
306
307 fn enter_raw_mode(&self) {
308 let _ = cterm::enable_raw_mode();
309 if let Ok(mut running) = self.running.lock() {
310 *running = true;
311 }
312 self.configure_mouse_tracking();
313 self.hide_cursor();
314 self.write("\x1b[?2004h");
318 #[cfg(unix)]
319 if keyboard_protocol_enabled() && self.is_tty() && io::stdin().is_terminal() {
320 self.enable_keyboard_protocol(|| {
321 cterm::supports_keyboard_enhancement().unwrap_or(false)
322 });
323 }
324 self.update_size();
325 self.flush();
326 }
327
328 fn exit_raw_mode(&self) {
329 self.write("\x1b[?2004l");
332 #[cfg(unix)]
333 self.disable_keyboard_protocol();
334 self.flush();
335 let _ = cterm::disable_raw_mode();
336 }
337
338 fn refresh_size(&self) {
339 self.update_size();
340 }
341
342 fn start(
343 &self,
344 on_input: Box<dyn Fn(InputEvent) + Send + Sync>,
345 on_resize: Box<dyn Fn() + Send + Sync>,
346 ) {
347 self.enter_raw_mode();
348
349 self.update_size();
351
352 let running = Arc::new(Mutex::new(true));
354 let running_clone = running.clone();
355 let info_clone = Arc::new(Mutex::new(self.info()));
356
357 std::thread::spawn(move || {
358 loop {
359 if !*running_clone.lock().unwrap() {
361 break;
362 }
363
364 match event::poll(Duration::from_millis(100)) {
366 Ok(true) => {
367 if let Ok(event) = event::read() {
368 match event {
369 Event::Key(key) => {
370 on_input(InputEvent::Key(normalize_key_event(key)))
371 }
372 Event::Mouse(mouse) => on_input(InputEvent::Mouse(mouse)),
373 Event::Resize(cols, rows) => {
374 if let Ok(mut info) = info_clone.lock() {
375 info.columns = cols as usize;
376 info.rows = rows as usize;
377 }
378 on_resize();
379 }
380 Event::FocusGained => on_input(InputEvent::FocusGained),
381 Event::FocusLost => on_input(InputEvent::FocusLost),
382 Event::Paste(_) => { }
383 }
384 }
385 }
386 Ok(false) => {} Err(_) => break, }
389 }
390 });
391
392 if let Ok(mut r) = self.running.lock() {
394 *r = true;
395 }
396 }
397
398 fn stop(&self) {
399 if let Ok(mut running) = self.running.lock() {
400 *running = false;
401 }
402
403 self.disable_mouse();
404 self.show_cursor();
405 self.write("\x1b[?2004l");
407 #[cfg(unix)]
408 self.disable_keyboard_protocol();
409 self.flush();
410
411 let _ = cterm::disable_raw_mode();
413 }
414
415 fn is_tty(&self) -> bool {
416 std::io::stdout().is_terminal()
417 }
418
419 fn set_progress(&self, active: bool) {
420 let state = if active { 1 } else { 0 };
425 self.write(&format!("\x1b]9;4;{state};0\x07"));
426 self.flush();
427 }
428
429 fn flush(&self) {
430 if let Ok(mut writer) = self.writer.lock() {
431 let _ = writer.flush();
432 }
433 }
434}
435
436pub fn normalize_key_event(key: KeyEvent) -> KeyEvent {
440 #[cfg(target_os = "macos")]
441 if key.code == event::KeyCode::Enter
442 && key.modifiers.is_empty()
443 && local_apple_terminal(
444 std::env::var("TERM_PROGRAM").ok().as_deref(),
445 ["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"]
446 .iter()
447 .any(|name| std::env::var_os(name).is_some()),
448 )
449 {
450 #[link(name = "CoreGraphics", kind = "framework")]
451 extern "C" {
452 fn CGEventSourceFlagsState(state_id: i32) -> u64;
453 }
454 let shift_pressed = unsafe { CGEventSourceFlagsState(0) } & (1 << 17) != 0;
457 return normalize_native_shift_enter(key, shift_pressed);
458 }
459 key
460}
461
462#[cfg(any(target_os = "macos", test))]
463fn local_apple_terminal(term_program: Option<&str>, ssh: bool) -> bool {
464 term_program == Some("Apple_Terminal") && !ssh
465}
466
467#[cfg(any(target_os = "macos", test))]
468fn normalize_native_shift_enter(mut key: KeyEvent, shift_pressed: bool) -> KeyEvent {
469 if key.code == event::KeyCode::Enter && key.modifiers.is_empty() && shift_pressed {
470 key.modifiers.insert(event::KeyModifiers::SHIFT);
471 }
472 key
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[derive(Clone)]
480 struct RecordedOutput(Arc<Mutex<Vec<u8>>>);
481 impl Write for RecordedOutput {
482 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
483 self.0.lock().unwrap().extend_from_slice(bytes);
484 Ok(bytes.len())
485 }
486 fn flush(&mut self) -> io::Result<()> {
487 Ok(())
488 }
489 }
490
491 #[test]
492 fn kitty_protocol_preserves_ime_text_and_restores_once_per_activation() {
493 let terminal = ProcessTerminal::new();
494 let output = RecordedOutput(Arc::new(Mutex::new(Vec::new())));
495 *terminal.writer.lock().unwrap() = Box::new(output.clone());
496 terminal.enable_keyboard_protocol(|| true);
497 assert!(terminal.info().supports_kitty_keyboard);
498 terminal.enable_keyboard_protocol(|| panic!("active protocol must not be pushed twice"));
499 terminal.disable_keyboard_protocol();
500 terminal.disable_keyboard_protocol();
501 terminal.enable_keyboard_protocol(|| panic!("resume must not query stdin"));
502 terminal.disable_keyboard_protocol();
503 let bytes = output.0.lock().unwrap().clone();
504 assert_eq!(bytes, b"\x1b[>7u\x1b[<u\x1b[>7u\x1b[<u");
505 let sequence = std::str::from_utf8(&bytes)
507 .unwrap()
508 .split('u')
509 .next()
510 .unwrap();
511 let mask: u8 = sequence.strip_prefix("\x1b[>").unwrap().parse().unwrap();
512 let flags = event::KeyboardEnhancementFlags::from_bits(mask).unwrap();
513 assert!(flags.contains(event::KeyboardEnhancementFlags::REPORT_EVENT_TYPES));
514 assert!(flags.contains(event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES));
515 assert!(flags.contains(event::KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS));
516 assert!(!flags.contains(event::KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES));
517 assert_eq!(mask & 16, 0);
519 }
520
521 #[test]
522 fn legacy_terminal_does_not_enable_an_encoding_the_reader_cannot_parse() {
523 let terminal = ProcessTerminal::new();
524 let output = RecordedOutput(Arc::new(Mutex::new(Vec::new())));
525 *terminal.writer.lock().unwrap() = Box::new(output.clone());
526 terminal.enable_keyboard_protocol(|| false);
527 assert!(!terminal.info().supports_kitty_keyboard);
528 terminal.disable_keyboard_protocol();
529 terminal.enable_keyboard_protocol(|| panic!("resume must reuse detection"));
530 terminal.disable_keyboard_protocol();
531 assert_eq!(output.0.lock().unwrap().as_slice(), b"\x1b[>7u\x1b[<u");
532 }
533
534 #[test]
535 fn native_shift_enter_only_changes_unmodified_enter_in_local_apple_terminal() {
536 use event::{KeyCode, KeyModifiers};
537 assert!(local_apple_terminal(Some("Apple_Terminal"), false));
538 assert!(!local_apple_terminal(Some("Apple_Terminal"), true));
539 assert!(!local_apple_terminal(Some("iTerm.app"), false));
540 let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
541 assert_eq!(
542 normalize_native_shift_enter(enter, true).modifiers,
543 KeyModifiers::SHIFT
544 );
545 assert_eq!(normalize_native_shift_enter(enter, false), enter);
546 for key in [
547 KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE),
548 KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL),
549 KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT),
550 ] {
551 assert_eq!(normalize_native_shift_enter(key, true), key);
552 }
553 }
554
555 #[test]
556 fn native_shift_enter_inserts_a_line_before_plain_enter_submits() {
557 use event::{KeyCode, KeyModifiers};
558 let editor = crate::Editor::simple();
559 let submitted = Arc::new(Mutex::new(Vec::new()));
560 let capture = submitted.clone();
561 editor.on_submit(Arc::new(move |text| {
562 capture.lock().unwrap().push(text.to_string())
563 }));
564 editor.insert("first");
565 let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
566 editor.handle_key(normalize_native_shift_enter(enter, true));
567 editor.insert("second");
568 assert_eq!(editor.get_text(), "first\nsecond");
569 assert!(submitted.lock().unwrap().is_empty());
570 editor.handle_key(normalize_native_shift_enter(enter, false));
571 assert_eq!(submitted.lock().unwrap().as_slice(), &["first\nsecond"]);
572 }
573
574 #[test]
575 fn mouse_tracking_default_preserves_native_selection_on_macos() {
576 assert!(!mouse_tracking_enabled_for(true));
577 assert!(mouse_tracking_enabled_for(false));
578 }
579
580 #[test]
581 fn ime_committed_utf8_text_reaches_editor_without_submitting() {
582 use event::{KeyCode, KeyModifiers};
583 let editor = crate::Editor::simple();
584 let submitted = Arc::new(Mutex::new(Vec::new()));
585 let capture = submitted.clone();
586 editor.on_submit(Arc::new(move |text| {
587 capture.lock().unwrap().push(text.to_string())
588 }));
589 let committed = "你好,世界𠮷 abc";
590 for ch in committed.chars() {
591 editor.handle_key(normalize_key_event(KeyEvent::new(
592 KeyCode::Char(ch),
593 KeyModifiers::NONE,
594 )));
595 }
596 assert_eq!(editor.get_text(), committed);
597 assert!(submitted.lock().unwrap().is_empty());
598 editor.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
599 assert_eq!(submitted.lock().unwrap().as_slice(), &[committed]);
600 }
601}