Three layouts collapsed in fe028a9, where the isMacChrome branches were
removed by a scripted edit that was wrong in two ways. The *bindings* were
dead by construction; the layout classes inside them were not, and nothing
replaced them.
- tab-bar: the edit appended a second `class` attribute to an element that
already had one. A duplicate attribute is silently dropped, so the
workspace strip lost h-9/pt-1/pl-1.5 entirely and collapsed under the
title bar. Merged into one attribute, with a comment saying why there is
exactly one. A template sweep found no other element carrying two.
- connection-manager + editor-window <header>: the edit deleted all five
[class.h-*]/[class.pl-*] bindings and put nothing back, leaving both
headers with no height and no left padding. Restored h-9 pl-3 (the
non-mac arm) and dropped the now-false "on macOS this row IS the
titlebar" comments — every window is frameless now.
Separately, the ListView never filled its panel: 7.css sets table-layout:
fixed but declares no width, so the table shrank to content and left a dead
grey gutter down the right of each pane. w-full is safe as a utility
precisely because 7.css declares no width for `skin` to outrank it with;
the unsized first <col> then absorbs the slack, which is what makes Name
elastic.
Verified on a live window capture, not from a build log: the tab strip has
its height back and both tables reach the pane edge. The connection-manager
window needs a click to reach and is unverified visually — its fix is a
verbatim copy of the editor-window one.
Co-Authored-By: Claude <noreply@anthropic.com>
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 — it's a one-file change.
Developer docs
Dense reference material lives in docs/: architecture (processes, windows, RPC, every module/service), build & release (Build-n versioning, per-target release pipeline, CI), platform notes (macOS/Windows/Linux matrix, runtime deps + smoke tests), and gotchas (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 ≥ 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 systemwebkit2gtk. Details and diagnostics in platform notes. - 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
bun install # installs root + ui workspace deps
Develop
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:
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
miniSCP.sh, which sets the NVIDIA workaround then execs 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
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 for signing,
notarization, and release hosting).
Rebranding
Everything user-facing reads from src/shared/branding.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
appNamechanges the app-data directory. Add the old name tolegacyNamesand 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, orresolvedIP:port— the hostname is DNS-resolved, somyserver.lanand 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/rsyncuse 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) 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) 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): 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.jsonis protected with anicaclscurrent-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 — 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 runminiSCP.sh. - NVIDIA GPUs need
WEBKIT_DISABLE_DMABUF_RENDERER=1or the window renders black. Every launch path (dev scripts and the packagedminiSCP.shwrapper) sets it automatically when an NVIDIA driver is detected — see gotchas if you add a new way to launch the app.
Full dependency table, diagnostics and the first-boot smoke test: platform notes.
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.spinin 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).