diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set
index 5dd9edd8..b729dee6 100755
--- a/bin/omarchy-theme-set
+++ b/bin/omarchy-theme-set
@@ -38,3 +38,4 @@ omarchy-theme-set-gnome
omarchy-theme-set-browser
omarchy-theme-set-vscode
omarchy-theme-set-cursor
+omarchy-theme-set-obsidian
diff --git a/bin/omarchy-theme-set-obsidian b/bin/omarchy-theme-set-obsidian
new file mode 100755
index 00000000..e67f0b62
--- /dev/null
+++ b/bin/omarchy-theme-set-obsidian
@@ -0,0 +1,659 @@
+#!/bin/bash
+
+# omarchy-theme-set-obsidian: Bootstrap and update Omarchy theme for Obsidian
+#
+# - Ensures registry at ~/.local/state/omarchy/obsidian-vaults
+# - If missing/empty, bootstraps by scanning ~/Documents/*/.obsidian and ~/Dropbox/*/.obsidian
+# - For each valid vault:
+# - Ensures .obsidian/themes/Omarchy/{manifest.json, theme.css}
+# - Updates theme.css (uses current theme’s obsidian.css if present; otherwise generates -- see below)
+
+# Theme automagic generation logic:
+#
+# - Background/foreground: read from ~/.config/omarchy/current/theme/alacritty.toml [colors.primary]
+# (background/foreground). Fallbacks: bg=#1a1b26, fg=#a9b1d6. Compute bg brightness for light/dark handling.
+# - Palette extraction: collect colors from Alacritty (primary/normal/bright/dim/selection), Waybar (@define-color),
+# and Hyprland (col.*_border; rgba->hex). Normalize, dedupe, and count frequencies.
+# - Slot ordering: remove bg/fg, sort remaining colors by frequency, then fill 13 slots by cycling. Map slots to:
+# h1–h6, links, inline code, marks, interactive accent, blockquote border; muted/faint use border color.
+# - Code colors: code background = closest color to bg (Euclidean RGB); if none, make a subtle bg variant (+/− RGB).
+# code foreground = closest color to fg; fallback #e0e0e0.
+# - Border color: from btop.theme theme[div_line]; else blended mix biased toward bg (≈ (bg+fg)/3).
+# - Selection: from Alacritty [colors.selection] (background/text), honoring CellForeground/Background.
+# If missing, background = 75% bg + 25% fg; text chosen for contrast vs selection background.
+# - Fonts: monospace from Alacritty [font] or fontconfig monospace; UI font from fontconfig sans-serif.
+
+VAULTS_FILE="$HOME/.local/state/omarchy/obsidian-vaults"
+CURRENT_THEME_DIR="$HOME/.config/omarchy/current/theme"
+
+# Ensure the vaults registry exists, or bootstrap from known locations
+ensure_vaults_file() {
+ mkdir -p "$(dirname "$VAULTS_FILE")"
+ # If file exists (even empty), do not scan; treat as authoritative
+ if [ -f "$VAULTS_FILE" ]; then
+ return
+ fi
+ local tmpfile
+ tmpfile="$(mktemp)"
+ # Scan a couple of common locations for //.obsidian
+ for base in "$HOME/Documents" "$HOME/Dropbox"; do
+ [ -d "$base" ] || continue
+ for d in "$base"/*/.obsidian; do
+ [ -d "$d" ] || continue
+ vault_dir="${d%/.obsidian}"
+ printf "%s\n" "$vault_dir" >>"$tmpfile"
+ done
+ done
+ if [ -s "$tmpfile" ]; then
+ sort -u "$tmpfile" >"$VAULTS_FILE"
+ else
+ : >"$VAULTS_FILE"
+ fi
+ rm -f "$tmpfile"
+}
+
+# Ensure theme directory and minimal manifest exist in a vault
+ensure_theme_scaffold() {
+ local vault_path="$1"
+ local theme_dir="$vault_path/.obsidian/themes/Omarchy"
+ mkdir -p "$theme_dir"
+ if [ ! -f "$theme_dir/manifest.json" ]; then
+ cat >"$theme_dir/manifest.json" <<'EOF'
+{
+ "name": "Omarchy",
+ "version": "1.0.0",
+ "minAppVersion": "0.16.0",
+ "description": "Automatically syncs with your current Omarchy system theme colors and fonts",
+ "author": "Omarchy",
+ "authorUrl": "https://omarchy.org"
+}
+EOF
+ fi
+ [ -f "$theme_dir/theme.css" ] || : >"$theme_dir/theme.css"
+}
+
+# Function to extract hex color from string
+extract_hex_color() {
+ echo "$1" | grep -oE '#[0-9a-fA-F]{6}' | head -1
+}
+
+# Function to convert RGB/RGBA to hex
+rgb_to_hex() {
+ local rgb_string="$1"
+ if [[ $rgb_string =~ rgba?\(([0-9]+),\s*([0-9]+),\s*([0-9]+) ]]; then
+ printf "#%02x%02x%02x\n" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
+ fi
+}
+
+# Convert hex to RGB components
+hex_to_rgb() {
+ local hex="${1#\#}"
+ printf "%d %d %d\n" "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
+}
+
+# Calculate perceived brightness (0-255)
+calculate_brightness() {
+ local hex="$1"
+ read -r r g b <<<"$(hex_to_rgb "$hex")"
+ # Use perceived brightness formula
+ echo $(((r * 299 + g * 587 + b * 114) / 1000))
+}
+
+# Calculate color distance (euclidean in RGB space)
+color_distance() {
+ local hex1="$1"
+ local hex2="$2"
+ read -r r1 g1 b1 <<<"$(hex_to_rgb "$hex1")"
+ read -r r2 g2 b2 <<<"$(hex_to_rgb "$hex2")"
+ echo $(((r1 - r2) * (r1 - r2) + (g1 - g2) * (g1 - g2) + (b1 - b2) * (b1 - b2)))
+}
+
+# Extract all colors with frequency count
+extract_all_colors_with_count() {
+ local -A color_counts
+ local color
+
+ # Extract from Alacritty config
+ if [ -f "$CURRENT_THEME_DIR/alacritty.toml" ]; then
+ # Primary colors
+ while IFS= read -r color; do
+ color=$(echo "$color" | tr '[:upper:]' '[:lower:]') # Lowercase for consistency
+ [ -n "$color" ] && ((color_counts["$color"]++))
+ done < <(grep -E "(background|foreground|cursor|text)" "$CURRENT_THEME_DIR/alacritty.toml" | sed "s/.*[\"']0x/#/;s/.*[\"']#/#/;s/[\"'].*//;s/.*#\([0-9a-fA-F]\{6\}\).*/\#\1/" | grep "^#")
+
+ # Normal colors
+ while IFS= read -r color; do
+ color=$(echo "$color" | tr '[:upper:]' '[:lower:]') # Lowercase for consistency
+ [ -n "$color" ] && ((color_counts["$color"]++))
+ done < <(grep -A 20 "\[colors.normal\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep -E "(black|red|green|yellow|blue|magenta|cyan|white)" | sed "s/.*[\"']0x/#/;s/.*[\"']#/#/;s/[\"'].*//;s/.*#\([0-9a-fA-F]\{6\}\).*/\#\1/" | grep "^#")
+
+ # Bright colors
+ while IFS= read -r color; do
+ # Add # if missing
+ [[ "$color" =~ ^[0-9a-fA-F]{6}$ ]] && color="#$color"
+ color=$(echo "$color" | tr '[:upper:]' '[:lower:]') # Lowercase for consistency
+ [[ "$color" =~ ^#[0-9a-f]{6}$ ]] && ((color_counts["$color"]++))
+ done < <(grep -A 20 "\[colors.bright\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep -E "(black|red|green|yellow|blue|magenta|cyan|white)" | sed "s/.*[\"']//;s/[\"'].*//")
+
+ # Dim colors if present
+ while IFS= read -r color; do
+ # Add # if missing
+ [[ "$color" =~ ^[0-9a-fA-F]{6}$ ]] && color="#$color"
+ color=$(echo "$color" | tr '[:upper:]' '[:lower:]') # Lowercase for consistency
+ [[ "$color" =~ ^#[0-9a-f]{6}$ ]] && ((color_counts["$color"]++))
+ done < <(grep -A 20 "\[colors.dim\]" "$CURRENT_THEME_DIR/alacritty.toml" 2>/dev/null | grep -E "(black|red|green|yellow|blue|magenta|cyan|white)" | sed "s/.*[\"']//;s/[\"'].*//")
+
+ # Selection colors
+ while IFS= read -r color; do
+ color=$(echo "$color" | tr '[:upper:]' '[:lower:]') # Lowercase for consistency
+ [ -n "$color" ] && ((color_counts["$color"]++))
+ done < <(grep -A 5 "\[colors.selection\]" "$CURRENT_THEME_DIR/alacritty.toml" 2>/dev/null | grep -E "(background|text)" | sed "s/.*[\"']0x/#/;s/.*[\"']#/#/;s/[\"'].*//;s/.*#\([0-9a-fA-F]\{6\}\).*/\#\1/" | grep "^#")
+ fi
+
+ # Extract from Waybar CSS
+ if [ -f "$CURRENT_THEME_DIR/waybar.css" ]; then
+ while IFS= read -r color; do
+ color=$(echo "$color" | tr '[:upper:]' '[:lower:]') # Lowercase for consistency
+ [ -n "$color" ] && ((color_counts["$color"]++))
+ done < <(grep -oE '@define-color [a-z_-]+ #[0-9a-fA-F]{6}' "$CURRENT_THEME_DIR/waybar.css" | grep -oE '#[0-9a-fA-F]{6}')
+ fi
+
+ # Extract from Hyprland config
+ if [ -f "$CURRENT_THEME_DIR/hyprland.conf" ]; then
+ while IFS= read -r color; do
+ if [[ $color == rgba* ]] || [[ $color == rgb* ]]; then
+ color=$(rgb_to_hex "$color")
+ fi
+ color=$(echo "$color" | tr '[:upper:]' '[:lower:]') # Lowercase for consistency
+ [ -n "$color" ] && ((color_counts["$color"]++))
+ done < <(grep -E "col\.(active|inactive)_border" "$CURRENT_THEME_DIR/hyprland.conf" | grep -oE 'rgba?\([^)]+\)|#[0-9a-fA-F]{6,8}' | sed 's/ff$//')
+ fi
+
+ # Output colors with their counts
+ for color in "${!color_counts[@]}"; do
+ echo "${color_counts[$color]} $color"
+ done
+}
+
+# Sort colors by frequency
+sort_colors_by_frequency() {
+ # Input is already "count color" format
+ sort -rn | cut -d' ' -f2
+}
+
+# Fill color slots with cycling if needed
+fill_color_slots() {
+ local -a colors=("$@")
+ local -a slots
+ local num_colors=${#colors[@]}
+
+ # Need 13 slots total (code colors are handled separately)
+ local slots_needed=13
+
+ if [ $num_colors -eq 0 ]; then
+ # No colors available, use defaults
+ colors=("#3d3d3d" "#5d5d5d" "#7d7d7d" "#9d9d9d" "#bd93f9" "#50fa7b")
+ num_colors=6
+ fi
+
+ # Fill slots, cycling if necessary
+ for ((i = 0; i < slots_needed; i++)); do
+ slots[$i]="${colors[$((i % num_colors))]}"
+ done
+
+ echo "${slots[@]}"
+}
+
+# Main color extraction and theme generation
+extract_theme_data() {
+ # Get primary colors from Alacritty
+ local bg_color="#1a1b26"
+ local fg_color="#a9b1d6"
+
+ if [ -f "$CURRENT_THEME_DIR/alacritty.toml" ]; then
+ local extracted_bg=$(grep -A 5 "\[colors.primary\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep "^background = " | sed "s/.*[\"']0x/#/;s/.*[\"']#/#/;s/[\"'].*//;s/.*#\([0-9a-fA-F]\{6\}\).*/\#\1/" | head -1 | tr '[:upper:]' '[:lower:]')
+ local extracted_fg=$(grep -A 5 "\[colors.primary\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep "^foreground = " | sed "s/.*[\"']0x/#/;s/.*[\"']#/#/;s/[\"'].*//;s/.*#\([0-9a-fA-F]\{6\}\).*/\#\1/" | head -1 | tr '[:upper:]' '[:lower:]')
+ [ -n "$extracted_bg" ] && bg_color="$extracted_bg"
+ [ -n "$extracted_fg" ] && fg_color="$extracted_fg"
+ fi
+
+ # Determine if light or dark theme
+ local bg_brightness=$(calculate_brightness "$bg_color")
+ local is_light_theme=false
+ [ $bg_brightness -gt 127 ] && is_light_theme=true
+
+ # Extract all colors with counts
+ local color_data=$(extract_all_colors_with_count)
+
+ # Filter out background and foreground colors for the main array
+ local filtered_data=$(echo "$color_data" | grep -v "$bg_color" | grep -v "$fg_color")
+
+ # Get all unique colors (including bg/fg) for distance calculations
+ local -a all_unique_colors
+ readarray -t all_unique_colors < <(echo "$color_data" | cut -d' ' -f2 | sort -u)
+
+ # Find the 3 closest colors to background for background variations
+ local -a bg_distances
+ for color in "${all_unique_colors[@]}"; do
+ if [ "$color" != "$bg_color" ]; then
+ distance=$(color_distance "$color" "$bg_color")
+ bg_distances+=("$distance:$color")
+ fi
+ done
+
+ # Sort by distance and get the closest color for code background
+ local -a closest_to_bg
+ readarray -t closest_to_bg < <(printf '%s\n' "${bg_distances[@]}" | sort -n | head -1 | cut -d: -f2)
+
+ # All background variations use the same as primary background
+ local bg_primary_alt="$bg_color"
+ local bg_secondary="$bg_color"
+ local bg_secondary_alt="$bg_color"
+
+ # Code block background uses the closest different color
+ local code_bg="${closest_to_bg[0]}"
+
+ # If no different color available, create a subtle variant for code blocks
+ if [ -z "$code_bg" ]; then
+ read -r r g b <<<"$(hex_to_rgb "$bg_color")"
+ if [ $bg_brightness -gt 127 ]; then
+ r=$((r - 10))
+ g=$((g - 10))
+ b=$((b - 10))
+ else
+ r=$((r + 15))
+ g=$((g + 15))
+ b=$((b + 15))
+ fi
+ [ $r -lt 0 ] && r=0
+ [ $r -gt 255 ] && r=255
+ [ $g -lt 0 ] && g=0
+ [ $g -gt 255 ] && g=255
+ [ $b -lt 0 ] && b=0
+ [ $b -gt 255 ] && b=255
+ code_bg=$(printf "#%02x%02x%02x" "$r" "$g" "$b")
+ fi
+
+ # Find closest color to foreground for code block text
+ local code_fg=""
+ min_distance=999999999
+ for color in "${all_unique_colors[@]}"; do
+ if [ "$color" != "$fg_color" ]; then
+ distance=$(color_distance "$color" "$fg_color")
+ if [ $distance -lt $min_distance ]; then
+ min_distance=$distance
+ code_fg="$color"
+ fi
+ fi
+ done
+ [ -z "$code_fg" ] && code_fg="#e0e0e0" # Fallback
+
+ # Extract text selection colors from Alacritty
+ local selection_bg=""
+ local selection_fg=""
+ if [ -f "$CURRENT_THEME_DIR/alacritty.toml" ]; then
+ selection_bg=$(grep -A 5 "\[colors.selection\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep "^background = " | sed "s/.*[\"']0x/#/;s/.*[\"']#/#/;s/[\"'].*//;s/.*#\([0-9a-fA-F]\{6\}\).*/\#\1/" | head -1 | tr '[:upper:]' '[:lower:]')
+ local selection_text=$(grep -A 5 "\[colors.selection\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep "^text = " | sed "s/.*[\"']0x/#/;s/.*[\"']#/#/;s/[\"'].*//;s/.*#\([0-9a-fA-F]\{6\}\).*/\#\1/" | head -1 | tr '[:upper:]' '[:lower:]')
+
+ # If text is set to CellForeground/CellBackground, use the appropriate color
+ if [ -z "$selection_text" ] || [[ "$(grep -A 5 "\[colors.selection\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep "^text = ")" == *"CellForeground"* ]]; then
+ selection_fg="$fg_color"
+ elif [[ "$(grep -A 5 "\[colors.selection\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep "^text = ")" == *"CellBackground"* ]]; then
+ selection_fg="$bg_color"
+ else
+ selection_fg="$selection_text"
+ fi
+ fi
+
+ # Fallback if no selection colors found
+ if [ -z "$selection_bg" ]; then
+ read -r r1 g1 b1 <<<"$(hex_to_rgb "$bg_color")"
+ read -r r2 g2 b2 <<<"$(hex_to_rgb "$fg_color")"
+ local r=$(((r1 * 3 + r2) / 4)) # 75% background, 25% foreground
+ local g=$(((g1 * 3 + g2) / 4))
+ local b=$(((b1 * 3 + b2) / 4))
+ selection_bg=$(printf "#%02x%02x%02x" "$r" "$g" "$b")
+ fi
+
+ # Use contrasting color for selection text if not defined
+ if [ -z "$selection_fg" ]; then
+ # Calculate brightness of selection background
+ local sel_brightness=$(calculate_brightness "$selection_bg")
+ if [ $sel_brightness -gt 127 ]; then
+ selection_fg="$bg_color" # Dark text on light selection
+ else
+ selection_fg="$fg_color" # Light text on dark selection
+ fi
+ fi
+
+ # Extract border color from btop theme
+ local border_color=""
+ if [ -f "$CURRENT_THEME_DIR/btop.theme" ]; then
+ # Look for theme[div_line] in btop theme
+ local btop_divline=$(grep 'theme\[div_line\]' "$CURRENT_THEME_DIR/btop.theme" | head -1)
+
+ if [ -n "$btop_divline" ]; then
+ # Extract the color value after the = sign
+ local extracted=$(echo "$btop_divline" | sed 's/.*=//' | xargs)
+
+ # Check if it's a hex color and lowercase it
+ if [[ $extracted =~ ^#[0-9a-fA-F]{6}$ ]]; then
+ border_color=$(echo "$extracted" | tr '[:upper:]' '[:lower:]')
+ elif [[ $extracted =~ ^[0-9a-fA-F]{6}$ ]]; then
+ # Add # if missing and lowercase
+ border_color=$(echo "#$extracted" | tr '[:upper:]' '[:lower:]')
+ fi
+ fi
+ fi
+
+ # Fallback if no border color found
+ if [ -z "$border_color" ]; then
+ # Use a color between bg and fg
+ read -r r1 g1 b1 <<<"$(hex_to_rgb "$bg_color")"
+ read -r r2 g2 b2 <<<"$(hex_to_rgb "$fg_color")"
+ local r=$(((r1 + r2) / 3)) # Closer to background
+ local g=$(((g1 + g2) / 3))
+ local b=$(((b1 + b2) / 3))
+ border_color=$(printf "#%02x%02x%02x" "$r" "$g" "$b")
+ fi
+
+ # Get unique colors array (without bg/fg) sorted by frequency
+ local -a unique_colors
+ readarray -t unique_colors < <(echo "$filtered_data" | sort_colors_by_frequency)
+
+ # Fill the 13 color slots (code colors handled separately)
+ local -a color_slots
+ readarray -t color_slots < <(fill_color_slots "${unique_colors[@]}" | tr ' ' '\n')
+
+ # Extract fonts
+ local monospace_font="CaskaydiaMono Nerd Font"
+ local ui_font="Liberation Sans"
+
+ if [ -f "$CURRENT_THEME_DIR/alacritty.toml" ]; then
+ local alacritty_font=$(grep -A 5 "\[font\]" "$CURRENT_THEME_DIR/alacritty.toml" | grep 'family = ' | head -1 | cut -d'"' -f2)
+ [ -n "$alacritty_font" ] && monospace_font="$alacritty_font"
+ fi
+
+ if [ -f "$HOME/.config/fontconfig/fonts.conf" ]; then
+ local fontconfig_mono=$(xmlstarlet sel -t -v '//match[@target="pattern"][test/string="monospace"]/edit[@name="family"]/string' "$HOME/.config/fontconfig/fonts.conf" 2>/dev/null || true)
+ [ -n "$fontconfig_mono" ] && monospace_font="$fontconfig_mono"
+
+ local fontconfig_sans=$(xmlstarlet sel -t -v '//match[@target="pattern"][test/string="sans-serif"]/edit[@name="family"]/string' "$HOME/.config/fontconfig/fonts.conf" 2>/dev/null || true)
+ [ -n "$fontconfig_sans" ] && ui_font="$fontconfig_sans"
+ fi
+
+ # Generate CSS with 14-slot system
+ cat </dev/null || echo "unknown")") */
+/* Colors sorted by frequency, backgrounds by distance */
+
+.theme-dark, .theme-light {
+ /* Core colors */
+ --background-primary: $bg_color;
+ --text-normal: $fg_color;
+
+ /* Background variations (always distance-based) */
+ --background-primary-alt: $bg_primary_alt;
+ --background-secondary: $bg_secondary;
+ --background-secondary-alt: $bg_secondary_alt;
+
+ /* Code block colors (always distance-based) */
+ --code-background: $code_bg;
+ --code-foreground: $code_fg;
+
+ /* Border color from btop theme */
+ --border-color: $border_color;
+
+ /* Selection colors from Alacritty */
+ --text-selection: $selection_bg;
+ --text-selection-fg: $selection_fg;
+
+ /* 13-slot color system for remaining elements */
+ --text-title-h1: ${color_slots[0]};
+ --text-title-h2: ${color_slots[1]};
+ --text-title-h3: ${color_slots[2]};
+ --text-title-h4: ${color_slots[3]};
+ --text-title-h5: ${color_slots[4]};
+ --text-title-h6: ${color_slots[4]}; /* Same as h5 */
+ --text-link: ${color_slots[5]};
+ --markup-code: ${color_slots[6]};
+ --text-mark: ${color_slots[7]};
+ --interactive-accent: ${color_slots[8]};
+ --blockquote-border: ${color_slots[9]};
+ --text-muted: $border_color; /* Use border color for muted text */
+ --text-faint: $border_color; /* Use border color for faint text */
+
+ /* Additional mappings */
+ --text-accent: var(--interactive-accent);
+ --text-accent-hover: var(--interactive-accent);
+ --text-error: var(--text-title-h1);
+ --text-error-hover: var(--text-title-h1);
+ --text-highlight-bg: $fg_color; /* Use text color as highlight background */
+ --text-on-accent: $bg_color;
+
+ --interactive-normal: var(--code-background);
+ --interactive-hover: var(--interactive-accent);
+ --interactive-accent-hover: var(--interactive-accent);
+ --interactive-success: var(--text-title-h2);
+
+ --scrollbar-bg: var(--background-primary);
+ --scrollbar-thumb-bg: var(--code-background);
+ --scrollbar-active-thumb-bg: var(--interactive-accent);
+
+ --background-modifier-border: var(--border-color);
+ --background-modifier-form-field: var(--code-background);
+ --background-modifier-form-field-highlighted: var(--code-background);
+ --background-modifier-box-shadow: rgba(0, 0, 0, 0.3);
+ --background-modifier-success: var(--interactive-success);
+ --background-modifier-error: var(--text-error);
+ --background-modifier-error-hover: var(--text-error);
+ --background-modifier-cover: rgba(0, 0, 0, 0.8);
+
+ --link-color: var(--text-link);
+ --link-color-hover: var(--text-link);
+ --link-unresolved-color: var(--text-muted);
+ --link-unresolved-opacity: 0.7;
+
+ --tag-color: var(--text-title-h3);
+ --tag-background: var(--code-background);
+
+ --graph-line: var(--text-muted);
+ --graph-node: var(--interactive-accent);
+ --graph-node-unresolved: var(--text-muted);
+ --graph-node-focused: var(--text-link);
+ --graph-node-tag: var(--text-title-h3);
+ --graph-node-attachment: var(--text-title-h2);
+
+ /* Fonts */
+ --font-interface-theme: "$ui_font";
+ --font-text-theme: "$ui_font";
+ --font-monospace-theme: "$monospace_font";
+}
+
+/* Headers */
+.cm-header-1, .markdown-rendered h1 { color: var(--text-title-h1); }
+.cm-header-2, .markdown-rendered h2 { color: var(--text-title-h2); }
+.cm-header-3, .markdown-rendered h3 { color: var(--text-title-h3); }
+.cm-header-4, .markdown-rendered h4 { color: var(--text-title-h4); }
+.cm-header-5, .markdown-rendered h5 { color: var(--text-title-h5); }
+.cm-header-6, .markdown-rendered h6 { color: var(--text-title-h6); }
+
+/* Code blocks */
+.markdown-rendered code {
+ font-family: var(--font-monospace-theme);
+ background-color: var(--code-background);
+ color: var(--markup-code);
+ padding: 2px 4px;
+ border-radius: 3px;
+}
+
+.markdown-rendered pre {
+ background-color: var(--code-background);
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 5px;
+}
+
+.markdown-rendered pre code {
+ background-color: transparent;
+ color: var(--code-foreground);
+}
+
+/* Syntax highlighting */
+.cm-s-obsidian span.cm-keyword { color: var(--text-title-h1); }
+.cm-s-obsidian span.cm-string { color: var(--text-title-h2); }
+.cm-s-obsidian span.cm-number { color: var(--text-title-h3); }
+.cm-s-obsidian span.cm-comment { color: var(--text-muted); }
+.cm-s-obsidian span.cm-operator { color: var(--text-link); }
+.cm-s-obsidian span.cm-variable { color: var(--text-normal); }
+.cm-s-obsidian span.cm-def { color: var(--text-link); }
+
+/* Highlighted text */
+.markdown-rendered mark,
+.cm-s-obsidian span.cm-highlight,
+mark {
+ background-color: var(--text-highlight-bg) !important;
+ color: var(--code-background) !important;
+}
+
+/* Links */
+.markdown-rendered a {
+ color: var(--text-link);
+}
+
+/* Blockquotes */
+.markdown-rendered blockquote {
+ border-left: 4px solid var(--blockquote-border);
+ padding-left: 1em;
+}
+
+/* Status bar */
+.status-bar {
+ background-color: var(--code-background);
+ border-top: 1px solid var(--background-modifier-border);
+}
+
+/* Active file */
+.workspace-leaf.mod-active .workspace-leaf-header-title {
+ color: var(--interactive-accent);
+}
+
+.nav-file-title.is-active {
+ background-color: var(--code-background);
+ color: var(--interactive-accent);
+}
+
+/* Text selection */
+::selection {
+ background-color: var(--text-selection);
+ color: var(--text-selection-fg);
+}
+
+/* Search results */
+.search-result-file-title {
+ color: var(--interactive-accent);
+}
+
+.search-result-file-match {
+ background-color: var(--code-background);
+ color: var(--text-normal);
+ border-left: 3px solid var(--interactive-accent);
+}
+
+.search-result-file-matched-text {
+ color: var(--code-background);
+}
+
+/* Tables */
+.markdown-rendered table {
+ border: 1px solid var(--background-modifier-border);
+}
+
+.markdown-rendered th {
+ background-color: var(--code-background);
+ color: var(--text-accent);
+}
+
+.markdown-rendered td {
+ border: 1px solid var(--background-modifier-border);
+}
+
+/* Callouts */
+.callout {
+ border-left: 4px solid var(--interactive-accent);
+ background-color: var(--code-background);
+}
+.callout * {
+ color: var(--text-normal);
+}
+
+/* Modal dialogs */
+.modal {
+ background-color: var(--background-primary);
+ border: 2px solid var(--background-modifier-border);
+}
+
+/* Settings */
+.vertical-tab-header-group-title {
+ color: var(--interactive-accent);
+}
+
+.vertical-tab-nav-item.is-active {
+ background-color: var(--code-background);
+ color: var(--interactive-accent);
+}
+EOF
+}
+
+# Option handling
+if [ "${1:-}" = "--reset" ]; then
+ echo "♻️ Resetting Omarchy themes and registry..."
+ if [ -f "$VAULTS_FILE" ] && [ -s "$VAULTS_FILE" ]; then
+ while IFS= read -r vault_path || [ -n "$vault_path" ]; do
+ case "$vault_path" in ""|\#*) continue ;; esac
+ vault_path="${vault_path%/}"
+ vault_name=$(basename "$vault_path")
+ theme_dir="$vault_path/.obsidian/themes/Omarchy"
+ if [ -d "$theme_dir" ]; then
+ rm -rf "$theme_dir"
+ echo " ✅ $vault_name (theme removed)"
+ else
+ echo " ℹ️ $vault_name (no theme present)"
+ fi
+ done <"$VAULTS_FILE"
+ fi
+ rm -f "$VAULTS_FILE"
+ echo "✅ Registry removed"
+ exit 0
+fi
+
+# Main update logic
+echo "🔄 Updating Obsidian vaults..."
+
+# Step 1: ensure registry exists (bootstrap if needed)
+ensure_vaults_file
+
+while IFS= read -r vault_path || [ -n "$vault_path" ]; do
+ case "$vault_path" in "" | \#*) continue ;; esac
+ vault_path="${vault_path%/}"
+ vault_name=$(basename "$vault_path")
+
+ # Step 2: verify path exists; log/skip gracefully if invalid
+ if [ ! -d "$vault_path" ] || [ ! -d "$vault_path/.obsidian" ]; then
+ echo " ❌ $vault_name (invalid entry: missing directory or .obsidian)"
+ continue
+ fi
+
+ # Ensure theme files exist for this vault
+ ensure_theme_scaffold "$vault_path"
+ THEME_DIR="$vault_path/.obsidian/themes/Omarchy"
+
+ # Step 3: update theme.css
+ if [ -f "$CURRENT_THEME_DIR/obsidian.css" ]; then
+ cp "$CURRENT_THEME_DIR/obsidian.css" "$THEME_DIR/theme.css"
+ echo " ✅ $vault_name (custom theme)"
+ else
+ extract_theme_data >"$THEME_DIR/theme.css"
+ echo " ✅ $vault_name (generated theme)"
+ fi
+done <"$VAULTS_FILE"
diff --git a/migrations/1760441237.sh b/migrations/1760441237.sh
new file mode 100644
index 00000000..1c2d89d3
--- /dev/null
+++ b/migrations/1760441237.sh
@@ -0,0 +1,3 @@
+echo "Install Omarchy theme on Obsidian vaults"
+
+omarchy-theme-set-obsidian