+
+---
+
+This started as a layer of overrides on top of Omarchy and is now its own
+desktop. It keeps the look, layout, and base functionality that were worth
+keeping, and owns every line that runs. There is no upstream package to track
+and nothing to re-apply after somebody else's release.
+
+## What it is
+
+- **[Hyprland](https://hyprland.org/)** as the compositor, configured in Lua.
+- **[Quickshell](https://quickshell.org/)** as the desktop: `blob-shell` is one
+ process hosting the bar, both menus, the panels, notifications, the lock
+ screen, and the OSD as plugins.
+- **A `blob-*` CLI** of 251 commands, dispatched by `blob`.
+- **24 themes** with a palette pipeline that retints the terminal, editor,
+ browser, shell, and lock screen from one `colors.toml`.
+
+## Layout
+
+| Path | Contents |
+| --- | --- |
+| `bin/` | the `blob--` commands |
+| `shell/` | the Quickshell desktop |
+| `hypr/` | personal Hyprland config, deployed to `~/.config/hypr` |
+| `default/` | the shipped defaults the commands and shell resolve: `hypr/` Lua layer, `themed/` templates, `blob/` menu tree |
+| `themes/` | colour themes |
+| `config/` | shipped app configs |
+| `shell.json` | bar layout and idle timings |
+| `hooks/` | event hooks, such as retinting on theme change |
+| `branding/` | icon, ASCII art, boot splash |
+| `wallpapers/` | 103 wallpapers, previewed in [the gallery](wallpaper-gallery/index.md) |
+| `session/` | wayland session entry and uwsm environment |
+| `docs/` | everything below |
+
+## Docs
+
+| Doc | Covers |
+| --- | --- |
+| [commands.md](docs/commands.md) | every command, generated from its own metadata |
+| [keybinds.md](docs/keybinds.md) | every binding, generated from the Lua |
+| [shell.md](docs/shell.md) | shell layout, bar config, plugin model |
+| [themes.md](docs/themes.md) | the palette pipeline and how to add a theme |
+| [widgets.md](docs/widgets.md) | the ported GTK widgets and where they went |
+| [menu.md](docs/menu.md) | the menu tree and what was trimmed |
+| [upstream.md](docs/upstream.md) | the fork base, for diffing later |
+
+## Install
+
+`BLOB_PATH` is a symlink to this checkout, so `bin/`, `shell/`, `themes/` and
+`default/` are always the working tree and edits are live. Only the handful of
+files that must sit under `~/.config` get copied.
+
+```bash
+./install.sh # link and deploy
+./install.sh --check # report what would change, write nothing
+./install.sh --force # overwrite files with local changes
+```
+
+Then log out and pick the Blob session.
+
+To back out, `./uninstall.sh` removes the session entry, the config, and the
+symlink, restoring any `.bak` the installer made. The checkout, the wallpapers,
+and the themes stay where they are.
+
+## Regenerating docs
+
+```bash
+blob-docs-commands
+blob-docs-keybinds
+```
+
+## Keys worth knowing
+
+| Keys | Action |
+| --- | --- |
+| `Super + Space` | Apps menu |
+| `Super + Alt + Space` | Root menu |
+| `Super + Ctrl + Q` | Quick settings |
+| `Super + Ctrl + M` | System monitor |
+| `Super + Alt + W` | Wallpaper picker |
+| `Super + Ctrl + L` | Lock |
+
+The rest are in [keybinds.md](docs/keybinds.md), or run `blob menu keybindings`.
diff --git a/bin/blob-docs-commands b/bin/blob-docs-commands
new file mode 100755
index 0000000..b8b8a68
--- /dev/null
+++ b/bin/blob-docs-commands
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+# blob:summary=Regenerate docs/commands.md from each command's own metadata
+# blob:hidden=true
+
+set -e
+
+bin_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+repo_dir="$(dirname "$bin_dir")"
+output="$repo_dir/docs/commands.md"
+
+metadata() {
+ sed -n "s/^# blob:$2=//p" "$1" | head -1
+}
+
+area_of() {
+ local name="${1#blob-}"
+ printf '%s\n' "${name%%-*}"
+}
+
+{
+ echo "# Commands"
+ echo
+ echo "Generated by \`blob-docs-commands\` from the \`# blob:summary=\` line in each"
+ echo "file. Do not edit by hand."
+ echo
+ echo "Commands marked hidden are plumbing other commands call, and are left out"
+ echo "of the \`blob\` listing."
+ echo
+
+ current_area=""
+ for file in "$bin_dir"/blob-*; do
+ [[ -f $file ]] || continue
+ name="$(basename "$file")"
+ area="$(area_of "$name")"
+
+ if [[ $area != "$current_area" ]]; then
+ echo
+ echo "## $area"
+ echo
+ echo "| Command | Does | Arguments |"
+ echo "| --- | --- | --- |"
+ current_area="$area"
+ fi
+
+ summary="$(metadata "$file" summary)"
+ args="$(metadata "$file" args)"
+ [[ -n $summary ]] || summary="-"
+ [[ -n $args ]] || args="-"
+ if grep -q '^# blob:hidden=true' "$file"; then
+ summary="$summary (hidden)"
+ fi
+ printf '| `%s` | %s | %s |\n' "$name" "$summary" "${args//|/\\|}"
+ done
+
+ echo
+ echo "## Totals"
+ echo
+ printf '%s commands.\n' "$(ls "$bin_dir"/blob-* | wc -l)"
+} > "$output"
+
+echo "Wrote $output"
diff --git a/bin/blob-docs-keybinds b/bin/blob-docs-keybinds
new file mode 100755
index 0000000..7587426
--- /dev/null
+++ b/bin/blob-docs-keybinds
@@ -0,0 +1,60 @@
+#!/bin/bash
+
+# blob:summary=Regenerate docs/keybinds.md from the Hyprland Lua bindings
+# blob:hidden=true
+
+set -e
+
+bin_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+repo_dir="$(dirname "$bin_dir")"
+output="$repo_dir/docs/keybinds.md"
+
+# o.bind("KEYS", "Description", ...) and o.bind_toggle("KEYS", "Description", ...)
+# are the only two forms the Lua layer uses to declare a binding.
+collect() {
+ local file="$1"
+ grep -ohE 'o\.bind(_toggle)?\(\s*"[^"]+",\s*"[^"]+"' "$file" 2>/dev/null |
+ sed -E 's/o\.bind(_toggle)?\(\s*"([^"]+)",\s*"([^"]+)"/\2\t\3/'
+}
+
+section() {
+ local title="$1"
+ shift
+ local rows
+ rows="$(for file in "$@"; do collect "$file"; done | sort -u -t$'\t' -k1,1)"
+ [[ -n $rows ]] || return 0
+
+ echo
+ echo "## $title"
+ echo
+ echo "| Keys | Action |"
+ echo "| --- | --- |"
+ printf '%s\n' "$rows" | while IFS=$'\t' read -r keys action; do
+ printf '| `%s` | %s |\n' "$keys" "$action"
+ done
+}
+
+{
+ echo "# Keybindings"
+ echo
+ echo "Generated by \`blob-docs-keybinds\` from the \`o.bind\` calls in the Hyprland"
+ echo "Lua files. Do not edit by hand."
+ echo
+ echo "Personal overrides in \`hypr/bindings.lua\` win over the defaults, and"
+ echo "\`hl.unbind\` in that file removes a default outright."
+
+ section "Personal" "$repo_dir/hypr/bindings.lua"
+ section "Applications" "$repo_dir/default/hypr/bindings/applications.lua"
+ section "Tiling" "$repo_dir/default/hypr/bindings/tiling.lua"
+ section "Media" "$repo_dir/default/hypr/bindings/media.lua"
+ section "Clipboard" "$repo_dir/default/hypr/bindings/clipboard.lua"
+ section "Utilities" "$repo_dir/default/hypr/bindings/utilities.lua"
+
+ echo
+ echo "## Unbound defaults"
+ echo
+ grep -ohE 'hl\.unbind\("[^"]+"\)' "$repo_dir/hypr/bindings.lua" 2>/dev/null |
+ sed -E 's/hl\.unbind\("([^"]+)"\)/- `\1`/' || echo "None."
+} > "$output"
+
+echo "Wrote $output"
diff --git a/bin/blob-system-sleep-monitor b/bin/blob-system-sleep-monitor
new file mode 100755
index 0000000..44010dd
--- /dev/null
+++ b/bin/blob-system-sleep-monitor
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+# blob:summary=Monitor sleep preparation and lock before suspend
+# blob:group=system
+# blob:hidden=true
+
+consume_sleep_events() {
+ local line sleep_lock
+ sleep_lock="$BLOB_PATH/bin/blob-system-sleep"
+
+ while IFS= read -r line; do
+ if [[ $line == *"boolean true"* ]]; then
+ "$sleep_lock"
+ return 0
+ fi
+ done
+}
+
+monitor_sleep_events() {
+ local monitor_fd monitor_pid status
+
+ coproc SLEEP_EVENTS {
+ exec dbus-monitor --system \
+ "type='signal',sender='org.freedesktop.login1',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'"
+ }
+ monitor_fd=${SLEEP_EVENTS[0]}
+ monitor_pid=$SLEEP_EVENTS_PID
+
+ cleanup_monitor() {
+ kill "$monitor_pid" 2>/dev/null || true
+ wait "$monitor_pid" 2>/dev/null || true
+ }
+ trap cleanup_monitor EXIT
+
+ consume_sleep_events <&"$monitor_fd"
+ status=$?
+ cleanup_monitor
+ trap - EXIT
+
+ return "$status"
+}
+
+case ${1:-} in
+ --consume)
+ consume_sleep_events
+ exit 0
+ ;;
+ --inhibited)
+ monitor_sleep_events
+ exit 0
+ ;;
+esac
+
+sleep_monitor="$BLOB_PATH/bin/blob-system-sleep-monitor"
+
+exec systemd-inhibit \
+ --what=sleep \
+ --mode=delay \
+ --who=Blob \
+ --why="Lock screen before suspend" \
+ "$sleep_monitor" --inhibited
diff --git a/config/btop/btop.conf b/config/btop/btop.conf
new file mode 100644
index 0000000..b53509d
--- /dev/null
+++ b/config/btop/btop.conf
@@ -0,0 +1,272 @@
+#? Config file for btop v.1.4.6
+
+#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
+#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
+color_theme = "current"
+
+#* If the theme set background should be shown, set to False if you want terminal background transparency.
+theme_background = true
+
+#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false.
+truecolor = true
+
+#* Set to true to force tty mode regardless if a real tty has been detected or not.
+#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols.
+force_tty = false
+
+#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.
+#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box.
+#* Use whitespace " " as separator between different presets.
+#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty"
+presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty"
+
+#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists.
+#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift.
+vim_keys = true
+
+#* Rounded corners on boxes, is ignored if TTY mode is ON.
+rounded_corners = true
+
+#* Use terminal synchronized output sequences to reduce flickering on supported terminals.
+terminal_sync = true
+
+#* Default symbols to use for graph creation, "braille", "block" or "tty".
+#* "braille" offers the highest resolution but might not be included in all fonts.
+#* "block" has half the resolution of braille but uses more common characters.
+#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY.
+#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view.
+graph_symbol = "braille"
+
+# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
+graph_symbol_cpu = "default"
+
+# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty".
+graph_symbol_gpu = "default"
+
+# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
+graph_symbol_mem = "default"
+
+# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
+graph_symbol_net = "default"
+
+# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
+graph_symbol_proc = "default"
+
+#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace.
+shown_boxes = "cpu mem net proc"
+
+#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.
+update_ms = 2000
+
+#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
+#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
+proc_sorting = "cpu lazy"
+
+#* Reverse sorting order, True or False.
+proc_reversed = false
+
+#* Show processes as a tree.
+proc_tree = false
+
+#* Use the cpu graph colors in the process list.
+proc_colors = true
+
+#* Use a darkening gradient in the process list.
+proc_gradient = true
+
+#* If process cpu usage should be of the core it's running on or usage of the total available cpu power.
+proc_per_core = false
+
+#* Show process memory as bytes instead of percent.
+proc_mem_bytes = true
+
+#* Show cpu graph for each process.
+proc_cpu_graphs = true
+
+#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate)
+proc_info_smaps = false
+
+#* Show proc box on left side of screen instead of right.
+proc_left = false
+
+#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop).
+proc_filter_kernel = false
+
+#* In tree-view, always accumulate child process resources in the parent process.
+proc_aggregate = false
+
+#* Should cpu and memory usage display be preserved for dead processes when paused.
+keep_dead_proc_usage = false
+
+#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available.
+#* Select from a list of detected attributes from the options menu.
+cpu_graph_upper = "Auto"
+
+#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available.
+#* Select from a list of detected attributes from the options menu.
+cpu_graph_lower = "Auto"
+
+#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off".
+show_gpu_info = "Auto"
+
+#* Toggles if the lower CPU graph should be inverted.
+cpu_invert_lower = true
+
+#* Set to True to completely disable the lower CPU graph.
+cpu_single_graph = false
+
+#* Show cpu box at bottom of screen instead of top.
+cpu_bottom = false
+
+#* Shows the system uptime in the CPU box.
+show_uptime = true
+
+#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo.
+show_cpu_watts = true
+
+#* Show cpu temperature.
+check_temp = true
+
+#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors.
+cpu_sensor = "Auto"
+
+#* Show temperatures for cpu cores also if check_temp is True and sensors has been found.
+show_coretemp = true
+
+#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core.
+#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine.
+#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries.
+#* Example: "4:0 5:1 6:3"
+cpu_core_map = ""
+
+#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine".
+temp_scale = "celsius"
+
+#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024.
+base_10_sizes = false
+
+#* Show CPU frequency.
+show_cpu_freq = true
+
+#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average".
+freq_mode = "first"
+
+#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
+#* Special formatting: /host = hostname | /user = username | /uptime = system uptime
+clock_format = "%X"
+
+#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort.
+background_update = true
+
+#* Custom cpu model name, empty string to disable.
+custom_cpu_name = ""
+
+#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ".
+#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user"
+disks_filter = ""
+
+#* Show graphs instead of meters for memory values.
+mem_graphs = true
+
+#* Show mem box below net box instead of above.
+mem_below_net = false
+
+#* Count ZFS ARC in cached and available memory.
+zfs_arc_cached = true
+
+#* If swap memory should be shown in memory box.
+show_swap = true
+
+#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk.
+swap_disk = true
+
+#* If mem box should be split to also show disks info.
+show_disks = true
+
+#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar.
+only_physical = true
+
+#* Read disks list from /etc/fstab. This also disables only_physical.
+use_fstab = true
+
+#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool)
+zfs_hide_datasets = false
+
+#* Set to true to show available disk space for privileged users.
+disk_free_priv = false
+
+#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view.
+show_io_stat = true
+
+#* Toggles io mode for disks, showing big graphs for disk read/write speeds.
+io_mode = false
+
+#* Set to True to show combined read/write io graphs in io mode.
+io_graph_combined = false
+
+#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ".
+#* Example: "/mnt/media:100 /:20 /boot:1".
+io_graph_speeds = ""
+
+#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False.
+net_download = 100
+
+net_upload = 100
+
+#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest.
+net_auto = true
+
+#* Sync the auto scaling for download and upload to whichever currently has the highest scale.
+net_sync = true
+
+#* Starts with the Network Interface specified here.
+net_iface = ""
+
+#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes.
+base_10_bitrate = "Auto"
+
+#* Show battery stats in top right if battery is present.
+show_battery = true
+
+#* Which battery to use if multiple are present. "Auto" for auto detection.
+selected_battery = "Auto"
+
+#* Show power stats of battery next to charge indicator.
+show_battery_watts = true
+
+#* Set loglevel for "~/.local/state/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG".
+#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info.
+log_level = "WARNING"
+
+#* Automatically save current settings to config file on exit.
+save_config_on_exit = true
+
+#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards.
+nvml_measure_pcie_speeds = true
+
+#* Measure PCIe throughput on AMD cards, may impact performance on certain cards.
+rsmi_measure_pcie_speeds = true
+
+#* Horizontally mirror the GPU graph.
+gpu_mirror_graph = true
+
+#* Set which GPU vendors to show. Available values are "nvidia amd intel"
+shown_gpus = "nvidia amd intel"
+
+#* Custom gpu0 model name, empty string to disable.
+custom_gpu_name0 = ""
+
+#* Custom gpu1 model name, empty string to disable.
+custom_gpu_name1 = ""
+
+#* Custom gpu2 model name, empty string to disable.
+custom_gpu_name2 = ""
+
+#* Custom gpu3 model name, empty string to disable.
+custom_gpu_name3 = ""
+
+#* Custom gpu4 model name, empty string to disable.
+custom_gpu_name4 = ""
+
+#* Custom gpu5 model name, empty string to disable.
+custom_gpu_name5 = ""
diff --git a/config/foot/foot.ini b/config/foot/foot.ini
new file mode 100644
index 0000000..3b4ec60
--- /dev/null
+++ b/config/foot/foot.ini
@@ -0,0 +1,26 @@
+[main]
+include=~/.local/state/blob/current/theme/foot.ini
+term=xterm-256color
+font=JetBrainsMono Nerd Font:size=9
+pad=14x14
+initial-window-mode=windowed
+workers=0
+
+[scrollback]
+lines=10000
+multiplier=7.0
+
+[cursor]
+style=block
+blink=no
+
+[key-bindings]
+clipboard-copy=Control+Insert Control+Shift+c XF86Copy
+primary-paste=none
+clipboard-paste=Shift+Insert Control+Shift+v XF86Paste
+
+[text-bindings]
+# Send Shift+Return as CSI-u so TUIs can distinguish it from Return.
+\x1b[13;2u=Shift+Return
+# Send Alt+Shift+Return as CSI-u so tmux can match M-S-Enter.
+\x1b[13;4u=Mod1+Shift+Return
diff --git a/config/git/config b/config/git/config
new file mode 100644
index 0000000..0f8e979
--- /dev/null
+++ b/config/git/config
@@ -0,0 +1,28 @@
+# See https://git-scm.com/docs/git-config
+
+[alias]
+ co = checkout
+ br = branch
+ ci = commit
+ st = status
+[init]
+ defaultBranch = master
+[pull]
+ rebase = true # Rebase (instead of merge) on pull
+[push]
+ autoSetupRemote = true # Automatically set upstream branch on push
+[diff]
+ algorithm = histogram # Clearer diffs on moved/edited lines
+ colorMoved = plain # Highlight moved blocks in diffs
+ mnemonicPrefix = true # More intuitive refs in diff output
+[commit]
+ verbose = true # Include diff comment in commit message template
+[column]
+ ui = auto # Output in columns when possible
+[branch]
+ sort = -committerdate # Sort branches by most recent commit first
+[tag]
+ sort = -version:refname # Sort version numbers as you would expect
+[rerere]
+ enabled = true # Record and reuse conflict resolutions
+ autoupdate = true # Apply stored conflict resolutions automatically
diff --git a/config/hypr/autostart.lua b/config/hypr/autostart.lua
new file mode 100644
index 0000000..de1a69a
--- /dev/null
+++ b/config/hypr/autostart.lua
@@ -0,0 +1,2 @@
+-- Extra autostart processes.
+-- o.launch_on_start("my-service")
diff --git a/config/hypr/bindings.lua b/config/hypr/bindings.lua
new file mode 100644
index 0000000..8bcc011
--- /dev/null
+++ b/config/hypr/bindings.lua
@@ -0,0 +1,29 @@
+-- Keep only your personal keybinding overrides here. Add new bindings or
+-- unbind defaults before replacing them.
+
+-- See current bindings and descriptions:
+-- blob menu keybindings --print
+
+-- To disable every Blob default binding, set this in
+-- ~/.config/hypr/hyprland.lua before require("default.hypr.blob"), then add
+-- only the bindings you want below:
+-- blob_default_bindings = false
+
+-- To disable all preinstalled app/webapp bindings, set:
+-- blob_preinstalled_bindings = false
+
+-- Add a new binding.
+-- o.bind("SUPER + SHIFT + R", "SSH", "alacritty -e ssh your-server")
+
+-- Change an existing binding by unbinding it first, then binding the key again.
+-- This example changes SUPER+SPACE from the launcher to the Blob root menu.
+-- hl.unbind("SUPER + SPACE")
+-- o.bind("SUPER + SPACE", "Blob menu", "blob-menu toggle root")
+
+-- Disable a default binding without replacing it.
+-- hl.unbind("SUPER + SHIFT + B")
+
+-- Logitech MX Keys examples:
+-- o.bind("SUPER + SHIFT + S", nil, "blob-capture-screenshot")
+-- o.bind("SUPER + H", nil, "voxtype record toggle")
+-- o.bind("SUPER + PERIOD", nil, "blob-shell shell toggle blob.emojis")
diff --git a/config/hypr/hyprland.lua b/config/hypr/hyprland.lua
new file mode 100644
index 0000000..cdb6935
--- /dev/null
+++ b/config/hypr/hyprland.lua
@@ -0,0 +1,29 @@
+-- Learn how to configure Hyprland: https://wiki.hypr.land/Configuring/Start/
+
+-- Blob's bootstrap keeps path setup out of this user config.
+dofile((os.getenv("BLOB_PATH") or "$HOME/.local/share/blob") .. "/default/hypr/bootstrap.lua")
+
+-- Disable all Blob default bindings. Add your own in hypr/bindings.lua.
+-- blob_default_bindings = false
+--
+-- Or disable only bindings for Blob's preinstalled apps/web apps while
+-- keeping core window-manager bindings:
+-- blob_preinstalled_bindings = false
+
+-- Load Blob defaults.
+require("default.hypr.blob")
+
+-- Put your personal overrides in these files. They're loaded after Blob's
+-- defaults so package updates can improve the defaults without rewriting your
+-- ~/.config/hypr files.
+require("hypr.monitors")
+require("hypr.input")
+require("hypr.bindings")
+require("hypr.looknfeel")
+require("hypr.autostart")
+
+-- Toggle config flags dynamically.
+require("default.hypr.toggles")
+
+-- Add any other personal Hyprland configuration below.
+-- o.window("qemu", { workspace = "5" })
diff --git a/config/hypr/hyprsunset.conf b/config/hypr/hyprsunset.conf
new file mode 100644
index 0000000..a3ba208
--- /dev/null
+++ b/config/hypr/hyprsunset.conf
@@ -0,0 +1,14 @@
+# Makes hyprsunset do nothing to the screen by default
+# Without this, the default applies some tint to the monitor
+profile {
+ time = 07:00
+ identity = true
+}
+
+# To enable auto switch to nightlight, add to your .config/hypr/autostart.lua:
+# o.launch_on_start("hyprsunset")
+# and use the following:
+# profile {
+# time = 20:00
+# temperature = 4000
+# }
diff --git a/config/hypr/input.lua b/config/hypr/input.lua
new file mode 100644
index 0000000..e956fae
--- /dev/null
+++ b/config/hypr/input.lua
@@ -0,0 +1,57 @@
+-- Keep only your personal input overrides here. Uncommented settings below
+-- replace Blob's defaults.
+
+-- Keyboard layout and options.
+-- See https://wiki.hypr.land/Configuring/Basics/Variables/#input
+-- hl.config({
+-- input = {
+-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt.
+-- kb_layout = "us,dk,eu",
+-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle",
+--
+-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards).
+-- kb_variant = "intl",
+--
+-- -- Change speed of keyboard repeat.
+-- repeat_rate = 40,
+-- repeat_delay = 250,
+--
+-- -- Start with numlock on by default.
+-- numlock_by_default = true,
+--
+-- -- Increase sensitivity for mouse/trackpad (default: 0).
+-- sensitivity = 0.35,
+--
+-- -- Turn off mouse acceleration (default: adaptive).
+-- accel_profile = "flat",
+--
+-- touchpad = {
+-- -- Use natural (inverse) scrolling.
+-- natural_scroll = true,
+--
+-- -- Use two-finger clicks for right-click instead of lower-right corner.
+-- clickfinger_behavior = true,
+--
+-- -- Control the speed of your scrolling.
+-- scroll_factor = 0.4,
+--
+-- -- Enable the touchpad while typing.
+-- disable_while_typing = false,
+--
+-- -- Left-click-and-drag with three fingers.
+-- drag_3fg = 1,
+-- },
+-- },
+-- })
+
+-- App-specific touchpad scroll speeds.
+-- o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 })
+-- o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 })
+
+-- Enable touchpad gestures for changing workspaces.
+-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/
+-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
+
+-- Enable touchpad gestures for moving focus (helpful on scrolling layout).
+-- hl.gesture({ fingers = 3, direction = "left", action = function() hl.dispatch(hl.dsp.focus({ direction = "l" })) end })
+-- hl.gesture({ fingers = 3, direction = "right", action = function() hl.dispatch(hl.dsp.focus({ direction = "r" })) end })
diff --git a/config/hypr/looknfeel.lua b/config/hypr/looknfeel.lua
new file mode 100644
index 0000000..18a5f38
--- /dev/null
+++ b/config/hypr/looknfeel.lua
@@ -0,0 +1,50 @@
+-- Change the default Blob look'n'feel.
+
+-- https://wiki.hypr.land/Configuring/Basics/Variables/#general
+-- hl.config({
+-- general = {
+-- -- No gaps between windows or borders.
+-- gaps_in = 0,
+-- gaps_out = 0,
+-- border_size = 0,
+--
+-- -- Change to niri-like side-scrolling layout.
+-- layout = "scrolling",
+-- },
+-- })
+
+-- https://wiki.hypr.land/Configuring/Basics/Variables/#decoration
+-- hl.config({
+-- decoration = {
+-- -- Use round window corners.
+-- rounding = 8,
+--
+-- -- Dim unfocused windows (0.0 = no dim, 1.0 = fully dimmed).
+-- dim_inactive = true,
+-- dim_strength = 0.15,
+-- },
+-- })
+
+-- https://wiki.hypr.land/Configuring/Basics/Variables/#animations
+-- hl.config({
+-- animations = {
+-- -- Disable all animations.
+-- enabled = false,
+-- },
+-- })
+
+-- https://wiki.hypr.land/Configuring/Basics/Variables/#layout
+-- hl.config({
+-- layout = {
+-- -- Avoid overly wide single-window layouts on wide screens.
+-- single_window_aspect_ratio = { 1, 1 },
+-- },
+-- })
+
+-- https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/
+-- hl.config({
+-- scrolling = {
+-- -- See only one column per screen instead of two.
+-- column_width = 0.97,
+-- },
+-- })
diff --git a/config/hypr/monitors.lua b/config/hypr/monitors.lua
new file mode 100644
index 0000000..420fd51
--- /dev/null
+++ b/config/hypr/monitors.lua
@@ -0,0 +1,14 @@
+-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
+-- List current monitors and supported resolutions with: hyprctl monitors all
+
+local blob_gdk_scale = 2
+local blob_monitor_scale = "auto"
+
+hl.env("GDK_SCALE", tostring(blob_gdk_scale))
+hl.monitor({ output = "", mode = "preferred", position = "auto", scale = blob_monitor_scale })
+
+-- Configure a specific monitor.
+-- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 })
+
+-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°).
+-- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 })
diff --git a/config/hypr/xdph.conf b/config/hypr/xdph.conf
new file mode 100644
index 0000000..63b66be
--- /dev/null
+++ b/config/hypr/xdph.conf
@@ -0,0 +1,4 @@
+screencopy {
+ allow_token_by_default = true
+ custom_picker_binary = hyprland-preview-share-picker
+}
diff --git a/config/imv/config b/config/imv/config
new file mode 100644
index 0000000..f96aa6a
--- /dev/null
+++ b/config/imv/config
@@ -0,0 +1,16 @@
+[binds]
+
+# Print the current image file
+ = exec lp "$imv_current_file"
+
+# Trash the current image and quit the viewer (recoverable from Trash)
+ = exec gio trash -- "$imv_current_file"; quit
+
+# Trash the current image and move to the next one
+ = exec gio trash -- "$imv_current_file"; close
+
+# Rotate the currently open image by 90 degrees
+ = exec mogrify -rotate 90 "$imv_current_file"
+
+# Edit the current image in Tensaku and quit the viewer
+ = exec tensaku-edit "$imv_current_file" & ; quit
diff --git a/config/lazygit/config.yml b/config/lazygit/config.yml
new file mode 100644
index 0000000..e69de29
diff --git a/config/starship.toml b/config/starship.toml
new file mode 100644
index 0000000..36f15d1
--- /dev/null
+++ b/config/starship.toml
@@ -0,0 +1,32 @@
+add_newline = true
+command_timeout = 200
+format = "[$directory$git_branch$git_status]($style)$character"
+
+[character]
+error_symbol = "[✗](bold cyan)"
+success_symbol = "[❯](bold cyan)"
+
+[directory]
+truncation_length = 2
+truncation_symbol = "…/"
+repo_root_style = "bold cyan"
+repo_root_format = "[$repo_root]($repo_root_style)[$path]($style)[$read_only]($read_only_style) "
+
+[git_branch]
+format = "[$branch]($style) "
+style = "italic cyan"
+
+[git_status]
+format = '[$all_status]($style)'
+style = "cyan"
+ahead = "⇡${count} "
+diverged = "⇕⇡${ahead_count}⇣${behind_count} "
+behind = "⇣${count} "
+conflicted = " "
+up_to_date = " "
+untracked = "? "
+modified = " "
+stashed = ""
+staged = ""
+renamed = ""
+deleted = ""
diff --git a/default/audio/filter-chain-host.conf b/default/audio/filter-chain-host.conf
new file mode 100644
index 0000000..a1f2752
--- /dev/null
+++ b/default/audio/filter-chain-host.conf
@@ -0,0 +1,40 @@
+# Host config for the Blob speaker tuning.
+#
+# This exists so the tuning gets its own PipeWire client rather than sharing
+# PipeWire's stock filter-chain.conf. That config merges every fragment in
+# ~/.config/pipewire/filter-chain.conf.d/, so hosting the tuning there would load
+# any unrelated filter a user keeps in that directory -- duplicating filters
+# already hosted elsewhere, and stopping them all when the tuning is switched off.
+#
+# Installed as ~/.config/pipewire/blob-speaker-tuning.conf with the tuning
+# graph merged from blob-speaker-tuning.conf.d/, and run with
+# pipewire -c blob-speaker-tuning.conf
+#
+# The contents are the minimum a filter-hosting client needs, taken from
+# /usr/share/pipewire/filter-chain.conf.
+
+context.properties = {
+ log.level = 0
+}
+
+context.spa-libs = {
+ audio.convert.* = audioconvert/libspa-audioconvert
+ support.* = support/libspa-support
+}
+
+context.modules = [
+ # Boost the audio thread priority.
+ { name = libpipewire-module-rt
+ args = { }
+ flags = [ ifexists nofail ]
+ }
+
+ # The native communication protocol.
+ { name = libpipewire-module-protocol-native }
+
+ # Lets this process provide nodes to PipeWire.
+ { name = libpipewire-module-client-node }
+
+ # Wraps nodes in an adapter with a converter and resampler.
+ { name = libpipewire-module-adapter }
+]
diff --git a/default/audio/tunings/dell-xps-2026/filter-chain.conf b/default/audio/tunings/dell-xps-2026/filter-chain.conf
new file mode 100644
index 0000000..4ef787d
--- /dev/null
+++ b/default/audio/tunings/dell-xps-2026/filter-chain.conf
@@ -0,0 +1,140 @@
+# Dell XPS 14 / XPS 16 (2026) speaker tuning.
+#
+# Biquad chain fitted to the measured response of the xps-audio-linux EasyEffects
+# profile under a dense pink-weighted multitone of 104 bin-aligned tones,
+# followed by a lookahead limiter. Measures 1.24 dB RMS against that reference
+# (0.97 dB weighted over the fit's own error metric).
+#
+# Fitted and measured on the XPS 14 (SKU 0DB9); the XPS 16 (0DBA) is covered on
+# report that the same profile suits it. See tuning.conf.
+#
+# Q below 200 Hz is capped at 1.8 on purpose. A closer magnitude fit is possible
+# with high-Q sections, but the reference produces its narrow bass features by
+# convolution, and reproducing them with high-Q biquads swung group delay 31 ms
+# across 63-80 Hz, which smears bass transients. The cap costs 0.33 dB and
+# halves the swing.
+#
+# This is a plain filter-chain sink rather than a WirePlumber smart filter. A
+# smart filter is the better shape -- it would leave the real device as the
+# default output instead of adding a second one -- but on PipeWire 1.6.8 /
+# WirePlumber 0.5.15 this graph loads and links correctly as a smart filter and
+# then passes audio through unprocessed: its controls are present and
+# mpv -> filter -> sink links are made, yet the filter's input monitor and the
+# speaker sink's monitor measure identically. Revisit when that is understood.
+#
+# Channels are wired explicitly because the limiter is a stereo plugin; a mono
+# graph is duplicated per channel and would limit each side independently,
+# shifting the stereo image on bass transients.
+
+context.modules = [
+ { name = libpipewire-module-filter-chain
+ args = {
+ node.description = "Laptop Speakers"
+ media.name = "Laptop Speakers"
+
+ filter.graph = {
+ nodes = [
+ { type = builtin name = s0_l label = bq_highpass control = { "Freq" = 60.9 "Q" = 1.0 } }
+ { type = builtin name = s1_l label = bq_highpass control = { "Freq" = 60.9 "Q" = 1.0 } }
+ { type = builtin name = s2_l label = bq_peaking control = { "Freq" = 83.4 "Q" = 1.8 "Gain" = -8.0 } }
+ { type = builtin name = s3_l label = bq_peaking control = { "Freq" = 100.4 "Q" = 1.59 "Gain" = 7.47 } }
+ { type = builtin name = s4_l label = bq_peaking control = { "Freq" = 250.5 "Q" = 2.966 "Gain" = -4.7 } }
+ { type = builtin name = s5_l label = bq_peaking control = { "Freq" = 419.8 "Q" = 3.0 "Gain" = -5.83 } }
+ { type = builtin name = s6_l label = bq_peaking control = { "Freq" = 631.3 "Q" = 2.515 "Gain" = -10.33 } }
+ { type = builtin name = s7_l label = bq_peaking control = { "Freq" = 894.4 "Q" = 4.0 "Gain" = -2.42 } }
+ { type = builtin name = s8_l label = bq_peaking control = { "Freq" = 1355.7 "Q" = 2.884 "Gain" = 6.92 } }
+ { type = builtin name = s9_l label = bq_peaking control = { "Freq" = 1707.2 "Q" = 1.311 "Gain" = -6.54 } }
+ { type = builtin name = s10_l label = bq_peaking control = { "Freq" = 3100.0 "Q" = 0.5 "Gain" = -10.09 } }
+ { type = builtin name = s11_l label = bq_peaking control = { "Freq" = 3200.0 "Q" = 1.048 "Gain" = 3.09 } }
+ { type = builtin name = s12_l label = bq_highshelf control = { "Freq" = 6015.2 "Q" = 1.5 "Gain" = -1.34 } }
+
+ { type = builtin name = s0_r label = bq_highpass control = { "Freq" = 60.9 "Q" = 1.0 } }
+ { type = builtin name = s1_r label = bq_highpass control = { "Freq" = 60.9 "Q" = 1.0 } }
+ { type = builtin name = s2_r label = bq_peaking control = { "Freq" = 83.4 "Q" = 1.8 "Gain" = -8.0 } }
+ { type = builtin name = s3_r label = bq_peaking control = { "Freq" = 100.4 "Q" = 1.59 "Gain" = 7.47 } }
+ { type = builtin name = s4_r label = bq_peaking control = { "Freq" = 250.5 "Q" = 2.966 "Gain" = -4.7 } }
+ { type = builtin name = s5_r label = bq_peaking control = { "Freq" = 419.8 "Q" = 3.0 "Gain" = -5.83 } }
+ { type = builtin name = s6_r label = bq_peaking control = { "Freq" = 631.3 "Q" = 2.515 "Gain" = -10.33 } }
+ { type = builtin name = s7_r label = bq_peaking control = { "Freq" = 894.4 "Q" = 4.0 "Gain" = -2.42 } }
+ { type = builtin name = s8_r label = bq_peaking control = { "Freq" = 1355.7 "Q" = 2.884 "Gain" = 6.92 } }
+ { type = builtin name = s9_r label = bq_peaking control = { "Freq" = 1707.2 "Q" = 1.311 "Gain" = -6.54 } }
+ { type = builtin name = s10_r label = bq_peaking control = { "Freq" = 3100.0 "Q" = 0.5 "Gain" = -10.09 } }
+ { type = builtin name = s11_r label = bq_peaking control = { "Freq" = 3200.0 "Q" = 1.048 "Gain" = 3.09 } }
+ { type = builtin name = s12_r label = bq_highshelf control = { "Freq" = 6015.2 "Q" = 1.5 "Gain" = -1.34 } }
+ { type = lv2
+ name = limiter
+ plugin = "http://lsp-plug.in/plugins/lv2/limiter_stereo"
+ control = {
+ # Both default to enabled: "alr" regulates level toward the
+ # threshold and "boost" normalises the threshold up to full
+ # scale. A fixed tuning must switch them off or its tone drifts
+ # with programme level.
+ "alr" = 0
+ "boost" = 0
+ "g_in" = 0.5456
+ "th" = 0.891
+ }
+ }
+ ]
+
+ links = [
+ { output = "s0_l:Out" input = "s1_l:In" }
+ { output = "s1_l:Out" input = "s2_l:In" }
+ { output = "s2_l:Out" input = "s3_l:In" }
+ { output = "s3_l:Out" input = "s4_l:In" }
+ { output = "s4_l:Out" input = "s5_l:In" }
+ { output = "s5_l:Out" input = "s6_l:In" }
+ { output = "s6_l:Out" input = "s7_l:In" }
+ { output = "s7_l:Out" input = "s8_l:In" }
+ { output = "s8_l:Out" input = "s9_l:In" }
+ { output = "s9_l:Out" input = "s10_l:In" }
+ { output = "s10_l:Out" input = "s11_l:In" }
+ { output = "s11_l:Out" input = "s12_l:In" }
+ { output = "s12_l:Out" input = "limiter:in_l" }
+ { output = "s0_r:Out" input = "s1_r:In" }
+ { output = "s1_r:Out" input = "s2_r:In" }
+ { output = "s2_r:Out" input = "s3_r:In" }
+ { output = "s3_r:Out" input = "s4_r:In" }
+ { output = "s4_r:Out" input = "s5_r:In" }
+ { output = "s5_r:Out" input = "s6_r:In" }
+ { output = "s6_r:Out" input = "s7_r:In" }
+ { output = "s7_r:Out" input = "s8_r:In" }
+ { output = "s8_r:Out" input = "s9_r:In" }
+ { output = "s9_r:Out" input = "s10_r:In" }
+ { output = "s10_r:Out" input = "s11_r:In" }
+ { output = "s11_r:Out" input = "s12_r:In" }
+ { output = "s12_r:Out" input = "limiter:in_r" }
+ ]
+
+ inputs = [ "s0_l:In" "s0_r:In" ]
+ outputs = [ "limiter:out_l" "limiter:out_r" ]
+ }
+
+ audio.channels = 2
+ audio.position = [ FL FR ]
+
+ capture.props = {
+ node.name = "blob_speaker_tuning"
+ media.class = Audio/Sink
+ }
+ playback.props = {
+ node.name = "blob_speaker_tuning_output"
+ node.passive = true
+ target.object = "@SPEAKER_SINK@"
+ # This stream is the filter's output and is a movable sink input like any
+ # other, so anything that reroutes "all streams" to a newly selected
+ # output would drag the processing along with it -- onto headphones, or
+ # into the tuning's own sink, which is a cycle. Pin it.
+ node.dont-move = true
+ # If the speaker sink is not present yet -- the tuning host can start
+ # before the device is discovered -- WirePlumber would otherwise link this
+ # output to whatever default exists, quietly tuning the wrong device while
+ # the tuning sink still looks healthy. Wait for the named target instead.
+ # Both are needed: without linger, WirePlumber destroys the node rather
+ # than waiting (see its scripts/linking/find-defined-target.lua).
+ node.dont-fallback = true
+ node.linger = true
+ }
+ }
+ }
+]
diff --git a/default/audio/tunings/dell-xps-2026/tuning.conf b/default/audio/tunings/dell-xps-2026/tuning.conf
new file mode 100644
index 0000000..e7bb633
--- /dev/null
+++ b/default/audio/tunings/dell-xps-2026/tuning.conf
@@ -0,0 +1,39 @@
+## Dell XPS 14 / XPS 16 (2026) internal speakers.
+##
+## Thirteen biquads and a lookahead limiter, applied as a PipeWire filter-chain
+## in front of the internal speaker sink. The stock Linux path already loads
+## Dell's Cirrus smart-amplifier firmware; this adds the perceptual voicing the
+## Windows Waves layer provides and Linux does not.
+
+description="Dell XPS 14/16 (2026) speakers"
+## Matched on the DMI product SKU, which is what Dell keys the Cirrus speaker
+## firmware on -- 10280db9 for the XPS 14 and 10280dba for the XPS 16 -- so it
+## identifies the speaker hardware itself rather than a marketing name. Compared as
+## whole values, so this cannot widen to the rest of the XPS line.
+##
+## 0DB9 XPS 14 -- measured here, see below
+## 0DBA XPS 16 -- included on report that this profile suits it, not measured
+match_sku=("0DB9" "0DBA")
+## Unescaped dots: this is passed to awk as a string, where a backslash escape
+## would be consumed before the regex sees it.
+sink_pattern='^alsa_output.*sof_sdw.*HiFi__Speaker__sink$'
+
+## Provenance. Derived by measuring the response of the xps-clone EasyEffects
+## profile from https://github.com/spencerbull/xps-audio-linux (MIT) and fitting
+## a biquad chain to it. No upstream asset is redistributed: the convolution
+## impulse response is not carried, so this tuning has no binary blob and is
+## sample-rate agnostic.
+derived_from="xps-audio-linux xps-clone (MIT, spencerbull)"
+validated_by="dhh"
+validated_on="2026-07-24"
+## The measurements below were taken on the XPS 14 (0DB9). The XPS 16 (0DBA) is
+## covered on report rather than measurement; re-measure there before treating
+## these figures as describing it.
+validated_hardware="XPS 14 DA14260 (0DB9)"
+
+## Measured against that reference under a dense pink-weighted multitone of 104
+## bin-aligned tones. See docs/AUDIO-TUNING.md for how to reproduce these.
+magnitude_rms_db="1.24"
+bass_group_delay_swing_ms="13.2"
+limiter_headroom_db="1.6" ## worst-case peak on a hot master vs -1 dBFS
+dynamic_range_delta_lu="0.1"
diff --git a/menu/menu.jsonc b/default/blob/blob-menu.jsonc
similarity index 100%
rename from menu/menu.jsonc
rename to default/blob/blob-menu.jsonc
diff --git a/default/foot/screensaver.ini b/default/foot/screensaver.ini
new file mode 100644
index 0000000..7130d06
--- /dev/null
+++ b/default/foot/screensaver.ini
@@ -0,0 +1,7 @@
+[main]
+font=JetBrainsMono Nerd Font:size=18
+pad=0x0
+
+[colors-dark]
+background=000000
+foreground=ffffff
diff --git a/hypr/default/apps.lua b/default/hypr/apps.lua
similarity index 100%
rename from hypr/default/apps.lua
rename to default/hypr/apps.lua
diff --git a/hypr/default/apps/1password.lua b/default/hypr/apps/1password.lua
similarity index 100%
rename from hypr/default/apps/1password.lua
rename to default/hypr/apps/1password.lua
diff --git a/hypr/default/apps/battlenet.lua b/default/hypr/apps/battlenet.lua
similarity index 100%
rename from hypr/default/apps/battlenet.lua
rename to default/hypr/apps/battlenet.lua
diff --git a/hypr/default/apps/bitwarden.lua b/default/hypr/apps/bitwarden.lua
similarity index 100%
rename from hypr/default/apps/bitwarden.lua
rename to default/hypr/apps/bitwarden.lua
diff --git a/hypr/default/apps/browser.lua b/default/hypr/apps/browser.lua
similarity index 100%
rename from hypr/default/apps/browser.lua
rename to default/hypr/apps/browser.lua
diff --git a/hypr/default/apps/davinci-resolve.lua b/default/hypr/apps/davinci-resolve.lua
similarity index 100%
rename from hypr/default/apps/davinci-resolve.lua
rename to default/hypr/apps/davinci-resolve.lua
diff --git a/hypr/default/apps/geforce.lua b/default/hypr/apps/geforce.lua
similarity index 100%
rename from hypr/default/apps/geforce.lua
rename to default/hypr/apps/geforce.lua
diff --git a/hypr/default/apps/hermes.lua b/default/hypr/apps/hermes.lua
similarity index 100%
rename from hypr/default/apps/hermes.lua
rename to default/hypr/apps/hermes.lua
diff --git a/hypr/default/apps/jetbrains.lua b/default/hypr/apps/jetbrains.lua
similarity index 100%
rename from hypr/default/apps/jetbrains.lua
rename to default/hypr/apps/jetbrains.lua
diff --git a/hypr/default/apps/localsend.lua b/default/hypr/apps/localsend.lua
similarity index 100%
rename from hypr/default/apps/localsend.lua
rename to default/hypr/apps/localsend.lua
diff --git a/hypr/default/apps/moonlight.lua b/default/hypr/apps/moonlight.lua
similarity index 100%
rename from hypr/default/apps/moonlight.lua
rename to default/hypr/apps/moonlight.lua
diff --git a/hypr/default/apps/omarchy-shell.lua b/default/hypr/apps/omarchy-shell.lua
similarity index 100%
rename from hypr/default/apps/omarchy-shell.lua
rename to default/hypr/apps/omarchy-shell.lua
diff --git a/hypr/default/apps/pip.lua b/default/hypr/apps/pip.lua
similarity index 100%
rename from hypr/default/apps/pip.lua
rename to default/hypr/apps/pip.lua
diff --git a/hypr/default/apps/qemu.lua b/default/hypr/apps/qemu.lua
similarity index 100%
rename from hypr/default/apps/qemu.lua
rename to default/hypr/apps/qemu.lua
diff --git a/hypr/default/apps/retroarch.lua b/default/hypr/apps/retroarch.lua
similarity index 100%
rename from hypr/default/apps/retroarch.lua
rename to default/hypr/apps/retroarch.lua
diff --git a/hypr/default/apps/screenshot-selection.lua b/default/hypr/apps/screenshot-selection.lua
similarity index 100%
rename from hypr/default/apps/screenshot-selection.lua
rename to default/hypr/apps/screenshot-selection.lua
diff --git a/hypr/default/apps/steam.lua b/default/hypr/apps/steam.lua
similarity index 100%
rename from hypr/default/apps/steam.lua
rename to default/hypr/apps/steam.lua
diff --git a/hypr/default/apps/system.lua b/default/hypr/apps/system.lua
similarity index 100%
rename from hypr/default/apps/system.lua
rename to default/hypr/apps/system.lua
diff --git a/hypr/default/apps/telegram.lua b/default/hypr/apps/telegram.lua
similarity index 100%
rename from hypr/default/apps/telegram.lua
rename to default/hypr/apps/telegram.lua
diff --git a/hypr/default/apps/terminals.lua b/default/hypr/apps/terminals.lua
similarity index 100%
rename from hypr/default/apps/terminals.lua
rename to default/hypr/apps/terminals.lua
diff --git a/hypr/default/apps/webcam-overlay.lua b/default/hypr/apps/webcam-overlay.lua
similarity index 100%
rename from hypr/default/apps/webcam-overlay.lua
rename to default/hypr/apps/webcam-overlay.lua
diff --git a/hypr/default/autostart.lua b/default/hypr/autostart.lua
similarity index 100%
rename from hypr/default/autostart.lua
rename to default/hypr/autostart.lua
diff --git a/hypr/default/bindings.lua b/default/hypr/bindings.lua
similarity index 100%
rename from hypr/default/bindings.lua
rename to default/hypr/bindings.lua
diff --git a/hypr/default/bindings/applications.lua b/default/hypr/bindings/applications.lua
similarity index 100%
rename from hypr/default/bindings/applications.lua
rename to default/hypr/bindings/applications.lua
diff --git a/hypr/default/bindings/clipboard.lua b/default/hypr/bindings/clipboard.lua
similarity index 100%
rename from hypr/default/bindings/clipboard.lua
rename to default/hypr/bindings/clipboard.lua
diff --git a/hypr/default/bindings/media.lua b/default/hypr/bindings/media.lua
similarity index 100%
rename from hypr/default/bindings/media.lua
rename to default/hypr/bindings/media.lua
diff --git a/hypr/default/bindings/tiling.lua b/default/hypr/bindings/tiling.lua
similarity index 100%
rename from hypr/default/bindings/tiling.lua
rename to default/hypr/bindings/tiling.lua
diff --git a/hypr/default/bindings/utilities.lua b/default/hypr/bindings/utilities.lua
similarity index 100%
rename from hypr/default/bindings/utilities.lua
rename to default/hypr/bindings/utilities.lua
diff --git a/hypr/default/blob.lua b/default/hypr/blob.lua
similarity index 100%
rename from hypr/default/blob.lua
rename to default/hypr/blob.lua
diff --git a/hypr/default/bootstrap.lua b/default/hypr/bootstrap.lua
similarity index 100%
rename from hypr/default/bootstrap.lua
rename to default/hypr/bootstrap.lua
diff --git a/hypr/default/disabled-input-device.lua b/default/hypr/disabled-input-device.lua
similarity index 100%
rename from hypr/default/disabled-input-device.lua
rename to default/hypr/disabled-input-device.lua
diff --git a/hypr/default/envs.lua b/default/hypr/envs.lua
similarity index 100%
rename from hypr/default/envs.lua
rename to default/hypr/envs.lua
diff --git a/hypr/default/helpers.lua b/default/hypr/helpers.lua
similarity index 100%
rename from hypr/default/helpers.lua
rename to default/hypr/helpers.lua
diff --git a/hypr/default/input.lua b/default/hypr/input.lua
similarity index 100%
rename from hypr/default/input.lua
rename to default/hypr/input.lua
diff --git a/hypr/default/looknfeel.lua b/default/hypr/looknfeel.lua
similarity index 100%
rename from hypr/default/looknfeel.lua
rename to default/hypr/looknfeel.lua
diff --git a/hypr/default/nvidia.lua b/default/hypr/nvidia.lua
similarity index 100%
rename from hypr/default/nvidia.lua
rename to default/hypr/nvidia.lua
diff --git a/hypr/default/paths.lua b/default/hypr/paths.lua
similarity index 100%
rename from hypr/default/paths.lua
rename to default/hypr/paths.lua
diff --git a/hypr/default/require_all.lua b/default/hypr/require_all.lua
similarity index 100%
rename from hypr/default/require_all.lua
rename to default/hypr/require_all.lua
diff --git a/hypr/default/require_optional.lua b/default/hypr/require_optional.lua
similarity index 100%
rename from hypr/default/require_optional.lua
rename to default/hypr/require_optional.lua
diff --git a/hypr/default/toggles.lua b/default/hypr/toggles.lua
similarity index 100%
rename from hypr/default/toggles.lua
rename to default/hypr/toggles.lua
diff --git a/hypr/default/toggles/flags.lua b/default/hypr/toggles/flags.lua
similarity index 100%
rename from hypr/default/toggles/flags.lua
rename to default/hypr/toggles/flags.lua
diff --git a/hypr/default/toggles/single-window-aspect-ratio.lua b/default/hypr/toggles/single-window-aspect-ratio.lua
similarity index 100%
rename from hypr/default/toggles/single-window-aspect-ratio.lua
rename to default/hypr/toggles/single-window-aspect-ratio.lua
diff --git a/hypr/default/toggles/window-no-gaps.lua b/default/hypr/toggles/window-no-gaps.lua
similarity index 100%
rename from hypr/default/toggles/window-no-gaps.lua
rename to default/hypr/toggles/window-no-gaps.lua
diff --git a/hypr/default/windows.lua b/default/hypr/windows.lua
similarity index 100%
rename from hypr/default/windows.lua
rename to default/hypr/windows.lua
diff --git a/hypr/default/workspace-layouts.lua b/default/hypr/workspace-layouts.lua
similarity index 100%
rename from hypr/default/workspace-layouts.lua
rename to default/hypr/workspace-layouts.lua
diff --git a/default/systemd/faster-shutdown.conf b/default/systemd/faster-shutdown.conf
new file mode 100644
index 0000000..90dce4f
--- /dev/null
+++ b/default/systemd/faster-shutdown.conf
@@ -0,0 +1,2 @@
+[Manager]
+DefaultTimeoutStopSec=5s
diff --git a/default/systemd/system-sleep/force-igpu b/default/systemd/system-sleep/force-igpu
new file mode 100644
index 0000000..e0eeaae
--- /dev/null
+++ b/default/systemd/system-sleep/force-igpu
@@ -0,0 +1,65 @@
+#!/bin/bash
+
+set -e
+
+# Use the Vfio to Integrated trick to turn off NVIDIA dgpu when in integrated mode
+# without needing to restart the computer. This is needed because computers like the Asus G14
+# will wake after suspend in Hybrid mode, even if the system was in Integrated mode before
+# suspending.
+
+restore_marker=/run/blob-force-igpu-integrated
+sleep_action=${SYSTEMD_SLEEP_ACTION:-$2}
+[[ -x /usr/bin/supergfxctl ]] || exit 0
+
+switch_mode() {
+ local expected="$1" current
+
+ if ! /usr/bin/timeout --kill-after=1s 3s /usr/bin/supergfxctl -m "$expected"; then
+ echo "Could not request the GPU transition to $expected mode" >&2
+ return 1
+ fi
+ for _ in {1..10}; do
+ if current=$(/usr/bin/timeout --kill-after=1s 2s /usr/bin/supergfxctl -g 2>/dev/null) &&
+ [[ $current == "$expected" ]]; then
+ return 0
+ fi
+ sleep 1
+ done
+
+ echo "Could not confirm the GPU transition to $expected mode" >&2
+ return 1
+}
+
+case "$1" in
+ pre)
+ # Remember the mode this sleep cycle started in. supergfxctl persists the
+ # temporary hibernate switch to Vfio, so post must not consult that mutable
+ # value when deciding whether to restore Integrated mode.
+ if [[ -L $restore_marker ]]; then
+ exit 1
+ elif [[ ! -f $restore_marker ]]; then
+ /usr/bin/grep -Eq '"mode"[[:space:]]*:[[:space:]]*"Integrated"' /etc/supergfxd.conf 2>/dev/null || exit 0
+ /usr/bin/install -m 0600 -o root -g root -T /dev/null "$restore_marker"
+ fi
+
+ # Before hibernating, switch to Vfio so the nvidia driver is detached from the dGPU.
+ # Without this, hibernate resume fails because the nvidia driver can't freeze a
+ # powered-off dGPU (returns -EIO), which aborts the entire resume.
+ if [[ $sleep_action == "hibernate" ]]; then
+ switch_mode Vfio
+ fi
+ ;;
+ post)
+ [[ -f $restore_marker && ! -L $restore_marker ]] || exit 0
+
+ # small delay so the device is fully re-enumerated
+ sleep 4
+
+ # force-bind dGPU to vfio (fully detached from nvidia)
+ switch_mode Vfio
+
+ # then go back to Integrated, which powers it off again
+ switch_mode Integrated
+ /usr/bin/rm -f -- "$restore_marker"
+ ;;
+esac
diff --git a/default/systemd/system-sleep/keyboard-backlight b/default/systemd/system-sleep/keyboard-backlight
new file mode 100644
index 0000000..014ab4a
--- /dev/null
+++ b/default/systemd/system-sleep/keyboard-backlight
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Turn off keyboard backlight before hibernate to prevent hang on power-off.
+# The ASUS keyboard controller can block S4 shutdown if LEDs are active.
+
+sleep_action=${SYSTEMD_SLEEP_ACTION:-$2}
+
+if [[ $1 == "pre" && $sleep_action == "hibernate" ]]; then
+ device=""
+ for candidate in /sys/class/leds/*kbd_backlight*; do
+ if [[ -e "$candidate" ]]; then
+ device="$(basename "$candidate")"
+ break
+ fi
+ done
+
+ if [[ -n "$device" ]]; then
+ brightnessctl -d "$device" set 0 >/dev/null 2>&1
+ fi
+fi
diff --git a/default/systemd/system-sleep/unmount-fuse b/default/systemd/system-sleep/unmount-fuse
new file mode 100644
index 0000000..7c27725
--- /dev/null
+++ b/default/systemd/system-sleep/unmount-fuse
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+# Lazy-unmount gvfsd-fuse filesystems before suspend/hibernate to prevent the
+# kernel's process freeze from timing out. FUSE daemons (like gvfsd-fuse from
+# Nautilus) can block in uninterruptible sleep during freeze, causing suspend
+# to silently fail. After wake, restart gvfs so the FUSE mount is restored.
+
+if [[ $1 == "pre" ]]; then
+ while IFS=' ' read -r _ mountpoint fstype _; do
+ if [[ $fstype == fuse.gvfsd-fuse ]]; then
+ mountpoint=$(printf '%b' "$mountpoint")
+ fusermount3 -uz "$mountpoint" 2>/dev/null || fusermount -uz "$mountpoint" 2>/dev/null || true
+ fi
+ done < /proc/mounts
+fi
+
+if [[ $1 == "post" ]]; then
+ # Run in background — user.slice is still frozen at this point, so a
+ # synchronous restart would block the thaw for up to 90 seconds.
+ (
+ sleep 5
+ for uid_dir in /run/user/*; do
+ uid=$(basename "$uid_dir")
+ if [[ -S $uid_dir/bus ]]; then
+ sudo -u "#$uid" env \
+ DBUS_SESSION_BUS_ADDRESS="unix:path=$uid_dir/bus" \
+ XDG_RUNTIME_DIR="$uid_dir" \
+ systemctl --user restart gvfs-daemon.service 2>/dev/null || true
+ fi
+ done
+ ) &
+fi
diff --git a/default/systemd/system/plocate-updatedb.service.d/10-blob.conf b/default/systemd/system/plocate-updatedb.service.d/10-blob.conf
new file mode 100644
index 0000000..571a53b
--- /dev/null
+++ b/default/systemd/system/plocate-updatedb.service.d/10-blob.conf
@@ -0,0 +1,3 @@
+[Service]
+ExecStart=
+ExecStart=/usr/bin/updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots
diff --git a/default/systemd/system/supergfxd.service.d/delay-start.conf b/default/systemd/system/supergfxd.service.d/delay-start.conf
new file mode 100644
index 0000000..19fc298
--- /dev/null
+++ b/default/systemd/system/supergfxd.service.d/delay-start.conf
@@ -0,0 +1,6 @@
+[Service]
+# Delay startup to avoid race condition with display manager initialization
+# when booting in Integrated mode. Without this delay, the system can freeze
+# on boot because supergfxd tries to disable the dGPU while the display
+# subsystem is still initializing.
+ExecStartPre=/bin/sleep 5
diff --git a/default/systemd/user/app.slice.d/10-oomd.conf b/default/systemd/user/app.slice.d/10-oomd.conf
new file mode 100644
index 0000000..22faec8
--- /dev/null
+++ b/default/systemd/user/app.slice.d/10-oomd.conf
@@ -0,0 +1,16 @@
+# Make user apps the only thing systemd-oomd is allowed to kill.
+#
+# Hyprland runs in session.slice, as wayland-wm@hyprland.desktop.service, while
+# everything launched through uwsm-app lands in app.slice/app-*.scope. Marking
+# only app.slice as a kill candidate means the compositor is structurally
+# ineligible: oomd takes the browser or terminal that caused the pressure, and
+# the session survives to show the notification about it. Setting this on
+# user@.service instead would put the compositor back in the candidate pool.
+#
+# Swap kill is a backstop for the slower shape of the same problem, where swap
+# fills before pressure spikes. It uses the global SwapUsedLimit (90%) from
+# /etc/systemd/oomd.conf.d/10-blob.conf, which also carries the pressure
+# thresholds.
+[Slice]
+ManagedOOMMemoryPressure=kill
+ManagedOOMSwap=kill
diff --git a/default/systemd/user/blob-crash-watch.service b/default/systemd/user/blob-crash-watch.service
new file mode 100644
index 0000000..da6906b
--- /dev/null
+++ b/default/systemd/user/blob-crash-watch.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=Announce process crashes and offer an AI diagnosis
+# Needs the session bus to notify, and uwsm-app to open the diagnosis terminal.
+# Both are up only after graphical-session.target.
+After=graphical-session.target
+PartOf=graphical-session.target
+ConditionEnvironment=WAYLAND_DISPLAY
+# Set by blob-toggle-crash-capture. Checked here so a disabled watcher stays
+# disabled across logins without the unit having to be disabled.
+ConditionPathExists=!%h/.local/state/blob/toggles/crash-capture-off
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/blob-crash-watch
+Restart=always
+RestartSec=5
+
+[Install]
+WantedBy=graphical-session.target
diff --git a/default/systemd/user/blob-fcitx5.service b/default/systemd/user/blob-fcitx5.service
new file mode 100644
index 0000000..9cc4521
--- /dev/null
+++ b/default/systemd/user/blob-fcitx5.service
@@ -0,0 +1,31 @@
+[Unit]
+Description=Fcitx5 input method (XCompose sequences)
+# fcitx5 turns the CapsLock compose sequences in ~/.XCompose into text for
+# Wayland clients.
+#
+# Wait for the compositor: fcitx5 needs WAYLAND_DISPLAY and DISPLAY, which uwsm
+# imports into the user manager before it reaches graphical-session.target.
+After=graphical-session.target
+# The wayland connection dies with the compositor, so follow the session rather
+# than linger against a socket that is gone.
+PartOf=graphical-session.target
+# After= is ordering only -- it does not stop anything from starting this unit
+# while the target is inactive. An `blob update` over SSH has a live user
+# manager (pam_systemd) and no graphical session, and a fcitx5 started there
+# comes up with no WAYLAND_DISPLAY and no way to reach any client. Worse, it
+# stays active, so the later graphical-session.target activation won't pull in a
+# working one -- Wants= does not restart what is already running. Skip the start
+# instead; the unit stays enabled and starts for real at graphical login.
+ConditionEnvironment=WAYLAND_DISPLAY
+
+[Service]
+Type=simple
+# notificationitem duplicates the tray entry blob already renders itself.
+ExecStart=/usr/bin/fcitx5 --disable notificationitem
+# always, not on-failure: fcitx5 exits 0 when it detects another instance owning
+# its bus name, and a clean exit still leaves the user with no input method.
+Restart=always
+RestartSec=2
+
+[Install]
+WantedBy=graphical-session.target
diff --git a/default/systemd/user/blob-sleep-lock.service b/default/systemd/user/blob-sleep-lock.service
new file mode 100644
index 0000000..377975c
--- /dev/null
+++ b/default/systemd/user/blob-sleep-lock.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=Lock Blob before suspend
+# The monitor calls into the running Blob shell. Wait until UWSM has imported
+# BLOB_PATH and WAYLAND_DISPLAY, but keep the default target ordering so the
+# monitor starts before graphical-session.target is reached.
+After=dbus.socket wayland-session-waitenv.service
+Requires=dbus.socket
+PartOf=graphical-session.target
+ConditionEnvironment=BLOB_PATH
+ConditionEnvironment=WAYLAND_DISPLAY
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/blob-system-sleep-monitor
+Restart=always
+RestartSec=2
+
+[Install]
+WantedBy=graphical-session.target
diff --git a/default/systemd/user/blob-speaker-tuning.service b/default/systemd/user/blob-speaker-tuning.service
new file mode 100644
index 0000000..c7a1387
--- /dev/null
+++ b/default/systemd/user/blob-speaker-tuning.service
@@ -0,0 +1,32 @@
+[Unit]
+Description=Blob speaker tuning filter-chain
+Documentation=https://github.com/basecamp/omarchy/blob/master/docs/AUDIO-TUNING.md
+# WirePlumber does the linking, so starting before it is up risks the output being
+# linked before the speaker device has been discovered.
+After=pipewire.service wireplumber.service
+Requires=pipewire.service
+Wants=wireplumber.service
+# Restart with the audio daemon, since the filter-chain loses its connection when
+# PipeWire goes away.
+PartOf=pipewire.service
+
+[Service]
+Type=simple
+# Hosts the tuning as a PipeWire *client* rather than loading it into the daemon
+# from pipewire.conf.d, which is only read at daemon startup. That is what lets
+# the tuning be switched on and off without restarting pipewire-pulse -- a
+# restart drops every PulseAudio client's connection, and applications that do
+# not reconnect (Spotify) have to be restarted by hand.
+#
+# It also contains failure: a malformed tuning breaks only this service, where a
+# bad drop-in in the daemon's own config stops PipeWire from starting at all.
+#
+# The config name is deliberately not PipeWire's stock filter-chain.conf, which
+# merges every fragment in ~/.config/pipewire/filter-chain.conf.d/ and would make
+# this service host unrelated user filters too.
+ExecStart=/usr/bin/pipewire -c blob-speaker-tuning.conf
+Restart=on-failure
+RestartSec=2
+
+[Install]
+WantedBy=graphical-session.target
diff --git a/default/systemd/user/bt-agent.service b/default/systemd/user/bt-agent.service
new file mode 100644
index 0000000..4c0117f
--- /dev/null
+++ b/default/systemd/user/bt-agent.service
@@ -0,0 +1,23 @@
+[Unit]
+Description=Bluetooth pairing agent (auto-accept)
+Documentation=man:bt-agent(1)
+ConditionPathIsDirectory=/sys/class/bluetooth
+# bluez must be reachable on the system bus before we can register.
+After=dbus.socket
+Requires=dbus.socket
+
+[Service]
+Type=simple
+# If bluetoothd is unavailable (for example in VMs or machines without a
+# usable adapter), skip cleanly instead of entering a restart loop.
+ExecCondition=/usr/bin/systemctl is-active --quiet bluetooth.service
+# NoInputNoOutput auto-accepts pair requests. Safe because the adapter
+# is only `pairable: true` when the user explicitly opens the blob
+# bluetoothPanel and starts scanning; outside that window bluez refuses
+# inbound pair attempts at a lower layer.
+ExecStart=/usr/bin/bt-agent -c NoInputNoOutput
+Restart=on-failure
+RestartSec=2
+
+[Install]
+WantedBy=graphical-session.target
diff --git a/default/systemd/user@.service.d/faster-shutdown.conf b/default/systemd/user@.service.d/faster-shutdown.conf
new file mode 100644
index 0000000..449242c
--- /dev/null
+++ b/default/systemd/user@.service.d/faster-shutdown.conf
@@ -0,0 +1,2 @@
+[Service]
+TimeoutStopSec=5s
diff --git a/themed/btop.theme.tpl b/default/themed/btop.theme.tpl
similarity index 100%
rename from themed/btop.theme.tpl
rename to default/themed/btop.theme.tpl
diff --git a/themed/chromium.theme.tpl b/default/themed/chromium.theme.tpl
similarity index 100%
rename from themed/chromium.theme.tpl
rename to default/themed/chromium.theme.tpl
diff --git a/themed/foot.ini.tpl b/default/themed/foot.ini.tpl
similarity index 100%
rename from themed/foot.ini.tpl
rename to default/themed/foot.ini.tpl
diff --git a/themed/hyprland-preview-share-picker.css.tpl b/default/themed/hyprland-preview-share-picker.css.tpl
similarity index 100%
rename from themed/hyprland-preview-share-picker.css.tpl
rename to default/themed/hyprland-preview-share-picker.css.tpl
diff --git a/themed/hyprland.lua.tpl b/default/themed/hyprland.lua.tpl
similarity index 100%
rename from themed/hyprland.lua.tpl
rename to default/themed/hyprland.lua.tpl
diff --git a/themed/kitty.conf.tpl b/default/themed/kitty.conf.tpl
similarity index 100%
rename from themed/kitty.conf.tpl
rename to default/themed/kitty.conf.tpl
diff --git a/themed/neovim.lua.tpl b/default/themed/neovim.lua.tpl
similarity index 100%
rename from themed/neovim.lua.tpl
rename to default/themed/neovim.lua.tpl
diff --git a/themed/shell.toml.tpl b/default/themed/shell.toml.tpl
similarity index 100%
rename from themed/shell.toml.tpl
rename to default/themed/shell.toml.tpl
diff --git a/themed/zen.css.tpl b/default/themed/zen.css.tpl
similarity index 100%
rename from themed/zen.css.tpl
rename to default/themed/zen.css.tpl
diff --git a/docs/commands.md b/docs/commands.md
new file mode 100644
index 0000000..27e8906
--- /dev/null
+++ b/docs/commands.md
@@ -0,0 +1,559 @@
+# Commands
+
+Generated by `blob-docs-commands` from the `# blob:summary=` line in each
+file. Do not edit by hand.
+
+Commands marked hidden are plumbing other commands call, and are left out
+of the `blob` listing.
+
+
+## audio
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-audio-availability` | Print PulseAudio sink availability for the shell | - |
+| `blob-audio-input-mute` | Toggle microphone mute. Drives the hardware mic-mute LED on laptops that expose one. | - |
+| `blob-audio-input-set` | Set the default audio input and move active streams | |
+| `blob-audio-restart` | Restart audio services and recover stuck USB audio devices. | - |
+| `blob-audio-sink` | Print the sink whose volume and mute a given output really uses | [sink-name] |
+| `blob-audio-sink-set` | Set the default audio output and move active streams | |
+| `blob-audio-sink-switch` | Switch between audio outputs while preserving the mute status | - |
+| `blob-audio-source-switch` | Cycle to the next media source and transfer playback when the current source is playing | [next\|previous] |
+| `blob-audio-tuning` | Manage the speaker tuning for this laptop | [--force] |
+| `blob-audio-volume` | Adjust output volume and show the Blob OSD | |
+
+## bar
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-bar` | Configure the bar and its widget layout | use \| reset \| defaults \| position \| transparent \| put [placement] \| move [placement] \| set [--json] [placement] |
+| `blob-bar-color` | Choose a legible transparent bar text color (hidden) | - |
+
+## battery
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-battery-low` | Send the low battery warning notification and run battery-low hooks. (hidden) | |
+| `blob-battery-status` | Returns a formatted battery status string with percentage and power draw/charge. | [--shell] |
+
+## bg
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-bg-cache` | Cache background switcher thumbnails for the current theme | - |
+| `blob-bg-current` | Show current background | - |
+| `blob-bg-install` | Open the current theme's user background folder | - |
+| `blob-bg-next` | Cycle to the next background for the current theme | - |
+| `blob-bg-set` | Set the current background image | |
+| `blob-bg-switcher` | Open the Blob background switcher | - |
+
+## bluetooth
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-bluetooth-device` | Control a Bluetooth device | [pair\|connect\|disconnect\|forget] |
+| `blob-bluetooth-power` | Turn Bluetooth on or off, remembered across reboots | |
+| `blob-bluetooth-restart` | Unblock and restart the bluetooth service. | - |
+
+## boot
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-boot` | - | - |
+
+## branding
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-branding-about` | Edit, set, or reset About branding | |
+| `blob-branding-screensaver` | Edit, set, or reset screensaver branding | |
+
+## brightness
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-brightness-ddc` | Show or adjust DDC/CI display brightness for a Hyprland monitor. | [+N%\|N%-\|N%] |
+| `blob-brightness-display` | Show or adjust brightness on the focused display. | [--no-osd] [--monitor name] [+N%\|N%-\|N%\|off\|on] |
+| `blob-brightness-display-apple` | Show or adjust Apple Studio Display and Apple XDR Display brightness using asdcontrol. | [--no-osd] [+N%\|N%-\|N%] |
+| `blob-brightness-keyboard` | Adjust keyboard backlight brightness using available steps. | [--no-osd] |
+| `blob-brightness-keyboard-mute` | Set the mic-mute indicator LED on laptops that expose a platform::micmute LED node. | |
+
+## capture
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-capture-qr` | Decode a QR code from a screenshot region | - |
+| `blob-capture-record` | Start or stop screen recording | [--fullscreen] [--with-desktop-audio] [--with-microphone-audio] [--with-webcam] [--webcam-device=] [--webcam-size=] [--resolution=] [--stop-recording] |
+| `blob-capture-record-webcam` | Pick a webcam and start a screen recording with it | - |
+| `blob-capture-region` | Pick a screen region over frozen screen content (hidden) | [region\|windows\|smart\|fullscreen] [--keep-freeze] [--match-monitor] \| --take-fullscreen \| --take-window \| --select-window |
+| `blob-capture-screenshot` | Take a screenshot | [smart\|region\|windows\|fullscreen] [slurp\|copy\|save] [--editor=] |
+| `blob-capture-text` | Extract text from a screenshot region with OCR | - |
+| `blob-capture-webcam-list` | List webcam devices that support video capture (hidden) | - |
+| `blob-capture-webcam-resize` | Resize the active webcam recording overlay | |
+
+## clipboard
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-clipboard-file` | Copy a file to the clipboard and paste it (hidden) | [--copy-only] |
+| `blob-clipboard-open` | Open a clipboard history entry (hidden) | --history-index |
+| `blob-clipboard-text` | Copy text to the clipboard and type or paste it (hidden) | [--shift-insert] [--copy-only] [--history-index \|] |
+
+## cmd
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-cmd-cwd` | Print the current working directory of the active terminal window (hidden) | - |
+| `blob-cmd-missing` | Check whether any required commands are missing | - |
+| `blob-cmd-present` | Check whether all required commands are available | - |
+
+## crash
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-crash-watch` | Watch for process crashes and offer an AI diagnosis (hidden) | - |
+
+## default
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-default-browser` | Set the default browser for Blob and XDG handlers | [chromium\|chrome\|brave\|brave-origin\|edge\|firefox\|zen] |
+| `blob-default-editor` | Set the default editor used by blob-launch-editor | [code\|cursor\|zed\|sublime_text\|helix\|vim\|emacs\|nvim] |
+| `blob-default-terminal` | Set the default terminal used by xdg-terminal-exec | [alacritty\|foot\|ghostty\|kitty] |
+
+## disk
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-disk-speedtest` | Measure live disk read and write speed | [target-dir] |
+
+## display
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-display-size` | Scale text everywhere — blob shell, GTK apps, and terminals | [size\|reset] |
+| `blob-display-state` | Print monitor panel state for the shell | - |
+
+## docs
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-docs-commands` | Regenerate docs/commands.md from each command's own metadata (hidden) | - |
+| `blob-docs-keybinds` | Regenerate docs/keybinds.md from the Hyprland Lua bindings (hidden) | - |
+
+## drive
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-drive-info` | Print drive information such as size, model, and mount details | |
+| `blob-drive-password` | Set a new encryption password for a drive selected. | - |
+| `blob-drive-select` | Select a drive from a list with info that includes space and brand. Used by blob-drive-password. | - |
+
+## file
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-file-select` | Pick files with the desktop file chooser | [--title ] [--multiple] [--directory] [--extensions ""] |
+
+## font
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-font-current` | Show current monospace font | - |
+| `blob-font-list` | List available monospace fonts | - |
+| `blob-font-set` | Set the system monospace font | |
+
+## git
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-git-url-check` | Check that a git URL names a repository, not a transport helper (hidden) | |
+
+## hibernation
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-hibernation-available` | Check if hibernation is supported | - |
+
+## hook
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-hook` | Run a named hook from ~/.config/blob/hooks/ and ~/.config/blob/hooks/.d/. | [name] [args...] |
+
+## hw
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-hw-clamshell` | Returns true when clamshell mode is active (hidden) | - |
+| `blob-hw-dell-xps-haptic-touchpad` | Match Dell XPS systems with the Synaptics haptic touchpad. | - |
+| `blob-hw-display` | Print the most likely display backlight device. | - |
+| `blob-hw-external` | Returns true when an external monitor is physically connected. | - |
+| `blob-hw-fingerprint` | Returns true when a fingerprint reader is present (hidden) | - |
+| `blob-hw-hybrid-gpu` | Detect whether the system has an active hybrid GPU configuration | - |
+| `blob-hw-laptop` | Returns true when running on a laptop (has a lid or laptop chassis). | - |
+| `blob-hw-laptop-closed` | Returns true when the laptop lid is closed (hidden) | - |
+| `blob-hw-match` | Match against the computer's DMI product name or product family (case-insensitive). | |
+| `blob-hw-nvidia` | Detect whether the computer has an NVIDIA GPU. | - |
+| `blob-hw-nvidia-gsp` | Detect whether the computer has an NVIDIA GPU with GSP firmware (Turing or newer). | - |
+| `blob-hw-nvidia-without-gsp` | Detect whether the computer has an NVIDIA GPU without GSP firmware (Maxwell/Pascal/Volta). | - |
+| `blob-hw-touchpad` | Print the detected Hyprland touchpad or trackpad device name | - |
+| `blob-hw-touchscreen` | Print the detected Hyprland touchscreen or tablet device name | - |
+| `blob-hw-webcam` | Check whether a webcam is available | - |
+
+## hypr
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-hypr-focus` | Focus a Hyprland window by application identity | |
+| `blob-hypr-monitor-clamshell` | Apply clamshell display state to Hyprland monitors (hidden) | - |
+| `blob-hypr-monitor-external` | Returns true when Hyprland has an active external monitor (hidden) | - |
+| `blob-hypr-monitor-focused` | Print the name of the currently focused Hyprland monitor. | - |
+| `blob-hypr-monitor-focused-apple` | Return success if the focused or named Hyprland monitor is an Apple display. | [monitor] |
+| `blob-hypr-monitor-internal` | Enable, disable, toggle, or recover the internal laptop display | |
+| `blob-hypr-monitor-laptop` | Print the name of the built-in laptop display, including disabled outputs. | - |
+| `blob-hypr-monitor-mirror` | Enable, disable, toggle, or recover mirroring the internal display to an external monitor | |
+| `blob-hypr-monitor-modeless` | Returns true when Hyprland has an enabled monitor with no mode (hidden) | - |
+| `blob-hypr-monitor-scaling` | Show, set, or adjust focused Hyprland monitor scaling | [up\|down\|SCALE] |
+| `blob-hypr-monitor-watch` | Watch Hyprland monitor events and recover monitor toggles when a monitor is removed | - |
+| `blob-hypr-reload-guard` | Pause or resume Hyprland config auto-reload around package transactions. (hidden) | - |
+| `blob-hypr-restart` | Reload hyprland configuration (used by the Blob theme switching). | - |
+| `blob-hypr-session-locked` | Returns true when the compositor holds a session lock (hidden) | - |
+| `blob-hypr-toggle` | Toggle permanent Hyprland flags by copying them into a directory that's sourced entirely. | [on\|off\|toggle] |
+| `blob-hypr-toggle-disabled` | Check if a Hyprland toggle is currently disabled (missing). | |
+| `blob-hypr-toggle-enabled` | Check if a Hyprland toggle is currently enabled. | |
+| `blob-hypr-window-close-all` | Close all open windows | - |
+| `blob-hypr-window-gaps-toggle` | Toggles the window gaps globally between no gaps and the default. | - |
+| `blob-hypr-window-pop` | Toggle to pop-out a tile to stay fixed on a display basis. | [width height x y] |
+| `blob-hypr-window-single-square-aspect-toggle` | Toggle single-window square aspect ratio. | - |
+| `blob-hypr-window-tiled-fullscreen-toggle` | Toggle tiled fullscreen for the focused Hyprland window | - |
+| `blob-hypr-window-transparency-toggle` | Toggles transparency for the currently focused window. | - |
+| `blob-hypr-window-width` | Save or restore the focused Hyprland window width | |
+| `blob-hypr-workspace-layout-toggle` | Toggle the layout on the current active workspace between dwindle and scrolling | - |
+
+## install
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-install-font` | Install a Nerd Font package and switch the system to it | |
+| `blob-install-launch` | Install a packaged app and launch it once it finishes | |
+
+## launch
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-launch-about` | Launch the fastfetch TUI that gives information about the current system. | - |
+| `blob-launch-browser` | Launch the default browser as determined by xdg-settings. | [url] |
+| `blob-launch-config-editor` | Open a config file in the user's editor and surface a toast | |
+| `blob-launch-docker-tui` | Open the Docker TUI (lazydocker) with access to the Docker daemon (hidden) | - |
+| `blob-launch-editor` | Launch the default editor selected via Blob defaults. | [--inline] |
+
+## launcher
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-launcher-remove` | Remove or uninstall the selected launcher entry (hidden) | |
+
+## launch
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-launch-floating` | Launch a floating terminal with the Blob presentation wrapper | |
+| `blob-launch-or-focus` | Launch an app or focus an existing window matching a pattern | |
+| `blob-launch-or-focus-tui` | Launch a TUI or focus an existing terminal window for it | [--app-id=] [args...] |
+| `blob-launch-screensaver` | Launch the Blob screensaver in the default terminal on the system with the correct font configuration. | - |
+| `blob-launch-shell` | Launch the Blob shell with its log kept in the journal (hidden) | - |
+| `blob-launch-tui` | Launch a TUI command in the default terminal with Blob styling | [--app-id=] [args...] |
+
+## menu
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-menu` | Control the Blob menu (toggle / summon / close / refresh) | [toggle\|summon\|close\|refresh\|ping] [route] |
+| `blob-menu-emoji` | Launch emojis | - |
+| `blob-menu-emoji-insert` | Insert an emoji into the focused application (hidden) | |
+| `blob-menu-file` | Pick a file from a menu | label paths formats [menu args...] |
+| `blob-menu-images` | Open a generic image selector menu | [--selected ] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--preload] [--cache-only] ... |
+| `blob-menu-keybindings` | Display Hyprland keybindings defined in your configuration using an interactive search menu. | - |
+| `blob-menu-plugin` | Pick a shell plugin to enable, disable, clone, or remove | |
+| `blob-menu-select` | Pick one option from a menu | prompt [option...] [-- menu args...] |
+| `blob-menu-share` | Share clipboard, files, or folders with LocalSend | [path...] |
+| `blob-menu-timezone` | Select and set the system timezone | - |
+
+## network
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-network-band` | Show or pin the Wi-Fi band for the active connection | [auto\|2.4\|5\|6] |
+| `blob-network-dns` | Show or configure the system DNS provider | [Cloudflare\|Google\|DHCP\|Custom] |
+| `blob-network-iwd` | - | - |
+| `blob-network-password` | Print the active Wi-Fi connection's password | |
+| `blob-network-qr` | Generate a Wi-Fi QR matrix for the shell | [--meta] [interface] |
+| `blob-network-restart` | Unblock and restart the Wi-Fi service. | - |
+| `blob-network-speedtest` | Measure live internet speed for one direction | [down\|up] |
+| `blob-network-status` | Print active network status for the shell | [--verbose] |
+
+## notification
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-notification-battery` | Show the current battery status notification | - |
+| `blob-notification-time` | Show the current time and date notification | - |
+| `blob-notification-wait` | Wait for the desktop notification server to accept notifications (hidden) | [timeout-seconds] |
+| `blob-notification-weather` | Toggle the current weather panel | - |
+
+## notify
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-notify-dismiss` | Dismiss a notification by summary substring. Used by the first-run notifications to dismiss them after clicking for action. | |
+| `blob-notify-send` | Send an Blob desktop notification | [--app-name ] [-g ] [-u ] [-i ] [-t ] [-r ] [-p] [--image ] [description] [--exec [args...]] |
+
+## osd
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-osd` | Show the Blob Quickshell on-screen display | [-i\|--icon ] [-m\|--message ] [-p\|--progress <0-100>] [-d\|--duration ] |
+
+## pkg
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-pkg-add` | Install Arch packages if they are missing | |
+| `blob-pkg-aur` | Returns true if the AUR is up and available. | - |
+| `blob-pkg-aur-install` | Show a fuzzy-finder TUI for picking new AUR packages to install. | - |
+| `blob-pkg-drop` | Remove all the named packages from the system if they're installed (otherwise ignore). | |
+| `blob-pkg-install` | Show a fuzzy-finder TUI for picking new Arch and OPR packages to install. | - |
+| `blob-pkg-missing` | Returns true if any of the named packages are missing from the system (or false if they're all there). | |
+| `blob-pkg-present` | Returns true if all of the named packages are installed on the system (or false if any of them are missing). | |
+| `blob-pkg-remove` | Show a fuzzy-finder TUI for picking packages installed on the system to be removed. | - |
+
+## plugin
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-plugin-add` | Add a shell plugin from git | [git-url] [--enable] [--yes] |
+| `blob-plugin-catalog` | Emit every first-party and user plugin manifest as JSON (hidden) | - |
+| `blob-plugin-clone` | Clone a built-in Blob shell plugin into your own config | [--edit] |
+| `blob-plugin-enable` | Enable a shell plugin | [placement] |
+| `blob-plugin-list` | List discovered shell plugins | [--json] |
+| `blob-plugin-remove` | Remove an installed shell plugin | [id] [--yes] |
+| `blob-plugin-validate` | Validate a plugin folder against the Blob plugin manifest schema | |
+
+## plymouth
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-plymouth-set` | Set the Plymouth boot theme colors and logo | |
+
+## power
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-power-list` | Returns a list of all the available power profiles on the system. | [--active-state] |
+
+## powerprofiles
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-powerprofiles-init` | Set the correct power profile on boot based on current AC/battery state. | - |
+
+## power
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-power-set` | Set and remember the power profile for AC or battery use | [autodetect\|ac\|battery] [power-saver\|balanced\|performance] |
+
+## refresh
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-refresh-config` | Copy a shipped user config from $BLOB_PATH/config into ~/.config (backs up your version). | |
+| `blob-refresh-hyprland` | Overwrite all the user Hyprland Lua configs in ~/.config/hypr with the Blob defaults. | - |
+| `blob-refresh-hyprsunset` | Overwrite the user config for hyprsunset with the Blob default and restart the service. | - |
+| `blob-refresh-plymouth` | Overwrite the user config for the Plymouth drive decryption and boot sequence with the Blob default and rebuild it. | - |
+| `blob-refresh-shell` | Reset shell.json to Blob defaults | - |
+
+## reminder
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-reminder` | Set and show lightweight desktop notification reminders | [-i\|--interactive] \| [message] \| show [-j\|--json] \| clear |
+
+## remove
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-remove-security-fido2` | Remove FIDO2 authentication from sudo and polkit | - |
+| `blob-remove-security-sudoless-docker` | Disable sudoless Docker by removing your user from the docker group | - |
+
+## restart
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-restart-app` | Restart an application by killing it and relaunching via uwsm. | [application-args...] |
+| `blob-restart-btop` | Reload btop configuration (used by the Blob theme switching). | - |
+| `blob-restart-gum` | Export the current theme's gum styling into the environment (hidden) | - |
+| `blob-restart-hyprsunset` | Restart the hyprsunset service (used for blue light filtering/night light). | - |
+| `blob-restart-terminal` | Reload supported terminal emulators after config changes | - |
+| `blob-restart-trackpad` | Reset the trackpad by unbinding and rebinding its driver. | - |
+| `blob-restart-xcompose` | Restart the XCompose input method service (fcitx5) to apply new compose key settings. | - |
+
+## screensaver
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-screensaver` | Run the Blob screensaver using random effects from TTE. | - |
+
+## secret
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-secret` | - | - |
+
+## setup
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-setup-direct-boot` | Add or remove an EFI boot entry for the Blob UKI, allowing the system to boot directly | - |
+| `blob-setup-security-fido2` | Set up FIDO2 authentication for sudo and polkit | - |
+| `blob-setup-security-fingerprint` | Set up fingerprint authentication for sudo, polkit, and lock screen | - |
+| `blob-setup-security-sshd` | Set up the OpenSSH server, open the firewall, and authorize an SSH key | [--key=] |
+| `blob-setup-security-sudoless-docker` | Enable sudoless Docker by adding your user to the docker group (root-equivalent!) | - |
+
+## shell
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-shell` | Send an IPC call to the running Blob shell | [-q] [args...] |
+| `blob-shell-config` | Shared helpers for editing ~/.config/blob/shell.json (source this, don't run it). (hidden) | - |
+| `blob-shell-restart` | Restart the Blob shell | - |
+
+## show
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-show-done` | Display a "Done!" message and wait for user to press any key. | - |
+| `blob-show-logo` | Display the Blob logo in the terminal using green color. | - |
+
+## state
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-state` | Manage persistent state files for Blob toggles and settings. (hidden) | |
+
+## sudo
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-sudo-docker` | Succeed when Docker needs sudo, fail when it can be used directly (hidden) | [--configured] |
+| `blob-sudo-keepalive` | Prompt for sudo once and keep the credential alive in the background. | - |
+| `blob-sudo-passwordless` | Toggle passwordless sudo for the current user. | [MINUTES] |
+
+## system
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-system-lid` | Lock and reconcile displays when the laptop lid closes (hidden) | - |
+| `blob-system-lock` | Lock the computer and turn off the display | - |
+| `blob-system-logout` | Log out after closing application windows | - |
+| `blob-system-reboot` | Reboot after closing application windows | - |
+| `blob-system-shutdown` | Shut down after closing application windows | - |
+| `blob-system-sleep` | Lock before suspend and wait for the session lock to become secure (hidden) | - |
+| `blob-system-sleep-monitor` | Monitor sleep preparation and lock before suspend (hidden) | - |
+| `blob-system-stats` | Print CPU and memory stats for the shell | [--bar-widget] |
+| `blob-system-wake` | Wake displays and restore brightness after idle | - |
+
+## tablet
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-tablet-follow` | - | - |
+
+## theme
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-theme-browser` | Apply the current theme color to Chromium, Chrome, Edge, and Brave (hidden) | - |
+| `blob-theme-browser-policy` | Write the current theme color into the browser policy directories (hidden) | |
+| `blob-theme-color` | Resolve semantic colors from an Blob theme colors.toml (hidden) | [--file ] (--all \| --raw \| [fallback]) |
+| `blob-theme-contrast` | - | - |
+| `blob-theme-current` | Show current theme | - |
+| `blob-theme-dir` | Print the directory holding a theme, preferring a user-installed copy | |
+| `blob-theme-dynamic` | Apply the pywal-generated theme built from the current wallpaper | - |
+| `blob-theme-extras` | List the user-installed themes that came from a git clone | - |
+| `blob-theme-foot` | Apply current Blob theme colors to running Foot terminals (hidden) | - |
+| `blob-theme-import` | Generate a theme's colors.toml from its alacritty.toml palette (hidden) | |
+| `blob-theme-install` | Install a theme from a git repository | [git-repo-url] |
+| `blob-theme-list` | List available themes | - |
+| `blob-theme-menu` | Pick a color theme from the local and bundled sets | [--mode\|--print] |
+| `blob-theme-osc` | Print OSC sequences for an Blob color theme (hidden) | - |
+| `blob-theme-refresh` | Refresh the current theme from its templates. | - |
+| `blob-theme-remove` | Remove a user-installed theme | [theme-name] |
+| `blob-theme-set` | Apply an Blob theme | |
+| `blob-theme-share` | Fetch a shared theme by link or id and apply it as blob-dynamic | |
+| `blob-theme-switcher` | Open the Blob theme switcher | - |
+| `blob-theme-templates` | Generate themed config files from Blob templates (hidden) | - |
+| `blob-theme-update` | Update user-installed git themes | - |
+
+## toggle
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-toggle` | Toggle Blob features between enabled and disabled | [toggle\|on\|off] |
+| `blob-toggle-bar` | Toggle bar visibility without killing the Blob shell | [toggle\|on\|off] |
+| `blob-toggle-crash-capture` | Toggle crash capture notifications | - |
+| `blob-toggle-enabled` | Check if a toggle is enabled (flag file exists) | |
+| `blob-toggle-glass` | - | - |
+| `blob-toggle-hybrid-gpu` | Toggle dedicated vs integrated GPU mode via supergfxd (for hybrid gpu laptops, like Asus G14). | - |
+| `blob-toggle-idle` | Toggle idle behavior so the system either idles normally or stays awake | [toggle\|stay-awake\|allow-idle\|status] |
+| `blob-toggle-input` | Enable, disable, or toggle a Hyprland input device (hidden) | [on\|off\|toggle] |
+| `blob-toggle-nightlight` | Toggle nightlight screen temperature | [--status] |
+| `blob-toggle-screensaver` | Toggle screensaver availability | - |
+| `blob-toggle-silencing` | Toggle notification do-not-disturb mode | - |
+| `blob-toggle-tablet` | - | - |
+| `blob-toggle-touchpad` | Enable, disable, or toggle the touchpad | [on\|off\|toggle] |
+| `blob-toggle-touchscreen` | Enable, disable, or toggle the touch functionality of the screen | [on\|off\|toggle] |
+
+## transcode
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-transcode` | Transcode pictures and videos for sharing | [--path path] [input] [format] [resolution] |
+| `blob-transcode-ascii` | Transcode an image into ASCII/Unicode art text | [--width ] [--height ] [--mode ] [--threshold ] [--invert] |
+
+## tui
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-tui-install` | Create a desktop launcher for a terminal UI app | [name command window-style icon-url-or-name] |
+| `blob-tui-remove` | Remove a terminal UI desktop launcher | [name] |
+| `blob-tui-remove-all` | Remove all TUIs installed via blob-tui-install. | - |
+
+## update
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-update` | Update system and AUR packages | [--no-confirm] |
+| `blob-update-available` | Print the number of pending package updates (hidden) | - |
+| `blob-update-firmware` | Update system firmware using fwupd. Ensures the fwupd EFI binary is installed | - |
+| `blob-update-time` | Restart system time synchronization | - |
+
+## wallpaper
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-wallpaper-set` | Set a wallpaper from ~/wallpapers and recolor the desktop from it | [--menu\|] |
+
+## weather
+
+| Command | Does | Arguments |
+| --- | --- | --- |
+| `blob-weather-card` | Print a one-line weather summary for the quick settings card (hidden) | - |
+| `blob-weather-icon` | Returns a weather condition icon, adjusted for live sunrise and sunset. | - |
+| `blob-weather-location` | Show or set the location used for weather reports | - |
+| `blob-weather-status` | Returns a formatted weather status string with temperature and wind speed. | - |
+
+## Totals
+
+252 commands.
diff --git a/docs/keybinds.md b/docs/keybinds.md
new file mode 100644
index 0000000..8a19a38
--- /dev/null
+++ b/docs/keybinds.md
@@ -0,0 +1,202 @@
+# Keybindings
+
+Generated by `blob-docs-keybinds` from the `o.bind` calls in the Hyprland
+Lua files. Do not edit by hand.
+
+Personal overrides in `hypr/bindings.lua` win over the defaults, and
+`hl.unbind` in that file removes a default outright.
+
+## Personal
+
+| Keys | Action |
+| --- | --- |
+| `SUPER + ALT + SPACE` | Blob menu |
+| `SUPER + ALT + W` | Wallpaper picker |
+| `SUPER + CTRL + M` | System monitor |
+| `SUPER + CTRL + Q` | Quick settings |
+| `SUPER + SPACE` | Apps menu |
+
+## Applications
+
+| Keys | Action |
+| --- | --- |
+| `SUPER + ALT + SHIFT + F` | File manager (cwd) |
+| `SUPER + RETURN` | Terminal |
+| `SUPER + SHIFT + ALT + B` | Browser (private) |
+| `SUPER + SHIFT + B` | Browser |
+| `SUPER + SHIFT + D` | Docker |
+| `SUPER + SHIFT + F` | File manager |
+| `SUPER + SHIFT + G` | Signal |
+| `SUPER + SHIFT + M` | Music |
+| `SUPER + SHIFT + N` | Editor |
+| `SUPER + SHIFT + O` | Obsidian |
+| `SUPER + SHIFT + RETURN` | Browser |
+
+## Tiling
+
+| Keys | Action |
+| --- | --- |
+| `ALT + SHIFT + TAB` | Focus on previous window |
+| `ALT + TAB` | Focus on next window |
+| `CTRL + ALT + DELETE` | Close all windows |
+| `CTRL + ALT + SHIFT + TAB` | Focus on previous monitor |
+| `CTRL + ALT + TAB` | Focus on next monitor |
+| `SUPER + ALT + code:20` | Expand window left a little |
+| `SUPER + ALT + code:21` | Shrink window left a little |
+| `SUPER + ALT + DOWN` | Move window to group on bottom |
+| `SUPER + ALT + F` | Full width |
+| `SUPER + ALT + G` | Move active window out of group |
+| `SUPER + ALT + Home` | Save window width |
+| `SUPER + ALT + LEFT` | Move window to group on left |
+| `SUPER + ALT + mouse_down` | Next window in group |
+| `SUPER + ALT + mouse_up` | Previous window in group |
+| `SUPER + ALT + RIGHT` | Move window to group on right |
+| `SUPER + ALT + S` | Move window to scratchpad |
+| `SUPER + ALT + SHIFT + TAB` | Previous window in group |
+| `SUPER + ALT + SLASH` | Monitor scaling down |
+| `SUPER + ALT + TAB` | Next window in group |
+| `SUPER + ALT + UP` | Move window to group on top |
+| `SUPER + code:20` | Expand window left |
+| `SUPER + code:21` | Shrink window left |
+| `SUPER + CTRL + code:20` | Expand window left a lot |
+| `SUPER + CTRL + code:21` | Shrink window left a lot |
+| `SUPER + CTRL + F` | Tiled full screen |
+| `SUPER + CTRL + LEFT` | Move grouped window focus left |
+| `SUPER + CTRL + RIGHT` | Move grouped window focus right |
+| `SUPER + CTRL + SHIFT + code:20` | Shrink window up a lot |
+| `SUPER + CTRL + SHIFT + code:21` | Expand window down a lot |
+| `SUPER + CTRL + TAB` | Former workspace |
+| `SUPER + DOWN` | Focus on below window |
+| `SUPER + F` | Full screen |
+| `SUPER + G` | Toggle window grouping |
+| `SUPER + Home` | Restore window width |
+| `SUPER + J` | Toggle window split |
+| `SUPER + L` | Toggle workspace layout |
+| `SUPER + LEFT` | Focus on left window |
+| `SUPER + mouse:272` | Move window |
+| `SUPER + mouse:273` | Resize window |
+| `SUPER + mouse_down` | Scroll active workspace forward |
+| `SUPER + mouse_up` | Scroll active workspace backward |
+| `SUPER + O` | Pop window out (float & pin) |
+| `SUPER + P` | Pseudo window |
+| `SUPER + RIGHT` | Focus on right window |
+| `SUPER + S` | Toggle scratchpad |
+| `SUPER + SHIFT + ALT + code:20` | Shrink window up a little |
+| `SUPER + SHIFT + ALT + code:21` | Expand window down a little |
+| `SUPER + SHIFT + ALT + DOWN` | Move workspace to down monitor |
+| `SUPER + SHIFT + ALT + LEFT` | Move workspace to left monitor |
+| `SUPER + SHIFT + ALT + RIGHT` | Move workspace to right monitor |
+| `SUPER + SHIFT + ALT + UP` | Move workspace to up monitor |
+| `SUPER + SHIFT + code:20` | Shrink window up |
+| `SUPER + SHIFT + code:21` | Expand window down |
+| `SUPER + SHIFT + DOWN` | Swap window down |
+| `SUPER + SHIFT + LEFT` | Swap window to the left |
+| `SUPER + SHIFT + RIGHT` | Swap window to the right |
+| `SUPER + SHIFT + TAB` | Previous workspace |
+| `SUPER + SHIFT + UP` | Swap window up |
+| `SUPER + SLASH` | Monitor scaling up |
+| `SUPER + T` | Toggle window floating/tiling |
+| `SUPER + TAB` | Next workspace |
+| `SUPER + UP` | Focus on above window |
+| `SUPER + W` | Close window |
+
+## Media
+
+| Keys | Action |
+| --- | --- |
+| `ALT + SHIFT + XF86AudioPlay` | Previous track |
+| `ALT + XF86AudioLowerVolume` | Volume down precise |
+| `ALT + XF86AudioPlay` | Next track |
+| `ALT + XF86AudioRaiseVolume` | Volume up precise |
+| `ALT + XF86MonBrightnessDown` | Brightness down precise |
+| `ALT + XF86MonBrightnessUp` | Brightness up precise |
+| `SHIFT + XF86AudioMute` | Switch audio output |
+| `SHIFT + XF86AudioPause` | Switch media source |
+| `SHIFT + XF86AudioPlay` | Switch media source |
+| `SHIFT + XF86MonBrightnessDown` | Brightness minimum |
+| `SHIFT + XF86MonBrightnessUp` | Brightness maximum |
+| `XF86AudioLowerVolume` | Volume down |
+| `XF86AudioMicMute` | Mute microphone |
+| `XF86AudioMute` | Mute |
+| `XF86AudioNext` | Next track |
+| `XF86AudioPause` | Pause |
+| `XF86AudioPlay` | Play |
+| `XF86AudioPrev` | Previous track |
+| `XF86AudioRaiseVolume` | Volume up |
+| `XF86Eject` | Eject media |
+| `XF86KbdBrightnessDown` | Keyboard brightness down |
+| `XF86KbdBrightnessUp` | Keyboard brightness up |
+| `XF86KbdLightOnOff` | Keyboard backlight cycle |
+| `XF86MonBrightnessDown` | Brightness down |
+| `XF86MonBrightnessUp` | Brightness up |
+| `XF86TouchpadOff` | Disable touchpad |
+| `XF86TouchpadOn` | Enable touchpad |
+| `XF86TouchpadToggle` | Toggle touchpad |
+
+## Clipboard
+
+| Keys | Action |
+| --- | --- |
+| `SUPER + C` | Universal copy |
+| `SUPER + CTRL + V` | Clipboard manager |
+| `SUPER + V` | Universal paste |
+| `SUPER + X` | Universal cut |
+
+## Utilities
+
+| Keys | Action |
+| --- | --- |
+| `ALT + PRINT` | Screenrecording |
+| `PRINT` | Screenshot |
+| `SUPER + ALT + code:34` | Make webcam overlay smaller |
+| `SUPER + ALT + code:35` | Make webcam overlay larger |
+| `SUPER + ALT + comma` | Invoke last notification |
+| `SUPER + ALT + SPACE` | Apps menu |
+| `SUPER + BACKSPACE` | Toggle window transparency |
+| `SUPER + comma` | Dismiss last notification |
+| `SUPER + CTRL + A` | Audio |
+| `SUPER + CTRL + ALT + B` | Show battery remaining |
+| `SUPER + CTRL + ALT + D` | Calendar |
+| `SUPER + CTRL + ALT + Delete` | Toggle laptop display mirroring |
+| `SUPER + CTRL + ALT + R` | Show reminders |
+| `SUPER + CTRL + ALT + T` | Show time |
+| `SUPER + CTRL + ALT + W` | Toggle weather |
+| `SUPER + CTRL + ALT + Z` | Reset zoom |
+| `SUPER + CTRL + B` | Bluetooth |
+| `SUPER + CTRL + BACKSPACE` | Toggle single-window square aspect |
+| `SUPER + CTRL + C` | Capture menu |
+| `SUPER + CTRL + comma` | Toggle silencing notifications |
+| `SUPER + CTRL + D` | Display |
+| `SUPER + CTRL + Delete` | Toggle laptop display |
+| `SUPER + CTRL + E` | Emojis |
+| `SUPER + CTRL + H` | Hardware menu |
+| `SUPER + CTRL + I` | Toggle locking on idle |
+| `SUPER + CTRL + L` | Lock system |
+| `SUPER + CTRL + N` | Toggle nightlight |
+| `SUPER + CTRL + O` | Toggle menu |
+| `SUPER + CTRL + P` | Power |
+| `SUPER + CTRL + PERIOD` | Transcode |
+| `SUPER + CTRL + PRINT` | Extract text (OCR) from screenshot |
+| `SUPER + CTRL + R` | Set reminder |
+| `SUPER + CTRL + S` | Share |
+| `SUPER + CTRL + SPACE` | Background switcher |
+| `SUPER + CTRL + T` | Activity |
+| `SUPER + CTRL + W` | Network |
+| `SUPER + CTRL + Z` | Zoom in |
+| `SUPER + ESCAPE` | System menu |
+| `SUPER + K` | Keybindings |
+| `SUPER + PRINT` | Color picker |
+| `SUPER + SHIFT + ALT + comma` | Open notification history |
+| `SUPER + SHIFT + BACKSPACE` | Toggle window gaps |
+| `SUPER + SHIFT + code:201` | Blob menu |
+| `SUPER + SHIFT + comma` | Dismiss all notifications |
+| `SUPER + SHIFT + CTRL + R` | Clear reminders |
+| `SUPER + SHIFT + CTRL + SPACE` | Theme menu |
+| `SUPER + SHIFT + SPACE` | Toggle top bar |
+| `SUPER + SPACE` | Blob menu |
+| `XF86PowerOff` | Power menu |
+
+## Unbound defaults
+
+- `SUPER + SPACE`
+- `SUPER + ALT + SPACE`
diff --git a/docs/shell.md b/docs/shell.md
new file mode 100644
index 0000000..c61e343
--- /dev/null
+++ b/docs/shell.md
@@ -0,0 +1,91 @@
+# The shell
+
+`blob-shell` is one long-running Quickshell process hosting the bar, panels,
+notifications, lock screen, OSD, and both menus. Everything runs inside it as a
+plugin, so summoning a panel is an IPC call into a process that is already up.
+
+## Layout
+
+```
+shell/
+ shell.qml entry point
+ Commons/ Style, Color, Border, Util
+ Ui/ shared components
+ services/ plugin registry, bar widget registry, app library
+ plugins/
+ bar/ the bar and its widgets
+ menu/ both menus
+ notifications/ daemon, popups, DND, history
+ notification-center/
+ lock/ polkit/ osd/ background/ clipboard/ emojis/
+ image-picker/ reminders/
+ panels/ audio bluetooth clock disk-speedtest monitor network
+ power quick-settings speedtest sysmon weather wifiqr
+ services/ battery idle media nightlight
+```
+
+First-party plugins are discovered under `shell/plugins` at a depth of two or
+three and are enabled without being listed anywhere. Third-party plugins live in
+`~/.config/blob/plugins//` and are enabled through `shell.json`.
+
+## Config
+
+`shell.json` is the whole bar config. It hot-reloads on save, and dragging a bar
+widget rewrites `~/.config/blob/shell.json` directly, so after rearranging by
+hand copy it back:
+
+```
+cp ~/.config/blob/shell.json shell.json
+```
+
+`./install.sh --check` reports the drift.
+
+Defaults live at `config/blob/shell.json`. A valid user file replaces them
+entirely rather than merging.
+
+## Bar layout
+
+| Section | Widgets |
+| --- | --- |
+| left | `blob.menu`, `blob.workspaces` |
+| center | `blob.clock` (anchored), `blob.system-update` |
+| right | `blob.tray`, `blob.bluetooth`, `blob.network`, `blob.audio`, `blob.monitor`, `blob.power`, `blob.notifications` |
+
+`blob.clock` and `blob.notifications` are `type: "command"` modules, which is how
+they carry their own click actions: the clock opens quick settings on left click,
+the shell's calendar on middle, and the timezone picker on right; the bell opens
+the notification centre on left and toggles silencing on right.
+
+The centre module named by `centerAnchor` cannot be dragged out of the centre.
+Every other widget still reorders.
+
+## Differences from upstream
+
+This shell was forked from Omarchy 4.0.4 and is not tracked against it. The
+changes beyond renaming:
+
+- `agents`, `tailscale`, `dropbox`, and `dev-gallery` plugins removed, about
+ 7,000 lines.
+- The `Dictation` bar indicator removed with voice typing.
+- Persistent workspaces 1-9 instead of 1-5.
+- Menu cards 440 wide instead of 300, with the Blob icon on the bar button.
+- The lock screen carries `branding/screensaver.txt` and an AGS-style password
+ field: square corners, a 2px border on the blue slot at half alpha going to
+ solid accent once typing starts, and urgent on a failed attempt.
+- `quick-settings`, `sysmon`, and `notification-center` are new, ported from the
+ GTK widgets. See [widgets.md](widgets.md).
+
+None of these are clones or overrides. They are edits to first-party source, so
+there is no clone machinery and nothing to re-apply after an upstream release.
+The fork base is recorded in [upstream.md](upstream.md).
+
+## Debugging
+
+```
+journalctl --user -t blob-shell -f
+blob-shell-restart
+```
+
+A QML error in a `service` plugin means that service never loads. For the lock
+that leaves the machine unlockable rather than locked open, so test it with
+`blob-shell lock lock` before trusting a change to it.
diff --git a/docs/themes.md b/docs/themes.md
new file mode 100644
index 0000000..10299dd
--- /dev/null
+++ b/docs/themes.md
@@ -0,0 +1,68 @@
+# Themes
+
+A theme is a directory under `themes/` holding at minimum a `colors.toml`.
+`blob-theme-set ` stages it, renders every template against it, and
+retints the running desktop.
+
+## What a theme can ship
+
+| File | Used by |
+| --- | --- |
+| `colors.toml` | required; the palette everything else derives from |
+| `backgrounds/` | the background switcher |
+| `shell.lock.toml` | lock screen surface tokens |
+| `neovim.lua` | the editor colorscheme |
+| `icons.theme` | GTK icon theme name |
+| `preview.png`, `unlock.png`, `preview-unlock.png` | theme and lock previews |
+
+## colors.toml
+
+Named keys are preferred: `background`, `foreground`, `accent`, `muted`,
+`selection`, the eight base colors, and their `bright_` variants. The pywal
+`color0`..`color15` form also works and is resolved through the same cascade:
+`color0` is the background, `color4` is blue, `color5` is magenta.
+
+That cascade lives in `blob-theme-color`, and `Commons/Color.qml` mirrors it so
+the shell and the CLI never disagree about what a slot means.
+
+## Templates
+
+`default/themed/*.tpl` are rendered per theme into
+`~/.local/state/blob/current/theme/`. Nine ship: foot, kitty, btop, chromium,
+hyprland, the screenshare picker, neovim, `shell.toml`, and zen.
+
+An app picks its colors up in one of two ways. Most include the generated file
+directly, so nothing has to run:
+
+```
+include ~/.local/state/blob/current/theme/kitty.conf
+```
+
+Zen does the same through a CSS import in `userChrome.css`. The rest are pushed
+by a short applier listed in `post_theme_commands` inside `blob-theme-set`:
+`blob-theme-foot`, `blob-theme-browser`, `blob-restart-terminal`,
+`blob-restart-btop`, `blob-hypr-restart`.
+
+Adding a template for another app means dropping a `.tpl` in `default/themed/`
+and, if it cannot include a file, adding an applier to that list.
+
+## The three ways to set a theme
+
+| Command | Does |
+| --- | --- |
+| `blob theme menu` | pick from the bundled and local sets |
+| `blob theme set ` | apply one by name |
+| `blob wallpaper set ` | extract a palette from a wallpaper with pywal, fix flat palettes with `blob-theme-contrast`, and apply it as `blob-dynamic` |
+| `blob theme share ` | fetch a shared palette and apply it as `blob-dynamic` |
+
+Aether is also installed and does the same job with a GUI. To have its palettes
+land where these do, add Blob as a custom app in Aether with a template writing
+`colors.toml` into `~/.local/state/blob/current/theme/` and a post-apply hook
+calling `blob-theme-refresh`.
+
+## Bundled themes
+
+Twenty-two came across from upstream, plus `flats` and `pitch-dark`. Their
+`backgrounds/` directories are empty on purpose: the upstream images were
+Omarchy branding, and `~/wallpapers` holds 103 of your own. A theme with no
+background of its own leaves the current wallpaper alone.
diff --git a/hooks/battery-low.sample b/hooks/battery-low.sample
new file mode 100644
index 0000000..024fd0b
--- /dev/null
+++ b/hooks/battery-low.sample
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# This hook is called with the current battery percentage when the low battery
+# notification is sent. To put it into use, remove .sample from the name.
+
+SOUND_FILE="/usr/share/sounds/freedesktop/stereo/dialog-warning.oga"
+
+if blob-cmd-present mpv && [[ -f $SOUND_FILE ]]; then
+ mpv --no-video "$SOUND_FILE" >/dev/null 2>&1
+fi
diff --git a/hooks/font-set.sample b/hooks/font-set.sample
new file mode 100644
index 0000000..3f0109a
--- /dev/null
+++ b/hooks/font-set.sample
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+# This hook is called with the snake-cased name of the font that has just been set.
+# To put it into use, remove .sample from the name.
+
+# Example: Show the name of the theme that was just set.
+# notify-send -u low "New font" "Your new font is $1"
diff --git a/hooks/post-update.sample b/hooks/post-update.sample
new file mode 100644
index 0000000..a43ba64
--- /dev/null
+++ b/hooks/post-update.sample
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+# This hook is called after an Blob system update has been performed.
+# To put it into use, remove .sample from the name.
+
+# Example: Show notification after the system has been updated.
+# notify-send -u low "Update Performed" "Your system is now up to date"
diff --git a/hooks/theme-set.sample b/hooks/theme-set.sample
new file mode 100644
index 0000000..2d13287
--- /dev/null
+++ b/hooks/theme-set.sample
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+# This hook is called with the snake-cased name of the theme that has just been set.
+# To put it into use, remove .sample from the name.
+
+# Example: Show the name of the theme that was just set.
+# notify-send -u low "New theme" "Your new theme is $1"
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..424ee36
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,196 @@
+#!/bin/bash
+
+set -e
+
+repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+blob_path="$HOME/.local/share/blob"
+config_dir="$HOME/.config"
+state_dir="$HOME/.local/state/blob"
+session_dir="/usr/share/wayland-sessions"
+font_dir="$HOME/.local/share/fonts"
+
+force=false
+check=false
+changes=0
+
+usage() {
+ cat < 0 )); do
+ case "$1" in
+ --force) force=true; shift ;;
+ --check) check=true; shift ;;
+ --help) usage ;;
+ *) echo "Unknown option: $1" >&2; usage ;;
+ esac
+done
+
+say() {
+ printf '%s\n' "$1"
+}
+
+note_change() {
+ changes=$((changes + 1))
+}
+
+# BLOB_PATH is a symlink to this checkout rather than a copy, so bin/, shell/,
+# themes/ and default/ are always the working tree. Every path the shell
+# resolves as $BLOB_PATH/... therefore needs no install step at all.
+link_blob_path() {
+ local current=""
+ [[ -L $blob_path ]] && current="$(readlink -f "$blob_path")"
+
+ if [[ $current == "$repo_dir" ]]; then
+ say "ok BLOB_PATH -> $repo_dir"
+ return
+ fi
+
+ if [[ -e $blob_path && ! -L $blob_path ]]; then
+ say "WARN $blob_path exists and is not a symlink; move it aside first"
+ note_change
+ return
+ fi
+
+ note_change
+ if [[ $check == true ]]; then
+ say "would link BLOB_PATH -> $repo_dir"
+ return
+ fi
+
+ ln -sfn "$repo_dir" "$blob_path"
+ say "link BLOB_PATH -> $repo_dir"
+}
+
+# Copy a file only when the destination is missing or matches what we last
+# wrote. A destination that differs is a local edit, reported and kept unless
+# --force says otherwise.
+install_file() {
+ local source="$1" dest="$2" label="$3"
+
+ if [[ ! -e $source ]]; then
+ say "SKIP $label (missing in repo)"
+ return
+ fi
+
+ if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
+ say "ok $label"
+ return
+ fi
+
+ note_change
+
+ if [[ -e $dest ]] && [[ $force == false ]]; then
+ say "DIFF $label (local changes kept; --force to overwrite)"
+ return
+ fi
+
+ if [[ $check == true ]]; then
+ say "would write $label"
+ return
+ fi
+
+ mkdir -p "$(dirname "$dest")"
+ [[ -e $dest ]] && cp "$dest" "$dest.bak"
+ cp "$source" "$dest"
+ say "write $label"
+}
+
+install_tree() {
+ local source="$1" dest="$2" label="$3"
+
+ if [[ ! -d $source ]]; then
+ say "SKIP $label (missing in repo)"
+ return
+ fi
+
+ local relative
+ while IFS= read -r relative; do
+ install_file "$source/$relative" "$dest/$relative" "$label/$relative"
+ done < <(cd "$source" && find . -type f -printf '%P\n' | sort)
+}
+
+install_session_entry() {
+ local source="$repo_dir/session/blob.desktop"
+ local dest="$session_dir/blob.desktop"
+
+ if [[ -e $dest ]] && cmp -s "$source" "$dest"; then
+ say "ok wayland session entry"
+ return
+ fi
+
+ note_change
+ if [[ $check == true ]]; then
+ say "would install wayland session entry (needs sudo)"
+ return
+ fi
+
+ sudo install -Dm644 "$source" "$dest"
+ say "write wayland session entry"
+}
+
+say "Blob installer"
+say "repo: $repo_dir"
+if [[ $check == true ]]; then
+ say "mode: check (nothing will be written)"
+fi
+say ""
+
+link_blob_path
+
+if [[ $check == false ]]; then
+ mkdir -p "$state_dir"/{toggles/hypr,indicators,notifications}
+ mkdir -p "$config_dir/blob"/{hooks,extensions,plugins,themed,themes}
+ mkdir -p "$HOME/wallpapers"
+fi
+
+install_tree "$repo_dir/hypr" "$config_dir/hypr" "hypr"
+# config/ is the shipped-defaults directory. Everything in it is reachable as
+# $BLOB_PATH/config/... so `blob-refresh-config` can reset a file, and most of it
+# is also the right thing to deploy on a fresh install. Two subdirectories are
+# reference-only and must not be copied into ~/.config:
+#
+# blob/ the shell defaults; copying them over ~/.config/blob/shell.json would
+# replace the user's bar layout with the stock one
+# hypr/ the stock Hyprland config; the personal hypr/ below is authoritative
+# and is installed to the same place
+for entry in "$repo_dir"/config/*; do
+ entry_name="$(basename "$entry")"
+ [[ $entry_name == blob || $entry_name == hypr ]] && continue
+ if [[ -d $entry ]]; then
+ install_tree "$entry" "$config_dir/$entry_name" "config/$entry_name"
+ else
+ install_file "$entry" "$config_dir/$entry_name" "config/$entry_name"
+ fi
+done
+install_tree "$repo_dir/hooks" "$config_dir/blob/hooks" "hooks"
+install_tree "$repo_dir/branding" "$config_dir/blob/branding" "branding"
+install_file "$repo_dir/shell.json" "$config_dir/blob/shell.json" "shell.json"
+install_file "$repo_dir/session/uwsm/default" "$config_dir/uwsm/default" "uwsm/default"
+install_file "$repo_dir/session/uwsm/env.d/10-blob" "$config_dir/uwsm/env.d/10-blob" "uwsm/env.d/10-blob"
+install_file "$repo_dir/fonts/omarchy.ttf" "$font_dir/omarchy.ttf" "fonts/omarchy.ttf"
+install_session_entry
+
+zen_profile="$(find "$config_dir/zen" -maxdepth 1 -type d -name '*.Default (release)*' 2>/dev/null | head -1)"
+if [[ -n $zen_profile ]]; then
+ install_file "$repo_dir/zen/userChrome.css" "$zen_profile/chrome/userChrome.css" "zen/userChrome.css"
+else
+ say "SKIP zen/userChrome.css (no Zen profile found)"
+fi
+
+say ""
+if [[ $check == true ]]; then
+ say "$changes item(s) would change."
+ (( changes == 0 )) || exit 1
+else
+ say "$changes item(s) changed."
+ say ""
+ say "Next: log out and pick the Blob session, then run 'blob theme set '."
+fi
diff --git a/uninstall.sh b/uninstall.sh
new file mode 100755
index 0000000..7e034c0
--- /dev/null
+++ b/uninstall.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+
+set -e
+
+blob_path="$HOME/.local/share/blob"
+config_dir="$HOME/.config"
+session_entry="/usr/share/wayland-sessions/blob.desktop"
+
+keep_state=false
+assume_yes=false
+
+usage() {
+ cat < 0 )); do
+ case "$1" in
+ --keep-state) keep_state=true; shift ;;
+ --yes) assume_yes=true; shift ;;
+ --help) usage ;;
+ *) echo "Unknown option: $1" >&2; usage ;;
+ esac
+done
+
+say() {
+ printf '%s\n' "$1"
+}
+
+restore_or_remove() {
+ local path="$1" label="$2"
+
+ if [[ -e $path.bak ]]; then
+ mv "$path.bak" "$path"
+ say "restore $label (from .bak)"
+ return
+ fi
+
+ if [[ -e $path ]]; then
+ rm -f "$path"
+ say "remove $label"
+ fi
+}
+
+say "This removes the Blob session, its config, and the BLOB_PATH symlink."
+say "Your checkout, wallpapers, and themes stay where they are."
+if [[ $keep_state == false ]]; then
+ say "It also removes ~/.local/state/blob (active theme, toggles, notifications)."
+fi
+say ""
+
+if [[ $assume_yes == false ]]; then
+ read -rp "Continue? [y/N] " answer
+ [[ $answer == [yY] ]] || exit 0
+fi
+
+if [[ -L $blob_path ]]; then
+ rm -f "$blob_path"
+ say "remove BLOB_PATH symlink"
+fi
+
+for relative in blob/shell.json uwsm/default uwsm/env.d/10-blob; do
+ restore_or_remove "$config_dir/$relative" "$relative"
+done
+
+if [[ -d $config_dir/blob ]]; then
+ rm -rf "$config_dir/blob"
+ say "remove ~/.config/blob"
+fi
+
+for lua in hyprland.lua monitors.lua input.lua looknfeel.lua bindings.lua autostart.lua hyprsunset.conf xdph.conf; do
+ restore_or_remove "$config_dir/hypr/$lua" "hypr/$lua"
+done
+
+if [[ $keep_state == false && -d $HOME/.local/state/blob ]]; then
+ rm -rf "$HOME/.local/state/blob"
+ say "remove ~/.local/state/blob"
+fi
+
+if [[ -e $session_entry ]]; then
+ sudo rm -f "$session_entry"
+ say "remove wayland session entry"
+fi
+
+say ""
+say "Done. Pick another session at the login screen before rebooting."