#!/usr/bin/env bash ############################################################################### # dread — Browse research library via dmenu, open in Zathura # # Maintainer: Kris Yotam # License: MIT # Created: 2026-02-15 # # Top-level categories: # Papers → ~/research/library/papers/ (field → subfield → PDF) # Texts → ~/research/library/texts/ (textbooks by field) # Primary Texts → ~/research/library/primary-texts/ (drama, essays, fiction, poetry) # Books → ~/Calibre Library/ (personal library via Calibre) # # All files opened in Zathura. ############################################################################### LIBRARY="$HOME/research/library" CALIBRE="$HOME/Calibre Library" ############################################################################### # helpers ############################################################################### # Drill into a directory: show subdirs, then files. Recurse on subdir selection. browse_dir() { local dir="$1" local prompt="$2" while true; do # Build list: subdirectories first, then readable files local items="" # Subdirectories (with trailing /, skip hidden) while IFS= read -r d; do [ -z "$d" ] && continue local dname dname=$(basename "$d") [[ "$dname" == .* ]] && continue items+="󰉋 ${dname}/"$'\n' done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d | sort) # Files (PDF, EPUB, DJVU, etc.) while IFS= read -r f; do [ -z "$f" ] && continue local name name=$(basename "$f") case "$name" in *.pdf) items+=" $name"$'\n' ;; *.epub) items+=" $name"$'\n' ;; *.djvu) items+=" $name"$'\n' ;; *.mobi) items+=" $name"$'\n' ;; *.cbz) items+="󰋩 $name"$'\n' ;; *) items+=" $name"$'\n' ;; esac done < <(find "$dir" -mindepth 1 -maxdepth 1 -type f \( -name "*.pdf" -o -name "*.epub" -o -name "*.djvu" -o -name "*.mobi" -o -name "*.cbz" \) | sort) [ -z "$items" ] && { notify-send "dread" "Empty: $(basename "$dir")"; return; } local choice choice=$(printf '%s' "$items" | sed '/^$/d' | dmenu -i -l 20 -p "$prompt") [ -z "$choice" ] && return # Strip icon prefix local name name=$(echo "$choice" | sed 's/^[^ ]* *//') if [[ "$name" == */ ]]; then # Directory selected — recurse name="${name%/}" browse_dir "$dir/$name" " $name" else # File selected — open in zathura zathura "$dir/$name" &>/dev/null & disown return fi done } # Browse Calibre library — flat list of all books via metadata.db browse_calibre() { local db="$CALIBRE/metadata.db" if [ ! -f "$db" ]; then notify-send "dread" "Calibre Library not found" return fi # Query all books: "Title — Author" with path local entries entries=$(sqlite3 "$db" " SELECT b.title, COALESCE(a.name, 'Unknown'), b.path, COALESCE(d.format, 'pdf'), COALESCE(d.name, b.title) FROM books b LEFT JOIN books_authors_link bal ON bal.book = b.id LEFT JOIN authors a ON a.id = bal.author LEFT JOIN data d ON d.book = b.id WHERE d.format IS NOT NULL ORDER BY b.title " 2>/dev/null) [ -z "$entries" ] && { notify-send "dread" "No books in Calibre"; return; } # Build dmenu list and parallel path array local menu="" local -a paths=() while IFS='|' read -r title author relpath fmt fname; do [ -z "$title" ] && continue local ext ext=$(echo "$fmt" | tr '[:upper:]' '[:lower:]') menu+=" ${title} — ${author}"$'\n' paths+=("$CALIBRE/$relpath/$fname.$ext") done <<< "$entries" local choice choice=$(printf '%s' "$menu" | sed '/^$/d' | dmenu -i -l 20 -p " Books") [ -z "$choice" ] && return # Find matching index (avoid Plan 9 grep which lacks -F) local idx=-1 local i=0 while IFS= read -r line; do [ -z "$line" ] && continue if [[ "$line" == "$choice" ]]; then idx=$i break fi ((i++)) done < <(printf '%s' "$menu" | sed '/^$/d') [[ $idx -lt 0 ]] && return local file="${paths[$idx]}" if [ -f "$file" ]; then zathura "$file" &>/dev/null & disown else notify-send "dread" "File not found: $(basename "$file")" fi } ############################################################################### # main ############################################################################### CHOICE=$(printf '%s\n' \ " Papers" \ " Texts" \ " Primary Texts" \ " Books" \ | dmenu -i -l 4 -p "󰂺 Library") [ -z "$CHOICE" ] && exit 0 case "$CHOICE" in *Papers*) browse_dir "$LIBRARY/papers" " Papers" ;; *Texts*) browse_dir "$LIBRARY/texts" " Texts" ;; *Primary\ Texts*) browse_dir "$LIBRARY/primary-texts" " Primary" ;; *Books*) browse_calibre ;; esac