A keyboard-driven, colored terminal UI for browsing the filesystem with Finder-level file operations, plus first-class iCloud Drive awareness: it shows which files are evicted (cloud-only), lets the user mark files/folders for download, runs a download queue with progress, and can evict local copies back to the cloud.
- Language / stack: Go + Bubble Tea (single static-ish binary; one small cgo bridge to Apple frameworks)
- Target platform: macOS 13+ (primary: macOS 26 "Tahoe"-era, FileProvider-based iCloud Drive). Non-macOS builds compile and run as a plain file browser with iCloud features disabled.
- Binary name:
mytermtui
These were verified on the target machine (macOS 26.4, Darwin 25.4.0) and are the foundation of the iCloud feature set. Do not design around .icloud placeholder files or brctl download — both are legacy.
- iCloud Drive root:
~/Library/Mobile Documents/com~apple~CloudDocs(Desktop/Documents sync also lives under~/Library/Mobile Documents). - Evicted ("cloud-only") files are dataless files, not placeholders. They appear with their real name and full logical size in
ls, but occupy zero blocks and carry theSF_DATALESSflag:A fully local file hassize=247393316 blocks=0 flags=0x40000060 # SF_DATALESS = 0x40000000blocks > 0and noSF_DATALESSbit. Detection is therefore a plainlstat:st_flags & SF_DATALESS != 0. In Go:syscall.Stat_t.Flags & 0x40000000. - Directories can be dataless too (contents not yet enumerated locally). Reading such a directory materializes its listing (cheap), not its files' contents.
brctl download/brctl evictno longer exist on macOS 26 (brctlretains only diagnose/log/dump/status/accounts/quota/monitor).fileproviderctlexposes no materialize/evict either. There is no supported CLI for download/evict anymore.- The supported programmatic APIs (Foundation, via cgo):
- Download:
-[NSFileManager startDownloadingUbiquitousItemAtURL:error:]— asynchronous; returns immediately, fileproviderd downloads in background. - Evict:
-[NSFileManager evictUbiquitousItemAtURL:error:]. - Status:
NSURLresource values (NSURLUbiquitousItemDownloadingStatusKey,NSURLIsUbiquitousItemKey). - Trash:
-[NSFileManager trashItemAtURL:resultingItemURL:error:].
- Download:
- Fallback download without cgo: simply
open()+read()a dataless file — the kernel materializes it on access (blocking). Usable but inferior (blocks a thread per file, no cancel). cgo path is the primary mechanism. - Accidental-download hazard: any tool that reads file contents (preview, checksum, grep) will silently trigger a download of a dataless file. The app must guard against this: a thread can opt out via
setiopolicy_np(IOPOL_TYPE_VFS_MATERIALIZE_DATALESS_FILES, IOPOL_SCOPE_THREAD, IOPOL_MATERIALIZE_DATALESS_FILES_OFF), after which reads on dataless files fail withEDEADLKinstead of downloading. The preview pane MUST run under this policy or skip dataless files. - Progress (revised after implementation): fileproviderd stages downloads out of view —
st_blocksstays 0 andSF_DATALESSremains set until the finished payload is swapped in — so lstat polling shows nothing. Spotlight hides ubiquitous attributes from non-entitled processes, andNSProgressfile-progress subscriptions receive no publications (both verified empirically). The one accessible source is Apple's entitledbrctl status, which prints per-itemdownloading:NN.N%lines with elided names (first{middle-count}last.ext) and exact sizes; the app polls it asynchronously and matches items by directory, size, and name pattern. - Filenames contain exotic Unicode (e.g. screen-recording names use U+202F narrow no-break space before "PM"). All paths are opaque bytes end-to-end; never round-trip through shell strings. Display through a sanitizer that preserves visual fidelity.
- Fast, keyboard-only navigation of the whole filesystem with a colorful, discoverable TUI (menus + status bar + help), usable by people who don't know vim.
- Finder-parity file management: open, rename, copy/move/duplicate, new folder/file, trash (recoverable, not
rm), get info, compress, quick look, reveal in Finder, hidden files, sorting, search. - iCloud: per-file sync status at a glance; mark any set of files/folders for download; queued, cancellable downloads with progress; evict local copies; never trigger downloads accidentally.
- Single runnable binary,
go build-simple, config optional.
- Not a file transfer tool (no SFTP/S3/MTP), no archives-as-virtual-dirs, no bulk rename engine, no plugin system.
- No Finder tags/labels editing, no Spotlight comment editing (read-only display is fine later).
- No management of non-iCloud FileProvider domains (Dropbox/GDrive) in v1 — the dataless detection will incidentally work for them; official support later.
- No mouse requirement (mouse clicks may work via Bubble Tea, but every feature must be reachable by keyboard).
┌ File Edit View Go iCloud Help ──────────────────────── mytermtui ┐ ← menu bar (F-keys/Alt)
│ 📁 ~/Library/Mobile Documents/com~apple~CloudDocs/demo-docs │ ← breadcrumb (editable)
├────────────────────────────────────────────┬───────────────────────────┤
│ Name Size Modified│ Preview / Info panel │
│ ▸ archived-recordings -- May 7 ☁│ (toggle: F3 / p) │
│ ▸ meeting-notes -- Dec 22 ✓│ │
│ ● demo-video-1.mov 2.9 GB Aug 13 ☁│ demo-video-2.mov │
│ ● demo-video-2.mov 247 MB Jun 27 ⇣│ Kind: QuickTime movie │
│ demo-video-3.mov 2.0 GB Sep 4 ☁│ Size: 247.4 MB (0 local) │
│ screenshot.png 14 KB Jan 3 ✓│ iCloud: ⇣ downloading 41%│
├────────────────────────────────────────────┴───────────────────────────┤
│ ⇣ Queue 2/5 · 1.2 GB of 5.6 GB · demo-video-2.mov 41% │ ← download bar (when active)
│ 3 selected (5.1 GB) · 23 items · sort: name ↑ · [?] help │ ← status bar
└─────────────────────────────────────────────────────────────────────────┘
- Menu bar — real pull-down menus (File/Edit/View/Go/iCloud/Help) opened with
F10orAlt+letter; navigable with arrows; every item shows its shortcut. Menus are the discoverability layer; shortcuts are the speed layer. - File list — the main pane. Columns: selection dot, name (with type icon), size, modified, iCloud status glyph. Column set is configurable.
- Preview/info panel — optional right split: text-file head, directory summary, image dimensions, and full metadata (Get Info). Never materializes dataless files (§1.7); shows "☁ evicted — press d to download" instead.
- Download bar — appears while the queue is non-empty: overall progress, current file, speed; hidden otherwise.
- Status bar — selection count/size, item count, sort mode, filter state, transient messages/errors.
- Modals — centered overlays for confirm dialogs, rename/new-name input, go-to-path (with tab completion), search, help (full keymap), and the queue manager.
| Glyph | State | Detection |
|---|---|---|
✓ (green) |
Local & synced | not dataless, inside iCloud root |
☁ (blue) |
Evicted, cloud-only | SF_DATALESS set |
⇣ (yellow, animated) |
Downloading | in queue / blocks growing |
⇡ (yellow) |
Uploading / not yet synced | brctl status parse (best-effort, v1.1) |
◌ (dim) |
Marked for download, queued | app state |
| (none) | Outside iCloud | path not under an iCloud root |
Directory rows aggregate: ☁ if any descendant known-evicted (computed lazily on visit, never recursively scanned up front), ✓ otherwise.
Dual scheme, both always active: arrows/Enter/F-keys (Finder-like, discoverable) and vim-ish letters (fast). All rebindable in config.
Navigation
| Key | Action |
|---|---|
↑/↓ or k/j |
move cursor |
→/l or Enter |
open: enter directory / open file with default app (open) |
←/h or Backspace |
go to parent |
g / G |
top / bottom · PgUp/PgDn page |
⌥←/⌥→ or [ / ] |
history back / forward |
~ |
home · / root · i iCloud Drive root |
⌘G or : |
go to path (input with tab-completion) |
z |
toggle hidden files · s sort menu (name/size/modified/kind, asc/desc, dirs-first) |
f |
filter-as-you-type in current dir · F recursive fuzzy find under cwd |
Selection
| Key | Action |
|---|---|
Space |
toggle select + move down |
v |
range-select mode · a select all · A/Esc clear |
File operations (act on selection, else cursor item)
| Key | Action |
|---|---|
c / x / p |
copy / cut / paste (app-internal clipboard) |
r or F2 |
rename (inline input, pre-filled) |
D or F8/Delete |
move to Trash (native trash — recoverable; confirm) |
⌘D |
duplicate ("name copy.ext") |
n / N or F7 |
new folder / new empty file |
o |
open · O open-with menu (apps from LaunchServices, fallback: prompt) |
Enter on app/pkg |
descend into bundle ("Show Package Contents") when z-hidden mode on |
q or F3 on file |
Quick Look (qlmanage -p, suspends TUI) — blocked on dataless files |
I or ⌘I |
Get Info panel (perms, dates, kind via UTI, size incl. on-disk vs logical, iCloud state) |
Z |
compress selection to .zip (ditto -ck --sequesterRsrc) |
R |
reveal in Finder (open -R) |
T |
open Terminal here · . copy path(s) to system clipboard (pbcopy) |
u |
undo last op (rename/move/trash restore — single-level, best-effort) |
iCloud (the differentiator)
| Key | Action |
|---|---|
d |
mark selection/cursor for download (dirs: recursive) → adds to queue |
e |
evict selection (remove local copy, keep in cloud; confirm; refuses if not yet uploaded) |
⌘d |
download entire current directory |
Q |
queue manager modal: reorder, pause/resume, cancel items, cancel all |
S |
iCloud summary for cwd: local vs evicted counts/bytes (computed on demand) |
App
| Key | Action |
|---|---|
? or F1 |
help overlay (searchable keymap) · F10/Alt+F menus · Ctrl+r refresh dir · Q(menu)/Ctrl+q quit (confirm if queue active) |
- Truecolor with 256-color fallback (Lip Gloss adaptive profiles); respects light/dark terminal backgrounds.
- Row coloring by kind: directories bold blue, symlinks cyan (broken: red), executables green, images/media magenta, hidden dimmed; selected rows get accent background; cursor row inverted.
- Built-in themes:
default(respects terminal palette),dracula,solarized; user themes in config.
mytermtui/
main.go // flag parsing (start dir, --version), tea.NewProgram
internal/ui/ // Bubble Tea: root model, panes, menus, modals, theming
internal/fs/ // dir listing, watchers, file ops (copy/move/trash…), sorting, filtering
internal/icloud/ // dataless detection, download/evict bridge, queue, progress polling
bridge_darwin.go // cgo → Foundation (startDownloading…, evict…, trashItem…)
bridge_stub.go // !darwin: everything returns ErrUnsupported
internal/config/ // TOML config + keymap + themes (~/.config/mytermtui/config.toml)
- Root
Modelholds: current dir state (entries, cursor, selection, sort/filter), history stack, panes' visibility, active modal, queue snapshot, theme, keymap. - All I/O via
tea.Cmds — the update loop never touches the disk. Directory reads, file ops, and stat-polls run in goroutines and come back as messages (dirLoadedMsg,opDoneMsg{err},queueTickMsg…). - Large-directory strategy:
os.ReadDirin a worker, entries streamed in batches (first paint < 50 ms even on 100k-entry dirs); lstat/iCloud-status enrichment done lazily for the visible window ± one page. - Live refresh: FSEvents is overkill for v1 — refresh on focus/op-completion plus a 2 s lstat re-check of visible rows (cheap, also drives glyph updates while downloads land).
- Detection (
icloud.Status(path)):lstat→Flags & SF_DATALESS; plusIsInICloud(path)by prefix-matching resolved path against~/Library/Mobile Documents/…. Pure Go, no cgo. - Bridge (cgo, darwin only): thin wrappers over
NSFileManagerstartDownloadingUbiquitousItemAtURL,evictUbiquitousItemAtURL,trashItemAtURL. Each takes a path, returns error string. No Objective-C beyond one.m-free cgo block; links-framework Foundation. - Download queue (
icloud.Queue):- FIFO of items (file paths; directories expand to their dataless descendants via a background walk that reads only listings, never contents).
- Concurrency: fileproviderd does the actual transfer, so "start" is cheap — but we cap in-flight materializations (default 3) to keep progress legible and disk pressure sane.
- Per-item lifecycle:
queued → starting → downloading(pct) → done | failed(err) | cancelled. - Progress: 500 ms tick per in-flight item; percent comes from a throttled async
brctl statusparse (see §1.8), never regressing between slow polls; overall bar aggregates bytes. Stalled > 30 s ⇒ markedstalledwith hint (network/quota). - Cancel: best-effort — evict the partially-materialized item. Persisted queue (JSON in state dir) so an interrupted session can resume marks.
- Safety rails: the preview/info goroutines set
IOPOL_MATERIALIZE_DATALESS_FILES_OFF(via the bridge) so only explicit queue items ever trigger downloads. Evict requires confirm and skips files with unsynced local changes (best-effort check viabrctl statusparse; on ambiguity, warn).
- Copy/move:
os.Renamefast-path, cross-device fallback to streamed copy with progress (reuses the download-bar UI); preserves permissions, times, xattrs (clonefile(2)fast-path on APFS viaunix.Clonefile). - Copying a dataless file requires materialization — prompt: "3 items are cloud-only (5.1 GB). Download first?" (never silently pull gigabytes).
- Trash via bridge
trashItemAtURL(correct Finder semantics incl. put-back); fallback~/.Trashmove if the call fails. - Every mutating op returns an
Undoclosure where feasible (rename back, move back, un-trash via saved put-back URL); single-level undo stack. - Name collisions: modal — Replace / Keep both (
copy,2) / Skip / Cancel, with "apply to all".
~/.config/mytermtui/config.toml (all optional):
[general]
start_dir = "~" # or "last"
show_hidden = false
confirm_trash = true
dirs_first = true
[icloud]
max_concurrent_downloads = 3
poll_interval_ms = 500
[theme]
name = "default" # or table of custom colors
[keys] # action = ["key", "key"...]
download = ["d"]
quit = ["ctrl+q"]| Dep | Purpose |
|---|---|
github.com/charmbracelet/bubbletea |
TUI runtime (Elm architecture) |
github.com/charmbracelet/bubbles |
list/viewport/textinput/progress/help components |
github.com/charmbracelet/lipgloss |
styling, layout, adaptive color |
golang.org/x/sys/unix |
lstat flags, Clonefile, iopolicy syscall |
github.com/BurntSushi/toml |
config |
cgo + -framework Foundation (darwin) |
download/evict/trash bridge |
Build: go build ./... → single binary (CGO_ENABLED=1 on darwin; pure Go elsewhere). No runtime dependencies; qlmanage/open/ditto/pbcopy are macOS built-ins invoked opportunistically.
- Permissions: browsing
~/Libraryand iCloud roots needs Full Disk Access for the terminal app. OnEPERM/EACCESat an iCloud path, show a one-line hint: "Grant Full Disk Access to your terminal in System Settings → Privacy & Security." - Unicode filenames: paths treated as bytes; rendering strips control chars and uses width-aware truncation (
…middle-ellipsis, Finder-style). Never construct shell command strings from names — always exec with arg vectors. - Huge dirs: streamed listing (§4.2); filter and fuzzy-find operate on the loaded snapshot.
- Symlink loops: never auto-resolve recursively; show target in info panel.
- Disk-full during download: surface fileproviderd stall as
stalled, keep queue paused-able. - Non-macOS / no iCloud account: iCloud menu items disabled with tooltip, everything else works.
- Terminal resize down to 60×15 gracefully (panels auto-collapse: preview → download bar → menu bar hints).
internal/fs: table-driven unit tests on tmpdirs (ops, collisions, sorting, undo).internal/icloud: detection tested against fixture stat flags; queue state-machine tested with a fake bridge; bridge itself smoke-tested manually (documented script) since it needs a live iCloud account.- UI:
teatestgolden-file tests for key flows (navigate, select, rename modal, queue render). - Manual acceptance checklist including: evicted
.movincom~apple~CloudDocsshows☁,d+ queue downloads it to✓,ereturns it to☁, preview of evicted file does not download it.
- M1 — Browser skeleton: listing, navigation, sorting, hidden toggle, theming, status bar, help overlay.
- M2 — iCloud read-only: dataless detection, status glyphs, iCloud summary, guarded preview (iopolicy).
- M3 — Download/evict: cgo bridge, mark + queue + progress bar + queue manager, evict.
- M4 — File ops: copy/cut/paste/rename/trash/duplicate/new/undo, collision dialogs, open/open-with/Quick Look/reveal.
- M5 — Polish: menus, go-to-path, fuzzy find, config/keymap, compress, Get Info, persistence, docs.
Each milestone ends runnable; M1–M3 alone already solve the motivating use case (find ☁ cloud-only videos, mark, download).