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.
This commit is contained in:
2026-08-21 22:59:26 -05:00
commit 5c57ee20f7
35 changed files with 870 additions and 0 deletions
+35
View File
@@ -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/
+55
View File
@@ -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/<timestamp>/`. 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.
+25
View File
@@ -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
+17
View File
@@ -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~
+4
View File
@@ -0,0 +1,4 @@
require("config.options")
require("config.plugins")
require("config.keymaps")
require("config.autocmds")
+10
View File
@@ -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,
})
+11
View File
@@ -0,0 +1,11 @@
local map = vim.keymap.set
map("n", "<leader>w", "<cmd>write<cr>", { desc = "Save file" })
map("n", "<leader>q", "<cmd>quit<cr>", { desc = "Quit" })
map("n", "<leader>e", "<cmd>Explore<cr>", { desc = "File explorer" })
map("n", "<Esc>", "<cmd>nohlsearch<cr>")
map("n", "<C-Left>", "<C-w><Left>")
map("n", "<C-Down>", "<C-w><Down>")
map("n", "<C-Up>", "<C-w><Up>")
map("n", "<C-Right>", "<C-w><Right>")
map("v", "J", ":m '>+1<cr>gv=gv")
map("v", "K", ":m '<-2<cr>gv=gv")
+31
View File
@@ -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 })
+10
View File
@@ -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",
})
+8
View File
@@ -0,0 +1,8 @@
{
"plugins": {
"nerdtree": {
"rev": "690d061b591525890f1471c6675bcb5bdc8cdff9",
"src": "https://github.com/preservim/nerdtree"
}
}
}
+28
View File
@@ -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
+14
View File
@@ -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
+4
View File
@@ -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
+16
View File
@@ -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
+60
View File
@@ -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"
+32
View File
@@ -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
+42
View File
@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2560" height="1440" viewBox="0 0 2560 1440">
<rect width="2560" height="1440" fill="#082f68"/>
<g fill="#dbeafe">
<circle cx="118" cy="112" r="1.5" opacity="0.8"/>
<circle cx="284" cy="336" r="1" opacity="0.65"/>
<circle cx="438" cy="178" r="2" opacity="0.85"/>
<circle cx="612" cy="514" r="1.5" opacity="0.7"/>
<circle cx="754" cy="92" r="1" opacity="0.75"/>
<circle cx="902" cy="284" r="1.5" opacity="0.9"/>
<circle cx="1088" cy="438" r="1" opacity="0.65"/>
<circle cx="1246" cy="152" r="2" opacity="0.8"/>
<circle cx="1414" cy="352" r="1" opacity="0.7"/>
<circle cx="1582" cy="86" r="1.5" opacity="0.85"/>
<circle cx="1748" cy="486" r="2" opacity="0.7"/>
<circle cx="1918" cy="224" r="1" opacity="0.9"/>
<circle cx="2086" cy="390" r="1.5" opacity="0.65"/>
<circle cx="2268" cy="126" r="2" opacity="0.8"/>
<circle cx="2442" cy="548" r="1" opacity="0.75"/>
<circle cx="178" cy="746" r="1" opacity="0.7"/>
<circle cx="362" cy="1028" r="2" opacity="0.8"/>
<circle cx="538" cy="842" r="1.5" opacity="0.65"/>
<circle cx="716" cy="1244" r="1" opacity="0.85"/>
<circle cx="886" cy="698" r="2" opacity="0.75"/>
<circle cx="1056" cy="1116" r="1.5" opacity="0.9"/>
<circle cx="1232" cy="908" r="1" opacity="0.65"/>
<circle cx="1402" cy="1302" r="2" opacity="0.8"/>
<circle cx="1574" cy="772" r="1" opacity="0.7"/>
<circle cx="1746" cy="1064" r="1.5" opacity="0.85"/>
<circle cx="1912" cy="876" r="2" opacity="0.7"/>
<circle cx="2088" cy="1268" r="1" opacity="0.9"/>
<circle cx="2260" cy="714" r="1.5" opacity="0.65"/>
<circle cx="2434" cy="1148" r="2" opacity="0.8"/>
<circle cx="328" cy="612" r="3.5" opacity="0.9"/>
<circle cx="676" cy="356" r="4" opacity="0.95"/>
<circle cx="1168" cy="682" r="3" opacity="0.85"/>
<circle cx="1462" cy="226" r="4.5" opacity="0.9"/>
<circle cx="1840" cy="624" r="3.5" opacity="0.95"/>
<circle cx="2184" cy="972" r="4" opacity="0.85"/>
<circle cx="948" cy="1268" r="3.5" opacity="0.9"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+23
View File
@@ -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
+28
View File
@@ -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": "<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>" },
"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"
}
}
+34
View File
@@ -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"
+51
View File
@@ -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"
+53
View File
@@ -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}'
+15
View File
@@ -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
+33
View File
@@ -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
+20
View File
@@ -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; }
+9
View File
@@ -0,0 +1,9 @@
show=drun
width=560
height=400
location=center
allow_images=true
insensitive=true
prompt=Search
term=kitty
+7
View File
@@ -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; }
+6
View File
@@ -0,0 +1,6 @@
export EDITOR=nvim
export VISUAL=nvim
export PAGER=less
export LESS='-R'
export PATH="$HOME/.local/bin:$PATH"
+11
View File
@@ -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}'
+16
View File
@@ -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
+16
View File
@@ -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 "$@"; }
+5
View File
@@ -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'
+16
View File
@@ -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)"
Executable
+119
View File
@@ -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 <temporary-directory>/yay\n'
printf ' cd <temporary-directory>/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."
+6
View File
@@ -0,0 +1,6 @@
[terminal]
vt = 1
[default_session]
command = "tuigreet --time --remember --remember-session --asterisks --cmd sway"
user = "greeter"