Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-terminal-color-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Refresh the `terminal` theme about three times faster after a terminal color-scheme switch, pick up switches from terminals that answer slowly, and settle on the final palette instead of a mix of old and new colors after rapid switches.
9 changes: 8 additions & 1 deletion docs/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ Hunk therefore matches whatever color scheme your terminal already uses.
Hunk keeps following the terminal while it runs. When the terminal reports a
color-scheme change (mode 2031, sent by Ghostty, kitty, herdr, and others) or
Hunk receives `SIGWINCH` (what tmux-aware theme tools send after rewriting a
pane's colors), Hunk probes the palette again and repaints if it changed.
pane's colors), Hunk probes the palette again and repaints if it changed. The
probe ends as soon as the terminal has answered, so a responsive terminal
repaints about a tenth of a second after it switches, and slower terminals,
including ones answering over SSH, are picked up as long as they reply within a
second and a half. This works the same when Hunk reads a piped patch
(`git diff | hunk patch -`, `hunk pager`). If the terminal announces a switch
again while a probe is still answering, Hunk discards that probe and asks again,
so rapid switches settle on the final palette.

Terminals that do not answer palette queries get a standard xterm-style palette
for their light or dark background.
Expand Down
32 changes: 22 additions & 10 deletions packages/hunk/src/core/theme/detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,26 @@ const OSC_COLOR_REPLY_PATTERN = new RegExp(
`\\x1b\\](?:4;(\\d+)|(10)|(11));${OSC_COLOR_SPEC}${OSC_TERMINATOR}`,
"gi",
);
// Terminals answer queries in order, so the primary device attributes reply marks the point where
// every color reply the terminal was ever going to send has already arrived.
const DEVICE_ATTRIBUTES_QUERY = "\x1b[c";
const DEVICE_ATTRIBUTES_REPLY_PATTERN = /\x1b\[\?[\d;]*c/;
const DEVICE_ATTRIBUTES_REPLY_SOURCE = "\\x1b\\[\\?[\\d;]*c";
const DEVICE_ATTRIBUTES_REPLY_PATTERN = new RegExp(DEVICE_ATTRIBUTES_REPLY_SOURCE);
const DEVICE_ATTRIBUTES_REPLY_SEQUENCE_PATTERN = new RegExp(`^${DEVICE_ATTRIBUTES_REPLY_SOURCE}$`);

/**
* Primary device attributes (DA1) query. Terminals answer queries in order, so writing it after
* color queries makes its reply mark the point where every color reply the terminal was ever
* going to send has already arrived.
*/
export const DEVICE_ATTRIBUTES_QUERY = "\x1b[c";

/** OSC 10 (foreground), OSC 11 (background), and OSC 4 (16-color palette) queries, ST-terminated. */
export const TERMINAL_COLOR_QUERY =
"\x1b]10;?\x1b\\\x1b]11;?\x1b\\" +
Array.from({ length: ANSI_PALETTE_SIZE }, (_, index) => `\x1b]4;${index};?\x1b\\`).join("");

/** Return whether one parsed terminal input sequence is a DA1 reply such as `ESC [ ? 62 ; 22 c`. */
export function isDeviceAttributesReply(sequence: string) {
return DEVICE_ATTRIBUTES_REPLY_SEQUENCE_PATTERN.test(sequence);
}

/** Convert xterm-style OSC color channels into 8-bit RGB. */
function parseHexChannel(channel: string) {
Expand Down Expand Up @@ -121,7 +137,7 @@ export function themeModeForTerminalColors(colors: TerminalColors | null | undef
}

/** Return whether a probe learned anything at all about the terminal's colors. */
function hasTerminalColors(colors: TerminalColors) {
export function hasTerminalColors(colors: TerminalColors) {
return (
colors.foreground !== undefined ||
colors.background !== undefined ||
Expand Down Expand Up @@ -171,10 +187,6 @@ export async function detectTerminalColors({
input.resume?.();
input.on("data", onData);

const paletteQueries = Array.from(
{ length: ANSI_PALETTE_SIZE },
(_, index) => `\x1b]4;${index};?\x1b\\`,
).join("");
output.write(`\x1b]10;?\x1b\\\x1b]11;?\x1b\\${paletteQueries}${DEVICE_ATTRIBUTES_QUERY}`);
output.write(`${TERMINAL_COLOR_QUERY}${DEVICE_ATTRIBUTES_QUERY}`);
});
}
Loading
Loading