Skip to content

About

Arch Linux · Hyprland · Matugen dynamic theming dotfiles

Resources

Stars

157 stars

Watchers

1 watching

Forks

Repository files navigation

dotfiles

Arch Linux, Hyprland, Quickshell, Matugen. Wallpaper sets the palette, everything else follows.

Desktop

Bar island, Zathura and kitty over a wallpaper-pulled palette. Every surface you see here (bar, OSD, toasts, login, terminals, editors) is recolored from that one image.

Fresh machine

yay -S hyprland quickshell kitty matugen starship zsh lsd zoxide fzf \
       cava btop rofi swayosd hyprlock hypridle hyprsunset sddm \
       xdg-desktop-portal-hyprland uwsm cliphist wl-clipboard \
       ttf-jetbrainsmono-nerd ttf-firacode-nerd inter-font
git clone https://github.com/codetesla51/uthman_dotfiles.git ~/dotfiles
cd ~/dotfiles && chmod +x install.sh && ./install.sh
set-wallpaper ~/Pictures/your-wallpaper.jpg

Reboot once after the first install (SDDM theme lands in /usr/share/sddm/themes/, user services start on fresh login). Lost after that: SUPER+K is the keybind cheatsheet.

How the whole thing fits together

Four pieces, and each one only knows about the one below it.

Hyprland         window manager, keybinds, layer rules, compositor
  └── quickshell bar, panels, OSD, notification daemon  (all QML)
        └── Matugen   one image -> every app's colors
              └── wallpaper   the only input

Hyprland owns windows and keys. It never draws the bar itself. Quickshell owns everything you see drawn by this repo, in one QML process. Matugen owns color, and knows nothing about QML. The wallpaper is the single input that flows upward through the other three.

A wallpaper change is the clearest way to see the whole chain move:

set-wallpaper ~/Pictures/new.jpg

That one command does five things in order:

  1. Symlinks the image to ~/.config/theme/current/background, which is what everything else treats as "current wallpaper".
  2. Hands it to awww (or swww) for a 0.7s wave transition, so the change is visible before any color moves.
  3. Calls getTheme, which runs matugen image to build a Material You palette.
  4. Regenerates the cursor theme through matugen-cursor, round-tripping it so Hyprland drops its cache and no logout is needed.
  5. Writes the basename to ~/.local/state/omarchy/current/theme.name as a nudge for the file manager to hot-reload.

Matugen then writes 37 output files from 19 templates, one file per app per destination. Nothing is copied or converted afterward. The shell notices on its own within a second.

How one image becomes 37 files

Each template is a file with {{ colors... }} placeholders. Matugen renders it and writes the result. The config is a flat list of template-to-output pairs, and several templates are rendered more than once because they need to land in two places:

[templates.hyprland-standalone]
input_path  = "~/.config/matugen/templates/hyprland-colors.conf"
output_path = "~/.config/theme/current/colors.conf"

[templates.hyprland-current]
input_path  = "~/.config/matugen/templates/hyprland-colors.conf"
output_path = "~/.config/omarchy/current/theme/colors.conf"

Same input, different output. That is why hyprland-theme, hyprland-current, and hyprland-standalone all read the same hyprland-colors.conf. Templates are cheap; outputs are where the duplication lives.

Where the 37 files land

Destination Count Who reads it
~/.config/theme/current/ 12 live config, rewritten every wallpaper change
~/.config/theme/themes/snow_black/ 10 named snapshots, accumulates history
~/.config/omarchy/current/theme/ 8 legacy path kept for compatibility
/usr/share/sddm/themes/elarun-custom/ 1 login screen, needs root path
~/uthman_notes/.obsidian/snippets/ 1 your notes vault
~/.config/quickshell/colors.css 1 the shell, polled every second
~/.config/{cava,cavasik,rofi,zathura}/ 4 audio visualizer, PDF reader, launcher

theme/current/ is what everything actually reads at runtime. snow_black/ is a saved snapshot that grows over time and is safe to delete. The omarchy/current/ copies exist because this setup started from omarchy defaults and some vendor configs still read that path.

