From cce3b87edb5ad7145024e58221422a7452723450 Mon Sep 17 00:00:00 2001
From: Barrett Ruth
Date: Sun, 8 Feb 2026 18:22:31 -0500
Subject: [PATCH] bootstrap and system config improvements
---
README.md | 2 +-
config/screen/.gitkeep | 0
flake.lock | 45 ++++++-
flake.nix | 9 +-
home/home.nix | 19 +++
home/modules/bootstrap.nix | 32 +++++
home/modules/packages.nix | 20 ++-
home/modules/shell.nix | 54 +++++---
home/modules/ui.nix | 2 +
hosts/xps15/configuration.nix | 21 +++-
scripts/ctl | 15 +++
scripts/doc | 13 ++
scripts/hypr | 147 +++++++++++++++-------
scripts/mux | 229 ++++++++++++++++++++++++++--------
scripts/theme | 28 +++--
scripts/wp | 6 +-
scripts/x | 12 ++
17 files changed, 513 insertions(+), 141 deletions(-)
create mode 100644 config/screen/.gitkeep
create mode 100644 home/modules/bootstrap.nix
diff --git a/README.md b/README.md
index 71f1dab..2a47f8c 100644
--- a/README.md
+++ b/README.md
@@ -323,7 +323,7 @@ Compare your 207 explicit Arch packages against what's in
(or use devShells per-project)
- **Dev tools**: cmake, ninja, gdb, valgrind, perf — add to HM or devShells
- **CLI**: fastfetch, socat, rsync, bind (dig), time — add to HM packages
-- **AUR equivalents**: voxtype, pikaur (not needed), sioyek (already in HM),
+- **AUR equivalents**: pikaur (not needed), sioyek (already in HM),
basedpyright, pistol, clipmenu (not needed on Wayland)
### Things you DON'T need on NixOS
diff --git a/config/screen/.gitkeep b/config/screen/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/flake.lock b/flake.lock
index 3abae8d..0fcec6a 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,5 +1,23 @@
{
"nodes": {
+ "claude-code": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ },
+ "locked": {
+ "lastModified": 1770491177,
+ "narHash": "sha256-4fB6bJg5p5jb9k/vR3KrObVquBMRVfPa1wEY/pz17nQ=",
+ "owner": "ryoppippi",
+ "repo": "claude-code-overlay",
+ "rev": "be6e534fc6d9737d558f8ba41190513391fef01b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ryoppippi",
+ "repo": "claude-code-overlay",
+ "type": "github"
+ }
+ },
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
@@ -82,7 +100,7 @@
"inputs": {
"flake-parts": "flake-parts",
"neovim-src": "neovim-src",
- "nixpkgs": "nixpkgs"
+ "nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1770509107,
@@ -130,6 +148,22 @@
}
},
"nixpkgs": {
+ "locked": {
+ "lastModified": 1768127708,
+ "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
"locked": {
"lastModified": 1770380644,
"narHash": "sha256-P7dWMHRUWG5m4G+06jDyThXO7kwSk46C1kgjEWcybkE=",
@@ -145,7 +179,7 @@
"type": "github"
}
},
- "nixpkgs_2": {
+ "nixpkgs_3": {
"locked": {
"lastModified": 1770380644,
"narHash": "sha256-P7dWMHRUWG5m4G+06jDyThXO7kwSk46C1kgjEWcybkE=",
@@ -161,7 +195,7 @@
"type": "github"
}
},
- "nixpkgs_3": {
+ "nixpkgs_4": {
"locked": {
"lastModified": 1769461804,
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
@@ -179,18 +213,19 @@
},
"root": {
"inputs": {
+ "claude-code": "claude-code",
"fonts": "fonts",
"home-manager": "home-manager",
"neovim-nightly": "neovim-nightly",
"nixos-hardware": "nixos-hardware",
- "nixpkgs": "nixpkgs_2",
+ "nixpkgs": "nixpkgs_3",
"zen-browser": "zen-browser"
}
},
"zen-browser": {
"inputs": {
"home-manager": "home-manager_2",
- "nixpkgs": "nixpkgs_3"
+ "nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1770480420,
diff --git a/flake.nix b/flake.nix
index 9e7f587..72b72ce 100644
--- a/flake.nix
+++ b/flake.nix
@@ -10,13 +10,14 @@
nixos-hardware.url = "github:NixOS/nixos-hardware";
neovim-nightly.url = "github:nix-community/neovim-nightly-overlay";
zen-browser.url = "github:0xc000022070/zen-browser-flake";
+ claude-code.url = "github:ryoppippi/claude-code-overlay";
fonts = {
url = "git+ssh://git@github.com/barrettruth/fonts.git";
flake = false;
};
};
- outputs = { nixpkgs, home-manager, nixos-hardware, neovim-nightly, zen-browser, fonts, ... }:
+ outputs = { nixpkgs, home-manager, nixos-hardware, neovim-nightly, zen-browser, claude-code, fonts, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
@@ -24,11 +25,15 @@
config.allowUnfreePredicate = pkg: builtins.elem (nixpkgs.lib.getName pkg) [
"slack"
"claude-code"
+ "claude"
"nvidia-x11"
"nvidia-settings"
"apple_cursor"
];
- overlays = [ neovim-nightly.overlays.default ];
+ overlays = [
+ neovim-nightly.overlays.default
+ claude-code.overlays.default
+ ];
};
in {
nixosConfigurations.xps15 = nixpkgs.lib.nixosSystem {
diff --git a/home/home.nix b/home/home.nix
index 3e3f8ba..65de890 100644
--- a/home/home.nix
+++ b/home/home.nix
@@ -4,6 +4,7 @@ let
isNixOS = builtins.pathExists /etc/NIXOS;
in {
imports = [
+ ./modules/bootstrap.nix
./modules/theme.nix
./modules/shell.nix
./modules/terminal.nix
@@ -31,5 +32,23 @@ in {
};
programs.home-manager.enable = true;
+
+ systemd.user.services.nix-flake-update = {
+ Unit.Description = "Update nix flake inputs";
+ Service = {
+ Type = "oneshot";
+ WorkingDirectory = "%h/nix-config";
+ ExecStart = "${pkgs.nix}/bin/nix flake update";
+ };
+ };
+
+ systemd.user.timers.nix-flake-update = {
+ Unit.Description = "Auto-update nix flake inputs";
+ Timer = {
+ OnCalendar = "daily";
+ Persistent = true;
+ };
+ Install.WantedBy = [ "timers.target" ];
+ };
};
}
diff --git a/home/modules/bootstrap.nix b/home/modules/bootstrap.nix
new file mode 100644
index 0000000..72e4d8c
--- /dev/null
+++ b/home/modules/bootstrap.nix
@@ -0,0 +1,32 @@
+{ lib, config, pkgs, ... }:
+
+let
+ homeDir = config.home.homeDirectory;
+ repoDir = "${homeDir}/nix-config";
+
+ directories = [ "dev" "dl" "img" "img/screen" "wp" ];
+in {
+ home.activation.createDirectories = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
+ for dir in ${lib.concatStringsSep " " directories}; do
+ $DRY_RUN_CMD mkdir -p "$HOME/$dir"
+ done
+ '';
+
+ home.activation.cloneNixConfig = lib.hm.dag.entryAfter [ "createDirectories" ] ''
+ if [ ! -d "${repoDir}" ]; then
+ $DRY_RUN_CMD ${pkgs.git}/bin/git clone git@github.com:barrettruth/nix-config.git "${repoDir}" || true
+ fi
+ '';
+
+ home.activation.linkWallpapers = lib.hm.dag.entryAfter [ "cloneNixConfig" ] ''
+ src="${repoDir}/config/screen"
+ dest="$HOME/img/screen"
+ if [ -d "$src" ]; then
+ for f in "$src"/*; do
+ [ -f "$f" ] || continue
+ name=$(basename "$f")
+ [ -L "$dest/$name" ] || $DRY_RUN_CMD ln -sf "$f" "$dest/$name"
+ done
+ fi
+ '';
+}
diff --git a/home/modules/packages.nix b/home/modules/packages.nix
index 050437b..a440548 100644
--- a/home/modules/packages.nix
+++ b/home/modules/packages.nix
@@ -1,6 +1,7 @@
{ pkgs, lib, config, zen-browser, system, ... }:
let
+ claude = true;
zen = true;
sioyek = true;
vesktop = true;
@@ -16,11 +17,24 @@ in {
signal-desktop
slack
bitwarden-desktop
- claude-code
]
+ ++ lib.optionals claude [ claude-code ]
++ lib.optionals zen [ zen-browser.packages.${system}.default ]
- ++ lib.optionals sioyek [ pkgs.sioyek ]
- ++ lib.optionals vesktop [ pkgs.vesktop ];
+ ++ lib.optionals sioyek [ sioyek ]
+ ++ lib.optionals vesktop [ vesktop ];
+
+ xdg.configFile."claude/settings.json" = lib.mkIf claude {
+ text = builtins.toJSON {
+ permissions.defaultMode = "acceptEdits";
+ network_access = true;
+ allowed_domains = [
+ "github.com"
+ "raw.githubusercontent.com"
+ "api.github.com"
+ ];
+ tools.web_fetch = true;
+ };
+ };
home.activation.linkZenProfile = lib.mkIf zen (
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
diff --git a/home/modules/shell.nix b/home/modules/shell.nix
index 755bd92..5ca08f9 100644
--- a/home/modules/shell.nix
+++ b/home/modules/shell.nix
@@ -136,6 +136,10 @@ in {
'';
};
+ home.file.".zshenv".text = ''
+ export THEME="${config.theme}"
+ '';
+
programs.zsh = {
enable = true;
dotDir = "${config.xdg.configHome}/zsh";
@@ -170,6 +174,7 @@ in {
initContent = ''
export GPG_TTY=$(tty)
+ gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 || true
export THEME="''${THEME:-${config.theme}}"
setopt auto_cd
@@ -315,15 +320,16 @@ in {
set -g default-shell "$SHELL"
set -g renumber-windows on
+ setw -g automatic-rename off
set -g pane-base-index 1
set -g status-position bottom
set -g status-interval 5
- set -g status-left ' '
- set -g status-right ""
- set-hook -g session-created 'run "mux bar #S"'
- set-hook -g session-closed 'run "mux bar #S"'
- set-hook -g client-session-changed 'run "mux bar #S"'
+ set -g @c ','
+ set-hook -g session-created 'run "mux bar"'
+ set-hook -g session-closed 'run "mux bar"'
+ set-hook -g client-session-changed 'run "mux bar"'
+ set-hook -g pane-mode-changed 'refresh-client -S'
set -g status-bg '${c.bg}'
set -g status-fg '${c.fg}'
@@ -344,9 +350,10 @@ in {
unbind Up; bind k selectp -U
unbind Right; bind l selectp -R
- unbind m; bind m choose-tree -Z "join-pane -t '%%'"
- unbind n; bind n break-pane
- unbind p; bind p choose-tree -Z "join-pane -s '%%'"
+ unbind :; bind : display-popup -E 'mux cmd'
+ unbind s; bind s display-popup -E 'mux pick-session'
+ unbind w; bind w display-popup -E 'mux pick-window'
+ unbind p; bind p display-popup -E 'mux pick-pane'
bind -r Left resizep -L 5
bind -r Right resizep -R 5
@@ -362,29 +369,32 @@ in {
unbind ?; bind ? if -F '#{pane_in_mode}' 'send q' 'copy-mode \; send ?'
bind -T copy-mode-vi v send -X begin-selection
- bind -T copy-mode-vi y send -X copy-pipe-and-cancel 'xclip -in -sel c'
+ bind -T copy-mode-vi y send -X copy-pipe-and-cancel 'test -n "$WAYLAND_DISPLAY" && wl-copy || xclip -in -sel c'
- unbind C-b; bind C-b set status
- unbind C-m; bind C-m set mouse\; run 'mux bar #S'
+ unbind b; bind b set status\; refresh -S
+ unbind m; bind m set -g mouse\; run 'mux bar'\; refresh -S
- unbind e; bind e neww -n 'tmux.conf' "sh -c 'nvim $XDG_CONFIG_HOME/tmux/tmux.conf && tmux source $XDG_CONFIG_HOME/tmux/tmux.conf'"
+ unbind ^; bind ^ last-window
- unbind H; bind H run 'mux switch 0'\; run 'mux bar #S'
- unbind J; bind J run 'mux switch 1'\; run 'mux bar #S'
- unbind K; bind K run 'mux switch 2'\; run 'mux bar #S'
- unbind L; bind L run 'mux switch 3'\; run 'mux bar #S'
- unbind \$; bind \$ run 'mux switch 4'\; run 'mux bar #S'
+ unbind e; bind e neww -n 'tmux.conf' "sh -c 'nvim $XDG_CONFIG_HOME/tmux/tmux.conf; tmux source $XDG_CONFIG_HOME/tmux/tmux.conf'"
+
+ unbind H; bind H run 'mux switch 0'\; refresh -S
+ unbind J; bind J run 'mux switch 1'\; refresh -S
+ unbind K; bind K run 'mux switch 2'\; refresh -S
+ unbind L; bind L run 'mux switch 3'\; refresh -S
+ unbind \$; bind \$ run 'mux switch 4'\; refresh -S
unbind Tab; bind Tab switchc -l
set-hook -g client-light-theme 'source ${config.xdg.configHome}/tmux/themes/daylight.conf'
set-hook -g client-dark-theme 'source ${config.xdg.configHome}/tmux/themes/midnight.conf'
- unbind N; bind N run 'mux nvim'
- unbind C; bind C run 'mux claude'
+ unbind A; bind A run 'mux ai'
+ unbind C; bind C run 'mux code'
unbind R; bind R run 'mux run'
unbind T; bind T run 'mux term'
unbind G; bind G run 'mux git'
+ unbind M; bind M run 'mux misc'
set -g lock-after-time 300
set -g lock-command "pipes -p 2"
@@ -402,6 +412,9 @@ in {
set -g window-status-activity-style fg='#7aa2f7',bg='#121212',bold
set -g pane-border-style fg='#3d3d3d'
set -g pane-active-border-style fg='#e0e0e0'
+ set -g copy-mode-selection-style fg='#121212',bg='yellow'
+ set -g copy-mode-current-match-style fg='#121212',bg='yellow'
+ set -g copy-mode-match-style 'reverse'
'';
xdg.configFile."tmux/themes/daylight.conf".text = ''
@@ -413,6 +426,9 @@ in {
set -g window-status-activity-style fg='#3b5bdb',bg='#f5f5f5',bold
set -g pane-border-style fg='#e8e8e8'
set -g pane-active-border-style fg='#1a1a1a'
+ set -g copy-mode-selection-style fg='#f5f5f5',bg='yellow'
+ set -g copy-mode-current-match-style fg='#f5f5f5',bg='yellow'
+ set -g copy-mode-match-style 'reverse'
'';
programs.lf = {
diff --git a/home/modules/ui.nix b/home/modules/ui.nix
index 73d9c82..2ff43e5 100644
--- a/home/modules/ui.nix
+++ b/home/modules/ui.nix
@@ -15,6 +15,8 @@ in {
libnotify
brightnessctl
pamixer
+ socat
+ (python3.withPackages (ps: [ ps.pillow ]))
xorg.xinit
xorg.xmodmap
xorg.xrdb
diff --git a/hosts/xps15/configuration.nix b/hosts/xps15/configuration.nix
index 9f51d6e..ec574b4 100644
--- a/hosts/xps15/configuration.nix
+++ b/hosts/xps15/configuration.nix
@@ -27,17 +27,35 @@
};
services.automatic-timezoned.enable = true;
+ services.geoclue2.enable = true;
+ services.pcscd.enable = true;
i18n.defaultLocale = "en_US.UTF-8";
security.pam.services.hyprlock = {};
+ security.doas = {
+ enable = true;
+ extraRules = [{
+ groups = [ "wheel" ];
+ persist = true;
+ keepEnv = true;
+ }];
+ };
+
+ environment.binsh = "${pkgs.dash}/bin/dash";
+
users.users.barrett = {
isNormalUser = true;
extraGroups = [ "wheel" "docker" "libvirt" "storage" "power" ];
shell = pkgs.zsh;
};
- programs.zsh.enable = true;
+ programs.zsh = {
+ enable = true;
+ shellInit = ''
+ export ZDOTDIR="$HOME/.config/zsh"
+ '';
+ };
programs.hyprland.enable = true;
hardware.nvidia = {
@@ -93,6 +111,7 @@
vim
wget
git
+ dash
ntfs3g
efibootmgr
dmidecode
diff --git a/scripts/ctl b/scripts/ctl
index 096da85..2e6ba10 100755
--- a/scripts/ctl
+++ b/scripts/ctl
@@ -1,21 +1,34 @@
#!/bin/sh
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "ctl: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
case "$1" in
screenshot)
dir="$HOME/img/ss"
mkdir -p "$dir"
file="$dir/$(openssl rand -hex 10)-$(date +'%Y-%m-%d_%H-%M-%S').png"
if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
+ require grim slurp wl-copy
grim -g "$(slurp)" "$file" && wl-copy < "$file"
else
+ require maim xclip
maim -s "$file" && xclip -selection clipboard -t image/png -i "$file" &
fi
;;
ocr)
+ require tesseract
dir="$HOME/img/ss"
mkdir -p "$dir"
file="$dir/$(openssl rand -hex 10)-$(date +'%Y-%m-%d_%H-%M-%S').png"
if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
+ require grim slurp wl-copy
(
region="$(slurp)"
[ -n "$region" ] || exit 0
@@ -23,6 +36,7 @@ ocr)
tesseract -l eng "$file" - 2>/dev/null | wl-copy
) /dev/null 2>&1 &
else
+ require maim xclip
(
maim -s "$file" &&
tesseract -l eng "$file" - 2>/dev/null | xclip -selection clipboard -in
@@ -58,6 +72,7 @@ keyboard)
esac
;;
audio)
+ require pactl
case "$2" in
out)
sinks="$(pactl list short sinks | awk '{print $1": "$2}')"
diff --git a/scripts/doc b/scripts/doc
index eec2b6a..2167604 100755
--- a/scripts/doc
+++ b/scripts/doc
@@ -1,11 +1,24 @@
#!/bin/sh
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "doc: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
+require sioyek
+
dir="$HOME/doc"
test -d "$dir" || exit
if [ "$XDG_SESSION_TYPE" = x11 ]; then
+ require dmenu
picker() { dmenu -i -l 10 -p "Select file or folder: "; }
else
+ require rofi
picker() { rofi -dmenu -i -l 10 -p "Select file or folder"; }
fi
diff --git a/scripts/hypr b/scripts/hypr
index cb0392f..bcb32c7 100755
--- a/scripts/hypr
+++ b/scripts/hypr
@@ -1,5 +1,14 @@
#!/bin/sh
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "hypr: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
usage() {
cat < [app] [args...]
@@ -9,7 +18,7 @@ Commands:
brightness {up,down} Adjust brightness accordingly and notify
spawnfocus [args...] Focus existing window or spawn app with args
pull [app] Pull window to current workspace (picker if no app)
- borders Initialize dynamic borders
+ windowrules Apply dynamic window rules
exit Safely exit hyprland
Options:
@@ -31,12 +40,14 @@ case "$cmd" in
exit 0
;;
exit)
+ require hyprctl
pkill hypridle
pkill hyprpaper
hyprctl dispatch exit
exit 0
;;
brightness)
+ require brightnessctl notify-send
BRIGHT_STEP=5
max_brightness="$(brightnessctl max)"
case "$1" in
@@ -55,6 +66,7 @@ brightness)
esac
;;
volume)
+ require pactl notify-send
SINK="@DEFAULT_SINK@"
VOL_STEP=5
get_vol() { pactl get-sink-volume "$SINK" | awk 'NR==1{print $5+0}'; }
@@ -92,60 +104,72 @@ volume)
fi
;;
pull)
+ require hyprctl jq rofi
APP="$1"
- if [ -n "$APP" ]; then
- case "$APP" in
- google-chrome | google-chrome-stable) CLASS="google-chrome" ;;
- chromium | ungoogled-chromium) CLASS="Chromium" ;;
- firefox) CLASS="firefox" ;;
- alacritty) CLASS="Alacritty" ;;
- code | vscodium) CLASS="Code" ;;
- signal-desktop | signal) CLASS="signal" ;;
- telegram-desktop | telegram) CLASS="TelegramDesktop" ;;
- ghostty) CLASS="com.mitchellh.ghostty" ;;
- bitwarden-desktop | bitwarden) CLASS="Bitwarden" ;;
- slack) CLASS="Slack" ;;
- discord) CLASS="discord" ;;
- vesktop) CLASS="vesktop" ;;
- *) CLASS="$APP" ;;
- esac
- CUR_ADDR=$(hyprctl -j activewindow | jq -r '.address')
- WIN_ADDRS=$(
- hyprctl -j clients 2>/dev/null | jq -r --arg class "$CLASS" '
- .[]? | select(
- ((.xdgTag // "") | ascii_downcase | contains($class | ascii_downcase)) or
- ((.initialClass // "") | ascii_downcase | contains($class | ascii_downcase)) or
- ((.class // "") | ascii_downcase | contains($class | ascii_downcase))
- ) | "\(.class)\t\(.title)\t\(.address)"'
- )
- WIN_COUNT=$(echo "$WIN_ADDRS" | grep -c .)
- if [ "$WIN_COUNT" -eq 1 ]; then
- WIN_ADDR=$(echo "$WIN_ADDRS" | awk -F'\t' '{print $3}')
- elif [ "$WIN_COUNT" -gt 1 ]; then
- SELECTED=$(echo "$WIN_ADDRS" |
- awk -F'\t' -v cur="$CUR_ADDR" '{if($3!=cur) print $1 ": " $2 "\t" $3}' |
- rofi -dmenu -i -p "pull window")
- WIN_ADDR=$(echo "$SELECTED" | awk -F'\t' '{print $2}')
- fi
- fi
- if [ -z "$SELECTED" ]; then
+ case "$APP" in
+ google-chrome | google-chrome-stable) CLASS="google-chrome" ;;
+ zen | zen-browser) CLASS="zen" ;;
+ chromium | ungoogled-chromium) CLASS="Chromium" ;;
+ firefox) CLASS="firefox" ;;
+ alacritty) CLASS="Alacritty" ;;
+ code | vscodium) CLASS="Code" ;;
+ signal-desktop | signal) CLASS="signal" ;;
+ telegram-desktop | telegram) CLASS="TelegramDesktop" ;;
+ ghostty) CLASS="com.mitchellh.ghostty" ;;
+ bitwarden-desktop | bitwarden) CLASS="Bitwarden" ;;
+ slack) CLASS="Slack" ;;
+ discord) CLASS="discord" ;;
+ vesktop) CLASS="vesktop" ;;
+ element-desktop | element) CLASS="element" ;;
+ *) CLASS="$APP" ;;
+ esac
+
+ CUR_WS=$(hyprctl activeworkspace -j | jq -r '.id')
+
+ CUR_ADDR=$(hyprctl -j activewindow | jq -r '.address // empty')
+
+ WIN_ADDRS=$(hyprctl -j clients 2>/dev/null | jq -r --arg pat "$CLASS" '
+ .[]?
+ | select(
+ (.class? | ascii_downcase | contains($pat | ascii_downcase)) or
+ (.initialClass? | ascii_downcase | contains($pat | ascii_downcase)) or
+ (.xdgTag? // "" | ascii_downcase | contains($pat | ascii_downcase))
+ )
+ | "\(.class)\t\(.title // "no title")\t\(.address)"
+ ')
+
+ WIN_COUNT=$(echo "$WIN_ADDRS" | grep -c '^' || :)
+
+ if [ "$WIN_COUNT" -eq 0 ]; then
exit 0
fi
- if [ -z "$WIN_ADDR" ]; then
- CUR_ADDR=$(hyprctl -j activewindow | jq -r '.address')
- WIN_ADDRS=$(hyprctl -j clients 2>/dev/null | jq -r '.[]? | "\(.class)\t\(.title)\t\(.address)"')
- SELECTED=$(echo "$WIN_ADDRS" |
- awk -F'\t' -v cur="$CUR_ADDR" '{if($3!=cur) print $1 ": " $2 "\t" $3}' |
- rofi -dmenu -i -p "pull window")
+
+ if [ "$WIN_COUNT" -eq 1 ]; then
+ WIN_ADDR=$(echo "$WIN_ADDRS" | awk -F'\t' '{print $3}')
+ else
+ ROFI_LINES=$(
+ echo "$WIN_ADDRS" |
+ awk -F'\t' -v cur="$CUR_ADDR" '
+ $3 != cur { printf "%s : %s\t%s\n", $1, $2, $3 }
+ '
+ )
+
+ [ -z "$ROFI_LINES" ] && exit 0
+
+ SELECTED=$(echo "$ROFI_LINES" | rofi -dmenu -i -p "pull window")
+
+ [ -z "$SELECTED" ] && exit 0
+
WIN_ADDR=$(echo "$SELECTED" | awk -F'\t' '{print $2}')
fi
- if [ -n "$WIN_ADDR" ]; then
- CURRENT_WS="$(hyprctl activeworkspace | head -n1 | awk -F'[()]' '{print $2}')"
- hyprctl dispatch movetoworkspace "$CURRENT_WS,address:$WIN_ADDR"
- hyprctl dispatch focuswindow "address:$WIN_ADDR"
- fi
+
+ [ -z "$WIN_ADDR" ] && exit 0
+
+ hyprctl dispatch movetoworkspace "${CUR_WS},address:${WIN_ADDR}"
+ hyprctl dispatch focuswindow "address:${WIN_ADDR}"
;;
spawnfocus)
+ require hyprctl jq rofi socat
WS=""
while [ $# -gt 0 ]; do
case "$1" in
@@ -177,6 +201,7 @@ spawnfocus)
case "$APP" in
google-chrome | google-chrome-stable) CLASS="google-chrome" ;;
chromium | ungoogled-chromium) CLASS="Chromium" ;;
+ zen | zen-browser) CLASS="zen" ;;
firefox) CLASS="firefox" ;;
alacritty) CLASS="Alacritty" ;;
code | vscodium) CLASS="Code" ;;
@@ -187,6 +212,7 @@ spawnfocus)
slack) CLASS="Slack" ;;
discord) CLASS="discord" ;;
vesktop) CLASS="vesktop" ;;
+ element-desktop | element) CLASS="element" ;;
*) CLASS="$APP" ;;
esac
@@ -275,6 +301,31 @@ spawnfocus)
done
fi
;;
+windowrules)
+ require hyprctl socat
+ socat -u UNIX-CONNECT:"$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" - | while IFS= read -r line; do
+ event="${line%%>>*}"
+ data="${line#*>>}"
+ case "$event" in
+ windowtitlev2)
+ window_id="${data%%,*}"
+ window_title="${data#*,}"
+ window_title=$(echo "$window_title" | tr '[:upper:]' '[:lower:]')
+ case "$window_title" in
+ *'extension: (bitwarden password manager) - bitwarden'*)
+ hyprctl --batch "dispatch setfloating address:0x$window_id; dispatch centerwindow address:0x$window_id"
+ ;;
+ *'sign in - google accounts '*)
+ hyprctl --batch "dispatch setfloating address:0x$window_id; \
+ dispatch resizewindowpixel exact 30% 70%,address:0x$window_id; \
+ dispatch centerwindow address:0x$window_id"
+ ;;
+ esac
+ ;;
+ esac
+ done
+ ;;
+
*)
echo "Unknown subcommand: $cmd"
usage
diff --git a/scripts/mux b/scripts/mux
index 3c6a8a4..019238e 100755
--- a/scripts/mux
+++ b/scripts/mux
@@ -1,7 +1,27 @@
#!/bin/sh
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "mux: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
+require tmux
+
+get_scope() {
+ _wname=$(tmux display-message -p '#{window_name}')
+ case "$_wname" in
+ ai@*|code@*|git@*|run@*|term@*|misc@*) printf '%s' "${_wname#*@}" ;;
+ *) printf '%s' "$_wname" ;;
+ esac
+}
+
spawn_or_focus() {
- name="$1"
+ scope=$(get_scope)
+ name="${1}@${scope}"
cmd="$2"
if tmux list-windows -F '#{window_name}' | grep -Fx "$name" >/dev/null; then
@@ -15,12 +35,69 @@ spawn_or_focus() {
fi
}
+pick_session() {
+ require fzf column
+ sel=$({
+ printf 'name\twindows\tstatus\n'
+ tmux list-sessions -F '#{session_name} #{session_windows}w #{?session_attached,*,}'
+ } | column -t -s "$(printf '\t')" |
+ fzf --reverse --header-lines 1 --prompt 'select-session> ')
+ [ -n "$sel" ] && tmux switch-client -t "${sel%% *}"
+}
+
+pick_window() {
+ require fzf column
+ sel=$({
+ printf 'target\tname\tcommand\tpanes\n'
+ tmux list-windows -a -F '#{window_index}:#{session_name} #{window_name} #{pane_current_command} #{window_panes}p'
+ } | column -t -s "$(printf '\t')" |
+ fzf --reverse --header-lines 1 --prompt 'select-window> ')
+ sel="${sel%% *}"
+ [ -n "$sel" ] && tmux switch-client -t "${sel#*:}:${sel%%:*}"
+}
+
+pick_pane() {
+ require fzf column
+ sel=$({
+ printf 'target\tcommand\tpath\n'
+ tmux list-panes -a -F '#{window_index}.#{pane_index}:#{session_name} #{pane_current_command} #{pane_current_path}' |
+ sed "s|$HOME|~|g"
+ } | column -t -s "$(printf '\t')" |
+ fzf --reverse --header-lines 1 --prompt 'select-pane> ')
+ sel="${sel%% *}"
+ [ -n "$sel" ] && tmux switch-client -t "${sel#*:}:${sel%%:*}"
+}
+
+target_pane() {
+ require fzf column
+ sel=$({
+ printf 'target\tcommand\tpath\n'
+ tmux list-panes -a -F '#{window_index}.#{pane_index}:#{session_name} #{pane_current_command} #{pane_current_path}' |
+ sed "s|$HOME|~|g"
+ } | column -t -s "$(printf '\t')" |
+ fzf --reverse --header-lines 1 --prompt "$1> ")
+ sel="${sel%% *}"
+ [ -n "$sel" ] && printf '%s' "${sel#*:}:${sel%%:*}"
+}
+
+confirm() {
+ printf '%s ' "$1"
+ old=$(stty -g)
+ stty raw -echo
+ c=$(dd bs=1 count=1 2>/dev/null)
+ stty "$old"
+ printf '\n'
+ [ "$c" = "y" ]
+}
+
case "$1" in
bar)
- [ "$2" ] || exit
- mouse=""
- if [ "$(tmux show-options | grep mouse | awk '{ print $NF }')" = "on" ]; then
- mouse='[m]'
+ session=$(tmux display-message -p '#S')
+ [ "$session" ] || exit
+ if [ "$(tmux show-options -gv mouse)" = "on" ]; then
+ indicator='#{?pane_in_mode,[mouse#{@c}copy],[mouse]}'
+ else
+ indicator='#{?pane_in_mode,[copy],}'
fi
set -f
keys="H J K L"
@@ -38,67 +115,39 @@ bar)
key='?'
fi
star=""
- [ "$sname" = "$2" ] && star="*"
- bar_content="$bar_content#[range=session|${sid}]$key:$sname$star#[norange] "
+ [ "$sname" = "$session" ] && star="*"
+ [ -n "$bar_content" ] && bar_content="$bar_content │ "
+ bar_content="$bar_content#[range=session|${sid}]$key:$sname$star#[norange]"
i=$((i + 1))
done
set +f
- left='#[align=left list=on] #{W:#[range=window|#{window_index}]#{window_index}:#{window_name}#{window_flags}#[norange] }#[nolist]'
- right="#[align=right]$mouse $bar_content"
+ left='#[align=left list=on] #{W:#[range=window|#{window_index}]#{window_index}:#{window_name}#{window_flags}#[norange]#{?window_end_flag,, │ }}#[nolist]'
+ right="#[align=right]$indicator $bar_content"
tmux set -g 'status-format[0]' "$left$right"
;;
switch)
session="$(tmux ls -F '#S' | tail -n "+$(($2 + 1))" | head -1)"
tmux switch -t "$session"
;;
-exec)
- name="$(basename "$PWD")"
- project="$(basename "$(dirname "$PWD")")/$name"
-
- case "$project" in
- */bmath)
- cmd='cmake -B build -DCMAKE_BUILD_TYPE=Debug && cmake --build build && ctest --test-dir build --output-on-failure'
- ;;
- */ag)
- cmd='cp -f autograder.py example && cd example && ./run_autograder'
- ;;
- */project-a-10)
- cmd='. venv/bin/activate && python manage.py runserver'
- ;;
- */theCourseForum2)
- cmd='docker compose up'
- ;;
- */atlas | */tinyground)
- cmd='pnpm run dev'
- ;;
- */interview-prep)
- cmd='pnpm run dev'
- ;;
- */neovim)
- cmd='make'
- ;;
- */TestCppClient)
- cmd='rm -f TestCppClientStatic && cmake -S . -B build/ && make && ./TestCppClientStatic'
- ;;
- sl/*)
- cmd='make clean install && make clean'
- [ "$name" = 'slock' ] && cmd="doas $cmd"
- ;;
- */barrettruth.com)
- cmd='pnpm dev'
- ;;
- esac
-
- echo " > $cmd" | sed "s|$HOME|~|g"
- eval "$cmd"
+pick-session)
+ pick_session
;;
-claude)
- spawn_or_focus claude 'claude --chrome'
+pick-window)
+ pick_window
;;
-nvim)
- spawn_or_focus nvim 'nvim -c "lua require([[config.tmux]]).run([[nvim]])"'
+pick-pane)
+ pick_pane
+ ;;
+ai)
+ require claude
+ spawn_or_focus ai 'claude'
+ ;;
+code)
+ require nvim
+ spawn_or_focus code 'nvim -c "lua require([[config.tmux]]).run([[nvim]])"'
;;
git)
+ require nvim git
pane_path=$(tmux display-message -p '#{pane_current_path}')
if ! git -C "$pane_path" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
tmux display-message "Not a git repository"
@@ -107,11 +156,85 @@ git)
fi
;;
run)
+ require nvim
spawn_or_focus run 'nvim -c "lua require([[config.tmux]]).run([[run]])"'
;;
term)
spawn_or_focus term
;;
+misc)
+ spawn_or_focus misc
+ ;;
+cmd)
+ require fzf
+ result=$(tmux list-commands |
+ sed 's/ / /' |
+ fzf --reverse --prompt ':' --print-query \
+ --delimiter '\t' --with-nth '1,2' --accept-nth '1' \
+ --bind 'ctrl-y:transform-query(echo {1})+disable-search')
+ rc=$?
+ query=$(printf '%s' "$result" | head -1)
+ action=$(printf '%s' "$result" | sed -n '2p')
+ [ $rc -eq 130 ] && exit
+ if [ -n "$action" ]; then
+ case "$query" in
+ "$action "*)
+ tmux $query
+ exit
+ ;;
+ esac
+ case "$action" in
+ switch-client) pick_session ;;
+ select-window) pick_window ;;
+ select-pane) pick_pane ;;
+ rename-window)
+ cur=$(tmux display-message -p '#{window_name}')
+ printf 'name [%s]: ' "$cur"
+ read -r name
+ [ -n "$name" ] && tmux rename-window "$name"
+ ;;
+ rename-session)
+ cur=$(tmux display-message -p '#S')
+ printf 'name [%s]: ' "$cur"
+ read -r name
+ [ -n "$name" ] && tmux rename-session "$name"
+ ;;
+ join-pane)
+ target=$(target_pane 'join-pane')
+ [ -n "$target" ] && tmux join-pane -s "$target"
+ ;;
+ swap-pane)
+ target=$(target_pane 'swap-pane')
+ [ -n "$target" ] && tmux swap-pane -t "$target"
+ ;;
+ select-layout)
+ layout=$(printf '%s\n' \
+ 'even-horizontal' \
+ 'even-vertical' \
+ 'main-horizontal' \
+ 'main-vertical' \
+ 'tiled' |
+ fzf --reverse --prompt 'layout> ')
+ [ -n "$layout" ] && tmux select-layout "$layout"
+ ;;
+ kill-pane)
+ confirm 'kill-pane? [y/N]:' && tmux kill-pane
+ ;;
+ kill-window)
+ confirm "kill-window \"$(tmux display-message -p '#{window_name}')\"? [y/N]:" && tmux kill-window
+ ;;
+ kill-session)
+ confirm "kill-session \"$(tmux display-message -p '#S')\"? [y/N]:" && tmux kill-session
+ ;;
+ kill-server)
+ confirm 'kill-server? [y/N]:' && tmux kill-server
+ ;;
+ *) tmux "$action" ;;
+ esac
+ elif [ -n "$query" ]; then
+ tmux $query
+ fi
+ ;;
*)
tmux attach
;;
diff --git a/scripts/theme b/scripts/theme
index 828911e..4f0e633 100755
--- a/scripts/theme
+++ b/scripts/theme
@@ -1,5 +1,14 @@
#!/bin/sh
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "theme: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
themes="daylight
midnight"
@@ -11,8 +20,10 @@ Linux)
theme="$1"
else
if [ "$XDG_SESSION_TYPE" = "wayland" ]; then
+ require rofi
theme="$(printf "%s\n" "$themes" | rofi -dmenu -p 'theme')"
else
+ require dmenu
theme="$(printf "%s\n" "$themes" | dmenu -p 'select theme: ')"
fi
fi
@@ -56,9 +67,10 @@ Linux)
midnight)
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
;;
- *)
+ daylight)
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
;;
+ *) ;;
esac
fi
@@ -95,27 +107,27 @@ Darwin)
;;
esac
-if tmux list-sessions >/dev/null 2>&1; then
- test -f "$XDG_CONFIG_HOME/tmux/tmux.conf" && tmux source-file "$XDG_CONFIG_HOME/tmux/tmux.conf"
- [ "$TMUX" ] && tmux refresh-client -S
-fi
-
test -d "$XDG_CONFIG_HOME/fzf/themes" && ln -sf "$XDG_CONFIG_HOME/fzf/themes/$theme" "$XDG_CONFIG_HOME/fzf/themes/theme"
test -d "$XDG_CONFIG_HOME/rg/themes" && ln -sf "$XDG_CONFIG_HOME/rg/themes/$theme" "$XDG_CONFIG_HOME/rg/themes/theme"
+test -d "$XDG_CONFIG_HOME/sioyek/themes" && ln -sf "$XDG_CONFIG_HOME/sioyek/themes/$theme.config" "$XDG_CONFIG_HOME/sioyek/themes/theme.config"
test -d "$XDG_CONFIG_HOME/task/themes" && ln -sf "$XDG_CONFIG_HOME/task/themes/$theme.theme" "$XDG_CONFIG_HOME/task/themes/theme.theme"
if command -v claude >/dev/null 2>&1; then
+ CLAUDE_CONFIG="${CLAUDE_CONFIG_DIR:-$HOME}/.claude.json"
+ claude_theme='light'
case "$theme" in
daylight)
- claude config set theme light
+ claude_theme='light'
;;
midnight)
- claude config set theme dark
+ claude_theme='dark'
;;
esac
+ test -f "$CLAUDE_CONFIG" && jq ".theme=\"$claude_theme\"" "$CLAUDE_CONFIG" >"$CLAUDE_CONFIG.tmp" && mv "$CLAUDE_CONFIG.tmp" "$CLAUDE_CONFIG"
fi
test -f ~/.zshenv && sed -i "s|^\(export THEME=\).*|\1$theme|" ~/.zshenv
+[ -n "$TMUX" ] && tmux setenv -g THEME "$theme"
for socket in /tmp/nvim-*.sock; do
test -S "$socket" && nvim --server "$socket" --remote-expr "luaeval('require(\"config.fzf_reload\").reload()')" 2>/dev/null || true
diff --git a/scripts/wp b/scripts/wp
index 7581fe6..40ade30 100755
--- a/scripts/wp
+++ b/scripts/wp
@@ -5,7 +5,11 @@ import random
import subprocess
import sys
-from PIL import Image, ImageDraw
+try:
+ from PIL import Image, ImageDraw
+except ImportError:
+ print("wp: missing dependency: pillow (pip install pillow)", file=sys.stderr)
+ sys.exit(1)
HOME = os.environ["HOME"]
DIR = f"{HOME}/img/screen"
diff --git a/scripts/x b/scripts/x
index 9ab902c..2e74ad7 100755
--- a/scripts/x
+++ b/scripts/x
@@ -1,9 +1,19 @@
#!/bin/sh
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "x: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
cmd="$1"; shift
case "$cmd" in
setup)
+ require xrdb xset xmodmap xss-lock slock
xrdb -merge "$XDG_CONFIG_HOME"/X11/xresources."$THEME"
xset b off
xset m 0
@@ -14,6 +24,7 @@ setup)
xss-lock -- slock &
;;
bg)
+ require xrandr feh
randr="$(xrandr | rg ' connected ')"
mons="$(echo "$randr" | wc -l)"
wpdir="$HOME"/img/wp
@@ -31,6 +42,7 @@ bg)
eval "$cmd"
;;
mon)
+ require xrandr
mons="$(xrandr | rg --no-config ' connected ' | awk '{ print $1 }')"
one="$(echo "$mons" | head -n 1)"