#!/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'