Skip to main content

uncurses/ansi/
status.rs

1//! Device Status Reports and cursor-position reports.
2//!
3//! ## Category
4//!
5//! This module emits DSR requests and report responses: cursor position, extended
6//! cursor position, light/dark preference, and terminal visibility reporting. It
7//! also carries DECRQSS and its DECRPSS response, which ask after a current
8//! setting rather than device status.
9//!
10//! ## CSI conventions
11//!
12//! ANSI DSR uses `ESC [ Ps n`; DEC-private DSR inserts `?`. Cursor reports use
13//! final byte `R`, while light/dark and visibility reports use private DSR
14//! numbers.
15//!
16//! ## Mode interaction
17//!
18//! The light/dark notification request is related to
19//! [`Mode::LIGHT_DARK`](crate::ansi::mode::Mode::LIGHT_DARK), DEC private mode
20//! 2031, and the visibility request to
21//! [`Mode::VISIBILITY_REPORTS`](crate::ansi::mode::Mode::VISIBILITY_REPORTS),
22//! DEC private mode 2033. Both queries report once without changing their
23//! mode. Cursor-position reports are independent of modes but may be
24//! interpreted relative to terminal origin behavior.
25
26use std::io::{self, Write};
27
28/// Request standard cursor position: exact bytes `ESC [ 6 n` (`b"\x1b[6n"`).
29///
30/// The terminal replies with CPR, `ESC [ <line> ; <column> R`, using one-based coordinates.
31pub const REQUEST_CURSOR_POSITION: &[u8] = b"\x1b[6n";
32
33/// Request extended cursor position: exact bytes `ESC [ ? 6 n` (`b"\x1b[?6n"`).
34///
35/// The terminal replies with a private cursor-position report, optionally including page.
36pub const REQUEST_EXTENDED_CURSOR_POSITION: &[u8] = b"\x1b[?6n";
37
38/// Request light/dark preference report: exact bytes `ESC [ ? 996 n` (`b"\x1b[?996n"`).
39pub const REQUEST_LIGHT_DARK_REPORT: &[u8] = b"\x1b[?996n";
40
41/// Request a terminal visibility report: exact bytes `ESC [ ? 998 n` (`b"\x1b[?998n"`).
42///
43/// The terminal replies with `ESC [ ? 999 ; Ps n`. This query does not change
44/// [`Mode::VISIBILITY_REPORTS`](crate::ansi::mode::Mode::VISIBILITY_REPORTS).
45pub const REQUEST_VISIBILITY_REPORT: &[u8] = b"\x1b[?998n";
46
47/// Write [`REQUEST_CURSOR_POSITION`], the standard DSR 6 cursor-position request.
48pub fn write_request_cursor_position<W: Write>(w: &mut W) -> io::Result<()> {
49    w.write_all(REQUEST_CURSOR_POSITION)
50}
51
52/// Write [`REQUEST_EXTENDED_CURSOR_POSITION`], the DEC private extended cursor-position request.
53pub fn write_request_extended_cursor_position<W: Write>(w: &mut W) -> io::Result<()> {
54    w.write_all(REQUEST_EXTENDED_CURSOR_POSITION)
55}
56
57/// Write [`REQUEST_LIGHT_DARK_REPORT`], the light/dark preference query.
58pub fn write_request_light_dark_report<W: Write>(w: &mut W) -> io::Result<()> {
59    w.write_all(REQUEST_LIGHT_DARK_REPORT)
60}
61
62/// Write [`REQUEST_VISIBILITY_REPORT`], the one-shot terminal visibility query.
63pub fn write_request_visibility_report<W: Write>(w: &mut W) -> io::Result<()> {
64    w.write_all(REQUEST_VISIBILITY_REPORT)
65}
66
67/// Request a current setting with DECRQSS, `ESC P $ q <selector> ESC \`.
68///
69/// `selector` spells the control function being asked about, as its private
70/// prefix, intermediates and final byte: `"m"` for SGR, `" q"` for
71/// `DECSCUSR` (cursor style), `"r"` for `DECSTBM` (scrolling region),
72/// `"\"q"` for `DECSCA`, `"$|"` for `DECSCPP`. xterm's private requests take
73/// a parameter too, as in `">4m"` for `XTQMODKEYS`. An empty selector is
74/// written like any other and the terminal reports it as unrecognized, which
75/// keeps one request paired with one reply.
76///
77/// The terminal answers with DECRPSS: `ESC P 1 $ r <D...D> ESC \` when it
78/// recognizes the request, where the data string is the setting spelled out
79/// as a CSI sequence without its introducer, and `ESC P 0 $ r ESC \` when it
80/// does not. Both decode as
81/// [`Event::SettingReport`](crate::event::Event::SettingReport). Because the
82/// unrecognized form echoes nothing back, only the request says which setting
83/// it was about. Use [`write_decrpss`] to encode either reply.
84pub fn write_decrqss<W: Write>(w: &mut W, selector: &str) -> io::Result<()> {
85    write!(w, "\x1bP$q{selector}\x1b\\")
86}
87
88/// Encode a Device Status Report request.
89///
90/// When `dec` is `false`, the format is `ESC [ <ps> n`; when `dec` is `true`, the format is `ESC [ ? <ps> n`.
91pub fn write_dsr_request<W: Write>(w: &mut W, dec: bool, ps: u16) -> io::Result<()> {
92    if dec {
93        write!(w, "\x1b[?{ps}n")
94    } else {
95        write!(w, "\x1b[{ps}n")
96    }
97}
98
99/// Encode a standard Cursor Position Report response, `ESC [ <line> ; <column> R`.
100///
101/// `line` and `column` are one-based terminal coordinates; values less than `1` are clamped to `1`.
102pub fn write_cpr<W: Write>(w: &mut W, line: u16, column: u16) -> io::Result<()> {
103    let l = line.max(1);
104    let c = column.max(1);
105    write!(w, "\x1b[{l};{c}R")
106}
107
108/// Encode an extended Cursor Position Report response.
109///
110/// With `page == 0`, emits `ESC [ ? <line> ; <column> R`; otherwise emits `ESC [ ? <line> ; <column> ; <page> R`. `line` and `column` are clamped to at least `1`.
111pub fn write_decxcpr<W: Write>(w: &mut W, line: u16, column: u16, page: u16) -> io::Result<()> {
112    let l = line.max(1);
113    let c = column.max(1);
114    if page == 0 {
115        write!(w, "\x1b[?{l};{c}R")
116    } else {
117        write!(w, "\x1b[?{l};{c};{page}R")
118    }
119}
120
121/// Encode a DECRPSS report, `ESC P <ps> $ r <D...D> ESC \`.
122///
123/// This is the terminal's answer to [`write_decrqss`]. `valid` says whether
124/// the terminal recognized the request. `settings` is the control function
125/// being reported, spelled out as every character of its CSI sequence except
126/// the introducer: `"0;4;5;7m"` for SGR, `"1;24r"` for `DECSTBM`, `"2 q"` for
127/// `DECSCUSR`, `">4;2m"` for xterm's `XTQMODKEYS`. A terminal sends no data
128/// string at all for an invalid request, so `false` emits `ESC P 0 $ r ESC \`
129/// and ignores `settings`.
130///
131/// Beware that the VT510 manual documents `Ps` the other way around, as `0`
132/// for valid and `1` for invalid. It is wrong: a VT420 tested in 1996 had the
133/// two reversed, and vttest, DEC STD 070 and xterm all treat `1` as the valid
134/// one.
135pub fn write_decrpss<W: Write>(w: &mut W, valid: bool, settings: &str) -> io::Result<()> {
136    if !valid {
137        return w.write_all(b"\x1bP0$r\x1b\\");
138    }
139    write!(w, "\x1bP1$r{settings}\x1b\\")
140}
141
142/// Encode a light/dark report response.
143///
144/// `dark == true` emits `ESC [ ? 997 ; 1 n`; `false` emits `ESC [ ? 997 ; 2 n`.
145pub fn write_light_dark_report<W: Write>(w: &mut W, dark: bool) -> io::Result<()> {
146    if dark {
147        w.write_all(b"\x1b[?997;1n")
148    } else {
149        w.write_all(b"\x1b[?997;2n")
150    }
151}
152
153/// Encode a terminal visibility report response.
154///
155/// `visible == true` emits `ESC [ ? 999 ; 1 n` (potentially visible); `false`
156/// emits `ESC [ ? 999 ; 2 n` (not visible).
157pub fn write_visibility_report<W: Write>(w: &mut W, visible: bool) -> io::Result<()> {
158    if visible {
159        w.write_all(b"\x1b[?999;1n")
160    } else {
161        w.write_all(b"\x1b[?999;2n")
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_cpr() {
171        let mut buf = Vec::new();
172        write_cpr(&mut buf, 10, 20).unwrap();
173        assert_eq!(buf, b"\x1b[10;20R");
174    }
175
176    #[test]
177    fn test_decxcpr() {
178        let mut buf = Vec::new();
179        write_decxcpr(&mut buf, 5, 6, 0).unwrap();
180        write_decxcpr(&mut buf, 5, 6, 2).unwrap();
181        assert_eq!(buf, b"\x1b[?5;6R\x1b[?5;6;2R");
182    }
183
184    #[test]
185    fn test_decrqss() {
186        let mut buf = Vec::new();
187        write_decrqss(&mut buf, "m").unwrap();
188        write_decrqss(&mut buf, " q").unwrap();
189        // Replies carry nothing that names the request, so they are matched by
190        // order. An empty selector still goes out, so the counts stay level.
191        write_decrqss(&mut buf, "").unwrap();
192        assert_eq!(buf, b"\x1bP$qm\x1b\\\x1bP$q q\x1b\\\x1bP$q\x1b\\");
193    }
194
195    #[test]
196    fn test_decrpss() {
197        let mut buf = Vec::new();
198        // The two examples the VT510 manual gives for DECRPSS.
199        write_decrpss(&mut buf, true, "0;4;5;7m").unwrap();
200        write_decrpss(&mut buf, true, "1;24r").unwrap();
201        // Not from the manual: xterm's private XTQMODKEYS, here to prove a
202        // private prefix survives the encoder.
203        write_decrpss(&mut buf, true, ">4;2m").unwrap();
204        // An invalid request gets no data string, whatever it is handed.
205        write_decrpss(&mut buf, false, "0;1m").unwrap();
206        assert_eq!(
207            buf,
208            b"\x1bP1$r0;4;5;7m\x1b\\\x1bP1$r1;24r\x1b\\\x1bP1$r>4;2m\x1b\\\x1bP0$r\x1b\\"
209        );
210    }
211
212    #[test]
213    fn test_dsr_request() {
214        let mut buf = Vec::new();
215        write_dsr_request(&mut buf, false, 5).unwrap();
216        write_dsr_request(&mut buf, true, 996).unwrap();
217        assert_eq!(buf, b"\x1b[5n\x1b[?996n");
218    }
219
220    #[test]
221    fn test_visibility_report() {
222        let mut buf = Vec::new();
223        write_request_visibility_report(&mut buf).unwrap();
224        assert_eq!(buf, b"\x1b[?998n");
225        // The query is the generic DEC-private DSR 998.
226        let mut generic = Vec::new();
227        write_dsr_request(&mut generic, true, 998).unwrap();
228        assert_eq!(buf, generic);
229
230        let mut buf = Vec::new();
231        write_visibility_report(&mut buf, true).unwrap();
232        write_visibility_report(&mut buf, false).unwrap();
233        assert_eq!(buf, b"\x1b[?999;1n\x1b[?999;2n");
234    }
235}