`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>
287 lines
13 KiB
Markdown
287 lines
13 KiB
Markdown
# miniSCP
|
||
|
||
A cross-platform, dual-panel SFTP/local file manager — a small WinSCP-style
|
||
commander built on **Bun**, **Electrobun**, and **Angular**.
|
||
|
||
Each of the two panels independently shows either the local filesystem or a
|
||
remote server over SSH/SFTP, so you can move files **local↔remote**,
|
||
**remote↔remote**, or **local↔local** through one symmetric engine.
|
||
|
||
> **Name & icon are placeholders.** See [Rebranding](#rebranding) — it's a
|
||
> one-file change.
|
||
|
||
## Developer docs
|
||
|
||
Dense reference material lives in [docs/](docs/): [architecture](docs/architecture.md)
|
||
(processes, windows, RPC, every module/service), [build & release](docs/build-and-release.md)
|
||
(Build-n versioning, per-target release pipeline, CI), [platform notes](docs/platform-notes.md)
|
||
(macOS/Windows/Linux matrix, runtime deps + smoke tests), and
|
||
[gotchas](docs/gotchas.md) (known landmines).
|
||
|
||
## Features
|
||
|
||
- Dual-panel commander UI; either panel can be Local or any saved connection.
|
||
- **Session tabs**: each tab is an independent left/right workspace; remote
|
||
sessions stay connected and transfers keep running in background tabs.
|
||
- Per-panel action toolbar with a direction-explicit send button, plus a
|
||
VSCode-style status bar showing live transfer progress (click to expand the
|
||
transfer queue with per-file speed and ETA).
|
||
- **Overwrite confirmation** on name collisions, with a source-vs-destination
|
||
comparison (size, modified date, which is newer) and batch replace-all /
|
||
skip-all.
|
||
- **Settings** (gear, top-right): light / dark / system theme and speed units
|
||
(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; 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,
|
||
are watched, and every save auto-uploads (visible in the transfer list) until
|
||
the session disconnects. Leftover temp files from a crash are offered for
|
||
cleanup at the next start.
|
||
- Browse, sort (name/size/modified, folders first), keyboard navigation.
|
||
- Create folder / file, rename, delete (recursive), change permissions (chmod).
|
||
- Upload, download, and remote→remote send — files **and** whole folders —
|
||
with a transfer queue, per-job progress, and cancel.
|
||
- Connection manager: saved connections organised into folders, password or
|
||
private-key auth (with passphrase).
|
||
|
||
## Stack
|
||
|
||
| Layer | Choice |
|
||
| ---------- | ------------------------------------------------------------- |
|
||
| Runtime | Bun (main process, all SSH/SFTP + file IO) |
|
||
| Desktop | Electrobun 1.18 (native webview, typed RPC bridge) |
|
||
| Frontend | Angular 22 (standalone, signals, zoneless) in `ui/` |
|
||
| Styling | Tailwind CSS v4, shadcn-style tokens (no component library) |
|
||
| Icons | @ng-icons/mynaui |
|
||
| SSH/SFTP | `ssh2` (pure-JS under Bun) |
|
||
|
||
The repo is a Bun workspace: the Electrobun app at the root, the Angular app in
|
||
`ui/`. Shared types and the RPC contract live in `src/shared/` and are imported
|
||
by both sides.
|
||
|
||
```
|
||
src/shared/ types.ts, rpc.ts (wire contract), branding.ts
|
||
src/bun/ Electrobun main process
|
||
fs/ FileSystemProvider + LocalProvider + SftpProvider + registry
|
||
transfers/ engine.ts (queue/stream copy) + walk.ts (recursive helpers)
|
||
store/ connections.json persistence
|
||
ui/src/app/ Angular components + signal services
|
||
```
|
||
|
||
## Prerequisites
|
||
|
||
- [Bun](https://bun.sh) ≥ 1.3 (`bun --version`).
|
||
- macOS: **Xcode Command Line Tools** (for the app build).
|
||
- Linux: **AppIndicator** — the app links it for the tray and will not start
|
||
without it (`ERR_DLOPEN_FAILED`). `libayatana-appindicator` (Arch),
|
||
`libayatana-appindicator3-1` (Debian/Ubuntu),
|
||
`libayatana-appindicator-gtk3` (Fedora), plus system `webkit2gtk`.
|
||
Details and diagnostics in [platform notes](docs/platform-notes.md).
|
||
- No Node install required — a preload shim (`scripts/node-version-shim.ts`)
|
||
satisfies the Angular CLI's Node version check while running under Bun.
|
||
|
||
## Setup
|
||
|
||
```bash
|
||
bun install # installs root + ui workspace deps
|
||
```
|
||
|
||
## Develop
|
||
|
||
```bash
|
||
bun run dev
|
||
```
|
||
|
||
This starts the Angular dev server (hot reload for UI code) and launches the
|
||
Electrobun app pointed at it. Edits under `ui/` reload instantly; edits to the
|
||
Bun main process (`src/bun/**`, RPC schema) trigger an app relaunch.
|
||
|
||
## Releases
|
||
|
||
Versioning is Sublime-style: **Build n**, where n is the commit count
|
||
(`git rev-list --count HEAD`), regenerated into the gitignored
|
||
`src/shared/version.gen.ts` by every build script (and `postinstall`).
|
||
|
||
One artifact per invocation, on the matching host — electrobun cannot
|
||
cross-compile between OSes:
|
||
|
||
```sh
|
||
bun run release -- --target mac-arm64 # on the Mac (native)
|
||
bun run release -- --target mac-x64 # on the same arm64 Mac, via Rosetta 2
|
||
bun run release -- --target win-x64 # on a Windows 11 x64 machine
|
||
bun run release -- --target linux-x64 # on a Linux x64 machine
|
||
```
|
||
|
||
Artifacts land in `release/<target>/miniSCP-Build<n>-<target>.*` — a `.dmg`
|
||
per mac arch; on Windows a `-Setup.zip` installer **and** a `-portable.zip`
|
||
(unzip anywhere and run); on Linux a portable `.tar.gz` (unpack and run
|
||
`bin/launcher`). The mac-x64 build fetches electrobun's x64 CLI once
|
||
into `~/.cache/miniscp/` and runs it under Rosetta so the whole pipeline
|
||
believes it is on an Intel Mac. CI: `.gitea/workflows/release.yml` runs the
|
||
same script on a macos + windows runner matrix once act_runners are
|
||
registered; trigger by pushing a `build-<n>` tag.
|
||
|
||
## Build a packaged app
|
||
|
||
```bash
|
||
bun run build # ng build (ui/dist) + electrobun build -> build/*/miniSCP*.app
|
||
```
|
||
|
||
The Angular bundle is copied into the app under `views://app`. The macOS icon is
|
||
compiled from `assets/icon.iconset` into `AppIcon.icns` automatically.
|
||
|
||
For a distributable (optimized, with auto-update channel) build, use
|
||
`bunx electrobun build --env=stable` (see the
|
||
[Electrobun docs](https://framework.blackboard.sh/electrobun/) for signing,
|
||
notarization, and release hosting).
|
||
|
||
## Rebranding
|
||
|
||
Everything user-facing reads from **[`src/shared/branding.ts`](src/shared/branding.ts)**:
|
||
|
||
```ts
|
||
export const BRANDING = {
|
||
appName: "miniSCP", // window title, bundle name, app-data folder
|
||
identifier: "es.teknes.miniscp",
|
||
legacyNames: [], // previous names; their data dirs migrate on boot
|
||
version: "0.1.0",
|
||
icon: "assets/icon.iconset",
|
||
} as const;
|
||
```
|
||
|
||
To change the icon: edit `assets/logo.svg` (app icon) and
|
||
`assets/tray/tray-glyph.svg` (menu-bar glyph), then run
|
||
`bun scripts/gen-icons.ts` — it regenerates the macOS iconset, `icon.png`, the
|
||
tray template image, and the tray progress frames from the SVGs
|
||
(`@resvg/resvg-js`, dev-only dependency).
|
||
|
||
> Renaming `appName` changes the app-data directory. Add the old name to
|
||
> `legacyNames` and the app moves the previous folder (connections, settings,
|
||
> edit temp files) into place on the next boot.
|
||
|
||
## Where connections are stored
|
||
|
||
Saved connections live in a plain JSON file in the OS app-data directory
|
||
(macOS: `~/Library/Application Support/miniSCP/connections.json`), written with
|
||
`0600` permissions.
|
||
|
||
> ⚠️ **Passwords are stored in plaintext** in this file (a deliberate v1
|
||
> simplicity choice). Anyone who can read the file can read the passwords. If
|
||
> that isn't acceptable for your use, use key-based auth or don't save the
|
||
> password (connect ad-hoc). OS-keychain storage is a natural future addition.
|
||
|
||
## Keyboard shortcuts
|
||
|
||
`Tab` switch panel · `↑`/`↓`/`PgUp`/`PgDn`/`Home`/`End` move cursor ·
|
||
`Space` select · `Enter` open folder · `Backspace` parent ·
|
||
`Ctrl/Cmd+A` select all · `F5` send to other panel · `F2` rename ·
|
||
`F7` new folder · `F8`/`Delete` delete · `Cmd/Ctrl+T` new tab ·
|
||
`Cmd/Ctrl+W` close tab · `Ctrl+Tab` next tab.
|
||
|
||
(On Macs, function keys may need `fn` — every action also has a toolbar button.)
|
||
|
||
## Same-file protection
|
||
|
||
Copying a file or folder onto itself would truncate it to zero bytes (the
|
||
destination is opened for writing before the source is read). miniSCP blocks
|
||
this at several layers:
|
||
|
||
- **Same session / same host**: every connection carries a filesystem identity
|
||
(`local`, or `resolvedIP:port` — the hostname is DNS-resolved, so
|
||
`myserver.lan` and its IP compare equal even across two panels/tabs holding
|
||
separate SSH sessions to the same server). Equal identity + equal or nested
|
||
paths ⇒ the transfer is refused before anything is opened.
|
||
- **Hidden aliases** (two *different* addresses that reach the same machine —
|
||
NAT, split DNS): when identities differ but source and destination paths
|
||
collide anyway, a probe file is written through one connection and looked up
|
||
through the other; if it's visible on both, they're the same machine and the
|
||
copy is refused. (SFTP offers no device/inode identity — the mechanism
|
||
`cp`/`rsync` use locally — so probing is the only deterministic test.)
|
||
- The overwrite dialog never offers to "Replace" a file with itself — such
|
||
items are auto-skipped with a notice.
|
||
|
||
**Known residual limitation**: the same filesystem mounted at *different
|
||
paths* on two connections (bind mounts, NFS exported twice, shared container
|
||
volumes) cannot be detected — the paths differ, so no guard or probe triggers.
|
||
Be careful when two panels reach the same data through different mount points.
|
||
|
||
## Portability notes
|
||
|
||
Platform seams are isolated behind `process.platform` / `PLATFORM` branches
|
||
marked with `PORTABILITY:` comments: editor launch (`open` / `cmd /c start` /
|
||
`xdg-open`), app pickers, owner/group lookups, tray images, window chrome,
|
||
shell-integration wording, drive listing, and the connections-file ACL. The
|
||
application menu ([src/bun/menu.ts](src/bun/menu.ts)) is macOS-only for now;
|
||
the in-app UI covers every menu action.
|
||
|
||
Right-click menus are **HTML, on every platform**
|
||
([ui/src/app/context-menu.ts](ui/src/app/context-menu.ts)) rather than native —
|
||
electrobun's native context menu is a documented no-op on Linux, and one menu
|
||
implementation beats two that drift. Only reveal / open-with-default / copy-path
|
||
cross to the backend, through the `shellItemAction` RPC.
|
||
|
||
## Windows (beta)
|
||
|
||
Windows 11+ x64 is supported as a **beta** target (win-arm64 deferred). All
|
||
electrobun APIs the app uses are implemented on Windows; known issues are
|
||
upstream:
|
||
|
||
- **HiDPI blur** ([electrobun#324](https://github.com/blackboardsh/electrobun/issues/324)):
|
||
WebView2 renders at 1x on scaled displays until upstream adds DPI awareness.
|
||
- **Notifications** use legacy balloon style, not Windows toast.
|
||
- **Unsigned binaries**: both the Setup.exe installer and the portable exe
|
||
trigger SmartScreen/Mark-of-the-Web warnings on first run. No Start Menu
|
||
entry or uninstaller (installer unpacks to `%LOCALAPPDATA%`).
|
||
- `connections.json` is protected with an `icacls` current-user ACL instead of
|
||
POSIX 0600 (which is meaningless on Windows).
|
||
|
||
First-Windows-boot smoke test: drives dropdown lists C:\ etc.; navigate to a
|
||
drive root ('..' disappears); local↔remote transfers; external editor launch
|
||
(`cmd /c start`) and the .exe picker; Show in Explorer / Recycle Bin; tray icon
|
||
renders colored; Permissions absent on local panels; Ctrl+labels in tooltips;
|
||
portable zip runs from any folder.
|
||
|
||
## Linux (beta)
|
||
|
||
Linux x64 is supported as a **beta** target and builds a portable tarball.
|
||
One hard requirement: the AppIndicator library listed under
|
||
[Prerequisites](#prerequisites) — electrobun links it for the tray, so without
|
||
it the app aborts at startup with `ERR_DLOPEN_FAILED` rather than merely losing
|
||
its tray icon.
|
||
|
||
- **No native menus at all** upstream (both the app menubar and context menus
|
||
are no-ops). The menubar was already macOS-only and the context menus are now
|
||
HTML, so nothing is missing in practice.
|
||
- **Tray visibility** depends on the desktop: KDE, XFCE, MATE and Cinnamon show
|
||
it; GNOME 3.26+ needs the AppIndicator extension. The app works either way.
|
||
- **Unsigned and unpackaged**: no AppImage, `.deb`, desktop entry or icon
|
||
installation — unpack the tarball and run `bin/launcher`.
|
||
- Harmless startup warnings (`GLXBadWindow`, `Failed to create GBM buffer`, an
|
||
appindicator deprecation notice) are documented in
|
||
[gotchas](docs/gotchas.md#linux).
|
||
|
||
Full dependency table, diagnostics and the first-boot smoke test:
|
||
[platform notes](docs/platform-notes.md).
|
||
|
||
## Known issues
|
||
|
||
- **Rare native crash under load.** A long-running session (multiple remote
|
||
connections, extended uptime) can hit a Bun panic — a segfault inside Bun
|
||
1.3.13's FFI thread-safe callbacks executing Electrobun's native→worker
|
||
bridge (`FFI_Callback_threadsafe_call` → `web_worker.spin` in the decoded
|
||
crash report). This is an upstream Bun × Electrobun bug, not app code; a
|
||
restart recovers cleanly and in-flight work isn't corrupted. Expected to
|
||
resolve when Electrobun bumps its pinned Bun (currently 1.3.13; 1.3.14+
|
||
available). Bun's own crash reporter link is printed to the console on panic.
|
||
|
||
## Not in v1
|
||
|
||
Drag-and-drop, file preview/edit, transfer resume, symlink following
|
||
(symlinks are listed but skipped by transfers), OS keychain, tab persistence
|
||
across restarts, Linux desktop integration (AppImage/.deb/.desktop entry —
|
||
the release is a portable tarball).
|