From 68e1e3cba737dfa97a292d88e59f871173edc8db Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:21:52 +0530 Subject: [PATCH 01/18] feat(ui): add smooth blinking cursor and remove preview SVGs --- package.json | 1 - preview/browse.svg | 125 ------------------------ preview/downloads.svg | 158 ------------------------------- preview/splash.svg | 60 ------------ scripts/render-previews-impl.tsx | 19 +--- src/ui/components/TextField.tsx | 28 +++++- 6 files changed, 28 insertions(+), 363 deletions(-) delete mode 100644 preview/browse.svg delete mode 100644 preview/downloads.svg delete mode 100644 preview/splash.svg diff --git a/package.json b/package.json index 4c95cf6..bdbadbc 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ }, "files": [ "dist", - "preview", "scripts/ensure-webrtc.cjs" ], "engines": { diff --git a/preview/browse.svg b/preview/browse.svg deleted file mode 100644 index 951a1c8..0000000 --- a/preview/browse.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - - - torlink - - ๐“ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - All - โ•ญโ”€ - Search - โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - Games - - โฏ - Search or paste a magnet linkโ€ฆ - - Movies - โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - TV - Anime - โ•ญโ”€ - Latest (5) - โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - - newest across all sources - - Downloads - - - Seeding - - # - Name - Size - Seed:Lch - Src - - - โฏ - 1 - Oppenheimer (2023) [1080pโ€ฆ - 1.96 GB - 1240:88 - YTS - - - 2 - Dune: Part Two (2024) [21โ€ฆ - 7.82 GB - 910:41 - YTS - - - 3 - Breaking Bad S05E14 1080pโ€ฆ - 1.49 GB - 540:31 - EZTV - - - 4 - [Erai-raws] Jujutsu Kaiseโ€ฆ - 1.21 GB - 320:12 - NYAA - - - 5 - Frieren - 28 [1080p] - 1.30 GB - - - SUB - - โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - โ†‘โ†“โ†โ†’ - Move - d - Download - y - Copy - s - Sort - / - Search - tab - Switch - ? - Keys - - \ No newline at end of file diff --git a/preview/downloads.svg b/preview/downloads.svg deleted file mode 100644 index 55d6248..0000000 --- a/preview/downloads.svg +++ /dev/null @@ -1,158 +0,0 @@ - - - - - torlink - - ๐“ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - All - โ•ญโ”€ - Downloads (1) - โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - Games - - โฏ - โ†“ - Dune: Part Two (2024) [2160p BluRay] - 7.82 GB - YTS - - Movies - - - - - - - - - - - - - - - - โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ - 64% 7.7 MB/s โ€ข41 6m - - TV - - - Anime - - Recently downloaded (2) - - - โœ“ - Elden Ring: Shadow of tโ€ฆ - 50.29 GB - 1hr ago - FG - - - Downloads - (1) - - โœ“ - Breaking Bad S05E14 108โ€ฆ - 1.49 GB - 1d 1hr ago - EZTV - - Seeding - - - - - โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - p - Pause - c - Cancel - e - Folder - s - Export - tab - Switch - ? - Keys - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/preview/splash.svg b/preview/splash.svg deleted file mode 100644 index bd904ab..0000000 --- a/preview/splash.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - - torlink - - ๐“ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A curated, terminal-native torrent downloader. - games ยท movies ยท tv ยท anime - โ•ญโ”€ - Search - โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - - โฏ - - S - earch or paste a magnet linkโ€ฆ - - โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - โ†ต - search ยท empty - โ†ต - browse ยท - ^c - quit - - \ No newline at end of file diff --git a/scripts/render-previews-impl.tsx b/scripts/render-previews-impl.tsx index 5c16724..02cd8e9 100644 --- a/scripts/render-previews-impl.tsx +++ b/scripts/render-previews-impl.tsx @@ -160,27 +160,17 @@ function save( } writeFileSync( join(OUT_DIR, `${name}.svg`), - ansiToSvg(frame, { cols: COLS, title: "torlink", ...extra }), + ansiToSvg(frame, { cols: COLS, title: "torhunt", ...extra }), ); console.log(`preview/${name}.svg`); } -const CATEGORIES = sourcesByGroup() - .map((g) => g.group.toLowerCase()) - .join(` ${ICON.dot} `); - save( "splash", makeStore({ view: "splash", region: "content" }), - + - - A curated, terminal-native torrent downloader. - - - {CATEGORIES} - - + {}} /> @@ -188,8 +178,7 @@ save( โ†ต search {` ${ICON.dot} `} - empty - โ†ต + โ‡ฅ browse {` ${ICON.dot} `} ^c diff --git a/src/ui/components/TextField.tsx b/src/ui/components/TextField.tsx index 2888555..d9233ad 100644 --- a/src/ui/components/TextField.tsx +++ b/src/ui/components/TextField.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Text, useInput } from "ink"; export interface TextFieldProps { @@ -86,12 +86,22 @@ export function TextField({ const [value, setValue] = useState(defaultValue); const [cursor, setCursor] = useState(defaultValue.length); const [historyIdx, setHistoryIdx] = useState(-1); + const [cursorVisible, setCursorVisible] = useState(true); const draftRef = useRef(defaultValue); + useEffect(() => { + if (isDisabled) return; + const interval = setInterval(() => { + setCursorVisible((v) => !v); + }, 530); + return () => clearInterval(interval); + }, [isDisabled]); + function apply(next: Edit): void { setHistoryIdx(-1); setValue(next.value); setCursor(Math.max(0, Math.min(next.value.length, next.cursor))); + setCursorVisible(true); if (next.value !== value) onChange?.(next.value); } @@ -149,10 +159,12 @@ export function TextField({ } if (key.home) { + setCursorVisible(true); setCursor(0); return; } if (key.end) { + setCursorVisible(true); setCursor(value.length); return; } @@ -161,6 +173,7 @@ export function TextField({ // named keys arrive with an empty input, so they'd hit its default arm // and vanish. if (key.leftArrow) { + setCursorVisible(true); if (key.ctrl || key.meta) { setCursor(wordLeft(value, cursor)); return; @@ -173,6 +186,7 @@ export function TextField({ return; } if (key.rightArrow) { + setCursorVisible(true); if (key.ctrl || key.meta) { setCursor(wordRight(value, cursor)); return; @@ -201,9 +215,11 @@ export function TextField({ apply(killToEnd(value, cursor)); return; case "a": + setCursorVisible(true); setCursor(0); return; case "e": + setCursorVisible(true); setCursor(value.length); return; // Every other ctrl combo is swallowed so views behind the field @@ -238,12 +254,16 @@ export function TextField({ if (placeholder) { return ( - {placeholder[0]} + {cursorVisible ? ( + {placeholder[0]} + ) : ( + {placeholder[0]} + )} {placeholder.slice(1)} ); } - return {CURSOR}; + return cursorVisible ? {CURSOR} : {CURSOR}; } // Compute a viewport window that keeps the cursor visible. @@ -267,7 +287,7 @@ export function TextField({ return ( {before} - {atChar} + {cursorVisible ? {atChar} : {atChar}} {after} ); From 07ef9a54fd16142c25fbd62567b47e08e2c68fba Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:01:12 +0530 Subject: [PATCH 02/18] 1.9.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ecd3c8..d3f5c8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "torlnk", - "version": "1.9.0", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "torlnk", - "version": "1.9.0", + "version": "1.9.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index bdbadbc..10ac48d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "torhunt", - "version": "1.9.0", + "version": "1.9.1", "description": "A sleek, zero-setup torrent finder and downloader that lives right in your terminal.", "type": "module", "bin": { From 34f9c9359a00538279b34df4eb7735f501bc0ed8 Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:19:05 +0530 Subject: [PATCH 03/18] fix(package): normalize bin script path to avoid npm publish warning --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10ac48d..903c8c7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A sleek, zero-setup torrent finder and downloader that lives right in your terminal.", "type": "module", "bin": { - "torhunt": "./dist/cli.cjs" + "torhunt": "dist/cli.cjs" }, "files": [ "dist", From d328b90929684a818a4d7bb31b7f1273e8d9855c Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:09:55 +0530 Subject: [PATCH 04/18] feat(notifications): add native OS desktop alerts on download completion --- scripts/render-previews-impl.tsx | 1 + src/config/config.ts | 6 +++ src/ui/App.tsx | 4 ++ src/ui/components/SettingsView.test.tsx | 1 + src/ui/components/SettingsView.tsx | 31 ++++++++++++-- src/ui/testHarness.ts | 1 + src/util/notify.test.ts | 10 +++++ src/util/notify.ts | 54 +++++++++++++++++++++++++ 8 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/util/notify.test.ts create mode 100644 src/util/notify.ts diff --git a/scripts/render-previews-impl.tsx b/scripts/render-previews-impl.tsx index 02cd8e9..e80e0a4 100644 --- a/scripts/render-previews-impl.tsx +++ b/scripts/render-previews-impl.tsx @@ -89,6 +89,7 @@ function makeStore( trackers: [], preventSleep: true, onComplete: "none", + notifyOnComplete: true, } as Config, setConfig: noop, theme: DEFAULT_THEME, diff --git a/src/config/config.ts b/src/config/config.ts index 6e3ae30..a5f5e9f 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -11,6 +11,7 @@ export interface Config { spinner: string; preventSleep: boolean; onComplete: OnCompleteAction; + notifyOnComplete: boolean; } export const defaultConfig: Config = { @@ -20,6 +21,7 @@ export const defaultConfig: Config = { spinner: "meter", preventSleep: true, onComplete: "none", + notifyOnComplete: true, }; export async function loadConfig(): Promise { @@ -52,6 +54,10 @@ export async function loadConfig(): Promise { parsed.onComplete === "sleep" || parsed.onComplete === "shutdown" || parsed.onComplete === "none" ? parsed.onComplete : defaultConfig.onComplete, + notifyOnComplete: + typeof parsed.notifyOnComplete === "boolean" + ? parsed.notifyOnComplete + : defaultConfig.notifyOnComplete, }; return cfg; } catch { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index fae2f07..09c47b1 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -32,6 +32,7 @@ import { triggerSleep, triggerShutdown, } from "../util/power"; +import { sendNotification } from "../util/notify"; import { StoreContext, type CaptureMode, @@ -249,6 +250,9 @@ export function App({ const onCompleted = (name: string): void => { setNotice(`${ICON.done} ${truncate(cleanText(name), 40)}`); + if (config.notifyOnComplete ?? true) { + sendNotification("torhunt โ€” Download Complete", cleanText(name)); + } if (queue.activeCount === 0 && queue.getItems().length === 0) { releaseKeepAwake(); if (config.onComplete === "sleep") { diff --git a/src/ui/components/SettingsView.test.tsx b/src/ui/components/SettingsView.test.tsx index 226004e..8a627b6 100644 --- a/src/ui/components/SettingsView.test.tsx +++ b/src/ui/components/SettingsView.test.tsx @@ -19,6 +19,7 @@ describe("SettingsView", () => { expect(ui.frame()).toContain("Color Theme:"); expect(ui.frame()).toContain("Spinner Style:"); expect(ui.frame()).toContain("Stay Awake:"); + expect(ui.frame()).toContain("Desktop Alerts:"); expect(ui.frame()).toContain("On Queue Finish:"); expect(ui.frame()).toContain("v1."); ui.unmount(); diff --git a/src/ui/components/SettingsView.tsx b/src/ui/components/SettingsView.tsx index ae3f1b3..a62c679 100644 --- a/src/ui/components/SettingsView.tsx +++ b/src/ui/components/SettingsView.tsx @@ -38,6 +38,7 @@ export function SettingsView() { { id: "theme", label: "Color Theme" }, { id: "spinner", label: "Spinner Loader" }, { id: "preventSleep", label: "Stay Awake" }, + { id: "notifyOnComplete", label: "Desktop Alerts" }, { id: "onComplete", label: "When Finished" }, ]; @@ -49,6 +50,14 @@ export function SettingsView() { setNotice(nextVal ? "Stay Awake enabled: OS will not sleep while downloading" : "Stay Awake disabled: Standard OS sleep enabled"); }; + const toggleNotifyOnComplete = () => { + const nextVal = !(config.notifyOnComplete ?? true); + const nextCfg = { ...config, notifyOnComplete: nextVal }; + setConfig(nextCfg); + saveConfig(nextCfg); + setNotice(nextVal ? "Desktop Alerts enabled: OS notification when download completes" : "Desktop Alerts disabled"); + }; + const cycleOnComplete = () => { const current = config.onComplete ?? "none"; const next: "none" | "sleep" | "shutdown" = @@ -92,6 +101,8 @@ export function SettingsView() { openSpinnerPicker(); } else if (item?.id === "preventSleep") { togglePreventSleep(); + } else if (item?.id === "notifyOnComplete") { + toggleNotifyOnComplete(); } else if (item?.id === "onComplete") { cycleOnComplete(); } @@ -180,11 +191,25 @@ export function SettingsView() { - {/* Item 4: On Complete */} + {/* Item 4: Desktop Alerts */} - {selectedIdx === 4 && focused ? "โ†’ " : " "}On Queue Finish: + {selectedIdx === 4 && focused ? "โ†’ " : " "}Desktop Alerts: + + + + + {(config.notifyOnComplete ?? true) ? "[Enabled]" : "[Disabled]"} + + + + + {/* Item 5: On Complete */} + + + + {selectedIdx === 5 && focused ? "โ†’ " : " "}On Queue Finish: @@ -209,7 +234,7 @@ export function SettingsView() { - Press โ†ต to edit folder, pick theme/spinner, toggle stay awake, or cycle finish action. + Press โ†ต to edit folder, pick theme/spinner, toggle stay awake/alerts, or cycle finish action. diff --git a/src/ui/testHarness.ts b/src/ui/testHarness.ts index c168a4f..73ad684 100644 --- a/src/ui/testHarness.ts +++ b/src/ui/testHarness.ts @@ -150,6 +150,7 @@ export function makeTestStore(overrides: Partial = {}): Store { trackers: [], preventSleep: true, onComplete: "none", + notifyOnComplete: true, } as Config, setConfig: noop, theme: DEFAULT_THEME, diff --git a/src/util/notify.test.ts b/src/util/notify.test.ts new file mode 100644 index 0000000..7ed5675 --- /dev/null +++ b/src/util/notify.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest"; +import { sendNotification } from "./notify"; + +describe("sendNotification utility", () => { + it("executes sendNotification without throwing errors or crashing", () => { + expect(() => { + sendNotification("torhunt", "Test notification message"); + }).not.toThrow(); + }); +}); diff --git a/src/util/notify.ts b/src/util/notify.ts new file mode 100644 index 0000000..9b7ff8e --- /dev/null +++ b/src/util/notify.ts @@ -0,0 +1,54 @@ +import { spawn } from "node:child_process"; +import os from "node:os"; + +/** + * Sends a native OS desktop notification. + * - Windows: Uses PowerShell ToastNotification API with App ID 'torhunt' + * - macOS: Uses AppleScript display notification + * - Linux: Uses notify-send + */ +export function sendNotification(title: string, message: string): void { + const platform = os.platform(); + const safeTitle = title.replace(/["'\\]/g, ""); + const safeMessage = message.replace(/["'\\]/g, ""); + + try { + if (platform === "win32") { + // Windows 10/11 native XML Toast Notification + const script = ` + [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null + [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null + $template = @" + + + + ${safeTitle} + ${safeMessage} + + + +"@ + $xml = New-Object Windows.Data.Xml.Dom.XmlDocument + $xml.LoadXml($template) + $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) + [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("torhunt").Show($toast) + `; + spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { + windowsHide: true, + stdio: "ignore", + }).unref(); + } else if (platform === "darwin") { + spawn( + "osascript", + ["-e", `display notification "${safeMessage}" with title "${safeTitle}"`], + { stdio: "ignore" }, + ).unref(); + } else if (platform === "linux") { + spawn("notify-send", [safeTitle, safeMessage], { + stdio: "ignore", + }).unref(); + } + } catch { + // Ignore if OS notification daemon is unavailable + } +} From dab972624be2fd4157720f66b978d0917ec1d7ab Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:35:54 +0530 Subject: [PATCH 05/18] fix(notifications): encode powershell script and use registered AppID for Windows toast --- src/util/notify.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/util/notify.ts b/src/util/notify.ts index 9b7ff8e..9751b89 100644 --- a/src/util/notify.ts +++ b/src/util/notify.ts @@ -16,9 +16,9 @@ export function sendNotification(title: string, message: string): void { if (platform === "win32") { // Windows 10/11 native XML Toast Notification const script = ` - [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null - [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null - $template = @" +[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null +[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null +$template = @" @@ -28,12 +28,14 @@ export function sendNotification(title: string, message: string): void { "@ - $xml = New-Object Windows.Data.Xml.Dom.XmlDocument - $xml.LoadXml($template) - $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) - [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("torhunt").Show($toast) - `; - spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { +$xml = New-Object Windows.Data.Xml.Dom.XmlDocument +$xml.LoadXml($template) +$toast = New-Object Windows.UI.Notifications.ToastNotification $xml +$appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe' +[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast) +`; + const encoded = Buffer.from(script, "utf16le").toString("base64"); + spawn("powershell", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], { windowsHide: true, stdio: "ignore", }).unref(); From ccd6cc8b51a8017c041ab1c4c5d306688a3abb08 Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:41:25 +0530 Subject: [PATCH 06/18] feat(notifications): add native macOS and Linux notification support with sound and app tags --- src/util/notify.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/util/notify.ts b/src/util/notify.ts index 9751b89..f2647e7 100644 --- a/src/util/notify.ts +++ b/src/util/notify.ts @@ -35,20 +35,33 @@ $appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\power [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast) `; const encoded = Buffer.from(script, "utf16le").toString("base64"); - spawn("powershell", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], { + const child = spawn("powershell", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], { windowsHide: true, stdio: "ignore", - }).unref(); + }); + child.on("error", () => {}); + child.unref(); } else if (platform === "darwin") { - spawn( + // macOS: Native AppleScript notification with system sound tone + const child = spawn( "osascript", - ["-e", `display notification "${safeMessage}" with title "${safeTitle}"`], + ["-e", `display notification "${safeMessage}" with title "${safeTitle}" sound name "Glass"`], { stdio: "ignore" }, - ).unref(); + ); + child.on("error", () => {}); + child.unref(); } else if (platform === "linux") { - spawn("notify-send", [safeTitle, safeMessage], { + // Linux: notify-send with -a torhunt app tag and normal urgency + const child = spawn("notify-send", ["-a", "torhunt", "-u", "normal", safeTitle, safeMessage], { stdio: "ignore", - }).unref(); + }); + child.on("error", () => { + // Fallback for older libnotify versions without -a flag + const fallback = spawn("notify-send", [safeTitle, safeMessage], { stdio: "ignore" }); + fallback.on("error", () => {}); + fallback.unref(); + }); + child.unref(); } } catch { // Ignore if OS notification daemon is unavailable From a31b4dfba77a69d37905f0a8871111aed2cb38d0 Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:47:08 +0530 Subject: [PATCH 07/18] fix(notifications): sanitize XML special characters and parentheses for PowerShell toast --- src/util/notify.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/notify.ts b/src/util/notify.ts index f2647e7..227247c 100644 --- a/src/util/notify.ts +++ b/src/util/notify.ts @@ -9,8 +9,8 @@ import os from "node:os"; */ export function sendNotification(title: string, message: string): void { const platform = os.platform(); - const safeTitle = title.replace(/["'\\]/g, ""); - const safeMessage = message.replace(/["'\\]/g, ""); + const safeTitle = title.replace(/[<>&"'\\]/g, "").replace(/[\$()]/g, ""); + const safeMessage = message.replace(/[<>&"'\\]/g, "").replace(/[\$()]/g, ""); try { if (platform === "win32") { From 1e8e4c1a6cbc88b38467c3b87b1626156789af3e Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:50:32 +0530 Subject: [PATCH 08/18] feat(cli): add notify-test subcommand for instant cross-platform desktop alert testing --- src/cli/args.ts | 3 +++ src/index.tsx | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/cli/args.ts b/src/cli/args.ts index 4754d63..6930bf4 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -26,6 +26,7 @@ export type CliCommand = | { kind: "files"; port?: number; host?: string; token?: string; dir?: string; daemon?: boolean } | { kind: "attach" } | { kind: "update"; force?: boolean } + | { kind: "notify-test" } | { kind: "invalid"; arg: string }; // Valueless boolean flags for the headless subcommands (everything else is a @@ -76,6 +77,7 @@ export function parseCliArgs(argv: string[]): CliCommand { if (a === "--version" || a === "-v") return { kind: "version" }; if (a === "--help" || a === "-h") return { kind: "help" }; if (a === "attach") return { kind: "attach" }; + if (a === "notify-test" || a === "--notify-test") return { kind: "notify-test" }; if (a === "update") return { kind: "update", force: args.slice(1).includes("--force") }; if (a === "watch") { const { bools, rest: r0 } = splitBooleans(args.slice(1)); @@ -135,6 +137,7 @@ usage torhunt attach open/reattach the TUI in a persistent tmux session torhunt update [--force] update to the latest release and restart any daemon (--force rebuilds/restarts even if already current) + torhunt notify-test test native OS desktop notifications torhunt --version print the version torhunt --help print this help message diff --git a/src/index.tsx b/src/index.tsx index 49dbad6..0a88d09 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -74,6 +74,12 @@ if (cmd.kind === "update") { dir: cmd.dir, }; void import("./daemon/files").then(({ runFiles }) => runFiles(options).catch(failHeadless)); +} else if (cmd.kind === "notify-test") { + void import("./util/notify").then(({ sendNotification }) => { + sendNotification("torhunt โ€” Test Notification", "Desktop notifications are working cleanly!"); + console.log("โœ” Notification test dispatched to your OS."); + process.exit(0); + }); } else { // Enter the alt-screen and hide the hardware cursor: the TUI draws its own From 3e5cc1bc7dda28afe27bad76ff425e1d36c1f7da Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:06:04 +0530 Subject: [PATCH 09/18] fix(notifications): use System.Windows.Forms NotifyIcon balloon tip and SystemSound for reliable Windows notifications --- src/util/notify.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/util/notify.ts b/src/util/notify.ts index 227247c..26ab246 100644 --- a/src/util/notify.ts +++ b/src/util/notify.ts @@ -14,25 +14,19 @@ export function sendNotification(title: string, message: string): void { try { if (platform === "win32") { - // Windows 10/11 native XML Toast Notification + // Windows 10/11 system tray notification balloon with OS audio chime const script = ` -[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null -[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null -$template = @" - - - - ${safeTitle} - ${safeMessage} - - - -"@ -$xml = New-Object Windows.Data.Xml.Dom.XmlDocument -$xml.LoadXml($template) -$toast = New-Object Windows.UI.Notifications.ToastNotification $xml -$appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe' -[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast) +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing +try { [System.Media.SystemSounds]::Asterisk.Play() } catch {} +$icon = [System.Drawing.SystemIcons]::Information +$notify = New-Object System.Windows.Forms.NotifyIcon +$notify.Icon = $icon +$notify.Text = "torhunt" +$notify.Visible = $true +$notify.ShowBalloonTip(5000, "${safeTitle}", "${safeMessage}", [System.Windows.Forms.ToolTipIcon]::Info) +Start-Sleep -s 5 +$notify.Dispose() `; const encoded = Buffer.from(script, "utf16le").toString("base64"); const child = spawn("powershell", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], { From 3834cafb18ded29a7caeda570ddc69fe92702536 Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:28:51 +0530 Subject: [PATCH 10/18] fix(notifications): spawn notification processes detached so short-lived CLI calls do not kill them --- src/util/notify.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/util/notify.ts b/src/util/notify.ts index 26ab246..87f5dd5 100644 --- a/src/util/notify.ts +++ b/src/util/notify.ts @@ -30,6 +30,7 @@ $notify.Dispose() `; const encoded = Buffer.from(script, "utf16le").toString("base64"); const child = spawn("powershell", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], { + detached: true, windowsHide: true, stdio: "ignore", }); @@ -40,18 +41,19 @@ $notify.Dispose() const child = spawn( "osascript", ["-e", `display notification "${safeMessage}" with title "${safeTitle}" sound name "Glass"`], - { stdio: "ignore" }, + { detached: true, stdio: "ignore" }, ); child.on("error", () => {}); child.unref(); } else if (platform === "linux") { // Linux: notify-send with -a torhunt app tag and normal urgency const child = spawn("notify-send", ["-a", "torhunt", "-u", "normal", safeTitle, safeMessage], { + detached: true, stdio: "ignore", }); child.on("error", () => { // Fallback for older libnotify versions without -a flag - const fallback = spawn("notify-send", [safeTitle, safeMessage], { stdio: "ignore" }); + const fallback = spawn("notify-send", [safeTitle, safeMessage], { detached: true, stdio: "ignore" }); fallback.on("error", () => {}); fallback.unref(); }); From 2404e64279735766e5835a58d63fa4dce0bc6bee Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:43:39 +0530 Subject: [PATCH 11/18] feat(notifications): trigger desktop notifications on download start and completion --- src/download/queue.ts | 2 ++ src/ui/App.tsx | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/download/queue.ts b/src/download/queue.ts index d80d619..575a463 100644 --- a/src/download/queue.ts +++ b/src/download/queue.ts @@ -153,6 +153,7 @@ export class DownloadQueue extends EventEmitter { if (start) { this.startEngine(item); this.ensurePoll(); + this.emit("started", item.name); } this.changed(); void this.persist(); @@ -194,6 +195,7 @@ export class DownloadQueue extends EventEmitter { next.status = "downloading"; next.speed = 0; this.startEngine(next); + this.emit("started", next.name); started = true; } if (started) { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 09c47b1..a4647cb 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -248,6 +248,13 @@ export function App({ updatePowerState(); queue.on("change", updatePowerState); + const onStarted = (name: string): void => { + if (config.notifyOnComplete ?? true) { + sendNotification("torhunt โ€” Download Started", cleanText(name)); + } + }; + queue.on("started", onStarted); + const onCompleted = (name: string): void => { setNotice(`${ICON.done} ${truncate(cleanText(name), 40)}`); if (config.notifyOnComplete ?? true) { @@ -266,6 +273,7 @@ export function App({ return () => { queue.off("change", updatePowerState); + queue.off("started", onStarted); queue.off("completed", onCompleted); releaseKeepAwake(); }; From bb5c59fe7fbe35436918e6f2065658ed30424a89 Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:04:15 +0530 Subject: [PATCH 12/18] fix(notifications): use Windows.SystemToast.Notification AppID for reliable native Windows 10/11 toast popups --- src/util/notify.ts | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/util/notify.ts b/src/util/notify.ts index 87f5dd5..4a04f66 100644 --- a/src/util/notify.ts +++ b/src/util/notify.ts @@ -14,23 +14,29 @@ export function sendNotification(title: string, message: string): void { try { if (platform === "win32") { - // Windows 10/11 system tray notification balloon with OS audio chime + // Windows 10/11 native WinRT Toast Notification const script = ` -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing -try { [System.Media.SystemSounds]::Asterisk.Play() } catch {} -$icon = [System.Drawing.SystemIcons]::Information -$notify = New-Object System.Windows.Forms.NotifyIcon -$notify.Icon = $icon -$notify.Text = "torhunt" -$notify.Visible = $true -$notify.ShowBalloonTip(5000, "${safeTitle}", "${safeMessage}", [System.Windows.Forms.ToolTipIcon]::Info) -Start-Sleep -s 5 -$notify.Dispose() +[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null +[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null +$template = @" + + + + ${safeTitle} + ${safeMessage} + + + +"@ +$xml = New-Object Windows.Data.Xml.Dom.XmlDocument +$xml.LoadXml($template) +$toast = New-Object Windows.UI.Notifications.ToastNotification $xml +$appId = 'Windows.SystemToast.Notification' +$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId) +$notifier.Show($toast) `; const encoded = Buffer.from(script, "utf16le").toString("base64"); const child = spawn("powershell", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], { - detached: true, windowsHide: true, stdio: "ignore", }); From d3a774d1a3d072bd993563e97e74c910f68adbee Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:33:15 +0530 Subject: [PATCH 13/18] feat(ui): add retro loader animations (Retro Cassette, CRT Scanline, 8-Bit Arcade, Synthwave Grid) --- src/ui/spinnerPresets.test.ts | 4 ++++ src/ui/spinnerPresets.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/ui/spinnerPresets.test.ts b/src/ui/spinnerPresets.test.ts index 0d1fa0a..38ee193 100644 --- a/src/ui/spinnerPresets.test.ts +++ b/src/ui/spinnerPresets.test.ts @@ -24,5 +24,9 @@ describe("spinnerPresets", () => { expect(getSpinner("non-existent")).toEqual(DEFAULT_SPINNER); expect(getSpinner("radar").id).toBe("radar"); expect(getSpinner("baton").id).toBe("baton"); + expect(getSpinner("cassette").id).toBe("cassette"); + expect(getSpinner("crt").id).toBe("crt"); + expect(getSpinner("arcade").id).toBe("arcade"); + expect(getSpinner("synthwave").id).toBe("synthwave"); }); }); diff --git a/src/ui/spinnerPresets.ts b/src/ui/spinnerPresets.ts index 7101b8c..add71ef 100644 --- a/src/ui/spinnerPresets.ts +++ b/src/ui/spinnerPresets.ts @@ -42,6 +42,34 @@ export const SPINNERS: readonly SpinnerPreset[] = [ frames: ["โ–ฐโ–ฑโ–ฑ", "โ–ฐโ–ฐโ–ฑ", "โ–ฐโ–ฐโ–ฐ", "โ–ฑโ–ฐโ–ฐ", "โ–ฑโ–ฑโ–ฐ", "โ–ฑโ–ฑโ–ฑ"], intervalMs: 100, }, + { + id: "cassette", + name: "Retro Cassette", + description: "Analog cassette tape reel spinning", + frames: ["[ โ— โ—ฏ ]", "[ โ—“ โ—ฏ ]", "[ โ—‘ โ—ฏ ]", "[ โ—’ โ—ฏ ]"], + intervalMs: 100, + }, + { + id: "crt", + name: "CRT Scanline", + description: "Retro CRT monitor phosphor sweep", + frames: ["โ–‘โ–’โ–“โ–ˆโ–“โ–’โ–‘", "โ–’โ–“โ–ˆโ–“โ–’โ–‘โ–‘", "โ–“โ–ˆโ–“โ–’โ–‘โ–‘โ–‘", "โ–ˆโ–“โ–’โ–‘โ–‘โ–‘โ–“", "โ–“โ–’โ–‘โ–‘โ–‘โ–“โ–ˆ", "โ–’โ–‘โ–‘โ–‘โ–“โ–ˆโ–“"], + intervalMs: 80, + }, + { + id: "arcade", + name: "Retro Arcade", + description: "8-bit arcade bounce scanner", + frames: ["[C ]", "[ C ]", "[ C ]", "[ C ]", "[ C ]", "[ C]", "[ C ]", "[ C ]", "[ C ]", "[ C ]"], + intervalMs: 80, + }, + { + id: "synthwave", + name: "Synthwave Grid", + description: "Neon synthwave neon pulse grid", + frames: ["โ–ฒ โ–ณ โ–ณ โ–ณ", "โ–ณ โ–ฒ โ–ณ โ–ณ", "โ–ณ โ–ณ โ–ฒ โ–ณ", "โ–ณ โ–ณ โ–ณ โ–ฒ", "โ–ณ โ–ณ โ–ฒ โ–ณ", "โ–ณ โ–ฒ โ–ณ โ–ณ"], + intervalMs: 90, + }, ] as const; export const DEFAULT_SPINNER = SPINNERS.find((s) => s.id === "meter") ?? SPINNERS[0]!; From ae6a6d2373ac2c144e89a5baecfefb9e1c12925a Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:38:58 +0530 Subject: [PATCH 14/18] feat(ui): add Retro Pac-Man spinner, keep CRT Scanline, and remove cassette/arcade/synthwave --- src/ui/spinnerPresets.test.ts | 4 +--- src/ui/spinnerPresets.ts | 24 +++++------------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/ui/spinnerPresets.test.ts b/src/ui/spinnerPresets.test.ts index 38ee193..4905c96 100644 --- a/src/ui/spinnerPresets.test.ts +++ b/src/ui/spinnerPresets.test.ts @@ -24,9 +24,7 @@ describe("spinnerPresets", () => { expect(getSpinner("non-existent")).toEqual(DEFAULT_SPINNER); expect(getSpinner("radar").id).toBe("radar"); expect(getSpinner("baton").id).toBe("baton"); - expect(getSpinner("cassette").id).toBe("cassette"); expect(getSpinner("crt").id).toBe("crt"); - expect(getSpinner("arcade").id).toBe("arcade"); - expect(getSpinner("synthwave").id).toBe("synthwave"); + expect(getSpinner("pacman").id).toBe("pacman"); }); }); diff --git a/src/ui/spinnerPresets.ts b/src/ui/spinnerPresets.ts index add71ef..1f93538 100644 --- a/src/ui/spinnerPresets.ts +++ b/src/ui/spinnerPresets.ts @@ -42,13 +42,6 @@ export const SPINNERS: readonly SpinnerPreset[] = [ frames: ["โ–ฐโ–ฑโ–ฑ", "โ–ฐโ–ฐโ–ฑ", "โ–ฐโ–ฐโ–ฐ", "โ–ฑโ–ฐโ–ฐ", "โ–ฑโ–ฑโ–ฐ", "โ–ฑโ–ฑโ–ฑ"], intervalMs: 100, }, - { - id: "cassette", - name: "Retro Cassette", - description: "Analog cassette tape reel spinning", - frames: ["[ โ— โ—ฏ ]", "[ โ—“ โ—ฏ ]", "[ โ—‘ โ—ฏ ]", "[ โ—’ โ—ฏ ]"], - intervalMs: 100, - }, { id: "crt", name: "CRT Scanline", @@ -57,18 +50,11 @@ export const SPINNERS: readonly SpinnerPreset[] = [ intervalMs: 80, }, { - id: "arcade", - name: "Retro Arcade", - description: "8-bit arcade bounce scanner", - frames: ["[C ]", "[ C ]", "[ C ]", "[ C ]", "[ C ]", "[ C]", "[ C ]", "[ C ]", "[ C ]", "[ C ]"], - intervalMs: 80, - }, - { - id: "synthwave", - name: "Synthwave Grid", - description: "Neon synthwave neon pulse grid", - frames: ["โ–ฒ โ–ณ โ–ณ โ–ณ", "โ–ณ โ–ฒ โ–ณ โ–ณ", "โ–ณ โ–ณ โ–ฒ โ–ณ", "โ–ณ โ–ณ โ–ณ โ–ฒ", "โ–ณ โ–ณ โ–ฒ โ–ณ", "โ–ณ โ–ฒ โ–ณ โ–ณ"], - intervalMs: 90, + id: "pacman", + name: "Retro Pac-Man", + description: "8-bit Pac-Man chomping dots", + frames: ["C โ€ข โ€ข โ€ข", "c โ€ข โ€ข โ€ข", " C โ€ข โ€ข", " c โ€ข โ€ข", " C โ€ข", " c โ€ข", " C", " c"], + intervalMs: 110, }, ] as const; From 222fca210bf943454fbab770a078bdfd016f457a Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:41:28 +0530 Subject: [PATCH 15/18] feat(ui): update Pac-Man loader to stationary chomper eating incoming dots --- src/ui/spinnerPresets.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ui/spinnerPresets.ts b/src/ui/spinnerPresets.ts index 1f93538..97d2e34 100644 --- a/src/ui/spinnerPresets.ts +++ b/src/ui/spinnerPresets.ts @@ -52,9 +52,14 @@ export const SPINNERS: readonly SpinnerPreset[] = [ { id: "pacman", name: "Retro Pac-Man", - description: "8-bit Pac-Man chomping dots", - frames: ["C โ€ข โ€ข โ€ข", "c โ€ข โ€ข โ€ข", " C โ€ข โ€ข", " c โ€ข โ€ข", " C โ€ข", " c โ€ข", " C", " c"], - intervalMs: 110, + description: "Stationary Pac-Man chomping incoming dots", + frames: [ + "C โ€ข โ€ข โ€ข", + "c โ€ข โ€ข", + "C โ€ข", + "c ", + ], + intervalMs: 120, }, ] as const; From b52788e75ae920704ed3f5d1f9b1da7f4fb0b507 Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:45:06 +0530 Subject: [PATCH 16/18] feat(ui): update Dot Matrix LED spinner with a 25-dot wave animation grid --- src/ui/spinnerPresets.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/ui/spinnerPresets.ts b/src/ui/spinnerPresets.ts index 97d2e34..b8faf56 100644 --- a/src/ui/spinnerPresets.ts +++ b/src/ui/spinnerPresets.ts @@ -50,16 +50,21 @@ export const SPINNERS: readonly SpinnerPreset[] = [ intervalMs: 80, }, { - id: "pacman", - name: "Retro Pac-Man", - description: "Stationary Pac-Man chomping incoming dots", + id: "matrix-dot", + name: "Dot Matrix LED", + description: "25-dot LED matrix wave and pulse grid", frames: [ - "C โ€ข โ€ข โ€ข", - "c โ€ข โ€ข", - "C โ€ข", - "c ", + "โ— โ—‹ โ—‹ โ—‹ โ—‹", + "โ— โ— โ—‹ โ—‹ โ—‹", + "โ— โ— โ— โ—‹ โ—‹", + "โ— โ— โ— โ— โ—‹", + "โ— โ— โ— โ— โ—", + "โ—‹ โ— โ— โ— โ—", + "โ—‹ โ—‹ โ— โ— โ—", + "โ—‹ โ—‹ โ—‹ โ— โ—", + "โ—‹ โ—‹ โ—‹ โ—‹ โ—", ], - intervalMs: 120, + intervalMs: 70, }, ] as const; From 3484f3b06b9d56e532f6ada5cfb01e3a0f18e249 Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:45:49 +0530 Subject: [PATCH 17/18] fix(test): update test assertion for matrix-dot preset --- src/ui/spinnerPresets.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/spinnerPresets.test.ts b/src/ui/spinnerPresets.test.ts index 4905c96..22823c9 100644 --- a/src/ui/spinnerPresets.test.ts +++ b/src/ui/spinnerPresets.test.ts @@ -25,6 +25,6 @@ describe("spinnerPresets", () => { expect(getSpinner("radar").id).toBe("radar"); expect(getSpinner("baton").id).toBe("baton"); expect(getSpinner("crt").id).toBe("crt"); - expect(getSpinner("pacman").id).toBe("pacman"); + expect(getSpinner("matrix-dot").id).toBe("matrix-dot"); }); }); From 0a85745ee20d4a7dd49fc0e387e1b5fabcf4ad58 Mon Sep 17 00:00:00 2001 From: AK CHAVAN <90214281+iamakchavan@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:51:09 +0530 Subject: [PATCH 18/18] fix(ui): remove matrix-dot preset; keep CRT Scanline and classic retro spinners --- src/ui/spinnerPresets.test.ts | 1 - src/ui/spinnerPresets.ts | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/src/ui/spinnerPresets.test.ts b/src/ui/spinnerPresets.test.ts index 22823c9..f0ebbe6 100644 --- a/src/ui/spinnerPresets.test.ts +++ b/src/ui/spinnerPresets.test.ts @@ -25,6 +25,5 @@ describe("spinnerPresets", () => { expect(getSpinner("radar").id).toBe("radar"); expect(getSpinner("baton").id).toBe("baton"); expect(getSpinner("crt").id).toBe("crt"); - expect(getSpinner("matrix-dot").id).toBe("matrix-dot"); }); }); diff --git a/src/ui/spinnerPresets.ts b/src/ui/spinnerPresets.ts index b8faf56..f9f1dc8 100644 --- a/src/ui/spinnerPresets.ts +++ b/src/ui/spinnerPresets.ts @@ -49,23 +49,6 @@ export const SPINNERS: readonly SpinnerPreset[] = [ frames: ["โ–‘โ–’โ–“โ–ˆโ–“โ–’โ–‘", "โ–’โ–“โ–ˆโ–“โ–’โ–‘โ–‘", "โ–“โ–ˆโ–“โ–’โ–‘โ–‘โ–‘", "โ–ˆโ–“โ–’โ–‘โ–‘โ–‘โ–“", "โ–“โ–’โ–‘โ–‘โ–‘โ–“โ–ˆ", "โ–’โ–‘โ–‘โ–‘โ–“โ–ˆโ–“"], intervalMs: 80, }, - { - id: "matrix-dot", - name: "Dot Matrix LED", - description: "25-dot LED matrix wave and pulse grid", - frames: [ - "โ— โ—‹ โ—‹ โ—‹ โ—‹", - "โ— โ— โ—‹ โ—‹ โ—‹", - "โ— โ— โ— โ—‹ โ—‹", - "โ— โ— โ— โ— โ—‹", - "โ— โ— โ— โ— โ—", - "โ—‹ โ— โ— โ— โ—", - "โ—‹ โ—‹ โ— โ— โ—", - "โ—‹ โ—‹ โ—‹ โ— โ—", - "โ—‹ โ—‹ โ—‹ โ—‹ โ—", - ], - intervalMs: 70, - }, ] as const; export const DEFAULT_SPINNER = SPINNERS.find((s) => s.id === "meter") ?? SPINNERS[0]!;