Two outputs are worth calling out because they are the only ones outside ~/.config:

  • SDDM writes to /usr/share/sddm/themes/, a system path. That is why install.sh needs to run once and why the first install asks for a reboot.
  • Obsidian writes into your notes vault, which lives outside this repo. The vault is not part of the dotfiles and is never committed.

Why the shell picks the trickiest path

The shell's palette comes from waybar-colors.css, named for Waybar even though no Waybar runs here. It is a plain list of 29 Material color roles:

@define-color primary   #adc6ff;
@define-color surface   #1c110f;
@define-color tertiary  #ffb780;

The template is still called waybar because it predates the rewrite and config.toml maps it, but the file it produces is quickshell's colors.css. Matugen also has a reload_apps.waybar = true setting, which sends a signal on write. Both are vestigial names. The shell does not use the signal; it polls the file instead, because Matugen replaces the file rather than editing it. A replaced file gets a new inode, which silently breaks FileView watchers, so Colors.qml re-reads on a 1s timer instead.

Tip

set-wallpaper backgrounds getTheme and waits on it, so the transition starts immediately while colors are still being computed. Total time from press to fully restyled desktop is about 3 seconds, of which matugen is 2.1 and the cursor is 0.9. They run in parallel, so adding more templates costs nothing to perceived speed.

Where each concern lives

