Files
Panama/tests/setup/nautilus-extensions-contract
Gabriel Brown 68cbf892e9 Teach the file manager to send and to shrink
Right-click a file to send it to your phone, or a video to make it
smaller. macOS has both behind the Share sheet and Quick Actions;
Windows has "Send to"; a stock Linux file manager has neither, and the
usual answer for the second one is a web uploader or an ffmpeg
incantation looked up again every time.

Neither adds machinery. Sending reuses panama-kdeconnect, the same
helper the Home & Phone page and quick settings already drive, so
there is one way to talk to a phone rather than two. The entry appears
only when a phone is actually reachable: an item that is present and
fails is worse than one that is absent, because the absence explains
itself.

The transcoder's two rules are both about not losing work. It never
writes to its input, and it never writes over an earlier output -- a
second run produces -2 rather than eating the first result. Verified
against a real encode: 1920x1080 became 854x480, with an even width
because H.264 rejects an odd one at the very end of a long encode,
which is the worst possible moment to find out.

Menus decide by mime type rather than extension, act on one file at a
time, and refuse anything that is not a local path. nautilus-python
turned out to be declared already; it now says it carries Panama's own
extensions too.
2026-08-22 06:52:04 -04:00

122 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Right-click a file and do something useful with it.
#
# macOS has a Share sheet and Quick Actions in Finder; Windows has "Send to".
# Both are among the few context-menu entries people genuinely reach for, and
# a stock Linux file manager has neither.
#
# What must hold:
#
# 1. The transcoder never writes over its input, and never over an earlier
# output. Somebody right-clicking a video to shrink it must not lose the
# original, and running it twice must not eat the first result.
# 2. It refuses what it cannot do rather than handing ffmpeg a guess.
# 3. The extensions only offer themselves for files they can act on, decided
# by mime type rather than extension, and only for a real local file.
# 4. Sending a file reuses panama-kdeconnect rather than inventing a second
# way to talk to a phone.
# 5. The menu entry for sending is absent when no phone is reachable. An
# entry that is present and fails is worse than one that is absent,
# because the absence explains itself.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
extensions="$repo_dir/config/local/share/nautilus-python/extensions"
share="$extensions/panama-share.py"
transcode_ext="$extensions/panama-transcode.py"
transcode="$repo_dir/bin/panama-transcode"
linker="$repo_dir/setup/scripts/link-dotfiles"
packages="$repo_dir/setup/packages/desktop-packages"
findings=()
note() { findings+=("$1"); }
for file in "$share" "$transcode_ext"; do
[[ -r "$file" ]] || note "$(basename "$file") is missing"
done
[[ -x "$transcode" ]] || { printf 'nautilus contract: %s is not executable\n' "$transcode" >&2; exit 1; }
# Both extensions must be importable Python, or Nautilus loads nothing and
# says nothing about why.
for file in "$share" "$transcode_ext"; do
[[ -r "$file" ]] || continue
python3 -c "import ast,sys; ast.parse(open(sys.argv[1]).read())" "$file" 2>/dev/null \
|| note "$(basename "$file") is not valid Python, so Nautilus would silently load nothing"
grep -q 'Nautilus.MenuProvider' "$file" \
|| note "$(basename "$file") does not implement MenuProvider, so it would never appear in a menu"
done
# ── 1 & 2. The transcoder protects the input and earlier outputs ────────────
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
if command -v ffmpeg >/dev/null 2>&1; then
ffmpeg -nostdin -loglevel error -f lavfi -i testsrc=duration=1:size=640x480:rate=10 \
-pix_fmt yuv420p "$work/clip.mp4" -y 2>/dev/null
before="$(sha256sum "$work/clip.mp4" | cut -d' ' -f1)"
"$transcode" video "$work/clip.mp4" 480p >/dev/null 2>&1 \
|| note 'transcoding a real video failed'
after="$(sha256sum "$work/clip.mp4" | cut -d' ' -f1)"
[[ "$before" == "$after" ]] || note 'the transcoder modified its own input'
[[ -f "$work/clip-480p.mp4" ]] || note 'the transcoder wrote no output beside the input'
first="$(sha256sum "$work/clip-480p.mp4" | cut -d' ' -f1)"
"$transcode" video "$work/clip.mp4" 480p >/dev/null 2>&1
[[ "$(sha256sum "$work/clip-480p.mp4" | cut -d' ' -f1)" == "$first" ]] \
|| note 'running the transcoder twice overwrote the first output'
[[ -f "$work/clip-480p-2.mp4" ]] \
|| note 'a second run did not write a numbered output, so one of the two was lost'
else
note 'ffmpeg is not installed, so the transcoder could not be exercised'
fi
# An unknown size or format is refused before ffmpeg is handed anything.
"$transcode" video "$work/clip.mp4" 9000p >/dev/null 2>&1 \
&& note 'an unknown video size was accepted'
"$transcode" image "$work/clip.mp4" bmp >/dev/null 2>&1 \
&& note 'an unknown image format was accepted'
"$transcode" video "$work/does-not-exist.mp4" 480p >/dev/null 2>&1 \
&& note 'a file that does not exist was accepted'
# ── 3. Only files they can act on ───────────────────────────────────────────
grep -q 'get_mime_type' "$transcode_ext" \
|| note 'the transcode menu decides by extension rather than mime type, so a mislabelled file gets an entry it fails on'
for file in "$share" "$transcode_ext"; do
[[ -r "$file" ]] || continue
grep -q "parsed.scheme != \"file\"" "$file" \
|| note "$(basename "$file") does not check that the file is local, so it would offer to act on a remote URI"
grep -q 'len(files) != 1' "$file" \
|| note "$(basename "$file") acts on a multiple selection, which would start one job per file from a single click"
done
# ── 4 & 5. Sending reuses the phone helper, and hides without a phone ───────
grep -q 'panama-kdeconnect' "$share" \
|| note 'the share extension does not use the existing phone helper'
grep -q 'send-file' "$share" \
|| note 'the share extension does not use the helper verb that sends a file'
grep -q 'reachable' "$share" \
|| note 'the share entry appears whether or not a phone is reachable, so it would be present and fail'
# ── Installed where Nautilus looks ──────────────────────────────────────────
grep -q 'nautilus-python/extensions' "$linker" \
|| note 'link-dotfiles does not publish the extensions, so Nautilus would never see them'
grep -qx 'nautilus-python' "$packages" \
|| note 'nautilus-python is not declared, so the extensions would not load on a fresh machine'
(( $(grep -cx 'nautilus-python' "$packages") == 1 )) \
|| note 'nautilus-python is declared more than once'
if (( ${#findings[@]} > 0 )); then
printf 'nautilus extensions contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'nautilus extensions contract: PASS\n'