--- /dev/null
+#+AUTHOR: Steven Baltakatei Sandoval
+#+DATE: 2026-08-01
+#+TITLE: gThumb prune
+
+** Summary
+This work describes a way to quickly move groups of photograph files
+(e.g. ~DSC00001.JPG~, ~DSC00001.ARW~, etc.) as well as associated
+sidecar files into desired directories via keyboard shortcuts in
+gThumb.
+
+The method centers around a Bash script named ~gthumb-prune.sh~ which
+may be bound to a gThumb keyboard shortcut to quickly move selected
+image files to a desired location. The Bash script is run by a short
+shell script specified in gThumbΚΌs π§ β Personalizeβ¦ menu.
+Additionally, sidecar files as well as associated files sharing the
+same file name stem (i.e. ~../*/DSC00001.*~) are also grouped and
+moved to the desired location.
+
+** Stats
+Version: 0.0.4
+License: GPLv3+
+gThumb version: ~3.12.6~
+
+** Usage
+Let us say, that we have a set of photos in the following
+locations:
+
+#+begin_example
+.
+βββ ARW
+βΒ Β βββ DSC00001.ARW
+βΒ Β βββ DSC00001.ARW.ots
+βΒ Β βββ DSC00002.ARW
+βΒ Β βββ DSC00002.ARW.ots
+βΒ Β βββ DSC00003.ARW
+βΒ Β βββ DSC00003.ARW.ots
+βββ JPG
+ βββ DSC00001.JPG
+ βββ DSC00001.JPG.ots
+ βββ DSC00002.JPG
+ βββ DSC00002.JPG.ots
+ βββ DSC00002.JPG
+ βββ DSC00002.JPG.ots
+#+end_example
+
+Let us say gThumb has the ~JPG~ directory open and we have selected
+~DSC00002.JPG~ which we want to keep. We also want to keep the
+associated ~DSC00002.JPG.ots~ as well as the ~DSC00002.ARW~ and
+~DSC00002.ARW.ots~ files in the neighboring ~ARW~ directory. We wish
+to discard all other photograph files.
+
+First, we configure a ~keep~ shortcut. In the gThumbΚΌs π§ β
+Personalize⦠menu, click the *New* button. Set the *Name* to
+~keep~. Set the *Command* to:
+
+: fscript="$HOME/.local/bin/gthumb-prune.sh"; if [ -f "$fscript" ]; then /bin/bash "$fscript" "keep" %F; fi;
+
+Where ~$HOME/.local/bin/gthumb-prune.sh~ is the path for the Bash
+script (see the ~Code~ section below).
+
+Set the *Shortcut* to ~Shift+K~. Make sure the box βTerminal command
+(shell script)β box is checked and the βExecute command once for every
+fileβ is *not* checked. Click the *Save* button to close the ~Edit
+Command~ window. Click the *Ok* button to close the ~Commands~ window.
+
+Now, with ~DSC00002.JPG~ selected, press ~Shift+K~ on your
+keyboard. The image should disappear from view and a new directory
+should appear in the the left navbar, reflecting a new directory structure.
+
+#+begin_example
+.
+βββ ARW
+βΒ Β βββ DSC00001.ARW
+βΒ Β βββ DSC00001.ARW.ots
+βΒ Β βββ DSC00003.ARW
+βΒ Β βββ DSC00003.ARW.ots
+βββ JPG
+βΒ Β βββ DSC00001.JPG
+βΒ Β βββ DSC00001.JPG.ots
+βΒ Β βββ DSC00003.JPG
+βΒ Β βββ DSC00003.JPG.ots
+βββ keep
+ βββ ARW
+ βΒ Β βββ DSC00002.ARW
+ βΒ Β βββ DSC00002.ARW.ots
+ βββ JPG
+ βββ DSC00002.JPG
+ βββ DSC00002.JPG.ots
+#+end_example
+
+The ~DSC0002.*~ files (4 in total) have been moved to now reside
+within a ~keep~ directory with their ~JPG~ and ~ARW~ subdirectory
+structure preserved. Thus, the ~./JPG/~ and ~./ARW/~ directories have
+been pruned.
+
+If we want to trash the remaining ~DSC00001.*~ and ~DSC00003.*~ files,
+we can create another shortcut to group and move files into a ~.trash~
+directory next to ~keep~.
+
+Repeat the procedure for creating a shortcut, but *Name* it ~rmall~
+and set the *Command* to be:
+
+: fscript="$HOME/.local/bin/gthumb-prune.sh"; if [ -f "$fscript" ]; then /bin/bash "$fscript" ".trash" %F; fi;
+
+Note the change from ~keep~ to ~.trash~. Set the keyboard *Shortcut*
+to ~Shift+T~.
+
+Now, with gThumb looking at the contents of ~./JPG~, and with
+~DSC00001.JPG~ and ~DSC00003.JPG~ selected, press ~Shift+T~. The two
+images should disappear, leaving ~./JPG~ now empty. The navbar to the
+left should not show any change since ~.trash~ is a dot directory and
+so is normally hidden. Nonetheless, the directory structure has now
+changed to:
+
+#+begin_example
+.
+βββ ARW
+βββ JPG
+βββ keep
+βΒ Β βββ ARW
+βΒ Β βΒ Β βββ DSC00002.ARW
+βΒ Β βΒ Β βββ DSC00002.ARW.ots
+βΒ Β βββ JPG
+βΒ Β βββ DSC00002.JPG
+βΒ Β βββ DSC00002.JPG.ots
+βββ .trash
+ βββ ARW
+ βΒ Β βββ DSC00001.ARW
+ βΒ Β βββ DSC00001.ARW.ots
+ βΒ Β βββ DSC00003.ARW
+ βΒ Β βββ DSC00003.ARW.ots
+ βββ JPG
+ βββ DSC00001.JPG
+ βββ DSC00001.JPG.ots
+ βββ DSC00003.JPG
+ βββ DSC00003.JPG.ots
+#+end_Example
+
+So, the contents of ~./JPG/~ may be pruned by selecting files you want
+to keep and pressing ~Shift+K~ and by selecting files you want to
+trash and pressing ~Shift+T~. Then, if you are really sure about
+deleting the undesired files, delete the contents of ~.trash~ via the
+terminal command:
+
+: $ rm -r ./.trash/*
+
+** Code
+gthumb-prune.sh:
+
+#+begin_code
+#!/bin/bash
+# Desc: Prunes files selected by gThumb.
+# Note: Moves files into a directory with a specified name.
+# Usage: gthumb-prune.sh [STR dirname] [FILESβ¦]
+# Example: $HOME/.local/bin/gthumb-prune.sh keep %F
+# If '%F' expanded to `~/Pics/MyJPGs/DSC00001.JPG`, then
+# "$HOME/Pics/*/DSC00001.*" files are moved to:
+# '~/Pics/keep/MyJPGs/' .
+#
+# If '%F' expanded to multiple files, then the move
+# operations to 'keep' are performed for each file.
+# Input: arg1 str directory to send selected files
+# arg2+ str path of selected file(s)
+# Version: 0.0.4
+
+fdebug=/tmp/gtPruneLog.txt;
+
+yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
+die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
+must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
+main() {
+ # Validate inputs
+ local groupName re1 fina;
+ ## Interpret first argument as name of group directory to move files into.
+ groupName="$1"; # e.g. 'keep'
+ ### RegEx matches:
+ ### - Forward slash (i.e. path) anywhere.
+ ### - A string consisting of just '.' (current working dir)
+ ### - A string consisting of just '..' (parent dir of working dir)
+ re1='/|^\.$|^\.\.$';
+ if [[ "$groupName" =~ $re1 ]]; then
+ die "FATAL:Group '${groupName}' must not be a path.";
+ fi;
+ if [[ -z "$groupName" ]]; then
+ die "FATAL:Group '${groupName}' may not be blank.";
+ fi;
+ shift;
+
+ ## Interpret remaining arguments as files and populate input file array
+ fina=("$@");
+
+ ## Validate input file array
+ ### Handle no argument case.
+ if [[ $# -le 0 ]]; then
+ die "FATAL:No arguments:$#";
+ fi;
+ ### Check each remaining array element is a file.
+ local fin;
+ for fin in "${fina[@]}"; do
+ if [[ ! -f "$fin" ]]; then
+ die "FATAL:Not a file:${fin}";
+ fi;
+ done;
+
+
+ # Process each file
+ local f;
+ for f in "${fina[@]}"; do
+ # Set up variables
+ local fBase dParent dParParent fNoExt dGroup;
+ fBase="$(basename "$f")";
+ dParent="$(dirname "$f")";
+ dParParent="$(dirname "$dParent")";
+ fNoExt="${fBase%.*}";
+ dGroup="${dParParent}/${groupName}";
+ yell "DEBUG:fdebug:${fdebug}";
+ yell "DEBUG:f:${f}"; # e.g. ~/Pics/MyJPGs/DSC00001.JPG
+ yell "DEBUG:fBase:${fBase}"; # e.g. DSC00001.JPG
+ yell "DEBUG:fNoExt:${fNoExt}"; # e.g. DSC00001
+ yell "DEBUG:dParent:${dParent}"; # e.g. ~/Pics/MyJPGs/
+ yell "DEBUG:dParParent:${dParParent}"; # e.g. ~/Pics/
+ yell "DEBUG:dGroup:${dGroup}"; # e.g. ~/Pics/keep/
+
+ # Move sibling files sharing the stem
+ local fSrc subDirName dDest;
+ for fSrc in "${dParParent}"/*/"${fNoExt}".*; do
+ if [ ! -e "$fSrc" ]; then continue; fi;
+ subDirName="$(basename "$(dirname "$fSrc")")";
+ dDest="${dGroup}/${subDirName}";
+ yell "DEBUG:fSrc:${fSrc}"; # e.g. ~/Pics/MyARWs/DSC00001.ARW.ots
+ yell "DEBUG:subDirName:${subDirName}"; # e.g. MyARWs
+ yell "DEBUG:dDest:${dDest}"; # e.g. ~/Pics/keep/MyARWs/
+ if [ ! -d "$dDest" ]; then
+ yell "STATUS:Destination dir does not exist. Creating:${dDest}";
+ must mkdir -p "$dDest";
+ else
+ yell "STATUS:Destination dir exists:${dDest}";
+ fi;
+ yell "STATUS: Moving ${fSrc} β ${dDest}/";
+
+ # Perform move ( e.g. mv ~/Pics/MyARWs/DSC00001.ARW.ots ~/Pics/keep/MyARWs/ )
+ mv -n "$fSrc" "${dDest}/";
+
+ # Check if move failed.
+ if [[ -e "$fSrc" ]]; then
+ yell "ERROR:Failed to move ${fSrc} β ${dDest}";
+ fi;
+ done;
+ done;
+};
+
+main "$@" 1>>"$fdebug" 2>&1;
+
+# Author: Steven Baltakatei Sandoval
+# License: GPLv3+
+#+end_code
+