refactor: implement component-based architecture for enhanced maintainability

- Reorganize directory structure into core/, features/, and themes/
- Colocate application Nix logic, configs, scripts, and theme overrides
- Implement 'Inversion of Control' for theming: apps now pull theme-specific layouts
- Update flake.nix and shared library paths to match the new structure
- Document the new Feature-Centric architecture in README.md
This commit is contained in:
Bernardo Magri
2026-04-12 14:51:15 +01:00
parent a9ee79a5ce
commit bbdf34ced8
535 changed files with 119 additions and 127 deletions

58
themes/engine/files.nix Normal file
View File

@@ -0,0 +1,58 @@
{ config, lib, ... }:
let
themePath = ../palettes + "/${config.nomarchy.theme}";
themeAppsPath = themePath + "/apps";
nordAppsPath = ../palettes/nord/apps;
# Check if theme has apps/hyprland.conf
hasHyprlandConf = builtins.pathExists (themeAppsPath + "/hyprland.conf");
in
{
xdg.configFile."nomarchy/current/theme" = {
source = themePath;
recursive = true;
};
# Ensure theme-specific hyprland config exists, fallback to nord if not
# Now checking in apps/ subdirectory
xdg.configFile."nomarchy/current/theme/apps/hyprland.conf" = lib.mkIf (!hasHyprlandConf) {
source = nordAppsPath + "/hyprland.conf";
};
# Legacy compatibility: symlink apps/hyprland.conf to root for scripts expecting old path
xdg.configFile."nomarchy/current/theme/hyprland.conf" = {
source =
if hasHyprlandConf
then themeAppsPath + "/hyprland.conf"
else nordAppsPath + "/hyprland.conf";
};
xdg.configFile."nomarchy/current/theme.name".text = config.nomarchy.theme;
# Expose branding assets
xdg.configFile."nomarchy/branding/logo.png".source = ../../core/branding/logo.png;
xdg.configFile."nomarchy/branding/logo.txt".source = ../../core/branding/logo.txt;
xdg.configFile."nomarchy/branding/logo.svg".source = ../../core/branding/logo.svg;
xdg.configFile."nomarchy/branding/icon.png".source = ../../core/branding/icon.png;
xdg.configFile."nomarchy/branding/icon.txt".source = ../../core/branding/icon.txt;
# Expose all themes to the system via local share for script accessibility
# We filter out images to prevent Nix Store bloat
xdg.dataFile."nomarchy/themes".source = builtins.path {
name = "nomarchy-themes-no-images";
path = ../palettes;
filter = path: type:
let
baseName = baseNameOf path;
in
! (type == "regular" && (
lib.hasSuffix ".jpg" baseName ||
lib.hasSuffix ".png" baseName ||
lib.hasSuffix ".jpeg" baseName
));
};
# Nautilus python extensions
xdg.dataFile."nautilus-python/extensions/localsend.py".source = ../../core/home/config/nautilus-python/extensions/localsend.py;
}

128
themes/engine/loader.nix Normal file
View File

@@ -0,0 +1,128 @@
{ config, lib, pkgs, ... }:
# Theme Loader Module
#
# This module handles loading and deploying theme-specific application configs.
# It reads the active theme from state and deploys configs from the theme's apps/
# subdirectory to appropriate locations.
#
# Theme structure expected:
# assets/themes/<theme-name>/
# ├── colors.toml # Color palette (required)
# ├── light.mode # Marker for light themes (optional)
# ├── icons.theme # Icon theme name
# ├── backgrounds/ # Wallpapers
# ├── apps/ # App-specific themed configs
# │ ├── alacritty.toml
# │ ├── kitty.conf
# │ ├── waybar.css
# │ ├── hyprland-colors.conf
# │ ├── mako.conf
# │ ├── swayosd.css
# │ ├── btop.theme
# │ ├── vscode.json
# │ └── neovim.lua
# └── preview.png
let
nomarchyLib = import ../lib { inherit lib; };
assetsPath = ../palettes;
activeTheme = config.nomarchy.theme;
themePath = assetsPath + "/${activeTheme}";
themeAppsPath = themePath + "/apps";
# Check if a theme has an apps directory
themeHasApps = builtins.pathExists themeAppsPath;
# Get the color palette for template processing
palette = nomarchyLib.getPalette activeTheme;
# Helper to check if a file exists in theme
hasThemeFile = name: builtins.pathExists (themePath + "/${name}");
hasThemeAppFile = name: themeHasApps && builtins.pathExists (themeAppsPath + "/${name}");
# All app configs are now in apps/ subdirectory
# Legacy root-level files are no longer supported
in
{
options.nomarchy.themeLoader = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable automatic theme app config loading.";
};
apps = {
btop = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to load btop theme from active theme.";
};
waybar = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to load waybar CSS from active theme.";
};
mako = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to load mako config from active theme.";
};
kitty = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to load kitty config from active theme.";
};
alacritty = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to load alacritty config from active theme.";
};
};
};
config = lib.mkIf config.nomarchy.themeLoader.enable {
# Deploy btop theme if available in apps/
xdg.configFile."btop/themes/nomarchy.theme" = lib.mkIf (config.nomarchy.themeLoader.apps.btop && hasThemeAppFile "btop.theme") {
source = lib.mkDefault (themeAppsPath + "/btop.theme");
};
# Note: Waybar CSS is generated inline in waybar.nix using colorScheme
# This loader would be used if themes provide complete waybar.css overrides
# For now, theme waybar.css files are only used for themes that need
# significantly different styling (like retro-82)
# Create theme info file for scripts
xdg.configFile."nomarchy/theme-loader/current".text = ''
THEME_NAME="${activeTheme}"
THEME_PATH="${toString themePath}"
THEME_HAS_APPS="${if themeHasApps then "true" else "false"}"
IS_LIGHT_MODE="${if config.nomarchy.isLightMode then "true" else "false"}"
ICONS_THEME="${config.nomarchy.iconsTheme}"
'';
# Expose palette as shell-sourceable file for scripts
xdg.configFile."nomarchy/theme-loader/palette.sh".text = ''
# Auto-generated palette for ${activeTheme}
# Source this file in scripts to get theme colors
BASE00="${palette.base00}"
BASE01="${palette.base01}"
BASE02="${palette.base02}"
BASE03="${palette.base03}"
BASE04="${palette.base04}"
BASE05="${palette.base05}"
BASE06="${palette.base06}"
BASE07="${palette.base07}"
BASE08="${palette.base08}"
BASE09="${palette.base09}"
BASE0A="${palette.base0A}"
BASE0B="${palette.base0B}"
BASE0C="${palette.base0C}"
BASE0D="${palette.base0D}"
BASE0E="${palette.base0E}"
BASE0F="${palette.base0F}"
'';
};
}

