Files
copnix/modules/home/home-file-manager-actions.ulu.nix
T
2026-10-16 16:15:38 +03:00

536 lines
17 KiB
Nix

{ ... }:
{
flake.homeModules.home-file-manager-actions =
{ pkgs, lib, ... }:
let
inherit (lib) concatStringsSep concatMap optional;
fmActions = pkgs.writeShellApplication {
name = "fm-actions";
runtimeInputs = with pkgs; [
poppler-utils
wl-clipboard
imagemagick
coreutils
libnotify
tesseract
ffmpeg
];
text = ''
op="''${1:-}"
if [ "$#" -gt 0 ]; then shift; fi
notify() {
notify-send -a Dolphin -i system-file-manager "$1" "''${2:-}" || true
}
finish() {
status=$?
if [ -t 0 ]; then
printf '\n[fm-actions] exit %s — press enter to close\n' "$status"
read -r _ || true
elif [ "$status" -ne 0 ]; then
notify-send -a Dolphin -u critical "action failed" \
"fm-actions ''${op:-?} exited with $status" || true
fi
}
trap finish EXIT
stem() { printf '%s' "''${1%.*}"; }
resolve_dnxhr() {
profile="$1"; shift
for f in "$@"; do
ffmpeg -nostdin -hide_banner -n -hwaccel auto -i "$f" \
-map 0:v:0 -map '0:a?' -fps_mode cfr \
-vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2,format=yuv422p' \
-c:v dnxhd -profile:v "$profile" \
-c:a pcm_s24le -ar 48000 -ac 2 \
"$(stem "$f")-resolve.mov"
done
notify "ready for Resolve" "$# video(s), ''${profile#dnxhr_}"
}
vaapi_dev=/dev/dri/renderD128
h264_backend() {
if [ -e "$vaapi_dev" ] && ffmpeg -nostdin -v error \
-init_hw_device vaapi=va:"$vaapi_dev" -filter_hw_device va \
-f lavfi -i nullsrc=s=256x256 -vf format=nv12,hwupload \
-frames:v 1 -c:v h264_vaapi -f null - 2>/dev/null; then
echo vaapi
elif ffmpeg -nostdin -v error -f lavfi -i nullsrc=s=256x256 \
-frames:v 1 -c:v h264_nvenc -f null - 2>/dev/null; then
echo nvenc
else
echo cpu
fi
}
# $1 = hq | small | tiny, rest = files
export_h264() {
mode="$1"; shift
even='scale=trunc(iw/2)*2:trunc(ih/2)*2'
cap='scale=min(iw\,1920):min(ih\,1920):force_original_aspect_ratio=decrease:force_divisible_by=2'
case "$mode" in
hq) vf="$even" qp=20 cq=21 crf=19 preset=medium ab=192k ;;
small) vf="$cap" qp=26 cq=28 crf=24 preset=medium ab=160k ;;
tiny) vf="$cap" crf=24 preset=slow ab=128k ;;
esac
if [ "$mode" = tiny ]; then backend=cpu; else backend="$(h264_backend)"; fi
echo "[fm-actions] H.264 encoder: $backend"
for f in "$@"; do
out="$(stem "$f")-h264.mp4"
case "$backend" in
vaapi)
ffmpeg -nostdin -hide_banner -n \
-init_hw_device vaapi=va:"$vaapi_dev" -filter_hw_device va \
-i "$f" -map 0:v:0 -map '0:a?' \
-vf "$vf,format=nv12,hwupload" \
-c:v h264_vaapi -profile:v high -rc_mode CQP -qp "$qp" \
-c:a aac -b:a "$ab" -movflags +faststart "$out"
;;
nvenc)
ffmpeg -nostdin -hide_banner -n -i "$f" -map 0:v:0 -map '0:a?' \
-vf "$vf,format=yuv420p" \
-c:v h264_nvenc -preset p5 -rc vbr -cq "$cq" -b:v 0 \
-c:a aac -b:a "$ab" -movflags +faststart "$out"
;;
cpu)
ffmpeg -nostdin -hide_banner -n -i "$f" -map 0:v:0 -map '0:a?' \
-vf "$vf,format=yuv420p" \
-c:v libx264 -crf "$crf" -preset "$preset" \
-c:a aac -b:a "$ab" -movflags +faststart "$out"
;;
esac
done
notify "exported H.264 ($backend)" "$# video(s)"
}
require_count() {
n="$1"; shift
if [ "$#" -ne "$n" ]; then
notify-send -a Dolphin -u critical "action failed" \
"expected $n item(s), got $#" || true
exit 2
fi
}
case "$op" in
copy-path)
printf '%s\n' "$@" | wl-copy
notify "path copied" "$# item(s)"
;;
copy-content)
cat -- "$@" | wl-copy
notify "contents copied" "$# file(s)"
;;
duplicate)
for f in "$@"; do cp -a -- "$f" "$f.copy"; done
notify "duplicated" "$# item(s)"
;;
symlink)
for f in "$@"; do ln -s -- "$f" "$f.link"; done
notify "symlinked" "$# item(s)"
;;
checksum)
sha256sum -- "$@"
;;
img-png)
for f in "$@"; do magick "$f" "$(stem "$f").png"; done
notify "converted to png" "$# image(s)"
;;
img-jpg)
for f in "$@"; do magick "$f" -quality 92 "$(stem "$f").jpg"; done
notify "converted to jpeg" "$# image(s)"
;;
img-webp)
for f in "$@"; do magick "$f" -quality 90 "$(stem "$f").webp"; done
notify "converted to webp" "$# image(s)"
;;
img-resize)
for f in "$@"; do
magick "$f" -resize '1920x1920>' "$(stem "$f")-1920.''${f##*.}"
done
notify "resized to 1920px" "$# image(s)"
;;
img-strip)
for f in "$@"; do magick "$f" -strip "$f"; done
notify "metadata stripped" "$# image(s)"
;;
wallpaper)
noctalia msg wallpaper-set "$1"
;;
vid-mp4)
for f in "$@"; do
ffmpeg -nostdin -hide_banner -i "$f" \
-c:v libx264 -crf 20 -preset medium -c:a aac -b:a 192k \
"$(stem "$f").mp4"
done
;;
vid-audio)
for f in "$@"; do
ffmpeg -nostdin -hide_banner -i "$f" -vn -c:a libmp3lame -q:a 2 \
"$(stem "$f").mp3"
done
;;
vid-gif)
for f in "$@"; do
palette="$(mktemp --suffix=.png)"
ffmpeg -nostdin -hide_banner -y -i "$f" \
-vf 'fps=15,scale=640:-1:flags=lanczos,palettegen' "$palette"
ffmpeg -nostdin -hide_banner -y -i "$f" -i "$palette" \
-lavfi 'fps=15,scale=640:-1:flags=lanczos[x];[x][1:v]paletteuse' \
"$(stem "$f").gif"
rm -f "$palette"
done
;;
resolve-lb) resolve_dnxhr dnxhr_lb "$@" ;;
resolve-sq) resolve_dnxhr dnxhr_sq "$@" ;;
resolve-hq) resolve_dnxhr dnxhr_hq "$@" ;;
resolve-wav)
for f in "$@"; do
ffmpeg -nostdin -hide_banner -n -i "$f" -vn \
-c:a pcm_s24le -ar 48000 -ac 2 "$(stem "$f")-resolve.wav"
done
notify "ready for Resolve" "$# audio file(s)"
;;
h264-hq) export_h264 hq "$@" ;;
h264-small) export_h264 small "$@" ;;
h264-tiny) export_h264 tiny "$@" ;;
media-info)
for f in "$@"; do
printf '\n=== %s ===\n' "$f"
ffprobe -hide_banner "$f" 2>&1 || true
done
;;
pdf-images)
for f in "$@"; do pdftoppm -png -r 150 "$f" "$(stem "$f")"; done
notify "PDF rendered" "$# document(s)"
;;
pdf-merge)
require_count 2 "$@" 2>/dev/null || true
pdfunite "$@" merged.pdf
notify "PDFs merged" "merged.pdf"
;;
compare)
require_count 2 "$@"
meld "$@"
;;
*)
echo "fm-actions: unknown operation: $op" >&2
exit 2
;;
esac
'';
};
fm = lib.getExe fmActions;
term = "ghostty --working-directory=%f";
typeMime = {
directories = [ "inode/directory" ];
everything = [ "all/all" ];
files = [ "all/allfiles" ];
audio-files = [ "audio/*" ];
image-files = [ "image/*" ];
video-files = [ "video/*" ];
text-files = [ "text/*" ];
};
pdfMimeTypes = [ "application/pdf" ];
actions = [
{
id = "open-terminal-here";
name = "open ghostty here";
icon = "utilities-terminal";
command = term;
types = [ "directories" ];
}
{
id = "open-root-terminal-here";
name = "open root terminal here";
icon = "dialog-password";
command = "${term} -e sudo -E -s";
types = [ "directories" ];
}
{
id = "open-yazi-here";
name = "open yazi here";
icon = "system-file-manager";
command = "${term} -e yazi";
types = [ "directories" ];
}
{
id = "open-as-root";
name = "open as root";
icon = "security-high";
command = "dolphin admin://%f";
types = [ "directories" ];
}
{
id = "open-lazyjj-here";
name = "open lazyjj here";
icon = "folder-open";
command = "${term} -e lazyjj";
types = [ "directories" ];
}
{
id = "open-lazygit-here";
name = "open lazygit here";
icon = "folder-open";
command = "${term} -e lazygit";
types = [ "directories" ];
}
{
id = "compare-in-meld";
name = "compare in meld";
icon = "document-properties";
command = "${fm} compare %F";
types = [ "everything" ];
}
{
id = "image-to-png";
name = "convert to PNG";
icon = "image-x-generic";
command = "${fm} img-png %F";
types = [ "image-files" ];
submenu = "Image";
}
{
id = "image-to-jpg";
name = "convert to JPEG";
icon = "image-x-generic";
command = "${fm} img-jpg %F";
types = [ "image-files" ];
submenu = "Image";
}
{
id = "image-to-webp";
name = "convert to WebP";
icon = "image-x-generic";
command = "${fm} img-webp %F";
types = [ "image-files" ];
submenu = "Image";
}
{
id = "image-resize-1920";
name = "resize to 1920px";
icon = "image-x-generic";
command = "${fm} img-resize %F";
types = [ "image-files" ];
submenu = "Image";
}
{
id = "image-strip-metadata";
name = "strip metadata";
icon = "image-x-generic";
command = "${fm} img-strip %F";
types = [ "image-files" ];
submenu = "Image";
}
{
id = "image-set-wallpaper";
name = "Set as Wallpaper";
icon = "preferences-desktop-wallpaper";
command = "${fm} wallpaper %f";
types = [ "image-files" ];
submenu = "Image";
}
{
id = "media-to-mp4";
name = "convert to MP4";
icon = "video-x-generic";
command = "ghostty -e ${fm} vid-mp4 %F";
types = [ "video-files" ];
submenu = "Media";
}
{
id = "media-extract-audio";
name = "extract Audio (MP3)";
icon = "audio-x-generic";
command = "ghostty -e ${fm} vid-audio %F";
types = [
"audio-files"
"video-files"
];
submenu = "Media";
}
{
id = "media-to-gif";
name = "make GIF";
icon = "video-x-generic";
command = "ghostty -e ${fm} vid-gif %F";
types = [ "video-files" ];
submenu = "Media";
}
{
id = "media-info";
name = "media info";
icon = "dialog-information";
command = "ghostty -e ${fm} media-info %F";
types = [
"audio-files"
"image-files"
"video-files"
];
submenu = "Media";
}
{
id = "resolve-dnxhr-lb";
name = "for Resolve: DNxHR LB (small, fast)";
icon = "video-x-generic";
command = "ghostty -e ${fm} resolve-lb %F";
types = [ "video-files" ];
submenu = "Resolve";
}
{
id = "resolve-dnxhr-sq";
name = "for Resolve: DNxHR SQ (balanced)";
icon = "video-x-generic";
command = "ghostty -e ${fm} resolve-sq %F";
types = [ "video-files" ];
submenu = "Resolve";
}
{
id = "resolve-dnxhr-hq";
name = "for Resolve: DNxHR HQ (quality)";
icon = "video-x-generic";
command = "ghostty -e ${fm} resolve-hq %F";
types = [ "video-files" ];
submenu = "Resolve";
}
{
id = "resolve-audio-wav";
name = "for Resolve: audio to WAV (PCM)";
icon = "audio-x-generic";
command = "ghostty -e ${fm} resolve-wav %F";
types = [ "audio-files" ];
submenu = "Resolve";
}
{
id = "resolve-export-h264-hq";
name = "to H.264 MP4: GPU, quality";
icon = "video-x-generic";
command = "ghostty -e ${fm} h264-hq %F";
types = [ "video-files" ];
submenu = "Resolve";
}
{
id = "resolve-export-h264-small";
name = "to H.264 MP4: GPU, small (max 1080p)";
icon = "video-x-generic";
command = "ghostty -e ${fm} h264-small %F";
types = [ "video-files" ];
submenu = "Resolve";
}
{
id = "resolve-export-h264-tiny";
name = "to H.264 MP4: CPU, smallest (slow)";
icon = "video-x-generic";
command = "ghostty -e ${fm} h264-tiny %F";
types = [ "video-files" ];
submenu = "Resolve";
}
{
id = "pdf-to-images";
name = "render to PNG";
icon = "application-pdf";
command = "${fm} pdf-images %F";
mimeTypes = pdfMimeTypes;
submenu = "PDF";
}
{
id = "pdf-merge";
name = "merge into merged.pdf";
icon = "application-pdf";
command = "${fm} pdf-merge %F";
mimeTypes = pdfMimeTypes;
submenu = "PDF";
}
{
id = "tools-copy-path";
name = "copy full path";
icon = "edit-copy";
command = "${fm} copy-path %F";
types = [ "everything" ];
submenu = "Tools";
}
{
id = "tools-copy-content";
name = "copy file contents";
icon = "edit-copy";
command = "${fm} copy-content %F";
types = [ "text-files" ];
submenu = "Tools";
}
{
id = "tools-checksum";
name = "SHA-256 Checksum";
icon = "dialog-information";
command = "ghostty -e ${fm} checksum %F";
types = [ "files" ];
submenu = "Tools";
}
{
id = "tools-duplicate";
name = "Duplicate";
icon = "edit-copy";
command = "${fm} duplicate %F";
types = [ "everything" ];
submenu = "Tools";
}
{
id = "tools-symlink";
name = "Create Symlink Here";
icon = "emblem-symbolic-link";
command = "${fm} symlink %F";
types = [ "everything" ];
submenu = "Tools";
}
{
id = "tools-dos2unix";
name = "Fix Line Endings";
icon = "text-x-generic";
command = "dos2unix -- %F";
types = [ "text-files" ];
submenu = "Tools";
}
];
mkServiceMenu =
{
id,
name,
command,
icon ? "",
types ? null,
mimeTypes ? null,
submenu ? null,
}:
let
mimes = if mimeTypes != null then mimeTypes else concatMap (t: typeMime.${t}) types;
in
concatStringsSep "\n" (
[
"[Desktop Entry]"
"Type=Service"
"X-KDE-ServiceTypes=KonqPopupMenu/Plugin"
"MimeType=${concatStringsSep ";" mimes};"
"Actions=${id};"
]
++ optional (submenu != null) "X-KDE-Submenu=${submenu}"
++ [
""
"[Desktop Action ${id}]"
"Name=${name}"
"Icon=${icon}"
"Exec=${command}"
]
);
in
{
home.packages = [ fmActions ];
xdg.dataFile = builtins.listToAttrs (
map (a: {
name = "kio/servicemenus/${a.id}.desktop";
value.text = mkServiceMenu a;
}) actions
);
};
}