1use std::{
4 collections::VecDeque,
5 fs::{File, OpenOptions},
6 io::{self, Read, Write},
7 mem::MaybeUninit,
8 os::fd::AsRawFd,
9 os::unix::fs::OpenOptionsExt,
10 time::{Duration, Instant},
11};
12
13use crate::{
14 ColorLevel, Command, CommandWriter, Event, EventSource, FocusChange, KeyCode, KeyEvent,
15 KeyKind, KeyboardEnhancementQuery, Modifiers, MouseButton, MouseEvent, MouseKind, PixelSize,
16 Position, RawModeControl, TerminalBackground, TerminalCapabilities, TerminalGraphicsProtocols,
17 TerminalInfo, TerminalOutput, TerminalQuery, TerminalSize, WindowSize,
18 backend::ansi::AnsiWriter,
19};
20
21const ESCAPE_TIMEOUT: Duration = Duration::from_millis(10);
22const QUERY_TIMEOUT: Duration = Duration::from_secs(2);
23const RESIZE_POLL_INTERVAL: Duration = Duration::from_millis(50);
24const MAX_CONTROL_RESPONSE_BYTES: usize = 64 * 1024;
25const BACKGROUND_QUERY: &[u8] = b"\x1b]11;?\x1b\\";
26const CAPABILITY_QUERY: &[u8] = concat!(
27 "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\",
28 "\x1b[c",
29 "\x1bP+q436f\x1b\\",
30 "\x1bP+q524742\x1b\\",
31 "\x1b[5n",
32)
33.as_bytes();
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36enum CapabilityResponse {
37 Kitty,
38 Sixel,
39 ColorCount(u16),
40 TrueColor,
41 Complete,
42 Ignored,
43}
44
45#[derive(Default)]
46struct CapabilityProbe {
47 complete: bool,
48 color_count: u16,
49 true_color: bool,
50 graphics_protocols: TerminalGraphicsProtocols,
51}
52
53impl CapabilityProbe {
54 fn record(&mut self, response: CapabilityResponse) {
55 match response {
56 CapabilityResponse::Kitty => {
57 self.graphics_protocols = self
58 .graphics_protocols
59 .union(TerminalGraphicsProtocols::KITTY);
60 }
61 CapabilityResponse::Sixel => {
62 self.graphics_protocols = self
63 .graphics_protocols
64 .union(TerminalGraphicsProtocols::SIXEL);
65 }
66 CapabilityResponse::ColorCount(count) => self.color_count = count,
67 CapabilityResponse::TrueColor => self.true_color = true,
68 CapabilityResponse::Complete => self.complete = true,
69 CapabilityResponse::Ignored => {}
70 }
71 }
72
73 fn capabilities(&self) -> TerminalCapabilities {
74 let color_level = if self.true_color {
75 ColorLevel::TrueColor
76 } else if self.color_count >= 256 {
77 ColorLevel::Ansi256
78 } else if self.color_count >= 16 {
79 ColorLevel::Ansi16
80 } else {
81 ColorLevel::None
82 };
83 TerminalCapabilities::none()
84 .with_color_level(color_level)
85 .with_graphics_protocols(self.graphics_protocols)
86 }
87}
88
89pub struct NativeTerminal {
95 output: AnsiWriter<File>,
96 original_termios: Option<libc::termios>,
97 input: Vec<u8>,
98 input_start: usize,
99 pending: VecDeque<Event>,
100 cursor_response: Option<Position>,
101 background_query_active: bool,
102 background_response_discarding: bool,
103 background_response: Option<Option<TerminalBackground>>,
104 capability_probe: Option<CapabilityProbe>,
105 capabilities: Option<TerminalCapabilities>,
106 last_size: TerminalSize,
107}
108
109impl NativeTerminal {
110 pub fn open() -> io::Result<Self> {
112 let file = OpenOptions::new()
113 .read(true)
114 .write(true)
115 .custom_flags(libc::O_CLOEXEC)
116 .open("/dev/tty")?;
117 Self::from_file(file)
118 }
119
120 fn from_file(file: File) -> io::Result<Self> {
121 let last_size = window_size(&file)?.cells();
122 Ok(Self {
123 output: AnsiWriter::new(file),
124 original_termios: None,
125 input: Vec::new(),
126 input_start: 0,
127 pending: VecDeque::new(),
128 cursor_response: None,
129 background_query_active: false,
130 background_response_discarding: false,
131 background_response: None,
132 capability_probe: None,
133 capabilities: None,
134 last_size,
135 })
136 }
137
138 pub fn terminal_info(&mut self) -> io::Result<TerminalInfo> {
140 let size = self.terminal_size()?;
141 let capabilities = self.terminal_capabilities()?;
142 Ok(TerminalInfo::new(size, capabilities))
143 }
144
145 fn file(&self) -> &File {
146 self.output.writer()
147 }
148
149 fn file_mut(&mut self) -> &mut File {
150 self.output.writer_mut()
151 }
152
153 fn next_event(&mut self, timeout: Option<Duration>) -> io::Result<Option<Event>> {
154 if let Some(event) = self.pending.pop_front() {
155 return Ok(Some(event));
156 }
157
158 self.next_wire_event(timeout)
159 }
160
161 fn next_wire_event(&mut self, timeout: Option<Duration>) -> io::Result<Option<Event>> {
162 let started = Instant::now();
163 let mut attempted_read = false;
164 let raw_mode = self.original_termios.is_some() || self.raw_mode_enabled()?;
165 loop {
166 if let Some(decoded) = decode(
167 self.unread_input(),
168 false,
169 raw_mode,
170 self.background_query_active,
171 self.background_response_discarding,
172 )? {
173 self.consume_input(decoded.consumed);
174 match decoded.item {
175 DecodedItem::Event(event) => return Ok(Some(event)),
176 DecodedItem::Cursor(position) => {
177 self.cursor_response = Some(position);
178 return Ok(None);
179 }
180 DecodedItem::Background(background) => {
181 self.background_response = Some(background);
182 return Ok(None);
183 }
184 DecodedItem::BackgroundOverflow => {
185 self.background_response_discarding = true;
186 return Ok(None);
187 }
188 DecodedItem::Response(response) => {
189 if let Some(probe) = &mut self.capability_probe {
190 probe.record(response);
191 }
192 return Ok(None);
193 }
194 }
195 }
196
197 if attempted_read && timeout.is_some_and(|limit| started.elapsed() >= limit) {
198 return Ok(None);
199 }
200
201 let remaining = timeout.map(|limit| limit.saturating_sub(started.elapsed()));
202 let mut wait = remaining.map_or(RESIZE_POLL_INTERVAL, |value| {
203 value.min(RESIZE_POLL_INTERVAL)
204 });
205 if self.unread_input().first() == Some(&0x1b) {
206 wait = wait.min(ESCAPE_TIMEOUT);
207 }
208 let read = self.read_input(wait)?;
209 attempted_read = true;
210 if !read {
211 if let Some(decoded) = decode(
212 self.unread_input(),
213 true,
214 raw_mode,
215 self.background_query_active,
216 self.background_response_discarding,
217 )? {
218 self.consume_input(decoded.consumed);
219 match decoded.item {
220 DecodedItem::Event(event) => return Ok(Some(event)),
221 DecodedItem::Cursor(position) => {
222 self.cursor_response = Some(position);
223 return Ok(None);
224 }
225 DecodedItem::Background(background) => {
226 self.background_response = Some(background);
227 return Ok(None);
228 }
229 DecodedItem::BackgroundOverflow => {
230 self.background_response_discarding = true;
231 return Ok(None);
232 }
233 DecodedItem::Response(response) => {
234 if let Some(probe) = &mut self.capability_probe {
235 probe.record(response);
236 }
237 return Ok(None);
238 }
239 }
240 }
241 let size = window_size(self.file())?.cells();
242 if size != self.last_size {
243 self.last_size = size;
244 return Ok(Some(Event::Resize(size)));
245 }
246 if timeout.is_some_and(|limit| started.elapsed() >= limit) {
247 return Ok(None);
248 }
249 }
250 }
251 }
252
253 fn unread_input(&self) -> &[u8] {
254 &self.input[self.input_start..]
255 }
256
257 fn consume_input(&mut self, consumed: usize) {
258 self.input_start += consumed;
259 if self.input_start == self.input.len() {
260 self.input.clear();
261 self.input_start = 0;
262 } else if self.input_start >= 4096 && self.input_start >= self.input.len() / 2 {
263 self.input.copy_within(self.input_start.., 0);
264 self.input.truncate(self.input.len() - self.input_start);
265 self.input_start = 0;
266 }
267 }
268
269 fn read_input(&mut self, timeout: Duration) -> io::Result<bool> {
270 if !wait_readable(self.file(), timeout)? {
271 return Ok(false);
272 }
273 let mut buffer = [0_u8; 4096];
274 match self.file_mut().read(&mut buffer) {
275 Ok(0) => Ok(false),
279 Ok(length) => {
280 self.input.extend_from_slice(&buffer[..length]);
281 Ok(true)
282 }
283 Err(error) if error.kind() == io::ErrorKind::WouldBlock => Ok(false),
284 Err(error) => Err(error),
285 }
286 }
287}
288
289impl TerminalOutput for NativeTerminal {
290 fn flush(&mut self) -> io::Result<()> {
291 self.output.flush()
292 }
293}
294
295impl CommandWriter for NativeTerminal {
296 fn write_command(&mut self, command: Command<'_>) -> io::Result<()> {
297 self.output.write_command(command)
298 }
299}
300
301impl RawModeControl for NativeTerminal {
302 fn is_interactive(&self) -> bool {
303 true
304 }
305
306 fn enable_raw_mode(&mut self) -> io::Result<()> {
307 if self.original_termios.is_some() {
308 return Ok(());
309 }
310 let original = get_termios(self.file())?;
311 let mut raw = original;
312 unsafe { libc::cfmakeraw(&mut raw) };
315 raw.c_cc[libc::VMIN] = 0;
318 raw.c_cc[libc::VTIME] = 0;
319 set_termios(self.file(), &raw)?;
320 self.original_termios = Some(original);
321 Ok(())
322 }
323
324 fn disable_raw_mode(&mut self) -> io::Result<()> {
325 let Some(original) = self.original_termios else {
326 return Ok(());
327 };
328 set_termios(self.file(), &original)?;
329 self.original_termios = None;
330 Ok(())
331 }
332}
333
334impl TerminalQuery for NativeTerminal {
335 fn terminal_size(&mut self) -> io::Result<TerminalSize> {
336 Ok(window_size(self.file())?.cells())
337 }
338
339 fn cursor_position(&mut self) -> io::Result<Position> {
340 self.cursor_response = None;
341 self.output.writer_mut().write_all(b"\x1b[6n")?;
342 self.output.flush()?;
343 let started = Instant::now();
344 loop {
345 if let Some(position) = self.cursor_response.take() {
346 return Ok(position);
347 }
348 let remaining = QUERY_TIMEOUT.saturating_sub(started.elapsed());
349 if remaining.is_zero() {
350 return Err(io::Error::new(
351 io::ErrorKind::TimedOut,
352 "terminal did not answer the cursor-position query",
353 ));
354 }
355 if let Some(event) = self.next_wire_event(Some(remaining))? {
356 self.pending.push_back(event);
357 }
358 }
359 }
360
361 fn window_size(&mut self) -> io::Result<WindowSize> {
362 window_size(self.file())
363 }
364
365 fn raw_mode_enabled(&mut self) -> io::Result<bool> {
366 let mode = get_termios(self.file())?;
367 Ok(mode.c_lflag & (libc::ICANON | libc::ECHO | libc::ISIG) == 0)
368 }
369
370 fn terminal_capabilities(&mut self) -> io::Result<TerminalCapabilities> {
371 if let Some(capabilities) = self.capabilities {
372 return Ok(capabilities);
373 }
374 let restore_raw_mode = self.original_termios.is_none();
375 if restore_raw_mode {
376 self.enable_raw_mode()?;
377 }
378
379 let query_result = self.query_terminal_capabilities();
380 let restore_result = if restore_raw_mode {
381 self.disable_raw_mode()
382 } else {
383 Ok(())
384 };
385
386 match (query_result, restore_result) {
387 (Ok(capabilities), Ok(())) => {
388 self.capabilities = Some(capabilities);
389 Ok(capabilities)
390 }
391 (Err(error), _) | (Ok(_), Err(error)) => Err(error),
392 }
393 }
394
395 fn terminal_background(&mut self) -> io::Result<Option<TerminalBackground>> {
396 let restore_raw_mode = self.original_termios.is_none();
397 if restore_raw_mode {
398 self.enable_raw_mode()?;
399 }
400
401 let query_result = self.query_terminal_background(QUERY_TIMEOUT);
402 let restore_result = if restore_raw_mode {
403 self.disable_raw_mode()
404 } else {
405 Ok(())
406 };
407
408 match (query_result, restore_result) {
409 (Ok(background), Ok(())) => Ok(background),
410 (Err(error), _) | (Ok(_), Err(error)) => Err(error),
411 }
412 }
413}
414
415impl NativeTerminal {
416 fn query_terminal_background(
417 &mut self,
418 timeout: Duration,
419 ) -> io::Result<Option<TerminalBackground>> {
420 self.background_response = None;
421 self.background_query_active = true;
422 self.background_response_discarding = false;
423 let result = (|| {
424 self.output.writer_mut().write_all(BACKGROUND_QUERY)?;
425 self.output.flush()?;
426
427 let started = Instant::now();
428 loop {
429 if let Some(background) = self.background_response.take() {
430 return Ok(background);
431 }
432 let remaining = timeout.saturating_sub(started.elapsed());
433 if remaining.is_zero() {
434 self.discard_unterminated_background_response();
435 return Ok(None);
436 }
437 if let Some(event) = self.next_wire_event(Some(remaining))? {
438 self.pending.push_back(event);
439 }
440 }
441 })();
442 self.background_query_active = false;
443 self.background_response_discarding = false;
444 result
445 }
446
447 fn discard_unterminated_background_response(&mut self) {
448 let discard = self.background_response_discarding
449 || (self.unread_input().starts_with(b"\x1b]11;")
450 && osc_terminator(&self.unread_input()[2..]).is_none());
451 if discard {
452 let discarded = self.unread_input().len();
453 if discarded != 0 {
454 self.consume_input(discarded);
455 }
456 }
457 }
458
459 fn query_terminal_capabilities(&mut self) -> io::Result<TerminalCapabilities> {
460 self.capability_probe = Some(CapabilityProbe::default());
461 let result = (|| {
462 self.output.writer_mut().write_all(CAPABILITY_QUERY)?;
463 self.output.flush()?;
464
465 let started = Instant::now();
466 loop {
467 if self
468 .capability_probe
469 .as_ref()
470 .is_some_and(|probe| probe.complete)
471 {
472 break;
473 }
474 let remaining = QUERY_TIMEOUT.saturating_sub(started.elapsed());
475 if remaining.is_zero() {
476 break;
477 }
478 if let Some(event) = self.next_wire_event(Some(remaining))? {
479 self.pending.push_back(event);
480 }
481 }
482 Ok(())
483 })();
484
485 let capabilities = self
486 .capability_probe
487 .take()
488 .unwrap_or_default()
489 .capabilities();
490 result.map(|()| capabilities)
491 }
492}
493
494impl KeyboardEnhancementQuery for NativeTerminal {
495 fn supports_keyboard_enhancement(&mut self) -> io::Result<bool> {
496 Ok(false)
499 }
500}
501
502impl EventSource for NativeTerminal {
503 fn read_event(&mut self) -> io::Result<Event> {
504 loop {
505 if let Some(event) = self.next_event(None)? {
506 return Ok(event);
507 }
508 }
509 }
510
511 fn poll_event(&mut self) -> io::Result<Option<Event>> {
512 self.next_event(Some(Duration::ZERO))
513 }
514
515 fn poll_event_timeout(&mut self, timeout: Duration) -> io::Result<Option<Event>> {
516 self.next_event(Some(timeout))
517 }
518}
519
520impl Drop for NativeTerminal {
521 fn drop(&mut self) {
522 if let Some(original) = self.original_termios {
523 let _restore_error = set_termios(self.file(), &original);
524 }
525 }
526}
527
528fn get_termios(file: &File) -> io::Result<libc::termios> {
529 let mut value = MaybeUninit::<libc::termios>::uninit();
530 if unsafe { libc::tcgetattr(file.as_raw_fd(), value.as_mut_ptr()) } == -1 {
533 Err(io::Error::last_os_error())
534 } else {
535 Ok(unsafe { value.assume_init() })
537 }
538}
539
540fn set_termios(file: &File, value: &libc::termios) -> io::Result<()> {
541 if unsafe { libc::tcsetattr(file.as_raw_fd(), libc::TCSANOW, value) } == -1 {
543 Err(io::Error::last_os_error())
544 } else {
545 Ok(())
546 }
547}
548
549fn window_size(file: &File) -> io::Result<WindowSize> {
550 let mut value = MaybeUninit::<libc::winsize>::zeroed();
551 if unsafe { libc::ioctl(file.as_raw_fd(), libc::TIOCGWINSZ, value.as_mut_ptr()) } == -1 {
553 return Err(io::Error::last_os_error());
554 }
555 let value = unsafe { value.assume_init() };
557 let cells = TerminalSize::new(usize::from(value.ws_col), usize::from(value.ws_row));
558 let pixels = (value.ws_xpixel != 0 && value.ws_ypixel != 0)
559 .then(|| PixelSize::new(usize::from(value.ws_xpixel), usize::from(value.ws_ypixel)));
560 Ok(WindowSize::new(cells, pixels))
561}
562
563fn wait_readable(file: &File, timeout: Duration) -> io::Result<bool> {
564 let mut descriptor = libc::pollfd {
565 fd: file.as_raw_fd(),
566 events: libc::POLLIN,
567 revents: 0,
568 };
569 let milliseconds = if timeout.is_zero() {
570 0
571 } else {
572 timeout.as_millis().clamp(1, i32::MAX as u128) as i32
573 };
574 let result = unsafe { libc::poll(&mut descriptor, 1, milliseconds) };
577 if result == -1 {
578 let error = io::Error::last_os_error();
579 if error.kind() == io::ErrorKind::Interrupted {
580 Ok(false)
581 } else {
582 Err(error)
583 }
584 } else if result == 0 {
585 Ok(false)
586 } else if descriptor.revents & libc::POLLIN != 0 {
587 Ok(true)
588 } else if descriptor.revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL) != 0 {
589 get_termios(file)?;
590 std::thread::sleep(timeout);
591 Ok(true)
592 } else {
593 Ok(false)
594 }
595}
596
597struct Decoded {
598 item: DecodedItem,
599 consumed: usize,
600}
601
602enum DecodedItem {
603 Event(Event),
604 Cursor(Position),
605 Background(Option<TerminalBackground>),
606 BackgroundOverflow,
607 Response(CapabilityResponse),
608}
609
610fn decode(
611 input: &[u8],
612 escape_complete: bool,
613 raw_mode: bool,
614 background_query_active: bool,
615 background_response_discarding: bool,
616) -> io::Result<Option<Decoded>> {
617 let Some(&first) = input.first() else {
618 return Ok(None);
619 };
620 if background_response_discarding {
621 return Ok(decode_osc_discard(input));
622 }
623 if first != 0x1b {
624 return decode_plain(input, raw_mode);
625 }
626 if input.len() == 1 {
627 return escape_complete
628 .then(|| decoded_key(KeyCode::Escape, Modifiers::NONE, 1))
629 .transpose();
630 }
631 match input[1] {
632 b'[' => decode_csi(input, escape_complete),
633 b']' if (background_query_active && possible_background_response(input))
634 || complete_background_response(input) =>
635 {
636 decode_osc(input, escape_complete)
637 }
638 b'P' => decode_control_string(input, b'P', escape_complete),
639 b'_' => decode_control_string(input, b'_', escape_complete),
640 b'O' => decode_ss3(input),
641 0x1b => decoded_key(KeyCode::Escape, Modifiers::NONE, 1).map(Some),
642 _ => {
643 let Some(mut decoded) = decode_plain(&input[1..], raw_mode)? else {
644 return if escape_complete {
645 decoded_key(KeyCode::Escape, Modifiers::NONE, 1).map(Some)
646 } else {
647 Ok(None)
648 };
649 };
650 if let DecodedItem::Event(Event::Key(ref mut key)) = decoded.item {
651 key.modifiers = key.modifiers | Modifiers::ALT;
652 }
653 decoded.consumed += 1;
654 Ok(Some(decoded))
655 }
656 }
657}
658
659fn possible_background_response(input: &[u8]) -> bool {
660 input
661 .strip_prefix(b"\x1b]")
662 .is_some_and(|body| b"11;".starts_with(body) || body.starts_with(b"11;"))
663}
664
665fn complete_background_response(input: &[u8]) -> bool {
666 input
667 .strip_prefix(b"\x1b]11;")
668 .is_some_and(|body| osc_terminator(body).is_some())
669}
670
671fn decode_osc(input: &[u8], escape_complete: bool) -> io::Result<Option<Decoded>> {
672 let unfinished_body = &input[2..];
673 let Some((body_end, terminator_length)) = osc_terminator(unfinished_body) else {
674 if input.len() > MAX_CONTROL_RESPONSE_BYTES {
675 let consumed = discardable_osc_bytes(input);
676 if consumed == 0 {
677 return Ok(None);
678 }
679 return Ok(Some(Decoded {
680 item: DecodedItem::BackgroundOverflow,
681 consumed,
682 }));
683 }
684 if b"11;".starts_with(unfinished_body) || unfinished_body.starts_with(b"11;") {
685 return Ok(None);
686 }
687 return if escape_complete {
688 decoded_key(KeyCode::Escape, Modifiers::NONE, 1).map(Some)
689 } else {
690 Ok(None)
691 };
692 };
693 let body = &input[2..2 + body_end];
694 let item = match body.strip_prefix(b"11;") {
695 Some(color) => DecodedItem::Background(parse_osc_rgb(color)),
696 None => DecodedItem::Response(CapabilityResponse::Ignored),
697 };
698 Ok(Some(Decoded {
699 item,
700 consumed: 2 + body_end + terminator_length,
701 }))
702}
703
704fn decode_osc_discard(input: &[u8]) -> Option<Decoded> {
705 if let Some((body_end, terminator_length)) = osc_terminator(input) {
706 return Some(Decoded {
707 item: DecodedItem::Background(None),
708 consumed: body_end + terminator_length,
709 });
710 }
711 let consumed = discardable_osc_bytes(input);
712 (consumed != 0).then_some(Decoded {
713 item: DecodedItem::Response(CapabilityResponse::Ignored),
714 consumed,
715 })
716}
717
718fn discardable_osc_bytes(input: &[u8]) -> usize {
719 input.len() - usize::from(input.last() == Some(&0x1b))
720}
721
722fn osc_terminator(body: &[u8]) -> Option<(usize, usize)> {
723 body.iter()
724 .enumerate()
725 .find_map(|(index, byte)| match byte {
726 0x07 => Some((index, 1)),
727 0x1b if body.get(index + 1) == Some(&b'\\') => Some((index, 2)),
728 _ => None,
729 })
730}
731
732fn parse_osc_rgb(value: &[u8]) -> Option<TerminalBackground> {
733 let components = value.strip_prefix(b"rgb:")?;
734 let mut components = components.split(|byte| *byte == b'/');
735 let red = scale_osc_component(components.next()?)?;
736 let green = scale_osc_component(components.next()?)?;
737 let blue = scale_osc_component(components.next()?)?;
738 if components.next().is_some() {
739 return None;
740 }
741 Some(TerminalBackground::new(red, green, blue))
742}
743
744fn scale_osc_component(component: &[u8]) -> Option<u16> {
745 if !(1..=4).contains(&component.len()) {
746 return None;
747 }
748 let component = std::str::from_utf8(component).ok()?;
749 let value = u16::from_str_radix(component, 16).ok()?;
750 let maximum = (1_u64 << (component.len() * 4)) - 1;
751 let scaled = (u64::from(value) * u64::from(u16::MAX) + maximum / 2) / maximum;
752 u16::try_from(scaled).ok()
753}
754
755fn decode_plain(input: &[u8], raw_mode: bool) -> io::Result<Option<Decoded>> {
756 let Some(&first) = input.first() else {
757 return Ok(None);
758 };
759 match first {
760 b'\r' => return decoded_key(KeyCode::Enter, Modifiers::NONE, 1).map(Some),
761 b'\n' if !raw_mode => return decoded_key(KeyCode::Enter, Modifiers::NONE, 1).map(Some),
762 b'\t' => return decoded_key(KeyCode::Tab, Modifiers::NONE, 1).map(Some),
763 0x7f => return decoded_key(KeyCode::Backspace, Modifiers::NONE, 1).map(Some),
764 0 => return decoded_key(KeyCode::Char(' '), Modifiers::CONTROL, 1).map(Some),
765 byte @ 1..=26 => {
766 return decoded_key(
767 KeyCode::Char(char::from(b'a' + byte - 1)),
768 Modifiers::CONTROL,
769 1,
770 )
771 .map(Some);
772 }
773 byte @ 0x1c..=0x1f => {
774 return decoded_key(
775 KeyCode::Char(char::from(b'4' + byte - 0x1c)),
776 Modifiers::CONTROL,
777 1,
778 )
779 .map(Some);
780 }
781 _ => {}
782 }
783 let width = utf8_width(first)?;
784 if input.len() < width {
785 return Ok(None);
786 }
787 let text = std::str::from_utf8(&input[..width])
788 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
789 let character = text
790 .chars()
791 .next()
792 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "empty UTF-8 input"))?;
793 let modifiers = if character.is_uppercase() {
794 Modifiers::SHIFT
795 } else {
796 Modifiers::NONE
797 };
798 decoded_key(KeyCode::Char(character), modifiers, width).map(Some)
799}
800
801fn decode_ss3(input: &[u8]) -> io::Result<Option<Decoded>> {
802 let Some(&code) = input.get(2) else {
803 return Ok(None);
804 };
805 let key = match code {
806 b'A' => KeyCode::Up,
807 b'B' => KeyCode::Down,
808 b'C' => KeyCode::Right,
809 b'D' => KeyCode::Left,
810 b'H' => KeyCode::Home,
811 b'F' => KeyCode::End,
812 b'P'..=b'S' => KeyCode::Function(code - b'P' + 1),
813 _ => return decoded_key(KeyCode::Escape, Modifiers::NONE, 1).map(Some),
814 };
815 decoded_key(key, Modifiers::NONE, 3).map(Some)
816}
817
818fn decode_control_string(
819 input: &[u8],
820 kind: u8,
821 escape_complete: bool,
822) -> io::Result<Option<Decoded>> {
823 let Some(end) = find_subslice(&input[2..], b"\x1b\\") else {
824 if input.len() > MAX_CONTROL_RESPONSE_BYTES {
825 return Err(io::Error::new(
826 io::ErrorKind::InvalidData,
827 "terminal control response exceeds the supported size",
828 ));
829 }
830 return if escape_complete {
831 decoded_key(KeyCode::Escape, Modifiers::NONE, 1).map(Some)
832 } else {
833 Ok(None)
834 };
835 };
836 let body = &input[2..2 + end];
837 let response = match kind {
838 b'_' if body == b"Gi=31;OK" => CapabilityResponse::Kitty,
839 b'P' => decode_termcap_response(body),
840 _ => CapabilityResponse::Ignored,
841 };
842 Ok(Some(Decoded {
843 item: DecodedItem::Response(response),
844 consumed: 2 + end + 2,
845 }))
846}
847
848fn decode_termcap_response(body: &[u8]) -> CapabilityResponse {
849 let Some(capability) = body.strip_prefix(b"1+r") else {
850 return CapabilityResponse::Ignored;
851 };
852 let (name, value) = capability
853 .iter()
854 .position(|byte| *byte == b'=')
855 .map_or((capability, &[][..]), |separator| {
856 (&capability[..separator], &capability[separator + 1..])
857 });
858 match name {
859 b"436f" => decode_hex_decimal(value)
860 .map(CapabilityResponse::ColorCount)
861 .unwrap_or(CapabilityResponse::Ignored),
862 b"524742" => CapabilityResponse::TrueColor,
863 _ => CapabilityResponse::Ignored,
864 }
865}
866
867fn decode_hex_decimal(value: &[u8]) -> Option<u16> {
868 if !value.len().is_multiple_of(2) {
869 return None;
870 }
871 let mut decoded = Vec::with_capacity(value.len() / 2);
872 for pair in value.as_chunks::<2>().0 {
873 let pair = std::str::from_utf8(pair).ok()?;
874 decoded.push(u8::from_str_radix(pair, 16).ok()?);
875 }
876 std::str::from_utf8(&decoded).ok()?.parse().ok()
877}
878
879fn decode_csi(input: &[u8], escape_complete: bool) -> io::Result<Option<Decoded>> {
880 if input.starts_with(b"\x1b[200~") {
881 let Some(end) = find_subslice(&input[6..], b"\x1b[201~") else {
882 return Ok(None);
883 };
884 let text = std::str::from_utf8(&input[6..6 + end])
885 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
886 .to_owned();
887 return Ok(Some(Decoded {
888 item: DecodedItem::Event(Event::Paste(text)),
889 consumed: 6 + end + 6,
890 }));
891 }
892 let Some(final_index) = input[2..]
893 .iter()
894 .position(|byte| (0x40..=0x7e).contains(byte))
895 .map(|index| index + 2)
896 else {
897 return if escape_complete {
898 decoded_key(KeyCode::Escape, Modifiers::NONE, 1).map(Some)
899 } else {
900 Ok(None)
901 };
902 };
903 let final_byte = input[final_index];
904 let body = &input[2..final_index];
905 let consumed = final_index + 1;
906 if final_byte == b'c' && body.starts_with(b"?") {
907 let sixel = body[1..]
908 .split(|byte| *byte == b';')
909 .any(|parameter| parameter == b"4");
910 return Ok(Some(Decoded {
911 item: DecodedItem::Response(if sixel {
912 CapabilityResponse::Sixel
913 } else {
914 CapabilityResponse::Ignored
915 }),
916 consumed,
917 }));
918 }
919 if final_byte == b'n' && body == b"0" {
920 return Ok(Some(Decoded {
921 item: DecodedItem::Response(CapabilityResponse::Complete),
922 consumed,
923 }));
924 }
925 if final_byte == b'R' {
926 let (row, column) = two_numbers(body)?;
927 return Ok(Some(Decoded {
928 item: DecodedItem::Cursor(Position::new(
929 column.saturating_sub(1),
930 row.saturating_sub(1),
931 )),
932 consumed,
933 }));
934 }
935 if body.starts_with(b"<") && matches!(final_byte, b'M' | b'm') {
936 return decode_mouse(body, final_byte, consumed).map(Some);
937 }
938 let modifiers = csi_modifiers(body);
939 let key = match final_byte {
940 b'A' => KeyCode::Up,
941 b'B' => KeyCode::Down,
942 b'C' => KeyCode::Right,
943 b'D' => KeyCode::Left,
944 b'H' => KeyCode::Home,
945 b'F' => KeyCode::End,
946 b'Z' => {
947 return decoded_key(KeyCode::BackTab, Modifiers::SHIFT, consumed).map(Some);
948 }
949 b'I' => {
950 return Ok(Some(Decoded {
951 item: DecodedItem::Event(Event::Focus(FocusChange::Gained)),
952 consumed,
953 }));
954 }
955 b'O' => {
956 return Ok(Some(Decoded {
957 item: DecodedItem::Event(Event::Focus(FocusChange::Lost)),
958 consumed,
959 }));
960 }
961 b'~' => match first_number(body)? {
962 1 | 7 => KeyCode::Home,
963 2 => KeyCode::Insert,
964 3 => KeyCode::Delete,
965 4 | 8 => KeyCode::End,
966 5 => KeyCode::PageUp,
967 6 => KeyCode::PageDown,
968 11 => KeyCode::Function(1),
969 12 => KeyCode::Function(2),
970 13 => KeyCode::Function(3),
971 14 => KeyCode::Function(4),
972 15 => KeyCode::Function(5),
973 17 => KeyCode::Function(6),
974 18 => KeyCode::Function(7),
975 19 => KeyCode::Function(8),
976 20 => KeyCode::Function(9),
977 21 => KeyCode::Function(10),
978 23 => KeyCode::Function(11),
979 24 => KeyCode::Function(12),
980 _ => return decoded_key(KeyCode::Escape, Modifiers::NONE, 1).map(Some),
981 },
982 _ => return decoded_key(KeyCode::Escape, Modifiers::NONE, 1).map(Some),
983 };
984 decoded_key(key, modifiers, consumed).map(Some)
985}
986
987fn decode_mouse(body: &[u8], final_byte: u8, consumed: usize) -> io::Result<Decoded> {
988 let text = std::str::from_utf8(body.strip_prefix(b"<").unwrap_or(body))
989 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
990 let mut parts = text.split(';');
991 let flags = parse_number(parts.next())?;
992 let column = parse_number(parts.next())?.saturating_sub(1);
993 let row = parse_number(parts.next())?.saturating_sub(1);
994 let modifiers = mouse_modifiers(flags);
995 let kind = if flags & 64 != 0 {
996 match flags & 3 {
997 0 => MouseKind::ScrollUp,
998 1 => MouseKind::ScrollDown,
999 2 => MouseKind::ScrollLeft,
1000 _ => MouseKind::ScrollRight,
1001 }
1002 } else if flags & 32 != 0 {
1003 if flags & 3 == 3 {
1004 MouseKind::Moved
1005 } else {
1006 MouseKind::Drag(mouse_button(flags))
1007 }
1008 } else if final_byte == b'm' {
1009 MouseKind::Up(mouse_button(flags))
1010 } else {
1011 MouseKind::Down(mouse_button(flags))
1012 };
1013 Ok(Decoded {
1014 item: DecodedItem::Event(Event::Mouse(MouseEvent {
1015 kind,
1016 position: Position::new(column, row),
1017 modifiers,
1018 })),
1019 consumed,
1020 })
1021}
1022
1023fn decoded_key(code: KeyCode, modifiers: Modifiers, consumed: usize) -> io::Result<Decoded> {
1024 Ok(Decoded {
1025 item: DecodedItem::Event(Event::Key(KeyEvent {
1026 code,
1027 modifiers,
1028 kind: KeyKind::Press,
1029 state: crate::KeyEventState::NONE,
1030 })),
1031 consumed,
1032 })
1033}
1034
1035fn csi_modifiers(body: &[u8]) -> Modifiers {
1036 let value = std::str::from_utf8(body)
1037 .ok()
1038 .and_then(|text| text.split(';').nth(1))
1039 .and_then(|value| value.parse::<u8>().ok())
1040 .unwrap_or(1)
1041 .saturating_sub(1);
1042 let mut modifiers = Modifiers::NONE;
1043 if value & 1 != 0 {
1044 modifiers = modifiers | Modifiers::SHIFT;
1045 }
1046 if value & 2 != 0 {
1047 modifiers = modifiers | Modifiers::ALT;
1048 }
1049 if value & 4 != 0 {
1050 modifiers = modifiers | Modifiers::CONTROL;
1051 }
1052 modifiers
1053}
1054
1055fn mouse_modifiers(flags: usize) -> Modifiers {
1056 let mut modifiers = Modifiers::NONE;
1057 if flags & 4 != 0 {
1058 modifiers = modifiers | Modifiers::SHIFT;
1059 }
1060 if flags & 8 != 0 {
1061 modifiers = modifiers | Modifiers::ALT;
1062 }
1063 if flags & 16 != 0 {
1064 modifiers = modifiers | Modifiers::CONTROL;
1065 }
1066 modifiers
1067}
1068
1069const fn mouse_button(flags: usize) -> MouseButton {
1070 match flags & 3 {
1071 0 => MouseButton::Left,
1072 1 => MouseButton::Middle,
1073 _ => MouseButton::Right,
1074 }
1075}
1076
1077fn two_numbers(body: &[u8]) -> io::Result<(usize, usize)> {
1078 let text = std::str::from_utf8(body)
1079 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
1080 let mut values = text.split(';');
1081 Ok((parse_number(values.next())?, parse_number(values.next())?))
1082}
1083
1084fn first_number(body: &[u8]) -> io::Result<usize> {
1085 let text = std::str::from_utf8(body)
1086 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
1087 parse_number(text.split(';').next())
1088}
1089
1090fn parse_number(value: Option<&str>) -> io::Result<usize> {
1091 value
1092 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing terminal parameter"))?
1093 .parse()
1094 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
1095}
1096
1097fn utf8_width(first: u8) -> io::Result<usize> {
1098 match first {
1099 0x00..=0x7f => Ok(1),
1100 0xc2..=0xdf => Ok(2),
1101 0xe0..=0xef => Ok(3),
1102 0xf0..=0xf4 => Ok(4),
1103 _ => Err(io::Error::new(
1104 io::ErrorKind::InvalidData,
1105 "invalid leading UTF-8 byte from terminal",
1106 )),
1107 }
1108}
1109
1110fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1111 haystack
1112 .windows(needle.len())
1113 .position(|window| window == needle)
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use std::{os::fd::FromRawFd, time::Duration};
1119
1120 use super::*;
1121 use crate::TerminalText;
1122
1123 fn event(bytes: &[u8]) -> Event {
1124 let decoded = decode(bytes, true, true, false, false)
1125 .expect("sequence is valid")
1126 .expect("sequence is complete");
1127 assert_eq!(decoded.consumed, bytes.len());
1128 match decoded.item {
1129 DecodedItem::Event(event) => event,
1130 DecodedItem::Cursor(_) => panic!("expected an event"),
1131 DecodedItem::Background(_) => panic!("expected an event"),
1132 DecodedItem::BackgroundOverflow => panic!("expected an event"),
1133 DecodedItem::Response(_) => panic!("expected an event"),
1134 }
1135 }
1136
1137 fn background(bytes: &[u8]) -> Option<TerminalBackground> {
1138 let decoded = decode(bytes, false, true, true, false)
1139 .expect("sequence is valid")
1140 .expect("sequence is complete");
1141 assert_eq!(decoded.consumed, bytes.len());
1142 match decoded.item {
1143 DecodedItem::Background(background) => background,
1144 _ => panic!("expected a background response"),
1145 }
1146 }
1147
1148 #[test]
1149 fn decoder_handles_text_navigation_modifiers_and_paste() {
1150 assert_eq!(
1151 event("漆".as_bytes()),
1152 Event::Key(KeyEvent::new(KeyCode::Char('漆')))
1153 );
1154 assert_eq!(
1155 event(b"\x1b[1;5A"),
1156 Event::Key(KeyEvent::new(KeyCode::Up).with_modifiers(Modifiers::CONTROL))
1157 );
1158 assert_eq!(
1159 event(b"\x1b]"),
1160 Event::Key(KeyEvent::new(KeyCode::Char(']')).with_modifiers(Modifiers::ALT))
1161 );
1162 assert_eq!(
1163 event(b"\x1b[200~a\nb\x1b[201~"),
1164 Event::Paste("a\nb".to_owned())
1165 );
1166 assert_eq!(
1167 event(b"\n"),
1168 Event::Key(KeyEvent::new(KeyCode::Char('j')).with_modifiers(Modifiers::CONTROL))
1169 );
1170 let canonical_newline = decode(b"\n", true, false, false, false)
1171 .expect("newline is valid")
1172 .expect("newline is complete");
1173 assert!(matches!(
1174 canonical_newline.item,
1175 DecodedItem::Event(Event::Key(KeyEvent {
1176 code: KeyCode::Enter,
1177 ..
1178 }))
1179 ));
1180 }
1181
1182 #[test]
1183 fn decoder_separates_cursor_replies_from_events() {
1184 let decoded = decode(b"\x1b[12;34R", false, true, false, false)
1185 .expect("reply is valid")
1186 .expect("reply is complete");
1187 assert!(matches!(decoded.item, DecodedItem::Cursor(Position { .. })));
1188 if let DecodedItem::Cursor(position) = decoded.item {
1189 assert_eq!(position, Position::new(33, 11));
1190 }
1191 }
1192
1193 #[test]
1194 fn decoder_normalizes_osc_background_components_to_sixteen_bits() {
1195 assert!(
1196 decode(b"\x1b]11;rgb:ffff", true, true, true, false)
1197 .expect("partial reply is valid")
1198 .is_none()
1199 );
1200 assert_eq!(
1201 background(b"\x1b]11;rgb:f/0/8\x07"),
1202 Some(TerminalBackground::new(0xffff, 0x0000, 0x8888))
1203 );
1204 assert_eq!(
1205 background(b"\x1b]11;rgb:ff/00/80\x1b\\"),
1206 Some(TerminalBackground::new(0xffff, 0x0000, 0x8080))
1207 );
1208 assert_eq!(
1209 background(b"\x1b]11;rgb:fff/000/800\x1b\\"),
1210 Some(TerminalBackground::new(0xffff, 0x0000, 0x8008))
1211 );
1212 assert_eq!(
1213 background(b"\x1b]11;rgb:ffff/0000/8000\x1b\\"),
1214 Some(TerminalBackground::new(0xffff, 0x0000, 0x8000))
1215 );
1216 }
1217
1218 #[test]
1219 fn decoder_marks_malformed_osc_background_replies_as_unavailable() {
1220 for response in [
1221 b"\x1b]11;rgb:/0/0\x07".as_slice(),
1222 b"\x1b]11;rgb:00000/0/0\x07".as_slice(),
1223 b"\x1b]11;rgb:g/0/0\x07".as_slice(),
1224 b"\x1b]11;rgb:0/0\x07".as_slice(),
1225 b"\x1b]11;rgb:0/0/0/0\x07".as_slice(),
1226 b"\x1b]11;not-a-color\x07".as_slice(),
1227 ] {
1228 assert_eq!(background(response), None);
1229 }
1230 }
1231
1232 #[test]
1233 fn decoder_extracts_confirmed_terminal_capabilities() {
1234 let responses = [
1235 (b"\x1b_Gi=31;OK\x1b\\".as_slice(), CapabilityResponse::Kitty),
1236 (b"\x1b[?64;4c".as_slice(), CapabilityResponse::Sixel),
1237 (
1238 b"\x1bP1+r436f=323536\x1b\\".as_slice(),
1239 CapabilityResponse::ColorCount(256),
1240 ),
1241 (
1242 b"\x1bP1+r524742=38\x1b\\".as_slice(),
1243 CapabilityResponse::TrueColor,
1244 ),
1245 (b"\x1b[0n".as_slice(), CapabilityResponse::Complete),
1246 ];
1247
1248 for (wire, expected) in responses {
1249 let decoded = decode(wire, false, true, false, false)
1250 .expect("response decodes")
1251 .expect("response is complete");
1252 assert_eq!(decoded.consumed, wire.len());
1253 assert!(matches!(decoded.item, DecodedItem::Response(actual) if actual == expected));
1254 }
1255 }
1256
1257 #[test]
1258 fn decoder_does_not_promote_negative_or_unrelated_responses() {
1259 for wire in [
1260 b"\x1b_Gi=31;EINVAL\x1b\\".as_slice(),
1261 b"\x1b[?64;1;2c".as_slice(),
1262 b"\x1bP0+r524742\x1b\\".as_slice(),
1263 b"\x1bP1+r756e6b6e6f776e=31\x1b\\".as_slice(),
1264 ] {
1265 let decoded = decode(wire, false, true, false, false)
1266 .expect("response decodes")
1267 .expect("response is complete");
1268 assert!(matches!(
1269 decoded.item,
1270 DecodedItem::Response(CapabilityResponse::Ignored)
1271 ));
1272 }
1273 }
1274
1275 #[test]
1276 fn decoder_rejects_unbounded_control_responses() {
1277 let mut response = b"\x1b_".to_vec();
1278 response.resize(MAX_CONTROL_RESPONSE_BYTES + 1, b'x');
1279
1280 let error = match decode(&response, false, true, false, false) {
1281 Err(error) => error,
1282 Ok(_) => panic!("oversized response is accepted"),
1283 };
1284 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
1285 }
1286
1287 #[test]
1288 fn capability_probe_uses_only_positive_responses() {
1289 let mut probe = CapabilityProbe::default();
1290 for response in [
1291 CapabilityResponse::Kitty,
1292 CapabilityResponse::Sixel,
1293 CapabilityResponse::ColorCount(256),
1294 CapabilityResponse::Complete,
1295 ] {
1296 probe.record(response);
1297 }
1298
1299 let capabilities = probe.capabilities();
1300 assert_eq!(capabilities.color_level(), ColorLevel::Ansi256);
1301 assert!(capabilities.attributes().is_empty());
1302 assert_eq!(
1303 capabilities.underline_styles(),
1304 crate::UnderlineStyles::empty()
1305 );
1306 assert!(!capabilities.underline_colors());
1307 assert!(!capabilities.hyperlinks());
1308 assert!(capabilities.supports_graphics(crate::TerminalGraphicsProtocol::Kitty));
1309 assert!(capabilities.supports_graphics(crate::TerminalGraphicsProtocol::Sixel));
1310 }
1311
1312 #[test]
1313 fn native_terminal_owns_command_input_mode_and_query_on_one_tty() {
1314 let (mut peer, mut terminal) = terminal_pair();
1315
1316 terminal.enable_raw_mode().expect("raw mode is enabled");
1317 assert!(terminal.raw_mode_enabled().expect("raw mode is inspected"));
1318 let raw = get_termios(terminal.file()).expect("raw mode is readable");
1319 assert_eq!(raw.c_cc[libc::VMIN], 0);
1320 assert_eq!(raw.c_cc[libc::VTIME], 0);
1321
1322 peer.write_all(b"\x1b[1;5A").expect("input is written");
1323 assert_eq!(
1324 terminal
1325 .poll_event_timeout(Duration::from_millis(100))
1326 .expect("event is read"),
1327 Some(Event::Key(
1328 KeyEvent::new(KeyCode::Up).with_modifiers(Modifiers::CONTROL)
1329 ))
1330 );
1331
1332 terminal
1333 .write_command(Command::Print(
1334 TerminalText::try_from("ok").expect("printable text"),
1335 ))
1336 .expect("command is written");
1337 terminal.flush().expect("command is flushed");
1338 let mut output = [0_u8; 2];
1339 peer.read_exact(&mut output).expect("command reaches tty");
1340 assert_eq!(&output, b"ok");
1341
1342 terminal.disable_raw_mode().expect("raw mode is restored");
1343 assert!(!terminal.raw_mode_enabled().expect("mode is inspected"));
1344 }
1345
1346 #[test]
1347 fn cursor_query_consumes_its_reply_without_losing_ordinary_events() {
1348 let (mut peer, mut terminal) = terminal_pair();
1349 terminal.enable_raw_mode().expect("raw mode is enabled");
1350 peer.write_all(b"x\x1b[12;34R")
1351 .expect("ordinary input and cursor reply are written");
1352
1353 assert_eq!(
1354 terminal.cursor_position().expect("query is answered"),
1355 Position::new(33, 11)
1356 );
1357 let mut query = [0_u8; 4];
1358 peer.read_exact(&mut query).expect("query reaches tty");
1359 assert_eq!(&query, b"\x1b[6n");
1360 assert_eq!(
1361 terminal.read_event().expect("queued event is retained"),
1362 Event::Key(KeyEvent::new(KeyCode::Char('x')))
1363 );
1364 }
1365
1366 #[test]
1367 fn background_query_uses_osc_11_and_preserves_ordinary_input() {
1368 let (mut peer, mut terminal) = terminal_pair();
1369 let (release_peer, keep_peer_open) = std::sync::mpsc::channel();
1370 let responder = std::thread::spawn(move || {
1371 let mut query = vec![0_u8; BACKGROUND_QUERY.len()];
1372 peer.read_exact(&mut query).expect("query reaches tty");
1373 assert_eq!(query, BACKGROUND_QUERY);
1374 peer.write_all(b"x\x1b]11;rgb:1234/5678/9abc\x1b\\")
1375 .expect("input and response reach tty");
1376 keep_peer_open
1377 .recv()
1378 .expect("peer remains open until the response is consumed");
1379 });
1380
1381 let background = terminal
1382 .terminal_background()
1383 .expect("background query succeeds");
1384 assert!(!terminal.raw_mode_enabled().expect("raw mode is restored"));
1385 release_peer.send(()).expect("responder remains available");
1386 responder.join().expect("responder completes");
1387 assert_eq!(
1388 background,
1389 Some(TerminalBackground::new(0x1234, 0x5678, 0x9abc))
1390 );
1391 assert_eq!(
1392 terminal.read_event().expect("queued event is retained"),
1393 Event::Key(KeyEvent::new(KeyCode::Char('x')))
1394 );
1395 }
1396
1397 #[test]
1398 fn background_query_preserves_alt_bracket_before_its_reply() {
1399 let (mut peer, mut terminal) = terminal_pair();
1400 let (release_peer, keep_peer_open) = std::sync::mpsc::channel();
1401 let responder = std::thread::spawn(move || {
1402 let mut query = vec![0_u8; BACKGROUND_QUERY.len()];
1403 peer.read_exact(&mut query).expect("query reaches tty");
1404 peer.write_all(b"\x1b]\x1b]11;rgb:1234/5678/9abc\x1b\\")
1405 .expect("input and response reach tty");
1406 keep_peer_open
1407 .recv()
1408 .expect("peer remains open until the response is consumed");
1409 });
1410
1411 let background = terminal
1412 .terminal_background()
1413 .expect("background query succeeds");
1414 release_peer.send(()).expect("responder remains available");
1415 responder.join().expect("responder completes");
1416 assert_eq!(
1417 background,
1418 Some(TerminalBackground::new(0x1234, 0x5678, 0x9abc))
1419 );
1420 assert_eq!(
1421 terminal.read_event().expect("queued event is retained"),
1422 Event::Key(KeyEvent::new(KeyCode::Char(']')).with_modifiers(Modifiers::ALT))
1423 );
1424 }
1425
1426 #[test]
1427 fn background_query_returns_none_for_malformed_and_timed_out_replies() {
1428 let (mut malformed_peer, mut malformed_terminal) = terminal_pair();
1429 let (release_malformed_peer, keep_malformed_peer_open) = std::sync::mpsc::channel();
1430 let malformed_responder = std::thread::spawn(move || {
1431 let mut query = vec![0_u8; BACKGROUND_QUERY.len()];
1432 malformed_peer
1433 .read_exact(&mut query)
1434 .expect("query reaches tty");
1435 malformed_peer
1436 .write_all(b"\x1b]11;rgb:nope\x07")
1437 .expect("malformed response reaches tty");
1438 keep_malformed_peer_open
1439 .recv()
1440 .expect("peer remains open until the response is consumed");
1441 });
1442 let malformed = malformed_terminal
1443 .terminal_background()
1444 .expect("malformed reply is not an I/O error");
1445 release_malformed_peer
1446 .send(())
1447 .expect("malformed responder remains available");
1448 malformed_responder
1449 .join()
1450 .expect("malformed responder completes");
1451 assert_eq!(malformed, None);
1452
1453 let (mut silent_peer, mut silent_terminal) = terminal_pair();
1454 silent_terminal
1455 .enable_raw_mode()
1456 .expect("raw mode is enabled");
1457 let (release_peer, keep_peer_open) = std::sync::mpsc::channel();
1458 let silent_responder = std::thread::spawn(move || {
1459 let mut query = vec![0_u8; BACKGROUND_QUERY.len()];
1460 silent_peer
1461 .read_exact(&mut query)
1462 .expect("query reaches tty");
1463 keep_peer_open
1464 .recv()
1465 .expect("peer remains open through timeout");
1466 });
1467 assert_eq!(
1468 silent_terminal
1469 .query_terminal_background(Duration::from_millis(20))
1470 .expect("timeout is not an I/O error"),
1471 None
1472 );
1473 silent_terminal
1474 .disable_raw_mode()
1475 .expect("raw mode is restored");
1476 release_peer.send(()).expect("silent peer is released");
1477 silent_responder.join().expect("silent responder completes");
1478 }
1479
1480 #[test]
1481 fn background_query_keeps_connection_failures_as_errors() {
1482 let (peer, mut terminal) = terminal_pair();
1483 drop(peer);
1484
1485 assert!(terminal.terminal_background().is_err());
1486 }
1487
1488 #[test]
1489 fn background_query_discards_an_unterminated_reply_before_resuming_events() {
1490 let (mut peer, mut terminal) = terminal_pair();
1491 terminal.enable_raw_mode().expect("raw mode is enabled");
1492 let (send_key, key_requested) = std::sync::mpsc::channel();
1493 let (release_peer, keep_peer_open) = std::sync::mpsc::channel();
1494 let responder = std::thread::spawn(move || {
1495 let mut query = vec![0_u8; BACKGROUND_QUERY.len()];
1496 peer.read_exact(&mut query).expect("query reaches tty");
1497 peer.write_all(b"\x1b]11;rgb:ffff/ffff")
1498 .expect("partial response reaches tty");
1499 key_requested.recv().expect("ordinary input is requested");
1500 peer.write_all(b"x").expect("ordinary input reaches tty");
1501 keep_peer_open
1502 .recv()
1503 .expect("peer remains open until input is consumed");
1504 });
1505
1506 assert_eq!(
1507 terminal
1508 .query_terminal_background(Duration::from_millis(20))
1509 .expect("partial response times out without an I/O error"),
1510 None
1511 );
1512 send_key.send(()).expect("ordinary input is released");
1513 assert_eq!(
1514 terminal.read_event().expect("event reading resumes"),
1515 Event::Key(KeyEvent::new(KeyCode::Char('x')))
1516 );
1517 terminal.disable_raw_mode().expect("raw mode is restored");
1518 release_peer.send(()).expect("responder remains available");
1519 responder.join().expect("responder completes");
1520 }
1521
1522 #[test]
1523 fn background_query_discards_an_oversized_reply_before_resuming_events() {
1524 let (mut peer, mut terminal) = terminal_pair();
1525 terminal.enable_raw_mode().expect("raw mode is enabled");
1526 let (send_key, key_requested) = std::sync::mpsc::channel();
1527 let (release_peer, keep_peer_open) = std::sync::mpsc::channel();
1528 let responder = std::thread::spawn(move || {
1529 let mut query = vec![0_u8; BACKGROUND_QUERY.len()];
1530 peer.read_exact(&mut query).expect("query reaches tty");
1531 let mut response = b"\x1b]11;rgb:".to_vec();
1532 response.resize(MAX_CONTROL_RESPONSE_BYTES + 16 * 1024, b'x');
1533 response.extend_from_slice(b"\x1b\\");
1534 peer.write_all(&response)
1535 .expect("oversized response reaches tty");
1536 key_requested.recv().expect("ordinary input is requested");
1537 peer.write_all(b"x").expect("ordinary input reaches tty");
1538 keep_peer_open
1539 .recv()
1540 .expect("peer remains open until input is consumed");
1541 });
1542
1543 assert_eq!(
1544 terminal
1545 .query_terminal_background(Duration::from_secs(1))
1546 .expect("oversized response is not an I/O error"),
1547 None
1548 );
1549 send_key.send(()).expect("ordinary input is released");
1550 assert_eq!(
1551 terminal.read_event().expect("event reading resumes"),
1552 Event::Key(KeyEvent::new(KeyCode::Char('x')))
1553 );
1554 terminal.disable_raw_mode().expect("raw mode is restored");
1555 release_peer.send(()).expect("responder remains available");
1556 responder.join().expect("responder completes");
1557 }
1558
1559 #[test]
1560 fn background_query_discards_a_trailing_escape_when_overflow_times_out() {
1561 let (mut peer, mut terminal) = terminal_pair();
1562 terminal.enable_raw_mode().expect("raw mode is enabled");
1563 let (send_key, key_requested) = std::sync::mpsc::channel();
1564 let (release_peer, keep_peer_open) = std::sync::mpsc::channel();
1565 let responder = std::thread::spawn(move || {
1566 let mut query = vec![0_u8; BACKGROUND_QUERY.len()];
1567 peer.read_exact(&mut query).expect("query reaches tty");
1568 let mut response = b"\x1b]11;rgb:".to_vec();
1569 response.resize(MAX_CONTROL_RESPONSE_BYTES, b'x');
1570 response.push(0x1b);
1571 peer.write_all(&response)
1572 .expect("unterminated oversized response reaches tty");
1573 key_requested.recv().expect("ordinary input is requested");
1574 peer.write_all(b"x").expect("ordinary input reaches tty");
1575 keep_peer_open
1576 .recv()
1577 .expect("peer remains open until input is consumed");
1578 });
1579
1580 assert_eq!(
1581 terminal
1582 .query_terminal_background(Duration::from_secs(1))
1583 .expect("unterminated overflow times out without an I/O error"),
1584 None
1585 );
1586 send_key.send(()).expect("ordinary input is released");
1587 assert_eq!(
1588 terminal.read_event().expect("event reading resumes"),
1589 Event::Key(KeyEvent::new(KeyCode::Char('x')))
1590 );
1591 terminal.disable_raw_mode().expect("raw mode is restored");
1592 release_peer.send(()).expect("responder remains available");
1593 responder.join().expect("responder completes");
1594 }
1595
1596 #[test]
1597 fn background_query_observes_a_fresh_value_on_each_call() {
1598 let (mut peer, mut terminal) = terminal_pair();
1599 let (release_peer, keep_peer_open) = std::sync::mpsc::channel();
1600 let responder = std::thread::spawn(move || {
1601 for response in [
1602 b"\x1b]11;rgb:0000/0000/0000\x1b\\".as_slice(),
1603 b"\x1b]11;rgb:ffff/ffff/ffff\x1b\\".as_slice(),
1604 ] {
1605 let mut query = vec![0_u8; BACKGROUND_QUERY.len()];
1606 peer.read_exact(&mut query).expect("query reaches tty");
1607 peer.write_all(response).expect("response reaches tty");
1608 }
1609 keep_peer_open
1610 .recv()
1611 .expect("peer remains open until both responses are consumed");
1612 });
1613
1614 assert_eq!(
1615 terminal
1616 .terminal_background()
1617 .expect("first query succeeds"),
1618 Some(TerminalBackground::new(0, 0, 0))
1619 );
1620 assert_eq!(
1621 terminal
1622 .terminal_background()
1623 .expect("second query succeeds"),
1624 Some(TerminalBackground::new(u16::MAX, u16::MAX, u16::MAX,))
1625 );
1626 release_peer.send(()).expect("responder remains available");
1627 responder.join().expect("responder completes");
1628 }
1629
1630 #[test]
1631 fn capability_query_uses_terminal_responses_and_preserves_input() {
1632 let (mut peer, mut terminal) = terminal_pair();
1633 let (release_peer, keep_peer_open) = std::sync::mpsc::channel();
1634 let responder = std::thread::spawn(move || {
1635 let mut query = vec![0_u8; CAPABILITY_QUERY.len()];
1636 peer.read_exact(&mut query).expect("query reaches tty");
1637 assert_eq!(query, CAPABILITY_QUERY);
1638 peer.write_all(
1639 concat!(
1640 "x",
1641 "\x1b_Gi=31;OK\x1b\\",
1642 "\x1b[?64;4c",
1643 "\x1bP1+r436f=323536\x1b\\",
1644 "\x1bP1+r524742=38\x1b\\",
1645 "\x1b[0n",
1646 )
1647 .as_bytes(),
1648 )
1649 .expect("responses reach tty");
1650 keep_peer_open
1651 .recv()
1652 .expect("peer stays open until the response is consumed");
1653 });
1654
1655 let capabilities = terminal
1656 .terminal_capabilities()
1657 .expect("capabilities are queried");
1658 assert!(!terminal.raw_mode_enabled().expect("raw mode is restored"));
1659 release_peer.send(()).expect("responder remains available");
1660 responder.join().expect("responder completes");
1661
1662 assert_eq!(capabilities.color_level(), ColorLevel::TrueColor);
1663 assert!(capabilities.attributes().is_empty());
1664 assert_eq!(
1665 capabilities.underline_styles(),
1666 crate::UnderlineStyles::empty()
1667 );
1668 assert!(capabilities.supports_graphics(crate::TerminalGraphicsProtocol::Kitty));
1669 assert!(capabilities.supports_graphics(crate::TerminalGraphicsProtocol::Sixel));
1670 assert_eq!(
1671 terminal
1672 .terminal_capabilities()
1673 .expect("confirmed capabilities are cached"),
1674 capabilities
1675 );
1676 assert_eq!(
1677 terminal.read_event().expect("queued event is retained"),
1678 Event::Key(KeyEvent::new(KeyCode::Char('x')))
1679 );
1680 }
1681
1682 fn terminal_pair() -> (File, NativeTerminal) {
1683 let mut peer_fd = -1;
1684 let mut terminal_fd = -1;
1685 let result = unsafe {
1688 libc::openpty(
1689 &mut peer_fd,
1690 &mut terminal_fd,
1691 std::ptr::null_mut(),
1692 std::ptr::null_mut(),
1693 std::ptr::null_mut(),
1694 )
1695 };
1696 assert_eq!(result, 0, "openpty failed: {}", io::Error::last_os_error());
1697 let peer = unsafe { File::from_raw_fd(peer_fd) };
1700 let terminal_file = unsafe { File::from_raw_fd(terminal_fd) };
1702 let terminal = NativeTerminal::from_file(terminal_file).expect("pty is a terminal");
1703 (peer, terminal)
1704 }
1705}