Make Linux a supported platform: HTML context menus + runtime deps + release target

`bun run start` died on Linux with ERR_DLOPEN_FAILED (missing
libayatana-appindicator3.so.1). Fixing the environment exposed the real gap:
electrobun's ContextMenu.showContextMenu is a documented no-op on Linux, and
since the UI suppresses the webview's own right-click menu, EVERY panel context
menu was silently dead there — Send to Other Panel, Open, Rename, Permissions,
Delete, Copy Path, Reveal and the column picker all unreachable. Electrobun
itself logs "Application menus are not supported on Linux. Implement menu UI in
your webview HTML instead." at boot.

- Context menus are now HTML on EVERY platform (ui/src/app/context-menu.ts +
  context-menu.service.ts), not just as a Linux fallback: one implementation
  cannot drift from a second one. The item builders are ported verbatim from
  the old bun menu, so ordering, per-platform labels and the Windows-local
  chmod hide are unchanged, and picks are emitted as the same `ctx-*` / `col:*`
  strings through RpcService.emitMenuAction — App.handleMenuAction is untouched
  and still the single dispatcher for both the macOS menubar and right-clicks.
  Adds what the native menu gave for free: arrow/Enter/Escape keys, viewport
  edge-flip, close on blur/resize/scroll. The Edit item now shows the real F4
  hint instead of the native menu's "e" mnemonic.
- New shellItemAction RPC replaces showRowMenu/showColumnsMenu — the only part
  the webview can't do itself (reveal, open-with-default, copy path). Deletes
  src/bun/context-menu.ts entirely.
- release.ts gains a linux-x64 target. Electrobun emits only the .tar.zst
  updater blob on Linux, so the artifact is a .tar.gz of the assembled app
  folder, mirroring the Windows portable zip.
