#!/usr/bin/env bash
###############################################################################
# dread — Browse research library via dmenu, open in Zathura
#
# Maintainer: Kris Yotam <krisyotam@pm.me>
# 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