diff --git a/README.md b/README.md
index 9254412..ae56a41 100644
--- a/README.md
+++ b/README.md
@@ -23,15 +23,17 @@ My current setup is built around these core components:
- **`hypr/`**: Hyprland configurations (keybindings, window rules, animations, monitor layout, lid-close display handling, and autostart). See [`hypr/keybinds.md`](hypr/keybinds.md) for custom keybindings.
- **`waybar/`**: Status bar layout, CSS styling, and custom interactive modules.
-- **`ags/`**: Custom desktop widgets built with TypeScript and GTK — media player, notification hub, quick settings, system monitor, and wallpaper picker.
+- **`ags/`**: Custom desktop widgets built with TypeScript and GTK — media player, notification hub, quick settings, system monitor, wallpaper picker, and theme picker.
- **`scripts/`**: Global utility scripts seamlessly exposed as commands by the installer. See [`commands.md`](commands.md).
- **`wallpapers/`**: A collection of local custom wallpapers for dynamic theming. See [`wallpaper-gallery/`](wallpaper-gallery/index.md) for the full gallery (split alphabetically across multiple pages).
+- **`themes/`**: Drop-in local color themes (one `colors.toml` per theme) that `install.sh` deploys into Omarchy's theme directory. See [`themes/README.md`](themes/README.md).
- **`omarchy/hooks/`**: Event hooks for the Omarchy system (e.g. automatically applying dynamic themes when changing wallpapers).
- **`branding/`**: Custom ASCII art and system branding assets.
## Docs
- [Wallpaper Gallery](wallpaper-gallery/index.md) — preview of every wallpaper in `wallpapers/`, split alphabetically across [0-9, A-D](wallpaper-gallery/a-d.md), [E-M](wallpaper-gallery/e-m.md), [M-R](wallpaper-gallery/m-r.md), [R-Y](wallpaper-gallery/r-z.md).
+- [Themes](themes/README.md) — how to add a local color theme.
- [Keybinds](hypr/keybinds.md) — custom Hyprland keybindings on top of Omarchy's defaults.
- [Commands](commands.md) — custom `blob_*` CLI commands exposed by the installer.
diff --git a/ags/app.ts b/ags/app.ts
index 279ac07..560e453 100644
--- a/ags/app.ts
+++ b/ags/app.ts
@@ -4,6 +4,7 @@ import Media from "./widget/Media"
import NotificationCenter from "./widget/Notifications"
import QuickSettings from "./widget/QuickSettings"
import SysMonitor from "./widget/SysMonitor"
+import ThemePicker from "./widget/ThemePicker"
import WallPicker from "./widget/WallPicker"
const SHOW_DESKTOP_MEDIA = false
@@ -25,6 +26,7 @@ app.start({
start("notification-center", NotificationCenter)
start("quick-settings", QuickSettings)
start("sys-monitor", SysMonitor)
+ start("theme-picker", ThemePicker)
start("wall-picker", WallPicker)
},
})
diff --git a/ags/style.css b/ags/style.css
index 9668486..0ecc271 100644
--- a/ags/style.css
+++ b/ags/style.css
@@ -84,7 +84,8 @@
.QuickSettings,
.SysMonitor,
.Calendar,
-.WallPicker {
+.WallPicker,
+.ThemePicker {
background-color: transparent;
}
@@ -349,6 +350,64 @@
font-size: 13px;
}
+.theme-picker {
+ min-width: 680px;
+ min-height: 460px;
+}
+
+.theme-scroll {
+ min-height: 360px;
+}
+
+.theme-chip {
+ padding: 8px;
+ border-radius: 10px;
+ border: 2px solid transparent;
+ background-color: alpha(@color0, 0.6);
+ transition: all 0.2s;
+ min-width: 200px;
+}
+
+.theme-chip:hover {
+ border: 2px solid alpha(@accent, 0.6);
+}
+
+.theme-chip.active {
+ border: 2px solid @accent;
+}
+
+.theme-chip-dynamic {
+ background-color: alpha(@accent, 0.15);
+}
+
+.theme-chip-dynamic.active {
+ background-color: @accent;
+}
+
+.theme-chip-dynamic.active .theme-chip-label,
+.theme-chip-dynamic.active .theme-chip-icon {
+ color: @background;
+}
+
+.theme-chip-icon {
+ font-size: 16px;
+ color: @accent;
+}
+
+.theme-swatches {
+ min-height: 36px;
+ border-radius: 6px;
+}
+
+.theme-swatch {
+ min-width: 10px;
+}
+
+.theme-chip-label {
+ font-size: 12px;
+ color: @foreground;
+}
+
.qs-side-panel {
min-width: 240px;
}
diff --git a/ags/widget/QuickSettings.tsx b/ags/widget/QuickSettings.tsx
index 291780e..8800843 100644
--- a/ags/widget/QuickSettings.tsx
+++ b/ags/widget/QuickSettings.tsx
@@ -176,6 +176,14 @@ function Toggles() {
{ closePanel(); sh("hyprpicker -a") }} />
+ {
+ closePanel()
+ sh("blob_theme")
+ }}
+ />
)
diff --git a/ags/widget/ThemePicker.tsx b/ags/widget/ThemePicker.tsx
new file mode 100644
index 0000000..b7d8587
--- /dev/null
+++ b/ags/widget/ThemePicker.tsx
@@ -0,0 +1,164 @@
+import app from "ags/gtk3/app"
+import { Astal, Gtk } from "ags/gtk3"
+import { createPoll } from "ags/time"
+import { For } from "ags"
+import { sh, shell } from "../lib/utils"
+
+export const THEMEPICKER_WINDOW = "theme-picker"
+
+const COLUMNS = 3
+
+type Theme = { slug: string; name: string; swatches: string[] }
+
+const listCommand =
+ "{ find \"$HOME/.config/omarchy/themes\" -mindepth 1 -maxdepth 1 -type d 2>/dev/null; " +
+ "find \"$OMARCHY_PATH/themes\" -mindepth 1 -maxdepth 1 -type d 2>/dev/null; } | " +
+ "while read -r d; do " +
+ "[ -f \"$d/colors.toml\" ] || continue; " +
+ "n=$(basename \"$d\"); " +
+ "[ \"$n\" = blob-dynamic ] && continue; " +
+ "c=$(grep -E '^color[1-6] *=' \"$d/colors.toml\" | sed -E 's/^color[0-9]+ *= *\"?([^\"]*)\"?.*/\\1/' | paste -sd,); " +
+ "p=$(echo \"$n\" | sed -E 's/(^|-)([a-z])/\\1\\u\\2/g; s/-/ /g'); " +
+ "echo \"$n|$p|$c\"; " +
+ "done | awk -F'|' '!seen[$1]++' | sort -t'|' -k2,2"
+
+const themes = createPoll(
+ [],
+ 5000,
+ shell(listCommand),
+ (stdout) =>
+ stdout
+ .split("\n")
+ .filter((line) => line.length > 0)
+ .map((line) => {
+ const [slug, name, swatchStr] = line.split("|")
+ return { slug, name: name ?? slug, swatches: (swatchStr ?? "").split(",").filter(Boolean) }
+ }),
+)
+
+const currentThemeName = createPoll(
+ "",
+ 2000,
+ shell("cat \"$HOME/.config/omarchy/current/theme.name\" 2>/dev/null"),
+ (stdout) => stdout.trim(),
+)
+
+const isDynamic = currentThemeName.as((name) => name === "blob-dynamic")
+
+const rows = themes.as((list) => {
+ const chunks: Theme[][] = []
+ for (let index = 0; index < list.length; index += COLUMNS) {
+ chunks.push(list.slice(index, index + COLUMNS))
+ }
+ return chunks
+})
+
+const applyTheme = (slug: string) => {
+ app.toggle_window(THEMEPICKER_WINDOW)
+ sh(`"$HOME/scripts/blob_theme.sh" "${slug}"`)
+}
+
+const applyDynamic = () => {
+ app.toggle_window(THEMEPICKER_WINDOW)
+ sh(`"$HOME/scripts/blob_theme.sh" --dynamic`)
+}
+
+function Swatches({ colors }: { colors: string[] }) {
+ return (
+
+ {colors.map((color) => (
+
+ ))}
+
+ )
+}
+
+function ThemeChip({ theme }: { theme: Theme }) {
+ const active = currentThemeName.as((name) => name === theme.slug)
+ return (
+
+ )
+}
+
+function DynamicChip() {
+ return (
+
+ )
+}
+
+export default function ThemePicker() {
+ const hasThemes = themes.as((list) => list.length > 0)
+ const isEmpty = themes.as((list) => list.length === 0)
+
+ return (
+
+
+
+
+
+
+
+
+ row.map((t) => t.slug).join("|")}>
+ {(row: Theme[]) => (
+
+ {row.map((theme) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/commands.md b/commands.md
index aa99955..418232e 100644
--- a/commands.md
+++ b/commands.md
@@ -4,8 +4,8 @@ The installer automatically exposes scripts from [`scripts/`](scripts/) as globa
| Command | Description |
| --- | --- |
-| `blob_wallpaper [path]` | Sets your background using Omarchy's background system. If used with an image from `~/wallpapers/` or a valid path, it leverages Pywal to generate a full system color palette and dynamically updates the `blob-dynamic` theme, AGS widgets, and Waybar. With no argument, it opens the AGS wallpaper picker. |
-| `blob_theme ` | Pulls a color palette shared from the wall-styles site and applies it as the `blob-dynamic` theme (e.g. `blob_theme "https://wall-styles.vercel.app/?id=ab12cd34ef"`). |
+| `blob_wallpaper [path]` | Sets your background using Omarchy's background system. If used with an image from `~/wallpapers/` or a valid path, it leverages Pywal to generate a full system color palette into the `blob-dynamic` theme and always updates the desktop background — but only switches your active color theme to it if you're already in dynamic mode (see `blob_theme --mode`), so it won't pull you out of a static theme. With no argument, it opens the AGS wallpaper picker. |
+| `blob_theme [name\|--dynamic\|--mode\|--print\|share-link-or-id]` | Manages color themes. With no argument, opens the AGS theme picker. `--dynamic` recolors from the current wallpaper without changing it; `--mode` prints `static` or `dynamic`; `--print` dumps the currently active theme's `colors.toml` to stdout, e.g. `blob_theme --print > themes/my-theme/colors.toml` to save a dynamically-generated palette you like. A `name` applies a local theme (see [`themes/`](themes/README.md)) or, if no local theme matches, falls back to pulling a shared palette from the wall-styles site by link or id (e.g. `blob_theme "https://wall-styles.vercel.app/?id=ab12cd34ef"`). |
| `blob_glass [on\|off\|toggle]` | A quick toggle to enable or disable window transparency on the fly. |
| `blob_boot [path]` | Safely updates your Plymouth boot splash image (defaults to `branding/boot_flash.png`) and rebuilds the `initramfs` (GRUB compatible via `mkinitcpio`). |
| `blob_wifi` | A streamlined script to connect to the GMU Eduroam Wi-Fi network using `iwd` and `systemd-resolved` (replaces NetworkManager). |
diff --git a/elephant/menus/blob_theme_selector.lua b/elephant/menus/blob_theme_selector.lua
new file mode 100644
index 0000000..977e138
--- /dev/null
+++ b/elephant/menus/blob_theme_selector.lua
@@ -0,0 +1,76 @@
+Name = "blobThemeSelector"
+NamePretty = "Blob's Theme Selector"
+Cache = false
+HideFromProviderlist = true
+SearchName = true
+
+local function ShellEscape(s)
+ return "'" .. s:gsub("'", "'\\''") .. "'"
+end
+
+function FormatName(slug)
+ local name = slug:gsub("-", " ")
+ name = name:gsub("%S+", function(word)
+ return word:sub(1, 1):upper() .. word:sub(2):lower()
+ end)
+ return name
+end
+
+function GetEntries()
+ local entries = {}
+ local home = os.getenv("HOME")
+ local omarchy_path = os.getenv("OMARCHY_PATH") or ""
+
+ entries[1] = {
+ Text = "Dynamic (from wallpaper)",
+ Value = "dynamic",
+ Actions = {
+ activate = "blob_theme --dynamic",
+ },
+ }
+
+ local dirs = {
+ home .. "/.config/omarchy/themes",
+ omarchy_path .. "/themes",
+ }
+
+ local seen = { ["blob-dynamic"] = true }
+
+ for _, themes_dir in ipairs(dirs) do
+ local handle = io.popen(
+ "find " .. ShellEscape(themes_dir) .. " -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort"
+ )
+ if handle then
+ for theme_dir in handle:lines() do
+ local slug = theme_dir:match("([^/]+)$")
+ if slug and not seen[slug] then
+ local colors_file = io.open(theme_dir .. "/colors.toml", "r")
+ if colors_file then
+ colors_file:close()
+ seen[slug] = true
+
+ local entry = {
+ Text = FormatName(slug),
+ Value = slug,
+ Actions = {
+ activate = "blob_theme " .. ShellEscape(slug),
+ },
+ }
+
+ local preview_file = io.open(theme_dir .. "/preview.png", "r")
+ if preview_file then
+ preview_file:close()
+ entry.Preview = theme_dir .. "/preview.png"
+ entry.PreviewType = "file"
+ end
+
+ table.insert(entries, entry)
+ end
+ end
+ end
+ handle:close()
+ end
+ end
+
+ return entries
+end
diff --git a/install.sh b/install.sh
index b338ee7..a30515a 100755
--- a/install.sh
+++ b/install.sh
@@ -69,7 +69,7 @@ backup_and_copy() {
if [ ! -e "$dest" ]; then
echo "[COPY] $name: New directory (creating)"
- mkdir -p "$(dirname "$dest")"
+ mkdir -p "$dest"
cp -r "$src"/* "$dest"/
return
fi
@@ -87,7 +87,7 @@ backup_and_copy() {
rm -rf "$dest.bak"
mkdir -p "$dest.bak"
cp -r "$dest"/* "$dest.bak/" 2>/dev/null || true
-
+
echo "[COPY] $name: Overwriting (--force)"
cp -r "$src"/* "$dest"/
else
@@ -95,6 +95,40 @@ backup_and_copy() {
fi
}
+# Like backup_and_copy, but for destinations that are pure git-tracked
+# mirrors with no local state worth protecting (wallpapers, themes).
+# No .bak sibling is created — a stale one would otherwise show up
+# alongside the real thing (e.g. in the theme picker's directory scan).
+replace_and_copy() {
+ local src="$1"
+ local dest="$2"
+ local name="$3"
+
+ if [ ! -e "$dest" ]; then
+ echo "[COPY] $name: New directory (creating)"
+ mkdir -p "$dest"
+ cp -r "$src"/* "$dest"/
+ return
+ fi
+
+ local src_hash=$(compute_hash "$src")
+ local dest_hash=$(compute_hash "$dest")
+
+ if [ "$src_hash" = "$dest_hash" ]; then
+ echo "[SKIP] $name: Up to date"
+ return
+ fi
+
+ if [ "$FORCE" = true ]; then
+ echo "[COPY] $name: Replacing (--force)"
+ rm -rf "$dest"
+ mkdir -p "$dest"
+ cp -r "$src"/* "$dest"/
+ else
+ echo "[SKIP] $name: Has local changes (use --force to overwrite)"
+ fi
+}
+
install_dependencies() {
echo "=== Checking Dependencies ==="
local deps_needed=()
@@ -146,10 +180,12 @@ check_file "$SCRIPT_DIR/ags/widget/Media.tsx" "$HOME_DIR/.config/ags/widget/Medi
check_file "$SCRIPT_DIR/ags/widget/Notifications.tsx" "$HOME_DIR/.config/ags/widget/Notifications.tsx" "ags/widget/Notifications.tsx" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/QuickSettings.tsx" "$HOME_DIR/.config/ags/widget/QuickSettings.tsx" "ags/widget/QuickSettings.tsx" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/SysMonitor.tsx" "$HOME_DIR/.config/ags/widget/SysMonitor.tsx" "ags/widget/SysMonitor.tsx" || check_status=1
+check_file "$SCRIPT_DIR/ags/widget/ThemePicker.tsx" "$HOME_DIR/.config/ags/widget/ThemePicker.tsx" "ags/widget/ThemePicker.tsx" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/WallPicker.tsx" "$HOME_DIR/.config/ags/widget/WallPicker.tsx" "ags/widget/WallPicker.tsx" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/WidgetCard.tsx" "$HOME_DIR/.config/ags/widget/WidgetCard.tsx" "ags/widget/WidgetCard.tsx" || check_status=1
check_file "$SCRIPT_DIR/waybar/style.css" "$HOME_DIR/.config/waybar/style.css" "waybar/style.css" || check_status=1
check_file "$SCRIPT_DIR/elephant/menus/blob_background_selector.lua" "$HOME_DIR/.config/elephant/menus/blob_background_selector.lua" "elephant/menus/blob_background_selector.lua" || check_status=1
+check_file "$SCRIPT_DIR/elephant/menus/blob_theme_selector.lua" "$HOME_DIR/.config/elephant/menus/blob_theme_selector.lua" "elephant/menus/blob_theme_selector.lua" || check_status=1
check_file "$SCRIPT_DIR/branding/about.txt" "$HOME_DIR/.config/omarchy/branding/about.txt" "branding/about.txt" || check_status=1
check_file "$SCRIPT_DIR/branding/screensaver.txt" "$HOME_DIR/.config/omarchy/branding/screensaver.txt" "branding/screensaver.txt" || check_status=1
@@ -165,6 +201,14 @@ for script in "$SCRIPT_DIR/scripts"/*.sh; do
fi
done
+if [ -d "$SCRIPT_DIR/themes" ]; then
+ for theme_dir in "$SCRIPT_DIR/themes"/*/; do
+ [ -d "$theme_dir" ] || continue
+ theme_name=$(basename "$theme_dir")
+ check_file "$theme_dir/colors.toml" "$HOME_DIR/.config/omarchy/themes/$theme_name/colors.toml" "themes/$theme_name/colors.toml" || check_status=1
+ done
+fi
+
echo ""
if [ "$CHECK" = true ]; then
@@ -191,7 +235,15 @@ backup_and_copy "$SCRIPT_DIR/hypr" "$HOME_DIR/.config/hypr" "Hyprland config"
backup_and_copy "$SCRIPT_DIR/branding" "$HOME_DIR/.config/omarchy/branding" "Branding files"
backup_and_copy "$SCRIPT_DIR/elephant" "$HOME_DIR/.config/elephant" "Elephant configs"
backup_and_copy "$SCRIPT_DIR/omarchy/hooks" "$HOME_DIR/.config/omarchy/hooks" "Omarchy hooks"
-backup_and_copy "$SCRIPT_DIR/wallpapers" "$HOME_DIR/wallpapers" "Custom wallpapers"
+replace_and_copy "$SCRIPT_DIR/wallpapers" "$HOME_DIR/wallpapers" "Custom wallpapers"
+
+if [ -d "$SCRIPT_DIR/themes" ]; then
+ for theme_dir in "$SCRIPT_DIR/themes"/*/; do
+ [ -d "$theme_dir" ] || continue
+ theme_name=$(basename "$theme_dir")
+ replace_and_copy "$theme_dir" "$HOME_DIR/.config/omarchy/themes/$theme_name" "Theme: $theme_name"
+ done
+fi
zen_profile=$(find "$HOME_DIR/.config/zen" -maxdepth 1 -type d -name "*.Default (release)*" 2>/dev/null | head -n 1)
if [ -n "$zen_profile" ]; then
diff --git a/scripts/blob_theme.sh b/scripts/blob_theme.sh
index e96ac47..50e1fe1 100644
--- a/scripts/blob_theme.sh
+++ b/scripts/blob_theme.sh
@@ -1,52 +1,119 @@
#!/bin/bash
-# Pull a color theme shared from the wall-styles website and apply it.
+# Manage color themes: local static themes installed under
+# ~/.config/omarchy/themes/, the pywal-generated dynamic theme
+# (blob-dynamic), and themes shared from the wall-styles website.
THEME_DIR="$HOME/.config/omarchy/themes/blob-dynamic"
+USER_THEMES_DIR="$HOME/.config/omarchy/themes"
+CURRENT_DIR="$HOME/.config/omarchy/current"
# Used only when a bare id is passed instead of a full share link.
# Override with: export BLOB_THEME_URL="https://your-deployment.vercel.app"
BASE_URL="${BLOB_THEME_URL:-https://wall-styles.vercel.app}"
-if [ -z "$1" ]; then
- echo "Usage: blob_theme "
- echo "Example: blob_theme \"https://wall-styles.vercel.app/?id=ab12cd34ef\""
- exit 1
-fi
+slugify() {
+ echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-'
+}
-INPUT="$1"
+current_mode() {
+ local name
+ name=$(cat "$CURRENT_DIR/theme.name" 2>/dev/null)
+ if [ "$name" = "blob-dynamic" ]; then
+ echo "dynamic"
+ else
+ echo "static"
+ fi
+}
-if [[ "$INPUT" == http*://* ]]; then
- HOST=$(echo "$INPUT" | sed -E 's#^(https?://[^/]+).*#\1#')
- ID=$(echo "$INPUT" | grep -oE 'id=[A-Za-z0-9]+' | head -1 | cut -d= -f2)
- if [ -z "$ID" ]; then
- echo "Error: no theme id found in link '$INPUT'."
+theme_exists_locally() {
+ local slug
+ slug=$(slugify "$1")
+ [ -d "$USER_THEMES_DIR/$slug" ] || [ -d "$OMARCHY_PATH/themes/$slug" ]
+}
+
+apply_static() {
+ local name="$1"
+ # Clean up a lingering gif-animation daemon from a previous dynamic
+ # wallpaper before switching to a static theme's own background.
+ awww kill --all >/dev/null 2>&1
+ omarchy-theme-set "$name"
+}
+
+apply_dynamic() {
+ if [ ! -f "$THEME_DIR/colors.toml" ]; then
+ echo "No dynamic theme yet. Pick a wallpaper first: blob_wallpaper"
exit 1
fi
- API="$HOST/api/theme?id=$ID&format=toml"
-else
- API="$BASE_URL/api/theme?id=$INPUT&format=toml"
-fi
+ # Skip the background step: this is purely a recolor of whatever
+ # wallpaper is already showing, not a wallpaper change.
+ OMARCHY_THEME_SKIP_BACKGROUND=1 omarchy-theme-set "blob-dynamic"
+}
-mkdir -p "$THEME_DIR/backgrounds"
+case "$1" in
+ "")
+ ags toggle theme-picker
+ ;;
+ --menu)
+ omarchy-launch-walker -m menus:blobThemeSelector --width 800 --minheight 400 -p "Select Theme…"
+ ;;
+ --mode)
+ current_mode
+ ;;
+ --print)
+ CURRENT_COLORS="$CURRENT_DIR/theme/colors.toml"
+ if [ ! -f "$CURRENT_COLORS" ]; then
+ echo "No active theme colors.toml found at $CURRENT_COLORS" >&2
+ exit 1
+ fi
+ # awk always terminates the last line with a newline, even if the
+ # source file doesn't. Omarchy's template engine reads colors.toml
+ # with a bash `while read` loop, which silently drops a final line
+ # missing its newline (e.g. color15) — some built-in themes are
+ # missing it too, so this guards every theme saved via --print.
+ awk '1' "$CURRENT_COLORS"
+ ;;
+ --dynamic)
+ apply_dynamic
+ ;;
+ *)
+ if theme_exists_locally "$1"; then
+ apply_static "$1"
+ else
+ # Not a local theme: treat it as a wall-styles share link or id.
+ INPUT="$1"
-echo "Fetching theme..."
-TMP=$(mktemp)
-if ! curl -fsSL "$API" -o "$TMP"; then
- echo "Error: could not fetch theme. The link may be expired (themes last one hour)."
- rm -f "$TMP"
- exit 1
-fi
+ if [[ "$INPUT" == http*://* ]]; then
+ HOST=$(echo "$INPUT" | sed -E 's#^(https?://[^/]+).*#\1#')
+ ID=$(echo "$INPUT" | grep -oE 'id=[A-Za-z0-9]+' | head -1 | cut -d= -f2)
+ if [ -z "$ID" ]; then
+ echo "Error: '$INPUT' is not a local theme, and no share id was found in the link."
+ exit 1
+ fi
+ API="$HOST/api/theme?id=$ID&format=toml"
+ else
+ API="$BASE_URL/api/theme?id=$INPUT&format=toml"
+ fi
-if ! grep -q '^background = ' "$TMP"; then
- echo "Error: the response did not look like a valid colors.toml."
- rm -f "$TMP"
- exit 1
-fi
+ mkdir -p "$THEME_DIR/backgrounds"
-mv "$TMP" "$THEME_DIR/colors.toml"
+ echo "Fetching theme..."
+ TMP=$(mktemp)
+ if ! curl -fsSL "$API" -o "$TMP"; then
+ echo "Error: could not fetch theme, and '$1' is not a local theme name. The link may be expired (themes last one hour)."
+ rm -f "$TMP"
+ exit 1
+ fi
-cat < "$THEME_DIR/neovim.lua"
+ if ! grep -q '^background = ' "$TMP"; then
+ echo "Error: the response did not look like a valid colors.toml."
+ rm -f "$TMP"
+ exit 1
+ fi
+
+ mv "$TMP" "$THEME_DIR/colors.toml"
+
+ cat < "$THEME_DIR/neovim.lua"
return {
{
"LazyVim/LazyVim",
@@ -57,7 +124,10 @@ return {
}
EOF
-# Apply the Blob-Dynamic theme (reloads waybar, ags, etc.)
-omarchy-theme-set "blob-dynamic"
+ # Apply the Blob-Dynamic theme (reloads waybar, ags, etc.)
+ omarchy-theme-set "blob-dynamic"
-echo "Applied shared theme to blob-dynamic."
+ echo "Applied shared theme to blob-dynamic."
+ fi
+ ;;
+esac
diff --git a/scripts/blob_wallpaper.sh b/scripts/blob_wallpaper.sh
index 3898a78..49a4c48 100755
--- a/scripts/blob_wallpaper.sh
+++ b/scripts/blob_wallpaper.sh
@@ -85,10 +85,19 @@ return {
}
EOF
-# Apply the Blob-Dynamic theme, but skip its own background step: it
+# Only switch the active color theme to blob-dynamic if you're already
+# in dynamic mode. The palette above and the background below are kept
+# up to date regardless, so `blob_theme --dynamic` always reflects the
+# latest wallpaper — but picking a wallpaper while on a static theme
+# should change the wallpaper, not silently pull you out of it.
+#
+# When it does apply, skip omarchy-theme-set's own background step: it
# launches swaybg asynchronously, which races with the gif handling
# below and can leave a static swaybg frame on top of an animated gif.
-OMARCHY_THEME_SKIP_BACKGROUND=1 omarchy-theme-set "blob-dynamic"
+CURRENT_THEME_NAME=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null)
+if [ "$CURRENT_THEME_NAME" = "blob-dynamic" ]; then
+ OMARCHY_THEME_SKIP_BACKGROUND=1 omarchy-theme-set "blob-dynamic"
+fi
CURRENT_BACKGROUND_LINK="$HOME/.config/omarchy/current/background"
ln -nsf "$THEME_DIR/backgrounds/$(basename "$IMAGE_PATH")" "$CURRENT_BACKGROUND_LINK"
diff --git a/themes/README.md b/themes/README.md
new file mode 100644
index 0000000..a552423
--- /dev/null
+++ b/themes/README.md
@@ -0,0 +1,60 @@
+# Themes
+
+Drop-in static color themes. Each subdirectory here becomes a theme
+selectable from `blob_theme`, the theme picker widget, and the walker
+menu, once `install.sh` copies it into `~/.config/omarchy/themes/`.
+
+## Adding a theme
+
+Create a directory named after the theme (lowercase, hyphens instead
+of spaces — e.g. `tokyo-night/`) containing a `colors.toml`:
+
+```toml
+accent = "#7aa2f7"
+cursor = "#c0caf5"
+foreground = "#c0caf5"
+background = "#1a1b26"
+selection_foreground = "#1a1b26"
+selection_background = "#7aa2f7"
+
+color0 = "#1a1b26"
+color1 = "#f7768e"
+color2 = "#9ece6a"
+color3 = "#e0af68"
+color4 = "#7aa2f7"
+color5 = "#bb9af7"
+color6 = "#7dcfff"
+color7 = "#c0caf5"
+color8 = "#414868"
+color9 = "#f7768e"
+color10 = "#9ece6a"
+color11 = "#e0af68"
+color12 = "#7aa2f7"
+color13 = "#bb9af7"
+color14 = "#7dcfff"
+color15 = "#c0caf5"
+```
+
+All 22 keys are required — this is the same format Omarchy themes and
+`blob_wallpaper`'s pywal-generated `blob-dynamic` theme already use, so
+`omarchy-theme-set` and its app-config templates pick it up with no
+extra work.
+
+### Saving a dynamic palette you like
+
+If a wallpaper's pywal-generated colors are worth keeping permanently,
+dump the currently active theme's `colors.toml` straight into a new
+theme folder instead of typing it out by hand:
+
+```bash
+mkdir -p themes/my-new-theme
+blob_theme --print > themes/my-new-theme/colors.toml
+```
+
+Run `./install.sh` (add `--force` if the theme already exists on disk
+and you're updating it) to deploy new/changed theme folders.
+
+Any theme already installed under `~/.config/omarchy/themes/` or
+bundled with Omarchy itself (`$OMARCHY_PATH/themes/`) shows up
+automatically too — this directory is just for ones you want tracked
+in dotfiles.
diff --git a/themes/flats/colors.toml b/themes/flats/colors.toml
new file mode 100644
index 0000000..4718f2a
--- /dev/null
+++ b/themes/flats/colors.toml
@@ -0,0 +1,23 @@
+accent = "#7babe3"
+cursor = "#cbd2d9"
+foreground = "#cbd2d9"
+background = "#0f1012"
+selection_foreground = "#0f1012"
+selection_background = "#7babe3"
+
+color0 = "#000000"
+color1 = "#d53838"
+color2 = "#38d4d5"
+color3 = "#d58638"
+color4 = "#d53886"
+color5 = "#38d586"
+color6 = "#3886d5"
+color7 = "#d4d4d4"
+color8 = "#949494"
+color9 = "#d53838"
+color10 = "#38d4d5"
+color11 = "#d58638"
+color12 = "#d53886"
+color13 = "#38d586"
+color14 = "#3886d5"
+color15 = "#d4d4d4"
diff --git a/themes/pitch-dark/colors.toml b/themes/pitch-dark/colors.toml
new file mode 100644
index 0000000..cf6db88
--- /dev/null
+++ b/themes/pitch-dark/colors.toml
@@ -0,0 +1,23 @@
+accent = "#d53838"
+cursor = "#d4d4d4"
+foreground = "#d4d4d4"
+background = "#000000"
+selection_foreground = "#000000"
+selection_background = "#d53838"
+
+color0 = "#000000"
+color1 = "#d53838"
+color2 = "#38d4d5"
+color3 = "#d58638"
+color4 = "#d53886"
+color5 = "#38d586"
+color6 = "#3886d5"
+color7 = "#d4d4d4"
+color8 = "#949494"
+color9 = "#d53838"
+color10 = "#38d4d5"
+color11 = "#d58638"
+color12 = "#d53886"
+color13 = "#38d586"
+color14 = "#3886d5"
+color15 = "#d4d4d4"