1mod buffer;
4#[cfg(feature = "terminal")]
5mod copy_mode;
6mod events;
7#[cfg(feature = "terminal-images")]
8mod graphics;
9mod layout;
10mod mod_private;
11mod node;
12mod osc;
13mod pty;
14mod reconcile;
15mod screen;
16mod scrollback_ledger;
17#[cfg(feature = "terminal")]
18mod selection;
19
20pub use buffer::TerminalBuffer;
21#[cfg(feature = "terminal")]
22pub use copy_mode::{CopyModeAction, CopyModeGrid, TerminalCopyMode};
23pub use events::{
24 KittyKeyboardFlags, MouseEncoding, MouseMode, MouseModeState, TerminalInputEvent,
25 TerminalInputKind, TerminalKeyModes, TerminalPasteShortcutBehavior, encode_paste,
26 focus_sequences, key_event_to_bytes, mouse_event_to_bytes, paste_sequences,
27 terminal_selection_text,
28};
29#[cfg(feature = "terminal-images")]
30pub use graphics::{TerminalImage, TerminalImageCrop, TerminalImagePlacement};
31pub use mod_private::Terminal;
32pub use osc::{
33 TerminalCommandPhase, TerminalSemanticEvent, TerminalSemanticState, TerminalWorkingDirectory,
34 TerminalWorkingDirectorySource,
35};
36#[cfg(unix)]
37pub use pty::TerminalPtyHandoff;
38pub use pty::{TerminalPty, TerminalPtyConfig, TerminalPtyError, TerminalPtyEvent};
39pub use screen::{
40 SemanticMark, SemanticMarkKind, TerminalCellSize, TerminalColorPalette, TerminalDecoration,
41 TerminalRenderSnapshot, TerminalScreen, TerminalScreenHandle, TerminalViewport,
42};
43#[cfg(feature = "terminal")]
44pub use selection::{
45 ScrollbackLineage, TerminalPos, TerminalSelection, TerminalSelectionEvent, absolute_line,
46 from_viewport, to_viewport, viewport_row,
47};
48
49pub(crate) use layout::{measure_terminal, terminal_content_layout, terminal_mouse_content_rect};
50pub(crate) use node::{TerminalNode, apply_terminal_selection_input};
51pub(crate) use reconcile::reconcile_terminal;
52
53#[cfg(feature = "terminal")]
54pub(crate) fn terminal_node_selection_text(
55 node: &TerminalNode,
56 sel: &TerminalSelection,
57 endpoint: crate::utils::SelectionEnd,
58 trim_row_end: bool,
59) -> String {
60 if let Some(screen) = node.screen.as_ref() {
61 return screen.selection_display_text(sel, endpoint, trim_row_end);
62 }
63 let Some(projected) = to_viewport(
64 sel,
65 node.scrollback_offset,
66 node.total_scrollback_rows,
67 node.viewport_rows,
68 ) else {
69 return String::new();
70 };
71 events::terminal_selection_text_with(&node.lines, &projected, endpoint, trim_row_end)
72}
73
74use crate::callback::{Callback, KeyHandler};
75use crate::core::element::{Element, ElementKind};
76use crate::style::{
77 BorderStyle, CaretShape, Color, Length, Padding, ScrollbarConfig, ScrollbarVariant, Span,
78 Style, StyleSlot,
79};
80use crate::widgets::ScrollEvent;
81use std::sync::Arc;
82
83impl Default for Terminal {
84 fn default() -> Self {
85 Self {
86 content: Arc::from(""),
87 cursor_row: 0,
88 cursor_col: 0,
89 show_cursor: true,
90 cursor_shape: CaretShape::Block,
91 cursor_blinking: true,
92 caret_color: None,
93 color_lines: None,
94 color_cache_key: 0,
95 screen: None,
96 decorations: Arc::from([] as [TerminalDecoration; 0]),
97 scrollback_offset: 0,
98 total_scrollback_rows: 0,
99 mouse_mode: MouseModeState::default(),
100 key_modes: TerminalKeyModes::default(),
101 #[cfg(feature = "terminal-images")]
102 images: Arc::from([]),
103 paste_shortcut_behavior: TerminalPasteShortcutBehavior::Forward,
104 selection: None,
105 selection_controlled: false,
106 selection_style: StyleSlot::Inherit,
107 on_selection: None,
108 on_resize: None,
109 on_mouse_forward: None,
110 scroll_wheel: true,
111 on_scroll: None,
112 on_scroll_to: None,
113 style: Style::default(),
114 hover_style: StyleSlot::Inherit,
115 focus_style: StyleSlot::Inherit,
116 focus_content_style: Style::default(),
117 border: false,
118 border_style: BorderStyle::default(),
119 padding: Padding::default(),
120 scrollbar: true,
121 scrollbar_variant: ScrollbarVariant::default(),
122 scrollbar_gap: 0,
123 scrollbar_thumb: None,
124 scrollbar_thumb_style: None,
125 scrollbar_thumb_focus_style: None,
126 scrollbar_track_style: None,
127 h_scrollbar: true,
128 h_scrollbar_variant: ScrollbarVariant::default(),
129 width: Length::Flex(1),
130 height: Length::Flex(1),
131 focusable: true,
132 tab_stop: true,
133 on_focus: None,
134 on_blur: None,
135 on_key: None,
136 on_input: None,
137 }
138 }
139}
140
141impl Terminal {
142 pub fn new() -> Self {
144 Self::default()
145 }
146
147 pub fn content(mut self, content: impl Into<Arc<str>>) -> Self {
149 self.content = content.into();
150 self
151 }
152
153 pub fn cursor(mut self, cursor: usize) -> Self {
155 let (row, col) = byte_to_row_col(self.content.as_ref(), cursor);
156 self.cursor_row = row;
157 self.cursor_col = col;
158 self
159 }
160
161 pub fn cursor_position(mut self, row: u16, col: u16) -> Self {
163 self.cursor_row = row;
164 self.cursor_col = col;
165 self
166 }
167
168 pub fn show_cursor(mut self, show_cursor: bool) -> Self {
170 self.show_cursor = show_cursor;
171 self
172 }
173
174 pub fn cursor_shape(mut self, shape: CaretShape) -> Self {
176 self.cursor_shape = shape;
177 self
178 }
179
180 pub fn cursor_blinking(mut self, blinking: bool) -> Self {
182 self.cursor_blinking = blinking;
183 self
184 }
185
186 pub fn caret_color(mut self, color: Color) -> Self {
188 self.caret_color = Some(color);
189 self
190 }
191
192 pub fn color_lines(mut self, color_lines: Arc<[Vec<Span>]>, cache_key: u64) -> Self {
197 self.color_lines = Some(color_lines);
198 self.color_cache_key = cache_key;
199 self
200 }
201
202 pub fn screen(mut self, screen: impl Into<TerminalScreenHandle>) -> Self {
215 self.screen = Some(screen.into());
216 self
217 }
218
219 pub fn decorations(mut self, decorations: impl Into<Arc<[TerminalDecoration]>>) -> Self {
225 self.decorations = decorations.into();
226 self
227 }
228
229 pub fn snapshot(mut self, snapshot: TerminalRenderSnapshot) -> Self {
231 self.content = snapshot.text;
232 self.cursor_row = snapshot.cursor_row;
233 self.cursor_col = snapshot.cursor_col;
234 self.show_cursor = snapshot.cursor_visible;
235 self.cursor_shape = snapshot.cursor_shape;
236 self.cursor_blinking = snapshot.cursor_blinking;
237 self.color_lines = Some(snapshot.color_lines);
238 self.color_cache_key = snapshot.sequence;
239 self.scrollback_offset = snapshot.scrollback_offset;
240 self.total_scrollback_rows = snapshot.total_scrollback_rows;
241 self.mouse_mode = snapshot.mouse_mode;
242 self.key_modes = snapshot.key_modes;
243 #[cfg(feature = "terminal-images")]
244 {
245 self.images = snapshot.images;
246 }
247 self
248 }
249
250 pub fn key_modes(mut self, key_modes: TerminalKeyModes) -> Self {
255 self.key_modes = key_modes;
256 self
257 }
258
259 pub fn paste_shortcut_behavior(mut self, behavior: TerminalPasteShortcutBehavior) -> Self {
261 self.paste_shortcut_behavior = behavior;
262 self
263 }
264
265 pub fn style(mut self, style: Style) -> Self {
267 self.style = style;
268 self
269 }
270
271 pub fn hover_style(mut self, style: Style) -> Self {
273 self.hover_style = StyleSlot::Replace(style);
274 self
275 }
276
277 pub fn extend_hover_style(mut self, style: Style) -> Self {
279 self.hover_style = StyleSlot::Extend(style);
280 self
281 }
282
283 pub fn inherit_hover_style(mut self) -> Self {
285 self.hover_style = StyleSlot::Inherit;
286 self
287 }
288
289 pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
291 self.hover_style = slot;
292 self
293 }
294
295 pub fn focus_style(mut self, style: Style) -> Self {
297 self.focus_style = StyleSlot::Replace(style);
298 self
299 }
300
301 pub fn extend_focus_style(mut self, style: Style) -> Self {
303 self.focus_style = StyleSlot::Extend(style);
304 self
305 }
306
307 pub fn inherit_focus_style(mut self) -> Self {
309 self.focus_style = StyleSlot::Inherit;
310 self
311 }
312
313 pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
315 self.focus_style = slot;
316 self
317 }
318
319 pub fn focus_content_style(mut self, style: Style) -> Self {
321 self.focus_content_style = style;
322 self
323 }
324
325 pub fn border(mut self, border: bool) -> Self {
327 self.border = border;
328 self
329 }
330
331 pub fn border_style(mut self, border_style: BorderStyle) -> Self {
333 self.border_style = border_style;
334 self
335 }
336
337 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
339 self.padding = padding.into();
340 self
341 }
342
343 pub fn scrollbar(mut self, scrollbar: bool) -> Self {
345 self.scrollbar = scrollbar;
346 self
347 }
348
349 pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
351 self.scrollbar_variant = config.variant;
352 self.scrollbar_gap = config.gap;
353 self.scrollbar_thumb = config.thumb;
354 self.scrollbar_thumb_style = config.thumb_style;
355 self.scrollbar_thumb_focus_style = config.thumb_focus_style;
356 self.scrollbar_track_style = config.track_style;
357 self
358 }
359
360 pub fn h_scrollbar(mut self, h_scrollbar: bool) -> Self {
362 self.h_scrollbar = h_scrollbar;
363 self
364 }
365
366 pub fn h_scrollbar_variant(mut self, style: ScrollbarVariant) -> Self {
368 self.h_scrollbar_variant = style;
369 self
370 }
371
372 pub fn scroll_wheel(mut self, scroll_wheel: bool) -> Self {
374 self.scroll_wheel = scroll_wheel;
375 self
376 }
377
378 pub fn on_scroll(mut self, cb: Callback<ScrollEvent>) -> Self {
380 self.on_scroll = Some(cb);
381 self
382 }
383
384 pub fn on_scroll_to(mut self, cb: Callback<usize>) -> Self {
390 self.on_scroll_to = Some(cb);
391 self
392 }
393
394 pub fn selection_style(mut self, style: Style) -> Self {
396 self.selection_style = StyleSlot::Replace(style);
397 self
398 }
399
400 pub fn extend_selection_style(mut self, style: Style) -> Self {
402 self.selection_style = StyleSlot::Extend(style);
403 self
404 }
405
406 pub fn inherit_selection_style(mut self) -> Self {
408 self.selection_style = StyleSlot::Inherit;
409 self
410 }
411
412 pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
414 self.selection_style = slot;
415 self
416 }
417
418 pub fn selection(mut self, selection: Option<TerminalSelection>) -> Self {
420 self.selection = selection;
421 self.selection_controlled = true;
422 self
423 }
424
425 pub fn on_selection(mut self, cb: Callback<TerminalSelectionEvent>) -> Self {
427 self.on_selection = Some(cb);
428 self
429 }
430
431 pub fn on_resize(mut self, cb: Callback<TerminalViewport>) -> Self {
436 self.on_resize = Some(cb);
437 self
438 }
439
440 pub fn on_mouse_forward(mut self, cb: Callback<Vec<u8>>) -> Self {
442 self.on_mouse_forward = Some(cb);
443 self
444 }
445
446 pub fn width(mut self, width: Length) -> Self {
448 self.width = width;
449 self
450 }
451
452 pub fn height(mut self, height: Length) -> Self {
454 self.height = height;
455 self
456 }
457
458 pub fn focusable(mut self, focusable: bool) -> Self {
460 self.focusable = focusable;
461 self
462 }
463
464 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
466 self.tab_stop = tab_stop;
467 self
468 }
469
470 pub fn on_focus(mut self, cb: Callback<()>) -> Self {
472 self.on_focus = Some(cb);
473 self
474 }
475
476 pub fn on_blur(mut self, cb: Callback<()>) -> Self {
478 self.on_blur = Some(cb);
479 self
480 }
481
482 pub fn on_key(mut self, handler: KeyHandler) -> Self {
484 self.on_key = Some(handler);
485 self
486 }
487
488 pub fn on_input(mut self, cb: Callback<TerminalInputEvent>) -> Self {
490 self.on_input = Some(cb);
491 self
492 }
493}
494
495impl From<Terminal> for Element {
496 fn from(mut terminal: Terminal) -> Self {
497 let on_input = terminal.on_input.clone();
498 let fallback_on_key = terminal.on_key.clone();
499 let key_modes = terminal.key_modes;
500 terminal.on_key = if on_input.is_some() || fallback_on_key.is_some() {
501 Some(KeyHandler::new(move |key| {
502 let mut handled = false;
503
504 if let Some(on_input) = on_input.as_ref()
505 && let Some(bytes) = key_event_to_bytes(key, key_modes)
506 {
507 on_input.emit(TerminalInputEvent {
508 kind: TerminalInputKind::Key,
509 key: Some(key),
510 bytes: bytes.into(),
511 });
512 handled = true;
513 }
514
515 if let Some(handler) = fallback_on_key.as_ref() {
516 handled = handler.handle(key) || handled;
517 }
518
519 handled
520 }))
521 } else {
522 None
523 };
524
525 Element::new(ElementKind::Terminal(terminal))
526 }
527}
528
529fn byte_to_row_col(value: &str, cursor: usize) -> (u16, u16) {
530 let cursor = cursor.min(value.len());
531 let mut row = 0u16;
532 let mut col = 0u16;
533 let mut seen = 0usize;
534
535 for ch in value.chars() {
536 if seen >= cursor {
537 break;
538 }
539 seen = seen.saturating_add(ch.len_utf8());
540 if ch == '\n' {
541 row = row.saturating_add(1);
542 col = 0;
543 } else {
544 col = col.saturating_add(1);
545 }
546 }
547
548 (row, col)
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use crate::core::event::{KeyCode, KeyEvent, KeyMods, MouseButton, MouseEvent, MouseKind};
555
556 fn enc(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
558 key_event_to_bytes(KeyEvent { code, mods }, TerminalKeyModes::default())
559 }
560
561 fn kitty(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
564 let modes = TerminalKeyModes {
565 kitty_keyboard: KittyKeyboardFlags {
566 disambiguate_escape_codes: true,
567 ..KittyKeyboardFlags::default()
568 },
569 ..TerminalKeyModes::default()
570 };
571 key_event_to_bytes(KeyEvent { code, mods }, modes)
572 }
573
574 const CTRL_SHIFT: KeyMods = KeyMods {
575 ctrl: true,
576 shift: true,
577 alt: false,
578 super_key: false,
579 };
580 const CTRL_ALT: KeyMods = KeyMods {
581 ctrl: true,
582 alt: true,
583 shift: false,
584 super_key: false,
585 };
586 const ALT_SHIFT: KeyMods = KeyMods {
587 alt: true,
588 shift: true,
589 ctrl: false,
590 super_key: false,
591 };
592
593 #[test]
594 fn ctrl_mapping_works() {
595 assert_eq!(enc(KeyCode::Char('c'), KeyMods::CTRL), Some(vec![3]));
596 }
597
598 #[test]
599 fn alt_prefixes_escape() {
600 assert_eq!(
601 enc(KeyCode::Char('x'), KeyMods::ALT),
602 Some(vec![0x1b, b'x'])
603 );
604 }
605
606 #[test]
607 fn ctrl_arrows_encode_xterm_modifier_param() {
608 for (code, expected) in [
610 (KeyCode::Left, "\x1b[1;5D"),
611 (KeyCode::Right, "\x1b[1;5C"),
612 (KeyCode::Up, "\x1b[1;5A"),
613 (KeyCode::Down, "\x1b[1;5B"),
614 (KeyCode::Home, "\x1b[1;5H"),
615 (KeyCode::End, "\x1b[1;5F"),
616 ] {
617 assert_eq!(
618 enc(code, KeyMods::CTRL),
619 Some(expected.as_bytes().to_vec()),
620 "ctrl {code:?}"
621 );
622 }
623 }
624
625 #[test]
626 fn shift_and_combined_modifiers_encode_params() {
627 assert_eq!(
628 enc(KeyCode::Left, KeyMods::SHIFT),
629 Some(b"\x1b[1;2D".to_vec())
630 );
631 assert_eq!(enc(KeyCode::Right, CTRL_SHIFT), Some(b"\x1b[1;6C".to_vec()));
633 assert_eq!(enc(KeyCode::Up, CTRL_ALT), Some(b"\x1b[1;7A".to_vec()));
634 assert_eq!(enc(KeyCode::Left, ALT_SHIFT), Some(b"\x1b[1;4D".to_vec()));
637 }
638
639 #[test]
640 fn shift_only_emulator_reserved_keys_keep_plain_form() {
641 for (code, expected) in [
645 (KeyCode::Insert, "\x1b[2~"),
646 (KeyCode::PageUp, "\x1b[5~"),
647 (KeyCode::PageDown, "\x1b[6~"),
648 ] {
649 assert_eq!(
650 enc(code, KeyMods::SHIFT),
651 Some(expected.as_bytes().to_vec()),
652 "shift {code:?}"
653 );
654 }
655
656 assert_eq!(
658 enc(KeyCode::Delete, KeyMods::SHIFT),
659 Some(b"\x1b[3;2~".to_vec())
660 );
661 assert_eq!(
662 enc(KeyCode::PageUp, CTRL_SHIFT),
663 Some(b"\x1b[5;6~".to_vec())
664 );
665 }
666
667 #[test]
668 fn ctrl_tilde_keys_encode_params() {
669 assert_eq!(
670 enc(KeyCode::Delete, KeyMods::CTRL),
671 Some(b"\x1b[3;5~".to_vec())
672 );
673 assert_eq!(
674 enc(KeyCode::PageUp, KeyMods::CTRL),
675 Some(b"\x1b[5;5~".to_vec())
676 );
677 assert_eq!(
679 enc(KeyCode::F(5), KeyMods::SHIFT),
680 Some(b"\x1b[15;2~".to_vec())
681 );
682 }
683
684 #[test]
685 fn high_function_keys_encode_through_f20() {
686 assert_eq!(
688 enc(KeyCode::F(13), KeyMods::NONE),
689 Some(b"\x1b[25~".to_vec())
690 );
691 assert_eq!(
692 enc(KeyCode::F(15), KeyMods::NONE),
693 Some(b"\x1b[28~".to_vec())
694 );
695 assert_eq!(
696 enc(KeyCode::F(20), KeyMods::CTRL),
697 Some(b"\x1b[34;5~".to_vec())
698 );
699 assert_eq!(enc(KeyCode::F(21), KeyMods::NONE), None);
701 }
702
703 #[test]
704 fn ctrl_backspace_sends_backward_kill_word() {
705 assert_eq!(
708 enc(KeyCode::Backspace, KeyMods::CTRL),
709 Some(vec![0x1b, 0x7f])
710 );
711 assert_eq!(enc(KeyCode::Backspace, KeyMods::NONE), Some(vec![0x7f]));
713 assert_eq!(
715 enc(KeyCode::Backspace, KeyMods::ALT),
716 Some(vec![0x1b, 0x7f])
717 );
718 }
719
720 #[test]
721 fn legacy_children_keep_legacy_enter_tab_and_lose_ctrl_digits() {
722 assert_eq!(enc(KeyCode::Enter, KeyMods::NONE), Some(vec![b'\r']));
727 assert_eq!(enc(KeyCode::Enter, KeyMods::CTRL), Some(vec![b'\r']));
728 assert_eq!(enc(KeyCode::Enter, CTRL_SHIFT), Some(vec![b'\r']));
729 assert_eq!(enc(KeyCode::Tab, KeyMods::NONE), Some(vec![b'\t']));
730 assert_eq!(enc(KeyCode::Tab, KeyMods::CTRL), Some(vec![b'\t']));
731 assert_eq!(
732 enc(KeyCode::BackTab, KeyMods::SHIFT),
733 Some(b"\x1b[Z".to_vec())
734 );
735 assert_eq!(enc(KeyCode::Esc, KeyMods::NONE), Some(vec![0x1b]));
736 assert_eq!(enc(KeyCode::Enter, KeyMods::ALT), Some(vec![0x1b, b'\r']));
738 assert_eq!(enc(KeyCode::Char('1'), KeyMods::CTRL), None);
739 }
740
741 #[test]
742 fn kitty_children_get_csi_u_for_chords_with_no_legacy_encoding() {
743 for (ch, expected) in [
747 ('1', "\x1b[49;5u"),
748 ('2', "\x1b[50;5u"),
749 ('9', "\x1b[57;5u"),
750 ] {
751 assert_eq!(
752 kitty(KeyCode::Char(ch), KeyMods::CTRL),
753 Some(expected.as_bytes().to_vec())
754 );
755 }
756
757 assert_eq!(
759 kitty(KeyCode::Enter, KeyMods::CTRL),
760 Some(b"\x1b[13;5u".to_vec())
761 );
762 assert_eq!(
763 kitty(KeyCode::Enter, KeyMods::SHIFT),
764 Some(b"\x1b[13;2u".to_vec())
765 );
766 assert_eq!(
767 kitty(KeyCode::Tab, KeyMods::CTRL),
768 Some(b"\x1b[9;5u".to_vec())
769 );
770 assert_eq!(
771 kitty(KeyCode::Backspace, KeyMods::CTRL),
772 Some(b"\x1b[127;5u".to_vec())
773 );
774 assert_eq!(
776 kitty(KeyCode::BackTab, KeyMods::NONE),
777 Some(b"\x1b[9;2u".to_vec())
778 );
779 assert_eq!(
781 kitty(KeyCode::Esc, KeyMods::NONE),
782 Some(b"\x1b[27u".to_vec())
783 );
784
785 assert_eq!(
788 kitty(KeyCode::Char('c'), KeyMods::CTRL),
789 Some(b"\x1b[99;5u".to_vec())
790 );
791 assert_eq!(
792 kitty(KeyCode::Char('C'), CTRL_SHIFT),
793 Some(b"\x1b[99;6u".to_vec())
794 );
795 assert_eq!(
796 kitty(KeyCode::Char('x'), KeyMods::ALT),
797 Some(b"\x1b[120;3u".to_vec())
798 );
799 }
800
801 #[test]
802 fn kitty_children_keep_legacy_bytes_for_text_and_functional_keys() {
803 assert_eq!(
805 kitty(KeyCode::Char('a'), KeyMods::NONE),
806 Some(b"a".to_vec())
807 );
808 assert_eq!(
809 kitty(KeyCode::Char('A'), KeyMods::SHIFT),
810 Some(b"A".to_vec())
811 );
812 assert_eq!(kitty(KeyCode::Enter, KeyMods::NONE), Some(vec![b'\r']));
813 assert_eq!(kitty(KeyCode::Tab, KeyMods::NONE), Some(vec![b'\t']));
814 assert_eq!(kitty(KeyCode::Backspace, KeyMods::NONE), Some(vec![0x7f]));
815
816 assert_eq!(
819 kitty(KeyCode::Left, KeyMods::NONE),
820 Some(b"\x1b[D".to_vec())
821 );
822 assert_eq!(
823 kitty(KeyCode::Left, KeyMods::CTRL),
824 Some(b"\x1b[1;5D".to_vec())
825 );
826 assert_eq!(
827 kitty(KeyCode::PageUp, KeyMods::CTRL),
828 Some(b"\x1b[5;5~".to_vec())
829 );
830 assert_eq!(
831 kitty(KeyCode::F(5), KeyMods::NONE),
832 Some(b"\x1b[15~".to_vec())
833 );
834
835 assert_eq!(
837 kitty(
838 KeyCode::Char('1'),
839 KeyMods {
840 super_key: true,
841 ..KeyMods::default()
842 }
843 ),
844 None
845 );
846 }
847
848 #[test]
849 fn ctrl_punctuation_maps_to_control_codes() {
850 assert_eq!(enc(KeyCode::Char('/'), KeyMods::CTRL), Some(vec![0x1f]));
853 assert_eq!(enc(KeyCode::Char('_'), KeyMods::CTRL), Some(vec![0x1f]));
854 assert_eq!(enc(KeyCode::Char('?'), KeyMods::CTRL), Some(vec![0x7f]));
855 assert_eq!(enc(KeyCode::Char('@'), KeyMods::CTRL), Some(vec![0x00]));
856 assert_eq!(enc(KeyCode::Char(' '), KeyMods::CTRL), Some(vec![0x00]));
857 assert_eq!(enc(KeyCode::Char('2'), KeyMods::CTRL), Some(vec![0x00]));
859 assert_eq!(enc(KeyCode::Char('8'), KeyMods::CTRL), Some(vec![0x7f]));
860 assert_eq!(enc(KeyCode::Char('1'), KeyMods::CTRL), None);
862 }
863
864 #[test]
865 fn super_modified_keys_are_dropped() {
866 let sup = KeyMods {
869 super_key: true,
870 ..KeyMods::default()
871 };
872 assert_eq!(enc(KeyCode::Char('c'), sup), None);
873 assert_eq!(enc(KeyCode::Left, sup), None);
874 assert_eq!(
875 enc(
876 KeyCode::Char('v'),
877 KeyMods {
878 super_key: true,
879 ctrl: true,
880 ..KeyMods::default()
881 }
882 ),
883 None
884 );
885 }
886
887 #[test]
888 fn plain_arrows_are_unmodified() {
889 assert_eq!(enc(KeyCode::Left, KeyMods::NONE), Some(b"\x1b[D".to_vec()));
890 assert_eq!(
892 enc(KeyCode::Left, KeyMods::ALT),
893 Some(b"\x1b\x1b[D".to_vec())
894 );
895 }
896
897 #[test]
898 fn app_cursor_mode_switches_unmodified_arrows_to_ss3() {
899 let modes = TerminalKeyModes {
902 app_cursor: true,
903 ..TerminalKeyModes::default()
904 };
905 let app = |code| {
906 key_event_to_bytes(
907 KeyEvent {
908 code,
909 mods: KeyMods::NONE,
910 },
911 modes,
912 )
913 };
914 assert_eq!(app(KeyCode::Up), Some(b"\x1bOA".to_vec()));
915 assert_eq!(app(KeyCode::Down), Some(b"\x1bOB".to_vec()));
916 assert_eq!(app(KeyCode::Right), Some(b"\x1bOC".to_vec()));
917 assert_eq!(app(KeyCode::Left), Some(b"\x1bOD".to_vec()));
918 assert_eq!(app(KeyCode::Home), Some(b"\x1bOH".to_vec()));
919 assert_eq!(app(KeyCode::End), Some(b"\x1bOF".to_vec()));
920
921 assert_eq!(
923 key_event_to_bytes(
924 KeyEvent {
925 code: KeyCode::Left,
926 mods: KeyMods::CTRL
927 },
928 modes
929 ),
930 Some(b"\x1b[1;5D".to_vec())
931 );
932 assert_eq!(
934 key_event_to_bytes(
935 KeyEvent {
936 code: KeyCode::PageUp,
937 mods: KeyMods::NONE
938 },
939 modes
940 ),
941 Some(b"\x1b[5~".to_vec())
942 );
943 }
944
945 #[test]
946 fn paste_is_bracketed_only_when_the_child_asked_for_it() {
947 let off = TerminalKeyModes::default();
948 assert_eq!(encode_paste("hi", off), b"hi".to_vec());
949
950 let on = TerminalKeyModes {
951 bracketed_paste: true,
952 ..TerminalKeyModes::default()
953 };
954 assert_eq!(encode_paste("hi", on), b"\x1b[200~hi\x1b[201~".to_vec());
955 }
956
957 #[test]
958 fn terminal_buffer_trims_lines() {
959 let mut buffer = TerminalBuffer::new(2);
960 buffer.push_text("a\nb\nc\n");
961 let snapshot = buffer.snapshot();
962 assert_eq!(snapshot.as_ref(), "b\nc");
963 }
964
965 #[test]
966 fn terminal_screen_applies_vt_sequences() {
967 let mut screen = TerminalScreen::new(4, 20, 128);
968 screen.process_bytes(b"\x1b[31mhello\x1b[0m\nworld");
969 let snapshot = screen.snapshot();
970 let mut lines = snapshot.lines().map(str::trim);
971 assert_eq!(lines.next(), Some("hello"));
972 assert_eq!(lines.next(), Some("world"));
973 }
974
975 #[test]
976 fn mouse_event_to_bytes_sgr_encodes_coordinates() {
977 let event = MouseEvent {
978 x: 2,
979 y: 3,
980 kind: MouseKind::Down(MouseButton::Left),
981 mods: KeyMods::default(),
982 };
983 use super::events::MouseEncoding;
984
985 let bytes = mouse_event_to_bytes(event, MouseEncoding::Sgr, (0, 0)).expect("mouse bytes");
986 assert_eq!(String::from_utf8(bytes).unwrap(), "\u{1b}[<0;3;4M");
987 }
988
989 #[test]
990 fn mouse_event_to_bytes_sgr_encodes_plain_motion() {
991 let event = MouseEvent {
995 x: 26,
996 y: 5,
997 kind: MouseKind::Moved,
998 mods: KeyMods::default(),
999 };
1000 use super::events::MouseEncoding;
1001
1002 let bytes = mouse_event_to_bytes(event, MouseEncoding::Sgr, (0, 0)).expect("mouse bytes");
1003 assert_eq!(String::from_utf8(bytes).unwrap(), "\u{1b}[<35;27;6M");
1004 }
1005
1006 #[test]
1007 fn grid_selection_extracts_text() {
1008 use crate::utils::{GridPos, GridSelection};
1009 let selection = GridSelection {
1010 anchor: GridPos { row: 0, col: 1 },
1011 cursor: GridPos { row: 1, col: 2 },
1012 };
1013 let lines = vec!["abcd", "efgh", "ijkl"];
1014 assert_eq!(selection.extract_text(&lines), "bcd\nef");
1015 }
1016}