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