From 5c57ee20f72a7a646004babb95bb9931069bce45 Mon Sep 17 00:00:00 2001 From: Owen Rummage Date: Fri, 21 Aug 2026 22:59:26 -0500 Subject: [PATCH] Add Arch Wayland dotfiles: sway/waybar/kitty/nvim/zsh, idempotent installer Auto-adapts sway workspace routing to however many monitors are connected (laptop vs. desktop) instead of hardcoding output names. --- .gitignore | 35 ++++++ README.md | 55 +++++++++ config/kitty/colors.conf | 25 ++++ config/kitty/kitty.conf | 17 +++ config/nvim/init.lua | 4 + config/nvim/lua/config/autocmds.lua | 10 ++ config/nvim/lua/config/keymaps.lua | 11 ++ config/nvim/lua/config/options.lua | 31 +++++ config/nvim/lua/config/plugins.lua | 10 ++ config/nvim/nvim-pack-lock.json | 8 ++ config/sway/conf.d/10-appearance.conf | 28 +++++ config/sway/conf.d/20-input.conf | 14 +++ config/sway/conf.d/40-autostart.conf | 4 + config/sway/config | 16 +++ config/sway/keybinds | 60 ++++++++++ config/sway/scripts/select-workspace.sh | 32 ++++++ config/sway/wallpapers/blue-horizon.svg | 42 +++++++ config/swaylock/config | 23 ++++ config/waybar/config.jsonc | 28 +++++ config/waybar/scripts/audio-input-cycle.sh | 34 ++++++ config/waybar/scripts/audio-output-cycle.sh | 51 +++++++++ config/waybar/scripts/network-cycle.sh | 53 +++++++++ config/waybar/scripts/power-menu.sh | 15 +++ config/waybar/scripts/rename-workspace.sh | 33 ++++++ config/waybar/style.css | 20 ++++ config/wofi/config | 9 ++ config/wofi/style.css | 7 ++ home/.zsh.d/10-environment.zsh | 6 + home/.zsh.d/20-options.zsh | 11 ++ home/.zsh.d/30-keybindings.zsh | 16 +++ home/.zsh.d/40-aliases.zsh | 16 +++ home/.zsh.d/90-prompt.zsh | 5 + home/.zshrc | 16 +++ install.sh | 119 ++++++++++++++++++++ system/greetd/config.toml | 6 + 35 files changed, 870 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/kitty/colors.conf create mode 100644 config/kitty/kitty.conf create mode 100644 config/nvim/init.lua create mode 100644 config/nvim/lua/config/autocmds.lua create mode 100644 config/nvim/lua/config/keymaps.lua create mode 100644 config/nvim/lua/config/options.lua create mode 100644 config/nvim/lua/config/plugins.lua create mode 100644 config/nvim/nvim-pack-lock.json create mode 100644 config/sway/conf.d/10-appearance.conf create mode 100644 config/sway/conf.d/20-input.conf create mode 100644 config/sway/conf.d/40-autostart.conf create mode 100644 config/sway/config create mode 100644 config/sway/keybinds create mode 100755 config/sway/scripts/select-workspace.sh create mode 100644 config/sway/wallpapers/blue-horizon.svg create mode 100644 config/swaylock/config create mode 100644 config/waybar/config.jsonc create mode 100755 config/waybar/scripts/audio-input-cycle.sh create mode 100755 config/waybar/scripts/audio-output-cycle.sh create mode 100755 config/waybar/scripts/network-cycle.sh create mode 100755 config/waybar/scripts/power-menu.sh create mode 100755 config/waybar/scripts/rename-workspace.sh create mode 100644 config/waybar/style.css create mode 100644 config/wofi/config create mode 100644 config/wofi/style.css create mode 100644 home/.zsh.d/10-environment.zsh create mode 100644 home/.zsh.d/20-options.zsh create mode 100644 home/.zsh.d/30-keybindings.zsh create mode 100644 home/.zsh.d/40-aliases.zsh create mode 100644 home/.zsh.d/90-prompt.zsh create mode 100644 home/.zshrc create mode 100755 install.sh create mode 100644 system/greetd/config.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27031a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Secrets — never commit tokens, keys, or credentials. +*.token +*.key +*.pem +*id_rsa* +*id_ed25519* +.env +.env.* +**/secrets/ +**/*secret* +!config/**/secrets*.example + +# 1Password / auth material some machines keep alongside dotfiles. +.config/op/ +op/*.token + +# Editor / tool junk. +*.swp +*.swo +*~ +.DS_Store +.netrwhist + +# Shell/session state that must stay per-machine. +.zsh_history +.zcompdump* +.cache/ + +# Neovim local/session artifacts. +config/nvim/.luarc.json +**/undodir/ + +# Backups written by install.sh (kept under $HOME, never in-repo, but +# ignore defensively in case one is ever created inside the checkout). +.dotfiles-backup/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4991b45 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Monochrome Arch Wayland dotfiles + +Modular dotfiles for a Sway desktop on Arch Linux. The installer can install the +required packages and creates symlinks back to this repository. + +## Install + +```sh +git clone git@git.noroutes.net:owen/dotfiles.git ~/code/dotfiles +cd ~/code/dotfiles +./install.sh +``` + +Useful options: + +```sh +./install.sh --no-packages # only create links +./install.sh --dry-run # show what would happen +./install.sh --help +``` + +Existing files are never deleted: conflicting paths are moved to +`~/.dotfiles-backup//`. Log out and choose Sway after installation. +Use `Alt+Shift+r` to reload Sway and `Alt+d` to open Wofi. + +## Layout + +- `config/sway/conf.d/` contains focused Sway fragments. +- `home/.zsh.d/*.zsh` is automatically sourced by `home/.zshrc`. +- Neovim settings and mappings live in separate Lua modules. +- The shared monochrome palette with blue accents is repeated in the native format each application + expects, so no template generator is required at runtime. + +## Single vs. dual monitor + +The same configuration works unmodified on the single-monitor laptop and the +dual-monitor desktop: + +- Sway ignores `output`/`workspace ... output` directives that reference a + display name which isn't currently connected, so the desktop-specific + `DP-3`/`DP-2` positioning in `config/sway/conf.d/10-appearance.conf` is a + harmless no-op on the laptop. +- `config/sway/scripts/select-workspace.sh` (bound to the workspace keys) + doesn't hardcode output names. It ranks whatever outputs are connected + left-to-right and maps workspaces 1-10 to the first, 11-20 to the second, + and so on — so it adapts automatically to however many monitors are + plugged in. + +## Secrets + +Nothing in this repository is or should be a secret. Machine-local +credentials (e.g. the 1Password service-account token read by +`home/.zshrc`) live outside the repo, in files under `$HOME` that +`.gitignore` also excludes defensively should they ever end up in the +checkout. diff --git a/config/kitty/colors.conf b/config/kitty/colors.conf new file mode 100644 index 0000000..f612949 --- /dev/null +++ b/config/kitty/colors.conf @@ -0,0 +1,25 @@ +foreground #d4d4d4 +background #000000 +selection_foreground #ffffff +selection_background #2563eb +cursor #60a5fa +cursor_text_color #000000 +url_color #7dd3fc +active_border_color #60a5fa +inactive_border_color #262626 +color0 #000000 +color1 #737373 +color2 #a3a3a3 +color3 #d4d4d4 +color4 #60a5fa +color5 #737373 +color6 #93c5fd +color7 #d4d4d4 +color8 #525252 +color9 #a3a3a3 +color10 #d4d4d4 +color11 #e5e5e5 +color12 #93c5fd +color13 #a3a3a3 +color14 #bfdbfe +color15 #ffffff diff --git a/config/kitty/kitty.conf b/config/kitty/kitty.conf new file mode 100644 index 0000000..4c497d4 --- /dev/null +++ b/config/kitty/kitty.conf @@ -0,0 +1,17 @@ +font_family JetBrainsMono Nerd Font +font_size 11.0 +background_opacity 0.85 +window_padding_width 8 +confirm_os_window_close 0 +enable_audio_bell no +cursor_shape beam +include colors.conf + +map ctrl+shift+enter new_window_with_cwd +map ctrl+shift+t new_tab_with_cwd + +# Send stable sequences for Zsh word-wise editing. +map ctrl+left send_text all \x1b[1;5D +map ctrl+right send_text all \x1b[1;5C +map ctrl+backspace send_text all \x1b\x7f +map ctrl+delete send_text all \x1b[3;5~ diff --git a/config/nvim/init.lua b/config/nvim/init.lua new file mode 100644 index 0000000..de8d3ce --- /dev/null +++ b/config/nvim/init.lua @@ -0,0 +1,4 @@ +require("config.options") +require("config.plugins") +require("config.keymaps") +require("config.autocmds") diff --git a/config/nvim/lua/config/autocmds.lua b/config/nvim/lua/config/autocmds.lua new file mode 100644 index 0000000..8998dd8 --- /dev/null +++ b/config/nvim/lua/config/autocmds.lua @@ -0,0 +1,10 @@ +local group = vim.api.nvim_create_augroup("dotfiles", { clear = true }) +vim.api.nvim_create_autocmd("TextYankPost", { + group = group, + callback = function() vim.highlight.on_yank({ timeout = 150 }) end, +}) +vim.api.nvim_create_autocmd("BufWritePre", { + group = group, + pattern = "*", + callback = function() pcall(vim.cmd, [[%s/\s\+$//e]]) end, +}) diff --git a/config/nvim/lua/config/keymaps.lua b/config/nvim/lua/config/keymaps.lua new file mode 100644 index 0000000..14fec05 --- /dev/null +++ b/config/nvim/lua/config/keymaps.lua @@ -0,0 +1,11 @@ +local map = vim.keymap.set +map("n", "w", "write", { desc = "Save file" }) +map("n", "q", "quit", { desc = "Quit" }) +map("n", "e", "Explore", { desc = "File explorer" }) +map("n", "", "nohlsearch") +map("n", "", "") +map("n", "", "") +map("n", "", "") +map("n", "", "") +map("v", "J", ":m '>+1gv=gv") +map("v", "K", ":m '<-2gv=gv") diff --git a/config/nvim/lua/config/options.lua b/config/nvim/lua/config/options.lua new file mode 100644 index 0000000..7434db5 --- /dev/null +++ b/config/nvim/lua/config/options.lua @@ -0,0 +1,31 @@ +vim.g.mapleader = " " +vim.g.maplocalleader = " " + +local opt = vim.opt +opt.number = true +opt.relativenumber = true +opt.cursorline = true +opt.termguicolors = true +opt.signcolumn = "yes" +opt.wrap = false +opt.scrolloff = 8 +opt.sidescrolloff = 8 +opt.tabstop = 2 +opt.shiftwidth = 2 +opt.expandtab = true +opt.smartindent = true +opt.ignorecase = true +opt.smartcase = true +opt.splitbelow = true +opt.splitright = true +opt.undofile = true +opt.updatetime = 250 +opt.timeoutlen = 400 + +vim.cmd.colorscheme("habamax") +vim.api.nvim_set_hl(0, "Normal", { fg = "#d4d4d4", bg = "#000000" }) +vim.api.nvim_set_hl(0, "NormalFloat", { fg = "#d4d4d4", bg = "#0a0a0a" }) +vim.api.nvim_set_hl(0, "CursorLine", { bg = "#0a0a0a" }) +vim.api.nvim_set_hl(0, "Visual", { bg = "#2563eb" }) +vim.api.nvim_set_hl(0, "LineNr", { fg = "#525252" }) +vim.api.nvim_set_hl(0, "CursorLineNr", { fg = "#60a5fa", bold = true }) diff --git a/config/nvim/lua/config/plugins.lua b/config/nvim/lua/config/plugins.lua new file mode 100644 index 0000000..d7bb100 --- /dev/null +++ b/config/nvim/lua/config/plugins.lua @@ -0,0 +1,10 @@ +vim.pack.add({ + "https://github.com/preservim/nerdtree", +}, { confirm = false }) + +vim.g.NERDTreeWinPos = "left" +vim.g.NERDTreeWinSize = 32 + +vim.api.nvim_create_user_command("Tree", "NERDTreeToggle", { + desc = "Toggle the NERDTree sidebar", +}) diff --git a/config/nvim/nvim-pack-lock.json b/config/nvim/nvim-pack-lock.json new file mode 100644 index 0000000..b963b22 --- /dev/null +++ b/config/nvim/nvim-pack-lock.json @@ -0,0 +1,8 @@ +{ + "plugins": { + "nerdtree": { + "rev": "690d061b591525890f1471c6675bcb5bdc8cdff9", + "src": "https://github.com/preservim/nerdtree" + } + } +} diff --git a/config/sway/conf.d/10-appearance.conf b/config/sway/conf.d/10-appearance.conf new file mode 100644 index 0000000..2cb9536 --- /dev/null +++ b/config/sway/conf.d/10-appearance.conf @@ -0,0 +1,28 @@ +font pango:JetBrainsMono Nerd Font 10 +gaps inner 8 +gaps outer 4 +default_border pixel 2 +default_floating_border pixel 2 + +client.focused #3b82f6 #000000 #60a5fa #2563eb #3b82f6 +client.focused_inactive #171717 #000000 #737373 #171717 #171717 +client.unfocused #0a0a0a #000000 #525252 #0a0a0a #0a0a0a +client.urgent #60a5fa #000000 #60a5fa #3b82f6 #60a5fa + +for_window [app_id="system-monitor"] floating enable, resize set 1000 700, move position center + +output * bg ~/.config/sway/wallpapers/blue-horizon.svg fill + +# Desktop-only dual-monitor arrangement: DP-3 is the centered primary +# display, DP-2 sits immediately to its left. Sway silently ignores +# `output` directives for names that don't exist, so this is a no-op on +# the single-monitor laptop (its internal panel, e.g. eDP-1, is untouched +# and simply gets everything). Workspace-to-output routing is not pinned +# here; scripts/select-workspace.sh assigns workspaces to whichever +# outputs are actually connected, in left-to-right order, so it adapts +# automatically on both machines without any per-host config. +output DP-3 position 0 0 +output DP-2 position -2560 0 + +output DP-3 scale 1 +output DP-2 scale 1 diff --git a/config/sway/conf.d/20-input.conf b/config/sway/conf.d/20-input.conf new file mode 100644 index 0000000..081b276 --- /dev/null +++ b/config/sway/conf.d/20-input.conf @@ -0,0 +1,14 @@ +input type:keyboard { + xkb_layout us + repeat_delay 300 + repeat_rate 40 +} + +input type:touchpad { + tap enabled + natural_scroll enabled + dwt enabled +} + +# Make workspace number bindings follow the monitor under the pointer. +focus_follows_mouse always diff --git a/config/sway/conf.d/40-autostart.conf b/config/sway/conf.d/40-autostart.conf new file mode 100644 index 0000000..57da1de --- /dev/null +++ b/config/sway/conf.d/40-autostart.conf @@ -0,0 +1,4 @@ +exec_always --no-startup-id sh -c 'pkill waybar; exec waybar' +exec --no-startup-id mako +exec --no-startup-id nm-applet --indicator +exec --no-startup-id /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 diff --git a/config/sway/config b/config/sway/config new file mode 100644 index 0000000..43724a4 --- /dev/null +++ b/config/sway/config @@ -0,0 +1,16 @@ +set $mod Mod1 +set $term kitty +set $menu wofi --show drun + +# Enable Xwayland +xwayland enable + +# Random xorg shit +exec_always dbus-update-activation-environment --systemd \ + WAYLAND_DISPLAY DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP XDG_SESSION_TYPE + +exec_always systemctl --user import-environment \ + WAYLAND_DISPLAY DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP XDG_SESSION_TYPE + +include ~/.config/sway/conf.d/*.conf +include ~/.config/sway/keybinds diff --git a/config/sway/keybinds b/config/sway/keybinds new file mode 100644 index 0000000..ec85025 --- /dev/null +++ b/config/sway/keybinds @@ -0,0 +1,60 @@ +floating_modifier $mod normal +bindsym $mod+Return exec $term +bindsym $mod+d exec $menu +bindsym $mod+Shift+q kill +bindsym $mod+Shift+c reload +bindsym $mod+Shift+r reload +bindsym $mod+Shift+e exec swaynag -t warning -m 'Exit Sway?' -b 'Exit' 'swaymsg exit' + +bindsym $mod+Left focus left +bindsym $mod+Down focus down +bindsym $mod+Up focus up +bindsym $mod+Right focus right +bindsym $mod+Shift+Left move left +bindsym $mod+Shift+Down move down +bindsym $mod+Shift+Up move up +bindsym $mod+Shift+Right move right + +bindsym $mod+f fullscreen toggle +bindsym $mod+Shift+space floating toggle + +set $workspace_script ~/.config/sway/scripts/select-workspace.sh +bindsym $mod+1 exec $workspace_script 1 +bindsym $mod+2 exec $workspace_script 2 +bindsym $mod+3 exec $workspace_script 3 +bindsym $mod+4 exec $workspace_script 4 +bindsym $mod+5 exec $workspace_script 5 +bindsym $mod+6 exec $workspace_script 6 +bindsym $mod+7 exec $workspace_script 7 +bindsym $mod+8 exec $workspace_script 8 +bindsym $mod+9 exec $workspace_script 9 +bindsym $mod+0 exec $workspace_script 10 +bindsym $mod+Shift+1 exec $workspace_script 1 move +bindsym $mod+Shift+2 exec $workspace_script 2 move +bindsym $mod+Shift+3 exec $workspace_script 3 move +bindsym $mod+Shift+4 exec $workspace_script 4 move +bindsym $mod+Shift+5 exec $workspace_script 5 move +bindsym $mod+Shift+6 exec $workspace_script 6 move +bindsym $mod+Shift+7 exec $workspace_script 7 move +bindsym $mod+Shift+8 exec $workspace_script 8 move +bindsym $mod+Shift+9 exec $workspace_script 9 move +bindsym $mod+Shift+0 exec $workspace_script 10 move + +bindsym $mod+Shift+s exec grim -g "$(slurp)" - | wl-copy --type image/png +bindsym Print exec grim -g "$(slurp)" - | wl-copy --type image/png +bindsym Shift+Print exec grim "$HOME/Pictures/screenshot-$(date +%F-%H%M%S).png" +bindsym XF86AudioRaiseVolume exec wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+ +bindsym XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- +bindsym XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle +bindsym XF86MonBrightnessUp exec brightnessctl set +5% +bindsym XF86MonBrightnessDown exec brightnessctl set 5%- + +mode "resize" { + bindsym Left resize shrink width 40px + bindsym Down resize grow height 40px + bindsym Up resize shrink height 40px + bindsym Right resize grow width 40px + bindsym Return mode "default" + bindsym Escape mode "default" +} +bindsym $mod+r mode "resize" diff --git a/config/sway/scripts/select-workspace.sh b/config/sway/scripts/select-workspace.sh new file mode 100755 index 0000000..c57c808 --- /dev/null +++ b/config/sway/scripts/select-workspace.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +slot=${1:-} +action=${2:-select} + +if [[ ! "$slot" =~ ^([1-9]|10)$ ]]; then + printf 'Usage: %s <1-10> [select|move]\n' "${0##*/}" >&2 + exit 2 +fi + +# Outputs are ordered left-to-right by position, not by name, so this works +# unmodified whether there is one output (laptop) or several (desktop): +# the focused output's rank in that order selects its block of 10 workspaces. +outputs=$(swaymsg -t get_outputs -r | jq -c 'sort_by(.rect.x)') +index=$(jq -r '[.[] | .focused] | index(true)' <<<"$outputs") +if [[ "$index" == null ]]; then + printf 'No focused output found\n' >&2 + exit 1 +fi +workspace=$((slot + index * 10)) + +case "$action" in + select) swaymsg workspace number "$workspace" >/dev/null ;; + move) + swaymsg move container to workspace number "$workspace" >/dev/null + ;; + *) + printf 'Unknown action: %s\n' "$action" >&2 + exit 2 + ;; +esac diff --git a/config/sway/wallpapers/blue-horizon.svg b/config/sway/wallpapers/blue-horizon.svg new file mode 100644 index 0000000..32e182f --- /dev/null +++ b/config/sway/wallpapers/blue-horizon.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/swaylock/config b/config/swaylock/config new file mode 100644 index 0000000..9657891 --- /dev/null +++ b/config/swaylock/config @@ -0,0 +1,23 @@ +daemonize +show-failed-attempts +clock +indicator +indicator-radius=100 +indicator-thickness=8 +font=JetBrainsMono Nerd Font +font-size=24 +color=000000 +inside-color=0a0a0add +inside-clear-color=2563ebdd +inside-ver-color=171717dd +inside-wrong-color=171717dd +ring-color=60a5faff +ring-clear-color=93c5fdff +ring-ver-color=38bdf8ff +ring-wrong-color=60a5faff +key-hl-color=60a5faff +text-color=d4d4d4ff +text-clear-color=ffffffff +text-ver-color=ffffffff +text-wrong-color=ffffffff +separator-color=00000000 diff --git a/config/waybar/config.jsonc b/config/waybar/config.jsonc new file mode 100644 index 0000000..e99cf24 --- /dev/null +++ b/config/waybar/config.jsonc @@ -0,0 +1,28 @@ +{ + "layer": "top", + "position": "top", + "height": 32, + "spacing": 6, + "modules-left": ["sway/workspaces", "sway/mode"], + "modules-center": ["clock"], + "modules-right": ["cpu", "memory", "custom/network", "pulseaudio", "backlight", "battery", "tray", "custom/power"], + "sway/workspaces": { + "disable-scroll": false, + "all-outputs": false, + "on-click-right": "~/.config/waybar/scripts/rename-workspace.sh" + }, + "clock": { "format": "{:%a %d %b %H:%M}", "tooltip-format": "{:%Y %B}\n{calendar}" }, + "cpu": { "format": "CPU {usage}%", "tooltip": true, "on-click": "kitty --class system-monitor --title 'System Monitor' btop" }, + "memory": { "format": "RAM {percentage}%", "tooltip-format": "{used:0.1f} GiB used", "on-click": "kitty --class system-monitor --title 'System Monitor' btop" }, + "custom/network": { "exec": "~/.config/waybar/scripts/network-cycle.sh", "return-type": "json", "interval": 300, "signal": 8, "on-click": "~/.config/waybar/scripts/network-cycle.sh next", "on-click-right": "~/.config/waybar/scripts/network-cycle.sh copy" }, + "pulseaudio": { "format": "{icon} {volume}%", "format-muted": "󰝟 muted", "format-icons": ["󰕿", "󰖀", "󰕾"], "tooltip-format": "{desc}", "on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", "on-click-right": "~/.config/waybar/scripts/audio-output-cycle.sh" }, + "backlight": { "format": "󰃠 {percent}%" }, + "battery": { "states": { "warning": 30, "critical": 15 }, "format": "{icon} {capacity}%", "format-charging": "󰂄 {capacity}%", "format-icons": ["󰁺", "󰁼", "󰁾", "󰂀", "󰁹"] }, + "tray": { "spacing": 8 }, + "custom/power": { + "format": "󰐥", + "tooltip": true, + "tooltip-format": "Power menu", + "on-click": "~/.config/waybar/scripts/power-menu.sh" + } +} diff --git a/config/waybar/scripts/audio-input-cycle.sh b/config/waybar/scripts/audio-input-cycle.sh new file mode 100755 index 0000000..cb3e975 --- /dev/null +++ b/config/waybar/scripts/audio-input-cycle.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +source_ids=() +current_index=-1 + +while IFS=$'\t' read -r id is_default; do + [[ -n "$id" ]] || continue + source_ids+=("$id") + [[ "$is_default" == true ]] && current_index=$((${#source_ids[@]} - 1)) +done < <( + wpctl status -n | awk ' + /^Audio$/ {in_audio = 1; next} + /^Video$/ {in_audio = 0; in_sources = 0} + in_audio && /Sources:/ {in_sources = 1; next} + /Filters:/ {in_sources = 0} + in_audio && in_sources && match($0, /[0-9]+\./) { + id = substr($0, RSTART, RLENGTH - 1) + print id "\t" (index($0, "*") ? "true" : "false") + } + ' +) + +((${#source_ids[@]} > 0)) || exit 0 +next_index=$(((current_index + 1) % ${#source_ids[@]})) +next_source_id="${source_ids[$next_index]}" +wpctl set-default "$next_source_id" + +source_name=$( + wpctl inspect "$next_source_id" \ + | awk -F' = ' '/node.nick/ {gsub(/^"|"$/, "", $2); print $2; exit}' +) +[[ -n "$source_name" ]] || source_name="Input $next_source_id" +notify-send -a Waybar -u low -r 9102 "Audio input changed" "$source_name" diff --git a/config/waybar/scripts/audio-output-cycle.sh b/config/waybar/scripts/audio-output-cycle.sh new file mode 100755 index 0000000..121e421 --- /dev/null +++ b/config/waybar/scripts/audio-output-cycle.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +sink_ids=() +current_index=-1 + +while IFS=$'\t' read -r id is_default; do + [[ -n "$id" ]] || continue + sink_ids+=("$id") + [[ "$is_default" == true ]] && current_index=$((${#sink_ids[@]} - 1)) +done < <( + wpctl status -n | awk ' + /^Audio$/ {in_audio = 1; next} + /^Video$/ {in_audio = 0; in_sinks = 0} + in_audio && /Sinks:/ {in_sinks = 1; next} + /Sources:/ {in_sinks = 0} + in_audio && in_sinks && match($0, /[0-9]+\./) { + id = substr($0, RSTART, RLENGTH - 1) + print id "\t" (index($0, "*") ? "true" : "false") + } + ' +) + +((${#sink_ids[@]} > 0)) || exit 0 +next_index=$(((current_index + 1) % ${#sink_ids[@]})) +next_sink_id="${sink_ids[$next_index]}" +wpctl set-default "$next_sink_id" + +# Move existing playback streams too; set-default alone only affects new ones. +while read -r stream_id; do + [[ -n "$stream_id" ]] || continue + if wpctl inspect "$stream_id" 2>/dev/null | grep -q 'media.class = "Stream/Output/Audio"'; then + wpctl set-target "$stream_id" "$next_sink_id" 2>/dev/null || true + fi +done < <( + wpctl status -n | awk ' + /^Audio$/ {in_audio = 1; next} + /^Video$/ {in_audio = 0; in_streams = 0} + in_audio && /Streams:/ {in_streams = 1; next} + in_audio && in_streams && match($0, /[0-9]+\./) { + print substr($0, RSTART, RLENGTH - 1) + } + ' +) + +sink_name=$( + wpctl inspect "$next_sink_id" \ + | awk -F' = ' '/node.nick/ {gsub(/^"|"$/, "", $2); print $2; exit}' +) +[[ -n "$sink_name" ]] || sink_name="Output $next_sink_id" +notify-send -a Waybar -u low -r 9102 "Audio output changed" "$sink_name" diff --git a/config/waybar/scripts/network-cycle.sh b/config/waybar/scripts/network-cycle.sh new file mode 100755 index 0000000..1bb3d29 --- /dev/null +++ b/config/waybar/scripts/network-cycle.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +state_file="${XDG_RUNTIME_DIR:-/tmp}/waybar-network-state-${UID}" +current_ip_file="${XDG_RUNTIME_DIR:-/tmp}/waybar-network-current-ip-${UID}" + +if [[ "${1:-}" == copy ]]; then + if [[ -s "$current_ip_file" ]]; then + current_ip=$(<"$current_ip_file") + printf '%s' "$current_ip" | wl-copy + notify-send -a Waybar -u low -r 9101 "IP address copied" "$current_ip" + fi + exit 0 +fi + +if [[ "${1:-}" == next ]]; then + state=0 + [[ -r "$state_file" ]] && read -r state <"$state_file" + printf '%s\n' "$((state + 1))" >"$state_file" + pkill -RTMIN+8 waybar 2>/dev/null || true + exit 0 +fi + +entries=() +while read -r interface addresses; do + [[ -n "$interface" && -n "$addresses" ]] || continue + entries+=("$interface $addresses") +done < <( + ip -o -4 addr show scope global \ + | awk '{sub(/\/.*/, "", $4); addresses[$2] = addresses[$2] (addresses[$2] ? "," : "") $4} END {for (interface in addresses) print interface, addresses[interface]}' \ + | sort +) + +public_ipv4=$(curl -4 -fsS --max-time 3 https://ifconfig.me/ip 2>/dev/null || true) +[[ -n "$public_ipv4" ]] && entries+=("public $public_ipv4") + +if ((${#entries[@]} == 0)); then + entries+=("network unavailable") +fi + +state=0 +[[ -r "$state_file" ]] && read -r state <"$state_file" +index=$((state % ${#entries[@]})) +tooltip=$(printf '%s\n' "${entries[@]}") +current_ip="${entries[$index]#* }" +if [[ "$current_ip" == unavailable ]]; then + : >"$current_ip_file" +else + printf '%s' "$current_ip" >"$current_ip_file" +fi + +jq -cn --arg text "󰈀 ${entries[$index]}" --arg tooltip "$tooltip" \ + '{text: $text, tooltip: $tooltip}' diff --git a/config/waybar/scripts/power-menu.sh b/config/waybar/scripts/power-menu.sh new file mode 100755 index 0000000..8d28a32 --- /dev/null +++ b/config/waybar/scripts/power-menu.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +choice=$( + printf '%s\n' 'Lock' 'Logout' 'Suspend' 'Reboot' 'Shut down' \ + | wofi --dmenu --prompt 'Power' --width 320 --height 300 +) || exit 0 + +case "$choice" in + Lock) swaylock -f ;; + Logout) swaymsg exit ;; + Suspend) systemctl suspend ;; + Reboot) systemctl reboot ;; + 'Shut down') systemctl poweroff ;; +esac diff --git a/config/waybar/scripts/rename-workspace.sh b/config/waybar/scripts/rename-workspace.sh new file mode 100755 index 0000000..88319bf --- /dev/null +++ b/config/waybar/scripts/rename-workspace.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +workspace=$(swaymsg -t get_workspaces -r | jq -c '.[] | select(.focused)') +number=$(jq -r '.num' <<<"$workspace") +current_name=$(jq -r '.name' <<<"$workspace") + +if (( number < 1 )); then + notify-send -a Waybar -u low "Workspace rename" \ + "The focused workspace does not have a numeric identity." + exit 1 +fi + +current_label=${current_name#"$number:"} +if [[ "$current_label" == "$current_name" ]]; then + current_label= +else + current_label=${current_label# } +fi + +label=$(printf '%s\n' "$current_label" | wofi --dmenu \ + --prompt "Name workspace $number (blank resets)" \ + --lines 1) || exit 0 + +label=${label//$'\n'/ } +if [[ -z "${label//[[:space:]]/}" ]]; then + new_name=$number +else + new_name="$number: $label" +fi + +quoted_name=$(jq -Rrn --arg name "$new_name" '$name | @json') +swaymsg "rename workspace to $quoted_name" >/dev/null diff --git a/config/waybar/style.css b/config/waybar/style.css new file mode 100644 index 0000000..272dceb --- /dev/null +++ b/config/waybar/style.css @@ -0,0 +1,20 @@ +@define-color bg #000000; +@define-color surface #0a0a0a; +@define-color blue #60a5fa; +@define-color blue-dark #2563eb; +@define-color text #d4d4d4; +@define-color muted #737373; + +* { border: none; border-radius: 0; font-family: "JetBrainsMono Nerd Font"; font-size: 13px; min-height: 0; } +window#waybar { background: @bg; color: @text; } +#workspaces button { padding: 0 10px; margin: 4px 2px; color: @muted; border-radius: 6px; } +#workspaces button:hover { background: @surface; background-image: none; color: @text; border: none; box-shadow: none; text-shadow: none; } +#workspaces button.focused { color: @bg; background: @blue; } +#workspaces button.focused:hover { background: @blue; color: @bg; } +#workspaces button.urgent { background: @blue-dark; color: @text; } +#mode, #clock, #cpu, #memory, #custom-network, #pulseaudio, #backlight, #battery, #tray, #custom-power { padding: 0 10px; margin: 4px 2px; background: transparent; border-radius: 6px; } +#mode:hover, #clock:hover, #cpu:hover, #memory:hover, #custom-network:hover, #pulseaudio:hover, #backlight:hover, #battery:hover, #tray:hover, #custom-power:hover { background: @surface; } +#clock { color: @blue; font-weight: bold; background: transparent; } +#cpu, #memory { color: @text; } +#custom-power { color: @blue; font-size: 16px; } +#battery.warning, #battery.critical { color: @blue; } diff --git a/config/wofi/config b/config/wofi/config new file mode 100644 index 0000000..eb86ca4 --- /dev/null +++ b/config/wofi/config @@ -0,0 +1,9 @@ +show=drun +width=560 +height=400 +location=center +allow_images=true +insensitive=true +prompt=Search +term=kitty + diff --git a/config/wofi/style.css b/config/wofi/style.css new file mode 100644 index 0000000..cf78b0a --- /dev/null +++ b/config/wofi/style.css @@ -0,0 +1,7 @@ +* { font-family: "JetBrainsMono Nerd Font"; font-size: 14px; } +window { background-color: #000000; color: #d4d4d4; border: 2px solid #3b82f6; border-radius: 10px; } +#input { margin: 12px; padding: 10px; color: #60a5fa; background: #0a0a0a; border: 1px solid #2563eb; border-radius: 7px; } +#inner-box { margin: 0 12px 12px; } +#entry { padding: 8px; border-radius: 6px; } +#entry:selected { background: #2563eb; color: #ffffff; } +#img { margin-right: 10px; } diff --git a/home/.zsh.d/10-environment.zsh b/home/.zsh.d/10-environment.zsh new file mode 100644 index 0000000..1b40a8e --- /dev/null +++ b/home/.zsh.d/10-environment.zsh @@ -0,0 +1,6 @@ +export EDITOR=nvim +export VISUAL=nvim +export PAGER=less +export LESS='-R' +export PATH="$HOME/.local/bin:$PATH" + diff --git a/home/.zsh.d/20-options.zsh b/home/.zsh.d/20-options.zsh new file mode 100644 index 0000000..3e02b81 --- /dev/null +++ b/home/.zsh.d/20-options.zsh @@ -0,0 +1,11 @@ +setopt auto_cd auto_pushd pushd_ignore_dups +setopt hist_ignore_all_dups hist_reduce_blanks hist_save_no_dups share_history +setopt interactive_comments extended_glob +HISTFILE="$HOME/.zsh_history" +HISTSIZE=50000 +SAVEHIST=10000 + +[[ -d "$HOME/.cache/zsh" ]] || mkdir -p "$HOME/.cache/zsh" +autoload -Uz compinit && compinit -d "$HOME/.cache/zsh/zcompdump" +zstyle ':completion:*' menu select +zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' diff --git a/home/.zsh.d/30-keybindings.zsh b/home/.zsh.d/30-keybindings.zsh new file mode 100644 index 0000000..2222200 --- /dev/null +++ b/home/.zsh.d/30-keybindings.zsh @@ -0,0 +1,16 @@ +bindkey -e +bindkey '^[[H' beginning-of-line +bindkey '^[[F' end-of-line +bindkey '^[[3~' delete-char +bindkey '^R' history-incremental-search-backward + +# Word-wise editing. The CSI bindings are emitted by Kitty (and many other +# terminals); the Meta bindings keep the same actions working elsewhere. +bindkey '^[b' backward-word +bindkey '^[f' forward-word +bindkey '^[[1;5D' backward-word +bindkey '^[[1;5C' forward-word + +bindkey '^[^?' backward-kill-word +bindkey '^[[127;5u' backward-kill-word +bindkey '^[[3;5~' kill-word diff --git a/home/.zsh.d/40-aliases.zsh b/home/.zsh.d/40-aliases.zsh new file mode 100644 index 0000000..191504a --- /dev/null +++ b/home/.zsh.d/40-aliases.zsh @@ -0,0 +1,16 @@ +alias ls='ls --color=auto --group-directories-first' +alias ll='ls -lah' +alias la='ls -A' +alias grep='grep --color=auto' +alias vim='nvim' +alias v='nvim' +alias sway-reload='swaymsg reload' +alias pkginstall='yay -Syu' +alias pkgsearch='yay -Ss' +alias pkgremove='yay -Rns' +alias pkgupdate='yay -Syu' + +mkcd() { mkdir -p -- "$1" && cd -- "$1"; } + +# Keep Kitty's terminfo locally, but advertise a widely supported type over SSH. +ssh() { TERM=xterm-256color command ssh "$@"; } diff --git a/home/.zsh.d/90-prompt.zsh b/home/.zsh.d/90-prompt.zsh new file mode 100644 index 0000000..0be001b --- /dev/null +++ b/home/.zsh.d/90-prompt.zsh @@ -0,0 +1,5 @@ +autoload -Uz colors && colors +setopt prompt_subst +PROMPT='%F{blue}%n%f@%F{cyan}%m%f %F{blue}%~%f %(?.%F{green}.%F{red})❯%f ' +RPROMPT='%F{240}%D{%H:%M}%f' + diff --git a/home/.zshrc b/home/.zshrc new file mode 100644 index 0000000..eb2b535 --- /dev/null +++ b/home/.zshrc @@ -0,0 +1,16 @@ +export ZDOTDIR="${ZDOTDIR:-$HOME}" +export ZSH_CONFIG_DIR="$HOME/.zsh.d" + +setopt null_glob +for zsh_config in "$ZSH_CONFIG_DIR"/*.zsh; do + source "$zsh_config" +done +unset zsh_config + + +# opencode +export PATH=/home/owen/.opencode/bin:$PATH +export PATH="$HOME/.npm-global/bin:$PATH" + +# 1Password service account (scoped to Homelab vault) — headless CLI auth, no biometric prompts +export OP_SERVICE_ACCOUNT_TOKEN="$(cat ~/.config/op/homelab-agent.token 2>/dev/null)" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..c7a411b --- /dev/null +++ b/install.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +DOTFILES_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +BACKUP_ROOT="${HOME}/.dotfiles-backup/$(date +%Y%m%d-%H%M%S)" +INSTALL_PACKAGES=true +DRY_RUN=false + +PACKAGES=( + sway swaybg swaylock waybar wofi kitty jq curl btop + greetd greetd-tuigreet + zsh neovim git base-devel + grim slurp wl-clipboard mako libnotify brightnessctl playerctl + pipewire pipewire-pulse wireplumber pavucontrol + network-manager-applet polkit-gnome xdg-desktop-portal-wlr + noto-fonts noto-fonts-emoji ttf-jetbrains-mono-nerd +) + +usage() { + cat <<'EOF' +Usage: ./install.sh [--no-packages] [--dry-run] [--help] + + --no-packages Skip pacman package installation + --dry-run Print actions without changing anything + --help Show this help +EOF +} + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +run() { + if "$DRY_RUN"; then printf ' '; printf '%q ' "$@"; printf '\n'; + else "$@"; fi +} + +while (($#)); do + case "$1" in + --no-packages) INSTALL_PACKAGES=false ;; + --dry-run) DRY_RUN=true ;; + --help|-h) usage; exit 0 ;; + *) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +link_path() { + local source=$1 target=$2 relative backup + if [[ -L "$target" && "$(readlink -f -- "$target")" == "$(readlink -f -- "$source")" ]]; then + log "Already linked: $target" + return + fi + if [[ -e "$target" || -L "$target" ]]; then + relative=${target#"${HOME}/"} + backup="${BACKUP_ROOT}/${relative}" + log "Backing up $target -> $backup" + run mkdir -p -- "$(dirname -- "$backup")" + run mv -- "$target" "$backup" + fi + run mkdir -p -- "$(dirname -- "$target")" + log "Linking $target -> $source" + run ln -s -- "$source" "$target" +} + +install_system_file() { + local source=$1 target=$2 relative backup + if [[ -r "$target" ]] && cmp -s -- "$source" "$target"; then + log "Already installed: $target" + return + fi + if [[ -e "$target" ]]; then + relative=${target#/} + backup="${BACKUP_ROOT}/${relative}" + log "Backing up $target -> $backup" + run mkdir -p -- "$(dirname -- "$backup")" + run sudo cp -- "$target" "$backup" + fi + log "Installing $target" + run sudo install -Dm644 -- "$source" "$target" +} + +if "$INSTALL_PACKAGES"; then + command -v pacman >/dev/null || { echo 'pacman was not found; use --no-packages on non-Arch systems.' >&2; exit 1; } + log "Installing Arch packages" + run sudo pacman -Syu --needed "${PACKAGES[@]}" + + if ! command -v yay >/dev/null; then + log "Installing yay from the AUR" + if "$DRY_RUN"; then + printf ' git clone https://aur.archlinux.org/yay.git /yay\n' + printf ' cd /yay && makepkg -si --noconfirm\n' + else + YAY_BUILD_DIR=$(mktemp -d) + git clone https://aur.archlinux.org/yay.git "$YAY_BUILD_DIR/yay" + ( + cd "$YAY_BUILD_DIR/yay" + makepkg -si --noconfirm + ) + rm -rf -- "$YAY_BUILD_DIR" + unset YAY_BUILD_DIR + fi + fi + + install_system_file "$DOTFILES_DIR/system/greetd/config.toml" /etc/greetd/config.toml + log "Enabling greetd" + run sudo systemctl enable greetd.service +fi + +for source in "$DOTFILES_DIR"/config/*; do + [[ -e "$source" ]] || continue + link_path "$source" "$HOME/.config/$(basename -- "$source")" +done +for source in "$DOTFILES_DIR"/home/.[!.]* "$DOTFILES_DIR"/home/..?*; do + [[ -e "$source" ]] || continue + link_path "$source" "$HOME/$(basename -- "$source")" +done + +if [[ "${SHELL:-}" != */zsh ]]; then + log "Zsh is installed. Run 'chsh -s /bin/zsh' to make it your login shell." +fi +log "Installation complete. Reload Sway with Alt+Shift+r." diff --git a/system/greetd/config.toml b/system/greetd/config.toml new file mode 100644 index 0000000..99e80d9 --- /dev/null +++ b/system/greetd/config.toml @@ -0,0 +1,6 @@ +[terminal] +vt = 1 + +[default_session] +command = "tuigreet --time --remember --remember-session --asterisks --cmd sway" +user = "greeter"