#!/usr/bin/env bash # Exit on error, undefined variables, and pipe failures set -euo pipefail show_help() { cat << EOF Usage: $(basename "$0") [options] Create a new note in your Obsidian vault. Options: -v, --vault Specify the Obsidian vault directory. --overwrite Overwrite the file if it already exists. -h, --help Show this help message. EOF } VAULT_DIR="" OVERWRITE="false" NOTE_PATH="" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in -v|--vault) if [[ -z "${2:-}" ]]; then echo "Error: --vault requires a directory path." >&2 exit 1 fi VAULT_DIR="$2" shift 2 ;; --overwrite) OVERWRITE="true" shift ;; -h|--help) show_help exit 0 ;; -*) echo "Error: Unknown option: $1" >&2 show_help exit 1 ;; *) if [[ -n "$NOTE_PATH" ]]; then echo "Error: Multiple note paths provided: $NOTE_PATH and $1" >&2 exit 1 fi NOTE_PATH="$1" shift ;; esac done if [[ -z "$NOTE_PATH" ]]; then echo "Error: Missing note path/name." >&2 show_help exit 1 fi # 1. Find the Obsidian vault. if [[ -n "$VAULT_DIR" ]]; then # -v/--vault was passed. Check that the directory exists and contains a .obsidian directory. if [[ ! -d "$VAULT_DIR" ]]; then echo "Error: Specified vault directory does not exist: $VAULT_DIR" >&2 exit 1 fi if [[ ! -d "$VAULT_DIR/.obsidian" ]]; then echo "Error: Specified directory is not an Obsidian vault (missing .obsidian): $VAULT_DIR" >&2 exit 1 fi VAULT_DIR="$(cd "$VAULT_DIR" && pwd)" else # Search directories upward for a parent (or current) directory containing a .obsidian subdirectory. # Stop at the user's home directory or a filesystem boundary. CURRENT_DIR="$PWD" FOUND_VAULT="" get_device_id() { local dir="$1" if [[ "$(uname)" == "Darwin" ]]; then stat -f '%d' "$dir" 2>/dev/null || echo "" else stat -c '%d' "$dir" 2>/dev/null || echo "" fi } CURRENT_DEV="$(get_device_id "$CURRENT_DIR")" while [[ "$CURRENT_DIR" != "/" && "$CURRENT_DIR" != "$HOME" ]]; do if [[ -d "$CURRENT_DIR/.obsidian" ]]; then FOUND_VAULT="$CURRENT_DIR" break fi PARENT_DIR="$(dirname "$CURRENT_DIR")" if [[ "$PARENT_DIR" == "$CURRENT_DIR" ]]; then break fi # Check filesystem boundary PARENT_DEV="$(get_device_id "$PARENT_DIR")" if [[ -n "$CURRENT_DEV" && -n "$PARENT_DEV" && "$CURRENT_DEV" != "$PARENT_DEV" ]]; then break fi CURRENT_DIR="$PARENT_DIR" CURRENT_DEV="$PARENT_DEV" done # Check the last checked directory (could be HOME or /) if [[ -z "$FOUND_VAULT" && -d "$CURRENT_DIR/.obsidian" ]]; then FOUND_VAULT="$CURRENT_DIR" fi if [[ -n "$FOUND_VAULT" ]]; then VAULT_DIR="$FOUND_VAULT" else # Fallback paths in order: ~/Notes, ~/Obsidian/Notes, ~/Personal/Notes, ~/Projects/Notes. FALLBACKS=( "$HOME/Notes" "$HOME/Obsidian/Notes" "$HOME/Personal/Notes" "$HOME/Projects/Notes" ) for path in "${FALLBACKS[@]}"; do if [[ -d "$path" && -d "$path/.obsidian" ]]; then VAULT_DIR="$(cd "$path" && pwd)" break fi done fi fi if [[ -z "$VAULT_DIR" ]]; then echo "Error: Could not find an Obsidian vault. Please specify one with -v/--vault." >&2 exit 1 fi # Strip leading slash from NOTE_PATH to handle relative paths properly NOTE_PATH="${NOTE_PATH#/}" # 2. Determine if current working directory is within the vault. # Resolve physical paths to handle symlinks cleanly. RESOLVED_PWD="$(pwd -P)" RESOLVED_VAULT="$(cd "$VAULT_DIR" && pwd -P)" if [[ "$RESOLVED_PWD" == "$RESOLVED_VAULT" || "$RESOLVED_PWD" == "$RESOLVED_VAULT"/* ]]; then # Within the vault, treat relative to CWD TARGET_FILE="$PWD/$NOTE_PATH" else # Not within the vault, treat relative to vault root TARGET_FILE="$VAULT_DIR/$NOTE_PATH" fi TARGET_DIR="$(dirname "$TARGET_FILE")" TARGET_BASE="$(basename "$TARGET_FILE")" # If there's no file extension on the argument, add .md. if [[ "$TARGET_BASE" != *.* ]]; then TARGET_BASE="$TARGET_BASE.md" fi TARGET_FILE="$TARGET_DIR/$TARGET_BASE" # If the necessary directory components don't exist, create them. if [[ ! -d "$TARGET_DIR" ]]; then mkdir -p "$TARGET_DIR" fi # 3. If the file exists and there's no --overwrite argument, throw an error. if [[ -f "$TARGET_FILE" && "$OVERWRITE" != "true" ]]; then echo "Error: File already exists: $TARGET_FILE (use --overwrite to replace it)" >&2 exit 1 fi # 4. If the path ends in .md (case-insensitive), generate appropriate YAML front matter and write it to the new file. if [[ "${TARGET_BASE,,}" == *.md ]]; then NOTE_TITLE="${TARGET_BASE%.*}" CURRENT_DATE="$(date +"%Y-%m-%d %H:%M")" cat << EOF > "$TARGET_FILE" --- title: "$NOTE_TITLE" date: $CURRENT_DATE tags: [] --- EOF else # Just touch the file to ensure it exists touch "$TARGET_FILE" fi # 5. Open the user's $EDITOR (falling back to vim/vi if unset) pointing to the new file. EDITOR="${EDITOR:-}" if [[ -z "$EDITOR" ]]; then if command -v vim >/dev/null 2>&1; then EDITOR="vim" else EDITOR="vi" fi fi exec "$EDITOR" "$TARGET_FILE"