use super::*; use crossterm::event::{KeyEvent, MouseEvent, MouseEventKind}; fn key(code: KeyCode, modifiers: KeyModifiers) -> Event { Event::Key(KeyEvent::new(code, modifiers)) } fn bytes(collector: &mut TerminalColors, text: &str, now: Instant) { for ch in text.chars() { let modifiers = if ch.is_ascii_uppercase() { KeyModifiers::SHIFT } else { KeyModifiers::NONE }; collector.push(key(KeyCode::Char(ch), modifiers), now); } } fn report(collector: &mut TerminalColors, body: &str, now: Instant) { collector.push(key(KeyCode::Char(']'), KeyModifiers::ALT), now); bytes(collector, body, now); collector.push(key(KeyCode::Char('g'), KeyModifiers::CONTROL), now); } fn drain(collector: &mut TerminalColors) -> Vec { std::iter::from_fn(|| collector.pop()).collect() } #[test] fn fragmented_triple_accepts_bel_and_both_st_encodings() { let now = Instant::now(); let mut collector = TerminalColors::default(); collector.query(&mut Vec::new(), now).unwrap(); collector.push(key(KeyCode::Esc, KeyModifiers::NONE), now); collector.push( key(KeyCode::Char(']'), KeyModifiers::NONE), now + Duration::from_millis(1), ); bytes( &mut collector, "11;rgb:0/12/345", now + Duration::from_millis(2), ); collector.push( key(KeyCode::Esc, KeyModifiers::NONE), now + Duration::from_millis(3), ); collector.push( key(KeyCode::Char('\\'), KeyModifiers::NONE), now + Duration::from_millis(4), ); assert_eq!(collector.colors, None); report( &mut collector, "4;2;rgb:f/ffff/ABCD", now + Duration::from_millis(5), ); collector.push( key(KeyCode::Char(']'), KeyModifiers::ALT), now + Duration::from_millis(6), ); bytes( &mut collector, "4;1;#FF1234", now + Duration::from_millis(7), ); collector.push( key(KeyCode::Char('\\'), KeyModifiers::ALT), now + Duration::from_millis(8), ); assert_eq!( collector.colors, Some(ThemeColors { background: [0, 18, 52], red: [255, 18, 52], green: [255, 255, 171], }) ); assert!(drain(&mut collector).is_empty()); } #[test] fn focus_refresh_atomically_replaces_light_and_dark_triples() { let now = Instant::now(); let mut collector = TerminalColors::default(); let palettes = [ ("11;#FDF6E3", [253, 246, 227]), ("11;rgb:0000/0000/0000", [0, 0, 0]), ]; for (cycle, (background, expected)) in palettes.into_iter().enumerate() { let time = now + Duration::from_secs(cycle as u64); let focus = Event::FocusGained; collector.push(focus.clone(), time); assert_eq!(collector.pop(), Some(focus)); let previous = collector.colors; collector.query(&mut Vec::new(), time).unwrap(); report(&mut collector, "4;1;rgb:ff/00/00", time); report(&mut collector, background, time); assert_eq!(collector.colors, previous); report(&mut collector, "4;2;rgb:00/ff/00", time); assert_eq!( collector.colors, Some(ThemeColors { background: expected, red: [255, 0, 0], green: [0, 255, 0], }) ); assert!(drain(&mut collector).is_empty()); } } #[test] fn periodic_refresh_detects_silent_change_without_overlapping_queries() { let now = Instant::now(); let mut collector = TerminalColors::default(); let mut output = Vec::new(); assert!(collector.refresh_due(now)); collector.query(&mut output, now).unwrap(); report(&mut collector, "11;#101010", now); report(&mut collector, "4;1;#FF0000", now); report(&mut collector, "4;2;#00FF00", now); let previous = collector.colors; assert!(previous.is_some()); // The terminal changes its palette without emitting a notification. let refresh = now + Duration::from_secs(1); assert!(!collector.refresh_due(refresh - Duration::from_nanos(1))); assert!(collector.refresh_due(refresh)); collector.query(&mut output, refresh).unwrap(); report(&mut collector, "11;#EEEEEE", refresh); report(&mut collector, "4;1;#AA1122", refresh); assert_eq!(collector.colors, previous); let typed = key(KeyCode::Char('i'), KeyModifiers::NONE); collector.push(typed.clone(), refresh); assert_eq!(drain(&mut collector), vec![typed]); // Even an overdue probe must wait until the incomplete query expires. let retry = refresh + Duration::from_secs(1); assert!(!collector.refresh_due(retry)); collector.expire(retry); assert_eq!(collector.colors, None); assert!(collector.refresh_due(retry)); collector.query(&mut output, retry).unwrap(); report(&mut collector, "11;#EEEEEE", retry); report(&mut collector, "4;1;#AA1122", retry); assert_eq!(collector.colors, None); report(&mut collector, "4;2;#22BB33", retry); assert_eq!( collector.colors, Some(ThemeColors { background: [238, 238, 238], red: [170, 17, 34], green: [34, 187, 51], }) ); assert!(drain(&mut collector).is_empty()); assert!(!collector.refresh_due(retry + Duration::from_millis(999))); assert!(collector.refresh_due(retry + Duration::from_secs(1))); } #[test] fn failed_refresh_clears_stale_colors_and_consumes_late_partial_reply() { let now = Instant::now(); let mut collector = TerminalColors::default(); collector.query(&mut Vec::new(), now).unwrap(); report(&mut collector, "11;#101010", now); report(&mut collector, "4;1;#FF0000", now); report(&mut collector, "4;2;#00FF00", now); assert!(collector.colors.is_some()); let refresh = now + Duration::from_secs(1); collector.query(&mut Vec::new(), refresh).unwrap(); report(&mut collector, "11;#EEEEEE", refresh); collector.push(key(KeyCode::Char(']'), KeyModifiers::ALT), refresh); bytes(&mut collector, "4;1;rgb:ffff/", refresh); collector.expire(refresh + QUERY_TIMEOUT); assert_eq!(collector.colors, None); let late = refresh + Duration::from_secs(1); bytes(&mut collector, "0000/0000", late); collector.push(key(KeyCode::Esc, KeyModifiers::NONE), late); collector.expire(late + Duration::from_secs(1)); collector.push( key(KeyCode::Char('\\'), KeyModifiers::NONE), late + Duration::from_secs(1), ); report(&mut collector, "4;2;#00FF00", late + Duration::from_secs(1)); assert_eq!(collector.colors, None); assert!(drain(&mut collector).is_empty()); let typed = key(KeyCode::Char('i'), KeyModifiers::NONE); collector.push(typed.clone(), late + Duration::from_secs(2)); assert_eq!(collector.pop(), Some(typed)); } #[test] fn unrelated_events_and_rejected_candidates_replay_exactly() { let now = Instant::now(); let mut collector = TerminalColors::default(); collector.query(&mut Vec::new(), now).unwrap(); let events = vec![ key(KeyCode::Char(']'), KeyModifiers::ALT), key(KeyCode::Char('4'), KeyModifiers::NONE), key(KeyCode::Char(';'), KeyModifiers::NONE), key(KeyCode::Char('3'), KeyModifiers::NONE), key(KeyCode::Char('g'), KeyModifiers::CONTROL), key(KeyCode::Char(']'), KeyModifiers::ALT | KeyModifiers::SHIFT), Event::Key(KeyEvent::new_with_kind( KeyCode::Char(']'), KeyModifiers::ALT, KeyEventKind::Release, )), Event::Key(KeyEvent::new_with_kind( KeyCode::Char('A'), KeyModifiers::SHIFT, KeyEventKind::Repeat, )), Event::Key(KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::CAPS_LOCK, }), Event::Mouse(MouseEvent { kind: MouseEventKind::Moved, column: 2, row: 3, modifiers: KeyModifiers::CONTROL, }), Event::FocusLost, Event::Resize(80, 24), key(KeyCode::Char(']'), KeyModifiers::ALT), Event::Paste("\x1b]11;rgb:ff/ff/ff\x07".into()), Event::FocusGained, ]; for event in &events { collector.push(event.clone(), now); } assert_eq!(drain(&mut collector), events); assert_eq!(collector.colors, None); } #[test] fn malformed_or_oversized_reports_and_lone_escape_do_not_eat_input() { let now = Instant::now(); let mut collector = TerminalColors::default(); for body in [ "11;rgb:12345/0/0", "11;rgb:1//3", "11;#12345", "11;rgb:1/2/3/4", ] { let mut expected = vec![key(KeyCode::Char(']'), KeyModifiers::ALT)]; expected.extend( body.chars() .map(|ch| key(KeyCode::Char(ch), KeyModifiers::NONE)), ); expected.push(key(KeyCode::Char('g'), KeyModifiers::CONTROL)); for event in &expected { collector.push(event.clone(), now); } assert_eq!(drain(&mut collector), expected); } let escape = key(KeyCode::Esc, KeyModifiers::NONE); collector.push(escape.clone(), now); assert_eq!(collector.pop(), None); collector.expire(now + ESCAPE_TIMEOUT); assert_eq!(collector.pop(), Some(escape)); assert_eq!(collector.pop(), None); let start = key(KeyCode::Char(']'), KeyModifiers::ALT); collector.push(start.clone(), now + Duration::from_secs(1)); collector.expire(now + Duration::from_secs(1) + QUERY_TIMEOUT); assert_eq!(collector.pop(), Some(start)); assert_eq!(collector.pop(), None); }