diff --git a/.chezmoi.yaml.tmpl b/.chezmoi.yaml.tmpl
index a8b4a2a..263965e 100644
--- a/.chezmoi.yaml.tmpl
+++ b/.chezmoi.yaml.tmpl
@@ -60,4 +60,22 @@ age:
data:
os_family: {{ $osFamily | quote }}
- os_id: {{ .chezmoi.osRelease.id | quote }}
\ No newline at end of file
+ os_id: {{ .chezmoi.osRelease.id | quote }}
+
+ # Sway/Wayland desktop stack — enabled per host. Pis don't need it
+ # (headless); x86_64 desktops do. Override at runtime by creating
+ # `~/.config/chezmoi/features/sway` (force ON) or
+ # `~/.config/chezmoi/features/no-sway` (force OFF).
+ #
+ # To toggle for a new box: `touch ~/.config/chezmoi/features/sway`
+ # then `chezmoi apply`.
+ {{- $swayOverrideOn := stat (joinPath .chezmoi.homeDir ".config/chezmoi/features/sway") }}
+ {{- $swayOverrideOff := stat (joinPath .chezmoi.homeDir ".config/chezmoi/features/no-sway") }}
+ {{- if $swayOverrideOn }}
+ sway: true
+ {{- else if $swayOverrideOff }}
+ sway: false
+ {{- else }}
+ {{- $swayHosts := list "miche" "byte" "kaiser" }}
+ sway: {{ has .chezmoi.hostname $swayHosts }}
+ {{- end }}
\ No newline at end of file
diff --git a/dot_config/foot/foot.ini b/dot_config/foot/foot.ini
new file mode 100644
index 0000000..9e1ce53
--- /dev/null
+++ b/dot_config/foot/foot.ini
@@ -0,0 +1,88 @@
+# foot terminal — gruvbox dark, Maple Mono 14pt with Terminus fallback
+# Reference: man foot.ini
+
+[main]
+font=Maple Mono CN:size=14,Terminus:size=14
+font-bold=Maple Mono CN:weight=bold:size=14
+font-italic=Maple Mono CN:slant=italic:size=14
+font-bold-italic=Maple Mono CN:weight=bold:slant=italic:size=14
+font-size-adjustment=0.5
+pad=8x8
+term=foot
+# (Previously set to "foot-extra", but that terminfo is not installed in the
+# CachyOS foot package; only foot, foot+base, foot-direct are. Setting
+# TERM=foot-extra breaks every ncurses program — htop, nvtop, vim, etc. —
+# with "cannot initialize terminal type". "foot" has 256 colors and
+# everything modern tools need.)
+# kaiser runs zsh as the login shell (SHELL=/bin/fish was misleading — that
+# comes from sway's inherited env, not the user's actual shell). Hardcode
+# /usr/bin/zsh here. foot does NOT expand $SHELL in the shell= value (it
+# passes the literal string to its child), so don't try the sh,-c trick —
+# it just makes foot spawn a binary literally named "${SHELL:-/bin/bash}"
+# and exit with "No such file or directory".
+shell=/usr/bin/zsh
+
+# PTY
+# utmphelper is not packaged with foot on this CachyOS build. Leave it unset
+# to disable utmp logging for foot sessions; harmless and not required.
+
+# Initial geometry
+initial-window-size-chars=120x32
+
+[scrollback]
+lines=10000
+indicator-position=relative
+
+[cursor]
+style=block
+blink=yes
+blink-rate=500
+beam-thickness=2px
+
+[mouse]
+hide-when-typing=yes
+alternate-scroll-mode=true
+
+[colors-dark]
+# Gruvbox dark palette (gruvbox-community) — bg0_h / fg / 16 ANSI
+# https://github.com/gruvbox-community/gruvbox
+background=1d2021
+foreground=ebdbb2
+# Slight transparency — 0.90 alpha. Foot 1.27+ uses a separate alpha key
+# (in [colors-dark], not 8-digit hex — that was foot 1.24 syntax).
+alpha=0.90
+
+# Regular ANSI (0-7)
+regular0=282828
+regular1=cc241d
+regular2=98971a
+regular3=d79921
+regular4=458588
+regular5=b16286
+regular6=689d6a
+regular7=a89984
+
+# Bright ANSI (8-15)
+bright0=928374
+bright1=fb4934
+bright2=b8bb26
+bright3=fabd2f
+bright4=83a598
+bright5=d3869b
+bright6=8ec07c
+bright7=ebdbb2
+
+# Selection (FG BG pair)
+selection-foreground=1d2021
+selection-background=ebdbb2
+# Search box (FG BG pairs)
+search-box-match=ebdbb2 458588
+search-box-no-match=ebdbb2 cc241d
+
+[bell]
+urgent=no
+notify=no
+visual=no
+
+[url]
+launch=xdg-open {url}
diff --git a/dot_config/foot/patch.py b/dot_config/foot/patch.py
new file mode 100644
index 0000000..dd53e37
--- /dev/null
+++ b/dot_config/foot/patch.py
@@ -0,0 +1,25 @@
+from pathlib import Path
+p = Path("/home/rain/.config/foot/foot.ini")
+text = p.read_text()
+old_lines = [
+ "# foot does not expand env variables in the `shell=` value, so we have to",
+ "# go through `sh -c` to make the parent's $SHELL take effect. When foot",
+ "# is launched from a fish session, $SHELL is /bin/fish, and foot runs fish.",
+ "# When launched from bash, $SHELL is /bin/bash, and foot runs bash. The",
+ "# default fallback is /bin/bash if $SHELL is unset for any reason.",
+ 'shell=sh,-c,"exec ${SHELL:-/bin/bash} -i"',
+]
+new_lines = [
+ "# kaiser runs zsh as the login shell. Hardcode /usr/bin/zsh here.",
+ "# foot does NOT expand $SHELL in shell= (it passes the literal string",
+ "# to its child process), so the sh,-c trick makes foot try to exec a",
+ '# binary literally named "${SHELL:-/bin/bash}" and die with',
+ '# "No such file or directory". Point shell= at the binary directly.',
+ "shell=/usr/bin/zsh",
+]
+old = "\n".join(old_lines)
+new = "\n".join(new_lines)
+assert old in text, "old block not found in foot.ini"
+text = text.replace(old, new, 1)
+p.write_text(text)
+print("OK, new size:", len(text))
diff --git a/dot_config/mako/config b/dot_config/mako/config
new file mode 100644
index 0000000..3abb9e5
--- /dev/null
+++ b/dot_config/mako/config
@@ -0,0 +1,22 @@
+# mako gruvbox-material-dark-hard
+font=Maple Mono NF 11
+background-color=#1d2021
+text-color=#ebdbb2
+border-size=2
+border-color=#d79921
+border-radius=0
+default-timeout=5000
+padding=10
+margin=8
+width=400
+
+[urgency=high]
+border-color=#fb4934
+default-timeout=0
+
+[urgency=normal]
+border-color=#d79921
+
+[urgency=low]
+border-color=#504945
+default-timeout=3000
diff --git a/dot_config/sway/caffeine.sh b/dot_config/sway/caffeine.sh
new file mode 100755
index 0000000..bf68488
--- /dev/null
+++ b/dot_config/sway/caffeine.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# caffeine.sh — toggle caffeine mode by killing/restarting swayidle
+# When ON: kill swayidle (no idle lock/screen-off)
+# When OFF: restart swayidle with normal timeouts
+
+WALLPAPER="$HOME/.local/share/wallpapers/teach-invaders-gruvbox.png"
+FLAG="/tmp/caffeine-inhibit"
+
+if [ -f "$FLAG" ]; then
+ # Turn OFF caffeine — restart swayidle
+ rm -f "$FLAG"
+ killall swayidle 2>/dev/null
+ swayidle -w \
+ timeout 300 "swaylock -f -i $WALLPAPER" \
+ timeout 600 'swaymsg "output * power off"' \
+ resume 'swaymsg "output * power on"' \
+ before-sleep "swaylock -f -i $WALLPAPER" &
+ notify-send -t 2000 "☕ Caffeine OFF" "Idle sleep enabled" 2>/dev/null
+else
+ # Turn ON caffeine — kill swayidle
+ touch "$FLAG"
+ killall swayidle 2>/dev/null
+ notify-send -t 2000 "☕ Caffeine ON" "Idle sleep disabled" 2>/dev/null
+fi
diff --git a/dot_config/sway/cheatsheet.md b/dot_config/sway/cheatsheet.md
new file mode 100644
index 0000000..11c1191
--- /dev/null
+++ b/dot_config/sway/cheatsheet.md
@@ -0,0 +1,48 @@
+# === sway keybinds cheatsheet ===
+# generated from ~/.config/sway/config — `Super` = the Windows key (Mod4)
+# reload this view with: Super + /
+
+# LAUNCHERS
+Super+Enter open foot terminal
+Super+D open wofi app launcher
+Super+E open thunar (file manager)
+Super+B open firefox
+Super+/ show this cheatsheet (in foot, via less)
+Super+Shift+E exit sway (with prompt)
+
+# WINDOW MANAGEMENT
+Super+Q kill focused window
+Super+F fullscreen toggle
+Super+V split toggle (vertical <-> horizontal)
+Super+S stacking layout
+Super+W tabbed layout
+Super+Space floating toggle
+
+# FOCUS / MOVE
+Super+arrows focus window in direction
+Super+Shift+arrows move window in direction
+
+# WORKSPACES
+Super+1 .. Super+0 switch to workspace N (0 = 10)
+Super+Tab toggle back to previous workspace
+Super+Shift+1..0 move focused window to workspace N
+
+# SCREENSHOTS
+Print region screenshot to file
+Super+Print full-screen screenshot to file
+Super+Shift+S region screenshot to clipboard
+
+# SCRATCHPAD
+Super+` toggle dropdown foot (sway scratchpad)
+
+# RESIZE MODE (Super+R enters, Esc/Enter exits)
+resize mode:
+ arrows shrink/grow 30px in direction
+ Esc / Enter exit resize mode
+
+# LOCK / IDLE
+Ctrl+Alt+L lock now
+5 min idle auto-lock
+10 min idle auto screen-off
+
+# TIP: Super+Shift+C reloads this config without logging out.
diff --git a/dot_config/sway/cheatsheet.sh b/dot_config/sway/cheatsheet.sh
new file mode 100755
index 0000000..24c516c
--- /dev/null
+++ b/dot_config/sway/cheatsheet.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# cheatsheet.sh — opens the sway keybind cheatsheet in a new foot terminal
+# running less. Press q to quit.
+exec foot sh -c 'exec less -SRFX "$HOME/.config/sway/cheatsheet.md"'
diff --git a/dot_config/sway/config b/dot_config/sway/config
new file mode 100644
index 0000000..047b750
--- /dev/null
+++ b/dot_config/sway/config
@@ -0,0 +1,148 @@
+# === ~/.config/sway/config ===
+# MINIMAL — single file, no includes. Tier-1+2+3 in one go.
+# We can split into includes later once this is proven to boot.
+
+# === variables ===
+set $mod Mod4
+set $term foot
+set $menu wofi --show drun --insensitive -p "launch"
+set $browser zen
+set $fileManager thunar
+
+# === output / input ===
+# Gruvbox wallpaper — sway's native bg command manages swaybg lifecycle internally
+output * bg $HOME/.local/share/wallpapers/teach-invaders-gruvbox.png fill
+
+# Touchpad: tap-to-click, natural scroll, drag
+input type:touchpad {
+ tap enabled
+ tap_button_map lrm
+ natural_scroll enabled
+ dwt enabled
+ accel_profile adaptive
+}
+
+# === window borders (gruvbox-hard) ===
+default_border pixel 2
+default_floating_border pixel 2
+gaps inner 8
+gaps outer 4
+smart_borders on
+smart_gaps on
+
+# gruvbox-orange focus border (replaces the default "generic blue")
+client.focused #d79921 #1d2021 #ebdbb2 #d79921 #d79921
+client.unfocused #3c3836 #1d2021 #a89984 #3c3836 #3c3836
+client.focused_inactive #504945 #1d2021 #a89984 #504945 #504945
+client.urgent #fb4934 #1d2021 #ebdbb2 #fb4934 #fb4934
+
+# === swaybar (replaced by waybar — keeping as fallback) ===
+# If waybar isn't installed yet, this provides a basic bar.
+bar {
+ swaybar_command waybar
+}
+
+# === autostart (kept tiny, all stderr silenced) ===
+# Make user-local themes/icons visible to GTK and Qt
+exec sh -c 'export XDG_DATA_DIRS="$HOME/.local/share:$HOME/.local/share/flatpak/exports/share:/usr/local/share:/usr/share"; export XDG_DATA_HOME="$HOME/.local/share"; export XDG_CONFIG_HOME="$HOME/.config"; systemctl --user import-environment XDG_DATA_DIRS XDG_DATA_HOME XDG_CONFIG_HOME 2>/dev/null; dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=sway'
+exec /usr/libexec/polkit-gnome-authentication-agent-1
+exec clipmenud
+exec sh -c 'systemctl --user start xdg-desktop-portal xdg-desktop-portal-wlr 2>/dev/null || true'
+exec sh -c 'gsettings set org.gnome.desktop.interface gtk-theme Gruvbox-Material-Dark-Hard 2>/dev/null; gsettings set org.gnome.desktop.interface icon-theme Gruvbox-Material-Dark 2>/dev/null; gsettings set org.gnome.desktop.interface color-scheme prefer-dark 2>/dev/null; true'
+exec gnome-keyring-daemon --start --components=secrets
+exec mako
+exec wlsunset -l 39.96 -L -82.99
+exec sworkstyle
+
+# === idle / lock ===
+exec swayidle -w \
+ timeout 300 '$HOME/.config/sway/lock-fancy.sh' \
+ timeout 600 'swaymsg "output * power off"' \
+ resume 'swaymsg "output * power on"' \
+ before-sleep '$HOME/.config/sway/lock-fancy.sh'
+
+# === keybinds ===
+bindsym $mod+Return exec $term
+bindsym $mod+d exec $menu
+bindsym $mod+e exec $fileManager
+bindsym $mod+b exec $browser
+bindsym $mod+q kill
+bindsym $mod+Shift+c reload
+bindsym Ctrl+Alt+l exec $HOME/.config/sway/lock-fancy.sh
+bindsym $mod+slash exec $HOME/.config/sway/cheatsheet.sh
+bindsym $mod+question exec $HOME/.config/sway/cheatsheet.sh
+bindsym $mod+Shift+e exec swaynag -t warning -m 'Exit Sway?' -B 'Yes, exit Sway' 'swaymsg exit'
+
+bindsym $mod+Left focus left
+bindsym $mod+Down focus down
+bindsym $mod+Up focus up
+bindsym $mod+Right focus right
+bindsym $mod+Shift+Left move left
+bindsym $mod+Shift+Down move down
+bindsym $mod+Shift+Up move up
+bindsym $mod+Shift+Right move right
+
+bindsym $mod+1 workspace number 1
+bindsym $mod+2 workspace number 2
+bindsym $mod+3 workspace number 3
+bindsym $mod+4 workspace number 4
+bindsym $mod+5 workspace number 5
+bindsym $mod+6 workspace number 6
+bindsym $mod+7 workspace number 7
+bindsym $mod+8 workspace number 8
+bindsym $mod+9 workspace number 9
+bindsym $mod+0 workspace number 10
+bindsym $mod+Tab workspace back_and_forth
+bindsym $mod+Shift+1 move container to workspace number 1
+bindsym $mod+Shift+2 move container to workspace number 2
+bindsym $mod+Shift+3 move container to workspace number 3
+bindsym $mod+Shift+4 move container to workspace number 4
+bindsym $mod+Shift+5 move container to workspace number 5
+bindsym $mod+Shift+6 move container to workspace number 6
+bindsym $mod+Shift+7 move container to workspace number 7
+bindsym $mod+Shift+8 move container to workspace number 8
+bindsym $mod+Shift+9 move container to workspace number 9
+bindsym $mod+Shift+0 move container to workspace number 10
+
+bindsym $mod+f fullscreen
+bindsym $mod+v split toggle splitv
+bindsym $mod+s layout stacking
+bindsym $mod+w layout tabbed
+bindsym $mod+space exec $menu
+bindsym $mod+Shift+space floating toggle
+
+bindsym Print exec grim -g "$(slurp)" ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png
+bindsym $mod+Print exec grim ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png
+bindsym $mod+Shift+s exec grim -g "$(slurp)" - | wl-copy
+
+mode "resize" {
+ bindsym Left resize shrink width 30px
+ bindsym Down resize grow height 30px
+ bindsym Up resize shrink height 30px
+ bindsym Right resize grow width 30px
+ bindsym Escape mode "default"
+ bindsym Return mode "default"
+}
+bindsym $mod+r mode "resize"
+
+# === F12 dropdown terminal ===
+# F12 opens a floating foot. Uses -a dropdown so for_window only floats
+# the dropdown, not regular foot windows from Super+Enter.
+# Close it with Super+Q or Ctrl+D.
+bindsym F12 exec foot -a dropdown
+bindsym $mod+grave exec foot -a dropdown
+for_window [app_id="dropdown"] floating enable, sticky enable, resize set width 1100 height 600, move position center
+
+# === media keys ===
+bindsym --locked XF86AudioRaiseVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
+bindsym --locked XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
+bindsym --locked XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
+bindsym --locked XF86AudioMicMute exec wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
+bindsym --locked XF86AudioPlay exec playerctl play-pause 2>/dev/null || true
+bindsym --locked XF86AudioNext exec playerctl next 2>/dev/null || true
+bindsym --locked XF86AudioPrev exec playerctl previous 2>/dev/null || true
+bindsym --locked XF86MonBrightnessUp exec sh -c 'echo $(( $(cat /sys/class/backlight/acpi_video0/brightness) + 1 > $(cat /sys/class/backlight/acpi_video0/max_brightness) ? $(cat /sys/class/backlight/acpi_video0/max_brightness) : $(cat /sys/class/backlight/acpi_video0/brightness) + 1 )) | sudo tee /sys/class/backlight/acpi_video0/brightness'
+bindsym --locked XF86MonBrightnessDown exec sh -c 'echo $(( $(cat /sys/class/backlight/acpi_video0/brightness) - 1 < 0 ? 0 : $(cat /sys/class/backlight/acpi_video0/brightness) - 1 )) | sudo tee /sys/class/backlight/acpi_video0/brightness'
+
+# pointer / mouse — the gruvbox-material-gtk repo doesn't ship an xcursor theme,
+# so leave the default alone. (Cursor shows fine on the default theme.)
diff --git a/dot_config/sway/config.d/00-gruvbox.conf b/dot_config/sway/config.d/00-gruvbox.conf
new file mode 100644
index 0000000..5d90acd
--- /dev/null
+++ b/dot_config/sway/config.d/00-gruvbox.conf
@@ -0,0 +1,18 @@
+# === ~/.config/sway/config.d/00-gruvbox.conf ===
+# gruvbox material dark hard — wired through gsettings so Thunar, Firefox chrome,
+# and any GTK3/4 app pick it up the moment sway starts.
+
+# Run gsettings in a sh -c wrapper so we can pipe stderr to /dev/null.
+# This is necessary because the very first exec block runs before dbus is
+# always reachable, and gsettings prints a long bar-warning to stderr when
+# it can't find the bus. We don't want the red bar to surface that.
+exec_always {
+ gsettings set org.gnome.desktop.interface gtk-theme 'Gruvbox-Material-Dark-Hard' 2>/dev/null
+ gsettings set org.gnome.desktop.interface icon-theme 'Gruvbox-Material-Dark' 2>/dev/null
+ gsettings set org.gnome.desktop.interface cursor-theme 'Gruvbox-Material-Dark' 2>/dev/null
+ gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' 2>/dev/null
+ gsettings set org.cinnamon.desktop.interface gtk-theme 'Gruvbox-Material-Dark-Hard' 2>/dev/null
+}
+
+# gnome-keyring (in case any GTK app needs secret service for ssh-agent bridge)
+exec gnome-keyring-daemon --start --components=secrets
diff --git a/dot_config/sway/config.d/10-keybinds.conf b/dot_config/sway/config.d/10-keybinds.conf
new file mode 100644
index 0000000..094d984
--- /dev/null
+++ b/dot_config/sway/config.d/10-keybinds.conf
@@ -0,0 +1,76 @@
+# === ~/.config/sway/config.d/10-keybinds.conf ===
+# sway keybindings. Mint+Cinnamon muscle memory → Super (Mod4).
+
+# === essentials ===
+bindsym $mod+Return exec $term
+bindsym $mod+d exec $menu
+bindsym $mod+e exec $fileManager
+bindsym $mod+b exec $browser
+bindsym $mod+q kill
+bindsym $mod+Shift+c reload
+bindsym $mod+Shift+e exec swaynag -t warning \
+ -m 'Exit Sway?' \
+ -B 'Yes, exit Sway' 'swaymsg exit'
+
+# === move focus ===
+bindsym $mod+Left focus left
+bindsym $mod+Down focus down
+bindsym $mod+Up focus up
+bindsym $mod+Right focus right
+bindsym $mod+Shift+Left move left
+bindsym $mod+Shift+Down move down
+bindsym $mod+Shift+Up move up
+bindsym $mod+Shift+Right move right
+
+# === workspace navigation ===
+bindsym $mod+1 workspace number 1
+bindsym $mod+2 workspace number 2
+bindsym $mod+3 workspace number 3
+bindsym $mod+4 workspace number 4
+bindsym $mod+5 workspace number 5
+bindsym $mod+6 workspace number 6
+bindsym $mod+7 workspace number 7
+bindsym $mod+8 workspace number 8
+bindsym $mod+9 workspace number 9
+bindsym $mod+0 workspace number 10
+bindsym $mod+Tab workspace back_and_forth
+bindsym $mod+Shift+1 move container to workspace number 1
+bindsym $mod+Shift+2 move container to workspace number 2
+bindsym $mod+Shift+3 move container to workspace number 3
+bindsym $mod+Shift+4 move container to workspace number 4
+bindsym $mod+Shift+5 move container to workspace number 5
+bindsym $mod+Shift+6 move container to workspace number 6
+bindsym $mod+Shift+7 move container to workspace number 7
+bindsym $mod+Shift+8 move container to workspace number 8
+bindsym $mod+Shift+9 move container to workspace number 9
+bindsym $mod+Shift+0 move container to workspace number 10
+
+# === layout ===
+bindsym $mod+f fullscreen
+bindsym $mod+v split toggle splitv
+bindsym $mod+s layout stacking
+bindsym $mod+w layout tabbed
+bindsym $mod+space floating toggle
+bindsym $mod+Shift+space focus mode_toggle
+
+# === screenshot (grim + slurp) ===
+bindsym Print exec grim -g "$(slurp)" ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png
+bindsym $mod+Print exec grim ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png
+bindsym $mod+Shift+s exec grim -g "$(slurp)" - | wl-copy
+
+# === resize ===
+mode "resize" {
+ bindsym Left resize shrink width 30px
+ bindsym Down resize grow height 30px
+ bindsym Up resize shrink height 30px
+ bindsym Right resize grow width 30px
+ bindsym Escape mode "default"
+ bindsym Return mode "default"
+}
+bindsym $mod+r mode "resize"
+
+# === scratchpads (quick terminal / browser) ===
+# Start a headless foot server, then a dropdown client window. Toggle with $mod+grave.
+bindsym $mod+grave exec foot --server
+bindsym $mod+apostrophe exec foot -a dropdown
+for_window [app_id="dropdown"] floating enable, sticky enable, resize set width 1100 height 600, move position center
diff --git a/dot_config/sway/config.d/20-autostart.conf b/dot_config/sway/config.d/20-autostart.conf
new file mode 100644
index 0000000..1952cff
--- /dev/null
+++ b/dot_config/sway/config.d/20-autostart.conf
@@ -0,0 +1,15 @@
+# === ~/.config/sway/config.d/20-autostart.conf ===
+# background services started when sway comes up.
+
+# polkit agent — needed for gparted, mount UIs in thunar, etc.
+exec /usr/libexec/polkit-gnome-authentication-agent-1
+
+# clipboard — start clipmenud directly (it watches the clipboard internally)
+# Drop the wl-paste --watch wrapper to avoid bar noise if the socket isn't ready.
+exec clipmenud
+
+# xdg-desktop-portal — required for native file/screen sharing in Firefox/Chromium.
+# Skip if services aren't enabled; the user can enable with:
+# systemctl --user enable --now xdg-desktop-portal xdg-desktop-portal-wlr
+# Until then, those systemctl lines are quiet no-ops.
+exec sh -c 'systemctl --user start xdg-desktop-portal xdg-desktop-portal-wlr 2>/dev/null || true'
diff --git a/dot_config/sway/config.d/50-launchers.conf b/dot_config/sway/config.d/50-launchers.conf
new file mode 100644
index 0000000..159d7f9
--- /dev/null
+++ b/dot_config/sway/config.d/50-launchers.conf
@@ -0,0 +1,17 @@
+# ~/.config/sway/config.d/50-launchers.conf
+# Launcher keybinds. The base $menu var in config is the drun launcher.
+# These add run mode, window switcher, and the powermenu.
+
+# $mod+grave already used by foot dropdown — don't clobber.
+# $mod+Tab already used by workspace back_and_forth — don't clobber.
+# $mod+slash is the cheatsheet — keep it.
+
+# Exec ad-hoc command (not in $PATH discovery)
+bindsym $mod+F12 exec wofi --show run --prompt "exec"
+
+# Sway window switcher (replaces `swayr`-style overview)
+bindsym $mod+F11 exec wofi --show window
+
+# Power menu — dmenu mode driven by wofi-powermenu script.
+# wofi has no custom-mode feature; the script does the dispatch.
+bindsym $mod+Escape exec $HOME/.local/bin/wofi-powermenu
diff --git a/dot_config/sway/lock-fancy.sh b/dot_config/sway/lock-fancy.sh
new file mode 100755
index 0000000..932b9bc
--- /dev/null
+++ b/dot_config/sway/lock-fancy.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# lock-fancy.sh — fancy swaylock: snapshot the screen, dim+blur it, and use as background.
+# grim + ffmpeg (both already installed). No swaylock-effects overlay needed.
+
+set -e
+
+TMP=/tmp/lock-bg-483833.png
+DIM=/tmp/lock-bg-dim-483833.png
+
+# Snapshot the current screen
+grim -t png "$TMP" 2>/dev/null
+
+# Dim + blur via ffmpeg (much faster than ImageMagick)
+if [ -f "$TMP" ] && command -v ffmpeg >/dev/null; then
+ ffmpeg -loglevel error -y -i "$TMP" -vf "boxblur=12:1,eq=brightness=-0.30:saturation=0.6" -frames:v 1 "$DIM"
+ IMG="$DIM"
+else
+ # Fallback to the regular wallpaper
+ IMG="$HOME/.local/share/wallpapers/teach-invaders-gruvbox.png"
+fi
+
+# Launch swaylock
+swaylock -f -i "$IMG" \
+ --indicator-radius 100 \
+ --indicator-thickness 6 \
+ --ring-color ebdbb2 \
+ --inside-color 1d202180 \
+ --key-hl-color fe8019 \
+ --bs-hl-color fb4934 \
+ --text-color ebdbb2 \
+ --separator-color 00000000 \
+ --fade-in 0.3
+
+rm -f "$TMP" "$DIM" 2>/dev/null
diff --git a/dot_config/sway/toggle-dropdown.sh b/dot_config/sway/toggle-dropdown.sh
new file mode 100755
index 0000000..ac63c9b
--- /dev/null
+++ b/dot_config/sway/toggle-dropdown.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+# toggle-dropdown.sh — toggle the dropdown foot terminal
+# Writes debug info to /tmp/sway-f12.log so we can diagnose from SSH
+
+LOG=/tmp/sway-f12.log
+echo "=== $(date) F12/Super+grave pressed ===" >> "$LOG"
+echo "WAYLAND_DISPLAY=$WAYLAND_DISPLAY" >> "$LOG"
+echo "USER=$USER XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" >> "$LOG"
+
+# Find the sway socket
+SOCK=$(ls -t /run/user/$(id -u)/sway-ipc.*.sock 2>/dev/null | head -1)
+echo "SOCK=$SOCK" >> "$LOG"
+
+if [ -z "$SOCK" ]; then
+ echo "ERROR: no sway socket found" >> "$LOG"
+ notify-send -t 2000 "sway: no IPC socket" 2>>"$LOG" || true
+ exit 1
+fi
+
+# Check if a foot window is in the scratchpad (hidden)
+HIDDEN=$(SWAYSOCK=$SOCK swaymsg -t get_tree 2>>"$LOG" | python3 -c "
+import sys, json
+try:
+ tree = json.load(sys.stdin)
+ def find_app(node, app_id):
+ if node.get('app_id') == app_id and node.get('name', '').startswith('__i3'):
+ return node
+ for c in node.get('nodes', []) + node.get('floating_nodes', []):
+ r = find_app(c, app_id)
+ if r is not None:
+ return r
+ return None
+ dd = find_app(tree, 'foot')
+ print('hidden' if dd is not None else 'none')
+except Exception as e:
+ print(f'err:{e}')
+" 2>>"$LOG")
+
+echo "HIDDEN=$HIDDEN" >> "$LOG"
+
+if [ "$HIDDEN" = "hidden" ]; then
+ SWAYSOCK=$SOCK swaymsg '[app_id="foot"] scratchpad show' >>"$LOG" 2>&1
+ echo "toggled: scratchpad show" >> "$LOG"
+else
+ # Spawn a new foot. Pass through WAYLAND_DISPLAY explicitly.
+ if [ -n "$WAYLAND_DISPLAY" ]; then
+ WAYLAND_DISPLAY=$WAYLAND_DISPLAY foot >>"$LOG" 2>&1 &
+ else
+ # try to find wayland-1 socket
+ if [ -S "/run/user/$(id -u)/wayland-1" ]; then
+ WAYLAND_DISPLAY=wayland-1 foot >>"$LOG" 2>&1 &
+ else
+ foot >>"$LOG" 2>&1 &
+ fi
+ fi
+ echo "spawned foot" >> "$LOG"
+fi
+echo "=== done ===" >> "$LOG"
diff --git a/dot_config/sway/wifi-menu.sh b/dot_config/sway/wifi-menu.sh
new file mode 100755
index 0000000..f47a54c
--- /dev/null
+++ b/dot_config/sway/wifi-menu.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# wifi-menu.sh — wofi-based WiFi network picker using nmcli
+# Lists available networks, connects on selection.
+
+# Get list of networks: SSID, signal, security
+SELECTED=$(nmcli -t -f SSID,SIGNAL,SECURITY device wifi list --rescan yes 2>/dev/null | \
+ grep -v '^$' | \
+ sort -t: -k2 -nr | \
+ awk -F: '{
+ sig = $2 + 0
+ if (sig > 75) bars = "████"
+ else if (sig > 50) bars = "███░"
+ else if (sig > 25) bars = "██░░"
+ else bars = "█░░░"
+ sec = ($3 != "" && $3 != "--") ? " " : ""
+ printf "%s %s%s\n", bars, $1, sec
+ }' | \
+ wofi --dmenu --insensitive --prompt "WiFi" -W 400 -H 500 2>/dev/null)
+
+# Exit if nothing selected
+[ -z "$SELECTED" ] && exit 0
+
+# Extract SSID (everything after the signal bars + spaces)
+SSID=$(echo "$SELECTED" | sed 's/^[█░ ]* //; s/ 🔒$//')
+
+# Connect
+nmcli device wifi connect "$SSID" 2>&1 | grep -q "successfully" && \
+ notify-send -t 3000 "WiFi" "Connected to $SSID" || \
+ notify-send -t 5000 "WiFi" "Failed to connect to $SSID"
diff --git a/dot_config/waybar/config b/dot_config/waybar/config
new file mode 100644
index 0000000..e6109f9
--- /dev/null
+++ b/dot_config/waybar/config
@@ -0,0 +1,102 @@
+{
+ "layer": "top",
+ "position": "top",
+ "height": 32,
+ "spacing": 0,
+ "modules-left": [
+ "sway/workspaces",
+ "sway/mode"
+ ],
+ "modules-center": [
+ "clock",
+ "custom/caffeine"
+ ],
+ "modules-right": [
+ "pulseaudio",
+ "battery",
+ "cpu",
+ "memory",
+ "tray",
+ "network"
+ ],
+ "sway/workspaces": {
+ "disable-scroll": true,
+ "format": "{name}",
+ "format-icons": {
+ "focused": "\u25cf",
+ "default": "\u25cb"
+ }
+ },
+ "clock": {
+ "format": " {:%a %b %d %H:%M}",
+ "tooltip-format": "{:%Y %B}\n{calendar}",
+ "format-alt": " {:%Y-%m-%d}"
+ },
+ "pulseaudio": {
+ "format": "{icon} {volume}%",
+ "format-muted": "\udb81\udd81 muted",
+ "format-icons": {
+ "default": [
+ "\udb81\udd7f",
+ "\udb81\udd80",
+ "\udb81\udd7e"
+ ]
+ },
+ "scroll-step": 5,
+ "on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
+ "on-click-right": "pavucontrol"
+ },
+ "battery": {
+ "states": {
+ "good": 80,
+ "warning": 30,
+ "critical": 15
+ },
+ "format": "{icon} {capacity}%",
+ "format-charging": "\udb80\udc84 {capacity}%",
+ "format-plugged": "\udb80\udc84 {capacity}%",
+ "format-icons": [
+ "\udb80\udc8e",
+ "\udb80\udc7a",
+ "\udb80\udc7b",
+ "\udb80\udc7c",
+ "\udb80\udc7d",
+ "\udb80\udc7e",
+ "\udb80\udc7f",
+ "\udb80\udc80",
+ "\udb80\udc81",
+ "\udb80\udc82",
+ "\udb80\udc79"
+ ]
+ },
+ "cpu": {
+ "format": "\udb83\udee0 {usage}%",
+ "tooltip": true
+ },
+ "memory": {
+ "format": "\udb80\udf5b {}%",
+ "tooltip-format": "{used:0.1f}G / {total:0.1f}G"
+ },
+ "network": {
+ "format-wifi": "\udb82\udd28 {essid}",
+ "format-ethernet": "\udb80\ude00 {ipaddr}",
+ "format-disconnected": "\udb82\udd2d disconnected",
+ "format-linked": "\udb82\udd2a {ifname}",
+ "tooltip-format": "{ifname} \u2014 {ipaddr}",
+ "on-click": "$HOME/.config/sway/wifi-menu.sh"
+ },
+ "custom/caffeine": {
+ "format": "{icon}",
+ "format-icons": {
+ "default": "\udb80\udd76",
+ "active": "\u2615"
+ },
+ "exec": "if [ -f /tmp/caffeine-inhibit ]; then echo '{\"alt\":\"active\",\"text\":\"caffeine\"}'; else echo '{\"alt\":\"default\",\"text\":\"caffeine\"}'; fi",
+ "exec-interval": 2,
+ "return-type": "json",
+ "on-click": "$HOME/.config/sway/caffeine.sh"
+ },
+ "tray": {
+ "spacing": 8
+ }
+}
diff --git a/dot_config/waybar/style.css b/dot_config/waybar/style.css
new file mode 100644
index 0000000..3bc6de5
--- /dev/null
+++ b/dot_config/waybar/style.css
@@ -0,0 +1,92 @@
+/* === waybar — gruvbox-material-dark-hard, polished === */
+
+* {
+ font-family: "Maple Mono NF";
+ font-size: 14px;
+ min-height: 0;
+ padding: 0;
+ margin: 0;
+ border: none;
+}
+
+/* transparent bar so the desktop bleeds through at the edges */
+window#waybar {
+ background-color: rgba(29, 32, 33, 0.92);
+ color: #ebdbb2;
+ border-bottom: 1px solid #3c3836;
+}
+
+window#waybar.hidden { opacity: 0.2; }
+
+/* workspaces */
+#workspaces button {
+ padding: 0 12px;
+ background: transparent;
+ color: #a89984;
+ box-shadow: inset 0 -2px transparent;
+ transition: all 200ms ease;
+}
+#workspaces button:hover {
+ background: #3c3836;
+ color: #ebdbb2;
+}
+#workspaces button.focused {
+ color: #fabd2f;
+ box-shadow: inset 0 -2px #fabd2f;
+}
+#workspaces button.urgent {
+ color: #fb4934;
+ animation: blink 1s linear infinite alternate;
+}
+
+#mode {
+ color: #fb4934;
+ background: #282828;
+ padding: 0 12px;
+ font-weight: bold;
+}
+
+#clock {
+ color: #ebdbb2;
+ font-weight: bold;
+ padding: 0 16px;
+ border-left: 1px solid #3c3836;
+ border-right: 1px solid #3c3836;
+}
+#clock:hover { color: #fabd2f; }
+
+/* right-side modules — shared layout, individual color */
+#pulseaudio, #battery, #cpu, #memory, #network, #tray, #custom-caffeine {
+ padding: 0 12px;
+ border-left: 1px solid #3c3836;
+ transition: all 200ms ease;
+}
+#pulseaudio:hover, #battery:hover, #cpu:hover, #memory:hover, #network:hover, #tray:hover, #custom-caffeine:hover {
+ background: #3c3836;
+}
+
+#pulseaudio { color: #b8bb26; }
+#pulseaudio.muted { color: #928374; }
+#battery { color: #d79921; }
+#battery.charging { color: #b8bb26; }
+#battery.good { color: #b8bb26; }
+#battery.warning { color: #fe8019; }
+#battery.critical {
+ color: #fb4934;
+ animation: blink 1s linear infinite alternate;
+}
+#cpu { color: #83a598; }
+#memory { color: #d3869b; }
+#network { color: #fabd2f; }
+#network.disconnected { color: #fb4934; }
+#tray { padding: 0 10px; }
+
+#custom-caffeine { color: #a89984; }
+#custom-caffeine.active { color: #fabd2f; }
+#custom-caffeine.active:hover { background: #3c3836; }
+
+#sway-workspaces { margin: 0 4px; }
+
+@keyframes blink {
+ to { opacity: 0.3; }
+}
diff --git a/dot_config/wofi/config b/dot_config/wofi/config
new file mode 100644
index 0000000..3c54b9e
--- /dev/null
+++ b/dot_config/wofi/config
@@ -0,0 +1,19 @@
+# ~/.config/wofi/config — gruvbox launcher defaults
+# Loaded by wofi before style.css. CLI flags still win.
+
+[window]
+location=top
+width=620
+height=420
+
+[prompt]
+prompt=go
+insensitive=true
+
+[search]
+matching=fuzzy
+sort_order=default
+
+[keybindings]
+return=activate
+escape=close
diff --git a/dot_config/wofi/powermenu.config b/dot_config/wofi/powermenu.config
new file mode 100644
index 0000000..07cb4eb
--- /dev/null
+++ b/dot_config/wofi/powermenu.config
@@ -0,0 +1,12 @@
+# ~/.config/wofi/powermenu.config — plain INI, not rasi
+# wofi's config format is INI. There is no rasi-style `configuration { }`
+# block. The icon theme comes from gsettings / dconf, not from here.
+
+location=top
+yoffset=32
+width=420
+height=280
+prompt=power
+insensitive=true
+hide_scroll=true
+matching=exact
diff --git a/dot_config/wofi/style.css b/dot_config/wofi/style.css
new file mode 100644
index 0000000..c652fcd
--- /dev/null
+++ b/dot_config/wofi/style.css
@@ -0,0 +1,57 @@
+/* wofi gruvbox-material-dark-hard — extended rice */
+window {
+ margin: 0px;
+ border: 2px solid #d79921;
+ border-radius: 10px;
+ background-color: #1d2021;
+ background-image: linear-gradient(to bottom, #1d2021, #282828);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
+ font-family: "Maple Mono NF";
+ font-size: 14px;
+}
+
+#input {
+ margin: 0px;
+ padding: 10px;
+ border: none;
+ border-radius: 10px 10px 0 0;
+ color: #ebdbb2;
+ background-color: #282828;
+}
+
+#inner-box {
+ margin: 0px;
+ border: none;
+ background-color: transparent;
+}
+
+#outer-box {
+ margin: 0px;
+ border: none;
+ background-color: transparent;
+}
+
+#scroll {
+ margin: 0px;
+ border: none;
+}
+
+#text {
+ margin: 0px;
+ padding: 6px 10px;
+ border: none;
+ color: #ebdbb2;
+ background-color: transparent;
+}
+
+#entry {
+ transition: background-color 120ms ease-out;
+}
+
+#entry:selected {
+ background-color: #3c3836;
+}
+
+#entry:selected #text {
+ color: #fabd2f;
+}
diff --git a/run_once_40-install-sway.sh.tmpl b/run_once_40-install-sway.sh.tmpl
new file mode 100644
index 0000000..e3000e4
--- /dev/null
+++ b/run_once_40-install-sway.sh.tmpl
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+# =============================================================================
+# run_once_40-install-sway.sh.tmpl
+# Install the sway + wofi + foot + supporting tooling desktop stack.
+#
+# Gated on .sway (set in .chezmoi.yaml.tmpl). When false (Pis, headless
+# boxes, etc.), this script exits 0 without doing anything.
+#
+# To opt a new box in: `touch ~/.config/chezmoi/features/sway` then
+# `chezmoi apply`. To opt out: `touch ~/.config/chezmoi/features/no-sway`.
+#
+# Idempotent: skips if sway is already installed.
+# =============================================================================
+set -euo pipefail
+
+log() { printf '\033[1;34m[sway]\033[0m %s\n' "$*"; }
+
+# --- 0. Gate on the .sway flag ---
+{{ if not .sway -}}
+log "sway not enabled for this host (.sway=false). Skipping."
+log "To enable: touch ~/.config/chezmoi/features/sway && chezmoi apply"
+exit 0
+{{ end -}}
+
+# --- 1. Install packages ---
+SWAY_PKGS=(sway wofi foot swaybg swaylock swayidle grim slurp waybar wl-clipboard)
+
+{{ if eq .os_family "arch" -}}
+# mako is in arch [extra]
+SWAY_PKGS+=(mako)
+{{ else if eq .os_family "debian" -}}
+# mako isn't packaged on debian. Use dunst (lightweight notification daemon,
+# widely compatible with sway/waybar setups).
+SWAY_PKGS+=(dunst)
+{{ else -}}
+log "WARNING: sway packages not configured for os_family={{ .os_family }}"
+{{ end -}}
+
+log "installing sway stack: ${SWAY_PKGS[*]}"
+
+{{ if eq .os_family "arch" -}}
+sudo pacman -S --needed --noconfirm "${SWAY_PKGS[@]}"
+{{ else if eq .os_family "debian" -}}
+export DEBIAN_FRONTEND=noninteractive
+sudo apt-get install -y --no-install-recommends "${SWAY_PKGS[@]}"
+{{ end -}}
+
+log "sway stack installed"
+sway --version 2>&1 | head -1
+foot --version 2>&1 | head -1
+wofi --version 2>&1 | head -1
+
+# --- 2. Mark the box so subsequent chezmoi applies know sway is enabled ---
+mkdir -p ~/.config/chezmoi/features
+touch ~/.config/chezmoi/features/sway
+log "marked box: ~/.config/chezmoi/features/sway"
+
+log "sway stack install complete"
\ No newline at end of file