| 214 |
214 |
|
pub resized: bool,
|
| 215 |
215 |
|
}
|
| 216 |
216 |
|
|
|
217 |
+ |
/// What the terminal answers about itself.
|
|
218 |
+ |
///
|
|
219 |
+ |
/// Every field here is something the grid cannot work out and a program can
|
|
220 |
+ |
/// ask for: the renderer owns cell geometry, the theme owns the default
|
|
221 |
+ |
/// colours, and the binary owns its own name. The grid holds them only so the
|
|
222 |
+ |
/// query arms have something true to say, and the binary keeps them current
|
|
223 |
+ |
/// through [`Grid::set_identity`].
|
|
224 |
+ |
///
|
|
225 |
+ |
/// The defaults are shop's own, so a grid nobody configured still answers
|
|
226 |
+ |
/// plausibly rather than answering zero.
|
|
227 |
+ |
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
228 |
+ |
pub struct Identity {
|
|
229 |
+ |
/// Reported by XTVERSION, as `name(version)`.
|
|
230 |
+ |
pub name: String,
|
|
231 |
+ |
pub version: String,
|
|
232 |
+ |
/// One cell in physical pixels, width then height. Follows the output
|
|
233 |
+ |
/// scale, so it changes when the window moves between displays.
|
|
234 |
+ |
pub cell_px: (u16, u16),
|
|
235 |
+ |
/// Default foreground and background, sRGB, as OSC 10 and 11 report them.
|
|
236 |
+ |
pub fg: [u8; 3],
|
|
237 |
+ |
pub bg: [u8; 3],
|
|
238 |
+ |
}
|
|
239 |
+ |
|
|
240 |
+ |
impl Default for Identity {
|
|
241 |
+ |
fn default() -> Self {
|
|
242 |
+ |
Self {
|
|
243 |
+ |
name: "shop".into(),
|
|
244 |
+ |
version: "0".into(),
|
|
245 |
+ |
cell_px: (8, 17),
|
|
246 |
+ |
fg: [0xe6, 0xde, 0xd3],
|
|
247 |
+ |
bg: [0x25, 0x23, 0x1f],
|
|
248 |
+ |
}
|
|
249 |
+ |
}
|
|
250 |
+ |
}
|
|
251 |
+ |
|
|
252 |
+ |
/// One channel as OSC 10/11 want it: four hex digits, the 8-bit value
|
|
253 |
+ |
/// doubled. `0x25` becomes `2525`, which is the 16-bit reading of the same
|
|
254 |
+ |
/// intensity and what every terminal sends.
|
|
255 |
+ |
fn osc_channel(v: u8) -> String {
|
|
256 |
+ |
format!("{v:02x}{v:02x}")
|
|
257 |
+ |
}
|
|
258 |
+ |
|
| 217 |
259 |
|
/// Cursor state (position + deferred-wrap flag).
|
| 218 |
260 |
|
#[derive(Copy, Clone, Debug, Default)]
|
| 219 |
261 |
|
pub struct Cursor {
|
| 287 |
329 |
|
cursor_keys_application: bool,
|
| 288 |
330 |
|
keypad_application: bool,
|
| 289 |
331 |
|
pending_title: Option<String>,
|
|
332 |
+ |
identity: Identity,
|
| 290 |
333 |
|
// Bytes the terminal owes the program, from queries it answered. The grid
|
| 291 |
334 |
|
// has no handle on the PTY, so it queues and the binary drains after every
|
| 292 |
335 |
|
// parse, the way it already does for the title.
|
| 340 |
383 |
|
cursor_keys_application: false,
|
| 341 |
384 |
|
keypad_application: false,
|
| 342 |
385 |
|
pending_title: None,
|
|
386 |
+ |
identity: Identity::default(),
|
| 343 |
387 |
|
pending_replies: Vec::new(),
|
| 344 |
388 |
|
// Initial state: everything dirty so first render populates the
|
| 345 |
389 |
|
// per-row cache.
|
| 437 |
481 |
|
self.pending_replies.extend_from_slice(bytes);
|
| 438 |
482 |
|
}
|
| 439 |
483 |
|
|
|
484 |
+ |
/// Tell the grid what to say about itself. Call at startup and whenever
|
|
485 |
+ |
/// any of it moves: cell size follows the output scale, and the colours
|
|
486 |
+ |
/// follow the theme.
|
|
487 |
+ |
pub fn set_identity(&mut self, identity: Identity) {
|
|
488 |
+ |
self.identity = identity;
|
|
489 |
+ |
}
|
|
490 |
+ |
|
| 440 |
491 |
|
pub fn cols(&self) -> u16 {
|
| 441 |
492 |
|
self.cols
|
| 442 |
493 |
|
}
|
| 1236 |
1287 |
|
// sixel, and claiming it means a client picks sixel over kitty
|
| 1237 |
1288 |
|
// graphics and draws nothing.
|
| 1238 |
1289 |
|
('c', false) if intermediates.is_empty() => self.reply(b"\x1b[?62;22c"),
|
|
1290 |
+ |
// XTVERSION. `DCS > | name(version) ST`, the form kitty and foot
|
|
1291 |
+ |
// both answer in, which is what makes it parseable by the clients
|
|
1292 |
+ |
// that ask.
|
|
1293 |
+ |
('q', false) if intermediates.first().copied() == Some(b'>') => {
|
|
1294 |
+ |
let reply = format!(
|
|
1295 |
+ |
"\x1bP>|{}({})\x1b\\",
|
|
1296 |
+ |
self.identity.name, self.identity.version
|
|
1297 |
+ |
);
|
|
1298 |
+ |
self.reply(reply.as_bytes());
|
|
1299 |
+ |
}
|
|
1300 |
+ |
// XTWINOPS reports. Only the three read-only ones: the rest of
|
|
1301 |
+ |
// this sequence moves and resizes windows, which is the
|
|
1302 |
+ |
// compositor's business and not something a program on a PTY gets
|
|
1303 |
+ |
// to do here.
|
|
1304 |
+ |
//
|
|
1305 |
+ |
// Sizes are physical pixels. Programs that place images need cell
|
|
1306 |
+ |
// size in particular, and the ioctl that also carries it
|
|
1307 |
+ |
// (TIOCSWINSZ) is not what all of them read.
|
|
1308 |
+ |
('t', false) if intermediates.is_empty() => {
|
|
1309 |
+ |
let (cw, ch) = self.identity.cell_px;
|
|
1310 |
+ |
match param1(params, 0) {
|
|
1311 |
+ |
// Text area, in pixels.
|
|
1312 |
+ |
14 => {
|
|
1313 |
+ |
let (w, h) = (self.cols * cw, self.rows * ch);
|
|
1314 |
+ |
self.reply(format!("\x1b[4;{h};{w}t").as_bytes());
|
|
1315 |
+ |
}
|
|
1316 |
+ |
// One cell, in pixels. Height first, as the report orders it.
|
|
1317 |
+ |
16 => self.reply(format!("\x1b[6;{ch};{cw}t").as_bytes()),
|
|
1318 |
+ |
// Text area, in cells.
|
|
1319 |
+ |
18 => {
|
|
1320 |
+ |
let (rows, cols) = (self.rows, self.cols);
|
|
1321 |
+ |
self.reply(format!("\x1b[8;{rows};{cols}t").as_bytes());
|
|
1322 |
+ |
}
|
|
1323 |
+ |
other => trace!("unhandled XTWINOPS {other}"),
|
|
1324 |
+ |
}
|
|
1325 |
+ |
}
|
| 1239 |
1326 |
|
('S', false) => {
|
| 1240 |
1327 |
|
self.scroll_up_in_region(param1(params, 1));
|
| 1241 |
1328 |
|
}
|
| 1314 |
1401 |
|
}
|
| 1315 |
1402 |
|
}
|
| 1316 |
1403 |
|
|
| 1317 |
|
- |
fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
|
|
1404 |
+ |
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
|
| 1318 |
1405 |
|
let Some(id) = params.first().and_then(|p| std::str::from_utf8(p).ok()) else {
|
| 1319 |
1406 |
|
return;
|
| 1320 |
1407 |
|
};
|
| 1328 |
1415 |
|
self.pending_title = Some(s.to_string());
|
| 1329 |
1416 |
|
}
|
| 1330 |
1417 |
|
}
|
|
1418 |
+ |
// OSC 10 and 11, default foreground and background. Only the `?`
|
|
1419 |
+ |
// query form: setting them is a separate feature, and answering a
|
|
1420 |
+ |
// set request would be worse than ignoring it.
|
|
1421 |
+ |
//
|
|
1422 |
+ |
// Programs ask in order to tell light from dark, so this decides
|
|
1423 |
+ |
// whether anything that adapts to the terminal's polarity adapts
|
|
1424 |
+ |
// the right way. shop's theme knows the answer; nothing else does.
|
|
1425 |
+ |
"10" | "11" if params.get(1) == Some(&b"?".as_slice()) => {
|
|
1426 |
+ |
let c = if id == "10" {
|
|
1427 |
+ |
self.identity.fg
|
|
1428 |
+ |
} else {
|
|
1429 |
+ |
self.identity.bg
|
|
1430 |
+ |
};
|
|
1431 |
+ |
let colour = format!(
|
|
1432 |
+ |
"rgb:{}/{}/{}",
|
|
1433 |
+ |
osc_channel(c[0]),
|
|
1434 |
+ |
osc_channel(c[1]),
|
|
1435 |
+ |
osc_channel(c[2])
|
|
1436 |
+ |
);
|
|
1437 |
+ |
// Terminated the way the question was. A client that asked
|
|
1438 |
+ |
// with BEL may well be parsing for one.
|
|
1439 |
+ |
let end: &str = if bell_terminated { "\x07" } else { "\x1b\\" };
|
|
1440 |
+ |
self.reply(format!("\x1b]{id};{colour}{end}").as_bytes());
|
|
1441 |
+ |
}
|
| 1331 |
1442 |
|
_ => {}
|
| 1332 |
1443 |
|
}
|
| 1333 |
1444 |
|
}
|
| 1747 |
1858 |
|
assert_cursor(&g, 0, 2);
|
| 1748 |
1859 |
|
}
|
| 1749 |
1860 |
|
|
|
1861 |
+ |
// ---- identity queries ----------------------------------------------
|
|
1862 |
+ |
|
|
1863 |
+ |
fn identified() -> Grid {
|
|
1864 |
+ |
let mut g = Grid::new(80, 24);
|
|
1865 |
+ |
g.set_identity(Identity {
|
|
1866 |
+ |
name: "shop".into(),
|
|
1867 |
+ |
version: "1.2.3".into(),
|
|
1868 |
+ |
cell_px: (9, 20),
|
|
1869 |
+ |
fg: [0xe6, 0xde, 0xd3],
|
|
1870 |
+ |
bg: [0x25, 0x23, 0x1f],
|
|
1871 |
+ |
});
|
|
1872 |
+ |
g
|
|
1873 |
+ |
}
|
|
1874 |
+ |
|
|
1875 |
+ |
fn reply_to(g: &mut Grid, bytes: &[u8]) -> String {
|
|
1876 |
+ |
feed(g, bytes);
|
|
1877 |
+ |
String::from_utf8(g.take_pending_replies()).unwrap()
|
|
1878 |
+ |
}
|
|
1879 |
+ |
|
|
1880 |
+ |
#[test]
|
|
1881 |
+ |
fn xtversion_names_the_terminal() {
|
|
1882 |
+ |
let mut g = identified();
|
|
1883 |
+ |
assert_eq!(reply_to(&mut g, b"\x1b[>q"), "\x1bP>|shop(1.2.3)\x1b\\");
|
|
1884 |
+ |
}
|
|
1885 |
+ |
|
|
1886 |
+ |
#[test]
|
|
1887 |
+ |
fn cell_size_is_reported_height_first() {
|
|
1888 |
+ |
// `CSI 6 ; height ; width t`. Getting the order wrong puts every
|
|
1889 |
+ |
// image preview at the wrong aspect, which is the sort of bug that
|
|
1890 |
+ |
// reads as a rendering problem rather than a reply problem.
|
|
1891 |
+ |
let mut g = identified();
|
|
1892 |
+ |
assert_eq!(reply_to(&mut g, b"\x1b[16t"), "\x1b[6;20;9t");
|
|
1893 |
+ |
}
|
|
1894 |
+ |
|
|
1895 |
+ |
#[test]
|
|
1896 |
+ |
fn text_area_is_reported_in_both_units() {
|
|
1897 |
+ |
let mut g = identified();
|
|
1898 |
+ |
// 80x24 cells of 9x20 px.
|
|
1899 |
+ |
assert_eq!(reply_to(&mut g, b"\x1b[14t"), "\x1b[4;480;720t");
|
|
1900 |
+ |
assert_eq!(reply_to(&mut g, b"\x1b[18t"), "\x1b[8;24;80t");
|
|
1901 |
+ |
}
|
|
1902 |
+ |
|
|
1903 |
+ |
#[test]
|
|
1904 |
+ |
fn cell_size_follows_the_identity() {
|
|
1905 |
+ |
let mut g = identified();
|
|
1906 |
+ |
g.set_identity(Identity {
|
|
1907 |
+ |
cell_px: (18, 40),
|
|
1908 |
+ |
..Identity::default()
|
|
1909 |
+ |
});
|
|
1910 |
+ |
assert_eq!(reply_to(&mut g, b"\x1b[16t"), "\x1b[6;40;18t");
|
|
1911 |
+ |
}
|
|
1912 |
+ |
|
|
1913 |
+ |
#[test]
|
|
1914 |
+ |
fn window_manipulation_is_not_obeyed() {
|
|
1915 |
+ |
// The same sequence resizes and moves windows. Those are the
|
|
1916 |
+ |
// compositor's, and a program on the PTY does not get to ask.
|
|
1917 |
+ |
let mut g = identified();
|
|
1918 |
+ |
assert_eq!(reply_to(&mut g, b"\x1b[3;0;0t"), "", "move window");
|
|
1919 |
+ |
assert_eq!(reply_to(&mut g, b"\x1b[8;50;100t"), "", "resize window");
|
|
1920 |
+ |
assert_eq!(g.cols(), 80);
|
|
1921 |
+ |
assert_eq!(g.rows(), 24);
|
|
1922 |
+ |
}
|
|
1923 |
+ |
|
|
1924 |
+ |
#[test]
|
|
1925 |
+ |
fn the_default_colours_are_answered_as_sixteen_bit() {
|
|
1926 |
+ |
let mut g = identified();
|
|
1927 |
+ |
assert_eq!(
|
|
1928 |
+ |
reply_to(&mut g, b"\x1b]11;?\x1b\\"),
|
|
1929 |
+ |
"\x1b]11;rgb:2525/2323/1f1f\x1b\\"
|
|
1930 |
+ |
);
|
|
1931 |
+ |
assert_eq!(
|
|
1932 |
+ |
reply_to(&mut g, b"\x1b]10;?\x1b\\"),
|
|
1933 |
+ |
"\x1b]10;rgb:e6e6/dede/d3d3\x1b\\"
|
|
1934 |
+ |
);
|
|
1935 |
+ |
}
|
|
1936 |
+ |
|
|
1937 |
+ |
#[test]
|
|
1938 |
+ |
fn a_colour_query_is_terminated_the_way_it_was_asked() {
|
|
1939 |
+ |
let mut g = identified();
|
|
1940 |
+ |
let bel = reply_to(&mut g, b"\x1b]11;?\x07");
|
|
1941 |
+ |
assert!(bel.ends_with('\x07'), "got {bel:?}");
|
|
1942 |
+ |
let st = reply_to(&mut g, b"\x1b]11;?\x1b\\");
|
|
1943 |
+ |
assert!(st.ends_with("\x1b\\"), "got {st:?}");
|
|
1944 |
+ |
}
|
|
1945 |
+ |
|
|
1946 |
+ |
#[test]
|
|
1947 |
+ |
fn setting_a_colour_is_not_mistaken_for_asking() {
|
|
1948 |
+ |
// OSC 11 with a value is a set request. shop does not implement it,
|
|
1949 |
+ |
// and answering it with the current colour would be a lie about
|
|
1950 |
+ |
// having done something.
|
|
1951 |
+ |
let mut g = identified();
|
|
1952 |
+ |
assert_eq!(reply_to(&mut g, b"\x1b]11;#ff0000\x1b\\"), "");
|
|
1953 |
+ |
}
|
|
1954 |
+ |
|
|
1955 |
+ |
#[test]
|
|
1956 |
+ |
fn identity_queries_leave_the_screen_alone() {
|
|
1957 |
+ |
let mut g = identified();
|
|
1958 |
+ |
feed(&mut g, b"hi");
|
|
1959 |
+ |
let _ = reply_to(&mut g, b"\x1b[>q\x1b[16t\x1b]11;?\x1b\\");
|
|
1960 |
+ |
assert_eq!(row_str(&g, 0), "hi");
|
|
1961 |
+ |
assert_cursor(&g, 0, 2);
|
|
1962 |
+ |
}
|
|
1963 |
+ |
|
| 1750 |
1964 |
|
#[test]
|
| 1751 |
1965 |
|
fn decckm_toggles_cursor_key_mode() {
|
| 1752 |
1966 |
|
let mut g = Grid::new(10, 3);
|