From 8aa2d15ae89e65746afcc22bf893e0dbc34356fd Mon Sep 17 00:00:00 2001 From: Rootiest Date: Wed, 16 Sep 2026 03:10:14 -0400 Subject: [PATCH] feat(md): launch MarkText detached, with an optional read-only sandbox `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`. --- functions/md.fish | 124 +++++++++++++++++++++++++++++++++++++++++++++ tests/test-md.fish | 71 ++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 functions/md.fish create mode 100644 tests/test-md.fish diff --git a/functions/md.fish b/functions/md.fish new file mode 100644 index 0000000..2a639f7 --- /dev/null +++ b/functions/md.fish @@ -0,0 +1,124 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# CATEGORY +# 03-editors-and-viewers +# +# DEPENDENCIES +# marktext, firejail, bkg +# +# SYNOPSIS +# md [-r] [--foreground] [marktext-args...] [FILE...] +# +# DESCRIPTION +# Opens files in MarkText, detached from the terminal so the shell stays +# usable and the editor survives closing the window that launched it. +# +# Every argument is forwarded to marktext untouched except the two flags +# below, which md consumes itself. marktext's own flags (--new-window, +# --safe, --disable-gpu, ...) therefore work exactly as documented in +# marktext --help. +# +# Flags whose entire purpose is terminal output -- --version, -v/--verbose +# and --debug -- imply --foreground, since backgrounding them would send +# the output you asked for to /dev/null. +# +# --read-only sandboxes the editor with firejail so saving fails instead of +# overwriting the file. MarkText has no read-only mode of its own. +# +# ARGUMENTS +# FILE... Markdown files to open +# -r, --read-only Open sandboxed, with every named file bound read-only +# --foreground Run in the foreground; do not detach +# -h, --help Show this help message +# +# EXIT STATUS +# 0 MarkText launched (or, with --foreground, exited successfully) +# 1 --read-only was requested without firejail or without an existing file +# +# EXAMPLE +# md README.md +# md --read-only NOTES.md +# md --foreground --debug draft.md +# md --new-window one.md two.md +# +# NOTES +# This file is autoloaded, so it never shadows an md function or alias +# defined elsewhere -- fish only looks here when nothing named md exists. +# A real md *binary* would be shadowed, so md hands off to it verbatim +# whenever marktext is not installed. +function md --wraps marktext --description 'Launch MarkText detached from the terminal' + # Without marktext this wrapper has nothing to offer, so give the name + # back to whatever md the system does provide. + if not type -q marktext + command md $argv + return $status + end + + __fish_help_header (status current-function) $argv; and return 0 + + # Split our own two flags out of the argument list. Everything else is + # marktext's business and is forwarded verbatim. + set -l read_only 0 + set -l foreground 0 + set -l args + for arg in $argv + switch $arg + case -r --read-only + set read_only 1 + case --foreground + set foreground 1 + case '*' + set -a args $arg + end + end + + # Backgrounding a flag that exists to print something defeats it. + for flag in --version -v --verbose --debug + if contains -- $flag $args + set foreground 1 + break + end + end + + set -l cmd marktext $args + + if test $read_only -eq 1 + if not type -q firejail + echo "md: --read-only requires firejail" >&2 + return 1 + end + + # firejail rejects relative --read-only targets. + set -l ro_flags + for arg in $args + if test -e $arg + set -a ro_flags --read-only=(path resolve $arg) + end + end + if test (count $ro_flags) -eq 0 + echo "md: --read-only needs an existing file to protect" >&2 + return 1 + end + + # MarkText is single-instance: a plain launch hands the file to an + # already-running -- unsandboxed, writable -- window and exits, + # silently defeating the sandbox. A private user-data directory + # forces an independent instance that the read-only bind covers. + set -l cache (set --query XDG_CACHE_HOME; and echo $XDG_CACHE_HOME; or echo "$HOME/.cache") + set -l ro_data "$cache/marktext-readonly" + mkdir -p $ro_data + or return 1 + + # --no-sandbox: Electron's own sandbox needs user namespaces that + # firejail has already taken away. + set cmd firejail $ro_flags marktext --no-sandbox --user-data-dir=$ro_data $args + end + + if test $foreground -eq 1 + $cmd + return $status + end + + bkg $cmd +end diff --git a/tests/test-md.fish b/tests/test-md.fish new file mode 100644 index 0000000..5f29645 --- /dev/null +++ b/tests/test-md.fish @@ -0,0 +1,71 @@ +#!/usr/bin/env fish +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Coverage for md: which flags it consumes, which it forwards verbatim, +# when it detaches, and the shape of the firejail read-only invocation. +# +# Runs isolated (no `# MODE:` marker). marktext, firejail and bkg are +# stubbed as functions that print their arguments, so the suite asserts on +# the assembled command line without launching an editor -- and passes on +# a machine that has none of the three installed. + +source (realpath (dirname (status filename)))/lib.fish +set -p fish_function_path $repo_root/functions + +# Stubs. `type -q` is satisfied by a function, so md takes the same +# branches it would with the real binaries present. +function marktext + echo "FG: $argv" +end +function firejail + echo "FJ: $argv" +end +function bkg + echo "BKG: $argv" +end + +set -l sandbox (path resolve (mktemp -d)) +set -l doc $sandbox/note.md +echo '# note' >$doc + +# Keep the read-only user-data directory out of the real ~/.cache. +set -gx XDG_CACHE_HOME $sandbox/cache + +section "md: forwarding and detaching" + +check "plain file detaches via bkg" "BKG: marktext $doc" (md $doc) +check "--foreground runs in place" "FG: $doc" (md --foreground $doc) +check "--foreground is never forwarded" "FG: $doc" (md --foreground $doc) +check "marktext flags pass through" "BKG: marktext --new-window $doc" (md --new-window $doc) + +section "md: output flags imply --foreground" + +check "version flag" "FG: --version" (md --version) +check "verbose flag" "FG: --verbose $doc" (md --verbose $doc) +check "--debug after our own flag" "FG: --debug $doc" (md --foreground --debug $doc) + +section "md: --read-only" + +set -l ro (md -r $doc) +check "-r sandboxes with firejail" true (string match -q 'BKG: firejail *' -- $ro; and echo true; or echo false) +check "-r binds the file read-only" true (string match -q "*--read-only=$doc*" -- $ro; and echo true; or echo false) +check "-r is not forwarded to marktext" false (string match -q '* -r *' -- $ro; and echo true; or echo false) +check "-r forces a private instance" true (string match -q "*--user-data-dir=$XDG_CACHE_HOME/marktext-readonly*" -- $ro; and echo true; or echo false) +check "-r still passes the file" true (string match -q "*marktext *$doc" -- $ro; and echo true; or echo false) +check "--read-only is the same flag" true (string match -q 'BKG: firejail *' -- (md --read-only $doc); and echo true; or echo false) + +# Relative paths must reach firejail absolute. +set -l start $PWD +cd $sandbox +check "-r resolves a relative path" true (string match -q "*--read-only=$doc*" -- (md -r note.md); and echo true; or echo false) +cd $start + +check "-r without an existing file fails" 1 (md -r $sandbox/absent.md 2>/dev/null; echo $status) +check "-r can be combined with --foreground" true (string match -q 'FJ: *' -- (md -r --foreground $doc); and echo true; or echo false) + +functions -e marktext firejail bkg +set -e XDG_CACHE_HOME +rm -rf $sandbox + +report