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)"