- Docs: platform-notes gets a Linux runtime-dependency table (AppIndicator is
  linked HARD — a missing lib is a boot failure, not a degraded tray), the
  ldd diagnostic, GNOME tray caveat, known issues and a first-boot smoke test;
  gotchas gets the ERR_DLOPEN_FAILED trail and the benign GLX/GBM startup
  noise; architecture/build-and-release/README updated accordingly.

Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (399 kB
initial), app boots on Linux with no errors beyond the documented benign
warnings. NOT verified on macOS/Windows — the native menu is retired on those
platforms too, so the menus need eyeballing on a Mac before the next release.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-15 14:23:58 +02:00
co-authored by Claude
parent fc03fa6216
commit f7ebed5478
18 changed files with 567 additions and 209 deletions
+19 -9
View File
@@ -10,7 +10,8 @@ Dense reference for development and debugging. Companion files:
its native launcher. Owns every privileged capability: filesystem, SSH,
window management, native menus/dialogs/tray, the connection/settings stores.
- **Webviews** (`ui/` Angular): one per window, system WebKit on macOS /
WebView2 on Windows. Zero privileged access — everything goes through RPC.
WebView2 on Windows / WebKitGTK on Linux. Zero privileged access — everything
goes through RPC.
- **RPC transport**: one WebSocket server in the bun process, multiplexed by
`webviewId` (each webview auto-connects using natively injected globals).
Requests are typed by the `AppRPC` schema in `src/shared/rpc.ts`.
@@ -148,10 +149,6 @@ would float over editor windows).
- `store/connections.ts` — plaintext-passwords JSON (deliberate v1 choice),
atomic tmp+rename, 0600 + win32 `icacls` ACL. `store/settings.ts` — defaults
spread-merge on load, so NEW SETTINGS KEYS need no backend change.
- `context-menu.ts` — native row/empty-space menus + the column picker.
Local shell actions (reveal/open/copy-path) run bun-side via Utils; others
forward through `menuAction` and the UI applies them to the row it
remembered (`FileActionsService.menuTarget`). Labels are per-platform.
- `menu.ts` — macOS-only app menu, rebuilt on every connections change (the
Connect submenu mirrors the store). ⌘W is intercepted by the menu, so bun
routes `close-tab` to the focused editor window first. Labels go through
@@ -161,7 +158,8 @@ would float over editor windows).
locale (from `settings.locale`, `system` via `Intl…resolvedOptions().locale`);
`t(key, params)` shares the same `src/shared/i18n` dictionaries as the UI.
Wired in index.ts on boot and on `settingsChanged` (relocalizes + rebuilds
the app menu). `context-menu.ts` labels resolve through `t()` at show-time.
the app menu). The context menu no longer needs this — it is Angular and reads
`I18nService` directly.
- `tray.ts` — static icon, click = activate main. macOS template image;
Windows colored icon. No `setImage` calls ever (they lose the template
treatment) and no menu (clicks arrive directly).
@@ -220,12 +218,24 @@ would float over editor windows).
boot and on the `settingsChanged` broadcast (so editor/manager windows follow).
- `file-actions.service.ts` — toolbar/keyboard/context-menu file ops, always
against an explicit panel; `menuTarget` remembers the right-clicked row.
- `context-menu.service.ts` + `context-menu.ts` — the panel right-click menus,
in HTML. Electrobun's native `showContextMenu` is a no-op on Linux, so it was
retired on ALL platforms rather than kept as a second code path (see
platform-notes.md). The service builds the item list (row / empty-space menu,
and the header column picker) and holds the open request as a signal; the
component renders it at the pointer with edge-flip clamping, arrow/Enter/Escape
keys, and close-on-blur/resize/scroll. Picked items are emitted verbatim as
`ctx-*` / `col:*` strings through `RpcService.emitMenuAction`, so they land in
the SAME `App.handleMenuAction` dispatcher the macOS app menubar uses — one
handler, two sources. The three shell actions the webview cannot perform
(reveal, open-with-default, copy path) go back to bun via the `shellItemAction`
RPC, which is all that remains of the old native menu.
- `file-icons.ts` — extension → category map driving glyphs, colors
(fixed `--icon-*` tokens, never accent-tinted) and the Kind column.
- `platform.ts` — preload-global readers (`PLATFORM`, `APP_MODE`,
`EDITOR_CONTEXT`, `IS_MAC_CHROME`, `IS_WINDOWS`, `MOD_LABEL`/`ALT_LABEL`)
+ `blockNativeContextMenu` (kills WebKit's Reload/Inspect menu outside
text fields).
`EDITOR_CONTEXT`, `IS_MAC`/`IS_MAC_CHROME`, `IS_WINDOWS`, `IS_LINUX`,
`MOD_LABEL`/`ALT_LABEL`) + `blockNativeContextMenu` (kills WebKit's
Reload/Inspect menu outside text fields — the app draws its own menu instead).
- `updater.service.ts` — auto-updater state for the tab-bar pill. Signals
`state` (`idle→available→downloading→ready`|`error`), `version`, `progress`.
Subscribes the `updateStatus` push (backend `Updater.checkForUpdate` on
+12 -3
View File
@@ -25,7 +25,8 @@ semver-shaped (`0.1.<n>`) for electrobun's config/updater; `BRANDING.buildLabel`
## Release builds
`bun scripts/release.ts [--target mac-arm64|mac-x64|win-x64]` (default: host).
`bun scripts/release.ts [--target mac-arm64|mac-x64|win-x64|linux-x64]`
(default: host).
Steps: gen-version → gen-icons → Angular build → `electrobun build
--env=stable` → copy/rename artifacts into
`release/<target>/miniSCP-Build<n>-<target>.*`.
@@ -37,7 +38,8 @@ Facts the script encodes:
- **No OS cross-compile.** The CLI builds for its host (its `targets` config
field is unread in 1.18.1); packaging needs host tools (codesign/hdiutil on
macOS, rcedit/PowerShell on Windows). Hence one script run per artifact, on
the matching machine; `win-x64` on a non-Windows host errors out.
the matching machine; `win-x64` on a non-Windows host and `linux-x64` on a
non-Linux host both error out.
- **mac-x64 from the arm64 Mac (Rosetta)**: the script downloads electrobun's
x64 CLI binary once (`electrobun-cli-darwin-x64.tar.gz` from the GitHub
release matching the installed version, cached at
@@ -56,7 +58,14 @@ Facts the script encodes:
(self-extracting installer → `%LOCALAPPDATA%`) plus the script's own
**portable zip** made from the assembled app folder under
`build/stable-win-x64/` (layout to confirm on the first real Windows build —
the step warns instead of failing).
the step warns instead of failing); Linux → only
`stable-linux-x64-miniSCP.tar.zst` + `update.json`, i.e. the updater feed and
nothing installable, so the script's **`.tar.gz` of the assembled app folder**
under `build/stable-linux-x64/` IS the release artifact (same best-effort
warn). No AppImage/.deb/.desktop — unpack and run `bin/launcher`.
- **Linux builds need the AppIndicator dev libraries present to RUN** (not to
build) — see platform-notes.md; a packaged tarball started on a machine
without them dies at `dlopen`.
- The script filters artifacts by app name, so stale artifacts from older
brandings don't get collected.
+28 -4
View File
@@ -48,7 +48,30 @@ touching the adjacent code.
- Tray: never call `setImage` after construction on macOS — the template
treatment is lost and the icon renders black on dark menu bars.
- `ApplicationMenu` is macOS-only in our usage; accelerators on Windows are
single-character only. ContextMenu exists on macOS+Windows, not Linux.
single-character only.
- **`ContextMenu.showContextMenu` is a silent no-op on Linux** (electrobun's own
`dist/api/bun/proc/linux.md` says so; the app also prints "Application menus are
not supported on Linux" at boot). Because the UI suppresses the webview's own
right-click menu, that left EVERY panel context menu dead on Linux — no error,
nothing happens. The menu is plain HTML now (`ui/src/app/context-menu.ts`) on
every platform; don't reintroduce the native one for "nativeness" on mac/win
without keeping a Linux path.
## Linux
- **`ERR_DLOPEN_FAILED` on `bun run start`** means a missing system library, not
broken app code. `libNativeWrapper.so` links `libayatana-appindicator3.so.1`
*hard* (for the tray), so the app cannot start at all without it — even though
the tray is a minor feature. Diagnose with
`ldd build/dev-linux-x64/*/bin/libNativeWrapper.so | grep 'not found'`; the
`libasar.so => not found` line in that output is a red herring (it ships beside
the wrapper and resolves at load). Install list per distro is in
[platform-notes.md](platform-notes.md).
- Benign startup noise on Linux — all non-fatal, the app runs normally:
`X11 Error: GLXBadWindow (code 170)`, `Failed to create GBM buffer of size
WxH: Invalid argument` (compositor/GLX chatter from the GTK webview), and
`libayatana-appindicator is deprecated. Please use libayatana-appindicator-glib`
(upstream electrobun's choice, not ours).
- **Window-open flash prevention is three-layer** — remove any one and the
white/inverted blink returns: (1) windows are created `hidden: true` and
revealed by `showWhenReady` on the webview's first `dom-ready` (800 ms
@@ -100,9 +123,10 @@ touching the adjacent code.
single-quoted entries** (`'…c'est…'`). Use double quotes for any value
containing an apostrophe. Plurals are `{ one, other }` maps (not ICU strings);
zh/ja supply only `other`.
- **Native menus can't use the Angular service** — `menu.ts`/`context-menu.ts`
go through backend `i18n.ts`, and the app menu must be rebuilt when the locale
changes (wired in index.ts `settingsChanged`), or it keeps the old language.
- **Native menus can't use the Angular service** — `menu.ts` goes through backend
`i18n.ts`, and the app menu must be rebuilt when the locale changes (wired in
index.ts `settingsChanged`), or it keeps the old language. The context menu no
longer has this problem: it is Angular now and reads `I18nService` directly.
- `eu`/`zh`/`ja` are AI drafts pending native review (see the i18n README).
- **Silent auto-refresh must snapshot sessionId+cwd and discard a mismatched
result.** A normal `refresh()` is masked by `busy` (input blocked), but the
+85 -10
View File
@@ -1,35 +1,41 @@
# Platform notes — macOS / Windows behavior matrix
# Platform notes — macOS / Windows / Linux behavior matrix
macOS is the primary target; Windows 11+ x64 is a **beta** target (win-arm64
deferred; Linux untargeted — electrobun lacks menus/context menus there).
Every platform seam carries a `PORTABILITY:` comment at the branch site.
macOS is the primary target; Windows 11+ x64 and Linux x64 are **beta** targets
(win-arm64 and linux-arm64 deferred). Every platform seam carries a
`PORTABILITY:` comment at the branch site.
## Seam inventory (where platform branches live)
| Concern | File | mac | win | other |
| Concern | File | mac | win | linux |
|---|---|---|---|---|
| Window chrome | `src/bun/windows.ts`, `index.ts` | hiddenInset + traffic-light offset, UI drag header | native titlebar, compact header | same as win |
| External editor launch | `src/bun/edits.ts launchEditor` | `open [-a app]` | `cmd /c start "" [app] file` | app directly / `xdg-open` |
| Editor app picker | `rpc-handlers.ts pickEditorApp` | /Applications, `.app` | %ProgramFiles%, `.exe` | home, any |
| Owner/group names | `fs/local.ts resolveId` | `id -nu` + `dscacheutil` | undefined → "—" | `id -nu` + `getent` |
| Drive roots | `fs/local.ts listLocalDrives` | empty | `fsutil fsinfo drives` | empty |
| chmod/Permissions | context-menu.ts, file-panel, file-actions | full | hidden for LOCAL panels (only flips read-only); SFTP unaffected | full |
| Shell wording | `context-menu.ts` | Reveal in Finder / Move to Trash | Show in Explorer / Move to Recycle Bin | Show in File Manager |
| chmod/Permissions | context-menu.service.ts, file-panel, file-actions | full | hidden for LOCAL panels (only flips read-only); SFTP unaffected | full |
| Shell wording | `ui context-menu.service.ts` | Reveal in Finder / Move to Trash | Show in Explorer / Move to Recycle Bin | Show in File Manager |
| Shortcut labels | `ui platform.ts MOD_LABEL/ALT_LABEL` | ⌘ / ⌥ | Ctrl+ / Alt+ | Ctrl+ / Alt+ |
| Tray | `src/bun/tray.ts` | 22 pt template PNG | 16 px colored PNG | colored |
| App menu | `src/bun/menu.ts` | full menu incl. About | none (early return) — About lives in the options popover | none |
| Tray | `src/bun/tray.ts` | 22 pt template PNG | 16 px colored PNG | colored (needs AppIndicator, see below) |
| App menu | `src/bun/menu.ts` | full menu incl. About | none (early return) — About lives in the options popover | none (unsupported upstream) |
| connections.json protection | `store/connections.ts` | chmod 0600 | `icacls` current-user ACL (0600 is meaningless) | 0600 |
| App data dir | `store/paths.ts` | ~/Library/Application Support | %APPDATA% | $XDG_CONFIG_HOME/.config |
| ssh-agent | `fs/sftp.ts` | $SSH_AUTH_SOCK | `\\.\pipe\openssh-ssh-agent` fallback | $SSH_AUTH_SOCK |
| Paths in UI | `ui format.ts joinPath/isRootPath` | "/" | separator detected from the dir string; drive-root regex | "/" |
The **context menu is HTML on every platform** (`ui/src/app/context-menu.ts` +
`context-menu.service.ts`), not a native electrobun menu — see the Linux section
below for why. Only the three shell actions it can't do itself (reveal, open with
the default app, copy path) cross to bun, via the `shellItemAction` RPC.
Remote (SFTP) paths are ALWAYS POSIX regardless of host or server OS
(`path.posix` throughout `fs/sftp.ts`).
## Memory footprint (expected, not a bug)
Investigated 2026-07 — packaged build. Roughly **~200 MB macOS / ~300 MB
Windows**, which is normal for a Bun + system-WebView app (30 MB is native /
Windows** (Linux sits near macOS: WebKitGTK, like WKWebView, is lighter than
Chromium), which is normal for a Bun + system-WebView app (30 MB is native /
no-web-engine territory; Tauri-class apps run ~80–200 MB, Electron 150–400 MB).
Not reducible to tens of MB without dropping the web UI.
@@ -49,6 +55,75 @@ Not reducible to tens of MB without dropping the web UI.
each), and each open tab keeps both panels' full `entries` arrays; terminal
transfers are retained until cleared. None affect normal use.
## Linux runtime dependencies (REQUIRED — the app will not start without them)
electrobun's `libNativeWrapper.so` links AppIndicator *hard* for the tray, so a
missing library is a hard boot failure (`ERR_DLOPEN_FAILED` out of `bun:ffi`
`dlopen`), not a degraded tray. The transitive deps (`libdbusmenu-glib`,
`libdbusmenu-gtk3`, `libayatana-indicator3`, `libayatana-ido3`) come in as
package dependencies:
| Distro | Install |
|---|---|
| Arch / CachyOS | `sudo pacman -S libayatana-appindicator` |
| Debian / Ubuntu | `sudo apt install libayatana-appindicator3-1` |
| Fedora | `sudo dnf install libayatana-appindicator-gtk3` |
GTK/WebKit itself comes from the distro too — electrobun uses the system webview
(`bundleCEF: false`), so `webkit2gtk` must be present.
Diagnosing a boot failure:
```sh
ldd build/dev-linux-x64/*/bin/libNativeWrapper.so | grep 'not found'
```
`libasar.so => not found` in that output is **expected and harmless** — it ships
in the same `bin/` folder and resolves at load time; only an unresolved
`libayatana-appindicator3.so.1` is fatal.
**Tray visibility** is a separate matter from linking: GNOME 3.26+ hides the
legacy tray, so the icon needs the
[AppIndicator extension](https://extensions.gnome.org/extension/615/appindicator-support/).
KDE Plasma, XFCE, MATE and Cinnamon show it out of the box. The app runs fine
either way — the tray is a convenience, and every action it offers exists in-app.
## Linux known issues (upstream electrobun)
- **No native menus of any kind.** Application menus and `showContextMenu` are
both no-ops (electrobun's `dist/api/bun/proc/linux.md`; the app logs
"Application menus are not supported on Linux" at boot). The app menubar was
already macOS-only, and the panel context menus are now HTML on every platform,
so nothing is missing at the UI level — but do not add a feature that depends
on a native menu.
- **Benign startup noise**: `X11 Error: GLXBadWindow (code 170)`, `Failed to
create GBM buffer of size WxH: Invalid argument`, and a deprecation warning
pointing at `libayatana-appindicator-glib`. None are fatal.
- **Unsigned, unpackaged**: the release artifact is a portable tarball — no
AppImage, `.deb`, `.desktop` entry or icon installation. Unpack and run
`bin/launcher`.
## First-Linux-boot smoke test
1. Install the AppIndicator package for your distro (table above), then
`bun run start`. The window opens and the tray icon appears (or doesn't, on
bare GNOME — that's the extension caveat, not a failure).
2. Right-click a file row: menu appears at the pointer, Send to Other Panel is
first, Permissions is present, Escape / click-outside / arrow keys + Enter all
work, and a right-click near the bottom-right flips the menu instead of
overflowing.
3. Right-click empty panel space (New Folder / New File / Refresh / Show in File
Manager) and a column header (checkmarks toggle columns live).
4. Copy Path puts the path on the clipboard; Show in File Manager opens the
distro's file manager; Open (local file) launches the default app via
`xdg-open`.
5. local↔remote transfers both directions; external editor round-trip
(temp download → save → auto-upload) with the inotify watcher.
6. Move to Trash lands the item in the desktop's trash; connections.json is 0600.
7. `bun scripts/release.ts --target linux-x64` produces
`release/linux-x64/miniSCP-Build<N>-linux-x64.tar.gz`; unpack it elsewhere and
run `bin/launcher`.
## Windows known issues (upstream electrobun, documented not fixed)
- **HiDPI blur** — WebView2 renders at 1x on scaled displays