#!/usr/bin/env bash
###############################################################################
# dnotes — Browse and create notes in ~/notes/ via dmenu
#
# Maintainer: Kris Yotam <krisyotam@pm.me>
# License: MIT
# Created: 2026-02-15
# Updated: 2026-02-24
#
# Lists note folders with nerd icons in dmenu. Selecting a folder lists
# its .md files for opening in Emacs. A "Create" option at the bottom
# prompts for frontmatter and creates a new .md note.
#
# Usage:
# dnotes
###############################################################################
NOTES_DIR="$HOME/notes"
###############################################################################
# folder icons
###############################################################################
declare -A ICONS=(
[cards]=""
[index]=""
[jottings]=""
[marginalia]=""
[slipbox]=""
)
CREATE_ICON=""
DEFAULT_ICON=""
###############################################################################
# helpers
###############################################################################
get_icon() {
local folder="$1"
echo "${ICONS[$folder]:-$DEFAULT_ICON}"
}
slugify() {
echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//'
}
open_in_emacs() {
local file="$1"
# Check if emacsclient can reach a running GUI Emacs
if emacsclient -e "(window-system)" 2>/dev/null | grep -q "x"; then
# GUI Emacs running with server — open in vertical split pane (like SPC p n)
emacsclient -e "(progn
(split-window-right 80)
(other-window 1)
(find-file \"$file\")
(raise-frame)
(select-frame-set-input-focus (selected-frame))
(evil-normal-state))" 2>/dev/null
else
# No GUI Emacs — launch one with the file
emacs "$file" &
fi
}
###############################################################################
# create note
###############################################################################
create_note() {
# Pick folder for new note
local MENU=""
for folder in slipbox jottings marginalia index cards; do
[ -d "$NOTES_DIR/$folder" ] && MENU+="$(get_icon "$folder") $folder"$'\n'
done
local CHOICE
CHOICE=$(printf '%s' "$MENU" | sed '/^$/d' | dmenu -i -l 10 -p "$CREATE_ICON Create in")
[ -z "$CHOICE" ] && exit 0
local FOLDER
FOLDER=$(echo "$CHOICE" | sed 's/^[^ ]* *//')
# Prompt for title
local TITLE
TITLE=$(printf '' | dmenu -p " Title:")
TITLE=$(echo "$TITLE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[ -z "$TITLE" ] && exit 0
local SLUG
SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g; s/[^a-z0-9-]//g; s/--*/-/g; s/^-//; s/-$//')
# Prompt for tags (comma-separated)
local TAGS_INPUT
TAGS_INPUT=$(echo "" | dmenu -p " Tags (comma-separated, e.g. math, calculus):")
# Format as YAML list: "math, calculus" → "[math, calculus]"
local TAGS="[]"
if [ -n "$TAGS_INPUT" ]; then
TAGS="[${TAGS_INPUT}]"
fi
# Prompt for certainty
local CERTAINTY
CERTAINTY=$(printf 'possible\ncertain\nlikely\nunlikely\nspeculative' | dmenu -i -l 5 -p " Certainty:")
[ -z "$CERTAINTY" ] && CERTAINTY="possible"
# Prompt for importance (1-10)
local IMPORTANCE
IMPORTANCE=$(printf '1\n2\n3\n4\n5\n6\n7\n8\n9\n10' | dmenu -i -l 10 -p " Importance:")
[ -z "$IMPORTANCE" ] && IMPORTANCE="5"
# Prompt for preview
local PREVIEW
PREVIEW=$(echo "" | dmenu -p " Preview:")
local DATE
DATE=$(date +%Y-%m-%d)
local FILEPATH="$NOTES_DIR/$FOLDER/${SLUG}.md"
if [ -f "$FILEPATH" ]; then
notify-send "dnotes" "Note already exists: $SLUG"
exit 1
fi
cat > "$FILEPATH" <<EOF
---
id: $SLUG
start: $DATE
finish:
certainty: $CERTAINTY
status: seed
importance: $IMPORTANCE
preview: $PREVIEW
tags: $TAGS
---
# $TITLE
EOF
open_in_emacs "$FILEPATH"
notify-send "dnotes" "Created: $FOLDER/$SLUG.md"
}
###############################################################################
# main
###############################################################################
# Build folder menu
MENU=""
for folder in slipbox jottings marginalia index cards; do
[ -d "$NOTES_DIR/$folder" ] && MENU+="$(get_icon "$folder") $folder"$'\n'
done
MENU+=" create"
# Select folder or create
CHOICE=$(printf '%s' "$MENU" | sed '/^$/d' | dmenu -i -l 10 -p " Notes")
[ -z "$CHOICE" ] && exit 0
# Check if Create was selected
SELECTED=$(echo "$CHOICE" | sed 's/^[^ ]* *//')
if [ "$SELECTED" = "create" ]; then
create_note
exit 0
fi
FOLDER="$SELECTED"
# List .md files in folder
FILES=$(find "$NOTES_DIR/$FOLDER" -maxdepth 1 -name "*.md" -type f | sort)
if [ -z "$FILES" ]; then
notify-send "dnotes" "No notes in $FOLDER"
exit 1
fi
# Build file menu from markdown titles
FILE_MENU=""
while IFS= read -r filepath; do
filename=$(basename "$filepath" .md)
# Extract title from # heading
title=$(grep -m1 '^# ' "$filepath" | sed 's/^# *//')
[ -z "$title" ] && title="$filename"
FILE_MENU+="$title [$filename]"$'\n'
done <<< "$FILES"
# Select file
FILE_CHOICE=$(printf '%s' "$FILE_MENU" | sed '/^$/d' | dmenu -i -l 20 -p "$(get_icon "$FOLDER") $FOLDER")
[ -z "$FILE_CHOICE" ] && exit 0
# Extract slug from selection
SLUG=$(echo "$FILE_CHOICE" | sed 's/.*\[//; s/\]//')
MD_FILE="$NOTES_DIR/$FOLDER/${SLUG}.md"
if [ -f "$MD_FILE" ]; then
open_in_emacs "$MD_FILE"
else
notify-send "dnotes" "File not found: $SLUG.md"
exit 1
fi