| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
use crate::{CursorShape, Grid, MouseEncoding, MouseTracking}; |
| 8 |
use shop_vt::{Params, Perform}; |
| 9 |
use tracing::trace; |
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
fn osc_channel(v: u8) -> String { |
| 15 |
format!("{v:02x}{v:02x}") |
| 16 |
} |
| 17 |
|
| 18 |
fn param1(params: &Params, default: u16) -> u16 { |
| 19 |
let first = params |
| 20 |
.iter() |
| 21 |
.next() |
| 22 |
.and_then(|p| p.first().copied()) |
| 23 |
.unwrap_or(0); |
| 24 |
if first == 0 { default } else { first } |
| 25 |
} |
| 26 |
|
| 27 |
fn param2(params: &Params, defaults: (u16, u16)) -> (u16, u16) { |
| 28 |
let mut it = params.iter(); |
| 29 |
let a = it.next().and_then(|p| p.first().copied()).unwrap_or(0); |
| 30 |
let b = it.next().and_then(|p| p.first().copied()).unwrap_or(0); |
| 31 |
let a = if a == 0 { defaults.0 } else { a }; |
| 32 |
let b = if b == 0 { defaults.1 } else { b }; |
| 33 |
(a, b) |
| 34 |
} |
| 35 |
|
| 36 |
impl Perform for Grid { |
| 37 |
fn print(&mut self, c: char) { |
| 38 |
self.place_char(c); |
| 39 |
} |
| 40 |
|
| 41 |
fn execute(&mut self, byte: u8) { |
| 42 |
|
| 43 |
|
| 44 |
|
| 45 |
self.invalidate_cur_row(); |
| 46 |
match byte { |
| 47 |
0x08 => { |
| 48 |
|
| 49 |
if self.cursor.col > 0 { |
| 50 |
self.cursor.col -= 1; |
| 51 |
} |
| 52 |
self.cursor.wrap_next = false; |
| 53 |
} |
| 54 |
0x09 => { |
| 55 |
|
| 56 |
let next = ((self.cursor.col / 8) + 1) * 8; |
| 57 |
self.cursor.col = next.min(self.cols - 1); |
| 58 |
self.cursor.wrap_next = false; |
| 59 |
} |
| 60 |
0x0A..=0x0C => { |
| 61 |
|
| 62 |
self.newline(); |
| 63 |
} |
| 64 |
0x0D => { |
| 65 |
|
| 66 |
self.cursor.col = 0; |
| 67 |
self.cursor.wrap_next = false; |
| 68 |
} |
| 69 |
0x07 => {} |
| 70 |
other => trace!("unhandled C0 {other:#x}"), |
| 71 |
} |
| 72 |
} |
| 73 |
|
| 74 |
fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) { |
| 75 |
|
| 76 |
|
| 77 |
|
| 78 |
self.invalidate_cur_row(); |
| 79 |
let private = intermediates.first().copied() == Some(b'?'); |
| 80 |
match (action, private) { |
| 81 |
('H' | 'f', false) => { |
| 82 |
let (row, col) = param2(params, (1, 1)); |
| 83 |
self.set_cursor(row, col); |
| 84 |
} |
| 85 |
('A', false) => { |
| 86 |
let n = param1(params, 1) as i32; |
| 87 |
self.move_by(-n, 0); |
| 88 |
} |
| 89 |
('B', false) => { |
| 90 |
let n = param1(params, 1) as i32; |
| 91 |
self.move_by(n, 0); |
| 92 |
} |
| 93 |
('C', false) => { |
| 94 |
let n = param1(params, 1) as i32; |
| 95 |
self.move_by(0, n); |
| 96 |
} |
| 97 |
('D', false) => { |
| 98 |
let n = param1(params, 1) as i32; |
| 99 |
self.move_by(0, -n); |
| 100 |
} |
| 101 |
('E', false) => { |
| 102 |
let n = param1(params, 1) as i32; |
| 103 |
self.move_by(n, 0); |
| 104 |
self.cursor.col = 0; |
| 105 |
} |
| 106 |
('F', false) => { |
| 107 |
let n = param1(params, 1) as i32; |
| 108 |
self.move_by(-n, 0); |
| 109 |
self.cursor.col = 0; |
| 110 |
} |
| 111 |
('G', false) => { |
| 112 |
let col = param1(params, 1); |
| 113 |
self.cursor.col = col.saturating_sub(1).min(self.cols - 1); |
| 114 |
self.cursor.wrap_next = false; |
| 115 |
} |
| 116 |
('d', false) => { |
| 117 |
let row = param1(params, 1); |
| 118 |
self.cursor.row = row.saturating_sub(1).min(self.rows - 1); |
| 119 |
self.cursor.wrap_next = false; |
| 120 |
} |
| 121 |
('J', false) => { |
| 122 |
self.erase_display(param1(params, 0)); |
| 123 |
} |
| 124 |
('K', false) => { |
| 125 |
self.erase_line(param1(params, 0)); |
| 126 |
} |
| 127 |
('L', false) => self.insert_lines(param1(params, 1)), |
| 128 |
('M', false) => self.delete_lines(param1(params, 1)), |
| 129 |
('@', false) => self.insert_chars(param1(params, 1)), |
| 130 |
('P', false) => self.delete_chars(param1(params, 1)), |
| 131 |
('X', false) => self.erase_chars(param1(params, 1)), |
| 132 |
|
| 133 |
|
| 134 |
|
| 135 |
|
| 136 |
('s', false) if intermediates.is_empty() => self.save_cursor(), |
| 137 |
('u', false) if intermediates.is_empty() => self.restore_cursor(), |
| 138 |
|
| 139 |
|
| 140 |
|
| 141 |
|
| 142 |
|
| 143 |
|
| 144 |
|
| 145 |
|
| 146 |
('c', false) if intermediates.is_empty() => self.reply(b"\x1b[?62;22c"), |
| 147 |
|
| 148 |
|
| 149 |
|
| 150 |
|
| 151 |
|
| 152 |
|
| 153 |
|
| 154 |
|
| 155 |
|
| 156 |
|
| 157 |
|
| 158 |
|
| 159 |
|
| 160 |
|
| 161 |
('n', false) if intermediates.is_empty() => match param1(params, 0) { |
| 162 |
5 => self.reply(b"\x1b[0n"), |
| 163 |
6 => { |
| 164 |
let (row, col) = (self.cursor.row + 1, self.cursor.col + 1); |
| 165 |
self.reply(format!("\x1b[{row};{col}R").as_bytes()); |
| 166 |
} |
| 167 |
other => trace!("unhandled DSR {other}"), |
| 168 |
}, |
| 169 |
|
| 170 |
|
| 171 |
|
| 172 |
('n', true) if param1(params, 0) == 6 => { |
| 173 |
let (row, col) = (self.cursor.row + 1, self.cursor.col + 1); |
| 174 |
self.reply(format!("\x1b[?{row};{col};1R").as_bytes()); |
| 175 |
} |
| 176 |
|
| 177 |
|
| 178 |
|
| 179 |
('q', false) if intermediates.first().copied() == Some(b'>') => { |
| 180 |
let reply = format!( |
| 181 |
"\x1bP>|{}({})\x1b\\", |
| 182 |
self.identity.name, self.identity.version |
| 183 |
); |
| 184 |
self.reply(reply.as_bytes()); |
| 185 |
} |
| 186 |
|
| 187 |
|
| 188 |
|
| 189 |
|
| 190 |
|
| 191 |
|
| 192 |
|
| 193 |
|
| 194 |
('t', false) if intermediates.is_empty() => { |
| 195 |
let (cw, ch) = self.identity.cell_px; |
| 196 |
match param1(params, 0) { |
| 197 |
|
| 198 |
14 => { |
| 199 |
let (w, h) = (self.cols * cw, self.rows * ch); |
| 200 |
self.reply(format!("\x1b[4;{h};{w}t").as_bytes()); |
| 201 |
} |
| 202 |
|
| 203 |
16 => self.reply(format!("\x1b[6;{ch};{cw}t").as_bytes()), |
| 204 |
|
| 205 |
18 => { |
| 206 |
let (rows, cols) = (self.rows, self.cols); |
| 207 |
self.reply(format!("\x1b[8;{rows};{cols}t").as_bytes()); |
| 208 |
} |
| 209 |
other => trace!("unhandled XTWINOPS {other}"), |
| 210 |
} |
| 211 |
} |
| 212 |
('S', false) => { |
| 213 |
self.scroll_up_in_region(param1(params, 1)); |
| 214 |
} |
| 215 |
('T', false) => { |
| 216 |
self.scroll_down_in_region(param1(params, 1)); |
| 217 |
} |
| 218 |
('r', false) => { |
| 219 |
|
| 220 |
|
| 221 |
|
| 222 |
|
| 223 |
|
| 224 |
|
| 225 |
|
| 226 |
let (top, bot) = param2(params, (1, self.rows)); |
| 227 |
let new_top = top.saturating_sub(1).min(self.rows - 1); |
| 228 |
let new_bottom = bot.saturating_sub(1).min(self.rows - 1); |
| 229 |
|
| 230 |
|
| 231 |
|
| 232 |
|
| 233 |
|
| 234 |
|
| 235 |
|
| 236 |
|
| 237 |
|
| 238 |
if new_top >= new_bottom { |
| 239 |
return; |
| 240 |
} |
| 241 |
self.unroll_region(); |
| 242 |
self.scroll_top = new_top; |
| 243 |
self.scroll_bottom = new_bottom; |
| 244 |
if self.is_partial_region() { |
| 245 |
self.unroll_active_ring(); |
| 246 |
} |
| 247 |
self.cursor.row = 0; |
| 248 |
self.cursor.col = 0; |
| 249 |
} |
| 250 |
('m', false) => self.apply_sgr(params), |
| 251 |
('q', false) if intermediates.first().copied() == Some(b' ') => { |
| 252 |
let shape = param1(params, 1); |
| 253 |
self.cursor_shape = match shape { |
| 254 |
0..=2 => CursorShape::Block, |
| 255 |
3 | 4 => CursorShape::Underline, |
| 256 |
5 | 6 => CursorShape::Bar, |
| 257 |
_ => self.cursor_shape, |
| 258 |
}; |
| 259 |
} |
| 260 |
('h' | 'l', true) => { |
| 261 |
for p in params.iter() { |
| 262 |
if let Some(&code) = p.first() { |
| 263 |
match code { |
| 264 |
25 => self.cursor.visible = action == 'h', |
| 265 |
1049 | 47 | 1047 => self.swap_alt(action == 'h'), |
| 266 |
|
| 267 |
|
| 268 |
|
| 269 |
|
| 270 |
|
| 271 |
1 => self.cursor_keys_application = action == 'h', |
| 272 |
1007 => self.alternate_scroll = action == 'h', |
| 273 |
2004 => self.bracketed_paste = action == 'h', |
| 274 |
2026 => self.sync_update = action == 'h', |
| 275 |
|
| 276 |
|
| 277 |
|
| 278 |
|
| 279 |
|
| 280 |
9 | 1000 | 1002 | 1003 => { |
| 281 |
let level = match code { |
| 282 |
9 => MouseTracking::Press, |
| 283 |
1000 => MouseTracking::Click, |
| 284 |
1002 => MouseTracking::Drag, |
| 285 |
_ => MouseTracking::Motion, |
| 286 |
}; |
| 287 |
if action == 'h' { |
| 288 |
self.mouse_tracking = level; |
| 289 |
} else if self.mouse_tracking == level { |
| 290 |
self.mouse_tracking = MouseTracking::Off; |
| 291 |
} |
| 292 |
} |
| 293 |
1006 => { |
| 294 |
self.mouse_encoding = if action == 'h' { |
| 295 |
MouseEncoding::Sgr |
| 296 |
} else { |
| 297 |
MouseEncoding::X10 |
| 298 |
} |
| 299 |
} |
| 300 |
|
| 301 |
|
| 302 |
|
| 303 |
|
| 304 |
|
| 305 |
|
| 306 |
|
| 307 |
|
| 308 |
1005 | 1015 => trace!("declined mouse encoding {code}"), |
| 309 |
_ => {} |
| 310 |
} |
| 311 |
} |
| 312 |
} |
| 313 |
} |
| 314 |
_ => trace!("unhandled CSI {action} private={private}"), |
| 315 |
} |
| 316 |
} |
| 317 |
|
| 318 |
fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, byte: u8) { |
| 319 |
self.invalidate_cur_row(); |
| 320 |
match byte { |
| 321 |
b'7' => self.save_cursor(), |
| 322 |
b'8' => self.restore_cursor(), |
| 323 |
|
| 324 |
|
| 325 |
b'=' => self.keypad_application = true, |
| 326 |
b'>' => self.keypad_application = false, |
| 327 |
b'M' => { |
| 328 |
|
| 329 |
if self.cursor.row == self.scroll_top { |
| 330 |
self.scroll_down_in_region(1); |
| 331 |
} else { |
| 332 |
self.cursor.row = self.cursor.row.saturating_sub(1); |
| 333 |
} |
| 334 |
} |
| 335 |
_ => trace!("unhandled ESC {}", byte as char), |
| 336 |
} |
| 337 |
} |
| 338 |
|
| 339 |
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { |
| 340 |
let Some(id) = params.first().and_then(|p| std::str::from_utf8(p).ok()) else { |
| 341 |
return; |
| 342 |
}; |
| 343 |
match id { |
| 344 |
|
| 345 |
|
| 346 |
"0" | "2" => { |
| 347 |
if let Some(payload) = params.get(1) |
| 348 |
&& let Ok(s) = std::str::from_utf8(payload) |
| 349 |
{ |
| 350 |
self.pending_title = Some(s.to_string()); |
| 351 |
} |
| 352 |
} |
| 353 |
|
| 354 |
|
| 355 |
|
| 356 |
|
| 357 |
|
| 358 |
|
| 359 |
|
| 360 |
"10" | "11" if params.get(1) == Some(&b"?".as_slice()) => { |
| 361 |
let c = if id == "10" { |
| 362 |
self.identity.fg |
| 363 |
} else { |
| 364 |
self.identity.bg |
| 365 |
}; |
| 366 |
let colour = format!( |
| 367 |
"rgb:{}/{}/{}", |
| 368 |
osc_channel(c[0]), |
| 369 |
osc_channel(c[1]), |
| 370 |
osc_channel(c[2]) |
| 371 |
); |
| 372 |
|
| 373 |
|
| 374 |
let end: &str = if bell_terminated { "\x07" } else { "\x1b\\" }; |
| 375 |
self.reply(format!("\x1b]{id};{colour}{end}").as_bytes()); |
| 376 |
} |
| 377 |
_ => {} |
| 378 |
} |
| 379 |
} |
| 380 |
} |
| 381 |
|
| 382 |
#[cfg(test)] |
| 383 |
mod tests { |
| 384 |
use crate::testutil::{assert_cursor, feed, identified, reply_to, row_str}; |
| 385 |
use crate::*; |
| 386 |
|
| 387 |
|
| 388 |
|
| 389 |
#[test] |
| 390 |
fn decscusr_sets_shape() { |
| 391 |
let mut g = Grid::new(10, 2); |
| 392 |
assert_eq!(g.cursor_shape(), CursorShape::Block); |
| 393 |
feed(&mut g, b"\x1b[3 q"); |
| 394 |
assert_eq!(g.cursor_shape(), CursorShape::Underline); |
| 395 |
feed(&mut g, b"\x1b[6 q"); |
| 396 |
assert_eq!(g.cursor_shape(), CursorShape::Bar); |
| 397 |
feed(&mut g, b"\x1b[1 q"); |
| 398 |
assert_eq!(g.cursor_shape(), CursorShape::Block); |
| 399 |
} |
| 400 |
|
| 401 |
|
| 402 |
|
| 403 |
#[test] |
| 404 |
fn sync_update_toggles_on_2026() { |
| 405 |
let mut g = Grid::new(10, 1); |
| 406 |
assert!(!g.sync_update()); |
| 407 |
feed(&mut g, b"\x1b[?2026h"); |
| 408 |
assert!(g.sync_update()); |
| 409 |
feed(&mut g, b"\x1b[?2026l"); |
| 410 |
assert!(!g.sync_update()); |
| 411 |
} |
| 412 |
|
| 413 |
|
| 414 |
|
| 415 |
#[test] |
| 416 |
fn alt_screen_swaps_and_restores() { |
| 417 |
let mut g = Grid::new(6, 2); |
| 418 |
feed(&mut g, b"MAIN"); |
| 419 |
assert_eq!(row_str(&g, 0), "MAIN"); |
| 420 |
feed(&mut g, b"\x1b[?1049h"); |
| 421 |
|
| 422 |
assert_eq!(row_str(&g, 0), ""); |
| 423 |
feed(&mut g, b"ALT"); |
| 424 |
assert_eq!(row_str(&g, 0), "ALT"); |
| 425 |
feed(&mut g, b"\x1b[?1049l"); |
| 426 |
assert_eq!(row_str(&g, 0), "MAIN"); |
| 427 |
} |
| 428 |
|
| 429 |
|
| 430 |
|
| 431 |
#[test] |
| 432 |
fn osc_title_sets_pending() { |
| 433 |
let mut g = Grid::new(10, 2); |
| 434 |
feed(&mut g, b"\x1b]0;hello\x07"); |
| 435 |
assert_eq!(g.take_pending_title().as_deref(), Some("hello")); |
| 436 |
|
| 437 |
assert!(g.take_pending_title().is_none()); |
| 438 |
} |
| 439 |
|
| 440 |
#[test] |
| 441 |
fn osc_two_also_sets_title() { |
| 442 |
let mut g = Grid::new(10, 2); |
| 443 |
feed(&mut g, b"\x1b]2;from OSC 2\x07"); |
| 444 |
assert_eq!(g.take_pending_title().as_deref(), Some("from OSC 2")); |
| 445 |
} |
| 446 |
|
| 447 |
|
| 448 |
|
| 449 |
#[test] |
| 450 |
fn da1_is_answered() { |
| 451 |
let mut g = Grid::new(10, 3); |
| 452 |
assert!(g.take_pending_replies().is_empty(), "nothing owed yet"); |
| 453 |
feed(&mut g, b"\x1b[c"); |
| 454 |
assert_eq!(g.take_pending_replies(), b"\x1b[?62;22c".to_vec()); |
| 455 |
} |
| 456 |
|
| 457 |
#[test] |
| 458 |
fn da1_does_not_claim_sixel() { |
| 459 |
|
| 460 |
|
| 461 |
let mut g = Grid::new(10, 3); |
| 462 |
feed(&mut g, b"\x1b[c"); |
| 463 |
let reply = String::from_utf8(g.take_pending_replies()).unwrap(); |
| 464 |
let attrs: Vec<&str> = reply |
| 465 |
.trim_start_matches("\x1b[?") |
| 466 |
.trim_end_matches('c') |
| 467 |
.split(';') |
| 468 |
.collect(); |
| 469 |
assert!(!attrs.contains(&"4"), "claimed sixel in {reply:?}"); |
| 470 |
} |
| 471 |
|
| 472 |
#[test] |
| 473 |
fn da1_with_an_explicit_zero_is_the_same_question() { |
| 474 |
let mut g = Grid::new(10, 3); |
| 475 |
feed(&mut g, b"\x1b[0c"); |
| 476 |
assert_eq!(g.take_pending_replies(), b"\x1b[?62;22c".to_vec()); |
| 477 |
} |
| 478 |
|
| 479 |
#[test] |
| 480 |
fn da2_is_not_answered_with_da1() { |
| 481 |
|
| 482 |
|
| 483 |
|
| 484 |
let mut g = Grid::new(10, 3); |
| 485 |
feed(&mut g, b"\x1b[>c"); |
| 486 |
assert!(g.take_pending_replies().is_empty()); |
| 487 |
} |
| 488 |
|
| 489 |
#[test] |
| 490 |
fn replies_are_drained_not_repeated() { |
| 491 |
let mut g = Grid::new(10, 3); |
| 492 |
feed(&mut g, b"\x1b[c"); |
| 493 |
assert!(!g.take_pending_replies().is_empty()); |
| 494 |
assert!(g.take_pending_replies().is_empty(), "drained once only"); |
| 495 |
} |
| 496 |
|
| 497 |
#[test] |
| 498 |
fn two_queries_in_one_parse_both_get_answers() { |
| 499 |
let mut g = Grid::new(10, 3); |
| 500 |
feed(&mut g, b"\x1b[c\x1b[c"); |
| 501 |
assert_eq!( |
| 502 |
g.take_pending_replies(), |
| 503 |
b"\x1b[?62;22c\x1b[?62;22c".to_vec() |
| 504 |
); |
| 505 |
} |
| 506 |
|
| 507 |
#[test] |
| 508 |
fn a_query_does_not_disturb_the_screen() { |
| 509 |
let mut g = Grid::new(10, 3); |
| 510 |
feed(&mut g, b"hi\x1b[c"); |
| 511 |
let _ = g.take_pending_replies(); |
| 512 |
assert_eq!(row_str(&g, 0), "hi"); |
| 513 |
assert_cursor(&g, 0, 2); |
| 514 |
} |
| 515 |
|
| 516 |
|
| 517 |
|
| 518 |
#[test] |
| 519 |
fn cpr_answers_where_the_cursor_is() { |
| 520 |
|
| 521 |
let mut g = Grid::new(20, 5); |
| 522 |
feed(&mut g, b"\x1b[3;7H"); |
| 523 |
assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[3;7R"); |
| 524 |
} |
| 525 |
|
| 526 |
#[test] |
| 527 |
fn cpr_from_the_home_position_is_one_one() { |
| 528 |
|
| 529 |
|
| 530 |
let mut g = Grid::new(20, 5); |
| 531 |
assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[1;1R"); |
| 532 |
} |
| 533 |
|
| 534 |
#[test] |
| 535 |
fn cpr_reports_the_absolute_row_inside_a_scrolling_region() { |
| 536 |
|
| 537 |
let mut g = Grid::new(20, 10); |
| 538 |
feed(&mut g, b"\x1b[3;8r\x1b[5;2H"); |
| 539 |
assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[5;2R"); |
| 540 |
} |
| 541 |
|
| 542 |
#[test] |
| 543 |
fn dsr_five_reports_good_health() { |
| 544 |
let mut g = Grid::new(20, 5); |
| 545 |
assert_eq!(reply_to(&mut g, b"\x1b[5n"), "\x1b[0n"); |
| 546 |
} |
| 547 |
|
| 548 |
#[test] |
| 549 |
fn decxcpr_keeps_the_private_marker_and_names_the_page() { |
| 550 |
let mut g = Grid::new(20, 5); |
| 551 |
feed(&mut g, b"\x1b[2;4H"); |
| 552 |
assert_eq!(reply_to(&mut g, b"\x1b[?6n"), "\x1b[?2;4;1R"); |
| 553 |
} |
| 554 |
|
| 555 |
#[test] |
| 556 |
fn an_unknown_dsr_is_answered_with_silence() { |
| 557 |
|
| 558 |
|
| 559 |
let mut g = Grid::new(20, 5); |
| 560 |
assert_eq!(reply_to(&mut g, b"\x1b[99n"), ""); |
| 561 |
} |
| 562 |
|
| 563 |
#[test] |
| 564 |
fn xtversion_names_the_terminal() { |
| 565 |
let mut g = identified(); |
| 566 |
assert_eq!(reply_to(&mut g, b"\x1b[>q"), "\x1bP>|shop(1.2.3)\x1b\\"); |
| 567 |
} |
| 568 |
|
| 569 |
#[test] |
| 570 |
fn cell_size_is_reported_height_first() { |
| 571 |
|
| 572 |
|
| 573 |
|
| 574 |
let mut g = identified(); |
| 575 |
assert_eq!(reply_to(&mut g, b"\x1b[16t"), "\x1b[6;20;9t"); |
| 576 |
} |
| 577 |
|
| 578 |
#[test] |
| 579 |
fn text_area_is_reported_in_both_units() { |
| 580 |
let mut g = identified(); |
| 581 |
|
| 582 |
assert_eq!(reply_to(&mut g, b"\x1b[14t"), "\x1b[4;480;720t"); |
| 583 |
assert_eq!(reply_to(&mut g, b"\x1b[18t"), "\x1b[8;24;80t"); |
| 584 |
} |
| 585 |
|
| 586 |
#[test] |
| 587 |
fn cell_size_follows_the_identity() { |
| 588 |
let mut g = identified(); |
| 589 |
g.set_identity(Identity { |
| 590 |
cell_px: (18, 40), |
| 591 |
..Identity::default() |
| 592 |
}); |
| 593 |
assert_eq!(reply_to(&mut g, b"\x1b[16t"), "\x1b[6;40;18t"); |
| 594 |
} |
| 595 |
|
| 596 |
#[test] |
| 597 |
fn window_manipulation_is_not_obeyed() { |
| 598 |
|
| 599 |
|
| 600 |
let mut g = identified(); |
| 601 |
assert_eq!(reply_to(&mut g, b"\x1b[3;0;0t"), "", "move window"); |
| 602 |
assert_eq!(reply_to(&mut g, b"\x1b[8;50;100t"), "", "resize window"); |
| 603 |
assert_eq!(g.cols(), 80); |
| 604 |
assert_eq!(g.rows(), 24); |
| 605 |
} |
| 606 |
|
| 607 |
#[test] |
| 608 |
fn the_default_colours_are_answered_as_sixteen_bit() { |
| 609 |
let mut g = identified(); |
| 610 |
assert_eq!( |
| 611 |
reply_to(&mut g, b"\x1b]11;?\x1b\\"), |
| 612 |
"\x1b]11;rgb:2525/2323/1f1f\x1b\\" |
| 613 |
); |
| 614 |
assert_eq!( |
| 615 |
reply_to(&mut g, b"\x1b]10;?\x1b\\"), |
| 616 |
"\x1b]10;rgb:e6e6/dede/d3d3\x1b\\" |
| 617 |
); |
| 618 |
} |
| 619 |
|
| 620 |
#[test] |
| 621 |
fn a_colour_query_is_terminated_the_way_it_was_asked() { |
| 622 |
let mut g = identified(); |
| 623 |
let bel = reply_to(&mut g, b"\x1b]11;?\x07"); |
| 624 |
assert!(bel.ends_with('\x07'), "got {bel:?}"); |
| 625 |
let st = reply_to(&mut g, b"\x1b]11;?\x1b\\"); |
| 626 |
assert!(st.ends_with("\x1b\\"), "got {st:?}"); |
| 627 |
} |
| 628 |
|
| 629 |
#[test] |
| 630 |
fn setting_a_colour_is_not_mistaken_for_asking() { |
| 631 |
|
| 632 |
|
| 633 |
|
| 634 |
let mut g = identified(); |
| 635 |
assert_eq!(reply_to(&mut g, b"\x1b]11;#ff0000\x1b\\"), ""); |
| 636 |
} |
| 637 |
|
| 638 |
#[test] |
| 639 |
fn identity_queries_leave_the_screen_alone() { |
| 640 |
let mut g = identified(); |
| 641 |
feed(&mut g, b"hi"); |
| 642 |
let _ = reply_to(&mut g, b"\x1b[>q\x1b[16t\x1b]11;?\x1b\\"); |
| 643 |
assert_eq!(row_str(&g, 0), "hi"); |
| 644 |
assert_cursor(&g, 0, 2); |
| 645 |
} |
| 646 |
|
| 647 |
#[test] |
| 648 |
fn decckm_toggles_cursor_key_mode() { |
| 649 |
let mut g = Grid::new(10, 3); |
| 650 |
assert!(!g.cursor_keys_application()); |
| 651 |
feed(&mut g, b"\x1b[?1h"); |
| 652 |
assert!(g.cursor_keys_application()); |
| 653 |
feed(&mut g, b"\x1b[?1l"); |
| 654 |
assert!(!g.cursor_keys_application()); |
| 655 |
} |
| 656 |
|
| 657 |
#[test] |
| 658 |
fn deckpam_and_deckpnm_toggle_the_keypad() { |
| 659 |
|
| 660 |
let mut g = Grid::new(10, 3); |
| 661 |
assert!(!g.keypad_application()); |
| 662 |
feed(&mut g, b"\x1b="); |
| 663 |
assert!(g.keypad_application()); |
| 664 |
feed(&mut g, b"\x1b>"); |
| 665 |
assert!(!g.keypad_application()); |
| 666 |
} |
| 667 |
|
| 668 |
#[test] |
| 669 |
fn bracketed_paste_toggles_on_2004() { |
| 670 |
let mut g = Grid::new(10, 3); |
| 671 |
assert!(!g.bracketed_paste(), "off until a program asks"); |
| 672 |
feed(&mut g, b"\x1b[?2004h"); |
| 673 |
assert!(g.bracketed_paste()); |
| 674 |
feed(&mut g, b"\x1b[?2004l"); |
| 675 |
assert!(!g.bracketed_paste()); |
| 676 |
} |
| 677 |
|
| 678 |
#[test] |
| 679 |
fn bracketed_paste_survives_an_alt_screen_round_trip() { |
| 680 |
|
| 681 |
|
| 682 |
let mut g = Grid::new(10, 3); |
| 683 |
feed(&mut g, b"\x1b[?2004h"); |
| 684 |
feed(&mut g, b"\x1b[?1049h"); |
| 685 |
feed(&mut g, b"\x1b[?1049l"); |
| 686 |
assert!(g.bracketed_paste()); |
| 687 |
} |
| 688 |
|
| 689 |
#[test] |
| 690 |
fn alternate_scroll_toggles_on_1007() { |
| 691 |
let mut g = Grid::new(10, 3); |
| 692 |
assert!(g.alternate_scroll(), "on until a program says otherwise"); |
| 693 |
feed(&mut g, b"\x1b[?1007l"); |
| 694 |
assert!(!g.alternate_scroll()); |
| 695 |
feed(&mut g, b"\x1b[?1007h"); |
| 696 |
assert!(g.alternate_scroll()); |
| 697 |
} |
| 698 |
|
| 699 |
#[test] |
| 700 |
fn alternate_scroll_survives_an_alt_screen_round_trip() { |
| 701 |
|
| 702 |
|
| 703 |
let mut g = Grid::new(10, 3); |
| 704 |
feed(&mut g, b"\x1b[?1007l"); |
| 705 |
feed(&mut g, b"\x1b[?1049h"); |
| 706 |
feed(&mut g, b"\x1b[?1049l"); |
| 707 |
assert!(!g.alternate_scroll()); |
| 708 |
} |
| 709 |
|
| 710 |
#[test] |
| 711 |
fn on_alt_follows_1049() { |
| 712 |
let mut g = Grid::new(10, 3); |
| 713 |
assert!(!g.on_alt()); |
| 714 |
feed(&mut g, b"\x1b[?1049h"); |
| 715 |
assert!(g.on_alt()); |
| 716 |
feed(&mut g, b"\x1b[?1049l"); |
| 717 |
assert!(!g.on_alt()); |
| 718 |
} |
| 719 |
} |
| 720 |
|