#!/usr/bin/env bash set -euo pipefail ############################################################################### # TOME -- Content manager for krisyotam.com # # Maintainer: Kris Yotam # License: MIT # Created: 2026-05-18 # Description: Unified content creator, editor, and metadata manager. # Three actions (create, edit, data) with two interfaces # (nnn/fzf TUI or dmenu). # # Usage: # tome -nnn -create Create new content entry (TUI) # tome -dmenu -create Create new content entry (dmenu) # tome -nnn -edit Browse content, open in write (TUI) # tome -dmenu -edit Browse content, open in write (dmenu) # tome -nnn -data Edit content metadata (TUI) # tome -dmenu -data Edit content metadata (dmenu) ############################################################################### ############################################################################### # config ############################################################################### DB="$HOME/dev/krisyotam.com/data/content.db" CONTENT_DIR="$HOME/.corpus/content" TIL_DIR="$HOME/.corpus/til" # Menu entries (order matters -- displayed as-is) # Indented entries are news publications MENU_ENTRIES=( "papers" "essays" "blog" "diary" "reviews" "verse" "news" " the-soapbox" " field-notes" " off-the-record" "til" "fiction" "ocs" "prayers" "progymnasmata" ) # Types that are actual content directories ALL_TYPES=(papers essays blog diary reviews verse news til fiction ocs prayers progymnasmata) # News publications NEWS_PUBS=("the-soapbox" "field-notes" "off-the-record") # Diary has no status/confidence/importance columns DIARY_FIELDS=(title preview category_slug state) STANDARD_FIELDS=(title preview category_slug status confidence importance state) VALID_STATUSES=("Notes" "Draft" "In Progress" "Finished" "Abandoned") VALID_CONFIDENCES=( "certain" "highly likely" "likely" "possible" "unlikely" "highly unlikely" "remote" "impossible" ) VALID_STATES=("active" "hidden") # nerd font icon map declare -A TYPE_ICONS=( [papers]="󰈙" [essays]="󰬝" [blog]="󰬞" [diary]="󰃮" [reviews]="󰓥" [verse]="󱗝" [news]="󰎕" [fiction]="󰂺" [ocs]="󰏫" [prayers]="󰥍" [progymnasmata]="󰑴" [til]="󰛄" [the-soapbox]="󰎕" [field-notes]="󰎕" [off-the-record]="󰎕" ) ############################################################################### # helpers ############################################################################### have_cmd() { command -v "$1" >/dev/null 2>&1; } die() { echo "ERROR: $1" >&2; exit 1; } sql() { sqlite3 "$DB" "$1"; } notify() { notify-send "tome" "$1" 2>/dev/null || true; } slugify() { echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ */ /g; s/ /-/g; s/--*/-/g; s/^-//; s/-$//' } check_slug_globally() { local slug="$1" for dir in "$CONTENT_DIR"/*/; do [ -d "$dir" ] || continue if [ -f "$dir/${slug}.mdx" ]; then echo "$(basename "$dir"):${slug}.mdx" return fi done # Also check til for subdir in "$TIL_DIR"/*/; do [ -d "$subdir" ] || continue if [ -f "$subdir/${slug}.mdx" ]; then echo "til/$(basename "$subdir"):${slug}.mdx" return fi done } # Get content directory for a type (or news publication) content_dir_for() { local selection="$1" case "$selection" in the-soapbox|field-notes|off-the-record) echo "$CONTENT_DIR/news" ;; til) echo "$TIL_DIR" ;; *) echo "$CONTENT_DIR/$selection" ;; esac } # Resolve a menu selection to its content type (for DB operations) db_type_for() { local selection="$1" case "$selection" in the-soapbox|field-notes|off-the-record) echo "news" ;; *) echo "$selection" ;; esac } today() { date +%Y-%m-%d; } ############################################################################### # argument parsing ############################################################################### UI_MODE="" ACTION_MODE="" for arg in "$@"; do case "$arg" in -nnn) UI_MODE="nnn" ;; -dmenu) UI_MODE="dmenu" ;; -create) ACTION_MODE="create" ;; -edit) ACTION_MODE="edit" ;; -data) ACTION_MODE="data" ;; *) echo "Unknown flag: $arg" >&2 echo "Usage: tome -nnn|-dmenu -create|-edit|-data" >&2 exit 1 ;; esac done if [[ -z "$UI_MODE" || -z "$ACTION_MODE" ]]; then echo "Usage: tome -nnn|-dmenu -create|-edit|-data" >&2 exit 1 fi ############################################################################### # dependency checks ############################################################################### check_deps_nnn() { have_cmd fzf || die "fzf not found" have_cmd nnn || die "nnn not found" } check_deps_dmenu() { have_cmd dmenu || die "dmenu not found" } check_deps_data() { have_cmd sqlite3 || die "sqlite3 not found" [[ -f "$DB" ]] || die "content.db not found at $DB" } ############################################################################### # menu rendering ############################################################################### # Build the fzf menu string fzf_menu() { for entry in "${MENU_ENTRIES[@]}"; do echo "$entry" done echo "quit" } # Build the dmenu menu string (with icons) dmenu_menu() { for entry in "${MENU_ENTRIES[@]}"; do local trimmed="${entry## }" local indent="${entry%%[! ]*}" local icon="${TYPE_ICONS[$trimmed]:-󰈙}" echo "${indent}${icon} ${trimmed}" done } # Parse a menu selection back to a clean type/publication name parse_selection() { echo "$1" | sed 's/^[[:space:]]*//' | sed 's/^[^ ]* *//' } ############################################################################### # banners ############################################################################### print_banner_main() { local cols cols="$(tput cols 2>/dev/null || echo 80)" if have_cmd figlet; then figlet -w "$cols" -f big "KRISYOTAM.COM" 2>/dev/null \ || figlet -w "$cols" "KRISYOTAM.COM" else echo "KRISYOTAM.COM" fi echo echo " est. 2025" echo " author: Kris Yotam" echo " script: tome" echo echo " \"Do I contradict myself? Very well then I contradict myself," echo " (I am large, I contain multitudes.)\"" echo " -- Walt Whitman" echo } print_banner_data() { local cols cols="$(tput cols 2>/dev/null || echo 80)" if have_cmd figlet; then figlet -w "$cols" -f big "DATA" 2>/dev/null \ || figlet -w "$cols" "DATA" else echo "=== DATA ===" fi echo echo " Content metadata editor for krisyotam.com" echo } ############################################################################### # create: write frontmatter + .mdx file (pure shell) ############################################################################### # Collect metadata and write the .mdx file write_new_entry() { local selection="$1" local type type="$(db_type_for "$selection")" local target_dir target_dir="$(content_dir_for "$selection")" mkdir -p "$target_dir" # For news publications browsing into subdirectory is not needed # since all news lives flat in content/news/ local title slug preview category status confidence importance local verse_type="" prayer_type="" form="" publication="" # Title if [[ "$UI_MODE" == "dmenu" ]]; then title="$(echo "" | dmenu -p "title:")" else read -rp " Title: " title fi [[ -z "$title" ]] && return # Slug slug="$(slugify "$title")" local collision collision="$(check_slug_globally "$slug")" if [[ -n "$collision" ]]; then if [[ "$UI_MODE" == "dmenu" ]]; then notify "Slug collision: $slug ($collision)" slug="$(echo "$slug" | dmenu -p "collision! alt slug:")" else echo " Slug collision: $slug ($collision)" read -rp " Alt slug: " slug fi [[ -z "$slug" ]] && return slug="$(slugify "$slug")" collision="$(check_slug_globally "$slug")" if [[ -n "$collision" ]]; then if [[ "$UI_MODE" == "dmenu" ]]; then notify "Still colliding: $slug. Aborting." else echo " Still colliding: $slug. Aborting." fi return fi fi # Preview if [[ "$UI_MODE" == "dmenu" ]]; then preview="$(echo "" | dmenu -p "preview:")" else read -rp " Preview: " preview fi # Category check_deps_data if [[ "$UI_MODE" == "dmenu" ]]; then category="$(sql "SELECT slug FROM categories ORDER BY slug" | dmenu -i -l 20 -p "category:")" else category="$(sql "SELECT slug FROM categories ORDER BY slug" | fzf --prompt="category> " --height=20 --reverse)" || category="" fi [[ -z "$category" ]] && return # Status (skip for diary) if [[ "$type" == "diary" ]]; then status="" confidence="" importance="" else if [[ "$UI_MODE" == "dmenu" ]]; then status="$(printf "%s\n" "${VALID_STATUSES[@]}" | dmenu -i -l 5 -p "status:")" confidence="$(printf "%s\n" "${VALID_CONFIDENCES[@]}" | dmenu -i -l 8 -p "confidence:")" importance="$(seq 1 10 | dmenu -i -l 10 -p "importance:")" else status="$(printf "%s\n" "${VALID_STATUSES[@]}" | fzf --prompt="status> " --height=8 --reverse)" || status="Draft" confidence="$(printf "%s\n" "${VALID_CONFIDENCES[@]}" | fzf --prompt="confidence> " --height=10 --reverse)" || confidence="possible" read -rp " Importance (1-10): " importance fi [[ -z "$status" ]] && status="Draft" [[ -z "$confidence" ]] && confidence="possible" [[ -z "$importance" ]] && importance="5" fi # Type-specific fields case "$type" in verse) if [[ "$UI_MODE" == "dmenu" ]]; then verse_type="$(printf 'haiku\nsonnet\nfree-verse\node\nlimerick\nvillanelle\nother' | dmenu -i -l 7 -p "verse type:")" else verse_type="$(printf 'haiku\nsonnet\nfree-verse\node\nlimerick\nvillanelle\nother' | fzf --prompt="verse type> " --height=10 --reverse)" || verse_type="" fi ;; prayers) if [[ "$UI_MODE" == "dmenu" ]]; then prayer_type="$(echo "" | dmenu -p "prayer type:")" form="$(echo "" | dmenu -p "form:")" else read -rp " Prayer type: " prayer_type read -rp " Form: " form fi ;; news) # Publication is determined by the menu selection case "$selection" in the-soapbox|field-notes|off-the-record) publication="$selection" ;; news) # Selected the parent "news" -- ask which publication if [[ "$UI_MODE" == "dmenu" ]]; then publication="$(printf '%s\n' "${NEWS_PUBS[@]}" | dmenu -i -l 3 -p "publication:")" else publication="$(printf '%s\n' "${NEWS_PUBS[@]}" | fzf --prompt="publication> " --height=5 --reverse)" || publication="the-soapbox" fi [[ -z "$publication" ]] && publication="the-soapbox" ;; esac ;; esac # Tags local tags="" if [[ "$UI_MODE" == "dmenu" ]]; then # Collect up to 5 tags local tag_list=() for _ in 1 2 3 4 5; do local tag tag="$(sql "SELECT slug FROM tags ORDER BY slug" | dmenu -i -l 20 -p "tag (empty=done):")" || break [[ -z "$tag" ]] && break tag_list+=("$tag") done tags="$(printf '%s\n' "${tag_list[@]}" 2>/dev/null | paste -sd',' -)" else echo " Tags (comma-separated, or empty):" read -rp " > " tags fi # Build the .mdx file local target_file="$target_dir/${slug}.mdx" local date date="$(today)" { echo "---" echo "title: \"$title\"" echo "slug: $slug" echo "type: $type" echo "category: $category" echo "start_date: '$date'" echo "end_date: ''" echo "state: active" [[ -n "$preview" ]] && echo "preview: \"$preview\"" if [[ "$type" != "diary" ]]; then echo "status: $status" echo "confidence: $confidence" echo "importance: $importance" fi # Type-specific fields [[ -n "$verse_type" ]] && echo "verse_type: $verse_type" [[ -n "$prayer_type" ]] && echo "prayer_type: $prayer_type" [[ -n "$form" ]] && echo "form: $form" [[ -n "$publication" ]] && echo "publication: $publication" # Tags as inline YAML array if [[ -n "$tags" ]]; then local tag_arr tag_arr="$(echo "$tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | awk '{printf "%s\"%s\"", (NR>1 ? ", " : ""), $0}')" echo "tags: [$tag_arr]" else echo "tags: []" fi echo "---" echo "" } > "$target_file" if [[ "$UI_MODE" == "dmenu" ]]; then notify "Created: $title ($slug)" # Open in write write "$target_file" & disown else echo echo " Created: $target_file" echo read -rp " Open in write? [Y/n] " yn case "$yn" in n|N) ;; *) write "$target_file" ;; esac fi } ############################################################################### # mode: -nnn -create ############################################################################### run_nnn_create() { check_deps_nnn clear print_banner_main local selection selection="$(fzf_menu | fzf --prompt="create> " --height=20 --reverse)" || exit 0 [[ "$selection" == "quit" ]] && exit 0 # Trim whitespace (for indented news publications) selection="${selection## }" # If they selected "news" (parent), let them pick publication or create generic write_new_entry "$selection" } ############################################################################### # mode: -dmenu -create ############################################################################### run_dmenu_create() { check_deps_dmenu local raw raw="$(dmenu_menu | dmenu -i -l 16 -p "create:")" [[ -z "$raw" ]] && exit 0 local selection selection="$(parse_selection "$raw")" [[ -z "$selection" ]] && exit 0 write_new_entry "$selection" } ############################################################################### # mode: -nnn -edit (browse and open existing content in write) ############################################################################### run_nnn_edit() { check_deps_nnn local nnn_write_helper="$HOME/.local/bin/nnn-write" [[ ! -x "$nnn_write_helper" ]] && die "nnn-write helper not found at $nnn_write_helper" clear print_banner_main while true; do local selection selection="$(fzf_menu | fzf --prompt="edit> " --height=20 --reverse)" || exit 0 [[ "$selection" == "quit" ]] && exit 0 selection="${selection## }" local target_dir target_dir="$(content_dir_for "$selection")" if [[ ! -d "$target_dir" ]]; then echo "Directory not found: $target_dir" read -rp "Press Enter..." _ continue fi # For news publications, filter to only show matching files case "$selection" in the-soapbox|field-notes|off-the-record) # Show only files matching this publication local tmpdir tmpdir="$(mktemp -d)" # Symlink matching files into tmpdir for nnn browsing for f in "$target_dir"/*.mdx; do [[ -f "$f" ]] || continue if grep -q "^publication: $selection" "$f" 2>/dev/null; then ln -s "$f" "$tmpdir/$(basename "$f")" fi done NNN_OPENER="$nnn_write_helper" \ NNN_FALLBACK_OPENER="${NNN_OPENER:-xdg-open}" \ nnn "$tmpdir" rm -rf "$tmpdir" ;; til) # TIL has subdirectories NNN_OPENER="$nnn_write_helper" \ NNN_FALLBACK_OPENER="${NNN_OPENER:-xdg-open}" \ nnn "$target_dir" ;; *) NNN_OPENER="$nnn_write_helper" \ NNN_FALLBACK_OPENER="${NNN_OPENER:-xdg-open}" \ nnn "$target_dir" ;; esac clear print_banner_main done } ############################################################################### # mode: -dmenu -edit ############################################################################### run_dmenu_edit() { check_deps_dmenu local raw raw="$(dmenu_menu | dmenu -i -l 16 -p "edit:")" [[ -z "$raw" ]] && exit 0 local selection selection="$(parse_selection "$raw")" [[ -z "$selection" ]] && exit 0 local target_dir target_dir="$(content_dir_for "$selection")" [[ ! -d "$target_dir" ]] && { notify "Directory not found: $target_dir"; exit 1; } local file case "$selection" in the-soapbox|field-notes|off-the-record) # Filter news files by publication file="$( for f in "$target_dir"/*.mdx; do [[ -f "$f" ]] || continue if grep -q "^publication: $selection" "$f" 2>/dev/null; then basename "$f" .mdx fi done | sort | dmenu -i -l 20 -p "$selection:" )" [[ -z "$file" ]] && exit 0 write "$target_dir/$file.mdx" ;; til) # TIL has subdirs -- first pick subdir, then file local subdir subdir="$(ls "$target_dir" | dmenu -i -l 10 -p "til topic:")" [[ -z "$subdir" ]] && exit 0 file="$(ls "$target_dir/$subdir"/*.mdx 2>/dev/null | xargs -I{} basename {} .mdx | sort | dmenu -i -l 20 -p "$subdir:")" [[ -z "$file" ]] && exit 0 write "$target_dir/$subdir/$file.mdx" ;; *) file="$( ls "$target_dir"/*.mdx 2>/dev/null | xargs -I{} basename {} .mdx | sort | dmenu -i -l 20 -p "$selection:" )" [[ -z "$file" ]] && exit 0 write "$target_dir/$file.mdx" ;; esac } ############################################################################### # metadata display (for -data mode) ############################################################################### show_metadata() { local type="$1" slug="$2" local title preview category state tags id title="$(sql "SELECT title FROM $type WHERE slug='$slug'")" preview="$(sql "SELECT preview FROM $type WHERE slug='$slug'")" category="$(sql "SELECT category_slug FROM $type WHERE slug='$slug'")" state="$(sql "SELECT state FROM $type WHERE slug='$slug'")" id="$(sql "SELECT id FROM $type WHERE slug='$slug'")" tags="$(sql "SELECT t.slug FROM tags t JOIN content_tags ct ON ct.tag_id = t.id WHERE ct.content_type = '$type' AND ct.content_id = $id ORDER BY t.slug" | paste -sd', ' -)" echo echo " ┌─ $slug ─────────────────────────────────" printf " │ %-14s %s\n" "Title:" "$title" printf " │ %-14s %s\n" "Preview:" "${preview:0:60}" printf " │ %-14s %s\n" "Category:" "$category" printf " │ %-14s %s\n" "Tags:" "$tags" if [[ "$type" != "diary" ]]; then local status confidence importance status="$(sql "SELECT status FROM $type WHERE slug='$slug'")" confidence="$(sql "SELECT confidence FROM $type WHERE slug='$slug'")" importance="$(sql "SELECT importance FROM $type WHERE slug='$slug'")" printf " │ %-14s %s\n" "Status:" "$status" printf " │ %-14s %s\n" "Confidence:" "$confidence" printf " │ %-14s %s\n" "Importance:" "$importance" fi if [[ "$type" == "news" ]]; then local pub pub="$(sql "SELECT publication FROM $type WHERE slug='$slug'")" printf " │ %-14s %s\n" "Publication:" "$pub" fi printf " │ %-14s %s\n" "State:" "$state" echo " └──────────────────────────────────────────" echo } ############################################################################### # field editing -- nnn/fzf mode (for -data) ############################################################################### edit_tags_nnn() { local type="$1" slug="$2" local id id="$(sql "SELECT id FROM $type WHERE slug='$slug'")" echo echo " Current tags:" sql "SELECT t.slug FROM tags t JOIN content_tags ct ON ct.tag_id = t.id WHERE ct.content_type = '$type' AND ct.content_id = $id ORDER BY t.slug" | while read -r tag; do echo " - $tag" done echo echo " Actions: [a]dd tag, [r]emove tag, [d]one" while true; do read -rp " tag> " action case "$action" in a|add) local new_tag new_tag="$( sql "SELECT slug FROM tags ORDER BY slug" | fzf --prompt="add tag> " --height=20 --reverse )" || continue local existing existing="$(sql "SELECT COUNT(*) FROM content_tags WHERE content_type='$type' AND content_id=$id AND tag_id=(SELECT id FROM tags WHERE slug='$new_tag')")" if [[ "$existing" -gt 0 ]]; then echo " Tag '$new_tag' already linked." continue fi local tag_id tag_id="$(sql "SELECT id FROM tags WHERE slug='$new_tag'")" sql "INSERT INTO content_tags (content_type, content_id, tag_id) VALUES ('$type', $id, $tag_id)" echo " Added tag: $new_tag" ;; r|remove) local rm_tag rm_tag="$( sql "SELECT t.slug FROM tags t JOIN content_tags ct ON ct.tag_id = t.id WHERE ct.content_type = '$type' AND ct.content_id = $id ORDER BY t.slug" | fzf --prompt="remove tag> " --height=10 --reverse )" || continue sql "DELETE FROM content_tags WHERE content_type='$type' AND content_id=$id AND tag_id=(SELECT id FROM tags WHERE slug='$rm_tag')" echo " Removed tag: $rm_tag" ;; d|done|q|quit) break ;; *) echo " Unknown action. Use [a]dd, [r]emove, or [d]one." ;; esac done } edit_field_nnn() { local type="$1" slug="$2" field="$3" local new_value="" case "$field" in title|preview) local current current="$(sql "SELECT $field FROM $type WHERE slug='$slug'")" echo " Current $field: $current" read -rp " New $field: " new_value [[ -z "$new_value" ]] && return new_value="${new_value//\'/\'\'}" sql "UPDATE $type SET $field='$new_value', updated_at=datetime('now') WHERE slug='$slug'" echo " Updated $field." ;; category_slug) new_value="$( sql "SELECT slug FROM categories ORDER BY slug" | fzf --prompt="category> " --height=20 --reverse )" || return sql "UPDATE $type SET category_slug='$new_value', updated_at=datetime('now') WHERE slug='$slug'" echo " Updated category to: $new_value" ;; tags) edit_tags_nnn "$type" "$slug" ;; status) new_value="$( printf "%s\n" "${VALID_STATUSES[@]}" | fzf --prompt="status> " --height=10 --reverse )" || return sql "UPDATE $type SET status='$new_value', updated_at=datetime('now') WHERE slug='$slug'" echo " Updated status to: $new_value" ;; confidence) new_value="$( printf "%s\n" "${VALID_CONFIDENCES[@]}" | fzf --prompt="confidence> " --height=12 --reverse )" || return sql "UPDATE $type SET confidence='$new_value', updated_at=datetime('now') WHERE slug='$slug'" echo " Updated confidence to: $new_value" ;; importance) read -rp " Importance (1-10): " new_value if [[ "$new_value" =~ ^[0-9]+$ ]] && (( new_value >= 1 && new_value <= 10 )); then sql "UPDATE $type SET importance=$new_value, updated_at=datetime('now') WHERE slug='$slug'" echo " Updated importance to: $new_value" else echo " Invalid. Must be 1-10." fi ;; publication) new_value="$( printf "%s\n" "${NEWS_PUBS[@]}" | fzf --prompt="publication> " --height=5 --reverse )" || return sql "UPDATE $type SET publication='$new_value', updated_at=datetime('now') WHERE slug='$slug'" echo " Updated publication to: $new_value" ;; state) new_value="$( printf "%s\n" "${VALID_STATES[@]}" | fzf --prompt="state> " --height=5 --reverse )" || return sql "UPDATE $type SET state='$new_value', updated_at=datetime('now') WHERE slug='$slug'" echo " Updated state to: $new_value" ;; *) echo " Unknown field: $field" ;; esac } ############################################################################### # field editing -- dmenu mode (for -data) ############################################################################### edit_field_dmenu() { local type="$1" slug="$2" field="$3" local current new_value case "$field" in title|preview) current="$(sql "SELECT $field FROM $type WHERE slug='$slug'")" new_value="$(echo "$current" | dmenu -p "$field:")" [[ -z "$new_value" ]] && return new_value="${new_value//\'/\'\'}" sql "UPDATE $type SET $field='$new_value', updated_at=datetime('now') WHERE slug='$slug'" notify "Updated $field" ;; category_slug) new_value="$(sql "SELECT slug FROM categories ORDER BY slug" | dmenu -i -l 15 -p "category:")" [[ -z "$new_value" ]] && return sql "UPDATE $type SET category_slug='$new_value', updated_at=datetime('now') WHERE slug='$slug'" notify "Category: $new_value" ;; tags) local id id="$(sql "SELECT id FROM $type WHERE slug='$slug'")" local action action="$(printf "add\nremove" | dmenu -i -p "tags:")" [[ -z "$action" ]] && return if [[ "$action" == "add" ]]; then local tag tag="$(sql "SELECT slug FROM tags ORDER BY slug" | dmenu -i -l 20 -p "add tag:")" [[ -z "$tag" ]] && return local tag_id tag_id="$(sql "SELECT id FROM tags WHERE slug='$tag'")" [[ -z "$tag_id" ]] && return sql "INSERT OR IGNORE INTO content_tags (content_type, content_id, tag_id) VALUES ('$type', $id, $tag_id)" notify "Added tag: $tag" else local tag tag="$(sql "SELECT t.slug FROM tags t JOIN content_tags ct ON ct.tag_id=t.id WHERE ct.content_type='$type' AND ct.content_id=$id ORDER BY t.slug" | dmenu -i -l 10 -p "remove:")" [[ -z "$tag" ]] && return sql "DELETE FROM content_tags WHERE content_type='$type' AND content_id=$id AND tag_id=(SELECT id FROM tags WHERE slug='$tag')" notify "Removed tag: $tag" fi ;; status) new_value="$(printf "Notes\nDraft\nIn Progress\nFinished\nAbandoned" | dmenu -i -l 5 -p "status:")" [[ -z "$new_value" ]] && return sql "UPDATE $type SET status='$new_value', updated_at=datetime('now') WHERE slug='$slug'" notify "Status: $new_value" ;; confidence) new_value="$(printf "certain\nhighly likely\nlikely\npossible\nunlikely\nhighly unlikely\nremote\nimpossible" | dmenu -i -l 8 -p "confidence:")" [[ -z "$new_value" ]] && return sql "UPDATE $type SET confidence='$new_value', updated_at=datetime('now') WHERE slug='$slug'" notify "Confidence: $new_value" ;; importance) new_value="$(seq 1 10 | dmenu -i -l 10 -p "importance:")" [[ -z "$new_value" ]] && return sql "UPDATE $type SET importance=$new_value, updated_at=datetime('now') WHERE slug='$slug'" notify "Importance: $new_value" ;; publication) new_value="$(printf '%s\n' "${NEWS_PUBS[@]}" | dmenu -i -l 3 -p "publication:")" [[ -z "$new_value" ]] && return sql "UPDATE $type SET publication='$new_value', updated_at=datetime('now') WHERE slug='$slug'" notify "Publication: $new_value" ;; state) new_value="$(printf "active\nhidden" | dmenu -i -l 2 -p "state:")" [[ -z "$new_value" ]] && return sql "UPDATE $type SET state='$new_value', updated_at=datetime('now') WHERE slug='$slug'" notify "State: $new_value" ;; esac } ############################################################################### # entry selection helpers ############################################################################### select_entry_fzf() { local type="$1" filter_pub="${2:-}" local query="SELECT title || ' (' || slug || ')' || char(9) || slug FROM $type WHERE state != '' " [[ -n "$filter_pub" ]] && query+="AND publication = '$filter_pub' " query+="ORDER BY title" sql "$query" | fzf --prompt="$type> " --delimiter=$'\t' --with-nth=1 \ --preview="sqlite3 '$DB' \"SELECT 'Title: ' || title || char(10) || 'Preview: ' || COALESCE(preview,'') || char(10) || 'Category: ' || COALESCE(category_slug,'') || char(10) || 'State: ' || COALESCE(state,'active') FROM $type WHERE slug='{2}'\"" \ --height=80% --reverse | awk -F'\t' '{print $2}' } select_entry_nnn_browse() { local type="$1" local content_path="$CONTENT_DIR/$type" [[ ! -d "$content_path" ]] && { echo ""; return; } local tmpfile tmpfile="$(mktemp)" NNN_TMPFILE="$tmpfile" nnn -p "$tmpfile" "$content_path" if [[ -s "$tmpfile" ]]; then local selected selected="$(cat "$tmpfile")" rm -f "$tmpfile" basename "$selected" .mdx else rm -f "$tmpfile" echo "" fi } ############################################################################### # mode: -nnn -data ############################################################################### run_nnn_data() { check_deps_nnn check_deps_data clear print_banner_data while true; do local selection selection="$(fzf_menu | fzf --prompt="data> " --height=20 --reverse)" || exit 0 [[ "$selection" == "quit" ]] && exit 0 selection="${selection## }" local type type="$(db_type_for "$selection")" # Determine publication filter for news local pub_filter="" case "$selection" in the-soapbox|field-notes|off-the-record) pub_filter="$selection" ;; esac local mode mode="$( printf "%s\n" "fzf (search by title)" "nnn (browse files)" "back" | fzf --prompt="select via> " --height=5 --reverse )" || continue [[ "$mode" == "back" ]] && continue local slug="" if [[ "$mode" == *"fzf"* ]]; then slug="$(select_entry_fzf "$type" "$pub_filter")" || continue else slug="$(select_entry_nnn_browse "$type")" || continue fi [[ -z "$slug" ]] && continue local count count="$(sql "SELECT COUNT(*) FROM $type WHERE slug='$slug'")" if [[ "$count" -eq 0 ]]; then echo " Slug '$slug' not found in $type table." read -rp " Press Enter..." _ continue fi while true; do clear print_banner_data show_metadata "$type" "$slug" local fields if [[ "$type" == "diary" ]]; then fields=("${DIARY_FIELDS[@]}" "tags" "done") elif [[ "$type" == "news" ]]; then fields=("${STANDARD_FIELDS[@]}" "publication" "tags" "done") else fields=("${STANDARD_FIELDS[@]}" "tags" "done") fi local field field="$( printf "%s\n" "${fields[@]}" | fzf --prompt="edit field> " --height=14 --reverse )" || break [[ "$field" == "done" ]] && break edit_field_nnn "$type" "$slug" "$field" read -rp " Press Enter..." _ done done } ############################################################################### # mode: -dmenu -data ############################################################################### run_dmenu_data() { check_deps_dmenu check_deps_data local raw raw="$(dmenu_menu | dmenu -i -l 16 -p "data:")" [[ -z "$raw" ]] && exit 0 local selection selection="$(parse_selection "$raw")" [[ -z "$selection" ]] && exit 0 local type type="$(db_type_for "$selection")" # Publication filter for news subs local pub_filter="" case "$selection" in the-soapbox|field-notes|off-the-record) pub_filter="$selection" ;; esac # Select entry local query="SELECT title || ' [' || slug || ']' FROM $type " [[ -n "$pub_filter" ]] && query+="WHERE publication = '$pub_filter' " query+="ORDER BY title" local slug slug="$( sql "$query" | dmenu -i -l 20 -p "$type:" | sed 's/.*\[\(.*\)\]/\1/' )" [[ -z "$slug" ]] && exit 0 # Edit loop while true; do local fields="title preview category_slug tags state" if [[ "$type" != "diary" ]]; then fields+=" status confidence importance" fi if [[ "$type" == "news" ]]; then fields+=" publication" fi local field field="$(echo "$fields" | dmenu -i -l 12 -p "field:")" [[ -z "$field" ]] && break edit_field_dmenu "$type" "$slug" "$field" done } ############################################################################### # dispatch ############################################################################### case "${UI_MODE}-${ACTION_MODE}" in nnn-create) run_nnn_create ;; dmenu-create) run_dmenu_create ;; nnn-edit) run_nnn_edit ;; dmenu-edit) run_dmenu_edit ;; nnn-data) run_nnn_data ;; dmenu-data) run_dmenu_data ;; esac