Concern Owner Notes
Windows, keys, blur, gaps ~/.config/hypr/*.conf vendor/ holds upstream defaults, personal files override
Everything drawn ~/.config/quickshell/ 55 modules, one palette object shared by all of them
Colors for everything ~/.config/matugen/ config.toml maps templates to outputs
Wallpaper ~/Pictures/ git-ignored, never committed
Terminal, prompt, history .zshrc + starship/atuin unrelated to the shell process

The separation is deliberate: if a theme breaks, only Matugen's templates are involved. If a panel misbehaves, only QML is involved. There is no config file that mixes the two.

How theming works

Matugen reads the wallpaper, builds a Material You palette, and fills variables in each template. One command recolors everything with no restarts:

matugen image ~/Pictures/your-wallpaper.jpg

Normally you call set-wallpaper instead, since that also swaps the image itself and regenerates the cursor.

~/.config/matugen/config.toml maps each template to its outputs. The main ones:

Template Output
waybar ~/.config/quickshell/colors.css (the file the live shell polls)
hyprland-* snow_black/colors.conf + theme/current
hyprlock-*, kitty-*, gtk, btop matching live files under theme/current
cava snow_black/cava_theme + ~/.config/cava/config (rewritten directly)
sddm /usr/share/sddm/themes/elarun-custom/Palette.qml (login follows every theme change)
firefox theme/current/firefox{,-usercontent}.css, symlinked as userChrome.css/userContent.css
rofi, neovim, swayosd, zed, obsidian, zathura per-app live outputs

Active colors live in ~/.config/theme/current/ and are rewritten on every run. snow_black/ keeps saved snapshots, theme-fallback/ ships the static seed.

Tip

Colors.qml polls ~/.config/quickshell/colors.css every second. Matugen replaces the file (new inode) instead of editing it, so the poller re-reads rather than watching. Palette swaps propagate with no restart.

Warning

Always run full matugen image with the same wallpaper source when you mean "refresh". Running it against a different image silently re-palettes all outputs at once. After any regen, git diff --stat in ~/dotfiles should show only what you expect.

Quickshell desktop shell

The shell replaces Waybar and SwayNC with one QML codebase. Entry is shell.qml, which instantiates the bar plus standalone windows sharing one Colors palette object. The bar window sits at y=0 h=54. The center tab is a trapezoid in classic mode, and SUPER ALT SPACE flips it into a floating black capsule. Clock, NowPlaying (album art, title/artist, live visualizer, hover transport), and the bell live flat inside it. System pills (CPU, memory, temp, network, battery, WatchCat) ride left and right as glass pills.

Heavy panels load on first use, then stay compiled in memory. shell.qml holds the IPC target and drives item.open in both directions, so a reopened panel is a flag flip instead of a fresh QML compile. Presses that arrive mid-compile are ignored rather than cancelling the open.

Hyprland binds and scripts talk to the shell over IPC, always with the identical path string:

quickshell -p ~/.config/quickshell                 # run. Never -p . or a realpath.
quickshell -p ~/.config/quickshell ipc show        # list every target
quickshell -p ~/.config/quickshell ipc call wifi toggle
quickshell -p ~/.config/quickshell ipc call notifications toggleDnd

Warning

The quickshell daemon owns org.freedesktop.Notifications. Keep swaync masked or it steals the bus back and toasts stop appearing.

Note

Mismatched -p strings fail with Target not found even while a shell is clearly running. Autostart, binds, and manual calls must all use ~/.config/quickshell verbatim.

Bar pills

Thin widgets. They display state and delegate detail to panels.

Module Role
ArchLogo Distro pill, brand accent
Workspaces Live Hyprland workspaces, segmented control
Clock Center time, click opens the clock card, right-click toggles long format
Cpu, Memory, Temp Usage pills, click opens the system monitor
Network Wi-Fi/Ethernet pill, click opens the wifi panel
Battery Charge pill with warning/critical states, click opens the battery panel
WatchCat Hotspot down-rate pill, click opens the watchdog panel
Tray System tray container
StatusPill One pill consolidating idle, DND, recording, and tray transient states
BellButton Notification bell with unread badge
NowPlaying Track art, title/artist, live cava spectrum, hover transport

Panels and windows

Overlays are PanelWindow layer-shell popups. Larger tools are regular Hyprland FloatingWindows with matching float/center/size rules. Every panel closes on Esc.

Module Role How it opens
WifiPanel Network manager, known networks, live throughput graph, Cloudflare speedtest SUPER+H, network pill
SystemMonitor Per-core CPU, RAM, network, process list with kill SUPER+U, cpu/memory pills
BatteryPanel Charge curve, power profile, time estimates SUPER ALT+B, battery pill
NotificationCenter Notification drawer, history, DND toggle SUPER+,
ControlCenter Bento grid: Now Playing, weather, quick toggles, network, activity, live cava visualizer SUPER ALT+P
AppLauncher Spotlight-style launcher, wheel scroll SUPER+Space
PkgManager pacman + AUR search, queue, install progress, updates SUPER+I
PdfViewer PDF/EPUB library: recents, favorites, full-text search, Zathura handoff SUPER ALT+N, utpdf <file>
Wallshelf Wallhaven storefront: browse, bulk download, set-as-wallpaper SUPER CTRL+Space
PhoneBridge ADB phone bridge: drag-send files, clipboard push, remote browser + pull, ring, screenshot SUPER ALT+K
ThemePanel Wallpaper slideshow with preview, applies via set-wallpaper SUPER+E
PowerMenu Lock, logout, suspend, reboot, shutdown SUPER+Escape
ClipboardPanel Clipboard manager (Hyprland window): history with image previews (cliphist backend) plus a Sticky shelf of pinned snippets in LocalStorage; click a pinned note to copy it SUPER CTRL+V (window), SUPER CTRL+P (pin current clipboard from anywhere)
KeybindsPanel Searchable, executable keybind cheatsheet SUPER+K
FastFetchWindow System info card SUPER+N
ScreenTime App-usage heatmap and top apps, Go daemon backend SUPER ALT+T, launcher
DriveHealth SMART health, disk space, throughput, speed test SUPER ALT+D, launcher
WorkspaceViewer Fullscreen overview of every workspace with live windows SUPER ALT+O
MonitorSettings Scale/orientation picker, whole-pixel warnings, persistent apply launcher
Earbuds Headset battery rings from bluetoothctl and UPower SUPER ALT+E
Bluetooth Adapter power, scan, pair/connect/disconnect launcher
Pomodoro Focus timer: phases, durations, session stats SUPER ALT+C, launcher
GitHubDash Heatmap, inbox, PRs, running actions launcher
Dictionary Definitions, IPA, audio, Datamuse related words launcher
Grap Instant ripgrep search across the home dir launcher
FailWatch Failed units, OOM/segfault alerts, pacman -Qkk launcher
ClockWindow Detached clock card bar clock click
MediaOsd Volume/brightness/mic overlay Fn keys
PassPrompt System password prompt (SUDO_ASKPASS backend) privileged panel actions

Unwired but still on disk: QuickNotes (scratch idea capture; nothing instantiates it, so its notes target is never served and the panel never opens) and WhatsApp (PARKED, not retired: kept with session intact but commented out of Bar.qml and unused over ban caution; WhatsApp pings come from PhoneBridge shade polling meanwhile). NetRate and WeatherIcon are shared helpers, not surfaces. If you rewire an unwired file, update PLUGINS.md and the cheatsheet entry with it.

PhoneBridge.md next to the module documents the ADB backend contract. PLUGINS.md is the registry of every module, its IPC target, and its bind. Keep both current when you change a module.

Notifications

The shell is its own notification daemon. Toasts carry action buttons, screenshot notifications carry a Save button that files the image into ~/Pictures/Screenshots, and everything lands in the history drawer. When an app ships no icon, the row falls back to a per-app Phosphor glyph (40+ apps mapped, from Firefox and WhatsApp down to brightnessctl and grim), so rows never show bare initial letters. notify-send -a <AppName> is all a sender needs for a correct icon.

Screen time

The screentime Go daemon (separate checkout at ~/projects/screentime, also runs as a user service) samples the focused window every 10 seconds into SQLite. ScreenTime.qml renders the heatmap and top apps from screentime --export all, and the control center activity bars read the same export. One source, both views agree.

Companion repos

These live outside this repo but plug into the shell:

Repo What it is
~/projects/screentime Screen time Go daemon + SQLite store
~/phonebridge ADB backend for the PhoneBridge module
~/whatsapp-bridge WhatsApp daemon (parked)
github.com/codetesla51/qemu-launch Bubble Tea QEMU launcher: ISOs from ~/Downloads, per-VM store in ~/VMs

WatchCat

Hotspot data watchdog for tethered laptops. Tracks every byte against a daily 1 GB cap. Notify-only: it never pauses or blocks traffic.

# daemon: nethogs per-process rates into sqlite plus live.json, 24/7
systemctl --user enable --now watchcat.service

# dashboard: static HTML regenerated from the database (auto-reloads every 30s)
python3 ~/.config/quickshell/scripts/watchcat-dash.py <out.html> 1024

The daemon samples in 12s chunks, flags "hot" processes (over 500 KB/s for 10s+), and warns at 500 MB and 1 GB via notify-send. History prunes to 7 days. Sudo is scoped to exactly one command in /etc/sudoers.d/watchcat (passwordless nethogs, nothing else).

Keybinds

SUPER+K opens the searchable cheatsheet. The same list is executable from there. Essentials:

Keys Action
SUPER+Space / SUPER+K Launcher / keybind cheatsheet
SUPER+Return, SUPER SHIFT+F Terminal, file manager
SUPER+H / SUPER+U / SUPER ALT+B Wi-Fi panel / system monitor / battery panel
SUPER+E / SUPER+I / SUPER ALT+N Theme / packages / PDF library
SUPER CTRL+Space Wallpaper store
SUPER ALT+P / T / K / D / Y Control center / screen time / phone / drives / WatchCat
SUPER ALT+C / E Pomodoro / earbuds
SUPER ALT+O / SUPER+, Workspace overview / notification center
SUPER+, / SUPER SHIFT+, Notifications / do-not-disturb
SUPER CTRL+V / SUPER CTRL+P / SUPER+N Clipboard window / pin clipboard to sticky shelf / system info
SUPER ALT+Space / SUPER SHIFT+Space Island style / bar sides
SUPER+Escape / SUPER SHIFT+Q / SUPER SHIFT+L Power menu / shutdown / log out (reboot lives in the power menu)
SUPER+arrows, SUPER+1-0 Focus, workspaces
Fn row Volume/brightness/mic via the quickshell OSD

The full list lives in .config/hypr/bindings.lua (vendored defaults alongside in .config/hypr/vendor/). Every panel also closes on Esc.

Helper scripts

~/.local/bin/ (stowed from this repo) holds the CLI surface: set-wallpaper, getTheme, shot, record, utpdf, vol, bright, nightlight, lock, phone-pair, qemu-launch, and more. These are what binds, panels, and muscle memory call.

Tools

What runs the desktop day to day, and where each piece lives:

Tool Job Wiring
awww Wallpaper daemon + transitions autostart.conf launches the daemon; set-wallpaper switches with a 0.7s wave (instant none at login)
matugen + matugen-cursor Theme colors + matching cursor getTheme regenerates on wallpaper switch; cursor round-trips through Bibata so Hyprland drops its cache — no logout needed
atuin Shell history (SQLite, per-directory smart) eval "$(atuin init zsh)" last in .zshrc, owns Ctrl+R; fully offline, atuin register for sync
shell-splash 技 terminal greeting .zshrc runs it for the outermost shell only; braille art dissolves in with a rice gradient re-read from colors.conf every launch
fastfetch System info with the 技 logo config.jsonc points at kanji.txt (gradient baked from the current rice)
chafa + PIL Art pipeline behind the splash/logo 技 rendered from Noto CJK → alpha-mask → braille; Hollow Knight mask baked solid-white for the launcher rail
hyprlock Screen lock hypridle + SUPER+L; fade entrance, pill input, mask art configurable in hyprlock.conf
quickshell Bar, island, all panels shell.qml root; bar popups lazy-load (battery stays resident); registry in PLUGINS.md, rules in AGENTS.md

Structure

~/dotfiles/
├── .config/
│   ├── hypr/                 # Hyprland: vendor defaults + personal *.conf overrides
│   │   ├── bindings.conf     # every keybind (cheatsheet reads this)
│   │   ├── windowrules.conf  # float/center/size for each quickshell window
│   │   ├── looknfeel.conf    # gaps, blur, shadows, animations, layerrules
│   │   ├── monitors.conf / input.conf / autostart.conf / envs.conf
│   │   └── vendor/           # upstream omarchy defaults, do not hand-edit
│   ├── quickshell/           # desktop shell (see module tables above)
│   │   ├── shell.qml         # root: bar + standalone windows + shared palette
│   │   ├── components/       # Bar.qml, Colors.qml
│   │   ├── modules/          # 55 feature files
│   │   ├── scripts/          # temperature.sh, watchcat daemon + dashboard, idle helpers
│   │   ├── colors.css        # generated by matugen, polled live, do not hand-edit
│   │   ├── PLUGINS.md        # module registry: IPC target + bind per module
│   │   └── AGENTS.md         # contributor rules for this codebase (read before editing)
│   ├── matugen/              # config.toml + templates (btop, gtk, rofi, nvim, zed, sddm, zathura, …)
│   ├── kitty/kitty.conf      # sources theme/current/kitty.conf
│   ├── systemd/user/         # helper services
├── .local/bin/             # set-wallpaper, getTheme, record, shot, …
├── sddm/elarun-custom/     # login theme source → /usr/share/sddm/themes/
├── firefox/user.js         # enables userChrome.css theming
├── wallpapers/             # local only, git-ignored, never committed
├── assets/                 # vendored blobs + README screenshots (stow-ignored)
├── theme-fallback/         # static snow_black seed
├── .zshrc                  # zsh + lsd/zoxide/fzf/starship/mise
├── .stow-local-ignore
└── install.sh

AGENTS.md inside .config/quickshell/ is the contributor contract for the shell: canonical IPC path, panel vs pill vs card patterns, the Amber Bento style tokens, and the verification loop. Read it before touching any .qml file.

Customizing

  • Keybinds: .config/hypr/bindings.conf (panel Binds page edits this file, Hyprland reloads it)
  • Look and feel: .config/hypr/looknfeel.conf plus ~/.config/theme/current/colors.conf
  • Bar and modules: .config/quickshell/modules/*.qml, then restart quickshell (never trust hot reload for verdicts)
  • New theme template: add it under .config/matugen/templates/, map it in config.toml, run matugen image once
  • Terminal: .config/kitty/kitty.conf for font and behavior. Colors always come from the generated files

Gotchas

  • Quickshell owns the notification bus. If toasts stop appearing, something (usually swaync) grabbed org.freedesktop.Notifications first. Mask it.
  • Generated files are not source. colors.css, zathurarc, cava/config, and everything under theme/current/ get overwritten by matugen. Edit the template, never the output.
  • Nerd Font codepoints drift between font builds. A glyph that exists can still render as the wrong symbol. Verify by glyph name with fontTools, not by codepoint presence. Same goes for Phosphor icons: this repo's build is ligature-mapped, so codepoints were resolved through its GSUB table.
  • wallshelf.ini holds the Wallhaven API key and is gitignored on purpose. Never move the key into wallshelf.json (tracked).
  • The tree is usually dirty. Theme outputs regenerate on every wallpaper change, so git status almost always shows modifications. That is normal. Commit only your own files.
  • No boot-menu snapshot entries. Snapper timelines run hourly, but recovery is manual (see below), not a Limine menu.

Disaster recovery (snapper)

Root is btrfs (@ + @home + @log + @pkg, snapshots in .snapshots, hourly timelines enabled).

Most breakages (bad update, nuked /etc file, dead Hypr config) still reach a TTY. Log in as root there and roll back. No USB needed:

snapper list                          # pick a number from before the breakage
snapper rollback 80
reboot

A USB stick with an Arch ISO is only needed when nothing boots at all: kernel panic, no login prompt. On this setup that means a bad kernel + initramfs after an interrupted update, a broken mkinitcpio.conf (check HOOKS before every mkinitcpio -P), a full / partition (snapshots live on it too), a bad /etc/fstab line, or a damaged ESP/limine.conf in /boot. Then:

# from the ISO:
mount -o subvol=@ /dev/sda2 /mnt
arch-chroot /mnt
snapper list
snapper rollback 80
reboot

@home/@log/@pkg are separate subvolumes, so rollback never touches your files, logs, or package cache. Snapper keeps the broken @ as a backup, so a bad rollback is itself reversible.

Day-to-day (no reboot involved):

snapper create -d "before i break things"
snapper undochange 82..0 /etc          # un-break one directory
snapper status 82..0                  # preview what changed first

Locked out (passwords, login, screen lock)

Three ladders, in order. This disk is not encrypted (plain btrfs on /dev/sda2, ESP on /dev/sda1), so a forgotten password is an inconvenience, never data loss. There is no scenario here where snapshots can't reach your files as long as you can boot an ISO.

Ladder 1: a TTY. CTRL ALT F3 through F6 (SDDM owns tty1, so do not fight it there). Log in as your user or as root and fix it directly:

passwd uthman                  # forgotten user password (as root)
loginctl unlock-sessions       # stuck at the hyprlock screen
snapper rollback 80            # broke it with an update, see above

SDDM login-looping but the TTY works almost always means the Hyprland session dies on start: check ~/.config/hypr/ for the last edit and journalctl --user -xe for the crash line before rolling anything back.

Ladder 2: no TTY at all, or both passwords lost. Boot an Arch ISO. sudo on this machine requires a password (no NOPASSWD backdoor outside the scoped nethogs rule), so if no password works anywhere, the ISO is the backstop, not a trick:

# from the ISO (by-UUID so a renamed disk can't fool you):
mount -o subvol=@ /dev/disk/by-uuid/600e2481-86c4-40a8-9c2a-a287f3fc1c64 /mnt
mount -o subvol=@home /dev/disk/by-uuid/600e2481-86c4-40a8-9c2a-a287f3fc1c64 /mnt/home
arch-chroot /mnt
passwd uthman                  # or: snapper list + snapper rollback 80
reboot

@home is a separate subvolume, so even a full @ rollback never touches your files. If you only need one file back, mount @home and copy it to a USB stick without chrooting at all.

Know before it happens. Root TTY login needs the root password; if root was never given one (locked ! account), Ladder 1's passwd path doesn't exist and you go straight to Ladder 2. Worth verifying now, while nothing is broken: sudo passwd -S root should not report L.

About

Arch Linux · Hyprland · Matugen dynamic theming dotfiles

Resources

Stars

157 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages