summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.emacs647
-rwxr-xr-xdunst/dmenu.sh11
-rw-r--r--dunst/dunstrc107
-rw-r--r--kitty/kitty.conf190
-rwxr-xr-xkitty/ssh_picker.sh202
-rw-r--r--polybar/config.ini456
-rw-r--r--polybar/config.ini~168
-rwxr-xr-xpolybar/launch.sh41
-rwxr-xr-xpolybar/launch.sh~11
-rwxr-xr-xpolybar/scripts/awake-status.sh17
-rwxr-xr-xpolybar/scripts/awake-toggle.sh122
-rwxr-xr-xpolybar/scripts/bar-mode-status.sh18
-rwxr-xr-xpolybar/scripts/bar-mode-toggle.sh48
-rwxr-xr-xpolybar/scripts/clipboard-menu.sh45
-rwxr-xr-xpolybar/scripts/display-brightness.sh39
-rwxr-xr-xpolybar/scripts/display-status.sh17
-rwxr-xr-xpolybar/scripts/display-toggle.sh40
-rwxr-xr-xpolybar/scripts/dunst-status.sh26
-rwxr-xr-xpolybar/scripts/kb-status.sh26
-rwxr-xr-xpolybar/scripts/kb-switch.sh36
-rwxr-xr-xpolybar/scripts/kb-switch.sh~17
-rwxr-xr-xpolybar/scripts/mic-status.sh21
-rwxr-xr-xpolybar/scripts/mic-toggle.sh13
-rwxr-xr-xpolybar/scripts/music-status.sh38
-rwxr-xr-xpolybar/scripts/net-speed.sh64
-rwxr-xr-xpolybar/scripts/polkit-agent.sh29
-rwxr-xr-xpolybar/scripts/power-menu.sh33
-rwxr-xr-xpolybar/scripts/scratchpad-status.sh18
-rwxr-xr-xpolybar/scripts/screenshot-menu.sh73
-rwxr-xr-xpolybar/scripts/temp-status.sh36
-rwxr-xr-xpolybar/scripts/updates-action.sh16
-rwxr-xr-xpolybar/scripts/updates-status.sh17
-rwxr-xr-xpolybar/scripts/wg-pc-status.sh32
-rwxr-xr-xpolybar/scripts/wg-pc-toggle.sh82
-rw-r--r--quickshell/gruvbar/README.md34
-rwxr-xr-xquickshell/gruvbar/launch.sh10
-rwxr-xr-xquickshell/gruvbar/scripts/action.sh83
-rwxr-xr-xquickshell/gruvbar/scripts/status.sh229
-rw-r--r--quickshell/gruvbar/shell.qml631
-rw-r--r--sddm/10-gruvbox-void.conf4
-rw-r--r--sddm/themes/gruvbox-void/Main.qml331
-rw-r--r--sddm/themes/gruvbox-void/metadata.desktop16
-rw-r--r--sddm/themes/gruvbox-void/theme.conf3
43 files changed, 4097 insertions, 0 deletions
diff --git a/.emacs b/.emacs
new file mode 100755
index 0000000..12a58c5
--- /dev/null
+++ b/.emacs
@@ -0,0 +1,647 @@
+;;; init.el --- Personal Emacs config -*- lexical-binding: t; -*-
+
+;; -------------------------
+;; Basic UI
+;; -------------------------
+(setq inhibit-startup-message t
+ confirm-kill-processes nil
+ ring-bell-function 'ignore)
+
+(tool-bar-mode -1)
+(menu-bar-mode -1)
+(scroll-bar-mode -1)
+(setq backup-directory-alist `(("." . ,(expand-file-name "/tmp/backups/" user-emacs-directory))))
+
+(setq auto-save-hook nil)
+(push '(fullscreen . maximized) default-frame-alist)
+
+(setq-default
+ indent-tabs-mode t
+ tab-width 8
+ window-combination-resize t
+ history-delete-duplicates t)
+
+;; -------------------------
+;; Package setup
+;; -------------------------
+(require 'package)
+
+(setq package-archives
+ '(("melpa" . "https://melpa.org/packages/")
+ ("gnu" . "https://elpa.gnu.org/packages/")
+ ("nongnu" . "https://elpa.nongnu.org/nongnu/")))
+
+(package-initialize)
+
+(unless package-archive-contents
+ (package-refresh-contents))
+
+(unless (package-installed-p 'use-package)
+ (package-install 'use-package))
+
+(require 'use-package)
+(setq use-package-always-ensure t)
+
+;; -------------------------
+;; Theme
+;; -------------------------
+(use-package gruvbox-theme)
+(load-theme 'gruvbox t)
+
+;; -------------------------
+;; Completion stack
+;; -------------------------
+(use-package vertico
+ :init
+ (vertico-mode))
+
+(use-package orderless
+ :init
+ (setq completion-styles '(orderless basic)))
+
+(use-package marginalia
+ :init
+ (marginalia-mode))
+
+(use-package consult
+ :bind (("C-x b" . consult-buffer)
+ ("C-x p f" . project-find-file)))
+
+(use-package savehist
+ :ensure nil
+ :init
+ (savehist-mode))
+
+(use-package embark
+ :bind (("C-." . embark-act)
+ ("C-;" . embark-dwim)))
+
+(use-package embark-consult)
+
+(use-package corfu
+ :init
+ (global-corfu-mode)
+ :custom
+ (corfu-auto t)
+ (corfu-cycle t)
+ (corfu-auto-delay 0.1)
+ (corfu-auto-prefix 1)
+ :bind (:map corfu-map
+ ("C-t" . corfu-next)
+ ("C-n" . corfu-previous)
+ ("<escape>" . corfu-quit)))
+
+(use-package cape
+ :init
+ (add-to-list 'completion-at-point-functions #'cape-file)
+ (add-to-list 'completion-at-point-functions #'cape-dabbrev))
+
+;; -------------------------
+;; Icons + UI polish
+;; -------------------------
+(use-package nerd-icons)
+
+(use-package nerd-icons-completion
+ :after marginalia
+ :init
+ (nerd-icons-completion-mode)
+ (add-hook 'marginalia-mode-hook
+ #'nerd-icons-completion-marginalia-setup))
+
+;; -------------------------
+;; File browser: Dirvish
+;; -------------------------
+(use-package dirvish
+ :init
+ ;; Makes ordinary `dired' buffers use Dirvish automatically.
+ (dirvish-override-dired-mode)
+ :custom
+ (dirvish-quick-access-entries
+ '(("h" "~/" "Home")
+ ("d" "~/Downloads/" "Downloads")
+ ("c" "~/Code/" "Code")
+ ("e" "~/.emacs.d/" "Emacs")))
+ (dirvish-mode-line-format
+ '(:left (sort symlink) :right (omit yank index)))
+ (dirvish-attributes
+ '(nerd-icons file-time file-size collapse subtree-state vc-state git-msg))
+ (dirvish-side-attributes
+ '(vc-state file-size nerd-icons collapse))
+ :config
+ ;; Preview images, GIFs, PDFs, archives, audio, and video thumbnails.
+ ;;
+ ;; Recommended system packages:
+ ;; Arch: sudo pacman -S ffmpegthumbnailer libvips poppler mpv imv
+ ;; Debian/Ubuntu: sudo apt install ffmpegthumbnailer libvips-tools poppler-utils mpv imv
+ ;;
+ ;; Notes:
+ ;; - Images preview automatically at point.
+ ;; - Videos preview as thumbnails; open them in mpv for playback.
+ (setq dirvish-preview-dispatchers
+ '(image gif video audio epub archive font pdf fallback))
+
+ ;; Open media asynchronously so Emacs/EXWM does not block while mpv/imv is open.
+ (defun my/open-file-externally (&optional file)
+ "Open FILE asynchronously using a suitable external program."
+ (interactive)
+ (let* ((file (or file (dired-get-file-for-visit)))
+ (lower (downcase file))
+ (program
+ (cond
+ ((string-match-p "\\.\\(mp4\\|mkv\\|webm\\|mov\\|avi\\)\\'" lower) "mpv")
+ ((string-match-p "\\.\\(png\\|jpg\\|jpeg\\|gif\\|webp\\|svg\\)\\'" lower) "imv")
+ (t "xdg-open"))))
+ (start-process "open-file-externally" nil program file)))
+
+ ;; Dired's `!' runs a foreground shell command unless you add `&'.
+ ;; This keeps `o' as a non-blocking media opener.
+
+ :bind
+ (("C-c f d" . dirvish)
+ ("C-c f j" . dirvish-side)
+ :map dirvish-mode-map
+ ("q" . quit-window)
+ ("TAB" . dirvish-subtree-toggle)
+ ("M-t" . dired-next-line)
+ ("M-n" . dired-previous-line)
+ ("RET" . dired-find-file)
+ ("o" . my/open-file-externally)
+ ("SPC" . dirvish-show-history)
+ ("a" . dirvish-quick-access)
+ ("f" . dirvish-file-info-menu)
+ ("y" . dirvish-yank-menu)
+ ("N" . dirvish-narrow)
+ ("^" . dired-up-directory)))
+
+;; -------------------------
+;; Which-key
+;; -------------------------
+(use-package which-key
+ :config
+ (which-key-mode)
+ (setq which-key-idle-delay 0.5))
+
+;; -------------------------
+;; Git
+;; -------------------------
+(use-package magit
+ :bind (("C-x g" . magit-status)))
+
+(use-package diff-hl
+ :init
+ (global-diff-hl-mode)
+ (add-hook 'magit-post-refresh-hook #'diff-hl-magit-post-refresh)
+ (add-hook 'prog-mode-hook #'diff-hl-mode))
+
+;; -------------------------
+;; Project
+;; -------------------------
+(use-package project
+ :ensure nil)
+
+;; -------------------------
+;; Browser / URL handling
+;; -------------------------
+(defvar my/browser "firefox")
+
+(defun my/open-browser ()
+ "Open the external browser."
+ (interactive)
+ (start-process-shell-command "browser" nil my/browser))
+
+(defun my/browse-url (url &optional _new-window)
+ "Open URL in the external browser."
+ (start-process-shell-command
+ "browser-url" nil
+ (format "%s %s" my/browser (shell-quote-argument url))))
+
+(setq browse-url-browser-function #'my/browse-url)
+
+;; -------------------------
+;; Dvorak-friendly movement fallbacks
+;;
+;; C-s and C-r are left as normal Emacs search keys:
+;; C-s = isearch-forward
+;; C-r = isearch-backward
+;;
+;; Dvorak movement is available on M-h/M-t/M-n/M-s:
+;; M-h = left
+;; M-t = down
+;; M-n = up
+;; M-s = right
+;; -------------------------
+(global-set-key (kbd "C-s") #'isearch-forward)
+(global-set-key (kbd "C-r") #'isearch-backward)
+
+(global-set-key (kbd "M-h") #'backward-char)
+(global-set-key (kbd "M-t") #'next-line)
+(global-set-key (kbd "M-n") #'previous-line)
+(global-set-key (kbd "M-s") #'forward-char)
+
+(use-package avy
+ :bind (("C-:" . avy-goto-char)
+ ("C-'" . avy-goto-word-1)))
+
+(use-package repeat
+ :ensure nil
+ :init
+ (repeat-mode))
+
+;; -------------------------
+;; Typst
+;; -------------------------
+;; Requires system binaries:
+;; typst - Typst compiler
+;; tinymist - Typst language server
+;;
+;; Arch/Artix:
+;; paru -S typst tinymist
+;;
+;; Then in Emacs:
+;; M-x treesit-install-language-grammar RET typst RET
+
+(use-package treesit
+ :ensure nil
+ :init
+ (add-to-list 'treesit-language-source-alist
+ '(typst "https://github.com/uben0/tree-sitter-typst")))
+
+(use-package typst-ts-mode
+ :mode ("\\.typ\\'" . typst-ts-mode)
+ :hook ((typst-ts-mode . eglot-ensure)
+ (typst-ts-mode . visual-line-mode))
+ :custom
+ (typst-ts-mode-watch-options "--open")
+ :bind (:map typst-ts-mode-map
+ ("C-c C-c" . typst-ts-compile)
+ ("C-c C-w" . typst-ts-watch-mode)
+ ("C-c C-p" . typst-ts-preview)))
+;; -------------------------
+;; Leader-like keybindings using C-c
+;; -------------------------
+
+;; GPT
+(global-set-key (kbd "C-c a a") #'gptel)
+(global-set-key (kbd "C-c a s") #'gptel-send)
+(global-set-key (kbd "C-c a r") #'gptel-rewrite)
+
+;; Buffers
+(global-set-key (kbd "C-c b b") #'consult-buffer)
+(global-set-key (kbd "C-c b d") #'kill-current-buffer)
+(global-set-key (kbd "C-c b n") #'next-buffer)
+(global-set-key (kbd "C-c b p") #'previous-buffer)
+(global-set-key (kbd "C-c b s") #'save-buffer)
+
+;; Files
+(global-set-key (kbd "C-c f f") #'find-file)
+(global-set-key (kbd "C-c f d") #'dirvish)
+(global-set-key (kbd "C-c f j") #'dirvish-side)
+(global-set-key (kbd "C-c f r") #'consult-recent-file)
+(global-set-key (kbd "C-c f s") #'save-buffer)
+
+;; Open external browser
+(global-set-key (kbd "C-c o b") #'my/open-browser)
+
+;; Windows — h/t/n/s mirrors your Dvorak movement
+(global-set-key (kbd "C-c w h") #'windmove-left)
+(global-set-key (kbd "C-c w t") #'windmove-down)
+(global-set-key (kbd "C-c w n") #'windmove-up)
+(global-set-key (kbd "C-c w s") #'windmove-right)
+(global-set-key (kbd "C-c w d") #'delete-window)
+(global-set-key (kbd "C-c w v") #'split-window-right)
+(global-set-key (kbd "C-c w -") #'split-window-below)
+(global-set-key (kbd "C-c w H") #'shrink-window-horizontally)
+(global-set-key (kbd "C-c w S") #'enlarge-window-horizontally)
+(global-set-key (kbd "C-c w N") #'enlarge-window)
+(global-set-key (kbd "C-c w T") #'shrink-window)
+
+;; Project
+(global-set-key (kbd "C-c p p") #'project-switch-project)
+(global-set-key (kbd "C-c p f") #'project-find-file)
+(global-set-key (kbd "C-c p g") #'consult-ripgrep)
+(global-set-key (kbd "C-c p b") #'consult-project-buffer)
+
+;; Git
+(global-set-key (kbd "C-c g g") #'magit-status)
+(global-set-key (kbd "C-c g b") #'magit-blame)
+(global-set-key (kbd "C-c g l") #'magit-log-current)
+(global-set-key (kbd "C-c g d") #'magit-diff-buffer-file)
+
+;; LSP / Code
+(global-set-key (kbd "C-c c a") #'eglot-code-actions)
+(global-set-key (kbd "C-c c r") #'eglot-rename)
+(global-set-key (kbd "C-c c f") #'eglot-format-buffer)
+(global-set-key (kbd "C-c c d") #'xref-find-definitions)
+(global-set-key (kbd "C-c c D") #'xref-find-references)
+(global-set-key (kbd "C-c c i") #'consult-imenu)
+
+;; Typst
+(global-set-key (kbd "C-c y c") #'typst-ts-compile)
+(global-set-key (kbd "C-c y w") #'typst-ts-watch-mode)
+(global-set-key (kbd "C-c y p") #'typst-ts-preview)
+
+;; Errors
+(global-set-key (kbd "C-c e n") #'flymake-goto-next-error)
+(global-set-key (kbd "C-c e p") #'flymake-goto-prev-error)
+(global-set-key (kbd "C-c e l") #'flymake-show-buffer-diagnostics)
+
+;; Search
+(global-set-key (kbd "C-c s s") #'consult-line)
+(global-set-key (kbd "C-c s g") #'consult-ripgrep)
+(global-set-key (kbd "C-c s i") #'consult-imenu)
+
+;; Toggles
+(global-set-key (kbd "C-c t s") #'flyspell-mode)
+(global-set-key (kbd "C-c t w") #'whitespace-mode)
+
+;; Spell language
+;; C-c l is already used by xkbmap-switcher, so use C-c d for dictionaries.
+(global-set-key (kbd "C-c d e") #'my/ispell-en)
+(global-set-key (kbd "C-c d c") #'my/ispell-cs)
+
+;; Quit
+(global-set-key (kbd "C-c q q") #'save-buffers-kill-emacs)
+
+;; -------------------------
+;; Eglot (LSP)
+;; -------------------------
+(use-package eglot
+ :ensure nil
+ :hook ((c-mode . eglot-ensure)
+ (c++-mode . eglot-ensure)
+ (c-ts-mode . eglot-ensure)
+ (c++-ts-mode . eglot-ensure)
+ (nasm-mode . eglot-ensure)
+ (python-mode . eglot-ensure)
+ (python-ts-mode . eglot-ensure)
+ (typst-ts-mode . eglot-ensure))
+ :custom
+ (eglot-autoshutdown t)
+ (eglot-confirm-server-initiated-edits nil)
+ :config
+ (defun my/eglot-format-on-save ()
+ (add-hook 'before-save-hook #'eglot-format-buffer nil t))
+
+ (add-hook 'c-mode-hook #'my/eglot-format-on-save)
+ (add-hook 'c++-mode-hook #'my/eglot-format-on-save)
+ (add-hook 'c-ts-mode-hook #'my/eglot-format-on-save)
+ (add-hook 'c++-ts-mode-hook #'my/eglot-format-on-save)
+
+ (setq c-ts-mode-indent-offset 8
+ c-ts-mode-indent-style 'bsd)
+
+ (add-to-list 'eglot-server-programs
+ '((c-mode c++-mode c-ts-mode c++-ts-mode)
+ . ("clangd"
+ "--background-index"
+ "--clang-tidy"
+ "--completion-style=detailed"
+ "--header-insertion=never"
+ "--fallback-style=none")))
+
+ (add-to-list 'eglot-server-programs
+ '(python-mode . ("pylsp")))
+
+ (add-to-list 'eglot-server-programs
+ '(typst-ts-mode . ("tinymist"))))
+
+;; -------------------------
+;; Flymake (used by Eglot)
+;; -------------------------
+(use-package flymake
+ :ensure nil
+ :custom
+ (flymake-no-changes-timeout 1.0)
+ :bind (:map flymake-mode-map
+ ("M-n" . flymake-goto-next-error)
+ ("M-p" . flymake-goto-prev-error)))
+
+;; -------------------------
+;; NASM mode
+;; -------------------------
+(use-package nasm-mode
+ :hook (asm-mode . nasm-mode))
+
+;; -------------------------
+;; EXWM
+;; -------------------------
+(use-package exwm
+ :config
+ (setq exwm-workspace-number 5)
+
+ (add-hook 'exwm-update-title-hook
+ (lambda ()
+ (exwm-workspace-rename-buffer
+ (format "%s: %s" exwm-class-name exwm-title))))
+
+ (setq mouse-autoselect-window t
+ focus-follows-mouse t)
+
+ (setq exwm-layout-show-all-buffers t
+ exwm-workspace-show-all-buffers t)
+
+ (defvar my/terminal "kitty")
+
+ (setq exwm-input-global-keys
+ `((,(kbd "s-r") . exwm-reset)
+
+ (,(kbd "s-<return>")
+ . (lambda ()
+ (interactive)
+ (start-process-shell-command "term" nil my/terminal)))
+
+ (,(kbd "s-b") . my/open-browser)
+
+ (,(kbd "s-d") . dirvish)
+
+ (,(kbd "s-x")
+ . (lambda ()
+ (interactive)
+ (start-process-shell-command "lock" nil "betterscreenlock -l")))
+
+ (,(kbd "s-q") . kill-buffer)
+ (,(kbd "s-f") . exwm-floating-toggle-floating)
+
+ ;; Window resize — uppercase H/T/N/S mirrors movement
+ (,(kbd "s-H") . shrink-window-horizontally)
+ (,(kbd "s-S") . enlarge-window-horizontally)
+ (,(kbd "s-N") . enlarge-window)
+ (,(kbd "s-T") . shrink-window)
+
+ ;; Window focus — lowercase mirrors movement
+ (,(kbd "s-h") . windmove-left)
+ (,(kbd "s-s") . windmove-right)
+ (,(kbd "s-n") . windmove-up)
+ (,(kbd "s-t") . windmove-down)
+
+ ,@(mapcar
+ (lambda (i)
+ `(,(kbd (format "s-%d" i))
+ . (lambda ()
+ (interactive)
+ (exwm-workspace-switch ,i))))
+ (number-sequence 0 4))
+
+ (,(kbd "s-<tab>") . exwm-workspace-switch-next)
+ (,(kbd "s-S-<tab>") . exwm-workspace-switch-previous)
+ (,(kbd "s-w") . exwm-workspace-move-window)
+
+ (,(kbd "s-&")
+ . (lambda (command)
+ (interactive (list (read-shell-command "$ ")))
+ (start-process-shell-command command nil command)))))
+
+ (setq exwm-manage-configurations
+ '(((string-match-p "dialog" exwm-class-name) floating t)
+ ((string-match-p "confirm" exwm-title) floating t)))
+
+ ;; NOTE: Your monitor names differ here from the xrandr command below.
+ ;; Keep whichever names are correct on your machine.
+ (setq exwm-randr-workspace-monitor-plist
+ '(0 "DP-4"
+ 1 "HDMI-0"))
+
+ (add-hook 'exwm-randr-screen-change-hook
+ (lambda ()
+ (start-process-shell-command
+ "xrandr" nil
+ "xrandr --output DP-3 --primary --auto \
+ --output HDMI-1 --auto --left-of DP-3")))
+
+ (exwm-randr-mode 1)
+
+ (defun my/exwm-init ()
+ (start-process-shell-command "pipewire" nil "pipewire")
+ (start-process-shell-command "pipewire-pulse" nil "pipewire-pulse")
+ (start-process-shell-command "wireplumber" nil "wireplumber"))
+
+ (add-hook 'exwm-init-hook #'my/exwm-init)
+
+ (when (and (getenv "DISPLAY")
+ (not (getenv "EXWM_SKIP")))
+ (exwm-enable)))
+
+;; -------------------------
+;; Audio keys
+;; -------------------------
+(global-set-key (kbd "<XF86AudioRaiseVolume>")
+ (lambda ()
+ (interactive)
+ (start-process-shell-command
+ "vol-up" nil "amixer set Master 5%+")))
+
+(global-set-key (kbd "<XF86AudioLowerVolume>")
+ (lambda ()
+ (interactive)
+ (start-process-shell-command
+ "vol-down" nil "amixer set Master 5%-")))
+
+(global-set-key (kbd "<XF86AudioMute>")
+ (lambda ()
+ (interactive)
+ (start-process-shell-command
+ "mute" nil "amixer set Master toggle")))
+
+;; -------------------------
+;; Navidrome
+;; -------------------------
+(use-package naviel
+ :load-path "~/Code/naviel/"
+ :config
+ (setq naviel-url "http://navi.cdatgoose.org")
+ (setq naviel-eq-backend 'lavfi))
+
+(use-package tasker
+ :load-path "~/Code/tasker"
+ :commands (tasker-list tasker-new tasker-open tasker-add-code-link)
+ :bind (:map tasker-list-mode-map
+ ("q" . quit-window)))
+
+
+
+
+;; -------------------------
+;; EAF
+;; -------------------------
+;; EAF is kept available, but it no longer overrides `browse-url'.
+;; Use `eaf-open-browser' manually when you specifically want EAF.
+(use-package eaf
+ :load-path "~/.emacs.d/site-lisp/emacs-application-framework"
+ :custom
+ (eaf-browser-continue-where-left-off t)
+ (eaf-browser-enable-adblocker t)
+ :config
+ (defalias 'browse-web #'eaf-open-browser))
+
+;; -------------------------
+;; XKB switcher (Dvorak-friendly)
+;; -------------------------
+(use-package xkbmap-switcher
+ :load-path "~/Code/keyswtch/"
+ :config
+ (global-set-key (kbd "C-c l") #'xkbmap-switcher-cycle)
+ (global-set-key (kbd "C-c L") #'xkbmap-switcher-set))
+
+;; -------------------------
+;; Spell checking (Hunspell EN + CZ)
+;; -------------------------
+(use-package flyspell
+ :ensure nil
+ :hook ((text-mode . flyspell-mode)
+ (prog-mode . flyspell-prog-mode))
+ :init
+ (setq ispell-program-name "hunspell")
+ (setq ispell-dictionary "en_US")
+ (setq ispell-local-dictionary-alist
+ '(("en_US"
+ "[A-Za-z]" "[^A-Za-z]" "[']" nil
+ ("-d" "en_US") nil utf-8)
+ ("cs_CZ"
+ "[A-Za-zÁ-ž]" "[^A-Za-zÁ-ž]" "[']" nil
+ ("-d" "cs_CZ") nil utf-8))))
+
+;; -------------------------
+;; Quick language switching
+;; -------------------------
+(defun my/ispell-en ()
+ "Switch spell check to English."
+ (interactive)
+ (ispell-change-dictionary "en_US")
+ (message "Switched to English (en_US)"))
+
+(defun my/ispell-cs ()
+ "Switch spell check to Czech."
+ (interactive)
+ (ispell-change-dictionary "cs_CZ")
+ (message "Switched to Czech (cs_CZ)"))
+
+(global-set-key (kbd "C-c s e") #'my/ispell-en)
+(global-set-key (kbd "C-c s c") #'my/ispell-cs)
+
+;; -------------------------
+;; Custom
+;; -------------------------
+(custom-set-variables
+ ;; custom-set-variables was added by Custom.
+ ;; If you edit it by hand, you could mess it up, so be careful.
+ ;; Your init file should contain only one such instance.
+ ;; If there is more than one, they won't work right.
+ '(org-safe-remote-resources
+ '("\\`/ssh:apps@5\\.189\\.176\\.18:/home/apps/portfolio/blog/org/thoughts\\.org\\'"
+ "\\`/ssh:apps@cdatgoose\\.org:/home/apps/portfolio/blog/org/thoughts\\.org\\'"
+ "\\`https://fniessen\\.github\\.io/org-html-themes/org/html-theme-readtheorg\\.setup\\'"
+ "\\`https://fniessen\\.github\\.io/org-html-themes/org/html-theme-bigblow\\.setup\\'"))
+ '(package-selected-packages
+ '(avy cape corfu diff-hl dirvish embark-consult empv exwm gptel
+ gruvbox-theme ibrowse magit marginalia nasm-mode naviel
+ nerd-icons-completion orderless tidal typst-ts-mode vertico
+ websocket xkbmap-switcher xkcd)))
+
+(custom-set-faces
+ ;; custom-set-faces was added by Custom.
+ ;; If you edit it by hand, you could mess it up, so be careful.
+ ;; Your init file should contain only one such instance.
+ ;; If there is more than one, they won't work right.
+ )
+
+;;; init.el ends here
diff --git a/dunst/dmenu.sh b/dunst/dmenu.sh
new file mode 100755
index 0000000..ec42048
--- /dev/null
+++ b/dunst/dmenu.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+exec /usr/bin/dmenu \
+ -p "dunst:" \
+ -fn "FiraCode Nerd Font-12" \
+ -nb "#282828" \
+ -nf "#ebdbb2" \
+ -sb "#d79921" \
+ -sf "#282828" \
+ -i
diff --git a/dunst/dunstrc b/dunst/dunstrc
new file mode 100644
index 0000000..5bbac66
--- /dev/null
+++ b/dunst/dunstrc
@@ -0,0 +1,107 @@
+[global]
+ monitor = 0
+ follow = keyboard
+
+ width = 360
+ height = (80, 260)
+ origin = top-right
+ offset = (12, 42)
+ scale = 0
+ notification_limit = 5
+
+ progress_bar = true
+ progress_bar_height = 8
+ progress_bar_frame_width = 0
+ progress_bar_min_width = 160
+ progress_bar_max_width = 340
+ progress_bar_corner_radius = 3
+
+ icon_corner_radius = 4
+ indicate_hidden = yes
+ transparency = 4
+ separator_height = 0
+ padding = 10
+ horizontal_padding = 12
+ text_icon_padding = 10
+ frame_width = 2
+ frame_color = "#d79921"
+ gap_size = 8
+ separator_color = frame
+ sort = urgency_descending
+
+ font = FiraCode Nerd Font 10
+ line_height = 2
+ markup = full
+ format = "<b>%s</b>\n%b"
+ alignment = left
+ vertical_alignment = center
+ show_age_threshold = 60
+ ellipsize = end
+ ignore_newline = no
+ stack_duplicates = true
+ hide_duplicate_count = false
+ show_indicators = yes
+ enable_posix_regex = true
+
+ enable_recursive_icon_lookup = true
+ icon_theme = Adwaita
+ icon_position = left
+ min_icon_size = 32
+ max_icon_size = 64
+
+ sticky_history = yes
+ history_length = 30
+
+ dmenu = /home/aag/.config/dunst/dmenu.sh
+ browser = /usr/bin/xdg-open
+ always_run_script = true
+ title = Dunst
+ class = Dunst
+ corner_radius = 5
+ corners = all
+ ignore_dbusclose = false
+ force_xwayland = false
+ force_xinerama = false
+
+ mouse_left_click = do_action, close_current
+ mouse_middle_click = context
+ mouse_right_click = close_all
+
+[experimental]
+ per_monitor_dpi = false
+ pause_on_mouse_over = false
+
+[urgency_low]
+ background = "#282828"
+ foreground = "#928374"
+ frame_color = "#504945"
+ highlight = "#689d6a"
+ timeout = 4
+ default_icon = dialog-information
+
+[urgency_normal]
+ background = "#282828"
+ foreground = "#ebdbb2"
+ frame_color = "#d79921"
+ highlight = "#d79921"
+ timeout = 6
+ default_icon = dialog-information
+
+[urgency_critical]
+ background = "#3c3836"
+ foreground = "#ebdbb2"
+ frame_color = "#cc241d"
+ highlight = "#cc241d"
+ timeout = 0
+ default_icon = dialog-warning
+
+[fullscreen_delay]
+ fullscreen = pushback
+
+[fullscreen_show_critical]
+ msg_urgency = critical
+ fullscreen = show
+
+[transient_history_ignore]
+ match_transient = yes
+ history_ignore = yes
diff --git a/kitty/kitty.conf b/kitty/kitty.conf
new file mode 100644
index 0000000..bfd1500
--- /dev/null
+++ b/kitty/kitty.conf
@@ -0,0 +1,190 @@
+# ============================================================
+# ~/.config/kitty/kitty.conf — gruvbox dark
+# pairs with .tmux.conf
+# ============================================================
+
+
+# ------------------------------------------------------------
+# font
+# ------------------------------------------------------------
+font_family JetBrains Mono Regular
+bold_font JetBrains Mono Bold
+italic_font JetBrains Mono Italic
+bold_italic_font JetBrains Mono Bold Italic
+
+font_size 13.0
+
+# fine-tune spacing
+adjust_line_height 2
+adjust_column_width 0
+disable_ligatures never
+
+
+# ------------------------------------------------------------
+# gruvbox dark colour scheme
+# ------------------------------------------------------------
+
+# background / foreground
+background #282828
+foreground #ebdbb2
+cursor #ebdbb2
+background_opacity 0.88
+# selection
+selection_background #d79921
+selection_foreground #282828
+
+# url underline
+url_color #83a598
+
+# black
+color0 #282828
+color8 #928374
+
+# red
+color1 #cc241d
+color9 #fb4934
+
+# green
+color2 #98971a
+color10 #b8bb26
+
+# yellow
+color3 #d79921
+color11 #fabd2f
+
+# blue
+color4 #458588
+color12 #83a598
+
+# purple
+color5 #b16286
+color13 #d3869b
+
+# cyan / aqua
+color6 #689d6a
+color14 #8ec07c
+
+# white
+color7 #a89984
+color15 #ebdbb2
+
+
+# ------------------------------------------------------------
+# cursor
+# ------------------------------------------------------------
+cursor_shape block
+cursor_blink_interval 0
+
+
+# ------------------------------------------------------------
+# scrollback
+# ------------------------------------------------------------
+scrollback_lines 10000
+scrollback_pager less --chop-long-lines --RAW-CONTROL-CHARS +INPUT_LINE_NUMBER
+
+
+# ------------------------------------------------------------
+# window / padding
+# ------------------------------------------------------------
+window_padding_width 8
+placement_strategy center
+hide_window_decorations no
+remember_window_size yes
+initial_window_width 220c
+initial_window_height 50c
+
+
+# ------------------------------------------------------------
+# tab bar — matches tmux status style
+# ------------------------------------------------------------
+tab_bar_edge bottom
+tab_bar_style powerline
+tab_powerline_style slanted
+tab_bar_background #3c3836
+tab_bar_margin_color #3c3836
+
+tab_title_template "{index}:{title}"
+active_tab_title_template "{index}:{title}"
+
+active_tab_foreground #282828
+active_tab_background #83a598
+active_tab_font_style bold
+
+inactive_tab_foreground #a89984
+inactive_tab_background #3c3836
+inactive_tab_font_style normal
+
+
+# ------------------------------------------------------------
+# bell
+# ------------------------------------------------------------
+enable_audio_bell no
+visual_bell_duration 0.0
+
+
+# ------------------------------------------------------------
+# performance
+# ------------------------------------------------------------
+repaint_delay 10
+input_delay 3
+sync_to_monitor yes
+background_blur 19
+
+# ------------------------------------------------------------
+# mouse
+# ------------------------------------------------------------
+mouse_hide_wait 3.0
+focus_follows_mouse no
+copy_on_select clipboard
+
+
+# ------------------------------------------------------------
+# keyboard shortcuts
+# ------------------------------------------------------------
+# clear defaults
+clear_all_shortcuts no
+
+# copy / paste
+map ctrl+shift+c copy_to_clipboard
+map ctrl+shift+v paste_from_clipboard
+
+# font size
+map ctrl+shift+equal change_font_size all +1.0
+map ctrl+shift+minus change_font_size all -1.0
+map ctrl+shift+0 change_font_size all 0
+
+# tabs
+map ctrl+shift+t new_tab_with_cwd
+map ctrl+shift+w close_tab
+map ctrl+shift+right next_tab
+map ctrl+shift+left previous_tab
+map ctrl+shift+1 goto_tab 1
+map ctrl+shift+2 goto_tab 2
+map ctrl+shift+3 goto_tab 3
+map ctrl+shift+4 goto_tab 4
+map ctrl+shift+5 goto_tab 5
+
+# windows (kitty splits — complement to tmux panes)
+map ctrl+shift+enter new_window_with_cwd
+map ctrl+shift+] next_window
+map ctrl+shift+[ previous_window
+
+# scrollback
+map ctrl+shift+up scroll_line_up
+map ctrl+shift+down scroll_line_down
+map ctrl+shift+page_up scroll_page_up
+map ctrl+shift+page_down scroll_page_down
+map ctrl+shift+home scroll_home
+map ctrl+shift+end scroll_end
+
+# reload config
+map ctrl+shift+F5 load_config_file
+
+map ctrl+shift+s launch --title="ssh picker" --hold bash ~/.config/kitty/ssh_picker.sh
+
+
+
+# ------------------------------------------------------------
+# shell integration
+# ------------------------------------------------------------
+shell_integration enabled
diff --git a/kitty/ssh_picker.sh b/kitty/ssh_picker.sh
new file mode 100755
index 0000000..6ad6e3a
--- /dev/null
+++ b/kitty/ssh_picker.sh
@@ -0,0 +1,202 @@
+#!/usr/bin/env bash
+# ~/.config/kitty/ssh_picker.sh
+# Launched by kitty as an overlay window.
+# Reads servers from ~/.ssh/config (Host entries) and lets
+# you fuzzy-filter with arrow keys, then opens kitten ssh.
+#
+# Dependencies: none beyond bash + standard coreutils.
+# Optional: fzf — if present it takes over the picker UI.
+
+set -euo pipefail
+
+# ------------------------------------------------------------
+# config — edit these or let them fall through to ~/.ssh/config
+# ------------------------------------------------------------
+SSH_CONFIG="${HOME}/.ssh/config"
+TERM_FALLBACK="xterm-256color"
+
+# ------------------------------------------------------------
+# gruvbox dark ANSI helpers
+# ------------------------------------------------------------
+RESET=$'\e[0m'
+BOLD=$'\e[1m'
+DIM=$'\e[2m'
+
+GB_BG=$'\e[48;2;40;40;40m' # #282828
+GB_BG1=$'\e[48;2;60;56;54m' # #3c3836
+GB_BG2=$'\e[48;2;80;73;69m' # #504945
+GB_YELLOW=$'\e[38;2;250;189;47m' # #fabd2f
+GB_BLUE=$'\e[38;2;131;165;152m' # #83a598
+GB_GREEN=$'\e[38;2;184;187;38m' # #b8bb26
+GB_RED=$'\e[38;2;251;73;52m' # #fb4934
+GB_ORANGE=$'\e[38;2;254;128;25m' # #fe8019
+GB_FG=$'\e[38;2;235;219;178m' # #ebdbb2
+GB_DIM=$'\e[38;2;168;153;132m' # #a89984
+
+# ------------------------------------------------------------
+# parse servers
+# ------------------------------------------------------------
+get_servers() {
+ local servers=()
+
+ # pull Host entries from ~/.ssh/config (skip wildcards)
+ if [[ -f "$SSH_CONFIG" ]]; then
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^[Hh]ost[[:space:]]+([^*?]+)$ ]]; then
+ local host="${BASH_REMATCH[1]// /}"
+ [[ -n "$host" ]] && servers+=("$host")
+ fi
+ done < "$SSH_CONFIG"
+ fi
+
+ # fallback examples if config is empty
+ if [[ ${#servers[@]} -eq 0 ]]; then
+ servers=(
+ "contabo server"
+ )
+ fi
+
+ printf '%s\n' "${servers[@]}"
+}
+
+# ------------------------------------------------------------
+# fzf path (preferred)
+# ------------------------------------------------------------
+if command -v fzf &>/dev/null; then
+ selected=$(get_servers | fzf \
+ --prompt=" ssh " \
+ --pointer=">" \
+ --marker="*" \
+ --height=40% \
+ --layout=reverse \
+ --border=rounded \
+ --info=inline \
+ --color="dark,\
+bg:#282828,\
+bg+:#3c3836,\
+fg:#ebdbb2,\
+fg+:#fbf1c7,\
+hl:#fabd2f,\
+hl+:#fabd2f,\
+border:#504945,\
+prompt:#83a598,\
+pointer:#fe8019,\
+marker:#b8bb26,\
+spinner:#d3869b,\
+header:#a89984,\
+info:#a89984" \
+ --header=" ctrl+c to cancel" \
+ --preview="echo {} | xargs -I% sh -c 'grep -A10 \"^Host %$\" ~/.ssh/config 2>/dev/null || echo \"no config entry\"'" \
+ --preview-window="right:40%:wrap" \
+ 2>/dev/null) || exit 0
+
+ [[ -z "$selected" ]] && exit 0
+ TERM="$TERM_FALLBACK" kitten ssh "$selected"
+ exit 0
+fi
+
+# ------------------------------------------------------------
+# built-in picker (no fzf)
+# ------------------------------------------------------------
+mapfile -t SERVERS < <(get_servers)
+TOTAL=${#SERVERS[@]}
+SELECTED=0
+QUERY=""
+FILTERED=("${SERVERS[@]}")
+
+draw() {
+ clear
+ printf '%s' "${GB_BG}"
+ printf '\n'
+ printf ' %s%s ssh picker%s\n' "${GB_YELLOW}${BOLD}" "" "${RESET}"
+ printf ' %s%s%s\n\n' "${GB_DIM}" "arrow keys to navigate, enter to connect, esc to quit" "${RESET}"
+
+ # search bar
+ printf ' %ssearch:%s %s%s%s\n\n' \
+ "${GB_DIM}" "${RESET}" \
+ "${GB_FG}${BOLD}" "${QUERY}" "${RESET}"
+
+ # server list
+ local i=0
+ for srv in "${FILTERED[@]}"; do
+ if [[ $i -eq $SELECTED ]]; then
+ printf ' %s%s > %s%s\n' "${GB_BG1}${GB_ORANGE}${BOLD}" "" "$srv" "${RESET}"
+ else
+ printf ' %s %s%s\n' "${GB_DIM}" "$srv" "${RESET}"
+ fi
+ (( i++ ))
+ done
+
+ printf '\n %s%s hosts found • install fzf for preview support%s\n' \
+ "${GB_DIM}" "${#FILTERED[@]}" "${RESET}"
+}
+
+filter() {
+ FILTERED=()
+ SELECTED=0
+ for srv in "${SERVERS[@]}"; do
+ if [[ -z "$QUERY" || "${srv,,}" == *"${QUERY,,}"* ]]; then
+ FILTERED+=("$srv")
+ fi
+ done
+}
+
+# hide cursor
+tput civis
+trap 'tput cnorm; clear' EXIT
+
+while true; do
+ draw
+
+ # read a single keypress (handles escape sequences)
+ IFS= read -r -s -N1 key
+
+ # check for escape sequences (arrow keys)
+ if [[ "$key" == $'\e' ]]; then
+ IFS= read -r -s -N1 -t 0.1 k2 || true
+ if [[ "$k2" == "[" ]]; then
+ IFS= read -r -s -N1 -t 0.1 k3 || true
+ case "$k3" in
+ A) # up
+ (( SELECTED > 0 )) && (( SELECTED-- ))
+ ;;
+ B) # down
+ (( SELECTED < ${#FILTERED[@]} - 1 )) && (( SELECTED++ ))
+ ;;
+ esac
+ else
+ # plain escape — quit
+ exit 0
+ fi
+ continue
+ fi
+
+ case "$key" in
+ $'\n'|$'\r')
+ [[ ${#FILTERED[@]} -eq 0 ]] && continue
+ tput cnorm
+ clear
+ host="${FILTERED[$SELECTED]}"
+ printf '%sconnecting to %s%s%s...\n\n' \
+ "${GB_DIM}" "${GB_BLUE}${BOLD}" "$host" "${RESET}"
+ TERM="$TERM_FALLBACK" kitten ssh "$host"
+ exit 0
+ ;;
+ $'\x7f'|$'\b')
+ # backspace
+ QUERY="${QUERY%?}"
+ filter
+ ;;
+ $'\x03')
+ # ctrl+c
+ exit 0
+ ;;
+ *)
+ # printable character — append to query
+ if [[ "$key" =~ ^[[:print:]]$ ]]; then
+ QUERY+="$key"
+ filter
+ fi
+ ;;
+ esac
+done
diff --git a/polybar/config.ini b/polybar/config.ini
new file mode 100644
index 0000000..649d289
--- /dev/null
+++ b/polybar/config.ini
@@ -0,0 +1,456 @@
+[colors]
+bg = #282828
+bg1 = #3c3836
+bg2 = #504945
+bg3 = #665c54
+bg4 = #7c6f64
+fg = #ebdbb2
+fg1 = #d5c4a1
+fg2 = #bdae93
+red = #cc241d
+green = #98971a
+yellow = #d79921
+orange = #d65d0e
+purple = #b16286
+aqua = #689d6a
+gray = #928374
+
+[bar/main]
+width = 100%
+height = 30
+monitor = ${env:MONITOR:}
+background = ${colors.bg}
+foreground = ${colors.fg}
+
+line-size = 2
+line-color = ${colors.yellow}
+border-bottom-size = 2
+border-color = ${colors.bg1}
+
+padding-left = 1
+padding-right = 2
+module-margin = 1
+
+font-0 = "FiraCode Nerd Font:style=Regular:size=11;3"
+font-1 = "FiraCode Nerd Font:style=Bold:size=11;3"
+
+modules-left = i3
+modules-center = date
+modules-right = ${env:POLYBAR_MODULES_RIGHT:bar-mode dunst awake pulseaudio keyboard wireguard net-speed network temp cpu memory filesystem tray}
+
+separator = "│"
+separator-foreground = ${colors.bg3}
+
+cursor-click = pointer
+
+fixed-center = true
+wm-restack = i3
+enable-ipc = true
+
+
+;; ── i3 ─────────────────────────────────────────
+
+[module/i3]
+type = internal/i3
+index-sort = true
+
+format = <label-state> <label-mode>
+
+label-mode = %mode%
+label-mode-background = ${colors.orange}
+label-mode-foreground = ${colors.bg}
+label-mode-padding = 2
+
+label-focused = %index%
+label-focused-foreground = ${colors.bg}
+label-focused-background = ${colors.yellow}
+label-focused-padding = 2
+
+label-unfocused = %index%
+label-unfocused-foreground = ${colors.fg2}
+label-unfocused-padding = 2
+
+label-visible = %index%
+label-visible-foreground = ${colors.fg1}
+label-visible-padding = 2
+
+label-urgent = %index%
+label-urgent-background = ${colors.red}
+label-urgent-foreground = ${colors.bg}
+label-urgent-padding = 2
+
+pin-workspaces = false
+scroll-up = i3wm-wsnext
+scroll-down = i3wm-wsprev
+
+label-empty = %index%
+label-empty-foreground = ${colors.bg3}
+label-empty-padding = 2
+
+;; ── Date ───────────────────────────────────────
+
+[module/date]
+type = internal/date
+interval = 5
+
+date = %a %d %b
+time = %H:%M
+
+format-prefix = "date "
+format-prefix-foreground = ${colors.aqua}
+
+label = %date% %time%
+
+
+;; ── Dunst ─────────────────────────────────────
+
+[module/dunst]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/dunst-status.sh
+interval = 2
+
+format-prefix = "ntf "
+format-prefix-foreground = ${colors.yellow}
+
+label = %output%
+
+click-left = dunstctl set-paused toggle
+click-middle = dunstctl history-pop
+click-right = dunstctl close-all
+
+
+;; ── Bar Mode ──────────────────────────────────
+
+[module/bar-mode]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/bar-mode-status.sh
+interval = 2
+
+format-prefix = "bar "
+format-prefix-foreground = ${colors.yellow}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/bar-mode-toggle.sh
+click-right = /home/aag/.config/polybar/scripts/bar-mode-toggle.sh compact
+click-middle = /home/aag/.config/polybar/scripts/bar-mode-toggle.sh full
+
+
+;; ── Force Awake ───────────────────────────────
+
+[module/awake]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/awake-status.sh
+interval = 2
+
+format-prefix = "wake "
+format-prefix-foreground = ${colors.orange}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/awake-toggle.sh
+click-middle = /home/aag/.config/polybar/scripts/awake-toggle.sh on
+click-right = /home/aag/.config/polybar/scripts/awake-toggle.sh off
+
+
+;; ── Updates ───────────────────────────────────
+
+[module/updates]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/updates-status.sh
+interval = 900
+
+format-prefix = "upd "
+format-prefix-foreground = ${colors.yellow}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/updates-action.sh
+
+
+;; ── Screenshot ────────────────────────────────
+
+[module/screenshot]
+type = custom/script
+exec = /usr/bin/printf menu
+interval = 3600
+
+format-prefix = "shot "
+format-prefix-foreground = ${colors.aqua}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/screenshot-menu.sh
+
+
+;; ── Power Menu ────────────────────────────────
+
+[module/power]
+type = custom/script
+exec = /usr/bin/printf menu
+interval = 3600
+
+format-prefix = "pwr "
+format-prefix-foreground = ${colors.red}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/power-menu.sh
+
+
+;; ── Clipboard ─────────────────────────────────
+
+[module/clipboard]
+type = custom/script
+exec = /usr/bin/printf menu
+interval = 3600
+
+format-prefix = "clip "
+format-prefix-foreground = ${colors.purple}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/clipboard-menu.sh
+
+
+;; ── Display ───────────────────────────────────
+
+[module/display]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/display-status.sh
+interval = 2
+
+format-prefix = "disp "
+format-prefix-foreground = ${colors.orange}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/display-toggle.sh
+scroll-up = /home/aag/.config/polybar/scripts/display-brightness.sh up
+scroll-down = /home/aag/.config/polybar/scripts/display-brightness.sh down
+
+
+;; ── Microphone ────────────────────────────────
+
+[module/mic]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/mic-status.sh
+interval = 2
+
+format-prefix = "mic "
+format-prefix-foreground = ${colors.purple}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/mic-toggle.sh
+
+
+;; ── PipeWire (via PulseAudio API) ─────────────
+
+[module/pulseaudio]
+type = internal/pulseaudio
+
+use-ui-max = true
+interval = 2
+
+format-volume = <label-volume>
+format-volume-prefix = "vol "
+format-volume-prefix-foreground = ${colors.green}
+format-muted = <label-muted>
+format-muted-prefix = "vol "
+format-muted-prefix-foreground = ${colors.gray}
+
+label-volume = %percentage%%
+label-muted = muted
+
+label-muted-foreground = ${colors.gray}
+
+click-left = pavucontrol
+click-right = pavucontrol
+click-middle = pactl set-sink-mute @DEFAULT_SINK@ toggle
+scroll-up = pactl set-sink-volume @DEFAULT_SINK@ +5%
+scroll-down = pactl set-sink-volume @DEFAULT_SINK@ -5%
+
+
+;; ── Music ─────────────────────────────────────
+
+[module/music]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/music-status.sh
+interval = 2
+
+format-prefix = "mus "
+format-prefix-foreground = ${colors.green}
+
+label = %output%
+
+click-left = playerctl play-pause
+click-middle = playerctl previous
+click-right = playerctl next
+
+
+;; ── Keyboard ──────────────────────────────────
+
+
+[module/keyboard]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/kb-status.sh
+interval = 2
+
+format-prefix = "kbd "
+format-prefix-foreground = ${colors.purple}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/kb-switch.sh
+
+
+;; ── WireGuard ─────────────────────────────────
+
+[module/wireguard]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/wg-pc-status.sh
+interval = 2
+
+format-prefix = "wg "
+format-prefix-foreground = ${colors.aqua}
+
+label = %output%
+
+click-left = /home/aag/.config/polybar/scripts/wg-pc-toggle.sh
+click-right = /home/aag/.config/polybar/scripts/wg-pc-toggle.sh down
+click-middle = /home/aag/.config/polybar/scripts/wg-pc-toggle.sh up
+
+
+;; ── Network ───────────────────────────────────
+
+[module/network]
+type = internal/network
+interface = ${env:POLYBAR_NETWORK:eth0}
+interval = 5
+
+format-connected = <label-connected>
+format-connected-prefix = "net "
+format-connected-prefix-foreground = ${colors.green}
+
+format-disconnected = <label-disconnected>
+format-disconnected-prefix = "net "
+format-disconnected-prefix-foreground = ${colors.red}
+
+label-connected = %local_ip%
+label-disconnected = offline
+
+
+;; ── Network Speed ─────────────────────────────
+
+[module/net-speed]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/net-speed.sh
+interval = 2
+
+format-prefix = "spd "
+format-prefix-foreground = ${colors.green}
+
+label = %output%
+
+
+;; ── Temperature ───────────────────────────────
+
+[module/temp]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/temp-status.sh
+interval = 5
+
+format-prefix = "tmp "
+format-prefix-foreground = ${colors.orange}
+
+label = %output%
+
+
+;; ── CPU ───────────────────────────────────────
+
+[module/cpu]
+type = internal/cpu
+interval = 2
+warn-percentage = 85
+
+format-prefix = "cpu "
+format-prefix-foreground = ${colors.orange}
+format-warn = <label-warn>
+format-warn-prefix = "cpu "
+format-warn-prefix-foreground = ${colors.red}
+
+label = %percentage%%
+label-warn = %percentage%%
+label-warn-foreground = ${colors.red}
+
+
+;; ── Memory ────────────────────────────────────
+
+[module/memory]
+type = internal/memory
+interval = 3
+warn-percentage = 80
+
+format-prefix = "ram "
+format-prefix-foreground = ${colors.purple}
+format-warn = <label-warn>
+format-warn-prefix = "ram "
+format-warn-prefix-foreground = ${colors.red}
+
+label = %percentage_used%%
+label-warn = %percentage_used%%
+label-warn-foreground = ${colors.red}
+
+
+;; ── Filesystem ────────────────────────────────
+
+[module/filesystem]
+type = internal/fs
+interval = 30
+warn-percentage = 85
+
+mount-0 = /
+
+format-mounted-prefix = "disk "
+format-mounted-prefix-foreground = ${colors.aqua}
+format-warn = <label-warn>
+format-warn-prefix = "disk "
+format-warn-prefix-foreground = ${colors.red}
+
+label-mounted = %percentage_used%%
+label-warn = %percentage_used%%
+label-warn-foreground = ${colors.red}
+label-unmounted = down
+label-unmounted-foreground = ${colors.red}
+
+
+;; ── Scratchpad ────────────────────────────────
+
+[module/scratchpad]
+type = custom/script
+exec = /home/aag/.config/polybar/scripts/scratchpad-status.sh
+interval = 2
+
+format-prefix = "scratch "
+format-prefix-foreground = ${colors.yellow}
+
+label = %output%
+
+click-left = i3-msg scratchpad show
+click-middle = i3-msg move scratchpad
+
+
+;; ── Tray ──────────────────────────────────────
+
+[module/tray]
+type = internal/tray
+
+format = <tray>
+format-margin = 0
+tray-spacing = 4pt
+
+
+[settings]
+screenchange-reload = true
+pseudo-transparency = false
diff --git a/polybar/config.ini~ b/polybar/config.ini~
new file mode 100644
index 0000000..18dfcaa
--- /dev/null
+++ b/polybar/config.ini~
@@ -0,0 +1,168 @@
+[colors]
+bg = #282828
+bg1 = #3c3836
+bg2 = #504945
+bg3 = #665c54
+bg4 = #7c6f64
+fg = #ebdbb2
+fg1 = #d5c4a1
+fg2 = #bdae93
+red = #cc241d
+green = #98971a
+yellow = #d79921
+orange = #d65d0e
+purple = #b16286
+aqua = #689d6a
+gray = #928374
+
+[bar/main]
+width = 100%
+height = 28
+monitor = ${env:MONITOR:}
+background = ${colors.bg}
+foreground = ${colors.fg}
+
+line-size = 2
+line-color = ${colors.yellow}
+
+padding-left = 1
+padding-right = 2
+module-margin = 1
+
+font-0 = "FiraCode Nerd Font:style=Regular:size=11;3"
+font-1 = "FiraCode Nerd Font:style=Bold:size=11;3"
+
+modules-left = i3
+modules-center = date
+modules-right = xkeyboard cpu memory filesystem
+
+separator = "│"
+separator-foreground = ${colors.bg3}
+
+cursor-click = pointer
+cursor-scroll = ns-resize
+
+enable-ipc = true
+
+
+;; ── i3 ─────────────────────────────────────────
+
+[module/i3]
+type = internal/i3
+index-sort = true
+
+format = <label-state>
+
+label-focused = %index%
+label-focused-foreground = ${colors.bg}
+label-focused-background = ${colors.yellow}
+label-focused-padding = 2
+
+label-unfocused = %index%
+label-unfocused-foreground = ${colors.fg2}
+label-unfocused-padding = 2
+
+label-visible = %index%
+label-visible-foreground = ${colors.fg1}
+label-visible-padding = 2
+
+label-urgent = %index%
+label-urgent-background = ${colors.red}
+label-urgent-foreground = ${colors.bg}
+label-urgent-padding = 2
+
+
+;; ── Date ───────────────────────────────────────
+
+[module/date]
+type = internal/date
+interval = 5
+
+date = %a %d %b
+time = %H:%M
+
+format-prefix = "time "
+format-prefix-foreground = ${colors.aqua}
+
+label = %date% %time%
+
+
+;; ── PipeWire (via PulseAudio API) ─────────────
+
+[module/pulseaudio]
+type = internal/pulseaudio
+
+use-ui-max = true
+interval = 2
+
+format-volume = <ramp-volume> <label-volume>
+format-muted = <label-muted>
+
+label-volume = %percentage%%
+label-muted = muted
+
+label-muted-foreground = ${colors.gray}
+
+ramp-volume-0 = vol-low
+ramp-volume-1 = vol-mid
+ramp-volume-2 = vol-high
+ramp-volume-foreground = ${colors.green}
+
+click-right = pavucontrol
+
+
+;; ── Keyboard ──────────────────────────────────
+
+
+[module/xkeyboard]
+type = internal/xkeyboard
+
+blacklist-0 = num lock
+blacklist-1 = scroll lock
+
+format-prefix = "kbd "
+format-prefix-foreground = ${colors.purple}
+
+label-layout = %layout%
+
+click-left = /usr/bin/env bash ~/.config/polybar/scripts/kb-switch.sh
+
+;; ── CPU ───────────────────────────────────────
+
+[module/cpu]
+type = internal/cpu
+interval = 2
+
+format-prefix = "cpu "
+format-prefix-foreground = ${colors.orange}
+
+label = %percentage%%
+
+
+;; ── Memory ────────────────────────────────────
+
+[module/memory]
+type = internal/memory
+interval = 3
+
+format-prefix = "ram "
+format-prefix-foreground = ${colors.purple}
+
+label = %percentage_used%%
+
+
+;; ── Filesystem ────────────────────────────────
+
+[module/filesystem]
+type = internal/fs
+interval = 30
+
+mount-0 = /
+
+format-mounted-prefix = "disk "
+format-mounted-prefix-foreground = ${colors.aqua}
+
+label-mounted = %percentage_used%%
+label-unmounted = down
+label-unmounted-foreground = ${colors.red}
+
diff --git a/polybar/launch.sh b/polybar/launch.sh
new file mode 100755
index 0000000..5b7e018
--- /dev/null
+++ b/polybar/launch.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/polybar/config.ini"
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+ mkdir -p "$state_dir" 2>/dev/null || true
+fi
+
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+
+mode_file="$state_dir/polybar-mode.state"
+compact_modules="bar-mode dunst awake pulseaudio keyboard wireguard net-speed network temp cpu memory filesystem tray"
+full_modules="bar-mode dunst awake updates screenshot power clipboard display mic pulseaudio music keyboard wireguard net-speed network temp cpu memory filesystem scratchpad tray"
+
+if [ -r "$mode_file" ] && [ "$(cat "$mode_file")" = "full" ]; then
+ export POLYBAR_MODULES_RIGHT="$full_modules"
+else
+ export POLYBAR_MODULES_RIGHT="$compact_modules"
+fi
+
+# Kill any running instances before i3 reloads the bar.
+killall -q polybar || true
+while pgrep -u "$UID" -x polybar >/dev/null; do
+ sleep 0.1
+done
+
+mapfile -t monitors < <(polybar --list-monitors 2>/dev/null | cut -d: -f1 || true)
+
+if [ "${#monitors[@]}" -eq 0 ]; then
+ polybar --reload main -c "$CONFIG" &
+ exit 0
+fi
+
+for monitor in "${monitors[@]}"; do
+ MONITOR="$monitor" polybar --reload main -c "$CONFIG" &
+done
diff --git a/polybar/launch.sh~ b/polybar/launch.sh~
new file mode 100755
index 0000000..7490090
--- /dev/null
+++ b/polybar/launch.sh~
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# Kill any running instances
+killall -q polybar
+while pgrep -u $UID -x polybar > /dev/null; do sleep 0.1; done
+
+# If you have multiple monitors, launch one bar per output:
+# for m in $(polybar --list-monitors | cut -d":" -f1); do
+# MONITOR=$m polybar main &
+# done
+
+polybar main 2>&1 | tee -a /tmp/polybar.log & disown
diff --git a/polybar/scripts/awake-status.sh b/polybar/scripts/awake-status.sh
new file mode 100755
index 0000000..98948f2
--- /dev/null
+++ b/polybar/scripts/awake-status.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+fi
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+state_file="$state_dir/polybar-awake.state"
+
+if [ -f "$state_file" ]; then
+ echo "%{F#98971a}on%{F-}"
+else
+ echo "%{F#928374}off%{F-}"
+fi
diff --git a/polybar/scripts/awake-toggle.sh b/polybar/scripts/awake-toggle.sh
new file mode 100755
index 0000000..608bbc2
--- /dev/null
+++ b/polybar/scripts/awake-toggle.sh
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+ mkdir -p "$state_dir"
+fi
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+state_file="$state_dir/polybar-awake.state"
+action="${1:-toggle}"
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+notify() {
+ if command -v dunstify >/dev/null 2>&1; then
+ dunstify -a polybar -u low "Force awake" "$1"
+ fi
+}
+
+save_current_state() {
+ local query
+ query="$(xset q)"
+
+ {
+ printf 'blanking=%s\n' "$(printf '%s\n' "$query" | awk '/prefer blanking:/ { print $3; exit }')"
+ printf 'exposures=%s\n' "$(printf '%s\n' "$query" | awk '/allow exposures:/ { print $6; exit }')"
+ printf 'timeout=%s\n' "$(printf '%s\n' "$query" | awk '/timeout:/ { print $2; exit }')"
+ printf 'cycle=%s\n' "$(printf '%s\n' "$query" | awk '/timeout:/ { print $4; exit }')"
+ printf 'standby=%s\n' "$(printf '%s\n' "$query" | awk '/Standby:/ { print $2; exit }')"
+ printf 'suspend=%s\n' "$(printf '%s\n' "$query" | awk '/Standby:/ { print $4; exit }')"
+ printf 'off=%s\n' "$(printf '%s\n' "$query" | awk '/Standby:/ { print $6; exit }')"
+ printf 'dpms=%s\n' "$(printf '%s\n' "$query" | awk '/DPMS is/ { print $3; exit }')"
+ } >"$state_file"
+}
+
+enable_awake() {
+ if [ ! -f "$state_file" ]; then
+ save_current_state
+ fi
+
+ xset s noblank
+ xset s off
+ xset -dpms
+ notify "enabled"
+}
+
+restore_value() {
+ local name="$1"
+ local fallback="$2"
+ local value
+
+ value="$(awk -F= -v key="$name" '$1 == key { print $2; exit }' "$state_file" 2>/dev/null || true)"
+ printf '%s' "${value:-$fallback}"
+}
+
+disable_awake() {
+ if [ -f "$state_file" ]; then
+ local blanking exposures timeout cycle standby suspend off dpms
+ blanking="$(restore_value blanking yes)"
+ exposures="$(restore_value exposures yes)"
+ timeout="$(restore_value timeout 600)"
+ cycle="$(restore_value cycle 600)"
+ standby="$(restore_value standby 600)"
+ suspend="$(restore_value suspend 600)"
+ off="$(restore_value off 600)"
+ dpms="$(restore_value dpms Enabled)"
+
+ if [ "$blanking" = "yes" ]; then
+ xset s blank
+ else
+ xset s noblank
+ fi
+
+ if [ "$exposures" = "yes" ]; then
+ xset s expose
+ else
+ xset s noexpose
+ fi
+
+ xset s "$timeout" "$cycle"
+ xset dpms "$standby" "$suspend" "$off"
+
+ if [ "$dpms" = "Enabled" ]; then
+ xset +dpms
+ else
+ xset -dpms
+ fi
+
+ rm -f "$state_file"
+ else
+ xset s on
+ xset +dpms
+ fi
+
+ notify "disabled"
+}
+
+case "$action" in
+ on)
+ enable_awake
+ ;;
+ off)
+ disable_awake
+ ;;
+ toggle)
+ if [ -f "$state_file" ]; then
+ disable_awake
+ else
+ enable_awake
+ fi
+ ;;
+ *)
+ notify "unknown action: $action"
+ exit 2
+ ;;
+esac
diff --git a/polybar/scripts/bar-mode-status.sh b/polybar/scripts/bar-mode-status.sh
new file mode 100755
index 0000000..7e6d787
--- /dev/null
+++ b/polybar/scripts/bar-mode-status.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+fi
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+
+mode_file="$state_dir/polybar-mode.state"
+
+if [ -r "$mode_file" ] && [ "$(cat "$mode_file")" = "full" ]; then
+ echo "%{F#d79921}less%{F-}"
+else
+ echo "%{F#98971a}more%{F-}"
+fi
diff --git a/polybar/scripts/bar-mode-toggle.sh b/polybar/scripts/bar-mode-toggle.sh
new file mode 100755
index 0000000..d943df1
--- /dev/null
+++ b/polybar/scripts/bar-mode-toggle.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+requested="${1:-toggle}"
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+ mkdir -p "$state_dir" 2>/dev/null || true
+fi
+
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+
+mode_file="$state_dir/polybar-mode.state"
+
+current="compact"
+if [ -r "$mode_file" ]; then
+ current="$(cat "$mode_file")"
+fi
+
+case "$requested" in
+ full|expand|expanded)
+ next="full"
+ ;;
+ compact|minimize|minimized)
+ next="compact"
+ ;;
+ toggle)
+ if [ "$current" = "full" ]; then
+ next="compact"
+ else
+ next="full"
+ fi
+ ;;
+ *)
+ exit 2
+ ;;
+esac
+
+printf '%s\n' "$next" >"$mode_file"
+
+if command -v dunstify >/dev/null 2>&1; then
+ dunstify -a polybar -u low "Polybar" "$next mode"
+fi
+
+/home/aag/.config/polybar/launch.sh >/tmp/polybar-mode-toggle.log 2>&1 &
diff --git a/polybar/scripts/clipboard-menu.sh b/polybar/scripts/clipboard-menu.sh
new file mode 100755
index 0000000..10fe648
--- /dev/null
+++ b/polybar/scripts/clipboard-menu.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+mkdir -p "$state_dir" 2>/dev/null || true
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+history_file="$state_dir/polybar-clipboard-history"
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+current=""
+if command -v xclip >/dev/null 2>&1; then
+ current="$(xclip -selection clipboard -o 2>/dev/null || true)"
+elif command -v xsel >/dev/null 2>&1; then
+ current="$(xsel --clipboard --output 2>/dev/null || true)"
+fi
+
+if [ -n "$current" ]; then
+ one_line="$(printf '%s' "$current" | tr '\n' ' ' | cut -c 1-180)"
+ if ! grep -Fxq "$one_line" "$history_file" 2>/dev/null; then
+ tmp="${history_file}.tmp"
+ { printf '%s\n' "$one_line"; cat "$history_file" 2>/dev/null; } | awk 'NF && !seen[$0]++' | head -n 50 >"$tmp"
+ mv "$tmp" "$history_file"
+ fi
+fi
+
+choice="$(
+ dmenu -i -p clipboard \
+ -fn "FiraCode Nerd Font-14" \
+ -nb "#282828" -nf "#ebdbb2" \
+ -sb "#d79921" -sf "#282828" <"$history_file"
+)"
+
+[ -n "$choice" ] || exit 0
+
+if command -v xclip >/dev/null 2>&1; then
+ printf '%s' "$choice" | xclip -selection clipboard
+elif command -v xsel >/dev/null 2>&1; then
+ printf '%s' "$choice" | xsel --clipboard --input
+fi
diff --git a/polybar/scripts/display-brightness.sh b/polybar/scripts/display-brightness.sh
new file mode 100755
index 0000000..ea3d04c
--- /dev/null
+++ b/polybar/scripts/display-brightness.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+direction="${1:-up}"
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+ mkdir -p "$state_dir"
+fi
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+state_file="$state_dir/polybar-display-brightness.state"
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+output="$(
+ xrandr --query 2>/dev/null |
+ awk '/ connected primary/ { print $1; exit } / connected/ && !out { out = $1 } END { if (out) print out }'
+)"
+
+[ -n "$output" ] || exit 0
+
+brightness="$(cat "$state_file" 2>/dev/null || printf '1.0')"
+
+case "$direction" in
+ up)
+ brightness="$(awk -v v="$brightness" 'BEGIN { v += 0.05; if (v > 1.00) v = 1.00; printf "%.2f", v }')"
+ ;;
+ down)
+ brightness="$(awk -v v="$brightness" 'BEGIN { v -= 0.05; if (v < 0.30) v = 0.30; printf "%.2f", v }')"
+ ;;
+esac
+
+printf '%s\n' "$brightness" >"$state_file"
+xrandr --output "$output" --brightness "$brightness"
diff --git a/polybar/scripts/display-status.sh b/polybar/scripts/display-status.sh
new file mode 100755
index 0000000..620e3da
--- /dev/null
+++ b/polybar/scripts/display-status.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+fi
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+state_file="$state_dir/polybar-display-night.state"
+
+if [ -f "$state_file" ]; then
+ echo "%{F#d65d0e}night%{F-}"
+else
+ echo "day"
+fi
diff --git a/polybar/scripts/display-toggle.sh b/polybar/scripts/display-toggle.sh
new file mode 100755
index 0000000..df810e8
--- /dev/null
+++ b/polybar/scripts/display-toggle.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+ mkdir -p "$state_dir"
+fi
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+state_file="$state_dir/polybar-display-night.state"
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+output="$(
+ xrandr --query 2>/dev/null |
+ awk '/ connected primary/ { print $1; exit } / connected/ && !out { out = $1 } END { if (out) print out }'
+)"
+
+[ -n "$output" ] || exit 0
+
+notify() {
+ if command -v dunstify >/dev/null 2>&1; then
+ dunstify -a polybar -u low "Display" "$1"
+ fi
+}
+
+if [ -f "$state_file" ]; then
+ xrandr --output "$output" --gamma 1:1:1
+ rm -f "$state_file"
+ notify "day mode"
+else
+ xrandr --output "$output" --gamma 1:0.88:0.72
+ : >"$state_file"
+ notify "night mode"
+fi
diff --git a/polybar/scripts/dunst-status.sh b/polybar/scripts/dunst-status.sh
new file mode 100755
index 0000000..da23ddd
--- /dev/null
+++ b/polybar/scripts/dunst-status.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v dunstctl >/dev/null 2>&1; then
+ echo "n/a"
+ exit 0
+fi
+
+paused="$(dunstctl is-paused 2>/dev/null || echo unavailable)"
+waiting="$(dunstctl count waiting 2>/dev/null || echo 0)"
+
+case "$paused" in
+ true)
+ if [ "$waiting" -gt 0 ] 2>/dev/null; then
+ echo "dnd $waiting"
+ else
+ echo "dnd"
+ fi
+ ;;
+ false)
+ echo "on"
+ ;;
+ *)
+ echo "off"
+ ;;
+esac
diff --git a/polybar/scripts/kb-status.sh b/polybar/scripts/kb-status.sh
new file mode 100755
index 0000000..551e587
--- /dev/null
+++ b/polybar/scripts/kb-status.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+query="$(setxkbmap -query 2>/dev/null || true)"
+layout="$(printf '%s\n' "$query" | awk '/^layout:/ { print $2; exit }')"
+variant="$(printf '%s\n' "$query" | awk '/^variant:/ { print $2; exit }')"
+
+case "${layout:-unknown}:${variant:-}" in
+ us:dvorak)
+ echo "us dv"
+ ;;
+ cz:*|cz:)
+ echo "cz"
+ ;;
+ *:)
+ echo "${layout:-unknown}"
+ ;;
+ *)
+ echo "${layout:-unknown} ${variant}"
+ ;;
+esac
diff --git a/polybar/scripts/kb-switch.sh b/polybar/scripts/kb-switch.sh
new file mode 100755
index 0000000..af231f0
--- /dev/null
+++ b/polybar/scripts/kb-switch.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+DMENU="${DMENU:-/usr/bin/dmenu}"
+SETXKBMAP="${SETXKBMAP:-/usr/bin/setxkbmap}"
+LOG_FILE="${XDG_RUNTIME_DIR:-/tmp}/polybar-kb-switch.log"
+
+exec 2>>"$LOG_FILE"
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+choice="$(
+ printf '%s\n' "us" "us dvorak" "cz" |
+ "$DMENU" -i -p "Keyboard" \
+ -b \
+ -fn "FiraCode Nerd Font-12" \
+ -nb "#282828" \
+ -nf "#ebdbb2" \
+ -sb "#d79921" \
+ -sf "#282828"
+)" || exit 0
+
+case "$choice" in
+ "us")
+ "$SETXKBMAP" us
+ ;;
+ "us dvorak")
+ "$SETXKBMAP" us -variant dvorak
+ ;;
+ "cz")
+ "$SETXKBMAP" cz
+ ;;
+esac
diff --git a/polybar/scripts/kb-switch.sh~ b/polybar/scripts/kb-switch.sh~
new file mode 100755
index 0000000..69ff036
--- /dev/null
+++ b/polybar/scripts/kb-switch.sh~
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+options="us\nus dvorak\ncs"
+
+choice=$(echo -e "$options" | dmenu -i -p "Keyboard layout")
+
+case "$choice" in
+ "us")
+ setxkbmap us
+ ;;
+ "us dvorak")
+ setxkbmap us -variant dvorak
+ ;;
+ "cs")
+ setxkbmap cz
+ ;;
+esac
diff --git a/polybar/scripts/mic-status.sh b/polybar/scripts/mic-status.sh
new file mode 100755
index 0000000..21c44ce
--- /dev/null
+++ b/polybar/scripts/mic-status.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v pactl >/dev/null 2>&1; then
+ echo "n/a"
+ exit 0
+fi
+
+mute="$(pactl get-source-mute @DEFAULT_SOURCE@ 2>/dev/null | awk '{ print $2 }' || true)"
+
+case "$mute" in
+ yes)
+ echo "%{F#cc241d}muted%{F-}"
+ ;;
+ no)
+ echo "%{F#98971a}on%{F-}"
+ ;;
+ *)
+ echo "n/a"
+ ;;
+esac
diff --git a/polybar/scripts/mic-toggle.sh b/polybar/scripts/mic-toggle.sh
new file mode 100755
index 0000000..de93cf6
--- /dev/null
+++ b/polybar/scripts/mic-toggle.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v pactl >/dev/null 2>&1; then
+ exit 0
+fi
+
+pactl set-source-mute @DEFAULT_SOURCE@ toggle
+
+if command -v dunstify >/dev/null 2>&1; then
+ status="$(pactl get-source-mute @DEFAULT_SOURCE@ 2>/dev/null | awk '{ print $2 }' || true)"
+ dunstify -a polybar -u low "Microphone" "${status:-toggled}"
+fi
diff --git a/polybar/scripts/music-status.sh b/polybar/scripts/music-status.sh
new file mode 100755
index 0000000..229e058
--- /dev/null
+++ b/polybar/scripts/music-status.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v playerctl >/dev/null 2>&1; then
+ echo "n/a"
+ exit 0
+fi
+
+status="$(playerctl status 2>/dev/null || true)"
+if [ -z "$status" ]; then
+ echo "off"
+ exit 0
+fi
+
+artist="$(playerctl metadata artist 2>/dev/null || true)"
+title="$(playerctl metadata title 2>/dev/null || true)"
+
+if [ -n "$artist" ] && [ -n "$title" ]; then
+ text="$artist - $title"
+elif [ -n "$title" ]; then
+ text="$title"
+else
+ text="$status"
+fi
+
+case "$status" in
+ Playing)
+ prefix="%{F#98971a}>%{F-}"
+ ;;
+ Paused)
+ prefix="%{F#d79921}=%{F-}"
+ ;;
+ *)
+ prefix="-"
+ ;;
+esac
+
+printf '%s %s\n' "$prefix" "$(printf '%s' "$text" | cut -c 1-32)"
diff --git a/polybar/scripts/net-speed.sh b/polybar/scripts/net-speed.sh
new file mode 100755
index 0000000..ead10fd
--- /dev/null
+++ b/polybar/scripts/net-speed.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+iface="${POLYBAR_NETWORK:-eth0}"
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+ mkdir -p "$state_dir"
+fi
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+state_file="$state_dir/polybar-net-${iface}.state"
+rx_file="/sys/class/net/$iface/statistics/rx_bytes"
+tx_file="/sys/class/net/$iface/statistics/tx_bytes"
+
+human() {
+ local bytes="$1"
+
+ if [ "$bytes" -ge 1048576 ]; then
+ awk -v v="$bytes" 'BEGIN { printf "%.1fM", v / 1048576 }'
+ elif [ "$bytes" -ge 1024 ]; then
+ awk -v v="$bytes" 'BEGIN { printf "%.0fK", v / 1024 }'
+ else
+ printf '%sB' "$bytes"
+ fi
+}
+
+if [ ! -r "$rx_file" ] || [ ! -r "$tx_file" ]; then
+ echo "n/a"
+ exit 0
+fi
+
+now="$(date +%s)"
+rx="$(cat "$rx_file")"
+tx="$(cat "$tx_file")"
+
+if [ -r "$state_file" ]; then
+ read -r old_now old_rx old_tx <"$state_file" || true
+else
+ old_now="$now"
+ old_rx="$rx"
+ old_tx="$tx"
+fi
+
+printf '%s %s %s\n' "$now" "$rx" "$tx" >"$state_file"
+
+delta=$((now - old_now))
+if [ "$delta" -le 0 ]; then
+ delta=1
+fi
+
+down=$(((rx - old_rx) / delta))
+up=$(((tx - old_tx) / delta))
+
+if [ "$down" -lt 0 ]; then
+ down=0
+fi
+
+if [ "$up" -lt 0 ]; then
+ up=0
+fi
+
+printf '%s/%s\n' "$(human "$down")" "$(human "$up")"
diff --git a/polybar/scripts/polkit-agent.sh b/polybar/scripts/polkit-agent.sh
new file mode 100755
index 0000000..ed86a5f
--- /dev/null
+++ b/polybar/scripts/polkit-agent.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+if pgrep -u "$UID" -f 'polkit-gnome-authentication-agent-1|polkit-kde-authentication-agent-1|lxqt-policykit-agent|polkit-mate-authentication-agent-1|xfce-polkit' >/dev/null 2>&1; then
+ exit 0
+fi
+
+agents=(
+ /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
+ /usr/lib/polkit-kde-authentication-agent-1
+ /usr/lib/lxqt-policykit/lxqt-policykit-agent
+ /usr/bin/lxqt-policykit-agent
+ /usr/lib/mate-polkit/polkit-mate-authentication-agent-1
+ /usr/lib/xfce-polkit/xfce-polkit
+)
+
+for agent in "${agents[@]}"; do
+ if [ -x "$agent" ]; then
+ exec "$agent"
+ fi
+done
+
+echo "No supported polkit authentication agent found" >&2
+exit 1
diff --git a/polybar/scripts/power-menu.sh b/polybar/scripts/power-menu.sh
new file mode 100755
index 0000000..ab59352
--- /dev/null
+++ b/polybar/scripts/power-menu.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+choice="$(
+ printf '%s\n' lock logout suspend reboot shutdown |
+ dmenu -i -p power \
+ -fn "FiraCode Nerd Font-14" \
+ -nb "#282828" -nf "#ebdbb2" \
+ -sb "#d79921" -sf "#282828"
+)"
+
+case "$choice" in
+ lock)
+ i3lock
+ ;;
+ logout)
+ i3-msg exit
+ ;;
+ suspend)
+ loginctl suspend
+ ;;
+ reboot)
+ loginctl reboot
+ ;;
+ shutdown)
+ loginctl poweroff
+ ;;
+esac
diff --git a/polybar/scripts/scratchpad-status.sh b/polybar/scripts/scratchpad-status.sh
new file mode 100755
index 0000000..426d3b2
--- /dev/null
+++ b/polybar/scripts/scratchpad-status.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v i3-msg >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
+ echo "n/a"
+ exit 0
+fi
+
+count="$(
+ i3-msg -t get_tree 2>/dev/null |
+ jq '[.. | objects | select(.name == "__i3_scratch") | .floating_nodes[]?] | length' 2>/dev/null || printf '0'
+)"
+
+if [ "$count" -gt 0 ] 2>/dev/null; then
+ echo "%{F#d79921}$count%{F-}"
+else
+ echo "0"
+fi
diff --git a/polybar/scripts/screenshot-menu.sh b/polybar/scripts/screenshot-menu.sh
new file mode 100755
index 0000000..b3064a9
--- /dev/null
+++ b/polybar/scripts/screenshot-menu.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+dir="${XDG_PICTURES_DIR:-$HOME/Pictures}/Screenshots"
+mkdir -p "$dir" 2>/dev/null || true
+if [ ! -w "$dir" ]; then
+ dir="/tmp"
+fi
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+notify() {
+ if command -v dunstify >/dev/null 2>&1; then
+ dunstify -a polybar -u low "Screenshot" "$1"
+ fi
+}
+
+menu() {
+ printf '%s\n' full selection window |
+ dmenu -i -p screenshot \
+ -fn "FiraCode Nerd Font-14" \
+ -nb "#282828" -nf "#ebdbb2" \
+ -sb "#d79921" -sf "#282828"
+}
+
+file="$dir/shot-$(date +%Y%m%d-%H%M%S).png"
+choice="$(menu)"
+
+case "$choice" in
+ full)
+ if command -v scrot >/dev/null 2>&1; then
+ scrot "$file"
+ elif command -v import >/dev/null 2>&1; then
+ import -window root "$file"
+ else
+ notify "No screenshot tool found"
+ exit 1
+ fi
+ ;;
+ selection)
+ if command -v flameshot >/dev/null 2>&1; then
+ flameshot gui -p "$dir"
+ exit 0
+ elif command -v maim >/dev/null 2>&1; then
+ maim -s "$file"
+ elif command -v scrot >/dev/null 2>&1; then
+ scrot -s "$file"
+ elif command -v import >/dev/null 2>&1; then
+ import "$file"
+ else
+ notify "No selection screenshot tool found"
+ exit 1
+ fi
+ ;;
+ window)
+ if command -v scrot >/dev/null 2>&1; then
+ scrot -u "$file"
+ elif command -v import >/dev/null 2>&1; then
+ import "$file"
+ else
+ notify "No window screenshot tool found"
+ exit 1
+ fi
+ ;;
+ *)
+ exit 0
+ ;;
+esac
+
+notify "$file"
diff --git a/polybar/scripts/temp-status.sh b/polybar/scripts/temp-status.sh
new file mode 100755
index 0000000..d1f7665
--- /dev/null
+++ b/polybar/scripts/temp-status.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+temp=""
+
+if command -v sensors >/dev/null 2>&1; then
+ temp="$(
+ sensors 2>/dev/null |
+ awk '
+ /Package id 0:/ { gsub(/[^0-9.-]/, "", $4); print int($4); exit }
+ /Tctl:/ { gsub(/[^0-9.-]/, "", $2); print int($2); exit }
+ /temp1:/ && $2 ~ /^\+/ { gsub(/[^0-9.-]/, "", $2); print int($2); exit }
+ '
+ )"
+fi
+
+if [ -z "$temp" ]; then
+ for zone in /sys/class/thermal/thermal_zone*/temp; do
+ [ -r "$zone" ] || continue
+ value="$(cat "$zone")"
+ if [ "$value" -gt 0 ] 2>/dev/null; then
+ temp=$((value / 1000))
+ break
+ fi
+ done
+fi
+
+if [ -z "$temp" ]; then
+ echo "n/a"
+elif [ "$temp" -ge 80 ]; then
+ echo "%{F#cc241d}${temp}C%{F-}"
+elif [ "$temp" -ge 65 ]; then
+ echo "%{F#d79921}${temp}C%{F-}"
+else
+ echo "${temp}C"
+fi
diff --git a/polybar/scripts/updates-action.sh b/polybar/scripts/updates-action.sh
new file mode 100755
index 0000000..a22a747
--- /dev/null
+++ b/polybar/scripts/updates-action.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cmd='sudo pacman -Syu; echo; read -r -p "Press Enter to close..."'
+
+if command -v kitty >/dev/null 2>&1; then
+ exec kitty --title "System update" /usr/bin/env bash -lc "$cmd"
+elif command -v foot >/dev/null 2>&1; then
+ exec foot --title "System update" /usr/bin/env bash -lc "$cmd"
+elif command -v xterm >/dev/null 2>&1; then
+ exec xterm -T "System update" -e /usr/bin/env bash -lc "$cmd"
+fi
+
+if command -v dunstify >/dev/null 2>&1; then
+ dunstify -a polybar -u normal "Updates" "No terminal found"
+fi
diff --git a/polybar/scripts/updates-status.sh b/polybar/scripts/updates-status.sh
new file mode 100755
index 0000000..75568ad
--- /dev/null
+++ b/polybar/scripts/updates-status.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if command -v checkupdates >/dev/null 2>&1; then
+ count="$( (checkupdates 2>/dev/null || true) | wc -l)"
+elif command -v pacman >/dev/null 2>&1; then
+ count="$( (pacman -Qu 2>/dev/null || true) | wc -l)"
+else
+ echo "n/a"
+ exit 0
+fi
+
+if [ "$count" -gt 0 ]; then
+ echo "%{F#d79921}$count%{F-}"
+else
+ echo "0"
+fi
diff --git a/polybar/scripts/wg-pc-status.sh b/polybar/scripts/wg-pc-status.sh
new file mode 100755
index 0000000..b708695
--- /dev/null
+++ b/polybar/scripts/wg-pc-status.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+iface="${WG_QUICK_PROFILE:-PC}"
+
+age_label() {
+ local latest now age
+
+ latest="$(wg show "$iface" latest-handshakes 2>/dev/null | awk 'NF { if ($2 > newest) newest = $2 } END { print newest + 0 }')"
+ if [ -z "$latest" ] || [ "$latest" -le 0 ] 2>/dev/null; then
+ return 0
+ fi
+
+ now="$(date +%s)"
+ age=$((now - latest))
+
+ if [ "$age" -lt 60 ]; then
+ printf ' %ss' "$age"
+ elif [ "$age" -lt 3600 ]; then
+ printf ' %sm' "$((age / 60))"
+ elif [ "$age" -lt 86400 ]; then
+ printf ' %sh' "$((age / 3600))"
+ else
+ printf ' %sd' "$((age / 86400))"
+ fi
+}
+
+if [ -d "/sys/class/net/$iface" ]; then
+ echo "%{F#98971a}up$(age_label)%{F-}"
+else
+ echo "%{F#cc241d}down%{F-}"
+fi
diff --git a/polybar/scripts/wg-pc-toggle.sh b/polybar/scripts/wg-pc-toggle.sh
new file mode 100755
index 0000000..5cd102f
--- /dev/null
+++ b/polybar/scripts/wg-pc-toggle.sh
@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+profile="${WG_QUICK_PROFILE:-PC}"
+iface="$profile"
+requested="${1:-toggle}"
+
+export DISPLAY="${DISPLAY:-:0}"
+if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+fi
+
+case "$profile" in
+ *[!A-Za-z0-9_.@-]*|"")
+ echo "Invalid WireGuard profile: $profile" >&2
+ exit 2
+ ;;
+esac
+
+log_file="${XDG_RUNTIME_DIR:-/tmp}/polybar-wg-$profile.log"
+
+notify() {
+ if command -v dunstify >/dev/null 2>&1; then
+ dunstify -a polybar -u low "WireGuard" "$1"
+ fi
+}
+
+polkit_agent_running() {
+ pgrep -u "$UID" -f 'polkit-gnome-authentication-agent-1|polkit-kde-authentication-agent-1|lxqt-policykit-agent|polkit-mate-authentication-agent-1|xfce-polkit' >/dev/null 2>&1
+}
+
+ensure_polkit_agent() {
+ if polkit_agent_running; then
+ return 0
+ fi
+
+ if [ -x /home/aag/.config/polybar/scripts/polkit-agent.sh ]; then
+ /usr/bin/setsid -f /home/aag/.config/polybar/scripts/polkit-agent.sh >>"$log_file" 2>&1
+ sleep 0.5
+ fi
+
+ polkit_agent_running
+}
+
+case "$requested" in
+ up|down)
+ action="$requested"
+ ;;
+ toggle)
+ if [ -d "/sys/class/net/$iface" ]; then
+ action="down"
+ else
+ action="up"
+ fi
+ ;;
+ *)
+ notify "Unknown action: $requested"
+ exit 2
+ ;;
+esac
+
+if [ "$(id -u)" -eq 0 ]; then
+ auth=()
+elif command -v pkexec >/dev/null 2>&1; then
+ if ! ensure_polkit_agent; then
+ notify "No polkit agent running"
+ echo "No polkit authentication agent is running." >"$log_file"
+ echo "Install/start polkit-gnome, then restart i3 or run ~/.config/polybar/scripts/polkit-agent.sh." >>"$log_file"
+ exit 1
+ fi
+ auth=(pkexec --disable-internal-agent)
+else
+ notify "pkexec not found"
+ exit 1
+fi
+
+if "${auth[@]}" /usr/bin/wg-quick "$action" "$profile" >"$log_file" 2>&1; then
+ notify "$profile $action complete"
+else
+ notify "$profile $action failed: $log_file"
+ exit 1
+fi
diff --git a/quickshell/gruvbar/README.md b/quickshell/gruvbar/README.md
new file mode 100644
index 0000000..3c759ec
--- /dev/null
+++ b/quickshell/gruvbar/README.md
@@ -0,0 +1,34 @@
+# Gruvbar Quickshell
+
+This is a Quickshell replacement bar for the current i3/Polybar setup. It keeps the Gruvbox palette and FiraCode Nerd Font look, but uses QML widgets instead of Polybar text modules.
+
+## Run
+
+```sh
+sudo pacman -S quickshell
+~/.config/quickshell/gruvbar/launch.sh
+```
+
+The config is a named Quickshell config, so this also works:
+
+```sh
+quickshell -c gruvbar
+```
+
+## Switch i3 From Polybar
+
+After testing the bar manually, change the i3 startup line from Polybar to Gruvbar:
+
+```i3
+# exec_always --no-startup-id ~/.config/polybar/launch.sh
+exec_always --no-startup-id /home/aag/.config/quickshell/gruvbar/launch.sh
+```
+
+## Widgets
+
+- Workspaces with click and scroll switching.
+- Expand/minimize button for compact and full modes.
+- Clickable notification, force-awake, volume, keyboard, WireGuard PC, display, mic, music, scratchpad, screenshot, clipboard, update, and power controls.
+- Live CPU, memory, disk, network, network speed, temperature, and clock status.
+
+Most actions reuse the existing Polybar scripts so the behavior stays aligned while the UI moves to Quickshell.
diff --git a/quickshell/gruvbar/launch.sh b/quickshell/gruvbar/launch.sh
new file mode 100755
index 0000000..a591a40
--- /dev/null
+++ b/quickshell/gruvbar/launch.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v quickshell >/dev/null 2>&1; then
+ echo "quickshell is not installed. Install it with: sudo pacman -S quickshell" >&2
+ exit 1
+fi
+
+pkill -f "quickshell.*-c gruvbar" 2>/dev/null || true
+exec quickshell -c gruvbar
diff --git a/quickshell/gruvbar/scripts/action.sh b/quickshell/gruvbar/scripts/action.sh
new file mode 100755
index 0000000..80d26d3
--- /dev/null
+++ b/quickshell/gruvbar/scripts/action.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+action="${1:-}"
+arg="${2:-}"
+polybar_scripts="/home/aag/.config/polybar/scripts"
+
+run_existing() {
+ local script="$1"
+ shift
+
+ if [ -x "$polybar_scripts/$script" ]; then
+ exec "$polybar_scripts/$script" "$@"
+ fi
+}
+
+case "$action" in
+ awake)
+ run_existing awake-toggle.sh "$arg"
+ ;;
+ clipboard)
+ run_existing clipboard-menu.sh
+ ;;
+ display)
+ run_existing display-toggle.sh
+ ;;
+ display-brightness)
+ run_existing display-brightness.sh "${arg:-up}"
+ ;;
+ dunst)
+ command -v dunstctl >/dev/null 2>&1 || exit 0
+ case "$arg" in
+ history) dunstctl history-pop ;;
+ close) dunstctl close-all ;;
+ *) dunstctl set-paused toggle ;;
+ esac
+ ;;
+ keyboard)
+ run_existing kb-switch.sh
+ ;;
+ mic)
+ run_existing mic-toggle.sh
+ ;;
+ music)
+ command -v playerctl >/dev/null 2>&1 || exit 0
+ case "$arg" in
+ prev) playerctl previous ;;
+ next) playerctl next ;;
+ *) playerctl play-pause ;;
+ esac
+ ;;
+ power)
+ run_existing power-menu.sh
+ ;;
+ screenshot)
+ run_existing screenshot-menu.sh
+ ;;
+ scratch)
+ command -v i3-msg >/dev/null 2>&1 || exit 0
+ case "$arg" in
+ move) i3-msg move scratchpad ;;
+ *) i3-msg scratchpad show ;;
+ esac
+ ;;
+ updates)
+ run_existing updates-action.sh
+ ;;
+ volume)
+ command -v pactl >/dev/null 2>&1 || {
+ [ "$arg" = "" ] && command -v pavucontrol >/dev/null 2>&1 && exec pavucontrol
+ exit 0
+ }
+ case "$arg" in
+ up) pactl set-sink-volume @DEFAULT_SINK@ +5% ;;
+ down) pactl set-sink-volume @DEFAULT_SINK@ -5% ;;
+ mute) pactl set-sink-mute @DEFAULT_SINK@ toggle ;;
+ *) command -v pavucontrol >/dev/null 2>&1 && pavucontrol ;;
+ esac
+ ;;
+ wg)
+ run_existing wg-pc-toggle.sh "$arg"
+ ;;
+esac
diff --git a/quickshell/gruvbar/scripts/status.sh b/quickshell/gruvbar/scripts/status.sh
new file mode 100755
index 0000000..f6d4402
--- /dev/null
+++ b/quickshell/gruvbar/scripts/status.sh
@@ -0,0 +1,229 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+kind="${1:-}"
+iface="${POLYBAR_NETWORK:-eth0}"
+profile="${WG_QUICK_PROFILE:-PC}"
+
+state_dir="${XDG_RUNTIME_DIR:-/tmp}"
+if [ ! -w "$state_dir" ]; then
+ state_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
+ mkdir -p "$state_dir" 2>/dev/null || true
+fi
+if [ ! -w "$state_dir" ]; then
+ state_dir="/tmp"
+fi
+
+human_bytes() {
+ local bytes="$1"
+
+ if [ "$bytes" -ge 1048576 ]; then
+ awk -v v="$bytes" 'BEGIN { printf "%.1fM", v / 1048576 }'
+ elif [ "$bytes" -ge 1024 ]; then
+ awk -v v="$bytes" 'BEGIN { printf "%.0fK", v / 1024 }'
+ else
+ printf '%sB' "$bytes"
+ fi
+}
+
+case "$kind" in
+ awake)
+ [ -f "$state_dir/polybar-awake.state" ] && echo on || echo off
+ ;;
+ cpu)
+ state_file="$state_dir/quickshell-cpu.state"
+ read -r cpu user nice system idle iowait irq softirq steal _ </proc/stat
+ idle_all=$((idle + iowait))
+ total=$((user + nice + system + idle + iowait + irq + softirq + steal))
+
+ if [ -r "$state_file" ]; then
+ read -r old_total old_idle <"$state_file" || true
+ else
+ old_total="$total"
+ old_idle="$idle_all"
+ fi
+
+ printf '%s %s\n' "$total" "$idle_all" >"$state_file"
+ diff_total=$((total - old_total))
+ diff_idle=$((idle_all - old_idle))
+
+ if [ "$diff_total" -le 0 ]; then
+ echo 0%
+ else
+ echo "$(((100 * (diff_total - diff_idle)) / diff_total))%"
+ fi
+ ;;
+ date)
+ date '+%a %d %b %H:%M'
+ ;;
+ disk)
+ df -P / | awk 'NR == 2 { print $5 }'
+ ;;
+ display)
+ [ -f "$state_dir/polybar-display-night.state" ] && echo night || echo day
+ ;;
+ dunst)
+ if ! command -v dunstctl >/dev/null 2>&1; then
+ echo off
+ elif [ "$(dunstctl is-paused 2>/dev/null || echo false)" = true ]; then
+ waiting="$(dunstctl count waiting 2>/dev/null || echo 0)"
+ [ "$waiting" -gt 0 ] 2>/dev/null && echo "dnd $waiting" || echo dnd
+ else
+ echo on
+ fi
+ ;;
+ keyboard)
+ export DISPLAY="${DISPLAY:-:0}"
+ if [ -z "${XAUTHORITY:-}" ] && [ -r "$HOME/.Xauthority" ]; then
+ export XAUTHORITY="$HOME/.Xauthority"
+ fi
+ query="$(setxkbmap -query 2>/dev/null || true)"
+ layout="$(printf '%s\n' "$query" | awk '/^layout:/ { print $2; exit }')"
+ variant="$(printf '%s\n' "$query" | awk '/^variant:/ { print $2; exit }')"
+ case "${layout:-unknown}:${variant:-}" in
+ us:dvorak) echo "us dv" ;;
+ cz:*|cz:) echo cz ;;
+ *:) echo "${layout:-unknown}" ;;
+ *) echo "${layout:-unknown} ${variant}" ;;
+ esac
+ ;;
+ mem)
+ awk '
+ /MemTotal:/ { total = $2 }
+ /MemAvailable:/ { avail = $2 }
+ END { if (total > 0) printf "%d%%\n", ((total - avail) * 100) / total; else print "n/a" }
+ ' /proc/meminfo
+ ;;
+ mic)
+ if ! command -v pactl >/dev/null 2>&1; then
+ echo n/a
+ else
+ mute="$(pactl get-source-mute @DEFAULT_SOURCE@ 2>/dev/null | awk '{ print $2 }' || true)"
+ [ "$mute" = yes ] && echo muted || echo on
+ fi
+ ;;
+ music)
+ if ! command -v playerctl >/dev/null 2>&1; then
+ echo n/a
+ exit 0
+ fi
+ status="$(playerctl status 2>/dev/null || true)"
+ [ -n "$status" ] || { echo off; exit 0; }
+ artist="$(playerctl metadata artist 2>/dev/null || true)"
+ title="$(playerctl metadata title 2>/dev/null || true)"
+ if [ -n "$artist" ] && [ -n "$title" ]; then
+ printf '%s - %s\n' "$artist" "$title" | cut -c 1-34
+ elif [ -n "$title" ]; then
+ printf '%s\n' "$title" | cut -c 1-34
+ else
+ echo "$status"
+ fi
+ ;;
+ net)
+ ip -o -4 addr show "$iface" 2>/dev/null | awk '{ split($4, ip, "/"); print ip[1]; found = 1; exit } END { if (!found) print "offline" }'
+ ;;
+ netspeed)
+ rx_file="/sys/class/net/$iface/statistics/rx_bytes"
+ tx_file="/sys/class/net/$iface/statistics/tx_bytes"
+ state_file="$state_dir/quickshell-net-${iface}.state"
+
+ if [ ! -r "$rx_file" ] || [ ! -r "$tx_file" ]; then
+ echo n/a
+ exit 0
+ fi
+
+ now="$(date +%s)"
+ rx="$(cat "$rx_file")"
+ tx="$(cat "$tx_file")"
+
+ if [ -r "$state_file" ]; then
+ read -r old_now old_rx old_tx <"$state_file" || true
+ else
+ old_now="$now"
+ old_rx="$rx"
+ old_tx="$tx"
+ fi
+
+ printf '%s %s %s\n' "$now" "$rx" "$tx" >"$state_file"
+ delta=$((now - old_now))
+ [ "$delta" -gt 0 ] || delta=1
+ down=$(((rx - old_rx) / delta))
+ up=$(((tx - old_tx) / delta))
+ [ "$down" -ge 0 ] || down=0
+ [ "$up" -ge 0 ] || up=0
+ printf '%s/%s\n' "$(human_bytes "$down")" "$(human_bytes "$up")"
+ ;;
+ scratch)
+ if command -v i3-msg >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
+ i3-msg -t get_tree 2>/dev/null |
+ jq '[.. | objects | select(.name == "__i3_scratch") | .floating_nodes[]?] | length' 2>/dev/null || echo 0
+ else
+ echo n/a
+ fi
+ ;;
+ temp)
+ if command -v sensors >/dev/null 2>&1; then
+ temp="$(
+ sensors 2>/dev/null |
+ awk '
+ /Package id 0:/ { gsub(/[^0-9.-]/, "", $4); print int($4); exit }
+ /Tctl:/ { gsub(/[^0-9.-]/, "", $2); print int($2); exit }
+ /temp1:/ && $2 ~ /^\+/ { gsub(/[^0-9.-]/, "", $2); print int($2); exit }
+ '
+ )"
+ else
+ temp=""
+ fi
+ if [ -z "$temp" ]; then
+ for zone in /sys/class/thermal/thermal_zone*/temp; do
+ [ -r "$zone" ] || continue
+ value="$(cat "$zone")"
+ if [ "$value" -gt 0 ] 2>/dev/null; then
+ temp=$((value / 1000))
+ break
+ fi
+ done
+ fi
+ [ -n "${temp:-}" ] && echo "${temp}C" || echo n/a
+ ;;
+ updates)
+ if command -v checkupdates >/dev/null 2>&1; then
+ (checkupdates 2>/dev/null || true) | wc -l
+ elif command -v pacman >/dev/null 2>&1; then
+ (pacman -Qu 2>/dev/null || true) | wc -l
+ else
+ echo n/a
+ fi
+ ;;
+ volume)
+ if ! command -v pactl >/dev/null 2>&1; then
+ echo n/a
+ exit 0
+ fi
+ mute="$(pactl get-sink-mute @DEFAULT_SINK@ 2>/dev/null | awk '{ print $2 }' || true)"
+ vol="$(pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | awk 'NR == 1 { print $5; exit }' || true)"
+ [ "$mute" = yes ] && echo muted || echo "${vol:-n/a}"
+ ;;
+ wg)
+ if [ ! -d "/sys/class/net/$profile" ]; then
+ echo down
+ exit 0
+ fi
+ if ! command -v wg >/dev/null 2>&1; then
+ echo up
+ exit 0
+ fi
+ latest="$(wg show "$profile" latest-handshakes 2>/dev/null | awk 'NF { if ($2 > newest) newest = $2 } END { print newest + 0 }' || true)"
+ if [ -z "$latest" ] || [ "$latest" -le 0 ] 2>/dev/null; then
+ echo up
+ exit 0
+ fi
+ age=$(($(date +%s) - latest))
+ if [ "$age" -lt 60 ]; then suffix="${age}s"; elif [ "$age" -lt 3600 ]; then suffix="$((age / 60))m"; elif [ "$age" -lt 86400 ]; then suffix="$((age / 3600))h"; else suffix="$((age / 86400))d"; fi
+ echo "up $suffix"
+ ;;
+ *)
+ echo n/a
+ exit 1
+ ;;
+esac
diff --git a/quickshell/gruvbar/shell.qml b/quickshell/gruvbar/shell.qml
new file mode 100644
index 0000000..0d03f70
--- /dev/null
+++ b/quickshell/gruvbar/shell.qml
@@ -0,0 +1,631 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.I3
+import Quickshell.Io
+
+ShellRoot {
+ id: root
+
+ property bool expanded: false
+ property int pendingWorkspace: 0
+ property int barHeight: 38
+ property int drawerHeight: 118
+ property string scriptDir: Quickshell.shellPath("scripts")
+ property string fontFamily: "FiraCode Nerd Font"
+ property string bg: "#282828"
+ property string bg1: "#3c3836"
+ property string bg2: "#504945"
+ property string bg3: "#665c54"
+ property string fg: "#ebdbb2"
+ property string fg1: "#d5c4a1"
+ property string red: "#cc241d"
+ property string green: "#98971a"
+ property string yellow: "#d79921"
+ property string orange: "#d65d0e"
+ property string purple: "#b16286"
+ property string aqua: "#689d6a"
+ property string gray: "#928374"
+ property string clockText: Qt.formatDateTime(new Date(), "hh:mm dd MMM")
+
+ signal refreshRequested(string actionName)
+
+ function statusCommand(name) {
+ return [root.scriptDir + "/status.sh", name];
+ }
+
+ function requestStatusRefresh(actionName) {
+ root.refreshRequested(actionName);
+ quickStatusRefresh.actionName = actionName;
+ settleStatusRefresh.actionName = actionName;
+ lateStatusRefresh.actionName = actionName;
+ quickStatusRefresh.restart();
+ settleStatusRefresh.restart();
+ lateStatusRefresh.restart();
+ }
+
+ function runAction(name, arg) {
+ var cmd = [root.scriptDir + "/action.sh", name];
+ if (arg !== undefined && arg !== "")
+ cmd.push(arg);
+
+ Quickshell.execDetached(cmd);
+ root.requestStatusRefresh(name);
+ }
+
+ function switchWorkspace(workspaceNumber) {
+ root.pendingWorkspace = workspaceNumber;
+ pendingWorkspaceReset.restart();
+ I3.dispatch("workspace number " + workspaceNumber);
+ }
+
+ function textColor(value, fallback) {
+ if (value === "n/a")
+ return root.gray;
+
+ if (value.indexOf("down") === 0 || value.indexOf("muted") === 0)
+ return root.red;
+
+ if (value.indexOf("dnd") === 0 || value.indexOf("night") === 0)
+ return root.orange;
+
+ if (value === "on" || value.indexOf("up") === 0 || value === "day")
+ return root.green;
+
+ return fallback;
+ }
+
+ Timer {
+ interval: 1000
+ running: true
+ repeat: true
+ onTriggered: root.clockText = Qt.formatDateTime(new Date(), "hh:mm dd MMM")
+ }
+
+ Timer {
+ id: quickStatusRefresh
+
+ property string actionName: ""
+
+ interval: 120
+ onTriggered: root.refreshRequested(actionName)
+ }
+
+ Timer {
+ id: settleStatusRefresh
+
+ property string actionName: ""
+
+ interval: 500
+ onTriggered: root.refreshRequested(actionName)
+ }
+
+ Timer {
+ id: lateStatusRefresh
+
+ property string actionName: ""
+
+ interval: 1600
+ onTriggered: root.refreshRequested(actionName)
+ }
+
+ Timer {
+ id: pendingWorkspaceReset
+
+ interval: 700
+ onTriggered: root.pendingWorkspace = 0
+ }
+
+ ListModel {
+ id: workspaceModel
+
+ ListElement {
+ ws: 1
+ }
+
+ ListElement {
+ ws: 2
+ }
+
+ ListElement {
+ ws: 3
+ }
+
+ ListElement {
+ ws: 4
+ }
+
+ ListElement {
+ ws: 5
+ }
+
+ ListElement {
+ ws: 6
+ }
+
+ ListElement {
+ ws: 7
+ }
+
+ ListElement {
+ ws: 8
+ }
+
+ ListElement {
+ ws: 9
+ }
+
+ ListElement {
+ ws: 10
+ }
+
+ }
+
+ Variants {
+ model: Quickshell.screens
+
+ PanelWindow {
+ id: bar
+
+ required property var modelData
+ readonly property bool canExpand: modelData.name === Quickshell.screens[0].name
+
+ screen: modelData
+ implicitHeight: root.barHeight
+ exclusiveZone: root.barHeight
+ aboveWindows: true
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: root.bg
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 1
+ color: root.bg2
+ }
+
+ Row {
+ id: taskStrip
+
+ anchors.centerIn: parent
+ height: 30
+ spacing: 6
+
+ ToggleButton {
+ visible: bar.canExpand
+ }
+
+ Repeater {
+ model: workspaceModel
+
+ delegate: WorkspaceButton {
+ required property int ws
+
+ workspaceNumber: ws
+ }
+
+ }
+
+ }
+
+ Row {
+ id: tray
+
+ anchors.right: parent.right
+ anchors.rightMargin: 8
+ anchors.verticalCenter: parent.verticalCenter
+ height: 28
+ spacing: 5
+
+ StatusPill {
+ title: "ntf"
+ command: root.statusCommand("dunst")
+ actionName: "dunst"
+ accent: root.yellow
+ }
+
+ StatusPill {
+ title: "wake"
+ command: root.statusCommand("awake")
+ actionName: "awake"
+ accent: root.orange
+ }
+
+ StatusPill {
+ title: "wg"
+ command: root.statusCommand("wg")
+ actionName: "wg"
+ accent: root.aqua
+ }
+
+ StatusPill {
+ title: "vol"
+ command: root.statusCommand("volume")
+ actionName: "volume"
+ actionArg: "mute"
+ wheelAction: "volume"
+ wheelUpArg: "up"
+ wheelDownArg: "down"
+ accent: root.green
+ }
+
+ StatusPill {
+ title: "kbd"
+ command: root.statusCommand("keyboard")
+ actionName: "keyboard"
+ accent: root.purple
+ }
+
+ ClockPill {
+ }
+
+ }
+
+ }
+
+ PopupWindow {
+ id: drawer
+
+ visible: root.expanded && bar.canExpand
+ grabFocus: false
+ anchor.window: bar
+ anchor.rect.x: Math.max(8, Math.round((bar.width - implicitWidth) / 2))
+ anchor.rect.y: root.barHeight + 6
+ implicitWidth: Math.max(320, Math.min(760, bar.width - 24))
+ implicitHeight: root.drawerHeight
+ color: "transparent"
+
+ Rectangle {
+ anchors.fill: parent
+ radius: 7
+ color: root.bg1
+ border.width: 1
+ border.color: root.bg3
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: 10
+ spacing: 10
+
+ Flow {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 7
+
+ StatusPill {
+ title: "net"
+ command: root.statusCommand("net")
+ accent: root.green
+ }
+
+ StatusPill {
+ title: "spd"
+ command: root.statusCommand("netspeed")
+ accent: root.green
+ }
+
+ StatusPill {
+ title: "tmp"
+ command: root.statusCommand("temp")
+ accent: root.orange
+ }
+
+ StatusPill {
+ title: "cpu"
+ command: root.statusCommand("cpu")
+ accent: root.orange
+ }
+
+ StatusPill {
+ title: "ram"
+ command: root.statusCommand("mem")
+ accent: root.purple
+ }
+
+ StatusPill {
+ title: "disk"
+ command: root.statusCommand("disk")
+ accent: root.aqua
+ }
+
+ StatusPill {
+ title: "upd"
+ command: root.statusCommand("updates")
+ actionName: "updates"
+ interval: 900000
+ accent: root.yellow
+ }
+
+ StatusPill {
+ title: "disp"
+ command: root.statusCommand("display")
+ actionName: "display"
+ wheelAction: "display-brightness"
+ wheelUpArg: "up"
+ wheelDownArg: "down"
+ accent: root.orange
+ }
+
+ StatusPill {
+ title: "mic"
+ command: root.statusCommand("mic")
+ actionName: "mic"
+ accent: root.purple
+ }
+
+ StatusPill {
+ title: "mus"
+ command: root.statusCommand("music")
+ actionName: "music"
+ interval: 2000
+ accent: root.green
+ }
+
+ StatusPill {
+ title: "scratch"
+ command: root.statusCommand("scratch")
+ actionName: "scratch"
+ accent: root.yellow
+ }
+
+ }
+
+ ColumnLayout {
+ Layout.preferredWidth: 84
+ Layout.fillHeight: true
+ spacing: 7
+
+ ActionButton {
+ label: "shot"
+ actionName: "screenshot"
+ accent: root.aqua
+ }
+
+ ActionButton {
+ label: "clip"
+ actionName: "clipboard"
+ accent: root.purple
+ }
+
+ ActionButton {
+ label: "power"
+ actionName: "power"
+ accent: root.red
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ component WorkspaceButton: Rectangle {
+ id: wsButton
+
+ required property int workspaceNumber
+ readonly property bool focused: I3.focusedWorkspace !== null && I3.focusedWorkspace.number === workspaceNumber
+ readonly property bool active: focused || root.pendingWorkspace === workspaceNumber
+
+ Layout.preferredWidth: 30
+ Layout.preferredHeight: 28
+ width: 30
+ height: 28
+ radius: 4
+ color: active ? root.yellow : (mouse.containsMouse ? root.bg2 : root.bg1)
+ border.width: 1
+ border.color: active ? root.yellow : root.bg2
+
+ Text {
+ anchors.centerIn: parent
+ text: String(wsButton.workspaceNumber)
+ color: wsButton.active ? root.bg : root.fg1
+ font.family: root.fontFamily
+ font.pixelSize: 13
+ font.bold: wsButton.active
+ }
+
+ MouseArea {
+ id: mouse
+
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: root.switchWorkspace(wsButton.workspaceNumber)
+ onWheel: wheel.angleDelta.y > 0 ? I3.dispatch("workspace next") : I3.dispatch("workspace prev")
+ }
+
+ }
+
+ component ToggleButton: Rectangle {
+ Layout.preferredHeight: 28
+ Layout.preferredWidth: 68
+ width: 68
+ height: 28
+ radius: 5
+ color: root.expanded ? root.bg2 : root.bg1
+ border.width: 1
+ border.color: root.yellow
+
+ Text {
+ anchors.centerIn: parent
+ text: root.expanded ? "close" : "start"
+ color: root.yellow
+ font.family: root.fontFamily
+ font.pixelSize: 12
+ font.bold: true
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.expanded = !root.expanded
+ }
+
+ }
+
+ component ClockPill: Rectangle {
+ width: 122
+ height: 28
+ radius: 5
+ color: root.bg1
+ border.width: 1
+ border.color: root.bg2
+
+ Text {
+ anchors.centerIn: parent
+ text: root.clockText
+ color: root.fg
+ font.family: root.fontFamily
+ font.pixelSize: 12
+ font.bold: true
+ }
+
+ }
+
+ component ActionButton: Rectangle {
+ id: button
+
+ property string label: ""
+ property string actionName: ""
+ property string actionArg: ""
+ property string accent: root.fg1
+
+ Layout.preferredWidth: 76
+ Layout.preferredHeight: 28
+ width: 76
+ height: 28
+ radius: 5
+ color: mouse.pressed ? root.bg3 : (mouse.containsMouse ? root.bg2 : root.bg)
+ border.width: 1
+ border.color: accent
+
+ Text {
+ id: labelText
+
+ anchors.centerIn: parent
+ text: button.label
+ color: button.accent
+ font.family: root.fontFamily
+ font.pixelSize: 12
+ font.bold: true
+ }
+
+ MouseArea {
+ id: mouse
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.runAction(button.actionName, button.actionArg)
+ }
+
+ }
+
+ component StatusPill: Rectangle {
+ id: pill
+
+ property string title: ""
+ property var command: []
+ property int interval: 2000
+ property string value: "..."
+ property string actionName: ""
+ property string actionArg: ""
+ property string wheelAction: ""
+ property string wheelUpArg: ""
+ property string wheelDownArg: ""
+ property string accent: root.fg1
+
+ function refresh() {
+ if (pill.command.length > 0)
+ proc.exec(pill.command);
+
+ }
+
+ function matchesRefresh(actionName) {
+ return actionName === "" || actionName === pill.actionName || actionName === pill.wheelAction;
+ }
+
+ Layout.preferredHeight: 28
+ Layout.preferredWidth: Math.max(56, label.implicitWidth + 18)
+ width: Math.max(56, label.implicitWidth + 18)
+ height: 28
+ radius: 5
+ color: mouse.pressed ? root.bg3 : (mouse.containsMouse ? root.bg2 : root.bg)
+ border.width: 1
+ border.color: root.bg2
+
+ Text {
+ id: label
+
+ anchors.centerIn: parent
+ text: pill.title + " " + pill.value
+ color: root.textColor(pill.value, pill.accent)
+ font.family: root.fontFamily
+ font.pixelSize: 12
+ }
+
+ Process {
+ id: proc
+
+ command: pill.command
+ running: true
+
+ stdout: StdioCollector {
+ onStreamFinished: pill.value = this.text.trim()
+ }
+
+ }
+
+ Connections {
+ function onRefreshRequested(actionName) {
+ if (pill.matchesRefresh(actionName))
+ pill.refresh();
+
+ }
+
+ target: root
+ }
+
+ Timer {
+ interval: pill.interval
+ running: true
+ repeat: true
+ onTriggered: pill.refresh()
+ }
+
+ MouseArea {
+ id: mouse
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: pill.actionName !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: {
+ if (pill.actionName !== "")
+ root.runAction(pill.actionName, pill.actionArg);
+
+ }
+ onWheel: {
+ if (pill.wheelAction !== "")
+ root.runAction(pill.wheelAction, wheel.angleDelta.y > 0 ? pill.wheelUpArg : pill.wheelDownArg);
+
+ }
+ }
+
+ }
+
+}
diff --git a/sddm/10-gruvbox-void.conf b/sddm/10-gruvbox-void.conf
new file mode 100644
index 0000000..28dd1dd
--- /dev/null
+++ b/sddm/10-gruvbox-void.conf
@@ -0,0 +1,4 @@
+[Theme]
+Current=gruvbox-void
+ThemeDir=/usr/share/sddm/themes
+Font=FiraCode Nerd Font
diff --git a/sddm/themes/gruvbox-void/Main.qml b/sddm/themes/gruvbox-void/Main.qml
new file mode 100644
index 0000000..88fc0cf
--- /dev/null
+++ b/sddm/themes/gruvbox-void/Main.qml
@@ -0,0 +1,331 @@
+import QtQuick 2.0
+import SddmComponents 2.0
+
+Rectangle {
+ id: root
+
+ property int sessionIndex: session.index
+ property string fontFamily: config.font ? config.font : "FiraCode Nerd Font"
+ property color bg: "#282828"
+ property color bg1: "#3c3836"
+ property color bg2: "#504945"
+ property color bg3: "#665c54"
+ property color fg: "#ebdbb2"
+ property color fg1: "#d5c4a1"
+ property color red: "#cc241d"
+ property color green: "#98971a"
+ property color yellow: "#d79921"
+ property color orange: "#d65d0e"
+ property color purple: "#b16286"
+ property color aqua: "#689d6a"
+ property string clockText: Qt.formatDateTime(new Date(), "hh:mm ddd dd MMM")
+
+ width: 1920
+ height: 1080
+ color: bg
+ Component.onCompleted: {
+ if (name.text == "")
+ name.focus = true;
+ else
+ password.focus = true;
+ }
+
+ TextConstants {
+ id: textConstants
+ }
+
+ Connections {
+ function onLoginSucceeded() {
+ promptMessage.text = textConstants.loginSucceeded;
+ promptMessage.color = green;
+ }
+
+ function onLoginFailed() {
+ password.text = "";
+ promptMessage.text = textConstants.loginFailed;
+ promptMessage.color = red;
+ }
+
+ function onInformationMessage(message) {
+ promptMessage.text = message;
+ promptMessage.color = orange;
+ }
+
+ target: sddm
+ }
+
+ Timer {
+ interval: 1000
+ running: true
+ repeat: true
+ onTriggered: root.clockText = Qt.formatDateTime(new Date(), "hh:mm ddd dd MMM")
+ }
+
+ Background {
+ anchors.fill: parent
+ source: config.background
+ fillMode: Image.PreserveAspectCrop
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "#99282828"
+ }
+
+ Rectangle {
+ id: topbar
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: 42
+ color: bg
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 1
+ color: bg2
+ }
+
+ Text {
+ anchors.left: parent.left
+ anchors.leftMargin: 16
+ anchors.verticalCenter: parent.verticalCenter
+ text: "void / i3"
+ color: yellow
+ font.family: fontFamily
+ font.pixelSize: 14
+ font.bold: true
+ }
+
+ Text {
+ anchors.right: parent.right
+ anchors.rightMargin: 16
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.clockText
+ color: fg
+ font.family: fontFamily
+ font.pixelSize: 13
+ font.bold: true
+ }
+
+ }
+
+ Rectangle {
+ id: panel
+
+ width: Math.min(430, parent.width - 80)
+ height: 356
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ radius: 6
+ color: "#e6282828"
+ border.width: 1
+ border.color: bg3
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: 22
+ spacing: 12
+
+ Text {
+ width: parent.width
+ text: textConstants.welcomeText.arg(sddm.hostName)
+ color: fg
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ font.family: fontFamily
+ font.pixelSize: 18
+ font.bold: true
+ }
+
+ Text {
+ width: parent.width
+ text: textConstants.userName
+ color: fg1
+ font.family: fontFamily
+ font.pixelSize: 12
+ font.bold: true
+ }
+
+ TextBox {
+ id: name
+
+ width: parent.width
+ height: 34
+ text: userModel.lastUser
+ color: bg1
+ borderColor: bg3
+ focusColor: yellow
+ hoverColor: aqua
+ textColor: fg
+ radius: 4
+ font.family: fontFamily
+ font.pixelSize: 14
+ KeyNavigation.backtab: rebootButton
+ KeyNavigation.tab: password
+ Keys.onPressed: {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ sddm.login(name.text, password.text, sessionIndex);
+ event.accepted = true;
+ }
+ }
+ }
+
+ Text {
+ width: parent.width
+ text: textConstants.password
+ color: fg1
+ font.family: fontFamily
+ font.pixelSize: 12
+ font.bold: true
+ }
+
+ PasswordBox {
+ id: password
+
+ width: parent.width
+ height: 34
+ color: bg1
+ borderColor: bg3
+ focusColor: yellow
+ hoverColor: aqua
+ textColor: fg
+ radius: 4
+ font.family: fontFamily
+ font.pixelSize: 14
+ tooltipFG: fg
+ tooltipBG: bg1
+ KeyNavigation.backtab: name
+ KeyNavigation.tab: session
+ Keys.onPressed: {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ sddm.login(name.text, password.text, sessionIndex);
+ event.accepted = true;
+ }
+ }
+ }
+
+ Row {
+ width: parent.width
+ height: 34
+ spacing: 10
+
+ ComboBox {
+ id: session
+
+ width: Math.floor((parent.width - parent.spacing) * 0.62)
+ height: parent.height
+ model: sessionModel
+ index: sessionModel.lastIndex
+ color: bg1
+ menuColor: bg1
+ borderColor: bg3
+ focusColor: yellow
+ hoverColor: aqua
+ textColor: fg
+ arrowColor: bg2
+ font.family: fontFamily
+ font.pixelSize: 13
+ KeyNavigation.backtab: password
+ KeyNavigation.tab: layoutBox
+ }
+
+ LayoutBox {
+ id: layoutBox
+
+ width: parent.width - session.width - parent.spacing
+ height: parent.height
+ color: bg1
+ menuColor: bg1
+ borderColor: bg3
+ focusColor: yellow
+ hoverColor: aqua
+ textColor: fg
+ arrowColor: bg2
+ font.family: fontFamily
+ font.pixelSize: 13
+ KeyNavigation.backtab: session
+ KeyNavigation.tab: loginButton
+ }
+
+ }
+
+ Text {
+ id: promptMessage
+
+ width: parent.width
+ height: 18
+ text: textConstants.prompt
+ color: fg1
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ font.family: fontFamily
+ font.pixelSize: 11
+ }
+
+ Row {
+ width: parent.width
+ height: 34
+ spacing: 8
+
+ Button {
+ id: loginButton
+
+ width: Math.floor((parent.width - (parent.spacing * 2)) / 3)
+ height: parent.height
+ text: textConstants.login
+ color: yellow
+ activeColor: orange
+ pressedColor: green
+ textColor: bg
+ font.family: fontFamily
+ font.pixelSize: 13
+ onClicked: sddm.login(name.text, password.text, sessionIndex)
+ KeyNavigation.backtab: layoutBox
+ KeyNavigation.tab: shutdownButton
+ }
+
+ Button {
+ id: shutdownButton
+
+ width: loginButton.width
+ height: parent.height
+ text: textConstants.shutdown
+ color: bg2
+ activeColor: red
+ pressedColor: red
+ textColor: fg
+ font.family: fontFamily
+ font.pixelSize: 13
+ onClicked: sddm.powerOff()
+ KeyNavigation.backtab: loginButton
+ KeyNavigation.tab: rebootButton
+ }
+
+ Button {
+ id: rebootButton
+
+ width: parent.width - loginButton.width - shutdownButton.width - (parent.spacing * 2)
+ height: parent.height
+ text: textConstants.reboot
+ color: bg2
+ activeColor: orange
+ pressedColor: orange
+ textColor: fg
+ font.family: fontFamily
+ font.pixelSize: 13
+ onClicked: sddm.reboot()
+ KeyNavigation.backtab: shutdownButton
+ KeyNavigation.tab: name
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/sddm/themes/gruvbox-void/metadata.desktop b/sddm/themes/gruvbox-void/metadata.desktop
new file mode 100644
index 0000000..26d1fa7
--- /dev/null
+++ b/sddm/themes/gruvbox-void/metadata.desktop
@@ -0,0 +1,16 @@
+[SddmGreeterTheme]
+Name=Gruvbox Void
+Description=Gruvbox SDDM theme matching the local i3 setup
+Author=aag
+Copyright=2026
+License=MIT
+Type=sddm-theme
+Version=1.0
+Website=
+Screenshot=
+MainScript=Main.qml
+ConfigFile=theme.conf
+TranslationsDirectory=translations
+Email=
+Theme-Id=gruvbox-void
+Theme-API=2.0
diff --git a/sddm/themes/gruvbox-void/theme.conf b/sddm/themes/gruvbox-void/theme.conf
new file mode 100644
index 0000000..f1a1ab6
--- /dev/null
+++ b/sddm/themes/gruvbox-void/theme.conf
@@ -0,0 +1,3 @@
+[General]
+background=void.png
+font=FiraCode Nerd Font