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
+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; }