Arch Linux, Hyprland, Quickshell, Matugen. Wallpaper sets the palette, everything else follows.
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.
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.jpgReboot 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.
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.jpgThat one command does five things in order:
- Symlinks the image to
~/.config/theme/current/background, which is what everything else treats as "current wallpaper". - Hands it to
awww(orswww) for a 0.7s wave transition, so the change is visible before any color moves. - Calls
getTheme, which runsmatugen imageto build a Material You palette. - Regenerates the cursor theme through
matugen-cursor, round-tripping it so Hyprland drops its cache and no logout is needed. - Writes the basename to
~/.local/state/omarchy/current/theme.nameas 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.
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.
| 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 whyinstall.shneeds 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.
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.
| 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.
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.jpgNormally 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.
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 toggleDndWarning
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.
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 |
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.
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.
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.
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 |
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> 1024The 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).
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.
~/.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.
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 |
~/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.
- Keybinds:
.config/hypr/bindings.conf(panel Binds page edits this file, Hyprland reloads it) - Look and feel:
.config/hypr/looknfeel.confplus~/.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 inconfig.toml, runmatugen imageonce - Terminal:
.config/kitty/kitty.conffor font and behavior. Colors always come from the generated files
- Quickshell owns the notification bus. If toasts stop appearing, something (usually
swaync) grabbedorg.freedesktop.Notificationsfirst. Mask it. - Generated files are not source.
colors.css,zathurarc,cava/config, and everything undertheme/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.iniholds the Wallhaven API key and is gitignored on purpose. Never move the key intowallshelf.json(tracked).- The tree is usually dirty. Theme outputs regenerate on every wallpaper change, so
git statusalmost 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.
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
rebootA 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 firstThree 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 aboveSDDM 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.