View File

@@ -0,0 +1,30 @@
{ config, pkgs, lib, ... }:
let
nomarchy-plymouth = pkgs.stdenv.mkDerivation {
pname = "nomarchy-plymouth";
version = "1.0";
src = ./plymouth;
installPhase = ''
mkdir -p $out/share/plymouth/themes/nomarchy
cp * $out/share/plymouth/themes/nomarchy/
# Fix path in the plymouth file to point to the nix store
sed -i "s|/[a-z]*/share/plymouth/themes/nomarchy|$out/share/plymouth/themes/nomarchy|g" $out/share/plymouth/themes/nomarchy/nomarchy.plymouth
'';
};
in
{
boot.initrd.systemd.enable = lib.mkDefault true;
boot.initrd.verbose = lib.mkDefault false;
console.earlySetup = lib.mkDefault true;
boot.consoleLogLevel = lib.mkDefault 0;
boot.plymouth = {
enable = lib.mkDefault true;
themePackages = lib.mkDefault [ nomarchy-plymouth ];
theme = lib.mkDefault "nomarchy";
};
boot.kernelParams = lib.mkDefault [ "quiet" "splash" "loglevel=3" "rd.systemd.show_status=false" "rd.udev.log_level=3" "udev.log_priority=3" "boot.shell_on_fail" ];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,11 @@
[Plymouth Theme]
Name=Nomarchy
Description=Omarchy splash screen.
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/nomarchy
ScriptFile=/usr/share/plymouth/themes/nomarchy/nomarchy.script
ConsoleLogBackgroundColor=0x1a1b26
MonospaceFont=Cantarell 11
Font=Cantarell 11

View File

@@ -0,0 +1,271 @@
# Omarchy Plymouth Theme Script
Window.SetBackgroundTopColor(0.101, 0.105, 0.149);
Window.SetBackgroundBottomColor(0.101, 0.105, 0.149);
logo.image = Image("logo.png");
# Calculate scale factor to make logo ~15% of screen height
logo_scale_factor = (Window.GetHeight() * 0.15) / logo.image.GetHeight();
logo_width = logo.image.GetWidth() * logo_scale_factor;
logo_height = logo.image.GetHeight() * logo_scale_factor;
logo.image = logo.image.Scale(logo_width, logo_height);
logo.sprite = Sprite(logo.image);
logo.sprite.SetX (Window.GetWidth() / 2 - logo.image.GetWidth() / 2);
logo.sprite.SetY (Window.GetHeight() / 2 - logo.image.GetHeight() / 2);
logo.sprite.SetOpacity (1);
# Use these to adjust the progress bar timing
global.fake_progress_limit = 0.7; # Target percentage for fake progress (0.0 to 1.0)
global.fake_progress_duration = 15.0; # Duration in seconds to reach limit
# Progress bar animation variables
global.fake_progress = 0.0;
global.real_progress = 0.0;
global.fake_progress_active = 0; # 0 / 1 boolean
global.animation_frame = 0;
global.fake_progress_start_time = 0; # Track when fake progress started
global.password_shown = 0; # Track if password dialog has been shown
global.max_progress = 0.0; # Track the maximum progress reached to prevent backwards movement
fun refresh_callback ()
{
global.animation_frame++;
# Animate fake progress to limit over time with easing
if (global.fake_progress_active == 1)
{
# Calculate elapsed time since start
elapsed_time = global.animation_frame / 50.0; # Convert frames to seconds (50 FPS)
# Calculate linear progress ratio (0 to 1) based on time
time_ratio = elapsed_time / global.fake_progress_duration;
if (time_ratio > 1.0)
time_ratio = 1.0;
# Apply easing curve: ease-out quadratic
# Formula: 1 - (1 - x)^2
eased_ratio = 1 - ((1 - time_ratio) * (1 - time_ratio));
# Calculate fake progress based on eased ratio
global.fake_progress = eased_ratio * global.fake_progress_limit;
# Update progress bar with fake progress
update_progress_bar(global.fake_progress);
}
}
Plymouth.SetRefreshFunction (refresh_callback);
#----------------------------------------- Helper Functions --------------------------------
fun update_progress_bar(progress)
{
# Only update if progress is moving forward
if (progress > global.max_progress)
{
global.max_progress = progress;
width = Math.Int(progress_bar.original_image.GetWidth() * progress);
if (width < 1) width = 1; # Ensure minimum width of 1 pixel
progress_bar.image = progress_bar.original_image.Scale(width, progress_bar.original_image.GetHeight());
progress_bar.sprite.SetImage(progress_bar.image);
}
}
fun show_progress_bar()
{
progress_box.sprite.SetOpacity(1);
progress_bar.sprite.SetOpacity(1);
}
fun hide_progress_bar()
{
progress_box.sprite.SetOpacity(0);
progress_bar.sprite.SetOpacity(0);
}
fun show_password_dialog()
{
lock.sprite.SetOpacity(1);
entry.sprite.SetOpacity(1);
}
fun hide_password_dialog()
{
lock.sprite.SetOpacity(0);
entry.sprite.SetOpacity(0);
for (index = 0; bullet.sprites[index]; index++)
bullet.sprites[index].SetOpacity(0);
}
fun start_fake_progress()
{
# Don't reset if we already have progress
if (global.max_progress == 0.0)
{
global.fake_progress = 0.0;
global.real_progress = 0.0;
update_progress_bar(0.0);
}
global.fake_progress_active = 1;
global.animation_frame = 0;
}
fun stop_fake_progress()
{
global.fake_progress_active = 0;
}
#----------------------------------------- Dialogue --------------------------------
lock.image = Image("lock.png");
entry.image = Image("entry.png");
bullet.image = Image("bullet.png");
entry.sprite = Sprite(entry.image);
entry.x = Window.GetWidth()/2 - entry.image.GetWidth() / 2;
entry.y = logo.sprite.GetY() + logo.image.GetHeight() + 40;
entry.sprite.SetPosition(entry.x, entry.y, 10001);
entry.sprite.SetOpacity(0);
# Scale lock to be slightly shorter than entry field height
# Original lock is 84x96, entry height determines scale
lock_height = entry.image.GetHeight() * 0.8;
lock_scale = lock_height / 96;
lock_width = 84 * lock_scale;
scaled_lock = lock.image.Scale(lock_width, lock_height);
lock.sprite = Sprite(scaled_lock);
lock.x = entry.x - lock_width - 15;
lock.y = entry.y + entry.image.GetHeight()/2 - lock_height/2;
lock.sprite.SetPosition(lock.x, lock.y, 10001);
lock.sprite.SetOpacity(0);
# Bullet array
bullet.sprites = [];
fun display_normal_callback ()
{
hide_password_dialog();
# Get current mode
mode = Plymouth.GetMode();
# Only show progress bar for boot and resume modes
if (mode == "boot" || mode == "resume")
{
show_progress_bar();
start_fake_progress();
}
}
fun display_password_callback (prompt, bullets)
{
global.password_shown = 1; # Mark that password dialog has been shown
# Reset progress when password dialog appears
stop_fake_progress();
hide_progress_bar();
global.max_progress = 0.0;
global.fake_progress = 0.0;
global.real_progress = 0.0;
show_password_dialog();
# Clear all bullets first
for (index = 0; bullet.sprites[index]; index++)
bullet.sprites[index].SetOpacity(0);
# Create and show bullets for current password (max 21)
max_bullets = 21;
bullets_to_show = bullets;
if (bullets_to_show > max_bullets)
bullets_to_show = max_bullets;
for (index = 0; index < bullets_to_show; index++)
{
if (!bullet.sprites[index])
{
# Scale bullet image to 7x7 pixels
scaled_bullet = bullet.image.Scale(7, 7);
bullet.sprites[index] = Sprite(scaled_bullet);
bullet.x = entry.x + 20 + index * (7 + 5);
bullet.y = entry.y + entry.image.GetHeight() / 2 - 3.5;
bullet.sprites[index].SetPosition(bullet.x, bullet.y, 10002);
}
bullet.sprites[index].SetOpacity(1);
}
}
Plymouth.SetDisplayNormalFunction(display_normal_callback);
Plymouth.SetDisplayPasswordFunction(display_password_callback);
#----------------------------------------- Progress Bar --------------------------------
progress_box.image = Image("progress_box.png");
progress_box.sprite = Sprite(progress_box.image);
progress_box.x = Window.GetWidth() / 2 - progress_box.image.GetWidth() / 2;
progress_box.y = entry.y + entry.image.GetHeight() / 2 - progress_box.image.GetHeight() / 2;
progress_box.sprite.SetPosition(progress_box.x, progress_box.y, 0);
progress_box.sprite.SetOpacity(0);
progress_bar.original_image = Image("progress_bar.png");
progress_bar.sprite = Sprite();
progress_bar.image = progress_bar.original_image.Scale(1, progress_bar.original_image.GetHeight());
progress_bar.x = Window.GetWidth() / 2 - progress_bar.original_image.GetWidth() / 2;
progress_bar.y = progress_box.y + (progress_box.image.GetHeight() - progress_bar.original_image.GetHeight()) / 2;
progress_bar.sprite.SetPosition(progress_bar.x, progress_bar.y, 1);
progress_bar.sprite.SetOpacity(0);
fun progress_callback (duration, progress)
{
global.real_progress = progress;
# If real progress is above limit, stop fake progress and use real progress
if (progress > global.fake_progress_limit)
{
stop_fake_progress();
update_progress_bar(progress);
}
}
Plymouth.SetBootProgressFunction(progress_callback);
#----------------------------------------- Quit --------------------------------
fun quit_callback ()
{
logo.sprite.SetOpacity (1);
}
Plymouth.SetQuitFunction(quit_callback);
#----------------------------------------- Message --------------------------------
message_sprite = Sprite();
message_sprite.SetPosition(10, 10, 10000);
fun display_message_callback (text)
{
my_image = Image.Text(text, 1, 1, 1);
message_sprite.SetImage(my_image);
}
fun hide_message_callback (text)
{
message_sprite.SetOpacity(0);
}
Plymouth.SetDisplayMessageFunction (display_message_callback);
Plymouth.SetHideMessageFunction (hide_message_callback);
# Initialize progress bar immediately for normal boots
if (Plymouth.GetMode() == "boot" || Plymouth.GetMode() == "resume")
{
show_progress_bar();
start_fake_progress();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1,6 @@
#!/bin/bash
# Returns the name of the current monospace font being used by extracting it from the Waybar stylesheet.
# This can be changed using nomarchy-font-set.
grep -oP 'font-family:\s*["'\'']?\K[^;"'\'']+' ~/.config/waybar/style.css | head -n1

View File

@@ -0,0 +1,5 @@
#!/bin/bash
# Returns a list of all the monospace fonts available on the system that can be set using nomarchy-font-set.
fc-list :spacing=100 -f "%{family[0]}\n" | grep -v -i -E 'emoji|signwriting|nomarchy' | sort -u

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Set the system-wide monospace font that should be used by the terminal, hyprlock, waybar, swayosd, etc.
# Declarative version for Nomarchy NixOS.
font_name="$1"
if [[ -z $font_name ]]; then
echo "Usage: nomarchy-font-set <font-name>"
exit 1
fi
STATE_DIR="$HOME/.config/nomarchy"
STATE_FILE="$STATE_DIR/state.json"
mkdir -p "$STATE_DIR"
[[ ! -f $STATE_FILE ]] && echo "{}" > "$STATE_FILE"
if fc-list | grep -iq "$font_name"; then
TMP_JSON=$(mktemp)
jq --arg font "$font_name" '.font = $font' "$STATE_FILE" > "$TMP_JSON" && mv "$TMP_JSON" "$STATE_FILE"
echo "Font set to $font_name declaratively. Applying changes..."
env-update
# Instant feedback for certain apps via IPC
if pgrep -x kitty; then
pkill -USR1 kitty
fi
if pgrep -x ghostty; then
pkill -SIGUSR2 ghostty
notify-send -u low " You must restart Ghostty to see font change"
fi
nomarchy-hook font-set "$font_name"
else
echo "Font '$font_name' not found."
exit 1
fi

View File

@@ -0,0 +1,7 @@
#!/bin/bash
# Display a "Done!" message with a spinner and wait for user to press any key.
# Used by various install scripts to indicate completion.
echo
gum spin --spinner "globe" --title "Done! Press any key to close..." -- bash -c 'read -n 1 -s'

View File

@@ -0,0 +1,9 @@
#!/bin/bash
# Display the Nomarchy logo in the terminal using green color.
# Used by various presentation scripts to show branding.
clear
echo -e "\033[32m"
cat < ~/.config/nomarchy/branding/logo.txt
echo -e "\033[0m"

View File

@@ -0,0 +1,7 @@
#!/bin/bash
CURRENT_THEME_NAME=$(cat "$HOME/.config/nomarchy/current/theme.name")
THEME_USER_BACKGROUNDS="$HOME/.config/nomarchy/backgrounds/$CURRENT_THEME_NAME"
mkdir -p "$THEME_USER_BACKGROUNDS"
nautilus "$THEME_USER_BACKGROUNDS"

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Cycles through the background images available for the current theme.
# Declarative + Hybrid (instant swww) for Nomarchy NixOS.
STATE_DIR="$HOME/.config/nomarchy"
STATE_FILE="$STATE_DIR/state.json"
mkdir -p "$STATE_DIR"
[[ ! -f $STATE_FILE ]] && echo "{}" > "$STATE_FILE"
THEME_NAME=$(jq -r '.theme // "nord"' "$STATE_FILE")
# Resolve themes directory (Built-in from Nix store via Home Manager, or user extra)
if [ -d "$HOME/.config/nomarchy/themes/$THEME_NAME" ]; then
THEMES_DIR="$HOME/.config/nomarchy/themes"
else
THEMES_DIR="$HOME/.local/share/nomarchy/themes"
fi
BG_DIR="$THEMES_DIR/$THEME_NAME/backgrounds"
if [ ! -d "$BG_DIR" ]; then
notify-send "No background directory found for theme $THEME_NAME"
exit 1
fi
mapfile -t BACKGROUNDS < <(ls "$BG_DIR" | sort)
TOTAL=${#BACKGROUNDS[@]}
if (( TOTAL == 0 )); then
notify-send "No backgrounds found in $BG_DIR"
exit 1
fi
CURRENT_BG=$(jq -r '.wallpaper' "$STATE_FILE")
INDEX=-1
for i in "${!BACKGROUNDS[@]}"; do
if [[ "$BG_DIR/${BACKGROUNDS[$i]}" == "$CURRENT_BG" ]]; then
INDEX=$i
break
fi
done
NEXT_INDEX=$(((INDEX + 1) % TOTAL))
NEW_BG="$BG_DIR/${BACKGROUNDS[$NEXT_INDEX]}"
TMP_JSON=$(mktemp)
jq --arg wp "$NEW_BG" '.wallpaper = $wp' "$STATE_FILE" > "$TMP_JSON" && mv "$TMP_JSON" "$STATE_FILE"
# Instant feedback via swww
if pgrep -x swww-daemon >/dev/null; then
swww img "$NEW_BG" --transition-type outer --transition-pos 0.85,0.97 --transition-step 90
else
swww init && swww img "$NEW_BG"
fi
echo "Background set to $NEW_BG declaratively."

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Sets the specified image as the current background
if [[ -z $1 ]]; then
echo "Usage: nomarchy-theme-bg-set <path-to-image>" >&2
exit 1
fi
BACKGROUND="$1"
CURRENT_BACKGROUND_LINK="$HOME/.config/nomarchy/current/background"
# Create symlink to the new background
ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK"
# Kill existing swaybg and start new one
pkill -x swaybg
setsid uwsm-app -- swaybg -i "$CURRENT_BACKGROUND_LINK" -m fill >/dev/null 2>&1 &

View File

@@ -0,0 +1,9 @@
#!/bin/bash
THEME_NAME_PATH="$HOME/.config/nomarchy/current/theme.name"
if [[ -f $THEME_NAME_PATH ]]; then
cat $THEME_NAME_PATH | sed -E 's/(^|-)([a-z])/\1\u\2/g; s/-/ /g'
else
echo "Unknown"
fi

View File

@@ -0,0 +1,33 @@
#!/bin/bash
# nomarchy-theme-install: Install a new theme from a git repo for Nomarchy
# Usage: nomarchy-theme-install <git-repo-url>
if [[ -z $1 ]]; then
echo -e "\e[32mSee https://manuals.omamix.org/2/the-nomarchy-manual/90/extra-themes\n\e[0m"
REPO_URL=$(gum input --placeholder="Git repo URL for theme" --header="")
else
REPO_URL="$1"
fi
if [[ -z $REPO_URL ]]; then
exit 1
fi
THEMES_DIR="$HOME/.config/nomarchy/themes"
THEME_NAME=$(basename "$REPO_URL" .git | sed -E 's/^nomarchy-//; s/-theme$//')
THEME_PATH="$THEMES_DIR/$THEME_NAME"
# Remove existing theme if present
if [[ -d $THEME_PATH ]]; then
rm -rf "$THEME_PATH"
fi
# Clone the repo directly to ~/.config/nomarchy/themes
if ! git clone "$REPO_URL" "$THEME_PATH"; then
echo "Error: Failed to clone theme repo."
exit 1
fi
# Apply the new theme with nomarchy-theme-set
nomarchy-theme-set $THEME_NAME

View File

@@ -0,0 +1,8 @@
#!/bin/bash
{
find ~/.config/nomarchy/themes/ -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -printf '%f\n'
find "$NOMARCHY_PATH/assets/themes/" -mindepth 1 -maxdepth 1 -type d -printf '%f\n'
} | sort -u | while read -r name; do
echo "$name" | sed -E 's/(^|-)([a-z])/\1\u\2/g; s/-/ /g'
done

View File

@@ -0,0 +1,9 @@
#!/bin/bash
# Refresh the current theme from its templates.
THEME_NAME_PATH="$HOME/.config/nomarchy/current/theme.name"
if [[ -f $THEME_NAME_PATH ]]; then
nomarchy-theme-set "$(cat $THEME_NAME_PATH)"
fi

View File

@@ -0,0 +1,35 @@
#!/bin/bash
# nomarchy-theme-remove: Remove a theme from Nomarchy by name
# Usage: nomarchy-theme-remove <theme-name>
if [[ -z $1 ]]; then
mapfile -t extra_themes < <(find ~/.config/nomarchy/themes -mindepth 1 -maxdepth 1 -type d ! -xtype l -printf '%f\n')
if (( ${#extra_themes[@]} > 0 )); then
THEME_NAME=$(printf '%s\n' "${extra_themes[@]}" | sort | gum choose --header="Remove extra theme")
else
echo "No extra themes installed."
exit 1
fi
else
THEME_NAME="$1"
fi
THEMES_DIR="$HOME/.config/nomarchy/themes"
CURRENT_DIR="$HOME/.config/nomarchy/current"
THEME_PATH="$THEMES_DIR/$THEME_NAME"
# Ensure a theme was set
if [[ -z $THEME_NAME ]]; then
exit 1
fi
# Check if theme exists before attempting removal
if [[ ! -d $THEME_PATH ]]; then
echo "Error: Theme '$THEME_NAME' not found."
exit 1
fi
# Now remove the theme directory for THEME_NAME
rm -rf "$THEME_PATH"

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Set the system theme declaratively.
# Usage: nomarchy-theme-set <theme-name>
THEME_NAME="$1"
if [[ -z $THEME_NAME ]]; then
echo "Usage: nomarchy-theme-set <theme-name>"
exit 1
fi
STATE_DIR="$HOME/.config/nomarchy"
STATE_FILE="$STATE_DIR/state.json"
# Resolve themes directory (Built-in from Nix store via Home Manager, or user extra)
if [ -d "$HOME/.config/nomarchy/themes/$THEME_NAME" ]; then
THEMES_DIR="$HOME/.config/nomarchy/themes"
else
THEMES_DIR="$HOME/.local/share/nomarchy/themes"
fi
mkdir -p "$STATE_DIR"
[[ ! -f $STATE_FILE ]] && echo "{}" > "$STATE_FILE"
if [ ! -d "$THEMES_DIR/$THEME_NAME" ] && ! [[ "$THEME_NAME" == "nord" ]]; then
echo "Theme '$THEME_NAME' not found in $THEMES_DIR"
fi
TMP_JSON=$(mktemp)
jq --arg theme "$THEME_NAME" '.theme = $theme' "$STATE_FILE" > "$TMP_JSON" && mv "$TMP_JSON" "$STATE_FILE"
# Sync to system state if we have permissions (for system-level theming like browser policies)
SYSTEM_STATE_FILE="/etc/nixos/state.json"
if [ -w "$SYSTEM_STATE_FILE" ] || [ -w "/etc/nixos" ]; then
sudo jq --arg theme "$THEME_NAME" '.theme = $theme' "$SYSTEM_STATE_FILE" > /tmp/system-state.json 2>/dev/null && sudo mv /tmp/system-state.json "$SYSTEM_STATE_FILE" 2>/dev/null || true
fi
# Try to find a background for this theme
BG_DIR="$THEMES_DIR/$THEME_NAME/backgrounds"
if [ -d "$BG_DIR" ]; then
BG=$(ls "$BG_DIR" | head -n 1)
if [ -n "$BG" ]; then
TMP_JSON=$(mktemp)
jq --arg wp "$BG_DIR/$BG" '.wallpaper = $wp' "$STATE_FILE" > "$TMP_JSON" && mv "$TMP_JSON" "$STATE_FILE"
fi
fi
echo "Theme set to $THEME_NAME. Applying changes with env-update..."
rm -rf "$HOME/.config/nomarchy/current/theme"
env-update
nomarchy-theme-set-templates
nomarchy-hook theme-set "$THEME_NAME"

View File

@@ -0,0 +1,4 @@
#!/bin/bash
nomarchy-theme-set-keyboard-asus-rog
nomarchy-theme-set-keyboard-f16

View File

@@ -0,0 +1,7 @@
#!/bin/bash
ASUSCTL_THEME=~/.config/nomarchy/current/theme/keyboard.rgb
if nomarchy-cmd-present asusctl; then
asusctl aura effect static -c $(sed 's/^#//' $ASUSCTL_THEME)
fi

View File

@@ -0,0 +1,22 @@
#!/bin/bash
FRAMEWORK16_THEME=~/.config/nomarchy/current/theme/keyboard.rgb
if nomarchy-cmd-present qmk_hid && [[ -f $FRAMEWORK16_THEME ]]; then
hex=$(cat "$FRAMEWORK16_THEME")
hex="${hex#\#}"
# Convert hex to QMK HSV (0-255 scale) using Python's colorsys
read -r h s <<< $(python3 -c "
import colorsys
r, g, b = int('$hex'[:2],16)/255, int('$hex'[2:4],16)/255, int('$hex'[4:6],16)/255
h, s, v = colorsys.rgb_to_hsv(r, g, b)
print(int(h * 255), int(s * 255))
")
qmk_hid via --rgb-effect 1 2>/dev/null
qmk_hid via --rgb-hue "$h" 2>/dev/null
qmk_hid via --rgb-saturation "$s" 2>/dev/null
qmk_hid via --rgb-brightness 100 2>/dev/null
qmk_hid via --save 2>/dev/null
fi

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Sync Nomarchy theme to all Obsidian vaults
CURRENT_THEME_DIR="$HOME/.config/nomarchy/current/theme"
[[ -f $CURRENT_THEME_DIR/obsidian.css ]] || exit 0
jq -r '.vaults | values[].path' ~/.config/obsidian/obsidian.json 2>/dev/null | while read -r vault_path; do
[[ -d $vault_path/.obsidian ]] || continue
theme_dir="$vault_path/.obsidian/themes/Nomarchy"
mkdir -p "$theme_dir"
[[ -f $theme_dir/manifest.json ]] || cat >"$theme_dir/manifest.json" <<'EOF'
{
"name": "Nomarchy",
"version": "1.0.0",
"minAppVersion": "0.16.0",
"description": "Automatically syncs with your current Nomarchy system theme colors and fonts",
"author": "Nomarchy",
"authorUrl": "https://nomarchy.org"
}
EOF
cp "$CURRENT_THEME_DIR/obsidian.css" "$theme_dir/theme.css"
done

View File

@@ -0,0 +1,46 @@
#!/bin/bash
TEMPLATES_DIR="$NOMARCHY_PATH/assets/themed"
USER_TEMPLATES_DIR="$HOME/.config/nomarchy/themed"
NEXT_THEME_DIR="$HOME/.config/nomarchy/current/theme"
COLORS_FILE="$NEXT_THEME_DIR/colors.toml"
# Convert hex color to decimal RGB (e.g., "#1e1e2e" -> "30,30,46")
hex_to_rgb() {
local hex="${1#\#}"
printf "%d,%d,%d" "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
}
# Only generate dynamic templates for themes with a colors.toml definition
if [[ -f $COLORS_FILE ]]; then
sed_script=$(mktemp)
while IFS='=' read -r key value; do
key="${key//[\"\' ]/}" # strip quotes and spaces from key
[[ $key && $key != \#* ]] || continue # skip empty lines and comments
value="${value#*[\"\']}"
value="${value%%[\"\']*}" # extract value between quotes (ignores inline comments)
printf 's|{{ %s }}|%s|g\n' "$key" "$value" # {{ key }} -> value
printf 's|{{ %s_strip }}|%s|g\n' "$key" "${value#\#}" # {{ key_strip }} -> value without leading #
if [[ $value =~ ^# ]]; then
rgb=$(hex_to_rgb "$value")
echo "s|{{ ${key}_rgb }}|${rgb}|g"
fi
done <"$COLORS_FILE" >"$sed_script"
shopt -s nullglob
# Process user templates first, then built-in templates (user overrides built-in)
for tpl in "$USER_TEMPLATES_DIR"/*.tpl "$TEMPLATES_DIR"/*.tpl; do
filename=$(basename "$tpl" .tpl)
output_path="$NEXT_THEME_DIR/$filename"
# Don't overwrite configs already exists in the output directory (copied from theme specific folder)
if [[ ! -f $output_path ]]; then
sed -f "$sed_script" "$tpl" >"$output_path"
fi
done
rm "$sed_script"
fi

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Nomarchy VS Code Theme Setter
# This script only updates the global state.json.
# Home Manager (modules/home/vscode.nix) handles the declarative settings injection.
STATE_DIR="$HOME/.config/nomarchy"
STATE_FILE="$STATE_DIR/state.json"
mkdir -p "$STATE_DIR"
[[ ! -f $STATE_FILE ]] && echo "{}" > "$STATE_FILE"
# Theme is already set in state.json by nomarchy-theme-set.
# This script is now mostly a placeholder to maintain the same workflow,
# triggering an env-update if needed to apply the declarative changes.
if [[ $NOMARCHY_TOGGLE_SKIP_VSCODE_THEME != "true" ]]; then
# We trigger env-update to apply the new VSCode theme declaratively.
env-update
fi

View File

@@ -0,0 +1,8 @@
#!/bin/bash
for dir in ~/.config/nomarchy/themes/*/; do
if [[ -d $dir ]] && [[ ! -L ${dir%/} ]] && [[ -d $dir/.git ]]; then
echo "Updating: $(basename "$dir")"
git -C "$dir" pull
fi
done

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Toggles the nightlight (hyprsunset).
# Hybrid: updates state.json and provides instant feedback.
STATE_DIR="$HOME/.config/nomarchy"
STATE_FILE="$STATE_DIR/state.json"
mkdir -p "$STATE_DIR"
# Initialize if doesn't exist
[[ ! -f $STATE_FILE ]] && echo "{}" > "$STATE_FILE"
if [[ $NOMARCHY_TOGGLE_NIGHTLIGHT == "false" ]]; then
NEW_VALUE="true"
hyprctl dispatch exec hyprsunset --temperature 4000
notify-send -u low " Nightlight enabled"
else
NEW_VALUE="false"
pkill hyprsunset
notify-send -u low " Nightlight disabled"
fi
TMP_JSON=$(mktemp)
jq --argjson val "$NEW_VALUE" '.nightlight = $val' "$STATE_FILE" > "$TMP_JSON" && mv "$TMP_JSON" "$STATE_FILE"
echo "Nightlight state set to $NEW_VALUE. Environment will be fully updated on next rebuild."

43
themes/engine/sddm.nix Normal file
View File

@@ -0,0 +1,43 @@
{ config, pkgs, lib, ... }:
let
nomarchy-sddm-theme = pkgs.stdenv.mkDerivation {
pname = "nomarchy-sddm-theme";
version = "1.0";
src = ./sddm/nomarchy;
nativeBuildInputs = [ pkgs.libsForQt5.qt5.wrapQtAppsHook ];
installPhase = ''
mkdir -p $out/share/sddm/themes/nomarchy
cp -r * $out/share/sddm/themes/nomarchy/
'';
propagatedBuildInputs = with pkgs.libsForQt5.qt5; [
qtgraphicaleffects
qtquickcontrols2
qtsvg
];
};
in
{
services.xserver.enable = lib.mkDefault true;
services.displayManager.sddm = {
enable = lib.mkDefault true;
wayland.enable = lib.mkDefault true;
theme = lib.mkDefault "nomarchy";
};
services.displayManager.defaultSession = lib.mkDefault "hyprland-uwsm";
services.displayManager.autoLogin = {
enable = lib.mkDefault true;
user = lib.mkDefault "nomarchy";
};
environment.systemPackages = lib.mkDefault [ nomarchy-sddm-theme ];
# Enable Hyprland system-level dependencies
programs.hyprland = {
enable = lib.mkDefault true;
withUWSM = lib.mkDefault true;
};
programs.uwsm.enable = lib.mkDefault true;
}

View File

@@ -0,0 +1,99 @@
import QtQuick 2.0
import SddmComponents 2.0
Rectangle {
id: root
width: 640
height: 480
color: "#000000"
property string currentUser: userModel.lastUser
property int sessionIndex: {
for (var i = 0; i < sessionModel.rowCount(); i++) {
var name = (sessionModel.data(sessionModel.index(i, 0), Qt.DisplayRole) || "").toString()
if (name.toLowerCase().indexOf("uwsm") !== -1)
return i
}
return Math.max(0, sessionModel.lastIndex)
}
Connections {
target: sddm
function onLoginFailed() {
errorMessage.text = "Login failed"
password.text = ""
password.focus = true
}
function onLoginSucceeded() {
errorMessage.text = ""
}
}
Column {
anchors.centerIn: parent
spacing: root.height * 0.04
width: parent.width
Image {
source: "logo.svg"
width: root.width * 0.35
height: Math.round(width * sourceSize.height / sourceSize.width)
fillMode: Image.PreserveAspectFit
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: root.width * 0.007
Text {
text: "\uf023"
color: "#ffffff"
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: root.height * 0.025
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: root.width * 0.17
height: root.height * 0.04
color: "#000000"
border.color: "#ffffff"
border.width: 1
clip: true
TextInput {
id: password
anchors.fill: parent
anchors.margins: root.height * 0.008
verticalAlignment: TextInput.AlignVCenter
echoMode: TextInput.Password
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: root.height * 0.02
font.letterSpacing: root.height * 0.004
passwordCharacter: "\u2022"
color: "#ffffff"
focus: true
Keys.onPressed: {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
sddm.login(root.currentUser, password.text, root.sessionIndex)
event.accepted = true
}
}
}
}
}
Text {
id: errorMessage
text: ""
color: "#f7768e"
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: root.height * 0.018
anchors.horizontalCenter: parent.horizontalCenter
}
}
Component.onCompleted: password.forceActiveFocus()
}

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="157.40488mm"
height="162.48518mm"
viewBox="0 0 157.40488 162.48518"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
sodipodi:docname="icon.svg"
inkscape:export-filename="logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.82803284"
inkscape:cx="216.175"
inkscape:cy="452.27675"
inkscape:window-width="1914"
inkscape:window-height="1012"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"><inkscape:page
x="0"
y="-1.1741086e-21"
width="157.40488"
height="162.48518"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-28.332214,-25.599269)"><g
id="g4"
transform="translate(3.1953242,-22.686801)"
style="fill:#000088;fill-opacity:1"><path
style="fill:#000088;fill-opacity:1;stroke-width:0.264583"
d="M 25.136891,85.823024 25.557592,210.77125 88.452409,174.38061 67.417351,160.49747 57.110174,166.38729 V 105.80633 Z"
id="path1" /><path
style="fill:#000088;fill-opacity:1;stroke-width:0.264583"
d="M 67.728991,112.41131 182.54178,185.60757 153.16137,202.85259 67.830432,148.17947 Z"
id="path2" /><path
style="fill:#000088;fill-opacity:1;stroke-width:0.264583"
d="M 139.74857,145.88014 140.00405,110.4959 54.800856,56.333749 25.675926,73.32329 Z"
id="path3" /><path
style="fill:#000088;fill-opacity:1;stroke-width:0.264583"
d="M 182.2863,172.70573 V 48.286069 l -62.59305,36.406166 20.82177,13.668277 10.21927,-5.74834 0.12774,60.165978 z"
id="path4" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,7 @@
[SddmGreeterTheme]
Name=Nomarchy
Description=Minimal terminal-style login theme matching the Limine bootloader aesthetic
Author=Nomarchy
Type=sddm-theme
Interface=Qt5
Version=1.0

View File

@@ -0,0 +1 @@
[General]

View File

@@ -0,0 +1,35 @@
{ config, lib, ... }:
# Compatibility shims for stylix target modules
# Stylix unconditionally imports all target modules, which expect certain
# program options to exist. This module defines stub options for programs
# we don't use to prevent evaluation errors.
{
options = {
# Neovim: stylix uses initLua but home-manager uses extraLuaConfig
programs.neovim.initLua = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Lua code to run at init (compatibility shim for stylix)";
};
# OpenCode: stylix expects programs.opencode options
programs.opencode = {
tui = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = {};
description = "OpenCode TUI settings (stylix compatibility shim)";
};
themes = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = {};
description = "OpenCode themes (stylix compatibility shim)";
};
};
};
config = lib.mkIf (config.programs.neovim.initLua != "") {
programs.neovim.extraLuaConfig = config.programs.neovim.initLua;
};
}

101
themes/engine/stylix.nix Normal file
View File

@@ -0,0 +1,101 @@
{ config, pkgs, inputs, lib, ... }:
# Stylix Integration Module
#
# This module handles base-level theming through Stylix:
# - Color scheme injection from the active theme's palette
# - Cursor configuration
# - Font configuration
# - GTK/GNOME theming
#
# App-specific theming is handled separately:
# - theme-loader.nix: Deploys theme's apps/ configs (btop, neovim, etc.)
# - waybar.nix: Generates waybar CSS from colorScheme
# - hyprland.nix: Handles hyprland border colors
#
# Stylix targets disabled here (we have custom implementations):
# - hyprland: Custom border/rule config
# - waybar: Custom CSS with theme colors
let
nomarchyLib = import ../lib { inherit lib; };
assetsPath = ../palettes;
activeThemeName = config.nomarchy.theme;
# Use shared wallpaper resolver
activeWallpaper = nomarchyLib.resolveWallpaper {
wallpaperPath = config.nomarchy.wallpaper;
themeName = activeThemeName;
inherit assetsPath;
};
# Get palette using shared library
currentPalette = nomarchyLib.getPalette activeThemeName;
in
{
imports = [ inputs.stylix.homeModules.stylix ];
stylix = {
enable = lib.mkDefault true;
enableReleaseChecks = lib.mkDefault false;
autoEnable = lib.mkDefault false; # Disable auto-detection, explicitly enable targets
image = lib.mkDefault activeWallpaper;
base16Scheme = lib.mkDefault currentPalette;
# Use detected light mode state
polarity = lib.mkDefault (if config.nomarchy.isLightMode then "light" else "dark");
cursor = lib.mkDefault {
package = config.nomarchy.cursor.package;
name = config.nomarchy.cursor.name;
size = 24;
};
fonts = lib.mkDefault {
monospace = {
package = pkgs.nerd-fonts.jetbrains-mono;
name = config.nomarchy.fonts.monospace;
};
sansSerif = {
package = pkgs.dejavu_fonts;
name = "DejaVu Sans";
};
serif = {
package = pkgs.dejavu_fonts;
name = "DejaVu Serif";
};
emoji = {
package = pkgs.noto-fonts-color-emoji;
name = "Noto Color Emoji";
};
sizes = {
applications = 11;
terminal = 11;
desktop = 11;
popups = 11;
};
};
# Enable theming for specific targets
targets = lib.mkDefault {
hyprland.enable = false; # We keep our custom hyprland config for borders/rules
waybar.enable = false; # We keep our custom waybar CSS
neovim.enable = false; # We deploy theme lua files via theme-loader instead
neovide.enable = false; # Neovide depends on neovim program module
alacritty.enable = true;
kitty.enable = true;
gtk.enable = true;
gnome.enable = true;
};
};
# GTK Icon Theme configuration
gtk = {
enable = lib.mkDefault true;
iconTheme = lib.mkDefault {
package = pkgs.yaru-theme;
name = config.nomarchy.iconsTheme;
};
};
}

View File

@@ -0,0 +1,64 @@
{ config, pkgs, lib, ... }:
let
nomarchyLib = import ../lib { inherit lib; };
themeList = builtins.concatStringsSep "\\n" nomarchyLib.themeNames;
nomarchy-theme-selector = pkgs.writeShellScriptBin "nomarchy-theme-selector" ''
SELECTED_THEME=$(echo -e "${themeList}" | walker --dmenu)
if [ -n "$SELECTED_THEME" ]; then
nomarchy-theme-set "$SELECTED_THEME"
fi
'';
nomarchy-font-selector = pkgs.writeShellScriptBin "nomarchy-font-selector" ''
STATE_DIR="${config.home.homeDirectory}/.config/nomarchy"
STATE_FILE="$STATE_DIR/state.json"
mkdir -p "$STATE_DIR"
[[ ! -f $STATE_FILE ]] && echo "{}" > "$STATE_FILE"
# Simple list of common nerd fonts, could be expanded
FONTS="JetBrainsMono Nerd Font\nRobotoMono Nerd Font\nFiraCode Nerd Font\nUbuntuMono Nerd Font"
SELECTED_FONT=$(echo -e "$FONTS" | walker --dmenu)
if [ -n "$SELECTED_FONT" ]; then
TMP_JSON=$(mktemp)
jq --arg font "$SELECTED_FONT" '.font = $font' "$STATE_FILE" > "$TMP_JSON" && mv "$TMP_JSON" "$STATE_FILE"
env-update
fi
'';
nomarchy-wallpaper-selector = pkgs.writeShellScriptBin "nomarchy-wallpaper-selector" ''
STATE_DIR="${config.home.homeDirectory}/.config/nomarchy"
STATE_FILE="$STATE_DIR/state.json"
THEMES_DIR="${config.xdg.dataHome}/nomarchy/themes"
mkdir -p "$STATE_DIR"
[[ ! -f $STATE_FILE ]] && echo "{}" > "$STATE_FILE"
# List all images in all themes backgrounds
# We search in the system-wide flake source for distro wallpapers to avoid Nix Store bloat
WALLPAPERS=$(find "${config.xdg.dataHome}/nomarchy/themes" -type f \( -name "*.jpg" -o -name "*.png" \) 2>/dev/null)
DISTRO_THEMES="/etc/nixos/nomarchy/assets/themes"
if [ -d "$DISTRO_THEMES" ]; then
WALLPAPERS="$WALLPAPERS\n$(find "$DISTRO_THEMES" -type f \( -name "*.jpg" -o -name "*.png" \))"
fi
# Include user themes if they exist
if [ -d "${config.home.homeDirectory}/.config/nomarchy/themes" ]; then
WALLPAPERS="$WALLPAPERS\n$(find "${config.home.homeDirectory}/.config/nomarchy/themes" -type f \( -name "*.jpg" -o -name "*.png" \))"
fi
SELECTED_WP=$(echo -e "$WALLPAPERS" | walker --dmenu)
if [ -n "$SELECTED_WP" ]; then
TMP_JSON=$(mktemp)
jq --arg wp "$SELECTED_WP" '.wallpaper = $wp' "$STATE_FILE" > "$TMP_JSON" && mv "$TMP_JSON" "$STATE_FILE"
swww img "$SELECTED_WP" --transition-type outer --transition-pos 0.85,0.97 --transition-step 90 &
env-update
fi
'';
in
{
home.packages = [ nomarchy-theme-selector nomarchy-font-selector nomarchy-wallpaper-selector ];
}