Files
miniSCP/README.md
T
jgamboaandClaude 8f8f230711 Remap any shortcut in Settings → Keyboard; the macOS menu bar follows
A Keyboard section in the Settings window edits the shared keymap: rows
by group with a search field, a chip per key (x removes), + to record,
reset per row and Reset all. Recording takes the keydown before every
other key in the window (Cmd+W records instead of closing), refuses
reserved combos without preventDefault (Cmd+Q still quits), requires a
chord or F-key for editor commands, and offers Replace when another
command in an overlapping scope owns the key. Rows flag conflicts and
plain character keys that would stop starting a type-to-filter query.
Fixed keys are listed read-only, in the platform's spelling.

Remaps are ordinary settings (only rows that differ from the defaults),
so every window, the menu hints and the tooltips follow live — and the
macOS menu bar: its accelerators are built from the keymap (only the
combos Electrobun's parser expresses faithfully) and rebuilt on a remap.
A main-window menu action fired from the editor or Settings window
brings the main window forward first.

Also: the "Enter / double-click on a file" setting under File list (do
nothing / open local files in their default app), arrow keys in the
Settings sidebar with focus on open and a hand-walked Tab, translations
for all seven languages (the spec now checks every language labels every
command), and a README shortcut table generated from the keymap.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-05 14:57:27 +02:00

383 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 / ⌘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 | @ng-icons/tdesign-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](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.
- 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
```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 (Hutch builds for its own OS and arch):
```sh
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
```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://electrobun.dev/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
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](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](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](https://github.com/blackboardsh/electrobun/issues/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](#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](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) 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).