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