#!/usr/bin/env bash set -euo pipefail ############################################################################### # KRIS EDIT — Content metadata editor for krisyotam.com # # Maintainer: Kris Yotam # License: MIT # Created: 2026-02-15 # Description: Edit metadata fields (title, preview, category, tags, status, # confidence, importance, state) for any content entry in # content.db. Select entries via fzf or nnn. ############################################################################### ############################################################################### # config ############################################################################### DB="/home/krisyotam/dev/krisyotam.com/public/data/content.db" CONTENT_DIR="$HOME/content" CONTENT_TYPES=( blog diary essays fiction news notes ocs papers progymnasmata reviews verse ) # 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") ############################################################################### # helpers ############################################################################### have_cmd() { command -v "$1" >/dev/null 2>&1; } die() { echo "ERROR: $1" >&2; exit 1; } have_cmd fzf || die "fzf not found" have_cmd sqlite3 || die "sqlite3 not found" [[ -f "$DB" ]] || die "content.db not found at $DB" sql() { sqlite3 "$DB" "$1"; } ############################################################################### # ui ############################################################################### print_banner() { local cols cols="$(tput cols 2>/dev/null || echo 80)" if have_cmd figlet; then figlet -w "$cols" -f big "EDIT" 2>/dev/null \ || figlet -w "$cols" "EDIT" else echo "=== EDIT ===" fi echo echo " Content metadata editor for krisyotam.com" echo } # Draw a box around metadata show_metadata() { local type="$1" slug="$2" local title preview category status confidence importance state tags 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'")" # Get tags local id 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 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 printf " │ %-14s %s\n" "State:" "$state" echo " └──────────────────────────────────────────" echo } ############################################################################### # field editing ############################################################################### edit_field() { 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 # Escape single quotes for SQL 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 "$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 ;; 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 } edit_tags() { 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 # Check if tag already linked 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 } ############################################################################### # entry selection ############################################################################### select_entry_fzf() { local type="$1" sql "SELECT title || ' (' || slug || ')' || char(9) || slug FROM $type WHERE state != '' ORDER BY title" | 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() { local type="$1" local content_path="$CONTENT_DIR/$type" [[ ! -d "$content_path" ]] && { echo ""; return; } # Use nnn to pick a file, extract slug from filename local tmpfile tmpfile="$(mktemp)" NNN_TMPFILE="$tmpfile" nnn -p "$tmpfile" "$content_path" if [[ -s "$tmpfile" ]]; then local selected selected="$(cat "$tmpfile")" rm -f "$tmpfile" # Extract slug from filename: /path/to/my-slug.mdx -> my-slug basename "$selected" .mdx else rm -f "$tmpfile" echo "" fi } ############################################################################### # main ############################################################################### main() { clear print_banner while true; do # Select content type local type type="$( printf "%s\n" "${CONTENT_TYPES[@]}" "quit" | fzf --prompt="type> " --height=15 --reverse )" || exit 0 [[ "$type" == "quit" ]] && exit 0 # Select how to pick entry 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")" || continue else slug="$(select_entry_nnn "$type")" || continue fi [[ -z "$slug" ]] && continue # Verify slug exists in DB 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 # Edit loop while true; do clear print_banner show_metadata "$type" "$slug" # Build field list based on type local fields if [[ "$type" == "diary" ]]; then fields=("${DIARY_FIELDS[@]}" "tags" "done") else fields=("${STANDARD_FIELDS[@]}" "tags" "done") fi local field field="$( printf "%s\n" "${fields[@]}" | fzf --prompt="edit field> " --height=12 --reverse )" || break [[ "$field" == "done" ]] && break edit_field "$type" "$slug" "$field" read -rp " Press Enter..." _ done done } main