From 2ad21f6a5a8295c2eec38af847277568a89999ca Mon Sep 17 00:00:00 2001 From: German <106097211+4rchila@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:32:19 -0600 Subject: [PATCH] island: optional clock in the compact island Settings -> General -> Clock (off by default) shows the time and date, centred, in the compact island and keeps the island compact instead of hiding after a minute. Meant for setups whose top panel, and its clock, auto-hides under the island. The clock only ticks while it is visible (re-armed on the minute boundary), so a hidden island costs nothing. --- windows/src-tauri/src/settings.rs | 5 +++++ windows/src/core/state.ts | 3 +++ windows/src/island/fsm.ts | 16 ++++++++++++++++ windows/src/island/island.ts | 32 +++++++++++++++++++++++++++++++ windows/src/settings/main.ts | 5 +++++ windows/src/style.css | 20 +++++++++++++++++++ 6 files changed, 81 insertions(+) diff --git a/windows/src-tauri/src/settings.rs b/windows/src-tauri/src/settings.rs index 2a8d7d359..937e2efd1 100644 --- a/windows/src-tauri/src/settings.rs +++ b/windows/src-tauri/src/settings.rs @@ -20,6 +20,10 @@ pub struct Settings { /// Defaulted explicitly so a settings.json written by an older build still loads. #[serde(default = "default_model")] pub model: String, + /// Show the time and date in the island while it is compact or hidden, + /// for setups where the top panel (and its clock) auto-hides under it. + #[serde(default)] + pub show_clock: bool, } fn default_model() -> String { @@ -43,6 +47,7 @@ impl Default for Settings { autostart: false, hooks_installed: false, model: default_model(), + show_clock: false, } } } diff --git a/windows/src/core/state.ts b/windows/src/core/state.ts index 01236b80a..9de16652f 100644 --- a/windows/src/core/state.ts +++ b/windows/src/core/state.ts @@ -92,6 +92,8 @@ export interface Settings { hooksInstalled: boolean; /** Claude model used by the chat. */ model: string; + /** Time and date in the island while compact or hidden. */ + showClock: boolean; } export const DEFAULT_SETTINGS: Settings = { @@ -106,6 +108,7 @@ export const DEFAULT_SETTINGS: Settings = { autostart: false, hooksInstalled: false, model: "claude-opus-5", + showClock: false, }; type Listener = () => void; diff --git a/windows/src/island/fsm.ts b/windows/src/island/fsm.ts index 8a24545ed..4846d74a6 100644 --- a/windows/src/island/fsm.ts +++ b/windows/src/island/fsm.ts @@ -18,6 +18,8 @@ export class IslandStateMachine { greetHoverCollapseDelay = 10; /** An alert waiting for an answer stays open, even when the mouse leaves. */ pinned = false; + /** The clock is on: the compact island stays up instead of hiding. */ + private keepPetit = false; private petitHide: number | null = null; private homeCollapse: number | null = null; @@ -102,10 +104,24 @@ export class IslandStateMachine { this.transition("hidden"); } + /** Clock on → the island rests compact (it shows the time) and never hides by + * itself; clock off → back to hiding after the usual delay. */ + setKeepPetit(on: boolean, mouseInside: boolean) { + if (this.keepPetit === on) return; + this.keepPetit = on; + if (on) { + this.clear("petitHide"); + if (this.state === "hidden") this.transition("petit"); + } else if (this.state === "petit" && !mouseInside) { + this.schedulePetitHide(); + } + } + // ── Timers ────────────────────────────────────────────────────────────────── private schedulePetitHide() { this.clear("petitHide"); + if (this.keepPetit) return; this.petitHide = window.setTimeout(() => { this.petitHide = null; if (this.state === "petit") this.transition("hidden"); diff --git a/windows/src/island/island.ts b/windows/src/island/island.ts index 7fdb61802..f3fb0b935 100644 --- a/windows/src/island/island.ts +++ b/windows/src/island/island.ts @@ -73,6 +73,8 @@ export class Island { /** Last shape handed to Rust for the click-through test. */ private pushedRect = { x: -1, y: -1, w: -1, h: -1 }; private homeCollapseAt: number | null = null; + private clock!: HTMLElement; + private clockTimer: number | null = null; // Bot hover → love (IslandWindowController.botHoverIn) private botHovering = false; @@ -175,6 +177,7 @@ export class Island { this.greetingCanvas = h("canvas", { id: "greeting-canvas" }); this.miniGrid = h("div", { id: "mini-grid" }); this.countdown = h("div", { id: "countdown" }); + this.clock = h("div", { id: "clock" }); this.header = buildHeader(actions); this.views = buildViews(actions, () => this.animateGeometry(false)); @@ -209,6 +212,7 @@ export class Island { this.botCanvas, this.miniGrid, this.countdown, + this.clock, ); const dpr = Math.min(2, window.devicePixelRatio || 1); @@ -861,6 +865,8 @@ export class Island { } // Compact mini grid + this.syncClock(); + const showGrid = State.mode === "compact"; this.miniGrid.style.opacity = showGrid ? "1" : "0"; if (showGrid) { @@ -880,11 +886,37 @@ export class Island { this.engine.setState(State.effectiveState); } + /** Time and date in the compact island, refreshed only while it is shown. */ + private syncClock() { + const on = State.settings.showClock && State.mode === "compact"; + this.clock.style.opacity = on ? "1" : "0"; + if (on && this.clockTimer == null) { + const tick = () => { + const now = new Date(); + const time = now.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }); + const date = now.toLocaleDateString(undefined, { weekday: "short", day: "numeric" }); + this.clock.textContent = `${time} · ${date}`; + }; + tick(); + // Re-arm on the minute boundary, then every minute: cheap and never late. + const toNextMinute = 60_000 - (Date.now() % 60_000) + 50; + this.clockTimer = window.setTimeout(() => { + tick(); + this.clockTimer = window.setInterval(tick, 60_000); + }, toNextMinute); + } else if (!on && this.clockTimer != null) { + window.clearTimeout(this.clockTimer); + window.clearInterval(this.clockTimer); + this.clockTimer = null; + } + } + /** Applies settings coming from Rust at boot. */ applySettings() { Sound.setEnabled(State.settings.soundEnabled); Sound.setVolume(State.settings.soundVolume); this.fsm.homeToPetitDelay = State.settings.autoCloseInterval; + this.fsm.setKeepPetit(State.settings.showClock, this.wasInIsland); State.notify(); } diff --git a/windows/src/settings/main.ts b/windows/src/settings/main.ts index 3ab9ab94a..fab99c33d 100644 --- a/windows/src/settings/main.ts +++ b/windows/src/settings/main.ts @@ -406,6 +406,11 @@ function generalSection(): HTMLElement { autoClose, h("span", { class: "hint", text: "seconds after you leave the island" }), ), + h("div", { class: "row" }, + h("label", { text: "Clock" }), + toggle(settings.showClock, (v) => { settings.showClock = v; void save(); }), + h("span", { class: "hint", text: "time and date in the island while it rests" }), + ), h("div", { class: "row" }, h("label", { text: "Island lives on" }), screen, diff --git a/windows/src/style.css b/windows/src/style.css index b2cf6756c..9e423769c 100644 --- a/windows/src/style.css +++ b/windows/src/style.css @@ -141,6 +141,26 @@ body { transition: opacity 0.4s ease-in-out, background 0.4s ease-in-out; } +/* Clock in the compact island (Settings → Clock). */ +#clock { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font: 600 12.5px var(--font); + letter-spacing: 0.2px; + font-variant-numeric: tabular-nums; + color: var(--ink); + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.25s ease-in-out; +} + #countdown { position: absolute; bottom: 0;