Compare commits
5 Commits
features/h
...
d3856ada0d
| Author | SHA1 | Date | |
|---|---|---|---|
| d3856ada0d | |||
| 3601f52600 | |||
| 98f9a77b23 | |||
| bc242ae956 | |||
| 4237b2ed9e |
@@ -59,7 +59,7 @@ List of programs and tools I use.
|
||||
|📁 File Manager | [thunar](https://wiki.archlinux.org/title/Thunar) |
|
||||
|📝 Editor | [vs code](https://github.com/microsoft/vscode) / [neovim (changing to soon)](https://github.com/neovim/neovim) |
|
||||
|🌐 Browser | [zen](https://github.com/zen-browser/desktop) / [firefox](https://github.com/topics/firefox-browser) / [startpage](https://blog-bucket-personal.s3.eu-west-2.amazonaws.com/Screenshot_02-May_08-44-23_6614.png) |
|
||||
|📊 Bar | [waybar](https://github.com/Alexays/Waybar) |
|
||||
|📊 Bar | [waybar](https://github.com/Alexays/Waybar) / [quickshell](https://quickshell.org/) |
|
||||
|🚀 Launcher | [rofi](https://github.com/davatorium/rofi) |
|
||||
|🔒 Lockscreen | [hyprlock](https://github.com/hyprwm/hyprlock) |
|
||||
|🚪 Login Menu | [sddm](https://github.com/sddm/sddm) |
|
||||
|
||||
@@ -33,6 +33,9 @@ hl.on("hyprland.start", function()
|
||||
-- Starting hypridle to start hyprlock
|
||||
hl.exec_cmd("hypridle")
|
||||
|
||||
-- Float + center the Bitwarden extension pop-out (window rules can't catch it — see the script)
|
||||
hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh")
|
||||
|
||||
hl.dispatch(hl.dsp.focus({ workspace = 3 }))
|
||||
|
||||
-- NOTE: the trailing "&&" here is a stray leftover from the original .conf (dangling, does nothing) — ported as-is
|
||||
|
||||
73
dotfiles/.config/hypr/scripts/FloatBitwardenPopup.sh
Executable file
73
dotfiles/.config/hypr/scripts/FloatBitwardenPopup.sh
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Float + resize (to 781x702) + center the Bitwarden browser-extension pop-out window.
|
||||
#
|
||||
# Why this exists instead of a window rule: Hyprland's `float`/`center`/`size`
|
||||
# rules are "static" — evaluated exactly once, at the instant a window is
|
||||
# created — and this popup opens with the generic title "Zen Browser" (same
|
||||
# initialTitle/initialClass as the main browser window), only updating to
|
||||
# "Extension: (Bitwarden Password Manager) - Bitwarden..." a moment after
|
||||
# it's already mapped. By then the static rule has already missed its one
|
||||
# chance and there's no field available at creation time to tell the popup
|
||||
# apart from a normal browser window. See layout/windowrules.lua and
|
||||
# https://github.com/hyprwm/Hyprland/issues/7402 for the same problem class.
|
||||
#
|
||||
# No socat/ncat is installed, so instead of an event-driven IPC listener this
|
||||
# just polls `hyprctl clients` at a short interval with jq (already
|
||||
# installed).
|
||||
#
|
||||
# Dispatch syntax note: Hyprland 0.55+'s Lua config changed `hyprctl dispatch`
|
||||
# to take a literal Lua expression calling hl.dsp.* (e.g.
|
||||
# `hyprctl dispatch 'hl.dsp.window.center()'`), not the old
|
||||
# "centerwindow"-style dispatcher strings. Verified live against the real
|
||||
# API in /usr/share/hypr/stubs/hl.meta.lua before wiring this in.
|
||||
#
|
||||
# Why it re-asserts for a few seconds instead of firing once: right after the
|
||||
# popup leaves the tiled layout, Hyprland briefly re-tiles the sibling window
|
||||
# to fill the freed space, and during that reflow this popup was observed
|
||||
# reporting `floating: false` again for one poll tick — snapping it back to
|
||||
# its old tiled geometry. Re-asserting float+center for a short settle window
|
||||
# after first detection rides that out. After the settle window it's left
|
||||
# alone, so a manual re-tile/move by you later isn't fought.
|
||||
|
||||
TITLE_PATTERN='Extension: \(Bitwarden Password Manager\)'
|
||||
POLL_INTERVAL=0.3
|
||||
SETTLE_TICKS=6 # ~2s of re-assertion after first detection, at POLL_INTERVAL
|
||||
TARGET_WIDTH=781
|
||||
TARGET_HEIGHT=702
|
||||
|
||||
declare -A ticks_left # address -> remaining re-assert ticks
|
||||
|
||||
while true; do
|
||||
sleep "$POLL_INTERVAL"
|
||||
|
||||
# Forget any tracked window that's no longer open.
|
||||
open_addresses=$(hyprctl clients -j | jq -r '.[].address')
|
||||
for addr in "${!ticks_left[@]}"; do
|
||||
grep -qF "$addr" <<<"$open_addresses" || unset 'ticks_left[$addr]'
|
||||
done
|
||||
|
||||
# Process substitution (not a pipe) so the while loop runs in this shell
|
||||
# and its writes to ticks_left actually persist across iterations.
|
||||
while IFS= read -r win; do
|
||||
title=$(jq -r '.title' <<<"$win")
|
||||
[[ "$title" =~ $TITLE_PATTERN ]] || continue
|
||||
|
||||
address=$(jq -r '.address' <<<"$win")
|
||||
floating=$(jq -r '.floating' <<<"$win")
|
||||
|
||||
# First time we've seen this window: start its settle window.
|
||||
[[ -v ticks_left[$address] ]] || ticks_left[$address]=$SETTLE_TICKS
|
||||
|
||||
# Already floating and past its settle window: leave it alone.
|
||||
[[ "$floating" == "true" && ${ticks_left[$address]} -le 0 ]] && continue
|
||||
|
||||
hyprctl dispatch "hl.dsp.focus({ window = \"address:$address\" })" >/dev/null
|
||||
hyprctl dispatch 'hl.dsp.window.float({ action = "enable" })' >/dev/null
|
||||
# resize({x=W,y=H}) sets the absolute size (not a delta) — verified live.
|
||||
# Must happen before center(), since resizing shifts what "centered" means.
|
||||
hyprctl dispatch "hl.dsp.window.resize({ x = $TARGET_WIDTH, y = $TARGET_HEIGHT })" >/dev/null
|
||||
hyprctl dispatch 'hl.dsp.window.center()' >/dev/null
|
||||
|
||||
ticks_left[$address]=$((ticks_left[$address] - 1))
|
||||
done < <(hyprctl clients -j | jq -c '.[] | {address, title, floating}')
|
||||
done
|
||||
259
dotfiles/.config/quickshell/scripts/CheckQuickshellSetup.sh
Executable file
259
dotfiles/.config/quickshell/scripts/CheckQuickshellSetup.sh
Executable file
@@ -0,0 +1,259 @@
|
||||
#!/bin/bash
|
||||
# Pre-flight check for everything this quickshell config depends on outside
|
||||
# its own QML — fonts, the compositor, backing D-Bus services, and the
|
||||
# external CLIs individual bar modules shell out to. Run this after
|
||||
# installing/stowing quickshell on a new machine, before assuming the bar
|
||||
# is just going to work.
|
||||
#
|
||||
# Usage: ./CheckQuickshellSetup.sh [path-to-quickshell-config]
|
||||
# Defaults to $XDG_CONFIG_HOME/quickshell or ~/.config/quickshell.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
QS_DIR="${1:-${XDG_CONFIG_HOME:-$HOME/.config}/quickshell}"
|
||||
|
||||
PASS=0
|
||||
WARN=0
|
||||
FAIL=0
|
||||
|
||||
c_green="\033[0;32m"; c_yellow="\033[0;33m"; c_red="\033[0;31m"; c_reset="\033[0m"
|
||||
|
||||
pass() { printf " ${c_green}✓${c_reset} %s\n" "$1"; PASS=$((PASS+1)); }
|
||||
warn() { printf " ${c_yellow}!${c_reset} %s\n" "$1"; WARN=$((WARN+1)); }
|
||||
fail() { printf " ${c_red}✗${c_reset} %s\n" "$1"; FAIL=$((FAIL+1)); }
|
||||
section() { printf "\n%s\n" "$1"; }
|
||||
|
||||
svc_state() {
|
||||
# active / inactive / missing / no-systemctl. $1 = unit, $2 = --user (optional)
|
||||
# NB: capture-then-here-string, not `cmd | grep -q`. Under `set -o
|
||||
# pipefail`, grep -q exits the instant it finds a match, SIGPIPE-ing a
|
||||
# still-writing producer — that producer's non-zero exit (not grep's
|
||||
# success) becomes the pipeline's status, which reads as "not found"
|
||||
# even when it was. Verified this actually happens with fc-list below.
|
||||
if ! command -v systemctl >/dev/null 2>&1; then echo "no-systemctl"; return; fi
|
||||
local unit_files
|
||||
unit_files=$(systemctl ${2:-} list-unit-files "$1" 2>/dev/null)
|
||||
if grep -q "$1" <<< "$unit_files"; then
|
||||
systemctl ${2:-} is-active "$1" 2>/dev/null || echo "inactive"
|
||||
else
|
||||
echo "missing"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 0. Quickshell itself + config files present ----------------------------
|
||||
|
||||
section "Quickshell core"
|
||||
|
||||
if command -v quickshell >/dev/null 2>&1; then
|
||||
ver=$(quickshell --version 2>&1 | head -1)
|
||||
pass "quickshell found ($(command -v quickshell)) — $ver"
|
||||
else
|
||||
fail "quickshell binary not found on PATH — install it before anything else here matters"
|
||||
fi
|
||||
|
||||
for f in "shell.qml" "config/Appearance.qml" "config/Colors.qml" "config/qmldir"; do
|
||||
if [ -f "$QS_DIR/$f" ]; then
|
||||
pass "found $f"
|
||||
else
|
||||
fail "missing $QS_DIR/$f — dotfiles not fully stowed/symlinked here?"
|
||||
fi
|
||||
done
|
||||
|
||||
# Nearly every module (ActiveWindow, Workspaces, PowerButton, PanelWindow's
|
||||
# layer-shell surfaces, HyprlandFocusGrab) assumes a live Hyprland session.
|
||||
if [ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ] && command -v hyprctl >/dev/null 2>&1 && hyprctl monitors >/dev/null 2>&1; then
|
||||
pass "Hyprland is the running compositor"
|
||||
else
|
||||
fail "Hyprland doesn't appear to be running — Quickshell.Hyprland and the layer-shell bar will not function"
|
||||
fi
|
||||
|
||||
# --- 1. Fonts -----------------------------------------------------------------
|
||||
|
||||
section "Fonts"
|
||||
|
||||
if command -v fc-list >/dev/null 2>&1; then
|
||||
# Captured once, then grepped via here-string — see the pipefail/SIGPIPE
|
||||
# note on svc_state() above; fc-list's output is exactly what triggers it.
|
||||
fc_out=$(fc-list)
|
||||
|
||||
if grep -qi "Material Symbols Outlined" <<< "$fc_out"; then
|
||||
pass "Material Symbols Outlined installed"
|
||||
else
|
||||
fail "Material Symbols Outlined not found — install 'ttf-material-symbols-variable' (AUR); every glyph icon in the bar/panels will render as fallback text without it"
|
||||
fi
|
||||
|
||||
if grep -qi "CaskaydiaCove Nerd Font Mono" <<< "$fc_out"; then
|
||||
pass "CaskaydiaCove Nerd Font Mono installed"
|
||||
else
|
||||
fail "CaskaydiaCove Nerd Font Mono not found — install a Cascadia Code Nerd Font package; clock/labels/window title use it"
|
||||
fi
|
||||
else
|
||||
warn "fc-list not found (fontconfig missing?) — can't verify fonts"
|
||||
fi
|
||||
|
||||
# --- 2. Backing services for native Quickshell modules ----------------------
|
||||
# These modules talk to system services directly over D-Bus rather than
|
||||
# shelling out — the service being up is the actual dependency, not a CLI.
|
||||
|
||||
section "Backing services (native QML modules)"
|
||||
|
||||
st=$(svc_state NetworkManager.service)
|
||||
case "$st" in
|
||||
active) pass "NetworkManager.service active — Quickshell.Networking (NetworkIndicator)" ;;
|
||||
missing) fail "NetworkManager not installed — required by NetworkIndicator.qml" ;;
|
||||
*) fail "NetworkManager.service is $st — run: sudo systemctl enable --now NetworkManager" ;;
|
||||
esac
|
||||
|
||||
st=$(svc_state bluetooth.service)
|
||||
case "$st" in
|
||||
active) pass "bluetooth.service active — Quickshell.Bluetooth (BluetoothIndicator)" ;;
|
||||
missing) fail "bluez not installed — required by BluetoothIndicator.qml" ;;
|
||||
*) fail "bluetooth.service is $st — run: sudo systemctl enable --now bluetooth" ;;
|
||||
esac
|
||||
|
||||
# Pipewire/wireplumber are user-session services, not system ones.
|
||||
st=$(svc_state pipewire.service --user)
|
||||
case "$st" in
|
||||
active) pass "pipewire.service (user) active — Quickshell.Services.Pipewire (Volume/VolumeOsd)" ;;
|
||||
missing) fail "pipewire not installed — required by Volume.qml/VolumeOsd.qml" ;;
|
||||
*) fail "pipewire.service (user) is $st — run: systemctl --user enable --now pipewire pipewire-pulse wireplumber" ;;
|
||||
esac
|
||||
|
||||
st=$(svc_state wireplumber.service --user)
|
||||
case "$st" in
|
||||
active) pass "wireplumber.service (user) active" ;;
|
||||
missing) warn "wireplumber not installed — pipewire falls back to pipewire-media-session if present, otherwise volume control may misbehave" ;;
|
||||
*) fail "wireplumber.service (user) is $st — run: systemctl --user enable --now wireplumber" ;;
|
||||
esac
|
||||
|
||||
# --- 3. External CLIs shelled out to via Process ----------------------------
|
||||
|
||||
section "External CLIs (Quickshell.Io Process calls)"
|
||||
|
||||
check_bin() {
|
||||
# $1 = binary, $2 = which module(s) use it, $3 = fail|warn on missing
|
||||
if command -v "$1" >/dev/null 2>&1; then
|
||||
pass "$1 found — used by $2"
|
||||
elif [ "$3" = "warn" ]; then
|
||||
warn "$1 not found — used by $2 (optional on this machine)"
|
||||
else
|
||||
fail "$1 not found — used by $2"
|
||||
fi
|
||||
}
|
||||
|
||||
check_bin nmcli "NetworkIndicator.qml (DNS provider panel, connection queries)" fail
|
||||
check_bin ip "NetworkIndicator.qml (gateway/IP lookups)" fail
|
||||
check_bin ping "NetworkIndicator.qml (latency/packet-loss stat)" fail
|
||||
check_bin cat "NetworkIndicator.qml (/sys/class/net throughput counters)" fail
|
||||
check_bin systemctl "PowerButton.qml (suspend/hibernate/reboot/poweroff)" fail
|
||||
check_bin tailscale "TailscaleIndicator.qml / VpnExclusivity" warn
|
||||
check_bin mullvad "VpnIndicator.qml / VpnExclusivity" warn
|
||||
check_bin protonvpn "VpnIndicator.qml / VpnExclusivity" warn
|
||||
|
||||
have_tailscale=0; have_mullvad=0; have_proton=0
|
||||
command -v tailscale >/dev/null 2>&1 && have_tailscale=1
|
||||
command -v mullvad >/dev/null 2>&1 && have_mullvad=1
|
||||
command -v protonvpn >/dev/null 2>&1 && have_proton=1
|
||||
|
||||
if [ $have_mullvad -eq 0 ] && [ $have_proton -eq 0 ]; then
|
||||
warn "neither Mullvad nor ProtonVPN CLI is installed — VpnIndicator.qml will show both as 'Not installed'"
|
||||
fi
|
||||
|
||||
# --- 4. VPN daemons + functional status (Mullvad/ProtonVPN/Tailscale) -------
|
||||
# The exclusivity logic (VpnExclusivity.qml backing off Tailscale when a
|
||||
# commercial VPN connects) is pure QML and ships with the dotfiles — it
|
||||
# needs no setup of its own. What varies per machine is whether these three
|
||||
# are actually installed, enabled, and callable without a password prompt.
|
||||
|
||||
section "VPN dual-setup (Mullvad/ProtonVPN + Tailscale)"
|
||||
|
||||
if [ $have_tailscale -eq 1 ]; then
|
||||
st=$(svc_state tailscaled.service)
|
||||
case "$st" in
|
||||
active) pass "tailscaled.service is active" ;;
|
||||
missing) fail "tailscaled.service not found — reinstall tailscale" ;;
|
||||
*) fail "tailscaled.service is $st — run: sudo systemctl enable --now tailscaled" ;;
|
||||
esac
|
||||
|
||||
if out=$(timeout 5 tailscale status --json 2>&1); then
|
||||
if python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1 <<< "$out"; then
|
||||
pass "tailscale status --json returns valid JSON"
|
||||
else
|
||||
warn "tailscale status --json returned non-JSON output"
|
||||
fi
|
||||
else
|
||||
fail "tailscale status --json failed: $(echo "$out" | head -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $have_mullvad -eq 1 ]; then
|
||||
st=$(svc_state mullvad-daemon.service)
|
||||
case "$st" in
|
||||
active) pass "mullvad-daemon.service is active" ;;
|
||||
missing) fail "mullvad-daemon.service not found — reinstall the Mullvad package" ;;
|
||||
*) fail "mullvad-daemon.service is $st — run: sudo systemctl enable --now mullvad-daemon" ;;
|
||||
esac
|
||||
|
||||
if out=$(timeout 5 mullvad status --json 2>&1); then
|
||||
if python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1 <<< "$out"; then
|
||||
pass "mullvad status --json returns valid JSON"
|
||||
else
|
||||
warn "mullvad status --json returned non-JSON output"
|
||||
fi
|
||||
else
|
||||
fail "mullvad status --json failed: $(echo "$out" | head -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $have_proton -eq 1 ]; then
|
||||
# ProtonVPN's CLI drives connections through NetworkManager rather than
|
||||
# its own daemon — already checked above, just re-verified here in context.
|
||||
if out=$(timeout 5 protonvpn status 2>&1); then
|
||||
if grep -qi "^Status:" <<< "$out"; then
|
||||
pass "protonvpn status returns a parseable 'Status:' line"
|
||||
else
|
||||
warn "protonvpn status has no 'Status:' line — applyProtonStatus() in VpnIndicator.qml may misread it: $(echo "$out" | head -1)"
|
||||
fi
|
||||
else
|
||||
fail "protonvpn status failed: $(echo "$out" | head -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
ts_up=0; mv_up=0; pv_up=0
|
||||
# Same capture-then-here-string reasoning as above — these outputs are small
|
||||
# enough that the SIGPIPE race is unlikely, but there's no upside to relying
|
||||
# on that instead of just being consistent.
|
||||
[ $have_tailscale -eq 1 ] && grep -q '"BackendState":"Running"' <<< "$(tailscale status --json 2>/dev/null)" && ts_up=1
|
||||
[ $have_mullvad -eq 1 ] && grep -qi "^Connected" <<< "$(mullvad status 2>/dev/null)" && mv_up=1
|
||||
[ $have_proton -eq 1 ] && grep -qi "^Status:\s*Connected" <<< "$(protonvpn status 2>/dev/null)" && pv_up=1
|
||||
|
||||
active_count=$((ts_up + mv_up + pv_up))
|
||||
if [ $active_count -le 1 ]; then
|
||||
pass "at most one tunnel active right now (tailscale=$ts_up mullvad=$mv_up protonvpn=$pv_up)"
|
||||
else
|
||||
warn "more than one tunnel is up right now (tailscale=$ts_up mullvad=$mv_up protonvpn=$pv_up) — bring extras down manually; quickshell only enforces exclusivity when you toggle from its own UI"
|
||||
fi
|
||||
|
||||
# --- 5. System tray (currently disabled in Bar.qml) -------------------------
|
||||
|
||||
section "System tray (Tray.qml)"
|
||||
|
||||
if grep -q "^\s*//\s*Tray\s*{}" "$QS_DIR/modules/bar/Bar.qml" 2>/dev/null; then
|
||||
warn "Tray {} is commented out in Bar.qml — skipping icon-theme check (nothing to render)"
|
||||
elif [ -f "$QS_DIR/modules/bar/Bar.qml" ]; then
|
||||
if command -v gtk-update-icon-cache >/dev/null 2>&1 || [ -d /usr/share/icons/hicolor ]; then
|
||||
pass "an icon theme (at least hicolor) is present for Quickshell.Services.SystemTray icon lookups"
|
||||
else
|
||||
warn "no icon theme found — tray icons may render blank"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Summary -----------------------------------------------------------------
|
||||
|
||||
section "Summary"
|
||||
printf " ${c_green}%d passed${c_reset}, ${c_yellow}%d warnings${c_reset}, ${c_red}%d failed${c_reset}\n" "$PASS" "$WARN" "$FAIL"
|
||||
|
||||
if [ $FAIL -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user