Skip to main content

urushi_terminal/backend/
native.rs

1//! Native Unix terminal backend.
2
3use 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
89/// One owned connection to the process's controlling terminal.
90///
91/// Input, output, raw mode, window inspection, protocol replies, and ordinary
92/// events all use the same `/dev/tty` endpoint. No external terminal framework
93/// participates in this backend.
94pub 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    /// Opens the process's controlling terminal as one input/output endpoint.
111    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    /// Inspects the terminal endpoint opened by this backend.
139    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            // Raw mode uses VMIN=0, so zero bytes means that no input became
276            // available before the read completed. wait_readable rejects a
277            // hangup once the endpoint is no longer a valid terminal.
278            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        // Matches POSIX cfmakeraw while retaining the exact original value for
313        // restoration rather than trying to reconstruct it later.
314        unsafe { libc::cfmakeraw(&mut raw) };
315        // poll owns the wait. Keeping read nonblocking prevents an input worker
316        // from holding the shared terminal connection after stale readiness.
317        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        // The native decoder currently requests only the portable legacy input
497        // protocol, so it must not advertise enhanced event negotiation.
498        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    // SAFETY: `value` points to writable storage for one termios value and the
531    // file descriptor remains open for the duration of the call.
532    if unsafe { libc::tcgetattr(file.as_raw_fd(), value.as_mut_ptr()) } == -1 {
533        Err(io::Error::last_os_error())
534    } else {
535        // SAFETY: tcgetattr returned success and therefore initialized value.
536        Ok(unsafe { value.assume_init() })
537    }
538}
539
540fn set_termios(file: &File, value: &libc::termios) -> io::Result<()> {
541    // SAFETY: `value` is a valid termios value and the descriptor stays open.
542    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    // SAFETY: `value` is writable winsize storage and the descriptor is open.
552    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    // SAFETY: ioctl returned success and initialized the winsize structure.
556    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    // SAFETY: `descriptor` is valid stack storage for one pollfd and its file
575    // descriptor remains open for the duration of the call.
576    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        // SAFETY: openpty initializes both descriptors on success. The null
1686        // name and settings pointers request the platform defaults.
1687        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        // SAFETY: openpty returned fresh owned descriptors and each is wrapped
1698        // exactly once.
1699        let peer = unsafe { File::from_raw_fd(peer_fd) };
1700        // SAFETY: see the ownership argument above for the peer descriptor.
1701        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}