#!/usr/bin/env bash ############################################################################### # dnotes -- browse and create md-roam notes in ~/lit/slipbox with dmenu ############################################################################### set -euo pipefail NOTES_DIR="${NOTES_DIR:-$HOME/lit/slipbox}" DMENU="${DMENU:-dmenu -i}" NOTE_ICON="󱉟" CREATE_ICON="+" NOTE_TYPES=("evergreen" "paper" "person" "question" "letter" "lecture" "company") NOTE_STATES=("active" "hidden") NOTE_STATUSES=("rough" "growing" "evergreen" "abandoned") NOTE_CERTAINTIES=("certain" "highly likely" "likely" "possible" "unlikely" "highly unlikely" "remote" "impossible") trim() { sed 's/^[[:space:]]*//;s/[[:space:]]*$//' } notify() { if command -v notify-send >/dev/null 2>&1; then notify-send "dnotes" "$1" fi } slugify() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//' } yaml_quote() { local value="$1" value="${value//\\/\\\\}" value="${value//\"/\\\"}" printf '"%s"' "$value" } inline_list() { local input="$1" input=$(printf '%s' "$input" | trim) if [ -z "$input" ]; then printf '[]' else printf '[%s]' "$input" fi } prompt_text() { local prompt="$1" local default="${2:-}" local value value=$(printf '%s' "$default" | $DMENU -p "$prompt") || exit 0 printf '%s' "$value" | trim } choose_one() { local prompt="$1" local default="$2" shift 2 local value value=$(printf '%s\n' "$@" | $DMENU -l "$#" -p "$prompt [$default]") || exit 0 if [ -z "$value" ]; then printf '%s' "$default" else printf '%s' "$value" fi } prompt_int_range() { local prompt="$1" local min="$2" local max="$3" local default="$4" local value while true; do value=$(prompt_text "$prompt [$default]" "$default") [ -z "$value" ] && value="$default" if [[ "$value" =~ ^[0-9]+$ ]] && [ "$value" -ge "$min" ] && [ "$value" -le "$max" ]; then printf '%s' "$value" return fi notify "$prompt must be between $min and $max" done } prompt_timestamp() { local prefix="$1" local year month day hour minute second year=$(prompt_int_range "$prefix year" 1 9999 "$(date -u +%Y)") month=$(prompt_int_range "$prefix month" 1 12 "$(date -u +%m)") day=$(prompt_int_range "$prefix day" 1 31 "$(date -u +%d)") hour=$(prompt_int_range "$prefix hour UTC" 0 23 "$(date -u +%H)") minute=$(prompt_int_range "$prefix minute UTC" 0 59 "$(date -u +%M)") second=$(prompt_int_range "$prefix second UTC" 0 59 "$(date -u +%S)") if ! date -u -d "$year-$month-$day $hour:$minute:$second" >/dev/null 2>&1; then notify "Invalid date" prompt_timestamp "$prefix" return fi printf '%04d-%02d-%02dT%02d:%02d:%02dZ' "$year" "$month" "$day" "$hour" "$minute" "$second" } open_in_emacs() { local file="$1" local escaped="${file//\\/\\\\}" escaped="${escaped//\"/\\\"}" if emacsclient -e "(window-system)" 2>/dev/null | grep -q "x"; then emacsclient -e "(progn (split-window-right 80) (other-window 1) (find-file \"$escaped\") (when (fboundp 'kris/prose-wrap-mode) (kris/prose-wrap-mode)) (raise-frame) (select-frame-set-input-focus (selected-frame)) (when (fboundp 'evil-normal-state) (evil-normal-state)))" >/dev/null 2>&1 || emacsclient -n "$file" else emacs "$file" & fi } sync_note_in_emacs() { local file="$1" local escaped="${file//\\/\\\\}" escaped="${escaped//\"/\\\"}" emacsclient -e "(when (fboundp 'org-roam-db-update-file) (org-roam-db-update-file \"$escaped\"))" >/dev/null 2>&1 || true } extract_title() { local file="$1" awk ' NR == 1 && $0 == "---" { fm = 1; next } fm && $0 == "---" { exit } fm && $0 ~ /^title:[[:space:]]*/ { sub(/^title:[[:space:]]*/, "") gsub(/^["'\'']|["'\'']$/, "") print exit } ' "$file" } create_note() { mkdir -p "$NOTES_DIR" local title slug default_slug type stime etime state status certainty importance preview linked_notes tags file title=$(prompt_text "Title") [ -z "$title" ] && exit 0 default_slug=$(slugify "$title") slug=$(prompt_text "Slug" "$default_slug") [ -z "$slug" ] && slug="$default_slug" type=$(choose_one "Type" "evergreen" "${NOTE_TYPES[@]}") stime=$(prompt_timestamp "Created") if [ "$(choose_one "Set completion time?" "no" "no" "yes")" = "yes" ]; then etime=$(prompt_timestamp "Completed") else etime="" fi state=$(choose_one "State" "active" "${NOTE_STATES[@]}") status=$(choose_one "Status" "rough" "${NOTE_STATUSES[@]}") certainty=$(choose_one "Certainty" "possible" "${NOTE_CERTAINTIES[@]}") importance=$(prompt_int_range "Importance" 0 10 5) preview=$(prompt_text "Preview") linked_notes=$(prompt_text "Linked note slugs, comma-separated") tags=$(prompt_text "Tags, comma-separated") file="$NOTES_DIR/$slug.md" if [ -e "$file" ]; then notify "Note already exists: $slug.md" exit 1 fi { printf '%s\n' '---' printf 'title: %s\n' "$(yaml_quote "$title")" printf 'slug: %s\n' "$slug" printf 'type: %s\n' "$type" printf 'stime: %s\n' "$stime" printf 'etime: %s\n' "$etime" printf 'state: %s\n' "$state" printf 'status: %s\n' "$status" printf 'certainty: %s\n' "$certainty" printf 'importance: %s\n' "$importance" printf 'preview: %s\n' "$(yaml_quote "$preview")" printf 'backlinks: 0\n' printf 'linkedNotes: %s\n' "$(inline_list "$linked_notes")" printf 'tags: %s\n' "$(inline_list "$tags")" printf '%s\n\n' '---' } > "$file" sync_note_in_emacs "$file" open_in_emacs "$file" notify "Created: $slug.md" } open_note() { local files menu choice slug file title files=$(find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' | sort) if [ -z "$files" ]; then notify "No notes in $NOTES_DIR" exit 1 fi menu="" while IFS= read -r file; do slug=$(basename "$file" .md) title=$(extract_title "$file") [ -z "$title" ] && title="$slug" menu+="$title [$slug]"$'\n' done <<< "$files" choice=$(printf '%s' "$menu" | $DMENU -l 20 -p "$NOTE_ICON Open note") || exit 0 [ -z "$choice" ] && exit 0 slug=$(printf '%s' "$choice" | sed 's/.*\[//; s/\]$//') file="$NOTES_DIR/$slug.md" if [ -f "$file" ]; then open_in_emacs "$file" else notify "File not found: $slug.md" exit 1 fi } main() { local choice choice=$(printf 'open\ncreate\n' | $DMENU -l 2 -p "$NOTE_ICON Notes") || exit 0 case "$choice" in open) open_note ;; create) create_note ;; *) exit 0 ;; esac } main "$@"