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
6 changes: 6 additions & 0 deletions .changeset/retry-theme-palette-reads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@kitlangton/ghui": patch
---

retry terminal palette reads during theme reload signals and keep the existing
system theme when the terminal only returns incomplete palette data
62 changes: 56 additions & 6 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createRoot, useRenderer, useTerminalDimensions } from "@opentui/react"
import { Effect } from "effect"
import { useEffect, useState } from "react"
import { loadStoredSystemThemeAutoReload } from "./themeStore.js"
import { colors, setSystemThemeColors } from "./ui/colors.js"
import { colors, isHexColor, setSystemThemeColors } from "./ui/colors.js"
import { LoadingLogoPane } from "./ui/LoadingLogo.js"
import { SPINNER_INTERVAL_MS } from "./ui/spinner.js"

Expand All @@ -32,6 +32,23 @@ type AppBundle = {
}

let notifySystemThemeReload = () => {}
let signalTimer: ReturnType<typeof globalThis.setTimeout> | null = null
let reloadToken = 0

const SYSTEM_THEME_SIGNAL_DELAY_MS = 150
const SYSTEM_THEME_RETRY_DELAYS_MS = [150, 300] as const

const hasCompleteTerminalPalette = (terminalColors: {
readonly palette: readonly (string | null)[]
readonly defaultForeground: string | null
readonly defaultBackground: string | null
}) =>
isHexColor(terminalColors.defaultForeground) &&
isHexColor(terminalColors.defaultBackground) &&
terminalColors.palette.length >= 16 &&
terminalColors.palette.slice(0, 16).every(isHexColor)

const sleep = (delayMs: number) => new Promise<void>((resolve) => globalThis.setTimeout(resolve, delayMs))

const StartupLogo = () => {
const startupRenderer = useRenderer()
Expand Down Expand Up @@ -64,22 +81,55 @@ const renderer = await createCliRenderer({
},
})

const reloadSystemThemeColors = async () => {
const readSystemThemeColors = () => {
renderer.clearPaletteCache()
const terminalColors = await renderer.getPalette({ timeout: 150, size: 16 })
return renderer.getPalette({ timeout: 150, size: 16 })
}

const readThemeColorsAfterRetries = async () => {
let palette: Awaited<ReturnType<typeof readSystemThemeColors>> | null = null

const readNext = async () => {
const terminalColors = await readSystemThemeColors()
if (hasCompleteTerminalPalette(terminalColors)) palette = terminalColors
}

await readNext()
for (const delayMs of SYSTEM_THEME_RETRY_DELAYS_MS) {
await sleep(delayMs)
await readNext()
}

return palette
}

const reloadSystemThemeColors = async () => {
const terminalColors = await readSystemThemeColors()
setSystemThemeColors(terminalColors)
renderer.setBackgroundColor(colors.background)
notifySystemThemeReload()
}

const reloadSystemThemeColorsFromSignal = async () => {
const reloadSystemThemeColorsFromSignal = async (token: number) => {
const systemThemeAutoReload = await Effect.runPromise(loadStoredSystemThemeAutoReload)
if (token !== reloadToken) return
if (!systemThemeAutoReload) return
await reloadSystemThemeColors()
const terminalColors = await readThemeColorsAfterRetries()
if (token !== reloadToken) return
if (terminalColors === null) return
setSystemThemeColors(terminalColors)
renderer.setBackgroundColor(colors.background)
notifySystemThemeReload()
}

process.on("SIGUSR2", () => {
void reloadSystemThemeColorsFromSignal().catch(() => {})
reloadToken += 1
const token = reloadToken
if (signalTimer !== null) globalThis.clearTimeout(signalTimer)
signalTimer = globalThis.setTimeout(() => {
signalTimer = null
void reloadSystemThemeColorsFromSignal(token).catch(() => {})
}, SYSTEM_THEME_SIGNAL_DELAY_MS)
})

const Bootstrap = () => {
Expand Down
6 changes: 5 additions & 1 deletion src/ui/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ interface TerminalThemeColors {
readonly highlightForeground: string | null
}

const readableHex = (value: string | null | undefined, fallback: string) => (typeof value === "string" && /^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/.test(value) ? value : fallback)
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/

export const isHexColor = (value: string | null | undefined): value is string => typeof value === "string" && HEX_COLOR_RE.test(value)

const readableHex = (value: string | null | undefined, fallback: string) => (isHexColor(value) ? value : fallback)

const hexToRgb = (hex: string) => {
const value = hex.replace(/^#/, "").slice(0, 6)
Expand Down