fix: refresh cached diff colors for external theme updates

This commit is contained in:
2026-10-11 21:53:28 +02:00
parent 5bf1f1df59
commit b41db31831
4 changed files with 75 additions and 5 deletions
+56
View File
@@ -7,6 +7,7 @@ use std::{
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyEventState, KeyModifiers};
const QUERY_TIMEOUT: Duration = Duration::from_millis(250);
const REFRESH_INTERVAL: Duration = Duration::from_secs(1);
const ESCAPE_TIMEOUT: Duration = Duration::from_millis(25);
const HEADERS: [&[u8]; 3] = [b"11;", b"4;1;", b"4;2;"];
const MAX_BODY: usize = 32;
@@ -39,6 +40,7 @@ enum Capture {
pub(crate) struct TerminalColors {
pub colors: Option<ThemeColors>,
query: Option<Query>,
refresh_at: Option<Instant>,
capture: Capture,
original: Vec<Event>,
body: [u8; MAX_BODY],
@@ -47,8 +49,13 @@ pub(crate) struct TerminalColors {
}
impl TerminalColors {
pub fn refresh_due(&self, now: Instant) -> bool {
self.query.is_none() && self.refresh_at.is_none_or(|deadline| now >= deadline)
}
pub fn query(&mut self, output: &mut impl Write, now: Instant) -> io::Result<()> {
self.expire(now);
self.refresh_at = Some(now + REFRESH_INTERVAL);
self.query = None;
if let Err(error) = output
.write_all(b"\x1b]11;?\x1b\\\x1b]4;1;?;2;?\x1b\\")
@@ -397,6 +404,55 @@ mod tests {
}
}
#[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();