appsrc — how was this app installed?¶
appsrc answers a question no single OS command does: where did this app come
from — Homebrew cask, the Mac App Store, a direct-download dmg/pkg, apt, snap,
flatpak, an AppImage, a language package manager, or a manual drop-in? It
combines several signals, ordered by specificity, and captures the evidence.
- Source:
dot_dotfiles/bin/executable_appsrc(single-fileuv run --script,rich-only) - Deployed to
~/.dotfiles/bin/appsrc(on PATH); run from the repo viajust appsrc … - Picker twin:
tv appsrc· Completions:57_appsrc_completion.{zsh,bash}
Usage¶
# Single lookup (live, no cache) — a shell command OR a GUI app name:
$ appsrc which docker
docker (Homebrew cask)
kind cli
path /Applications/Docker.app/Contents/Resources/bin/docker
package docker-desktop
note CLI shim into Docker.app
$ appsrc which "Google Chrome" # GUI app by name
$ appsrc firefox # bare word == `appsrc which firefox`
$ appsrc which --path /Applications/Foo.app # classify a path directly
# Batch inventory (GUI apps + CLI tools), cached:
$ appsrc # == appsrc scan
$ appsrc scan --kind gui # GUI apps only
$ appsrc scan --source cask # filter by source id/label
$ appsrc scan --json | jq '.[] | select(.source=="manual")'
$ appsrc scan --tsv # name<TAB>source<TAB>path<TAB>kind<TAB>package
$ appsrc scan --refresh # ignore cache, recompute
# How much space does it cost (bundle + caches / data / containers)?
$ appsrc size docker # bundle + ~/Library/Containers/… + caches, totalled
$ appsrc size "Google Chrome" --json
$ appsrc scan --kind gui --size # add a bundle-size column to the inventory
# Navigate the whole inventory interactively:
$ appsrc tui # sortable dashboard — `s` sorts by size, `/` filters
$ appsrc scan --sort size # biggest first (implies --size)
which is always live and gathers full evidence (codesign signer, bundle id,
download-provenance URL). scan is cached (see below) and skips the slow
per-app forks for anything an authoritative inventory already claims.
What it detects¶
| Source id | Label | Platform |
|---|---|---|
homebrew-cask / homebrew-formula |
Homebrew cask / formula | macOS |
mac-app-store |
Mac App Store | macOS |
direct-download-pkg / direct-download-dmg |
Direct download (pkg / dmg-web) | macOS |
apt / snap / flatpak / appimage / linuxbrew |
apt·dpkg / snap / flatpak / AppImage / Linuxbrew | Linux |
cargo npm pipx uv mise go gem |
language package managers | both |
macos-system / manual / unknown |
OS-bundled / hand-placed / unattributable | both |
Each record carries a confidence — authoritative (a manager explicitly owns
it) or heuristic (inferred from provenance/signing).
How it decides (two passes)¶
Pass A — forward inventory (authoritative). Ask each manager once what it
owns and match in memory (no per-item forks): brew list --cask + a single
brew info --cask --json=v2 to map .app→cask, brew list --formula,
mas list; on Linux snap list / flatpak list / dpkg -S / Linuxbrew prefix;
CLI tools by resolved-path prefix (~/.cargo/bin, ~/.local/share/uv, …).
Pass B — per-item heuristics. For anything Pass A doesn't claim (and every
which), walk an ordered decision tree:
- macOS
.app: Caskroom metadata → cask;mas list/ App-Store receipt / Apple re-signing → App Store;pkgutil --file-inforeceipt → pkg;mdls kMDItemWhereFromsprovenance URL → dmg/web; else manual.codesignAuthority +CFBundleIdentifierare captured as tie-break evidence. - Linux command:
realpath→/snap/→ snap;/var/lib/flatpak→ flatpak;.appimage→ AppImage; Linuxbrew prefix → linuxbrew;dpkg -S/rpm -qfowner → apt;/usr/localor$HOMEunowned → manual.
Caveats baked in¶
_MASReceiptis NOT trusted alone — it exists in some standalone bundles too (seepitfalls/tailscale-another-copy-app-store-leftover.md). App Store is confirmed bymas listmembership,mdlsreceipt, or theApple Mac OS Application Signingcodesign Authority.- Shared user-bin trap. A binary in
~/.local/bincan't be attributed by path (uv, pipx, and go-with-a-redirectedGOBINall land there).appsrccontent-checks it withgo version— genuine Go binaries are labelledgo, everything elsemanual. It deliberately ignoresGOBINwhen it points at a shared dir. - Cask drift. A cask that declares an app but has no Caskroom entry (a
drag-installed
.app, perpitfalls/brew-bundle-redownloads-manually-installed-cask.md) is flaggedhomebrew-caskwithconfidence: heuristicand a drift note.
Space & associated storage (appsrc size)¶
appsrc size <name> (or --path) reports an app's real disk cost: the bundle
plus the storage it uses elsewhere — the caches, data, containers and prefs
an uninstaller would hunt for. The reveal is often large:
$ appsrc size docker
Docker (Homebrew cask) total 25.3G
bundle 2.1G other 23.3G
Size Category Path
2.1G bundle /Applications/Docker.app
23.3G container ~/Library/Containers/com.docker.docker
220.0K cache ~/Library/Caches/com.docker.docker
4.0K prefs ~/Library/Preferences/com.docker.docker.plist
size prefers the GUI app over a same-named CLI (a CLI shim living inside a
.app is mapped back to the app), so appsrc size docker sizes Docker.app +
its containers, not the docker shim. It's always live (no cache).
How associated storage is found:
- exact — keyed on the app's bundle id: ~/Library/{Caches,HTTPStorages,
Containers,WebKit}/<bid>, Preferences/<bid>.plist, Saved Application State,
Group Containers/*<bid>*, matching LaunchAgents/LaunchDaemons.
- fuzzy (? in the table) — name candidates (app name, CFBundleName,
bundle-id product, Vendor/Product) against Application Support / Caches /
Logs; for CLIs, the XDG dirs ~/.config|.cache|.local/share|.local/state/<name>.
Caveats: fuzzy matches are heuristic — they catch Chrome's Application
Support/Google/Chrome and VSCode's Code, but a differently-named data dir can
be missed, and a shared vendor dir can over-match. Every row shows its real path
and is tagged exact/fuzzy; nothing is ever deleted. Overlapping
parent/child paths are de-duplicated so space isn't double-counted. System
(/Library) paths that need root are listed but marked needs root (no sudo).
du is bounded by a 20 s per-path timeout (→ ?), so a pathological cache dir
can't hang the tool.
appsrc scan --size adds a bundle-size column to the inventory (bundle only —
associated storage is size-command territory); it's cached per-item by mtime,
so re-scans are fast and a bundle size refreshes when the app updates.
Cache¶
scan caches to ${XDG_CACHE_HOME:-~/.cache}/appsrc/<hostname>.json, keyed
per item by mtime — a re-scan only recomputes apps whose bundle changed. The
forward-inventory calls (cheap) always re-run. Flags: -r/--refresh (recompute
all), --no-cache (don't read or write). Ballpark on a laptop: full GUI+CLI
scan ~15 s cold, ~5 s warm (~2 200 items); which is instant.
Integration¶
appsrc tui— interactive Textual dashboard over the full inventory. A background worker scans (fast, no sizes);/filters by name/source,ssizes the currently-visible rows and sorts biggest-first (boundingducost to what's on screen),o/nsort by source/name,Enteropens the size breakdown,t/mshow the selected tool'stldr/manpage (suspends into the pager),y/rcopy path / reveal,Rrescan,qquits. Needs a real terminal.tv appsrc— fuzzy-pick the full GUI+CLI inventory; type a source label (cask,manual,app-store,apt…) to narrow.Enter= detail,Alt+S= size,Alt+T/Alt+M= tldr/man,Alt+C= copy path,Alt+O= reveal,Alt+J= JSON.tv appsrc-size— the same picker pre-sorted by footprint (biggest first, human sizes shown) for hunting space hogs; preview/Enter= the size report.- Completions shell out to
appsrc scan --list-namesfor GUI app names + PATH commands, soappsrc which/size <Tab>completes either.
appsrc is the runtime companion to
docs/this_repo/tool-managers.md, which records
how this repo installs tools by design; appsrc inspects whatever is
actually on the machine.
Maintaining it¶
Four things bite if you don't know them:
- Everything lives in the one launcher. Detection, sizing and the inline
AppsrcTUIare all indot_dotfiles/bin/executable_appsrc— there is deliberately noscripts/appsrc/package; the TUI calls the same module functions the CLI does.textualis declared in the PEP 723 header only and imported lazily insidedo_tui, so the plain CLI never pays for it. - Mirror the mnemonics. The TUI's
BINDINGSletters (size /osource /name /tldr /man /ycopy /reveal) and thetv appsrc/tv appsrc-sizechannels'Alt+<letter>actions are intentionally the same letters so muscle memory carries between the fast picker and the sortable dashboard. Change one → change both, same commit. scan --tsvcolumn order is a wire format. It isname·source·path·kind·package[·size], and both.tomlchannels index into it with{split:\t:N}. Reordering or inserting a column silently shifts every picker field — fix the channels in the same commit.- TUI subprocess work must be asyncio-native. Never call blocking
subprocessinside a@work(thread=True)worker; it intermittently deadlocks into an empty table with no crash (a Textual pilot test still passes — only a real terminal hangs). Usecreate_subprocess_exec. Seepitfalls/appsrc-tui-subprocess-thread-hang.md. Relatedly, the filterInputstayscan_focus=Falseuntil/opens it — otherwise it swallows the single-letter keybindings.