#!/usr/bin/env bash # ███ ███ ███ ███ # █ █ █ █ █ ██ # █ █ ███ █ ███ # note # needs: fzf, rg # written mostly by chatgpt and deepseek (ai) in april 2026 # agentic engineer: brontosaurusrex set -euo pipefail # set -x NOTES_DIR="${NOTES_DIR:-$HOME/notes}" # try multiple editors for ed in "${EDITOR:-}" hx vim nano; do if command -v "$ed" >/dev/null 2>&1; then EDITOR="$ed" break fi done export FZF_DEFAULT_OPTS="--pointer='>' --color=fg:-1,bg:-1,hl:-1,fg+:-1,bg+:-1,hl+:-1,info:-1,prompt:-1,spinner:-1,pointer:-1,marker:-1,border:-1" # deps for cmd in fzf rg; do if ! command -v "$cmd" &>/dev/null; then echo "Error: $cmd is required" >&2 exit 1 fi done mkdir -p "$NOTES_DIR" today() { date +"%Y-%m-%d" } slugify() { local input="${1:-}" input="$( echo "$input" \ | tr '[:upper:]' '[:lower:]' \ | tr -cd 'a-z0-9 -' \ | tr ' ' '-' \ | sed 's/-\+/-/g' \ | sed 's/^-//' \ | sed 's/-$//' )" echo "$input" } open_note_fzf() { local files # shellcheck disable=SC2016 mapfile -t files < <( find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' \ -printf '%T@ %f\n' \ | sort -nr \ | cut -d' ' -f2- \ | fzf \ --prompt="open> " \ --multi \ --marker='*' \ --bind='tab:toggle' \ --preview='fold -s -w "$FZF_PREVIEW_COLUMNS" "'"$NOTES_DIR"'"/{}' \ --preview-window=right:50% \ --height=80% \ --reverse ) [[ ${#files[@]} -eq 0 ]] && return "$EDITOR" "${files[@]/#/$NOTES_DIR/}" } search_notes() { local query="${1:-}" local selection file line # shellcheck disable=SC2016 selection=$( rg -i -m1 --no-heading --line-number --color=never "$query" "$NOTES_DIR" \ | fzf \ --delimiter : \ --prompt="search> " \ --preview='fold -s -w "$FZF_PREVIEW_COLUMNS" {1}' \ --bind "change:reload:rg -i -m1 --no-heading --line-number --color=never {q} '$NOTES_DIR' || true" \ --phony \ --query "$query" \ --preview-window=right:50% \ --height=80% \ --reverse ) [[ -z "$selection" ]] && return file="${selection%%:*}" line="${selection#*:}" line="${line%%:*}" "$EDITOR" "$file" "+$line" } create_note() { local text="$1" local title slug file [[ -z "$text" ]] && return # first line, max 6 words title="$( printf '%s\n' "$text" \ | head -n1 \ | awk '{ for(i=1;i<=6 && i<=NF;i++) printf "%s%s",$i,(i<6 && i "$file" < " IFS= read -r input fi # If still empty, do nothing [[ -z "$input" ]] && return create_note "$input" } list_notes() { find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' \ -printf '%T@ %f\n' \ | sort -n \ | cut -d' ' -f2- } latest_note() { local file file=$( find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' \ -printf '%T@|%p\n' \ | sort -nr \ | head -n1 \ | cut -d'|' -f2- ) [[ -n "$file" ]] || return 1 printf '%s\n' "$file" } cat_note() { local query="${1:-}" local file="" local count if [[ "$query" =~ ^[0-9]+$ ]]; then count="$query" find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' \ -printf '%T@|%p\n' \ | sort -nr \ | head -n "$count" \ | cut -d'|' -f2- \ | while IFS= read -r f; do #printf '\n===== %s =====\n\n' "$(basename "$f")" cat "$f" printf '\n' done return fi if [[ -z "$query" ]]; then if ! file="$(latest_note)"; then echo "No notes found" return 1 fi else while IFS= read -r f; do if [[ "${f,,}" == *"${query,,}"* ]]; then file="$f" break fi done < <( find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' ) fi [[ -z "$file" ]] && { echo "No match found" exit 1 } cat "$file" } # this is called with cr (cat round) instead of c (cat), needs roundboxPy on path # https://raw.githubusercontent.com/brontosaurusrex/bucentaur/refs/heads/master/.experiments/bin/roundboxPy cat_note_roundbox() { local query="${1:-}" local file="" local count if [[ "$query" =~ ^[0-9]+$ ]]; then count="$query" find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' \ -printf '%T@|%p\n' \ | sort -nr \ | head -n "$count" \ | cut -d'|' -f2- \ | while IFS= read -r f; do roundboxPy < "$f" done return fi if [[ -z "$query" ]]; then if ! file="$(latest_note)"; then echo "No notes found" return 1 fi else while IFS= read -r f; do if [[ "${f,,}" == *"${query,,}"* ]]; then file="$f" break fi done < <( find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' ) fi [[ -z "$file" ]] && { echo "No match found" exit 1 } roundboxPy < "$file" } open_note() { local query="${1:-}" local file="" if [[ -z "$query" ]]; then open_note_fzf return fi while IFS= read -r f; do if [[ "${f,,}" == *"${query,,}"* ]]; then file="$f" break fi done < <( find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' ) [[ -z "$file" ]] && { echo "No match found" exit 1 } "$EDITOR" "$file" } remove_note() { local file trash_dir target trash_dir="$NOTES_DIR/.trash" mkdir -p "$trash_dir" # shellcheck disable=SC2016 file=$( find "$NOTES_DIR" -maxdepth 1 -type f -name '*.md' \ -printf '%T@ %f\n' \ | sort -nr \ | cut -d' ' -f2- \ | fzf \ --prompt="trash> " \ --pointer=">" \ --color="pointer:red" \ --preview='fold -s -w "$FZF_PREVIEW_COLUMNS" "'"$NOTES_DIR"'"/{}' \ --preview-window=right:50% \ --height=80% \ --reverse ) [[ -z "$file" ]] && return printf "Move '%s' to trash? [y/N] " "$file" read -r confirm if [[ ! "$confirm" =~ ^[yY]$ ]]; then echo "Aborted" return fi target="$trash_dir/$file" if [[ -e "$target" ]]; then target="$trash_dir/$(date +%s)-$file" fi mv -- "$NOTES_DIR/$file" "$target" echo "Moved to trash: $file" } usage() { cat < will ask for input note n "title" > new note with explicit title note "text" > create/reopen note note < file.txt > create note from stdin note l > list note > open (fzf) multiple (tab) note o > open (fzf) multiple (tab) note s > search (rg|fzf) note c > cat latest n notes note cr > cat latest n notes (with roundboxPy) note rm > move to .trash note pull > git pull note push > git push note h > this help storage: $NOTES_DIR EOF } cmd="${1:-}" # capture stdin once stdin_data="" if [[ ! -t 0 ]]; then stdin_data="$(cat)" fi case "$cmd" in new|+|n) shift if [[ $# -eq 0 ]]; then printf "> " IFS= read -r input create_note "$input" else create_note "$*" fi ;; list|l) list_notes ;; ""|open|o) shift || true if [[ -n "$stdin_data" ]]; then create_note "$stdin_data" else open_note "$@" fi ;; remove|rm) shift || true remove_note "$@" ;; cat|c) shift || true cat_note "$@" ;; cr) shift || true cat_note_roundbox "$@" ;; grep|search|g|s) shift || true search_notes "$@" ;; h|-h|--help) usage ;; pull) git -C "$NOTES_DIR" pull ;; push) git -C "$NOTES_DIR" add . \ && git -C "$NOTES_DIR" commit -m "note commit" \ && git -C "$NOTES_DIR" push ;; *) if [[ -n "$stdin_data" ]]; then create_note "$stdin_data" else create_note "$*" fi ;; esac