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
9 changed files with 2651 additions and 2672 deletions
+2295 -2667
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -1050,6 +1050,49 @@ functions). They are active in all interactive sessions.
Example:
less /var/log/syslog
### md
Synopsis: md [-r] [--foreground] [marktext-args...] [FILE...]
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
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.
Example:
md README.md
md --read-only NOTES.md
md --foreground --debug draft.md
md --new-window one.md two.md
**Dependencies:** `marktext`, `firejail`, `bkg`
### rawfish
Synopsis: rawfish [args...]
@@ -1651,6 +1694,8 @@ functions). They are active in all interactive sessions.
Example:
bkg firefox
**Used by:** `md`
### detach
Synopsis: detach [-h] [--version] <command> [args...]
@@ -3258,6 +3303,8 @@ matter if you already use that specific tool. Skipped by
| `docker` | Container runtime; gates the Docker context indicator in the right prompt and backs the `ld` wrapper. Both consumers are guarded with `type -q docker` and degrade gracefully without it. Installing the daemon package does not enable/start the service — do that yourself if you want it running. |
| `yt-dlp` | Video/media downloader; backs the `yt-dlp` wrapper function. The wrapper falls back to the system `yt-dlp` and the rest of the config works without it. |
| `screen` | GNU screen; fallback backend for `jobrunner` when `tmux` is unavailable. |
| `marktext` | Markdown editor; backs the `md` wrapper, which is the only thing that references it. No distro packages it under a common name, so `fish-deps` offers the AUR package (`marktext-bin`) on Arch and otherwise installs upstream's AppImage to `~/.local/bin/marktext`. |
| `firejail` | Sandbox; needed only by `md --read-only`, which uses it to make MarkText unable to save over the file it opened. Every other `md` invocation works without it. |
## Terminal Emulators
+2
View File
@@ -57,6 +57,8 @@ matter if you already use that specific tool. Skipped by
| `docker` | Container runtime; gates the Docker context indicator in the right prompt and backs the `ld` wrapper. Both consumers are guarded with `type -q docker` and degrade gracefully without it. Installing the daemon package does not enable/start the service — do that yourself if you want it running. |
| `yt-dlp` | Video/media downloader; backs the `yt-dlp` wrapper function. The wrapper falls back to the system `yt-dlp` and the rest of the config works without it. |
| `screen` | GNU screen; fallback backend for `jobrunner` when `tmux` is unavailable. |
| `marktext` | Markdown editor; backs the `md` wrapper, which is the only thing that references it. No distro packages it under a common name, so `fish-deps` offers the AUR package (`marktext-bin`) on Arch and otherwise installs upstream's AppImage to `~/.local/bin/marktext`. |
| `firejail` | Sandbox; needed only by `md --read-only`, which uses it to make MarkText unable to save over the file it opened. Every other `md` invocation works without it. |
## Terminal Emulators
+5 -5
View File
@@ -36,27 +36,27 @@ function _fish_deps_catalog
set -g _fdc_bins \
uv cargo fish starship fzf zoxide direnv paru yay \
wakatime tailscale \
eza lsd bat btop dust duf prettyping go ov rg lazygit lazydocker docker trash kitty wezterm python3 yt-dlp screen mpv vlc
eza lsd bat btop dust duf prettyping go ov rg lazygit lazydocker docker trash kitty wezterm python3 yt-dlp screen mpv vlc marktext firejail
set -g _fdc_tiers \
rec rec req rec req rec rec rec rec \
int int \
rec rec rec opt opt opt opt opt rec rec opt opt opt rec term term rec opt opt opt opt
rec rec rec opt opt opt opt opt rec rec opt opt opt rec term term rec opt opt opt opt opt opt
set -g _fdc_cargo \
"" "" "" starship "" zoxide "" "" "" \
"" "" \
eza lsd bat "" du-dust "" "" "" "" ripgrep "" "" "" trashy "" "" "" "" "" "" ""
eza lsd bat "" du-dust "" "" "" "" ripgrep "" "" "" trashy "" "" "" "" "" "" "" "" ""
set -g _fdc_pm \
uv cargo fish starship fzf zoxide direnv "" yay \
wakatime tailscale \
eza lsd bat btop dust duf prettyping go ov ripgrep lazygit lazydocker docker trash kitty wezterm python yt-dlp screen mpv vlc
eza lsd bat btop dust duf prettyping go ov ripgrep lazygit lazydocker docker trash kitty wezterm python yt-dlp screen mpv vlc "" firejail
set -g _fdc_special \
curl-uv rustup-installer git-cargo-fish curl-installer fzf-update "" "" paru-build yay-build \
wakatime-binary "" \
"" "" "" "" "" "" "" "" go-ov "" "" curl-lazydocker "" "" "" "" "" "" "" "" ""
"" "" "" "" "" "" "" "" go-ov "" "" curl-lazydocker "" "" "" "" "" "" "" "" "" marktext-release ""
end
# SYNOPSIS
+20
View File
@@ -126,6 +126,20 @@ function _fish_deps_install
case wakatime-binary
set -a methods special-wakatime
set -a method_labels "binary download (github releases)"
case marktext-release
# Upstream packages MarkText for the AUR and for its own
# GitHub releases only -- no distro carries it under a
# common name, so off Arch the AppImage is the only
# option and $_fdc_pm is deliberately empty.
if type -q paru
set -a methods special-marktext-paru
set -a method_labels "paru -S marktext-bin (AUR)"
else if type -q yay
set -a methods special-marktext-yay
set -a method_labels "yay -S marktext-bin (AUR)"
end
set -a methods special-marktext-appimage
set -a method_labels "AppImage download (~/.local/bin/marktext)"
case go-ov
if type -q go
set -a methods special-go-ov
@@ -251,6 +265,12 @@ function _fish_deps_install
test $_go_status -eq 0
case special-lazydocker
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
case special-marktext-paru
paru -S --noconfirm marktext-bin
case special-marktext-yay
yay -S --noconfirm marktext-bin
case special-marktext-appimage
_fish_deps_marktext_appimage
case special-wakatime
set -l _arch (uname -m)
switch $_arch
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _fish_deps_marktext_appimage
#
# DESCRIPTION
# Installs (or upgrades in place) MarkText as an AppImage at
# ~/.local/bin/marktext. This is the install path for systems whose
# package manager does not carry MarkText at all -- upstream ships only
# the AUR package (marktext-bin) and its own GitHub release assets, so
# apt/dnf/brew have nothing to offer.
#
# The release assets embed their version in the filename, so there is no
# stable /releases/latest/download URL; the download URL is read from the
# GitHub API instead.
#
# Upstream builds the Linux AppImage for x86_64 only.
#
# EXIT STATUS
# 0 MarkText installed at ~/.local/bin/marktext
# 1 Unsupported architecture, or the download or install failed
#
# EXAMPLE
# _fish_deps_marktext_appimage
#
# NOTES
# An AppImage needs FUSE to self-mount. Where FUSE is unavailable, run it
# as `marktext --appimage-extract-and-run`.
function _fish_deps_marktext_appimage
set -l arch (uname -m)
if test "$arch" != x86_64
echo " MarkText publishes a Linux AppImage for x86_64 only (this is $arch)." >&2
return 1
end
if not type -q curl
echo " curl is required to download the MarkText AppImage." >&2
return 1
end
set -l url (curl -fsSL https://api.github.com/repos/marktext/marktext/releases/latest |
string match -r '"browser_download_url":\s*"([^"]*-linux-[^"]*\.AppImage)"')[2]
if test -z "$url"
echo " Could not find a Linux AppImage in the latest MarkText release." >&2
return 1
end
set -l dest "$HOME/.local/bin/marktext"
set -l tmp (mktemp -d)
set -l ok 0
curl -fL "$url" -o "$tmp/marktext"
and mkdir -p (dirname $dest)
and chmod +x "$tmp/marktext"
# Replace via mv, not a write into $dest: overwriting a running AppImage
# in place corrupts the live mount.
and mv -f "$tmp/marktext" "$dest"
and set ok 1
rm -rf $tmp
if test $ok -eq 1
fish_add_path "$HOME/.local/bin"
end
test $ok -eq 1
end
+21
View File
@@ -96,6 +96,27 @@ function _fish_deps_update
continue
end
# marktext: AUR where it exists, else refresh the AppImage. Only an
# AppImage we own is refreshed -- a distro-packaged marktext belongs
# to that package manager, and ~/.local/bin/marktext would shadow it.
if test "$special" = marktext-release
if type -q paru
echo "Updating $bin..."
paru -S --noconfirm marktext-bin
set updated_any 1
else if type -q yay
echo "Updating $bin..."
yay -S --noconfirm marktext-bin
set updated_any 1
else if test -f "$HOME/.local/bin/marktext"
echo "Updating $bin..."
_fish_deps_marktext_appimage
set updated_any 1
end
set i (math $i + 1)
continue
end
# wakatime: re-download the binary from github releases
if test "$special" = wakatime-binary
echo "Updating $bin..."
+124
View File
@@ -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
+71
View File
@@ -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