#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# TOME -- Content manager for krisyotam.com
#
# Maintainer: Kris Yotam <krisyotam@pm.me>
# 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