feat(md): add an md wrapper for MarkText, with an optional read-only sandbox #151

Merged
rootiest merged 2 commits from feat/md-marktext-wrapper into main 2026-09-16 07:58:30 +00:00
Owner

What

Adds md, a wrapper that launches MarkText detached from the terminal, and registers marktext and firejail as Optional-tier dependencies of fish-deps.

md forwards every argument to marktext untouched except two flags of its own:

Flag Effect
-r, --read-only Sandbox with firejail so saving fails instead of overwriting the file
--foreground Do not detach; run in the foreground

Flags whose entire purpose is terminal output — --version, -v/--verbose, --debug — imply --foreground, since backgrounding them would send the requested output to /dev/null.

The read-only sandbox

MarkText has no read-only mode, so -r binds each named file read-only with firejail. The non-obvious part is that MarkText is single-instance: a plain sandboxed launch hands the file to an already-running — unsandboxed, writable — window and exits, silently defeating the sandbox. -r therefore also passes a private --user-data-dir, which forces an independent instance that the read-only bind actually covers, plus --no-sandbox, since Electron's own sandbox needs the user namespaces firejail has already taken away.

Verified against the real binaries: firejail --read-only=<file> marktext --no-sandbox --user-data-dir=<dir> --version launches, and a write to a --read-only-bound file inside the sandbox fails with Permission denied, leaving the file unchanged.

Why no conf.d file and no opinionated guard

md is autoloaded, so it can never shadow an md function or alias defined elsewhere — fish only looks in functions/ when nothing named md already exists. A real md binary would be shadowed, so the body hands the name straight back to it whenever marktext is not installed. No guard variable and no source-time gating are needed for either case.

It also carries no C1 guard: md is a novel command name rather than a command shadow, the same as bkg and detach.

Dependencies

Both new entries are Optional tier (skipped unless fish-deps install --optional/--all). firejail is a plain system package everywhere. marktext is not — upstream ships an AUR package and its own GitHub release assets, and no distro carries it under a common name — so its _fdc_pm entry is deliberately empty and a marktext-release special offers:

  1. paru/yay -S marktext-bin where an AUR helper is present
  2. otherwise, upstream's AppImage installed to ~/.local/bin/marktext

The release assets embed their version in the filename, so there is no stable /releases/latest/download URL; _fish_deps_marktext_appimage reads the download URL from the GitHub API. Upstream builds the Linux AppImage for x86_64 only, and the helper says so rather than downloading an unusable binary.

fish-deps update refreshes marktext through the AUR where available, and otherwise only when ~/.local/bin/marktext exists — a distro-packaged marktext belongs to that package manager, and dropping an AppImage into ~/.local/bin would shadow it.

Testing

New tests/test-md.fish (16 assertions) stubs marktext, firejail and bkg as functions that print their arguments, so it asserts on the assembled command line without launching an editor and passes on a machine that has none of the three installed. It covers flag forwarding, flag stripping, the implied-foreground set, relative-path resolution for the firejail bind, the private user-data directory, and the failure when -r names no existing file.

Full suite: 249/249 syntax, 249/249 indent, test-md 16/16. The 6 pre-existing mkrep assertion failures on main are unchanged by this branch.

Docs

docs/manual/06-dependency-catalog.md gains a row for each new dependency; md's manual entry is generated from its header comment. docs/fish-config.md and docs/fish-config.1 are regenerated.

