Files
jgamboaandClaude 5be41db032 Redraw every icon on the pixel grid it renders at
On a 1x screen TDesign's glyphs smeared at 14 and 16px: their 24-unit
strokes land between pixels, so every ~1px line covered two at half ink.
All 37 icon/size pairs are now TDesign's shapes redrawn on their own grid
(viewBox 0 0 N N, so one unit is one CSS pixel):

- Straight lines are runs of whole pixels that end on pixel edges, with
  no caps or joins. WebKit drops square caps on a path of several
  subpaths, and Skia strokes 1px lines as hairlines that leave a joined
  corner pixel three-quarters inked.
- Slants and curves are 1.25px so they read as heavy as the crisp runs.
  Their ends, vertices and circle extremes sit on pixel centres.
- server24 keeps TDesign's 24px geometry; only its dots move onto whole
  pixels.

Only closeCircle24 (TDesign's own 24px grid) and the spinner still come
from @ng-icons/tdesign-icons. This also fixes add14 from 5d23809, whose
square-capped ends rendered half-inked in WebKit. icons.spec.ts now also
checks that every grid name's glyph uses that grid's viewBox.

Checked at 1x and 2x in WebKit (the app's macOS engine) and in Chrome
(Skia, as in WebView2 and WebKitGTK); straight runs fill whole pixels in
both. In WebKit at 1x, a copy of the tab bar, toolbar, file rows, menu
and shortcut-row markup rendered every straight-line glyph with zero
blur, including icons laid out on fractional pixels.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 13:49:42 +02:00

17 KiB
Raw Permalink Blame History

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 / ⌘E / toolbar / File menu): a built-in editor works on local and remote files directly; or pick an external app (Settings → 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), full keyboard control with remappable shortcuts (see below). Enter / double-click on a local file can open it in its default app (Settings → File list; off by default).
  • 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 2.0 via Hutch (native webview, typed RPC bridge)
Frontend Angular 22 (standalone, signals, zoneless) in ui/
Styling Tailwind CSS v4, Oxide-derived tokens (no component library)
Icons TDesign shapes redrawn on 14/16px pixel grids (@ng-icons)
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 system webkit2gtk. 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.
  • Network on the first bun install: Electrobun 2.x's npm package is a bootstrap that downloads its build tool (Hutch), the Electrobun core and the pinned Bun runtime into ~/.hutch (~200 MB), and projects the SDK into the gitignored .hutch/devkit/.

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 (Hutch builds for its own OS and arch):

bun run release -- --target mac-arm64   # on an Apple Silicon Mac
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 on the Mac; on Windows a -Setup.zip installer and a -portable.zip (unzip anywhere and run); on Linux Electrobun's -Setup.tar.gz installer. There is no Intel Mac build: Electrobun 2.x publishes no macOS x64 core. 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 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

The whole app works from the keyboard, and every command can be remapped in Settings → Keyboard: press + on a row and type the keys, see conflicts, reset one row or all. Remaps apply live in every window, the right-click menus' hints and the toolbar tooltips follow, and so does the macOS menu bar. The defaults:

Command macOS Windows / Linux
File list
Move up ↑ ↑
Move down ↓ ↓
Page up PgUp PgUp
Page down PgDn PgDn
First item Home Home
Last item End End
Select / deselect Space Space Ins
Select all ⌘A Ctrl+A
Open ↩ ⌘↓ Enter
Parent folder ⌫ ⌘↑ Backspace Alt+↑
Switch panel ⇥ ⇧⇥ Tab Shift+Tab
Show the item's menu ⇧F10 ⌃↩ Shift+F10 Menu
Files
Send to the other panel F5 F5
Edit ⌘E F4 F4 Ctrl+E
Rename F2 F2
New folder ⇧⌘N F7 F7 Ctrl+Shift+N
New file ⌘N ⇧F4 Shift+F4 Ctrl+N
Permissions ⌘I Ctrl+I
Delete F8 ⌦ F8 Del
Move to Trash ⌘⌫ Ctrl+Del
Copy path ⌥⌘C Ctrl+Shift+C
Reveal in file manager ⌥⌘R Alt+Shift+R
Open in default app ⌘O Ctrl+O
Refresh ⌘R Ctrl+R
Go to path ⌘L Ctrl+L
Choose what the panel shows ⌘K Ctrl+K
Sorting
Sort by name ⌥1 Alt+1
Sort by size ⌥2 Alt+2
Sort by modified ⌥3 Alt+3
Sort by kind ⌥4 Alt+4
Sort by rights ⌥5 Alt+5
Sort by owner ⌥6 Alt+6
Sort by group ⌥7 Alt+7
Tabs
New tab ⌘T Ctrl+T
Close tab ⌘W Ctrl+W
Close other tabs ⌥⌘W Ctrl+Shift+W
Close tabs to the right — —
Next tab ⌃⇥ ⇧⌘] Ctrl+Tab Ctrl+PgDn
Previous tab ⌃⇧⇥ ⇧⌘[ Ctrl+Shift+Tab Ctrl+PgUp
Go to tab 1 ⌘1 Ctrl+1
Go to tab 2 ⌘2 Ctrl+2
Go to tab 3 ⌘3 Ctrl+3
Go to tab 4 ⌘4 Ctrl+4
Go to tab 5 ⌘5 Ctrl+5
Go to tab 6 ⌘6 Ctrl+6
Go to tab 7 ⌘7 Ctrl+7
Go to tab 8 ⌘8 Ctrl+8
Go to last tab ⌘9 Ctrl+9
Move tab left ⌃⇧PgUp Ctrl+Shift+PgUp
Move tab right ⌃⇧PgDn Ctrl+Shift+PgDn
Disconnect tab ⌘D Ctrl+D
Window
Settings ⌘, Ctrl+,
Connection manager ⇧⌘K Ctrl+Shift+K
Show / hide transfers ⌘J Ctrl+J
Install update — —
Bigger interface ⌘= ⇧⌘= ⌘Num + Ctrl+= Ctrl+Shift+= Ctrl+Num +
Smaller interface ⌘- Ctrl+-
Interface at 100% ⌘0 Ctrl+0
Close window ⌘W Ctrl+W
Editor
Save ⌘S Ctrl+S
Find ⌘F Ctrl+F
Find next ⌘G F3
Find previous ⇧⌘G Shift+F3
Wrap lines ⌥Z Alt+Z
Connection manager
New connection ⌘N Ctrl+N
New stack ⇧⌘N Ctrl+Shift+N
Save connection ⌘S Ctrl+S
Connect / open stack ⌘↩ Ctrl+Enter
Delete connection or stack ⌘⌫ Del
Rename stack F2 F2
Import from WinSCP ⌘O Ctrl+O

Fixed keys (not remappable): Esc closes menus, dialogs and the find bar — in the file list it clears the filter, then cancels a connect, then clears the selection. Enter / Esc confirm / cancel dialogs and Tab walks their controls (on macOS too, without the system's Keyboard navigation setting). Typing in the file list filters it (Backspace trims); Shift + any movement key selects the rows the cursor passes. ↑ ↓ Home End Enter work in every menu.

(On Macs, function keys may need fn — every F-key command also has a ⌘ key or 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) is macOS-only for now; the in-app UI covers every menu action.

Right-click menus are HTML, on every platform (ui/src/app/core/context-menu.service.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 rendered at 1x on scaled displays on Electrobun 1.18; 2.x ships per-monitor DPI setup — to re-check on the first 2.x Windows build.
  • 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. The installer unpacks to %LOCALAPPDATA% and, since Electrobun 2.x, ships an uninstaller.
  • 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 ships Electrobun's own installer (-Setup.tar.gz). 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 no AppImage or .deb — unpack the -Setup.tar.gz and run its installer.
  • NVIDIA GPUs need WEBKIT_DISABLE_DMABUF_RENDERER=1 or the window renders black. The dev scripts set it automatically when an NVIDIA driver is detected; an installed build needs it set by hand (e.g. in its desktop entry) — see gotchas.

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) could 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. Electrobun 2.0.2 pins Bun 1.4.0, which may have fixed it — not yet confirmed under load. 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 AppImage/.deb packages (the release is Electrobun's Setup installer).