#!/bin/bash # omarchy-theme-set-obsidian: Bootstrap and update Omarchy theme for Obsidian # # - Ensures registry at ~/.local/state/omarchy/obsidian-vaults # - Populates by extracting vault paths from ~/.config/obsidian/obsidian.json # - 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_vaults_file() { mkdir -p "$(dirname "$VAULTS_FILE")" local tmpfile tmpfile="$(mktemp)" # Extract the Obsidian vault location from config file //.obsidian jq -r '.vaults | values[].path' ~/.config/obsidian/obsidian.json 2>/dev/null >>"$tmpfile" if [ -s "$tmpfile" ]; then sort -u "$tmpfile" >"$VAULTS_FILE" else : >"$VAULTS_FILE" fi rm "$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"