## What Adds `md`, a wrapper that launches MarkText detached from the terminal, and registers `marktext` and `firejail` as Optional-tier dependencies of `fish-deps`. `md` forwards every argument to `marktext` untouched except two flags of its own: | Flag | Effect | |---|---| | `-r`, `--read-only` | Sandbox with firejail so saving fails instead of overwriting the file | | `--foreground` | Do not detach; run in the foreground | Flags whose entire purpose is terminal output — `--version`, `-v`/`--verbose`, `--debug` — imply `--foreground`, since backgrounding them would send the requested output to `/dev/null`. ## The read-only sandbox MarkText has no read-only mode, so `-r` binds each named file read-only with firejail. The non-obvious part is that **MarkText is single-instance**: a plain sandboxed launch hands the file to an already-running — unsandboxed, writable — window and exits, silently defeating the sandbox. `-r` therefore also passes a private `--user-data-dir`, which forces an independent instance that the read-only bind actually covers, plus `--no-sandbox`, since Electron's own sandbox needs the user namespaces firejail has already taken away. Verified against the real binaries: `firejail --read-only=<file> marktext --no-sandbox --user-data-dir=<dir> --version` launches, and a write to a `--read-only`-bound file inside the sandbox fails with `Permission denied`, leaving the file unchanged. ## Why no conf.d file and no opinionated guard `md` is autoloaded, so it can never shadow an `md` function or alias defined elsewhere — fish only looks in `functions/` when nothing named `md` already exists. A real `md` *binary* would be shadowed, so the body hands the name straight back to it whenever `marktext` is not installed. No guard variable and no source-time gating are needed for either case. It also carries no C1 guard: `md` is a novel command name rather than a command shadow, the same as `bkg` and `detach`. ## Dependencies Both new entries are Optional tier (skipped unless `fish-deps install --optional`/`--all`). `firejail` is a plain system package everywhere. `marktext` is not — upstream ships an AUR package and its own GitHub release assets, and no distro carries it under a common name — so its `_fdc_pm` entry is deliberately empty and a `marktext-release` special offers: 1. `paru`/`yay -S marktext-bin` where an AUR helper is present 2. otherwise, upstream's AppImage installed to `~/.local/bin/marktext` The release assets embed their version in the filename, so there is no stable `/releases/latest/download` URL; `_fish_deps_marktext_appimage` reads the download URL from the GitHub API. Upstream builds the Linux AppImage for x86_64 only, and the helper says so rather than downloading an unusable binary. `fish-deps update` refreshes marktext through the AUR where available, and otherwise only when `~/.local/bin/marktext` exists — a distro-packaged marktext belongs to that package manager, and dropping an AppImage into `~/.local/bin` would shadow it. ## Testing New `tests/test-md.fish` (16 assertions) stubs `marktext`, `firejail` and `bkg` as functions that print their arguments, so it asserts on the assembled command line without launching an editor and passes on a machine that has none of the three installed. It covers flag forwarding, flag stripping, the implied-foreground set, relative-path resolution for the firejail bind, the private user-data directory, and the failure when `-r` names no existing file. Full suite: 249/249 syntax, 249/249 indent, `test-md` 16/16. The 6 pre-existing `mkrep` assertion failures on `main` are unchanged by this branch. ## Docs `docs/manual/06-dependency-catalog.md` gains a row for each new dependency; `md`'s manual entry is generated from its header comment. `docs/fish-config.md` and `docs/fish-config.1` are regenerated.
rootiest added the Kind/FeatureArea/FunctionsArea/DocsArea/Tests labels 2026-09-16 07:12:46 +00:00
rootiest added 2 commits 2026-09-16 07:57:06 +00:00
`md` forwards every argument to marktext untouched except two flags of its
own: `--read-only`/`-r` and `--foreground`. By default it detaches via
`bkg`, so the shell stays usable and the editor outlives the window that
launched it.

MarkText has no read-only mode, so `-r` sandboxes it with firejail, binding
each named file read-only. The subtlety is that MarkText is single-instance:
a plain launch hands the file to an already-running, unsandboxed, writable
window and exits, silently defeating the sandbox. `-r` therefore also passes
a private `--user-data-dir`, which forces an independent instance the
read-only bind actually covers, plus `--no-sandbox`, since Electron's own
sandbox needs the user namespaces firejail has already taken away.

Flags whose entire purpose is terminal output (`--version`, `-v`/`--verbose`,
`--debug`) imply `--foreground`; backgrounding them would send the output
you asked for to /dev/null.

The function is autoloaded and so never shadows an `md` function or alias
defined elsewhere -- fish only looks in functions/ when nothing named `md`
exists. A real `md` *binary* would be shadowed, so the body hands off to it
verbatim whenever marktext is not installed. No conf.d file and no
opinionated guard: `md` is a novel name rather than a command shadow, the
same as `bkg` and `detach`.
Both back the new `md` wrapper and nothing else, so both land in the
Optional tier, skipped by `fish-deps install`/`sync` unless `--optional`
(or `--all`) is passed.

firejail is a plain system package everywhere, so it needs no special
handling. marktext is not: upstream ships an AUR package and its own
GitHub release assets, and no distro carries it under a common name. Its
`_fdc_pm` entry is therefore deliberately empty, and a `marktext-release`
special offers the AUR package via paru/yay where one is present and
otherwise installs upstream's AppImage to ~/.local/bin/marktext.

The release assets embed their version in the filename, so there is no
stable /releases/latest/download URL to fetch -- `_fish_deps_marktext_appimage`
reads the download URL from the GitHub API instead. Upstream builds the
Linux AppImage for x86_64 only, and the helper says so rather than
downloading an unusable binary.

`fish-deps update` refreshes marktext through the AUR where available, and
otherwise only when ~/.local/bin/marktext exists -- a distro-packaged
marktext belongs to that package manager, and dropping an AppImage into
~/.local/bin would shadow it.
rootiest force-pushed feat/md-marktext-wrapper from d9dec4c11d to f153a3db87 2026-09-16 07:57:06 +00:00 Compare
rootiest merged commit fc5ecb591b into main 2026-09-16 07:58:30 +00:00
rootiest deleted branch feat/md-marktext-wrapper 2026-09-16 07:58:31 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rootiest/fish-config#151