const std = @import("std");const debug_trace = @import("../../core/shared/debug_trace.zig");const theme_protocol = @import("theme_protocol.zig"); // OSC 11 shares Escape's prefix: allow fragmented replies without imposing// the cursor probe's full 100 ms delay on a standalone Escape key.pub const response_idle_timeout_ms: i64 = 75;pub const background_response_timeout_ms: i64 = 200;const background_sample_interval_ms: i64 = 1000;const query_retry_delay_ms: i64 = 1000; const max_candidate_bytes = 64; pub const ThemeUpdate = struct { light: bool, rgb: ?theme_protocol.Rgb = null,}; pub const ForwardBytes = struct { storage: [max_candidate_bytes + 1]u8 = undefined, len: u8 = 0, pub fn slice(self: *const ForwardBytes) []const u8 { return self.storage[0..self.len]; }}; pub const FeedResult = union(enum) { pending, consumed, forward: ForwardBytes,}; pub const QueryRequest = enum { response_fence, background,}; const BackgroundQuery = struct { deadline_ms: i64, background: ?ThemeUpdate = null,}; const QueryState = union(enum) { idle, awaiting_response_fence: i64, background_ready, awaiting_background: BackgroundQuery,}; pub const Monitor = struct { enabled: bool = false, candidate: [max_candidate_bytes]u8 = undefined, candidate_len: u8 = 0, candidate_deadline_ms: i64 = 0, deferred: [max_candidate_bytes]u8 = undefined, deferred_start: u8 = 0, deferred_len: u8 = 0, deferred_input_in_flight: bool = false, query_state: QueryState = .idle, theme_dirty: bool = false, retry_after_ms: i64 = 0, next_sample_ms: ?i64 = null, settled_update: ?ThemeUpdate = null, pub fn start(self: *Monitor) void { self.enabled = true; } pub fn hasPendingInput(self: *const Monitor) bool { return self.candidate_len > 0 or self.deferred_start < self.deferred_len or self.deferred_input_in_flight; } pub fn ownsInput(self: *const Monitor) bool { if (!self.enabled) return false; if (self.hasPendingInput()) return true; return switch (self.query_state) { .awaiting_response_fence, .awaiting_background => true, .idle, .background_ready => false, }; } pub fn feed(self: *Monitor, byte: u8, now_ms: i64) FeedResult { if (!self.enabled) return forwardByte(byte); if (self.candidate_len == 0 and byte != 0x1b) return forwardByte(byte); if (self.candidate_len == 1 and byte == 0x1b) { self.candidate_deadline_ms = addMillis(now_ms, response_idle_timeout_ms); return forwardByte(byte); } if (self.candidate_len == self.candidate.len) return self.forwardCandidateWith(byte); self.candidate[self.candidate_len] = byte; self.candidate_len += 1; self.candidate_deadline_ms = addMillis(now_ms, response_idle_timeout_ms); const candidate = self.candidate[0..self.candidate_len]; if (std.mem.eql(u8, candidate, dark_response)) { self.candidate_len = 0; self.queueRefresh(); return .consumed; } if (std.mem.eql(u8, candidate, light_response)) { self.candidate_len = 0; self.queueRefresh(); return .consumed; } if (std.mem.startsWith(u8, dark_response, candidate) or std.mem.startsWith(u8, light_response, candidate)) { return .pending; } switch (classifyPrimaryDeviceAttributes(candidate)) { .pending => return .pending, .complete => { self.candidate_len = 0; self.candidate_deadline_ms = 0; self.finishResponseFence(); return .consumed; }, .invalid => {}, } // While enabled, own complete parseable OSC 11 replies. Retain only // samples bracketed by response fences; discard every other reply. if (std.mem.startsWith(u8, osc11_prefix, candidate)) return .pending; if (std.mem.startsWith(u8, candidate, osc11_prefix)) { if (!std.mem.endsWith(u8, candidate, "\x07") and !std.mem.endsWith(u8, candidate, "\x1b\\")) { return .pending; } if (theme_protocol.parseOsc11Response(candidate)) |background| { self.candidate_len = 0; self.candidate_deadline_ms = 0; switch (self.query_state) { .awaiting_background => |*query| { query.background = .{ .light = background.light, .rgb = background.rgb, }; }, else => {}, } return .consumed; } } return self.forwardCandidate(); } pub fn poll(self: *Monitor, now_ms: i64) void { if (self.candidate_len > 0 and now_ms >= self.candidate_deadline_ms) { const candidate = self.candidate[0..self.candidate_len]; if (std.mem.startsWith(u8, candidate, osc11_prefix)) { debug_trace.logf( "theme", "theme osc11 candidate dropped bytes={d} reason=osc11_idle_timeout", .{self.candidate_len}, ); self.candidate_len = 0; self.candidate_deadline_ms = 0; } else { self.deferCandidate(); } } const query_deadline = switch (self.query_state) { .awaiting_response_fence => |deadline_ms| deadline_ms, .awaiting_background => |query| query.deadline_ms, .idle, .background_ready => return, }; if (now_ms >= query_deadline) { debug_trace.logf("theme", "theme_query_timeout phase={s}", .{@tagName(self.query_state)}); self.query_state = .idle; self.theme_dirty = true; self.retry_after_ms = addMillis(now_ms, query_retry_delay_ms); } } pub fn takeQueryRequest(self: *Monitor, now_ms: i64) ?QueryRequest { switch (self.query_state) { .idle => { if (now_ms < self.retry_after_ms) return null; if (!self.theme_dirty) { const next_sample_ms = self.next_sample_ms orelse { self.next_sample_ms = addMillis(now_ms, background_sample_interval_ms); return null; }; if (now_ms < next_sample_ms) return null; } self.next_sample_ms = addMillis(now_ms, background_sample_interval_ms); self.query_state = .{ .awaiting_response_fence = addMillis(now_ms, background_response_timeout_ms), }; return .response_fence; }, .background_ready => { self.theme_dirty = false; self.query_state = .{ .awaiting_background = .{ .deadline_ms = addMillis(now_ms, background_response_timeout_ms), } }; return .background; }, .awaiting_response_fence, .awaiting_background => return null, } } pub fn failQuery(self: *Monitor, now_ms: i64) void { if (self.query_state == .idle) return; self.query_state = .idle; self.theme_dirty = true; self.retry_after_ms = addMillis(now_ms, query_retry_delay_ms); } pub fn takeSettledUpdate(self: *Monitor) ?ThemeUpdate { const update = self.settled_update; self.settled_update = null; return update; } pub fn takeDeferredByte(self: *Monitor) ?u8 { std.debug.assert(!self.deferred_input_in_flight); if (self.deferred_start >= self.deferred_len) return null; const byte = self.deferred[self.deferred_start]; self.deferred_start += 1; if (self.deferred_start == self.deferred_len) { self.deferred_start = 0; self.deferred_len = 0; } self.deferred_input_in_flight = true; return byte; } pub fn consumeDeferredInputDispatch(self: *Monitor) bool { const in_flight = self.deferred_input_in_flight; self.deferred_input_in_flight = false; return in_flight; } fn queueRefresh(self: *Monitor) void { if (self.settled_update != null) { debug_trace.logf("theme", "theme_update_dropped reason=newer_notification", .{}); self.settled_update = null; } self.theme_dirty = true; self.retry_after_ms = 0; } fn finishResponseFence(self: *Monitor) void { switch (self.query_state) { .awaiting_response_fence => self.query_state = .background_ready, .awaiting_background => |query| { const update = query.background orelse return; self.query_state = .idle; self.retry_after_ms = 0; if (self.theme_dirty) { debug_trace.logf("theme", "theme_sample_dropped reason=newer_notification", .{}); return; } self.settled_update = update; }, .idle, .background_ready => {}, } } fn deferCandidate(self: *Monitor) void { if (self.candidate_len == 0) return; std.debug.assert(self.deferred_start == self.deferred_len); @memcpy(self.deferred[0..self.candidate_len], self.candidate[0..self.candidate_len]); self.deferred_start = 0; self.deferred_len = self.candidate_len; self.candidate_len = 0; self.candidate_deadline_ms = 0; } fn forwardCandidate(self: *Monitor) FeedResult { var forwarded = ForwardBytes{}; @memcpy(forwarded.storage[0..self.candidate_len], self.candidate[0..self.candidate_len]); forwarded.len = self.candidate_len; self.candidate_len = 0; self.candidate_deadline_ms = 0; return .{ .forward = forwarded }; } fn forwardCandidateWith(self: *Monitor, byte: u8) FeedResult { var forwarded = self.forwardCandidate().forward; forwarded.storage[forwarded.len] = byte; forwarded.len += 1; return .{ .forward = forwarded }; }}; const dark_response = "\x1b[?997;1n";const light_response = "\x1b[?997;2n";const response_fence = "\x1b[?1;2c";const primary_device_attributes_prefix = "\x1b[?";const osc11_prefix = "\x1b]11;rgb:"; const ResponseStatus = enum { invalid, pending, complete }; fn classifyPrimaryDeviceAttributes(bytes: []const u8) ResponseStatus { if (std.mem.startsWith(u8, primary_device_attributes_prefix, bytes)) return .pending; if (!std.mem.startsWith(u8, bytes, primary_device_attributes_prefix)) return .invalid; const parameters = bytes[primary_device_attributes_prefix.len..]; var expect_digit = true; for (parameters, 0..) |byte, index| { if (std.ascii.isDigit(byte)) { expect_digit = false; continue; } if (byte == ';' and !expect_digit) { expect_digit = true; continue; } if (byte == 'c') { return if (!expect_digit and index + 1 == parameters.len) .complete else .invalid; } return .invalid; } return .pending;} fn addMillis(now_ms: i64, duration_ms: i64) i64 { return std.math.add(i64, now_ms, duration_ms) catch std.math.maxInt(i64);} fn forwardByte(byte: u8) FeedResult { var forwarded = ForwardBytes{}; forwarded.storage[0] = byte; forwarded.len = 1; return .{ .forward = forwarded };} fn beginBackgroundSampleForTest(monitor: *Monitor, now_ms: i64) !void { try std.testing.expectEqual(QueryRequest.response_fence, monitor.takeQueryRequest(now_ms).?); for (response_fence) |byte| _ = monitor.feed(byte, now_ms + 1); try std.testing.expectEqual(QueryRequest.background, monitor.takeQueryRequest(now_ms + 2).?);} test "theme monitor queues a fenced background sample for a theme notification" { var monitor = Monitor{}; monitor.start(); for (dark_response) |byte| { _ = monitor.feed(byte, 0); } try std.testing.expectEqual(QueryRequest.response_fence, monitor.takeQueryRequest(0).?); try std.testing.expect(monitor.takeQueryRequest(0) == null); for (response_fence) |byte| _ = monitor.feed(byte, 1); try std.testing.expectEqual(QueryRequest.background, monitor.takeQueryRequest(2).?); try std.testing.expect(monitor.takeQueryRequest(2) == null);} test "theme monitor settles a strict OSC 11 response" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); for ("\x1b]11;rgb:ffff/ffff/ffff\x1b\\") |byte| _ = monitor.feed(byte, 3); for (response_fence) |byte| _ = monitor.feed(byte, 4); const update = monitor.takeSettledUpdate().?; try std.testing.expect(update.light); try std.testing.expectEqual(@as(u8, 0xff), update.rgb.?.r);} test "theme monitor trusts terminal background over OS preference" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); for ("\x1b]11;rgb:1c1c/1c1c/1c1c\x1b\\") |byte| _ = monitor.feed(byte, 3); for (response_fence) |byte| _ = monitor.feed(byte, 4); const update = monitor.takeSettledUpdate().?; try std.testing.expect(!update.light); try std.testing.expectEqual(@as(u8, 0x1c), update.rgb.?.r);} test "theme monitor uses the last background before the response fence" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); for ("\x1b]11;rgb:1c1c/1c1c/1c1c\x1b\\") |byte| _ = monitor.feed(byte, 3); for ("\x1b]11;rgb:ffff/ffff/ffff\x1b\\") |byte| _ = monitor.feed(byte, 4); for (response_fence) |byte| _ = monitor.feed(byte, 5); const update = monitor.takeSettledUpdate().?; try std.testing.expect(update.light); try std.testing.expectEqual(@as(u8, 0xff), update.rgb.?.r);} test "theme monitor drains stale backgrounds before sampling" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try std.testing.expectEqual(QueryRequest.response_fence, monitor.takeQueryRequest(0).?); for ("\x1b]11;rgb:1c1c/1c1c/1c1c\x1b\\") |byte| _ = monitor.feed(byte, 1); for (response_fence) |byte| _ = monitor.feed(byte, 2); try std.testing.expect(monitor.takeSettledUpdate() == null); try std.testing.expectEqual(QueryRequest.background, monitor.takeQueryRequest(3).?); for (response_fence) |byte| _ = monitor.feed(byte, 4); try std.testing.expect(monitor.takeSettledUpdate() == null); for ("\x1b]11;rgb:ffff/ffff/ffff\x1b\\") |byte| _ = monitor.feed(byte, 4); for (response_fence) |byte| _ = monitor.feed(byte, 5); const update = monitor.takeSettledUpdate().?; try std.testing.expect(update.light); try std.testing.expectEqual(@as(u8, 0xff), update.rgb.?.r);} test "theme monitor retries after a background query timeout" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); const timed_out_ms = background_response_timeout_ms + 2; monitor.poll(timed_out_ms); try std.testing.expect(monitor.takeSettledUpdate() == null); for ("\x1b]11;rgb:ffff/ffff/ffff\x1b\\") |byte| { try std.testing.expect(switch (monitor.feed(byte, background_response_timeout_ms + 1)) { .pending, .consumed => true, .forward => false, }); } try std.testing.expect(monitor.takeSettledUpdate() == null); try std.testing.expect(monitor.takeQueryRequest(timed_out_ms) == null); try std.testing.expectEqual( QueryRequest.response_fence, monitor.takeQueryRequest(timed_out_ms + 1000).?, );} test "theme monitor backs off after a query write failure" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); _ = monitor.takeQueryRequest(0).?; monitor.failQuery(1); try std.testing.expect(monitor.takeQueryRequest(1000) == null); try std.testing.expectEqual(QueryRequest.response_fence, monitor.takeQueryRequest(1001).?);} test "theme monitor consumes OSC 11 long after query timeout without forwarding" { var monitor = Monitor{}; monitor.start(); for (dark_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); monitor.poll(background_response_timeout_ms + 2); try std.testing.expect(monitor.takeSettledUpdate() == null); // Far past any former late-ownership window; still must not forward. const late_ms = background_response_timeout_ms * 5; for ("\x1b]11;rgb:0000/0000/0000\x07") |byte| { try std.testing.expect(switch (monitor.feed(byte, late_ms)) { .pending, .consumed => true, .forward => false, }); } try std.testing.expect(monitor.takeSettledUpdate() == null);} test "theme monitor forwards a timed out partial candidate exactly once" { var monitor = Monitor{}; monitor.start(); for ("\x1b[") |byte| _ = monitor.feed(byte, 0); try std.testing.expect(monitor.hasPendingInput()); monitor.poll(response_idle_timeout_ms); var forwarded: [2]u8 = undefined; for (&forwarded) |*byte| { byte.* = monitor.takeDeferredByte().?; try std.testing.expect(monitor.hasPendingInput()); try std.testing.expect(monitor.consumeDeferredInputDispatch()); } try std.testing.expectEqualStrings("\x1b[", &forwarded); try std.testing.expect(monitor.takeDeferredByte() == null); try std.testing.expect(!monitor.hasPendingInput());} test "theme monitor separates user escape from an adjacent terminal response" { var monitor = Monitor{}; monitor.start(); for (dark_response) |byte| _ = monitor.feed(byte, 0);Lines 1–500 of 662. Load more
const std = @import("std"); const debug_trace = @import("../../core/shared/debug_trace.zig"); const theme_protocol = @import("theme_protocol.zig"); // OSC 11 shares Escape's prefix: allow fragmented replies without imposing // the cursor probe's full 100 ms delay on a standalone Escape key. pub const response_idle_timeout_ms: i64 = 75; pub const background_response_timeout_ms: i64 = 200; const background_sample_interval_ms: i64 = 1000; const query_retry_delay_ms: i64 = 1000; const max_candidate_bytes = 64; pub const ThemeUpdate = struct { light: bool, rgb: ?theme_protocol.Rgb = null, }; pub const ForwardBytes = struct { storage: [max_candidate_bytes + 1]u8 = undefined, len: u8 = 0, pub fn slice(self: *const ForwardBytes) []const u8 { return self.storage[0..self.len]; } }; pub const FeedResult = union(enum) { pending, consumed, forward: ForwardBytes, }; pub const QueryRequest = enum { response_fence, background, }; const BackgroundQuery = struct { deadline_ms: i64, background: ?ThemeUpdate = null, }; const QueryState = union(enum) { idle, awaiting_response_fence: i64, background_ready, awaiting_background: BackgroundQuery, }; pub const Monitor = struct { enabled: bool = false, candidate: [max_candidate_bytes]u8 = undefined, candidate_len: u8 = 0, candidate_deadline_ms: i64 = 0, deferred: [max_candidate_bytes]u8 = undefined, deferred_start: u8 = 0, deferred_len: u8 = 0, deferred_input_in_flight: bool = false, query_state: QueryState = .idle, theme_dirty: bool = false, retry_after_ms: i64 = 0, next_sample_ms: ?i64 = null, settled_update: ?ThemeUpdate = null, pub fn start(self: *Monitor) void { self.enabled = true; } pub fn hasPendingInput(self: *const Monitor) bool { return self.candidate_len > 0 or self.deferred_start < self.deferred_len or self.deferred_input_in_flight; } pub fn ownsInput(self: *const Monitor) bool { if (!self.enabled) return false; if (self.hasPendingInput()) return true; return switch (self.query_state) { .awaiting_response_fence, .awaiting_background => true, .idle, .background_ready => false, }; } pub fn feed(self: *Monitor, byte: u8, now_ms: i64) FeedResult { if (!self.enabled) return forwardByte(byte); if (self.candidate_len == 0 and byte != 0x1b) return forwardByte(byte); if (self.candidate_len == 1 and byte == 0x1b) { self.candidate_deadline_ms = addMillis(now_ms, response_idle_timeout_ms); return forwardByte(byte); } if (self.candidate_len == self.candidate.len) return self.forwardCandidateWith(byte); self.candidate[self.candidate_len] = byte; self.candidate_len += 1; self.candidate_deadline_ms = addMillis(now_ms, response_idle_timeout_ms); const candidate = self.candidate[0..self.candidate_len]; if (std.mem.eql(u8, candidate, dark_response)) { self.candidate_len = 0; self.queueRefresh(); return .consumed; } if (std.mem.eql(u8, candidate, light_response)) { self.candidate_len = 0; self.queueRefresh(); return .consumed; } if (std.mem.startsWith(u8, dark_response, candidate) or std.mem.startsWith(u8, light_response, candidate)) { return .pending; } switch (classifyPrimaryDeviceAttributes(candidate)) { .pending => return .pending, .complete => { self.candidate_len = 0; self.candidate_deadline_ms = 0; self.finishResponseFence(); return .consumed; }, .invalid => {}, } // While enabled, own complete parseable OSC 11 replies. Retain only // samples bracketed by response fences; discard every other reply. if (std.mem.startsWith(u8, osc11_prefix, candidate)) return .pending; if (std.mem.startsWith(u8, candidate, osc11_prefix)) { if (!std.mem.endsWith(u8, candidate, "\x07") and !std.mem.endsWith(u8, candidate, "\x1b\\")) { return .pending; } if (theme_protocol.parseOsc11Response(candidate)) |background| { self.candidate_len = 0; self.candidate_deadline_ms = 0; switch (self.query_state) { .awaiting_background => |*query| { query.background = .{ .light = background.light, .rgb = background.rgb, }; }, else => {}, } return .consumed; } } return self.forwardCandidate(); } pub fn poll(self: *Monitor, now_ms: i64) void { if (self.candidate_len > 0 and now_ms >= self.candidate_deadline_ms) { const candidate = self.candidate[0..self.candidate_len]; if (std.mem.startsWith(u8, candidate, osc11_prefix)) { debug_trace.logf( "theme", "theme osc11 candidate dropped bytes={d} reason=osc11_idle_timeout", .{self.candidate_len}, ); self.candidate_len = 0; self.candidate_deadline_ms = 0; } else { self.deferCandidate(); } } const query_deadline = switch (self.query_state) { .awaiting_response_fence => |deadline_ms| deadline_ms, .awaiting_background => |query| query.deadline_ms, .idle, .background_ready => return, }; if (now_ms >= query_deadline) { debug_trace.logf("theme", "theme_query_timeout phase={s}", .{@tagName(self.query_state)}); self.query_state = .idle; self.theme_dirty = true; self.retry_after_ms = addMillis(now_ms, query_retry_delay_ms); } } pub fn takeQueryRequest(self: *Monitor, now_ms: i64) ?QueryRequest { switch (self.query_state) { .idle => { if (now_ms < self.retry_after_ms) return null; if (!self.theme_dirty) { const next_sample_ms = self.next_sample_ms orelse { self.next_sample_ms = addMillis(now_ms, background_sample_interval_ms); return null; }; if (now_ms < next_sample_ms) return null; } self.next_sample_ms = addMillis(now_ms, background_sample_interval_ms); self.query_state = .{ .awaiting_response_fence = addMillis(now_ms, background_response_timeout_ms), }; return .response_fence; }, .background_ready => { self.theme_dirty = false; self.query_state = .{ .awaiting_background = .{ .deadline_ms = addMillis(now_ms, background_response_timeout_ms), } }; return .background; }, .awaiting_response_fence, .awaiting_background => return null, } } pub fn failQuery(self: *Monitor, now_ms: i64) void { if (self.query_state == .idle) return; self.query_state = .idle; self.theme_dirty = true; self.retry_after_ms = addMillis(now_ms, query_retry_delay_ms); } pub fn takeSettledUpdate(self: *Monitor) ?ThemeUpdate { const update = self.settled_update; self.settled_update = null; return update; } pub fn takeDeferredByte(self: *Monitor) ?u8 { std.debug.assert(!self.deferred_input_in_flight); if (self.deferred_start >= self.deferred_len) return null; const byte = self.deferred[self.deferred_start]; self.deferred_start += 1; if (self.deferred_start == self.deferred_len) { self.deferred_start = 0; self.deferred_len = 0; } self.deferred_input_in_flight = true; return byte; } pub fn consumeDeferredInputDispatch(self: *Monitor) bool { const in_flight = self.deferred_input_in_flight; self.deferred_input_in_flight = false; return in_flight; } fn queueRefresh(self: *Monitor) void { if (self.settled_update != null) { debug_trace.logf("theme", "theme_update_dropped reason=newer_notification", .{}); self.settled_update = null; } self.theme_dirty = true; self.retry_after_ms = 0; } fn finishResponseFence(self: *Monitor) void { switch (self.query_state) { .awaiting_response_fence => self.query_state = .background_ready, .awaiting_background => |query| { const update = query.background orelse return; self.query_state = .idle; self.retry_after_ms = 0; if (self.theme_dirty) { debug_trace.logf("theme", "theme_sample_dropped reason=newer_notification", .{}); return; } self.settled_update = update; }, .idle, .background_ready => {}, } } fn deferCandidate(self: *Monitor) void { if (self.candidate_len == 0) return; std.debug.assert(self.deferred_start == self.deferred_len); @memcpy(self.deferred[0..self.candidate_len], self.candidate[0..self.candidate_len]); self.deferred_start = 0; self.deferred_len = self.candidate_len; self.candidate_len = 0; self.candidate_deadline_ms = 0; } fn forwardCandidate(self: *Monitor) FeedResult { var forwarded = ForwardBytes{}; @memcpy(forwarded.storage[0..self.candidate_len], self.candidate[0..self.candidate_len]); forwarded.len = self.candidate_len; self.candidate_len = 0; self.candidate_deadline_ms = 0; return .{ .forward = forwarded }; } fn forwardCandidateWith(self: *Monitor, byte: u8) FeedResult { var forwarded = self.forwardCandidate().forward; forwarded.storage[forwarded.len] = byte; forwarded.len += 1; return .{ .forward = forwarded }; } }; const dark_response = "\x1b[?997;1n"; const light_response = "\x1b[?997;2n"; const response_fence = "\x1b[?1;2c"; const primary_device_attributes_prefix = "\x1b[?"; const osc11_prefix = "\x1b]11;rgb:"; const ResponseStatus = enum { invalid, pending, complete }; fn classifyPrimaryDeviceAttributes(bytes: []const u8) ResponseStatus { if (std.mem.startsWith(u8, primary_device_attributes_prefix, bytes)) return .pending; if (!std.mem.startsWith(u8, bytes, primary_device_attributes_prefix)) return .invalid; const parameters = bytes[primary_device_attributes_prefix.len..]; var expect_digit = true; for (parameters, 0..) |byte, index| { if (std.ascii.isDigit(byte)) { expect_digit = false; continue; } if (byte == ';' and !expect_digit) { expect_digit = true; continue; } if (byte == 'c') { return if (!expect_digit and index + 1 == parameters.len) .complete else .invalid; } return .invalid; } return .pending; } fn addMillis(now_ms: i64, duration_ms: i64) i64 { return std.math.add(i64, now_ms, duration_ms) catch std.math.maxInt(i64); } fn forwardByte(byte: u8) FeedResult { var forwarded = ForwardBytes{}; forwarded.storage[0] = byte; forwarded.len = 1; return .{ .forward = forwarded }; } fn beginBackgroundSampleForTest(monitor: *Monitor, now_ms: i64) !void { try std.testing.expectEqual(QueryRequest.response_fence, monitor.takeQueryRequest(now_ms).?); for (response_fence) |byte| _ = monitor.feed(byte, now_ms + 1); try std.testing.expectEqual(QueryRequest.background, monitor.takeQueryRequest(now_ms + 2).?); } test "theme monitor queues a fenced background sample for a theme notification" { var monitor = Monitor{}; monitor.start(); for (dark_response) |byte| { _ = monitor.feed(byte, 0); } try std.testing.expectEqual(QueryRequest.response_fence, monitor.takeQueryRequest(0).?); try std.testing.expect(monitor.takeQueryRequest(0) == null); for (response_fence) |byte| _ = monitor.feed(byte, 1); try std.testing.expectEqual(QueryRequest.background, monitor.takeQueryRequest(2).?); try std.testing.expect(monitor.takeQueryRequest(2) == null); } test "theme monitor settles a strict OSC 11 response" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); for ("\x1b]11;rgb:ffff/ffff/ffff\x1b\\") |byte| _ = monitor.feed(byte, 3); for (response_fence) |byte| _ = monitor.feed(byte, 4); const update = monitor.takeSettledUpdate().?; try std.testing.expect(update.light); try std.testing.expectEqual(@as(u8, 0xff), update.rgb.?.r); } test "theme monitor trusts terminal background over OS preference" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); for ("\x1b]11;rgb:1c1c/1c1c/1c1c\x1b\\") |byte| _ = monitor.feed(byte, 3); for (response_fence) |byte| _ = monitor.feed(byte, 4); const update = monitor.takeSettledUpdate().?; try std.testing.expect(!update.light); try std.testing.expectEqual(@as(u8, 0x1c), update.rgb.?.r); } test "theme monitor uses the last background before the response fence" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); for ("\x1b]11;rgb:1c1c/1c1c/1c1c\x1b\\") |byte| _ = monitor.feed(byte, 3); for ("\x1b]11;rgb:ffff/ffff/ffff\x1b\\") |byte| _ = monitor.feed(byte, 4); for (response_fence) |byte| _ = monitor.feed(byte, 5); const update = monitor.takeSettledUpdate().?; try std.testing.expect(update.light); try std.testing.expectEqual(@as(u8, 0xff), update.rgb.?.r); } test "theme monitor drains stale backgrounds before sampling" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try std.testing.expectEqual(QueryRequest.response_fence, monitor.takeQueryRequest(0).?); for ("\x1b]11;rgb:1c1c/1c1c/1c1c\x1b\\") |byte| _ = monitor.feed(byte, 1); for (response_fence) |byte| _ = monitor.feed(byte, 2); try std.testing.expect(monitor.takeSettledUpdate() == null); try std.testing.expectEqual(QueryRequest.background, monitor.takeQueryRequest(3).?); for (response_fence) |byte| _ = monitor.feed(byte, 4); try std.testing.expect(monitor.takeSettledUpdate() == null); for ("\x1b]11;rgb:ffff/ffff/ffff\x1b\\") |byte| _ = monitor.feed(byte, 4); for (response_fence) |byte| _ = monitor.feed(byte, 5); const update = monitor.takeSettledUpdate().?; try std.testing.expect(update.light); try std.testing.expectEqual(@as(u8, 0xff), update.rgb.?.r); } test "theme monitor retries after a background query timeout" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); const timed_out_ms = background_response_timeout_ms + 2; monitor.poll(timed_out_ms); try std.testing.expect(monitor.takeSettledUpdate() == null); for ("\x1b]11;rgb:ffff/ffff/ffff\x1b\\") |byte| { try std.testing.expect(switch (monitor.feed(byte, background_response_timeout_ms + 1)) { .pending, .consumed => true, .forward => false, }); } try std.testing.expect(monitor.takeSettledUpdate() == null); try std.testing.expect(monitor.takeQueryRequest(timed_out_ms) == null); try std.testing.expectEqual( QueryRequest.response_fence, monitor.takeQueryRequest(timed_out_ms + 1000).?, ); } test "theme monitor backs off after a query write failure" { var monitor = Monitor{}; monitor.start(); for (light_response) |byte| _ = monitor.feed(byte, 0); _ = monitor.takeQueryRequest(0).?; monitor.failQuery(1); try std.testing.expect(monitor.takeQueryRequest(1000) == null); try std.testing.expectEqual(QueryRequest.response_fence, monitor.takeQueryRequest(1001).?); } test "theme monitor consumes OSC 11 long after query timeout without forwarding" { var monitor = Monitor{}; monitor.start(); for (dark_response) |byte| _ = monitor.feed(byte, 0); try beginBackgroundSampleForTest(&monitor, 0); monitor.poll(background_response_timeout_ms + 2); try std.testing.expect(monitor.takeSettledUpdate() == null); // Far past any former late-ownership window; still must not forward. const late_ms = background_response_timeout_ms * 5; for ("\x1b]11;rgb:0000/0000/0000\x07") |byte| { try std.testing.expect(switch (monitor.feed(byte, late_ms)) { .pending, .consumed => true, .forward => false, }); } try std.testing.expect(monitor.takeSettledUpdate() == null); } test "theme monitor forwards a timed out partial candidate exactly once" { var monitor = Monitor{}; monitor.start(); for ("\x1b[") |byte| _ = monitor.feed(byte, 0); try std.testing.expect(monitor.hasPendingInput()); monitor.poll(response_idle_timeout_ms); var forwarded: [2]u8 = undefined; for (&forwarded) |*byte| { byte.* = monitor.takeDeferredByte().?; try std.testing.expect(monitor.hasPendingInput()); try std.testing.expect(monitor.consumeDeferredInputDispatch()); } try std.testing.expectEqualStrings("\x1b[", &forwarded); try std.testing.expect(monitor.takeDeferredByte() == null); try std.testing.expect(!monitor.hasPendingInput()); } test "theme monitor separates user escape from an adjacent terminal response" { var monitor = Monitor{}; monitor.start(); for (dark_response) |byte| _ = monitor.feed(byte, 0);