diff --git a/README.md b/README.md index 0ab8784..5eeb718 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,8 @@ remote server over SSH/SFTP, so you can move files **local↔remote**, (MB/s or Mbps), persisted across restarts. - On macOS the tab bar is fused with the window titlebar; other platforms keep a standard titlebar. -- **Menu-bar (tray) icon**: template icon that adapts to light/dark; during - transfers it fades and fills an aggregate progress bar (delivered/total bytes - of the whole queue). Its menu lists your tabs with live per-tab transfer - info — click one to jump to it — plus Show and Quit. +- **Menu-bar (tray) icon**: template icon that adapts to light/dark; click it + to bring the app to the front. - **Edit files** (F4 / toolbar / File menu): a built-in editor works on local and remote files directly; or pick an external app (Options → Editor → Custom → Choose…). External edits of remote files download to a temp folder, diff --git a/assets/tray/progress/p0.png b/assets/tray/progress/p0.png deleted file mode 100644 index 7b79961..0000000 Binary files a/assets/tray/progress/p0.png and /dev/null differ diff --git a/assets/tray/progress/p10.png b/assets/tray/progress/p10.png deleted file mode 100644 index 18c4dfc..0000000 Binary files a/assets/tray/progress/p10.png and /dev/null differ diff --git a/assets/tray/progress/p100.png b/assets/tray/progress/p100.png deleted file mode 100644 index b9fd386..0000000 Binary files a/assets/tray/progress/p100.png and /dev/null differ diff --git a/assets/tray/progress/p15.png b/assets/tray/progress/p15.png deleted file mode 100644 index 982ce19..0000000 Binary files a/assets/tray/progress/p15.png and /dev/null differ diff --git a/assets/tray/progress/p20.png b/assets/tray/progress/p20.png deleted file mode 100644 index 2fb9287..0000000 Binary files a/assets/tray/progress/p20.png and /dev/null differ diff --git a/assets/tray/progress/p25.png b/assets/tray/progress/p25.png deleted file mode 100644 index 4b0e5f1..0000000 Binary files a/assets/tray/progress/p25.png and /dev/null differ diff --git a/assets/tray/progress/p30.png b/assets/tray/progress/p30.png deleted file mode 100644 index b4fbd46..0000000 Binary files a/assets/tray/progress/p30.png and /dev/null differ diff --git a/assets/tray/progress/p35.png b/assets/tray/progress/p35.png deleted file mode 100644 index 1b6cb88..0000000 Binary files a/assets/tray/progress/p35.png and /dev/null differ diff --git a/assets/tray/progress/p40.png b/assets/tray/progress/p40.png deleted file mode 100644 index 18b3307..0000000 Binary files a/assets/tray/progress/p40.png and /dev/null differ diff --git a/assets/tray/progress/p45.png b/assets/tray/progress/p45.png deleted file mode 100644 index 1cba41b..0000000 Binary files a/assets/tray/progress/p45.png and /dev/null differ diff --git a/assets/tray/progress/p5.png b/assets/tray/progress/p5.png deleted file mode 100644 index 1cf4c40..0000000 Binary files a/assets/tray/progress/p5.png and /dev/null differ diff --git a/assets/tray/progress/p50.png b/assets/tray/progress/p50.png deleted file mode 100644 index f193657..0000000 Binary files a/assets/tray/progress/p50.png and /dev/null differ diff --git a/assets/tray/progress/p55.png b/assets/tray/progress/p55.png deleted file mode 100644 index 2d14b62..0000000 Binary files a/assets/tray/progress/p55.png and /dev/null differ diff --git a/assets/tray/progress/p60.png b/assets/tray/progress/p60.png deleted file mode 100644 index c66d77b..0000000 Binary files a/assets/tray/progress/p60.png and /dev/null differ diff --git a/assets/tray/progress/p65.png b/assets/tray/progress/p65.png deleted file mode 100644 index 182e845..0000000 Binary files a/assets/tray/progress/p65.png and /dev/null differ diff --git a/assets/tray/progress/p70.png b/assets/tray/progress/p70.png deleted file mode 100644 index a5767d3..0000000 Binary files a/assets/tray/progress/p70.png and /dev/null differ diff --git a/assets/tray/progress/p75.png b/assets/tray/progress/p75.png deleted file mode 100644 index 7b380af..0000000 Binary files a/assets/tray/progress/p75.png and /dev/null differ diff --git a/assets/tray/progress/p80.png b/assets/tray/progress/p80.png deleted file mode 100644 index f53be36..0000000 Binary files a/assets/tray/progress/p80.png and /dev/null differ diff --git a/assets/tray/progress/p85.png b/assets/tray/progress/p85.png deleted file mode 100644 index 26b85d8..0000000 Binary files a/assets/tray/progress/p85.png and /dev/null differ diff --git a/assets/tray/progress/p90.png b/assets/tray/progress/p90.png deleted file mode 100644 index b5f51de..0000000 Binary files a/assets/tray/progress/p90.png and /dev/null differ diff --git a/assets/tray/progress/p95.png b/assets/tray/progress/p95.png deleted file mode 100644 index eabadd7..0000000 Binary files a/assets/tray/progress/p95.png and /dev/null differ diff --git a/scripts/gen-icons.ts b/scripts/gen-icons.ts index cb73429..273bb21 100644 --- a/scripts/gen-icons.ts +++ b/scripts/gen-icons.ts @@ -1,7 +1,7 @@ // Regenerate every raster icon asset from the SVG sources (dev-time only). // bun scripts/gen-icons.ts // Produces: assets/icon.png (1024), assets/icon.iconset/* (macOS app icon), -// assets/tray/tray-44-template.png, assets/tray/progress/p{0..100}.png. +// assets/tray/tray-44-template.png. import { mkdir } from "node:fs/promises"; import path from "node:path"; import { Resvg } from "@resvg/resvg-js"; @@ -31,23 +31,4 @@ for (const size of [16, 32, 128, 256, 512]) { const trayGlyph = await Bun.file(path.join(assets, "tray", "tray-glyph.svg")).text(); await writePng(path.join(assets, "tray", "tray-44-template.png"), trayGlyph, 44); -// ---- tray progress frames: faded arrows + centered bar filled to pct ---- -function progressSvg(pct: number): string { - const trackX = 4; - const trackW = 16; - const fillW = (trackW * pct) / 100; - return ` - - - - - - ${fillW > 0 ? `` : ""} -`; -} - -for (let pct = 0; pct <= 100; pct += 5) { - await writePng(path.join(assets, "tray", "progress", `p${pct}.png`), progressSvg(pct), 44); -} - -console.log("icons regenerated: app iconset, tray template, 21 progress frames"); +console.log("icons regenerated: app iconset + tray template"); diff --git a/src/bun/index.ts b/src/bun/index.ts index 0ff2ce0..997b45b 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -38,7 +38,6 @@ const edits = new EditManager(registry, engine, settings); const events = { sessionClosed: (_info: { sessionId: string; reason: string }) => {}, connectionsChanged: () => {}, - workspacesSync: (_sync: import("../shared/rpc").WorkspaceSyncInfo) => {}, }; const rpc = createAppRPC({ registry, engine, store, settings, edits, events }); const url = await getMainViewUrl(); @@ -65,16 +64,11 @@ const win = new BrowserWindow({ rpc, }); -const tray = new AppTray(win, engine); -tray.onAction = (action) => rpc.send.menuAction({ action }); +new AppTray(win); -engine.onUpdate = (snapshot) => { - rpc.send.transferUpdate(snapshot); - tray.updateProgress(); -}; +engine.onUpdate = (snapshot) => rpc.send.transferUpdate(snapshot); events.sessionClosed = (info) => rpc.send.sessionClosed(info); events.connectionsChanged = () => void applyApplicationMenu(store); -events.workspacesSync = (sync) => tray.updateTabs(sync); void settings.load().then((s) => engine.setConcurrency(effectiveConcurrency(s))); void applyApplicationMenu(store); diff --git a/src/bun/rpc-handlers.ts b/src/bun/rpc-handlers.ts index bf3262e..fcf367a 100644 --- a/src/bun/rpc-handlers.ts +++ b/src/bun/rpc-handlers.ts @@ -1,5 +1,5 @@ import { BrowserView, Utils } from "electrobun/bun"; -import type { AppRPC, WorkspaceSyncInfo } from "../shared/rpc"; +import type { AppRPC } from "../shared/rpc"; import { effectiveConcurrency, type Result } from "../shared/types"; import { readFileText, writeFileText, type EditManager } from "./edits"; import type { SessionRegistry } from "./fs/registry"; @@ -23,7 +23,6 @@ export interface AppDeps { events: { sessionClosed: (info: { sessionId: string; reason: string }) => void; connectionsChanged: () => void; - workspacesSync: (sync: WorkspaceSyncInfo) => void; }; } @@ -203,9 +202,6 @@ export function createAppRPC({ registry, engine, store, settings, edits, events return {}; }), }, - messages: { - workspacesSync: (sync) => events.workspacesSync(sync), - }, }, }); } diff --git a/src/bun/transfers/engine.ts b/src/bun/transfers/engine.ts index 6563a54..db9a0c0 100644 --- a/src/bun/transfers/engine.ts +++ b/src/bun/transfers/engine.ts @@ -146,35 +146,6 @@ export class TransferEngine { if (job && TERMINAL.includes(job.snapshot.state)) this.jobs.delete(jobId); } - /** Live totals over every non-terminal job (drives the tray progress icon). */ - aggregateProgress(): { - activeCount: number; - doneBytes: number; - totalBytes: number; - bySession: Record; - } { - const result = { - activeCount: 0, - doneBytes: 0, - totalBytes: 0, - bySession: {} as Record, - }; - for (const job of this.jobs.values()) { - const snap = job.snapshot; - if (TERMINAL.includes(snap.state)) continue; - result.activeCount++; - result.doneBytes += snap.doneBytes; - result.totalBytes += snap.totalBytes; - for (const sessionId of new Set([snap.srcSessionId, snap.dstSessionId])) { - const entry = (result.bySession[sessionId] ??= { count: 0, done: 0, total: 0 }); - entry.count++; - entry.done += snap.doneBytes; - entry.total += snap.totalBytes; - } - } - return result; - } - /** Fail every non-terminal job that touches a dead session. */ failSession(sessionId: string, reason: string): void { for (const job of this.jobs.values()) { diff --git a/src/bun/tray.ts b/src/bun/tray.ts index 4a97d45..167d2a0 100644 --- a/src/bun/tray.ts +++ b/src/bun/tray.ts @@ -1,130 +1,22 @@ import { Tray, type BrowserWindow } from "electrobun/bun"; -import { BRANDING } from "../shared/branding"; -import type { WorkspaceSyncInfo } from "../shared/rpc"; -import type { TransferEngine } from "./transfers/engine"; - -const BASE_IMAGE = "views://assets/tray/tray-44-template.png"; -const PROGRESS_IMAGE = (pct: number) => `views://assets/tray/progress/p${pct}.png`; -const REFRESH_THROTTLE_MS = 500; /** - * Menu-bar icon: the app glyph when idle; while transfers run the icon swaps - * to pre-rendered frames (faded arrows + fill bar = queue doneBytes/totalBytes). - * The menu lists the tabs (synced from the webview) with per-tab job info. + * Menu-bar icon: the app glyph as a macOS template image (adapts to + * light/dark). Clicking it brings the app window to the front. No menu is ever + * set, so clicks arrive directly; no setImage swaps, so the template + * treatment from construction always applies. */ export class AppTray { private tray: Tray; - private tabs: WorkspaceSyncInfo = { workspaces: [], activeId: 0 }; - private lastRefresh = 0; - private refreshTimer: ReturnType | null = null; - private currentImage = BASE_IMAGE; - /** Forwards tray menu actions the UI must handle (activate-tab:). */ - onAction: (action: string) => void = () => {}; - constructor( - private win: BrowserWindow, - private engine: TransferEngine, - ) { + constructor(win: BrowserWindow) { this.tray = new Tray({ title: "", - image: BASE_IMAGE, + image: "views://assets/tray/tray-44-template.png", template: true, width: 22, height: 22, }); - - this.tray.on("tray-clicked", (event) => { - const { action } = (event as { data: { action: string } }).data; - if (!action) return; // icon click with a menu set -> macOS opens the menu - if (action === "show-app") { - this.win.activate(); - } else if (action === "quit") { - process.exit(0); // electrobun's clean-quit override - } else if (action.startsWith("activate-tab:")) { - this.win.activate(); - this.onAction(action); - } - }); - - this.rebuildMenu(); - } - - updateTabs(sync: WorkspaceSyncInfo): void { - this.tabs = sync; - this.scheduleRefresh(); - } - - /** Called on every engine update; throttled internally. */ - updateProgress(): void { - this.scheduleRefresh(); - } - - private scheduleRefresh(): void { - const now = Date.now(); - const wait = Math.max(0, REFRESH_THROTTLE_MS - (now - this.lastRefresh)); - if (this.refreshTimer) return; - this.refreshTimer = setTimeout(() => { - this.refreshTimer = null; - this.lastRefresh = Date.now(); - this.refresh(); - }, wait); - } - - private refresh(): void { - const progress = this.engine.aggregateProgress(); - - // icon: nearest 5% frame while transferring, base glyph when idle - let image = BASE_IMAGE; - if (progress.activeCount > 0 && progress.totalBytes > 0) { - const pct = Math.round(((progress.doneBytes / progress.totalBytes) * 100) / 5) * 5; - image = PROGRESS_IMAGE(Math.max(0, Math.min(100, pct))); - } - if (image !== this.currentImage) { - this.currentImage = image; - this.tray.setImage(image); - } - - this.rebuildMenu(progress); - } - - private rebuildMenu(progress = this.engine.aggregateProgress()): void { - const items: Parameters[0] = []; - - for (const ws of this.tabs.workspaces) { - const jobs = ws.sessionIds - .map((id) => progress.bySession[id]) - .filter((v): v is NonNullable => !!v); - const count = jobs.reduce((n, j) => n + j.count, 0); - const total = jobs.reduce((n, j) => n + j.total, 0); - const done = jobs.reduce((n, j) => n + j.done, 0); - const suffix = count - ? ` — ${count} transfer${count === 1 ? "" : "s"} · ${total ? Math.round((done / total) * 100) : 0}%` - : ""; - items.push({ - type: "normal", - label: `${ws.name}${suffix}`, - action: `activate-tab:${ws.id}`, - checked: ws.id === this.tabs.activeId, - }); - } - - if (progress.activeCount > 0) { - items.push({ type: "divider" }); - const pct = progress.totalBytes - ? Math.round((progress.doneBytes / progress.totalBytes) * 100) - : 0; - items.push({ - type: "normal", - label: `${progress.activeCount} active transfer${progress.activeCount === 1 ? "" : "s"} · ${pct}%`, - action: "noop", - enabled: false, - }); - } - - items.push({ type: "divider" }); - items.push({ type: "normal", label: `Show ${BRANDING.appName}`, action: "show-app" }); - items.push({ type: "normal", label: "Quit", action: "quit" }); - - this.tray.setMenu(items); + this.tray.on("tray-clicked", () => win.activate()); } } diff --git a/src/shared/rpc.ts b/src/shared/rpc.ts index ce9de9b..3c2ff18 100644 --- a/src/shared/rpc.ts +++ b/src/shared/rpc.ts @@ -15,13 +15,7 @@ import type { // pulling in bun-only type modules. // // bun.requests = calls the webview makes, handled in src/bun/rpc-handlers.ts -// bun.messages = fire-and-forget pushes from the webview (tray state) // webview.messages = push events from bun, handled in ui rpc.service.ts - -export interface WorkspaceSyncInfo { - workspaces: { id: number; name: string; remote: boolean; sessionIds: string[] }[]; - activeId: number; -} export type AppRPC = { bun: { requests: { @@ -103,9 +97,7 @@ export type AppRPC = { }; clearEditLeftovers: { params: Record; response: Result }; }; - messages: { - workspacesSync: WorkspaceSyncInfo; - }; + messages: Record; }; webview: { requests: Record; diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index c63fb78..a45f3c1 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -67,13 +67,8 @@ export class App { }); } - /** Native application-menu / tray actions forwarded from the bun process. */ + /** Native application-menu actions forwarded from the bun process. */ private handleMenuAction(action: string): void { - if (action.startsWith('activate-tab:')) { - const id = Number(action.slice('activate-tab:'.length)); - if (Number.isFinite(id)) this.panels.setActiveWorkspace(id); - return; - } if (action.startsWith('connect:')) { const id = action.slice('connect:'.length); const connection = this.connections.connections().find((c) => c.id === id); diff --git a/ui/src/app/panels.service.ts b/ui/src/app/panels.service.ts index a864536..d5785f1 100644 --- a/ui/src/app/panels.service.ts +++ b/ui/src/app/panels.service.ts @@ -1,4 +1,4 @@ -import { Injectable, computed, effect, signal, type WritableSignal } from '@angular/core'; +import { Injectable, computed, signal, type WritableSignal } from '@angular/core'; import type { ConnectionConfig, FileEntry, SavedConnection } from '@shared/types'; import { RpcService } from './rpc.service'; import { ToastService } from './toast.service'; @@ -180,21 +180,6 @@ export class PanelsService { ) { this.newWorkspace(); - // keep the bun-side tray informed about tabs (names, sessions, active) - if (this.rpc.available) { - effect(() => { - this.rpc.send.workspacesSync({ - workspaces: this.workspaces().map((ws) => ({ - id: ws.id, - name: this.workspaceName(ws), - remote: this.workspaceIsRemote(ws), - sessionIds: [ws.left.sessionId(), ws.right.sessionId()], - })), - activeId: this.activeWorkspaceId(), - }); - }); - } - // a session died on its own — reset every panel bound to it, in any tab this.rpc.onSessionClosed((event) => { for (const workspace of this.workspaces()) { diff --git a/ui/src/app/rpc.service.ts b/ui/src/app/rpc.service.ts index cdd00cd..4fdd40d 100644 --- a/ui/src/app/rpc.service.ts +++ b/ui/src/app/rpc.service.ts @@ -87,20 +87,4 @@ export class RpcService { }); } - /** Fire-and-forget messages to the bun process (e.g. workspacesSync). */ - get send() { - const rpc = this.rpc; - if (!rpc) throw new Error('RPC is only available inside the desktop app'); - const ready = this.ready; - return new Proxy({} as (typeof rpc)['send'], { - get: - (_target, message: string) => - (payload?: unknown) => { - void ready.then(() => { - const messages = rpc.send as unknown as Record void>; - messages[message](payload); - }); - }, - }); - } }