Compare commits
45 Commits
features/a
...
788d4939f0
| Author | SHA1 | Date | |
|---|---|---|---|
| 788d4939f0 | |||
| 9a038b74c3 | |||
| 954dedf7cd | |||
| 90d0e2bed0 | |||
| 14f72af3f2 | |||
| 836848dedd | |||
| fbaffc0816 | |||
| ccae379490 | |||
| 7f0fad5922 | |||
| b40d4a9fa4 | |||
| 19d6936aa6 | |||
| 4a57e9902f | |||
| 1201e6a4cc | |||
| 626c7dbcc1 | |||
| 35374800bf | |||
| 7cb7f10ff6 | |||
| 88484f8495 | |||
| 1babf2e334 | |||
| cc78daea77 | |||
| 0415804b67 | |||
| 75aa6b945b | |||
| 8b65136048 | |||
| 4bd3d327c6 | |||
| 786a3d191a | |||
| ad5efc9597 | |||
| 850da708c2 | |||
| de39c40460 | |||
| 72db6ca784 | |||
| 84828e58bc | |||
| 26b7bf4ecc | |||
| 5b58c27e80 | |||
| 82be653c04 | |||
| d395a155af | |||
| 243c90a809 | |||
| 38df0301f2 | |||
| 714fadc997 | |||
| 99e2225b51 | |||
| 4aaa1d2d98 | |||
| 14b7e748d4 | |||
| a0b9ea29fe | |||
| 007843db6e | |||
| 764652d826 | |||
| 8dedfe30fa | |||
| bc54a4b5b7 | |||
| d306270dc8 |
15
README.md
15
README.md
@@ -39,13 +39,26 @@ mkdir Projects
|
|||||||
git clone ssh://git@gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles.git
|
git clone ssh://git@gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Once that is complete, run this command:
|
On a fresh install, run the bootstrap script. It installs drivers (auto-detected),
|
||||||
|
every package in `system/packages.txt`, yay, flatpaks, then links the dotfiles and
|
||||||
|
system config below:
|
||||||
|
```console
|
||||||
|
~/Projects/dotfiles/bootstrap.sh # asks about the optional SVT-AV1 + ffmpeg rebuild
|
||||||
|
~/Projects/dotfiles/bootstrap.sh --svt-av1 # or --no-svt-av1 to skip the question
|
||||||
|
```
|
||||||
|
|
||||||
|
To only link the dotfiles, run this command:
|
||||||
```console
|
```console
|
||||||
stow -d ~/Projects/dotfiles/ -t ~/ dotfiles
|
stow -d ~/Projects/dotfiles/ -t ~/ dotfiles
|
||||||
```
|
```
|
||||||
|
|
||||||
This will sync wallpapers/config files to where they are usually located
|
This will sync wallpapers/config files to where they are usually located
|
||||||
|
|
||||||
|
System-level config (e.g. pacman options) lives in `system/` and is installed with sudo:
|
||||||
|
```console
|
||||||
|
~/Projects/dotfiles/system/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
## 📦 Programs
|
## 📦 Programs
|
||||||
|
|
||||||
List of programs and tools I use.
|
List of programs and tools I use.
|
||||||
|
|||||||
168
bootstrap.sh
Executable file
168
bootstrap.sh
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sets up a fresh Arch install: drivers, packages, dotfiles and system config.
|
||||||
|
# Safe to re-run; anything already installed is skipped.
|
||||||
|
#
|
||||||
|
# Usage: ./bootstrap.sh [--svt-av1 | --no-svt-av1]
|
||||||
|
# --svt-av1 also install svt-av1-essential-git and rebuild ffmpeg against it
|
||||||
|
# --no-svt-av1 skip that without asking
|
||||||
|
# (neither) ask
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
list="$repo/system/packages.txt"
|
||||||
|
|
||||||
|
svt_av1=ask
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--svt-av1) svt_av1=yes ;;
|
||||||
|
--no-svt-av1) svt_av1=no ;;
|
||||||
|
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
echo "Run this as your normal user, not root (it uses sudo where needed)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
# Prints the packages under a [section] of packages.txt, skipping comments
|
||||||
|
packages() {
|
||||||
|
awk -v want="[$1]" '
|
||||||
|
/^\[/ { section = $1; next }
|
||||||
|
section == want { sub(/#.*/, ""); if (NF) print $1 }
|
||||||
|
' "$list"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- System config -----------------------------------------------------------
|
||||||
|
|
||||||
|
step "System config (pacman options)"
|
||||||
|
"$repo/system/install.sh"
|
||||||
|
|
||||||
|
step "Enabling multilib (needed for Steam and 32-bit drivers)"
|
||||||
|
if ! grep -q '^\[multilib\]' /etc/pacman.conf; then
|
||||||
|
sudo sed -i '/^#\[multilib\]/,/^#Include/ s/^#//' /etc/pacman.conf
|
||||||
|
fi
|
||||||
|
sudo pacman -Syu --needed --noconfirm pciutils git base-devel
|
||||||
|
|
||||||
|
# --- Hardware ----------------------------------------------------------------
|
||||||
|
|
||||||
|
step "Detecting CPU and GPU"
|
||||||
|
hw=()
|
||||||
|
|
||||||
|
case "$(grep -m1 '^vendor_id' /proc/cpuinfo | awk '{print $3}')" in
|
||||||
|
AuthenticAMD) hw+=(amd-ucode); echo "CPU: AMD" ;;
|
||||||
|
GenuineIntel) hw+=(intel-ucode); echo "CPU: Intel" ;;
|
||||||
|
*) echo "CPU: unknown vendor, skipping microcode" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Vendor column of every display controller, e.g. "NVIDIA Corporation"
|
||||||
|
gpus="$(lspci -mm | awk -F'"' '$2 ~ /VGA|3D|Display/ { print $4 }')"
|
||||||
|
if grep -q NVIDIA <<<"$gpus"; then
|
||||||
|
echo "GPU: NVIDIA"
|
||||||
|
# nvidia-open only ships modules for the stock `linux` kernel; any other
|
||||||
|
# kernel needs the DKMS build. Supports Turing (GTX 16xx / RTX 20xx) and newer.
|
||||||
|
if pacman -Qq linux-lts linux-zen linux-hardened >/dev/null 2>&1; then
|
||||||
|
hw+=(nvidia-open-dkms linux-headers)
|
||||||
|
else
|
||||||
|
hw+=(nvidia-open)
|
||||||
|
fi
|
||||||
|
hw+=(nvidia-utils lib32-nvidia-utils libva-nvidia-driver)
|
||||||
|
fi
|
||||||
|
if grep -q 'Advanced Micro Devices' <<<"$gpus"; then
|
||||||
|
echo "GPU: AMD"
|
||||||
|
hw+=(mesa lib32-mesa vulkan-radeon lib32-vulkan-radeon)
|
||||||
|
fi
|
||||||
|
if grep -q 'Intel Corporation' <<<"$gpus"; then
|
||||||
|
echo "GPU: Intel"
|
||||||
|
hw+=(mesa lib32-mesa vulkan-intel lib32-vulkan-intel intel-media-driver)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Installed before the main list so Steam's 32-bit Vulkan dependency is
|
||||||
|
# already satisfied by the right driver
|
||||||
|
if (( ${#hw[@]} )); then
|
||||||
|
sudo pacman -S --needed --noconfirm "${hw[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Packages ----------------------------------------------------------------
|
||||||
|
|
||||||
|
step "Official repo packages"
|
||||||
|
mapfile -t pkgs < <(packages pacman)
|
||||||
|
sudo pacman -S --needed --noconfirm "${pkgs[@]}"
|
||||||
|
|
||||||
|
step "yay (AUR helper)"
|
||||||
|
if ! command -v yay >/dev/null; then
|
||||||
|
build="$(mktemp -d)"
|
||||||
|
git clone https://aur.archlinux.org/yay-bin.git "$build/yay-bin"
|
||||||
|
(cd "$build/yay-bin" && makepkg -si --noconfirm)
|
||||||
|
rm -rf "$build"
|
||||||
|
else
|
||||||
|
echo "already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "AUR packages"
|
||||||
|
mapfile -t pkgs < <(packages aur)
|
||||||
|
yay -S --needed --noconfirm "${pkgs[@]}"
|
||||||
|
|
||||||
|
step "Flatpak apps"
|
||||||
|
sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||||
|
mapfile -t pkgs < <(packages flatpak)
|
||||||
|
sudo flatpak install -y --noninteractive flathub "${pkgs[@]}"
|
||||||
|
|
||||||
|
step "Apps from official install scripts"
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
for app in $(packages installer); do
|
||||||
|
if command -v "$app" >/dev/null || [[ -x "$HOME/.local/bin/$app" ]]; then
|
||||||
|
echo "$app: already installed"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$app" in
|
||||||
|
claude) curl -fsSL https://claude.ai/install.sh | bash ;;
|
||||||
|
oh-my-posh) curl -fsSL https://ohmyposh.dev/install.sh | bash -s -- -d "$HOME/.local/bin" ;;
|
||||||
|
*) echo "$app: no install recipe in bootstrap.sh, skipping" >&2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Optional: SVT-AV1 essential + ffmpeg rebuild ----------------------------
|
||||||
|
|
||||||
|
if [[ $svt_av1 == ask ]]; then
|
||||||
|
read -rp $'\nInstall svt-av1-essential-git and rebuild ffmpeg against it? (slow) [y/N] ' reply
|
||||||
|
[[ $reply == [yY]* ]] && svt_av1=yes || svt_av1=no
|
||||||
|
fi
|
||||||
|
if [[ $svt_av1 == yes ]]; then
|
||||||
|
step "svt-av1-essential-git"
|
||||||
|
# Interactive on purpose: pacman asks to replace the conflicting svt-av1,
|
||||||
|
# and --noconfirm would answer no
|
||||||
|
yay -S --needed svt-av1-essential-git
|
||||||
|
step "Rebuilding ffmpeg against svt-av1-essential-git"
|
||||||
|
"$repo/system/rebuild-ffmpeg.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Services ----------------------------------------------------------------
|
||||||
|
|
||||||
|
step "Enabling services"
|
||||||
|
# Not started now: sddm would take over the screen mid-script. They come up on reboot.
|
||||||
|
mapfile -t units < <(packages services)
|
||||||
|
sudo systemctl enable "${units[@]}"
|
||||||
|
|
||||||
|
# --- Dotfiles ----------------------------------------------------------------
|
||||||
|
|
||||||
|
step "Linking dotfiles with stow"
|
||||||
|
# Create real parent dirs first, otherwise stow symlinks the whole directory
|
||||||
|
# (e.g. ~/.config) into the repo and every app's files end up in git
|
||||||
|
mkdir -p "$HOME/.config" "$HOME/.local/bin" "$HOME/.local/share" "$HOME/Pictures"
|
||||||
|
# Sunshine keeps credentials and logs beside its config; those must stay out of git
|
||||||
|
mkdir -p "$HOME/.config/sunshine"
|
||||||
|
mkdir -p -m 700 "$HOME/.ssh"
|
||||||
|
if ! stow -d "$repo" -t "$HOME" --restow dotfiles; then
|
||||||
|
echo "stow hit conflicts (listed above). Move those files aside, then run:" >&2
|
||||||
|
echo " stow -d \"$repo\" -t \"\$HOME\" dotfiles" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(getent passwd "$USER" | cut -d: -f7)" != */zsh ]]; then
|
||||||
|
step "Setting zsh as login shell"
|
||||||
|
chsh -s /usr/bin/zsh
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Done — reboot to load drivers and start Hyprland"
|
||||||
1
dotfiles/.config/environment.d/ssh-agent.conf
Normal file
1
dotfiles/.config/environment.d/ssh-agent.conf
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SSH_AUTH_SOCK=${HOME}/.bitwarden-ssh-agent.sock
|
||||||
@@ -47,3 +47,11 @@ regular4=53BDFA # blue
|
|||||||
regular5=FAE994 # magenta
|
regular5=FAE994 # magenta
|
||||||
regular6=99faf2 # cyan
|
regular6=99faf2 # cyan
|
||||||
regular7=C7C7C7 # white
|
regular7=C7C7C7 # white
|
||||||
|
|
||||||
|
[key-bindings]
|
||||||
|
clipboard-copy=Control+c XF86Copy
|
||||||
|
clipboard-paste=Control+Shift+v XF86Paste
|
||||||
|
|
||||||
|
[text-bindings]
|
||||||
|
# Ctrl+C now copies, so send the interrupt signal (^C) from Ctrl+Shift+C instead
|
||||||
|
\x03=Control+Shift+c
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ file:///home/thomas/Documents
|
|||||||
file:///home/thomas/Downloads
|
file:///home/thomas/Downloads
|
||||||
file:///home/thomas/Pictures
|
file:///home/thomas/Pictures
|
||||||
file:///home/thomas/Music
|
file:///home/thomas/Music
|
||||||
|
file:///home/thomas/Videos
|
||||||
smb://192.168.1.84/media/
|
smb://192.168.1.84/media/
|
||||||
smb://192.168.1.85/music/
|
smb://192.168.1.85/music/
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ env = ELECTRON_OZONE_PLATFORM_HINT,auto # auto selects Wayland if possible, X11
|
|||||||
# This is from Hyprland Wiki. Below will be activated nvidia gpu detected
|
# This is from Hyprland Wiki. Below will be activated nvidia gpu detected
|
||||||
# See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
|
# See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
|
||||||
|
|
||||||
#env = LIBVA_DRIVER_NAME,nvidia
|
env = LIBVA_DRIVER_NAME,nvidia
|
||||||
#env = __GLX_VENDOR_LIBRARY_NAME,nvidia
|
env = __GLX_VENDOR_LIBRARY_NAME,nvidia
|
||||||
#env = NVD_BACKEND,direct
|
env = NVD_BACKEND,direct
|
||||||
#env = GSK_RENDERER,ngl
|
env = GSK_RENDERER,ngl
|
||||||
|
|
||||||
# additional ENV's for nvidia. Caution, activate with care
|
# additional ENV's for nvidia. Caution, activate with care
|
||||||
#env = GBM_BACKEND,nvidia-drm
|
#env = GBM_BACKEND,nvidia-drm
|
||||||
@@ -87,3 +87,6 @@ env = ELECTRON_OZONE_PLATFORM_HINT,auto # auto selects Wayland if possible, X11
|
|||||||
# env = HYPRLAND_NO_RT,1 # Disables realtime priority setting by Hyprland.
|
# env = HYPRLAND_NO_RT,1 # Disables realtime priority setting by Hyprland.
|
||||||
# env = HYPRLAND_NO_SD_NOTIFY,1 # If systemd, disables the 'sd_notify' calls.
|
# env = HYPRLAND_NO_SD_NOTIFY,1 # If systemd, disables the 'sd_notify' calls.
|
||||||
# env = HYPRLAND_NO_SD_VARS,1 # Disables management of variables in systemd and dbus activation environments.
|
# env = HYPRLAND_NO_SD_VARS,1 # Disables management of variables in systemd and dbus activation environments.
|
||||||
|
|
||||||
|
# Bitwarden SSH agent (so GUI apps like VS Code see it, not just shells)
|
||||||
|
env = SSH_AUTH_SOCK,/home/thomas/.bitwarden-ssh-agent.sock
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ decoration {
|
|||||||
rounding = 10
|
rounding = 10
|
||||||
|
|
||||||
active_opacity = 1.0
|
active_opacity = 1.0
|
||||||
inactive_opacity = 0.9
|
inactive_opacity = 1.0
|
||||||
fullscreen_opacity = 1.0
|
fullscreen_opacity = 1.0
|
||||||
|
|
||||||
dim_inactive = true
|
dim_inactive = true
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
# <<<<< TUI AUTO START
|
# <<<<< TUI AUTO START
|
||||||
monitor=desc:LG Electronics LG ULTRAWIDE 0x0002E68A,2560x1080@60.00000,1920x-740,1.00000000,transform,3,vrr,0
|
monitor=desc:LG Electronics LG ULTRAWIDE 0x0002E68A,2560x1080@60.00000,1920x-740,1.00000000,transform,3,vrr,0
|
||||||
monitor=desc:HP Inc. HP 524pf VNG447017D,1920x1080@100.00000,-1920x0,1.00000000,transform,0,vrr,0
|
monitor=desc:HP Inc. HP 524pf VNG447017D,1920x1080@100.00000,-1920x0,1.00000000,transform,0,vrr,0
|
||||||
monitor=desc:Acer Technologies VG240Y S 0x126191DE,1920x1080@165.00301,0x0,1.00000000,transform,0,vrr,1,bitdepth,10,cm,wide
|
monitor=desc:Acer Technologies VG240Y S 0x126191DE,1920x1080@165.00301,0x0,1.00000000,transform,0,vrr,0,bitdepth,10,cm,wide
|
||||||
# <<<<< TUI AUTO END
|
# <<<<< TUI AUTO END
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ hl.config({
|
|||||||
rounding = 10,
|
rounding = 10,
|
||||||
|
|
||||||
active_opacity = 1.0,
|
active_opacity = 1.0,
|
||||||
inactive_opacity = 0.9,
|
inactive_opacity = 1.0,
|
||||||
fullscreen_opacity = 1.0,
|
fullscreen_opacity = 1.0,
|
||||||
|
|
||||||
dim_inactive = true,
|
dim_inactive = true,
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") -- auto selects Wayland if possib
|
|||||||
-- This is from the Hyprland wiki. Uncomment below if an nvidia GPU is detected.
|
-- This is from the Hyprland wiki. Uncomment below if an nvidia GPU is detected.
|
||||||
-- See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
|
-- See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
|
||||||
|
|
||||||
-- hl.env("LIBVA_DRIVER_NAME", "nvidia")
|
hl.env("LIBVA_DRIVER_NAME", "nvidia")
|
||||||
-- hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia")
|
hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia")
|
||||||
-- hl.env("NVD_BACKEND", "direct")
|
hl.env("NVD_BACKEND", "direct")
|
||||||
-- hl.env("GSK_RENDERER", "ngl")
|
hl.env("GSK_RENDERER", "ngl")
|
||||||
|
|
||||||
-- additional ENV's for nvidia. Caution, activate with care
|
-- additional ENV's for nvidia. Caution, activate with care
|
||||||
-- hl.env("GBM_BACKEND", "nvidia-drm")
|
-- hl.env("GBM_BACKEND", "nvidia-drm")
|
||||||
@@ -85,3 +85,6 @@ hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") -- auto selects Wayland if possib
|
|||||||
-- hl.env("HYPRLAND_NO_RT", "1") -- Disables realtime priority setting by Hyprland.
|
-- hl.env("HYPRLAND_NO_RT", "1") -- Disables realtime priority setting by Hyprland.
|
||||||
-- hl.env("HYPRLAND_NO_SD_NOTIFY", "1") -- If systemd, disables the 'sd_notify' calls.
|
-- hl.env("HYPRLAND_NO_SD_NOTIFY", "1") -- If systemd, disables the 'sd_notify' calls.
|
||||||
-- hl.env("HYPRLAND_NO_SD_VARS", "1") -- Disables management of variables in systemd and dbus activation environments.
|
-- hl.env("HYPRLAND_NO_SD_VARS", "1") -- Disables management of variables in systemd and dbus activation environments.
|
||||||
|
|
||||||
|
-- Bitwarden SSH agent (so GUI apps like VS Code see it, not just shells)
|
||||||
|
hl.env("SSH_AUTH_SOCK", os.getenv("HOME") .. "/.bitwarden-ssh-agent.sock")
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
-- whatever it last wrote. If you reinstall HyprMod, prefer letting its own "Migrate to Lua…"
|
-- whatever it last wrote. If you reinstall HyprMod, prefer letting its own "Migrate to Lua…"
|
||||||
-- wizard take over managing this file instead of hand-editing it going forward.
|
-- wizard take over managing this file instead of hand-editing it going forward.
|
||||||
|
|
||||||
|
-- vrr forced off globally: the per-monitor vrr = 0 flag below does NOT override this when
|
||||||
|
-- misc.vrr is 1 (tested live on Hyprland 0.56.2 — DP-2 stayed vrr:true until this global
|
||||||
|
-- was changed). DP-2 (Acer VG240Y S) flickers under VRR when idle in fullscreen (mouse
|
||||||
|
-- stationary -> no repaint damage -> frame delivery stalls near the panel's VRR floor).
|
||||||
|
-- The other two monitors don't negotiate VRR at all, so this has no effect on them.
|
||||||
hl.config({
|
hl.config({
|
||||||
general = {
|
general = {
|
||||||
allow_tearing = true,
|
allow_tearing = true,
|
||||||
@@ -13,17 +18,19 @@ hl.config({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
misc = {
|
misc = {
|
||||||
vrr = 1,
|
vrr = 0,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Monitors
|
-- Monitors
|
||||||
hl.monitor({ output = "DP-2", mode = "1920x1080@165.00Hz", position = "0x0", scale = 1, bitdepth = 10, cm = "wide" })
|
hl.monitor({ output = "DP-2", mode = "1920x1080@165.00Hz", position = "0x0", scale = 1, bitdepth = 10, cm = "wide", vrr = 0 })
|
||||||
|
|
||||||
-- Keybinds
|
-- Keybinds
|
||||||
-- (unbind = SUPER SHIFT, Print / unbind = SUPER, up from the source .conf have no Lua equivalent needed —
|
-- (unbind = SUPER SHIFT, Print / unbind = SUPER, up from the source .conf have no Lua equivalent needed —
|
||||||
-- we just don't define a conflicting bind elsewhere; SUPER + up -> movefocus u below is otherwise identical
|
-- we just don't define a conflicting bind elsewhere; SUPER + up -> movefocus u below is otherwise identical
|
||||||
-- to the one already in keybinds.lua, kept here too for parity with the original file)
|
-- to the one already in keybinds.lua, kept here too for parity with the original file)
|
||||||
hl.bind("SUPER + Print", hl.dsp.exec_cmd("hyprshot -o " .. os.getenv("HOME") .. "/Pictures/Screenshots-Linux -m region"))
|
hl.bind("SUPER + Print", hl.dsp.exec_cmd([[sh -c 'if command -v hyprshot >/dev/null; then hyprshot -o "$HOME/Pictures/Screenshots-Linux" -m region; else grim -g "$(slurp)" "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"; fi']])) -- drag an area (hyprshot if installed, else grim/slurp)
|
||||||
|
hl.bind("SUPER + SHIFT + S", hl.dsp.exec_cmd([[sh -c 'grim -g "$(slurp)" "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"']])) -- region; Surface keyboard has no Print key
|
||||||
|
hl.bind("SUPER + F6", hl.dsp.exec_cmd([[sh -c 'grim "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"']])) -- full screen
|
||||||
hl.bind("SUPER + up", hl.dsp.focus({ direction = "u" }))
|
hl.bind("SUPER + up", hl.dsp.focus({ direction = "u" }))
|
||||||
hl.bind("SUPER + SHIFT + Print", hl.dsp.exec_cmd("hyprshot -o " .. os.getenv("HOME") .. "/Pictures/Screenshots-Linux -m window"))
|
hl.bind("SUPER + SHIFT + Print", hl.dsp.exec_cmd([[sh -c 'if command -v hyprshot >/dev/null; then hyprshot -o "$HOME/Pictures/Screenshots-Linux" -m window; else grim "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"; fi']])) -- window (hyprshot) or full screen (fallback)
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ hl.on("hyprland.start", function()
|
|||||||
-- Startup
|
-- Startup
|
||||||
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
|
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
|
||||||
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
|
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
|
||||||
|
-- restart portals once the env is imported, otherwise screen sharing breaks
|
||||||
|
hl.exec_cmd("sleep 1 && systemctl --user restart xdg-desktop-portal-hyprland && systemctl --user restart xdg-desktop-portal")
|
||||||
|
|
||||||
|
-- Sunshine (virtual monitor only on desktop; skipped on laptops, detected by a system battery)
|
||||||
|
hl.exec_cmd("sleep 1 && ! ls -d /sys/class/power_supply/BAT* >/dev/null 2>&1 && hyprctl output create headless")
|
||||||
|
|
||||||
-- Polkit (Polkit Gnome / KDE)
|
-- Polkit (Polkit Gnome / KDE)
|
||||||
hl.exec_cmd(scriptsDir .. "/Polkit.sh")
|
hl.exec_cmd(scriptsDir .. "/Polkit.sh")
|
||||||
@@ -23,7 +28,7 @@ hl.on("hyprland.start", function()
|
|||||||
-- hl.exec_cmd("ags")
|
-- hl.exec_cmd("ags")
|
||||||
-- hl.exec_cmd("blueman-applet")
|
-- hl.exec_cmd("blueman-applet")
|
||||||
-- hl.exec_cmd("rog-control-center")
|
-- hl.exec_cmd("rog-control-center")
|
||||||
hl.exec_cmd("waybar")
|
-- hl.exec_cmd("waybar")
|
||||||
hl.exec_cmd("qs") -- quickshell AGS Desktop Overview alternative
|
hl.exec_cmd("qs") -- quickshell AGS Desktop Overview alternative
|
||||||
|
|
||||||
-- clipboard manager
|
-- clipboard manager
|
||||||
@@ -31,8 +36,12 @@ hl.on("hyprland.start", function()
|
|||||||
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
||||||
|
|
||||||
-- Starting hypridle to start hyprlock
|
-- Starting hypridle to start hyprlock
|
||||||
|
-- Sunshine streams hold an idle inhibitor (scripts/SunshineIdle.sh) so this never locks mid-stream
|
||||||
hl.exec_cmd("hypridle")
|
hl.exec_cmd("hypridle")
|
||||||
|
|
||||||
|
-- Sunshine for streaming media
|
||||||
|
hl.exec_cmd("sunshine")
|
||||||
|
|
||||||
-- Float + center the Bitwarden extension pop-out (window rules can't catch it — see the script)
|
-- Float + center the Bitwarden extension pop-out (window rules can't catch it — see the script)
|
||||||
hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh")
|
hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh")
|
||||||
|
|
||||||
@@ -43,6 +52,7 @@ hl.on("hyprland.start", function()
|
|||||||
hl.exec_cmd("sleep 3 && vesktop")
|
hl.exec_cmd("sleep 3 && vesktop")
|
||||||
hl.dispatch(hl.dsp.focus({ workspace = 1 }))
|
hl.dispatch(hl.dsp.focus({ workspace = 1 }))
|
||||||
hl.exec_cmd("bitwarden-desktop --ozone-platform-hint=auto --silent")
|
hl.exec_cmd("bitwarden-desktop --ozone-platform-hint=auto --silent")
|
||||||
|
hl.exec_cmd("seafile-applet")
|
||||||
|
|
||||||
-- Here are a list of features available but disabled by default
|
-- Here are a list of features available but disabled by default
|
||||||
-- hl.exec_cmd("awww-daemon --format xrgb && awww img " .. os.getenv("HOME") .. "/Pictures/wallpapers/mecha-nostalgia.png") -- persistent wallpaper
|
-- hl.exec_cmd("awww-daemon --format xrgb && awww img " .. os.getenv("HOME") .. "/Pictures/wallpapers/mecha-nostalgia.png") -- persistent wallpaper
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
general:allow_tearing = 1
|
general:allow_tearing = 1
|
||||||
general:snap:enabled = 0
|
general:snap:enabled = 0
|
||||||
misc:vrr = 1
|
# vrr forced off globally: the per-monitor vrr,0 flag below does NOT override this when
|
||||||
|
# misc:vrr is 1 (tested live on Hyprland 0.56.2 — DP-2 stayed vrr:true until this global
|
||||||
|
# was changed). DP-2 (Acer VG240Y S) flickers under VRR when idle in fullscreen (mouse
|
||||||
|
# stationary -> no repaint damage -> frame delivery stalls near the panel's VRR floor).
|
||||||
|
# The other two monitors don't negotiate VRR at all, so this has no effect on them.
|
||||||
|
misc:vrr = 0
|
||||||
|
|
||||||
# Monitors
|
# Monitors
|
||||||
monitor = DP-2, 1920x1080@165.00Hz, 0x0, 1, bitdepth, 10, cm, wide
|
monitor = DP-2, 1920x1080@165.00Hz, 0x0, 1, bitdepth, 10, cm, wide, vrr, 0
|
||||||
|
|
||||||
# Keybinds
|
# Keybinds
|
||||||
unbind = SUPER SHIFT, Print
|
unbind = SUPER SHIFT, Print
|
||||||
|
|||||||
@@ -116,23 +116,22 @@ hl.bind(mainMod .. " + SHIFT + tab", hl.dsp.focus({ workspace = "m-1" }))
|
|||||||
hl.bind(mainMod .. " + U", hl.dsp.workspace.toggle_special("nyx"))
|
hl.bind(mainMod .. " + U", hl.dsp.workspace.toggle_special("nyx"))
|
||||||
hl.bind(mainMod .. " + SHIFT + U", hl.dsp.window.move({ workspace = "special:nyx" }))
|
hl.bind(mainMod .. " + SHIFT + U", hl.dsp.window.move({ workspace = "special:nyx" }))
|
||||||
|
|
||||||
-- The following mappings use key codes to better support various keyboard layouts
|
-- Number-row keys (the Lua bind parser does not accept the code:NN keycode syntax)
|
||||||
-- 1 is code:10, 2 is code:11, etc
|
|
||||||
-- Switch workspaces with mainMod + [0-9]
|
-- Switch workspaces with mainMod + [0-9]
|
||||||
for i, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
|
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
|
||||||
hl.bind(mainMod .. " + code:" .. code, hl.dsp.focus({ workspace = i })) -- code:10 = key 1 ... code:19 = key 0
|
hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Move active window and follow to workspace mainMod + SHIFT + [0-9]
|
-- Move active window and follow to workspace mainMod + SHIFT + [0-9]
|
||||||
for i, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
|
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
|
||||||
hl.bind(mainMod .. " + SHIFT + code:" .. code, hl.dsp.window.move({ workspace = i, follow = true }))
|
hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i, follow = true }))
|
||||||
end
|
end
|
||||||
hl.bind(mainMod .. " + SHIFT + bracketleft", hl.dsp.window.move({ workspace = -1, follow = true })) -- brackets [
|
hl.bind(mainMod .. " + SHIFT + bracketleft", hl.dsp.window.move({ workspace = -1, follow = true })) -- brackets [
|
||||||
hl.bind(mainMod .. " + SHIFT + bracketright", hl.dsp.window.move({ workspace = 1, follow = true })) -- brackets ]
|
hl.bind(mainMod .. " + SHIFT + bracketright", hl.dsp.window.move({ workspace = 1, follow = true })) -- brackets ]
|
||||||
|
|
||||||
-- Move active window to a workspace silently mainMod + CTRL + [0-9]
|
-- Move active window to a workspace silently mainMod + CTRL + [0-9]
|
||||||
for i, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
|
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
|
||||||
hl.bind(mainMod .. " + CTRL + code:" .. code, hl.dsp.window.move({ workspace = i, follow = false }))
|
hl.bind(mainMod .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = i, follow = false }))
|
||||||
end
|
end
|
||||||
hl.bind(mainMod .. " + CTRL + bracketleft", hl.dsp.window.move({ workspace = -1, follow = false })) -- brackets [
|
hl.bind(mainMod .. " + CTRL + bracketleft", hl.dsp.window.move({ workspace = -1, follow = false })) -- brackets [
|
||||||
hl.bind(mainMod .. " + CTRL + bracketright", hl.dsp.window.move({ workspace = 1, follow = false })) -- brackets ]
|
hl.bind(mainMod .. " + CTRL + bracketright", hl.dsp.window.move({ workspace = 1, follow = false })) -- brackets ]
|
||||||
|
|||||||
@@ -10,5 +10,6 @@
|
|||||||
-- <<<<< TUI AUTO START
|
-- <<<<< TUI AUTO START
|
||||||
hl.monitor({ output = "desc:LG Electronics LG ULTRAWIDE 0x0002E68A", mode = "2560x1080@60.00000", position = "1920x-740", scale = 1.00000000, transform = 3, vrr = 0 })
|
hl.monitor({ output = "desc:LG Electronics LG ULTRAWIDE 0x0002E68A", mode = "2560x1080@60.00000", position = "1920x-740", scale = 1.00000000, transform = 3, vrr = 0 })
|
||||||
hl.monitor({ output = "desc:HP Inc. HP 524pf VNG447017D", mode = "1920x1080@100.00000", position = "-1920x0", scale = 1.00000000, transform = 0, vrr = 0 })
|
hl.monitor({ output = "desc:HP Inc. HP 524pf VNG447017D", mode = "1920x1080@100.00000", position = "-1920x0", scale = 1.00000000, transform = 0, vrr = 0 })
|
||||||
hl.monitor({ output = "desc:Acer Technologies VG240Y S 0x126191DE", mode = "1920x1080@165.00301", position = "0x0", scale = 1.00000000, transform = 0, vrr = 1, bitdepth = 10, cm = "wide" })
|
hl.monitor({ output = "desc:Acer Technologies VG240Y S 0x126191DE", mode = "1920x1080@165.00301", position = "0x0", scale = 1.00000000, transform = 0, vrr = 0, bitdepth = 10, cm = "wide" })
|
||||||
|
hl.monitor({ output = "HEADLESS-1", mode = "1728x1080@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })
|
||||||
-- <<<<< TUI AUTO END
|
-- <<<<< TUI AUTO END
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ hl.window_rule({ workspace = "3", match = { class = "^([Vv]esktop|[Dd]iscord|[Ss
|
|||||||
hl.window_rule({ size = "20% 20%", match = { class = "^([Pp]lexamp)" } })
|
hl.window_rule({ size = "20% 20%", match = { class = "^([Pp]lexamp)" } })
|
||||||
|
|
||||||
-- FLOAT
|
-- FLOAT
|
||||||
hl.window_rule({ name = "windowrule-75", float = true, opacity = "0.9 0.7", size = "monitor_w*0.7 monitor_h*0.7", match = { tag = "wallpaper*" } })
|
hl.window_rule({ name = "windowrule-75", float = true, opacity = "0.9 0.9", size = "monitor_w*0.7 monitor_h*0.7", match = { tag = "wallpaper*" } })
|
||||||
hl.window_rule({ name = "windowrule-76", float = true, opacity = "0.8 0.7", size = "monitor_w*0.7 monitor_h*0.7", match = { tag = "settings*" } })
|
hl.window_rule({ name = "windowrule-76", float = true, opacity = "0.8 0.8", size = "monitor_w*0.7 monitor_h*0.7", match = { tag = "settings*" } })
|
||||||
hl.window_rule({ name = "windowrule-77", float = true, match = { tag = "viewer*" } })
|
hl.window_rule({ name = "windowrule-77", float = true, match = { tag = "viewer*" } })
|
||||||
hl.window_rule({ name = "windowrule-78", float = true, match = { class = "([Zz]oom|onedriver|onedriver-launcher)$" } })
|
hl.window_rule({ name = "windowrule-78", float = true, match = { class = "([Zz]oom|onedriver|onedriver-launcher)$" } })
|
||||||
hl.window_rule({ name = "windowrule-79", float = true, match = { class = "(org.gnome.Calculator)", title = "(Calculator)" } })
|
hl.window_rule({ name = "windowrule-79", float = true, match = { class = "(org.gnome.Calculator)", title = "(Calculator)" } })
|
||||||
@@ -162,17 +162,17 @@ hl.window_rule({ name = "windowrule-92", float = true, center = true, size = "mo
|
|||||||
-- END of float popups and dialogue #######
|
-- END of float popups and dialogue #######
|
||||||
|
|
||||||
-- OPACITY
|
-- OPACITY
|
||||||
-- hl.window_rule({ opacity = "0.9 0.7", match = { tag = "browser*" } })
|
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "browser*" } })
|
||||||
-- hl.window_rule({ opacity = "0.9 0.8", match = { tag = "projects*" } })
|
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "projects*" } })
|
||||||
-- hl.window_rule({ opacity = "0.94 0.86", match = { tag = "im*" } })
|
-- hl.window_rule({ opacity = "0.94 0.94", match = { tag = "im*" } })
|
||||||
-- hl.window_rule({ opacity = "0.94 0.86", match = { tag = "multimedia*" } })
|
-- hl.window_rule({ opacity = "0.94 0.94", match = { tag = "multimedia*" } })
|
||||||
-- hl.window_rule({ opacity = "0.9 0.8", match = { tag = "file-manager*" } })
|
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "file-manager*" } })
|
||||||
hl.window_rule({ name = "windowrule-93", opacity = "0.8 0.7", match = { tag = "terminal*" } })
|
hl.window_rule({ name = "windowrule-93", opacity = "0.8 0.8", match = { tag = "terminal*" } })
|
||||||
-- hl.window_rule({ opacity = "0.82 0.75", match = { tag = "viewer*" } })
|
-- hl.window_rule({ opacity = "0.82 0.82", match = { tag = "viewer*" } })
|
||||||
hl.window_rule({ name = "windowrule-94", opacity = "0.8 0.7", match = { class = "^(gedit|org.gnome.TextEditor|mousepad)$" } })
|
hl.window_rule({ name = "windowrule-94", opacity = "0.8 0.8", match = { class = "^(gedit|org.gnome.TextEditor|mousepad)$" } })
|
||||||
hl.window_rule({ name = "windowrule-95", opacity = "0.9 0.8", match = { class = "^(deluge)$" } })
|
hl.window_rule({ name = "windowrule-95", opacity = "0.9 0.9", match = { class = "^(deluge)$" } })
|
||||||
hl.window_rule({ name = "windowrule-96", opacity = "0.9 0.8", match = { class = "^(seahorse)$" } }) -- gnome-keyring gui
|
hl.window_rule({ name = "windowrule-96", opacity = "0.9 0.9", match = { class = "^(seahorse)$" } }) -- gnome-keyring gui
|
||||||
-- hl.window_rule({ opacity = "0.95 0.75", match = { title = "^(Picture-in-Picture)$" } })
|
-- hl.window_rule({ opacity = "0.95 0.95", match = { title = "^(Picture-in-Picture)$" } })
|
||||||
|
|
||||||
-- SIZE
|
-- SIZE
|
||||||
-- hl.window_rule({ size = "25% 25%", match = { title = "^(Firefox)$" } })
|
-- hl.window_rule({ size = "25% 25%", match = { title = "^(Firefox)$" } })
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
hl.workspace_rule({ workspace = 1, monitor = "DP-2", default = true })
|
hl.workspace_rule({ workspace = 1, monitor = "DP-2", default = true })
|
||||||
hl.workspace_rule({ workspace = 2, monitor = "DP-1", default = true })
|
hl.workspace_rule({ workspace = 2, monitor = "DP-1", default = true })
|
||||||
hl.workspace_rule({ workspace = 3, monitor = "HDMI-A-1", default = true })
|
hl.workspace_rule({ workspace = 3, monitor = "HDMI-A-1", default = true })
|
||||||
|
hl.workspace_rule({ workspace = 9, monitor = "HEADLESS-1", default = true })
|
||||||
-- hl.workspace_rule({ workspace = 4, monitor = "eDP-1" })
|
-- hl.workspace_rule({ workspace = 4, monitor = "eDP-1" })
|
||||||
-- hl.workspace_rule({ workspace = 5, monitor = "DP-2" })
|
-- hl.workspace_rule({ workspace = 5, monitor = "DP-2" })
|
||||||
-- hl.workspace_rule({ workspace = 6, monitor = "DP-2" })
|
-- hl.workspace_rule({ workspace = 6, monitor = "DP-2" })
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
music_icon="$HOME/.config/swaync/icons/music.png"
|
music_icon="$HOME/.config/swaync/icons/music.png"
|
||||||
|
|
||||||
|
# Use the app's own icon for known players
|
||||||
|
case "$(playerctl metadata --format '{{playerName}}' 2>/dev/null)" in
|
||||||
|
Plexamp) music_icon="com.plexamp.Plexamp" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Play the next track
|
# Play the next track
|
||||||
play_next() {
|
play_next() {
|
||||||
playerctl next
|
playerctl next
|
||||||
|
|||||||
29
dotfiles/.config/hypr/scripts/SunshineIdle.sh
Executable file
29
dotfiles/.config/hypr/scripts/SunshineIdle.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Keeps hypridle from locking the screen while Sunshine is streaming.
|
||||||
|
# Hooked up as Sunshine's global_prep_cmd, so it runs for every app:
|
||||||
|
# start -> hold a systemd idle inhibitor (hypridle respects these) and
|
||||||
|
# unlock hyprlock if the session was already locked
|
||||||
|
# stop -> release the inhibitor so normal idle locking resumes
|
||||||
|
|
||||||
|
pidfile="${XDG_RUNTIME_DIR:-/tmp}/sunshine-idle-inhibit.pid"
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
if ! { [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; }; then
|
||||||
|
setsid systemd-inhibit --what=idle --who=Sunshine --why="Streaming" \
|
||||||
|
sleep infinity >/dev/null 2>&1 &
|
||||||
|
echo $! > "$pidfile"
|
||||||
|
fi
|
||||||
|
# SIGUSR1 tells hyprlock to unlock
|
||||||
|
pkill -USR1 -x hyprlock
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
[ -f "$pidfile" ] && kill "$(cat "$pidfile")" 2>/dev/null
|
||||||
|
rm -f "$pidfile"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 start|stop" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
@@ -1,143 +1,37 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# HyprFlux — https://github.com/ahmad9059/HyprFlux
|
# Volume controls for audio and mic via wpctl (PipeWire).
|
||||||
# Scripts for volume controls for audio and mic
|
# Quickshell's VolumeOsd watches PipeWire directly, so no notifications here.
|
||||||
|
|
||||||
iDIR="$HOME/.config/swaync/icons"
|
SINK="@DEFAULT_AUDIO_SINK@"
|
||||||
sDIR="$HOME/.config/hypr/scripts"
|
SOURCE="@DEFAULT_AUDIO_SOURCE@"
|
||||||
|
STEP="5%"
|
||||||
|
|
||||||
# Get Volume
|
# Print volume as an integer percent, or "Muted"
|
||||||
get_volume() {
|
get_volume() {
|
||||||
volume=$(pamixer --get-volume)
|
local out
|
||||||
if [[ "$volume" -eq "0" ]]; then
|
out=$(wpctl get-volume "$1")
|
||||||
|
if [[ "$out" == *MUTED* ]]; then
|
||||||
echo "Muted"
|
echo "Muted"
|
||||||
else
|
else
|
||||||
echo "$volume %"
|
awk '{ printf "%d %%\n", $2 * 100 + 0.5 }' <<< "$out"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get icons
|
is_muted() {
|
||||||
get_icon() {
|
[[ "$(wpctl get-volume "$1")" == *MUTED* ]]
|
||||||
current=$(get_volume)
|
|
||||||
if [[ "$current" == "Muted" ]]; then
|
|
||||||
echo "$iDIR/volume-mute.png"
|
|
||||||
elif [[ "${current%\%}" -le 30 ]]; then
|
|
||||||
echo "$iDIR/volume-low.png"
|
|
||||||
elif [[ "${current%\%}" -le 60 ]]; then
|
|
||||||
echo "$iDIR/volume-mid.png"
|
|
||||||
else
|
|
||||||
echo "$iDIR/volume-high.png"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Notify
|
# Raising/lowering while muted unmutes first, matching the old behaviour
|
||||||
notify_user() {
|
inc() { is_muted "$1" && wpctl set-mute "$1" 0 || wpctl set-volume -l 1.0 "$1" "$STEP+"; }
|
||||||
if [[ "$(get_volume)" == "Muted" ]]; then
|
dec() { is_muted "$1" && wpctl set-mute "$1" 0 || wpctl set-volume "$1" "$STEP-"; }
|
||||||
notify-send -e -h string:x-canonical-private-synchronous:volume_notif -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$(get_icon)" " Volume:" " Muted"
|
|
||||||
else
|
|
||||||
notify-send -e -h int:value:"$(get_volume | sed 's/%//')" -h string:x-canonical-private-synchronous:volume_notif -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$(get_icon)" " Volume Level:" " $(get_volume)" &&
|
|
||||||
"$sDIR/Sounds.sh" --volume
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Increase Volume
|
case "$1" in
|
||||||
inc_volume() {
|
--inc) inc "$SINK" ;;
|
||||||
if [ "$(pamixer --get-mute)" == "true" ]; then
|
--dec) dec "$SINK" ;;
|
||||||
toggle_mute
|
--toggle) wpctl set-mute "$SINK" toggle ;;
|
||||||
else
|
--toggle-mic) wpctl set-mute "$SOURCE" toggle ;;
|
||||||
pamixer -i 5 --allow-boost --set-limit 150 && notify_user
|
--mic-inc) inc "$SOURCE" ;;
|
||||||
fi
|
--mic-dec) dec "$SOURCE" ;;
|
||||||
}
|
--get-mic) get_volume "$SOURCE" ;;
|
||||||
|
*) get_volume "$SINK" ;;
|
||||||
# Decrease Volume
|
esac
|
||||||
dec_volume() {
|
|
||||||
if [ "$(pamixer --get-mute)" == "true" ]; then
|
|
||||||
toggle_mute
|
|
||||||
else
|
|
||||||
pamixer -d 5 && notify_user
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Toggle Mute
|
|
||||||
toggle_mute() {
|
|
||||||
if [ "$(pamixer --get-mute)" == "false" ]; then
|
|
||||||
pamixer -m && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/volume-mute.png" " Mute"
|
|
||||||
elif [ "$(pamixer --get-mute)" == "true" ]; then
|
|
||||||
pamixer -u && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$(get_icon)" " Volume:" " Switched ON"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Toggle Mic
|
|
||||||
toggle_mic() {
|
|
||||||
if [ "$(pamixer --default-source --get-mute)" == "false" ]; then
|
|
||||||
pamixer --default-source -m && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/microphone-mute.png" " Microphone:" " Switched OFF"
|
|
||||||
elif [ "$(pamixer --default-source --get-mute)" == "true" ]; then
|
|
||||||
pamixer -u --default-source u && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/microphone.png" " Microphone:" " Switched ON"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
# Get Mic Icon
|
|
||||||
get_mic_icon() {
|
|
||||||
current=$(pamixer --default-source --get-volume)
|
|
||||||
if [[ "$current" -eq "0" ]]; then
|
|
||||||
echo "$iDIR/microphone-mute.png"
|
|
||||||
else
|
|
||||||
echo "$iDIR/microphone.png"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get Microphone Volume
|
|
||||||
get_mic_volume() {
|
|
||||||
volume=$(pamixer --default-source --get-volume)
|
|
||||||
if [[ "$volume" -eq "0" ]]; then
|
|
||||||
echo "Muted"
|
|
||||||
else
|
|
||||||
echo "$volume %"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Notify for Microphone
|
|
||||||
notify_mic_user() {
|
|
||||||
volume=$(get_mic_volume)
|
|
||||||
icon=$(get_mic_icon)
|
|
||||||
notify-send -e -h int:value:"$volume" -h "string:x-canonical-private-synchronous:volume_notif" -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$icon" " Mic Level:" " $volume"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Increase MIC Volume
|
|
||||||
inc_mic_volume() {
|
|
||||||
if [ "$(pamixer --default-source --get-mute)" == "true" ]; then
|
|
||||||
toggle_mic
|
|
||||||
else
|
|
||||||
pamixer --default-source -i 5 && notify_mic_user
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Decrease MIC Volume
|
|
||||||
dec_mic_volume() {
|
|
||||||
if [ "$(pamixer --default-source --get-mute)" == "true" ]; then
|
|
||||||
toggle-mic
|
|
||||||
else
|
|
||||||
pamixer --default-source -d 5 && notify_mic_user
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Execute accordingly
|
|
||||||
if [[ "$1" == "--get" ]]; then
|
|
||||||
get_volume
|
|
||||||
elif [[ "$1" == "--inc" ]]; then
|
|
||||||
inc_volume
|
|
||||||
elif [[ "$1" == "--dec" ]]; then
|
|
||||||
dec_volume
|
|
||||||
elif [[ "$1" == "--toggle" ]]; then
|
|
||||||
toggle_mute
|
|
||||||
elif [[ "$1" == "--toggle-mic" ]]; then
|
|
||||||
toggle_mic
|
|
||||||
elif [[ "$1" == "--get-icon" ]]; then
|
|
||||||
get_icon
|
|
||||||
elif [[ "$1" == "--get-mic-icon" ]]; then
|
|
||||||
get_mic_icon
|
|
||||||
elif [[ "$1" == "--mic-inc" ]]; then
|
|
||||||
inc_mic_volume
|
|
||||||
elif [[ "$1" == "--mic-dec" ]]; then
|
|
||||||
dec_mic_volume
|
|
||||||
else
|
|
||||||
get_volume
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
accent_primary = "#007acc"
|
|
||||||
accent_secondary = "#06b6d4"
|
|
||||||
background = "#1e1e1e"
|
|
||||||
foreground = "#d4d4d8"
|
|
||||||
opacity = 0.91
|
|
||||||
BIN
dotfiles/.config/quickshell/assets/bitwarden.png
Normal file
BIN
dotfiles/.config/quickshell/assets/bitwarden.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 294 KiB |
27
dotfiles/.config/quickshell/assets/sunshine.svg
Normal file
27
dotfiles/.config/quickshell/assets/sunshine.svg
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Sunshine_Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||||
|
y="0px" width="256px" height="256px" viewBox="10 6 236 245" style="enable-background:new 0 0 256 256;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FDD107;}
|
||||||
|
.st1{fill:#F89A1C;}
|
||||||
|
.st2{fill:#EF3E23;}
|
||||||
|
.st3{fill:#F26222;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M118.7688675,20.7120476c0,0-63.8333359,26-74.3333359,83.8333282s37.1666718,91.5,86.3333282,75.3333282
|
||||||
|
s70.3333435-51,81.8333435-86.9999924c0,0-9.3333435,100.4999924-96.1666718,115.4999924s-118.1666641-50-82.1666641-119.8333282
|
||||||
|
C44.2688675,67.0453796,80.5188675,29.6287155,118.7688675,20.7120476z"/>
|
||||||
|
<path class="st1" d="M118.7688675,20.7120476c0,0-41.125,3.6666679-83.25,61.0416679s-28.125,139.125,34.25,149.375
|
||||||
|
s115.8749924-44.875,133.5-82.375s15.1666718-61.4583282,9.75-77.8749924c0,0,0.6666718,36.4166641-13.3333282,59.6666641
|
||||||
|
s-29.75,46.3333282-65.0833282,62.1666718s-74.1666718,13.75-95.4166718-19.25s-5.9166641-76.0833359-0.2916641-85.3333359
|
||||||
|
S72.3938675,33.7953796,118.7688675,20.7120476z"/>
|
||||||
|
<path class="st2" d="M73.0188675,39.6287155c0,0,38.125-28.125,76.8749924-28.125s63,28.25,68.5,52.25s6,54.125-11.5,87.6249924
|
||||||
|
s-37.375,56-79.1249924,76.125s-84.625,2.75-84.625,2.75s25.9769745,25.8750153,71.0509872,16.5
|
||||||
|
c45.0740051-9.375,82.2406769-40.875,98.4073486-69.5s28.7916565-57.3749924,27.6666565-92.2499924s-23.75-54.5-31.25-60.25
|
||||||
|
s-23.1875-17.8125-58.1875-16.5625S86.4563675,29.8162155,73.0188675,39.6287155z"/>
|
||||||
|
<path class="st3" d="M73.0188675,39.6287155c0,0,35-32.8125,82.4374924-32.8125s69.1875,24.8125,78.875,44.6875
|
||||||
|
s21.8125,70-12.1875,122.9999924s-74.625,67.375-93.625,71.625s-42.4311447,4.269165-59.1114044-1.2299957
|
||||||
|
c0,0,35.1947479,8.3966675,66.7780762-7.4366608s51.6666718-32.1666718,74.0833282-68.8333435
|
||||||
|
s25.9166718-72.7499924,22.1666718-93.9166565s-12.1666718-42.4166718-36.5-56.3333359s-56.7291718-10.531251-74.4791641-4.531251
|
||||||
|
S91.9876175,26.4099655,73.0188675,39.6287155z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
72
dotfiles/.config/quickshell/config/SystemUpdates.qml
Normal file
72
dotfiles/.config/quickshell/config/SystemUpdates.qml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
// Shared state for SystemUpdate.qml, the bar's package-update indicator.
|
||||||
|
// Bar.qml is instantiated once per screen (see shell.qml's Variants), so
|
||||||
|
// without this being a singleton, plugging in extra monitors would mean
|
||||||
|
// running `checkupdates` that many times in parallel on every poll — which
|
||||||
|
// isn't just wasteful, it's actively unreliable: concurrent invocations
|
||||||
|
// race over checkupdates' own sync directory and can intermittently report
|
||||||
|
// 0 pending updates. One shared Process here means exactly one `checkupdates`
|
||||||
|
// run at a time, and every bar's icon reflects the same result.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// checkupdates syncs a throwaway copy of the pacman db, so it's not
|
||||||
|
// free to run often — matches the old waybar custom/updater module's
|
||||||
|
// ~21.7 minute poll interval (see .config/waybar/config).
|
||||||
|
readonly property int refreshIntervalMs: 1300 * 1000
|
||||||
|
|
||||||
|
property int updateCount: 0
|
||||||
|
property bool checking: false
|
||||||
|
property bool updating: false
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (_checkProc.running) return
|
||||||
|
root.checking = true
|
||||||
|
_checkProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// scriptPath: absolute path to update-system.sh (resolved by the caller
|
||||||
|
// via Quickshell.shellPath, which isn't available as a plain import
|
||||||
|
// here without pulling in the whole Quickshell singleton for one call).
|
||||||
|
function runUpdate(scriptPath) {
|
||||||
|
if (_updateProc.running) return
|
||||||
|
root.updating = true
|
||||||
|
_updateProc.command = [scriptPath]
|
||||||
|
_updateProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// QtObject has no default property, so these are attached via property
|
||||||
|
// assignment rather than as plain child elements (same as Colors.qml's
|
||||||
|
// FileView).
|
||||||
|
readonly property Process _checkProc: Process {
|
||||||
|
command: ["sh", "-c", "checkupdates 2>/dev/null | wc -l"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: checkCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
root.updateCount = parseInt(checkCollector.text.trim(), 10) || 0
|
||||||
|
root.checking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs update-system.sh, which opens its own terminal and blocks until
|
||||||
|
// that terminal is closed — so this only exits once the user's actually
|
||||||
|
// done updating, which is exactly when a re-check matters.
|
||||||
|
readonly property Process _updateProc: Process {
|
||||||
|
onExited: {
|
||||||
|
root.updating = false
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Timer _timer: Timer {
|
||||||
|
interval: root.refreshIntervalMs
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
242
dotfiles/.config/quickshell/config/WeatherData.qml
Normal file
242
dotfiles/.config/quickshell/config/WeatherData.qml
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
// Live weather for the bar's Weather.qml icon. A singleton for the same
|
||||||
|
// reason as SystemUpdates.qml: Bar.qml is instantiated once per monitor
|
||||||
|
// (shell.qml's Variants), so without this being shared, every extra screen
|
||||||
|
// would mean another set of network requests running in parallel.
|
||||||
|
//
|
||||||
|
// Location is auto-detected from the public IP (via ipwho.is, no API key)
|
||||||
|
// and cached to disk (~/.cache/quickshell/weather-location.json) so it's
|
||||||
|
// only ever looked up once, not on every quickshell restart — IP-geolocation
|
||||||
|
// free tiers rate-limit fast, and a desktop's location barely ever changes
|
||||||
|
// anyway. Only the weather itself re-polls on a timer. Conditions come from
|
||||||
|
// Open-Meteo (api.open-meteo.com) — also free and keyless, and it does its
|
||||||
|
// own °C/km-h conversion server-side via query params.
|
||||||
|
//
|
||||||
|
// Loosely modeled on omarchy's weather panel
|
||||||
|
// (github.com/omacom/omarchy, quattro branch,
|
||||||
|
// shell/plugins/panels/weather/Model.js) for the WMO weather-code
|
||||||
|
// grouping, but the icons are this bar's own Material Symbols glyphs
|
||||||
|
// rather than pulling in a separate icon font just for this.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Location barely ever changes for a desktop machine, so it's fetched
|
||||||
|
// once (and cached); the weather itself is worth re-checking
|
||||||
|
// periodically.
|
||||||
|
readonly property int refreshIntervalMs: 15 * 60 * 1000
|
||||||
|
|
||||||
|
property bool locating: true
|
||||||
|
property bool loading: false
|
||||||
|
property bool hasLocation: false
|
||||||
|
property string locationName: ""
|
||||||
|
|
||||||
|
property real latitude: 0
|
||||||
|
property real longitude: 0
|
||||||
|
|
||||||
|
function saveLocation() {
|
||||||
|
_locationCache.setText(JSON.stringify({
|
||||||
|
latitude: root.latitude,
|
||||||
|
longitude: root.longitude,
|
||||||
|
name: root.locationName
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from Weather.qml's search box once you pick a result — always
|
||||||
|
// wins over whatever IP-based guess was cached before, and persists so
|
||||||
|
// it sticks across restarts.
|
||||||
|
function selectLocation(name, lat, lon) {
|
||||||
|
root.latitude = lat
|
||||||
|
root.longitude = lon
|
||||||
|
root.locationName = name
|
||||||
|
root.hasLocation = true
|
||||||
|
root.locating = false
|
||||||
|
root.saveLocation()
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool searching: false
|
||||||
|
property var searchResults: []
|
||||||
|
|
||||||
|
function search(query) {
|
||||||
|
const q = String(query || "").trim()
|
||||||
|
if (q.length < 2) {
|
||||||
|
root.searchResults = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.searching = true
|
||||||
|
_searchProc.command = ["curl", "-s", "--max-time", "6",
|
||||||
|
"https://geocoding-api.open-meteo.com/v1/search?count=5&language=en&format=json&name=" + encodeURIComponent(q)]
|
||||||
|
_searchProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
property real tempC: 0
|
||||||
|
property real feelsLikeC: 0
|
||||||
|
property int humidity: 0
|
||||||
|
property real windKmh: 0
|
||||||
|
property int weatherCode: 0
|
||||||
|
property bool isDay: true
|
||||||
|
|
||||||
|
// [{date, code, maxC, minC}] — tomorrow onward; today is already
|
||||||
|
// covered by the current-conditions fields above.
|
||||||
|
property var forecast: []
|
||||||
|
|
||||||
|
function locate() {
|
||||||
|
if (_locateProc.running) return
|
||||||
|
root.locating = true
|
||||||
|
_locateProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!root.hasLocation || _weatherProc.running) return
|
||||||
|
root.loading = true
|
||||||
|
_weatherProc.command = ["curl", "-s", "--max-time", "8",
|
||||||
|
"https://api.open-meteo.com/v1/forecast?latitude=" + root.latitude
|
||||||
|
+ "&longitude=" + root.longitude
|
||||||
|
+ "¤t=temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,is_day,wind_speed_10m"
|
||||||
|
+ "&daily=weather_code,temperature_2m_max,temperature_2m_min"
|
||||||
|
+ "&temperature_unit=celsius&wind_speed_unit=kmh&timezone=auto&forecast_days=4"]
|
||||||
|
_weatherProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material Symbols Rounded glyph for a WMO weather code (see
|
||||||
|
// https://open-meteo.com/en/docs#weathervariables), grouped the same
|
||||||
|
// way omarchy's Model.js groups them into its own icon set.
|
||||||
|
function iconFor(code, day) {
|
||||||
|
if (code === 0) return day ? "sunny" : "clear_night"
|
||||||
|
if (code === 1 || code === 2) return day ? "partly_cloudy_day" : "partly_cloudy_night"
|
||||||
|
if (code === 3) return "cloud"
|
||||||
|
if (code === 45 || code === 48) return "foggy"
|
||||||
|
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].indexOf(code) !== -1) return "rainy"
|
||||||
|
if ([71, 73, 75, 77, 85, 86].indexOf(code) !== -1) return "weather_snowy"
|
||||||
|
if (code === 95 || code === 96 || code === 99) return "thunderstorm"
|
||||||
|
return "cloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
// QtObject has no default property, so these are attached via property
|
||||||
|
// assignment rather than as plain child elements (same as Colors.qml's
|
||||||
|
// FileView and SystemUpdates.qml's Process/Timer).
|
||||||
|
|
||||||
|
// Read once at startup; onLoaded/onLoadFailed (below) decide whether a
|
||||||
|
// network lookup is still needed.
|
||||||
|
readonly property FileView _locationCache: FileView {
|
||||||
|
path: Quickshell.env("HOME") + "/.cache/quickshell/weather-location.json"
|
||||||
|
onLoaded: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text())
|
||||||
|
const lat = parseFloat(data.latitude)
|
||||||
|
const lon = parseFloat(data.longitude)
|
||||||
|
if (!isNaN(lat) && !isNaN(lon)) {
|
||||||
|
root.latitude = lat
|
||||||
|
root.longitude = lon
|
||||||
|
root.locationName = data.name || ""
|
||||||
|
root.hasLocation = true
|
||||||
|
root.locating = false
|
||||||
|
root.refresh()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// fall through to a fresh lookup below
|
||||||
|
}
|
||||||
|
root.locate()
|
||||||
|
}
|
||||||
|
onLoadFailed: root.locate()
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Process _locateProc: Process {
|
||||||
|
command: ["curl", "-s", "--max-time", "6", "https://ipwho.is/"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: locateCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(locateCollector.text)
|
||||||
|
const lat = parseFloat(data.latitude)
|
||||||
|
const lon = parseFloat(data.longitude)
|
||||||
|
root.hasLocation = data.success !== false && !isNaN(lat) && !isNaN(lon)
|
||||||
|
if (root.hasLocation) {
|
||||||
|
root.latitude = lat
|
||||||
|
root.longitude = lon
|
||||||
|
root.locationName = data.city || data.region || data.country || ""
|
||||||
|
root.saveLocation()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Weather: locate parse failed:", e)
|
||||||
|
root.hasLocation = false
|
||||||
|
}
|
||||||
|
root.locating = false
|
||||||
|
if (root.hasLocation) root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Process _weatherProc: Process {
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: weatherCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(weatherCollector.text)
|
||||||
|
const current = data.current || {}
|
||||||
|
root.tempC = current.temperature_2m
|
||||||
|
root.feelsLikeC = current.apparent_temperature
|
||||||
|
root.humidity = current.relative_humidity_2m
|
||||||
|
root.windKmh = current.wind_speed_10m
|
||||||
|
root.weatherCode = current.weather_code
|
||||||
|
root.isDay = current.is_day === 1
|
||||||
|
|
||||||
|
const daily = data.daily || {}
|
||||||
|
const times = daily.time || []
|
||||||
|
const days = []
|
||||||
|
// Start at 1 — index 0 is today, already shown as the
|
||||||
|
// current conditions above; this is "what's coming".
|
||||||
|
for (let i = 1; i < times.length && days.length < 3; i++) {
|
||||||
|
days.push({
|
||||||
|
date: times[i],
|
||||||
|
code: (daily.weather_code || [])[i],
|
||||||
|
maxC: (daily.temperature_2m_max || [])[i],
|
||||||
|
minC: (daily.temperature_2m_min || [])[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
root.forecast = days
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Weather: failed to parse Open-Meteo response:", e)
|
||||||
|
}
|
||||||
|
root.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Process _searchProc: Process {
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: searchCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(searchCollector.text)
|
||||||
|
const results = data.results || []
|
||||||
|
root.searchResults = results.filter(function(r) {
|
||||||
|
return r && r.name && r.latitude !== undefined && r.longitude !== undefined
|
||||||
|
}).map(function(r) {
|
||||||
|
return {
|
||||||
|
name: r.name,
|
||||||
|
description: [r.admin1, r.country].filter(function(p) { return !!p }).join(", "),
|
||||||
|
latitude: r.latitude,
|
||||||
|
longitude: r.longitude
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
root.searchResults = []
|
||||||
|
}
|
||||||
|
root.searching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Timer _timer: Timer {
|
||||||
|
interval: root.refreshIntervalMs
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,3 +2,5 @@ module config
|
|||||||
singleton Colors 1.0 Colors.qml
|
singleton Colors 1.0 Colors.qml
|
||||||
singleton Appearance 1.0 Appearance.qml
|
singleton Appearance 1.0 Appearance.qml
|
||||||
singleton VpnExclusivity 1.0 VpnExclusivity.qml
|
singleton VpnExclusivity 1.0 VpnExclusivity.qml
|
||||||
|
singleton SystemUpdates 1.0 SystemUpdates.qml
|
||||||
|
singleton WeatherData 1.0 WeatherData.qml
|
||||||
|
|||||||
@@ -35,8 +35,12 @@ PanelWindow {
|
|||||||
ActiveWindow {}
|
ActiveWindow {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Clock {
|
RowLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
spacing: Appearance.spacing
|
||||||
|
Weather { barWindow: bar }
|
||||||
|
Clock {}
|
||||||
|
SystemUpdate { barWindow: bar }
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -46,7 +50,7 @@ PanelWindow {
|
|||||||
rightMargin: Appearance.spacing
|
rightMargin: Appearance.spacing
|
||||||
}
|
}
|
||||||
spacing: Appearance.spacing
|
spacing: Appearance.spacing
|
||||||
//Tray {}
|
PinnedTray {}
|
||||||
TailscaleIndicator { barWindow: bar }
|
TailscaleIndicator { barWindow: bar }
|
||||||
VpnIndicator { barWindow: bar }
|
VpnIndicator { barWindow: bar }
|
||||||
NetworkIndicator { barWindow: bar }
|
NetworkIndicator { barWindow: bar }
|
||||||
|
|||||||
@@ -222,6 +222,82 @@ Item {
|
|||||||
root.passwordSsid = ""
|
root.passwordSsid = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Captive portal ---------------------------------------------------
|
||||||
|
// NetworkManager tells "portal" (a walled-garden sign-in page sits
|
||||||
|
// between us and the real internet) apart from "full" connectivity only
|
||||||
|
// via an active probe — nmcli's cached CONNECTIVITY field just reflects
|
||||||
|
// NM's own internal check, which runs every few minutes and is far too
|
||||||
|
// slow to catch a portal right after joining a network. So this forces a
|
||||||
|
// fresh probe itself, same shell-out approach as the stats below, and
|
||||||
|
// runs it whenever we're connected — not gated to panel.visible like
|
||||||
|
// those — since the whole point is noticing this before the popup is
|
||||||
|
// ever opened.
|
||||||
|
//
|
||||||
|
// "limited" is treated the same as "portal", not just "full" vs.
|
||||||
|
// everything-else: NM only reports "portal" when the probe request
|
||||||
|
// actually comes back *different* (redirected/rewritten) from what it
|
||||||
|
// expected. Plenty of real captive portals instead black-hole every
|
||||||
|
// request that isn't to their own login domain, so the probe just times
|
||||||
|
// out — which is also why this is slow to resolve — and NM reports
|
||||||
|
// "limited" (it got a route/gateway, but the internet check itself
|
||||||
|
// never landed) rather than "portal". From the user's side both look
|
||||||
|
// identical: connected, but no real internet.
|
||||||
|
property string connectivityState: "unknown"
|
||||||
|
readonly property bool connectivityRestricted: root.connectivityState === "portal" || root.connectivityState === "limited"
|
||||||
|
|
||||||
|
// A plain, unencrypted HTTP page whose only job is to be interceptable
|
||||||
|
// by a captive portal's redirect. An https:// URL mostly just gets
|
||||||
|
// silently blocked instead of redirected, which looks identical to the
|
||||||
|
// portal not existing at all.
|
||||||
|
readonly property string captivePortalCheckUrl: "http://neverssl.com/"
|
||||||
|
|
||||||
|
// Auto-opened once per network join (mirrors TailscaleIndicator's
|
||||||
|
// openAuthUrl/_authUrlOpened) so flapping connectivity on the same
|
||||||
|
// network doesn't keep popping a new browser tab — the manual "Open"
|
||||||
|
// button in the panel banner below still works regardless.
|
||||||
|
property bool _portalAutoOpened: false
|
||||||
|
property bool _wasConnected: false
|
||||||
|
onConnectedDeviceChanged: {
|
||||||
|
const isConnected = root.connectedDevice !== null
|
||||||
|
if (isConnected && !root._wasConnected) {
|
||||||
|
root._portalAutoOpened = false
|
||||||
|
root.connectivityState = "unknown"
|
||||||
|
}
|
||||||
|
root._wasConnected = isConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectivityRestrictedChanged: {
|
||||||
|
if (root.connectivityRestricted && !root._portalAutoOpened)
|
||||||
|
root.openCaptivePortal()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCaptivePortal() {
|
||||||
|
root._portalAutoOpened = true
|
||||||
|
Qt.openUrlExternally(root.captivePortalCheckUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: connectivityProc
|
||||||
|
command: ["nmcli", "networking", "connectivity", "check"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: connectivityCollector
|
||||||
|
onStreamFinished: root.connectivityState = connectivityCollector.text.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: connectivityTimer
|
||||||
|
// Poll quickly while things are still unresolved (fresh join, or an
|
||||||
|
// active portal we're waiting to clear) and back off once
|
||||||
|
// connectivity is confirmed full, rather than shelling out every
|
||||||
|
// few seconds forever for no reason.
|
||||||
|
interval: root.connectivityState === "full" ? 20000 : 3000
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
running: root.connectedDevice !== null
|
||||||
|
onTriggered: if (!connectivityProc.running) connectivityProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
// --- Live stats --------------------------------------------------
|
// --- Live stats --------------------------------------------------
|
||||||
// None of this comes from Quickshell's Networking API — it only knows
|
// None of this comes from Quickshell's Networking API — it only knows
|
||||||
// about devices/networks, not throughput, ping, or routing. So this
|
// about devices/networks, not throughput, ping, or routing. So this
|
||||||
@@ -595,6 +671,34 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Small warning badge over the icon's corner when the connection looks
|
||||||
|
// restricted (a captive portal, confirmed or just suspected — see
|
||||||
|
// connectivityRestricted above) — same treatment as TailscaleIndicator's
|
||||||
|
// login-needed badge, so "this needs your attention" reads the same way
|
||||||
|
// across indicators.
|
||||||
|
Rectangle {
|
||||||
|
visible: root.connectivityRestricted
|
||||||
|
width: Math.max(7, label.font.pixelSize * 0.42)
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: Colors.urgent
|
||||||
|
border.color: Colors.surface
|
||||||
|
border.width: 1
|
||||||
|
anchors.right: label.right
|
||||||
|
anchors.bottom: label.bottom
|
||||||
|
anchors.rightMargin: -1
|
||||||
|
anchors.bottomMargin: -1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "!"
|
||||||
|
color: Colors.surface
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Math.max(6, parent.height * 0.75)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
@@ -890,6 +994,88 @@ Item {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Captive portal banner — shown when the connectivity probe
|
||||||
|
// above finds either a confirmed sign-in page or a
|
||||||
|
// "limited" result standing between this network and the
|
||||||
|
// real internet (see connectivityRestricted). openCaptivePortal()
|
||||||
|
// already fired once automatically when this was first
|
||||||
|
// detected; this stays up as a manual retry in case that tab
|
||||||
|
// got closed.
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.connectivityRestricted
|
||||||
|
radius: 0
|
||||||
|
color: Qt.rgba(Colors.urgent.r, Colors.urgent.g, Colors.urgent.b, 0.18)
|
||||||
|
border.color: Colors.urgent
|
||||||
|
border.width: 1
|
||||||
|
implicitHeight: portalBannerRow.implicitHeight + 12
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: portalBannerRow
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "lock_open"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: Colors.urgent
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.connectivityState === "portal" ? "Sign-in required" : "No internet access"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSize
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.connectivityState === "portal"
|
||||||
|
? "This network needs a web sign-in first"
|
||||||
|
: "This network may need a web sign-in first"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitWidth: openPortalText.implicitWidth + 16
|
||||||
|
implicitHeight: 24
|
||||||
|
radius: 0
|
||||||
|
color: openPortalMouse.containsMouse ? Colors.urgent : "transparent"
|
||||||
|
border.color: Colors.urgent
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: openPortalText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Open"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: openPortalMouse.containsMouse ? Colors.primaryText : Colors.urgent
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
id: openPortalMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.openCaptivePortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Live stats — ping/loss/throughput/IP/gateway for whichever
|
// Live stats — ping/loss/throughput/IP/gateway for whichever
|
||||||
// device is currently connected.
|
// device is currently connected.
|
||||||
GridLayout {
|
GridLayout {
|
||||||
|
|||||||
110
dotfiles/.config/quickshell/modules/bar/PinnedTray.qml
Normal file
110
dotfiles/.config/quickshell/modules/bar/PinnedTray.qml
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import "../../config"
|
||||||
|
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: root
|
||||||
|
spacing: Appearance.spacing * 1.
|
||||||
|
|
||||||
|
|
||||||
|
readonly property var pinned: [
|
||||||
|
{
|
||||||
|
name: "Bitwarden",
|
||||||
|
icon: Qt.resolvedUrl("../../assets/bitwarden.png"),
|
||||||
|
binary: "bitwarden",
|
||||||
|
launch: ["bitwarden"],
|
||||||
|
secondary: ["pkill", "bitwarden"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Seafile",
|
||||||
|
icon: "seafile",
|
||||||
|
binary: "seafile-applet",
|
||||||
|
launch: ["seafile-applet"],
|
||||||
|
secondary: ["pkill", "seafile-applet"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sunshine",
|
||||||
|
icon: Qt.resolvedUrl("../../assets/sunshine.svg"),
|
||||||
|
binary: "sunshine",
|
||||||
|
launch: ["systemctl", "--user", "start", "app-dev.lizardbyte.app.Sunshine.service"],
|
||||||
|
secondary: ["xdg-open", "https://xol.twhomelab.tailnet:47990"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function findTrayItem(name) {
|
||||||
|
const needle = name.toLowerCase()
|
||||||
|
for (const item of SystemTray.items.values) {
|
||||||
|
if (item.id.toLowerCase().includes(needle) || item.title.toLowerCase().includes(needle))
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.pinned
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: slot
|
||||||
|
required property var modelData
|
||||||
|
readonly property var trayItem: root.findTrayItem(modelData.name)
|
||||||
|
readonly property bool running: trayItem !== null
|
||||||
|
property bool installed: false // Application installation check
|
||||||
|
|
||||||
|
visible: slot.installed
|
||||||
|
implicitWidth: Appearance.iconSize
|
||||||
|
implicitHeight: Appearance.iconSize
|
||||||
|
|
||||||
|
Process {
|
||||||
|
running: true
|
||||||
|
command: ["which", slot.modelData.binary]
|
||||||
|
onExited: (exitCode) => slot.installed = exitCode === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
// modelData.icon is a QUrl when it comes from
|
||||||
|
// Qt.resolvedUrl() (Bitwarden/Sunshine's local assets), not
|
||||||
|
// a QString — coerce to string before using String methods
|
||||||
|
// on it, or this throws instead of ever matching.
|
||||||
|
source: slot.running
|
||||||
|
? slot.trayItem.icon
|
||||||
|
: (String(slot.modelData.icon).includes("://")
|
||||||
|
? slot.modelData.icon
|
||||||
|
: Quickshell.iconPath(slot.modelData.icon))
|
||||||
|
opacity: slot.running ? 1.0 : 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
Process { id: launcher }
|
||||||
|
Process { id: secondaryProc }
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -4
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
if (slot.modelData.secondary) {
|
||||||
|
if (!secondaryProc.running) {
|
||||||
|
secondaryProc.command = slot.modelData.secondary
|
||||||
|
secondaryProc.running = true
|
||||||
|
}
|
||||||
|
} else if (slot.running) {
|
||||||
|
slot.trayItem.secondaryActivate()
|
||||||
|
}
|
||||||
|
} else if (slot.running) {
|
||||||
|
slot.trayItem.activate()
|
||||||
|
} else if (!launcher.running) {
|
||||||
|
launcher.command = slot.modelData.launch
|
||||||
|
launcher.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
194
dotfiles/.config/quickshell/modules/bar/SystemUpdate.qml
Normal file
194
dotfiles/.config/quickshell/modules/bar/SystemUpdate.qml
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import "../../config"
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
implicitWidth: row.implicitWidth + 6
|
||||||
|
implicitHeight: Appearance.barHeight
|
||||||
|
|
||||||
|
|
||||||
|
required property var barWindow
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: row
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: label
|
||||||
|
font.family: SystemUpdates.checking ? Appearance.iconFontFamilyRounded : Appearance.fontFamily
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: SystemUpdates.checking ? 15 : 22
|
||||||
|
color: Colors.foreground
|
||||||
|
text: SystemUpdates.checking ? "progress_activity" : ""
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: SystemUpdates.checking
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 900
|
||||||
|
onRunningChanged: if (!running) label.rotation = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: label.color
|
||||||
|
text: SystemUpdates.updateCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
SystemUpdates.refresh()
|
||||||
|
} else {
|
||||||
|
panel.visible = !panel.visible
|
||||||
|
grab.active = panel.visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: panel
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
readonly property int contentWidth: 200
|
||||||
|
readonly property int contentPadding: 16
|
||||||
|
implicitWidth: contentWidth + contentPadding * 2
|
||||||
|
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
||||||
|
|
||||||
|
anchor {
|
||||||
|
window: root.barWindow
|
||||||
|
rect.x: 0
|
||||||
|
rect.y: 0
|
||||||
|
rect.width: root.barWindow.width
|
||||||
|
rect.height: root.barWindow.height
|
||||||
|
edges: Edges.Bottom
|
||||||
|
gravity: Edges.Bottom
|
||||||
|
margins.bottom: -4
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: grab
|
||||||
|
windows: [panel]
|
||||||
|
onCleared: panel.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 0
|
||||||
|
color: Colors.surface
|
||||||
|
border.color: Colors.muted
|
||||||
|
border.width: 3
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: panel.contentPadding
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: label.text
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: 23
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "System Update"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSize
|
||||||
|
color: Colors.foreground
|
||||||
|
text: {
|
||||||
|
if (SystemUpdates.updating) return "Update running in a terminal…"
|
||||||
|
if (SystemUpdates.checking) return "Checking for updates…"
|
||||||
|
if (SystemUpdates.updateCount === 0) return "System is up to date."
|
||||||
|
return SystemUpdates.updateCount + (SystemUpdates.updateCount === 1 ? " update available." : " updates available.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: updateBtn
|
||||||
|
width: parent.width
|
||||||
|
radius: 0
|
||||||
|
implicitHeight: btnContent.implicitHeight + 12
|
||||||
|
enabled: !SystemUpdates.updating
|
||||||
|
opacity: SystemUpdates.updating ? 0.5 : 1
|
||||||
|
color: btnMouse.containsMouse ? Colors.primary : "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -3
|
||||||
|
radius: 0
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Colors.foreground
|
||||||
|
border.width: btnMouse.containsMouse ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: btnContent
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: 6
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.preferredWidth: 20
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
text: "download"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: btnMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "Update Now"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSize
|
||||||
|
color: btnMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: btnMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: !SystemUpdates.updating
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
SystemUpdates.runUpdate(Quickshell.shellPath("scripts/update-system.sh"))
|
||||||
|
panel.visible = false
|
||||||
|
grab.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import Quickshell.Widgets
|
|||||||
import "../../config"
|
import "../../config"
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: Appearance.spacing * 0.6
|
spacing: Appearance.spacing
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: SystemTray.items
|
model: SystemTray.items
|
||||||
|
|||||||
@@ -137,15 +137,25 @@ Item {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
// Scroll distance is accumulated rather than stepping per event:
|
||||||
|
// a mouse wheel sends one 120-unit event per notch, but a touchpad
|
||||||
|
// sends a stream of tiny events, which at a fixed step per event
|
||||||
|
// made touchpad scrolling far too fast. One notch's worth (120) of
|
||||||
|
// scrolling = one volume step, regardless of input device.
|
||||||
|
property real wheelAccum: 0
|
||||||
|
readonly property real volumeStep: 0.05
|
||||||
|
readonly property real unitsPerStep: 120
|
||||||
onClicked: {
|
onClicked: {
|
||||||
panel.visible = !panel.visible
|
panel.visible = !panel.visible
|
||||||
grab.active = panel.visible
|
grab.active = panel.visible
|
||||||
}
|
}
|
||||||
onWheel: (wheel) => {
|
onWheel: (wheel) => {
|
||||||
if (!root.sink || !root.sink.audio) return
|
if (!root.sink || !root.sink.audio) return
|
||||||
const step = 0.05
|
wheelAccum += wheel.angleDelta.y
|
||||||
const delta = wheel.angleDelta.y > 0 ? step : -step
|
const steps = Math.trunc(wheelAccum / unitsPerStep)
|
||||||
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + delta))
|
if (steps === 0) return
|
||||||
|
wheelAccum -= steps * unitsPerStep
|
||||||
|
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + steps * volumeStep))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,41 @@ Item {
|
|||||||
// to connect once the current one has finished disconnecting.
|
// to connect once the current one has finished disconnecting.
|
||||||
property string pendingConnect: ""
|
property string pendingConnect: ""
|
||||||
|
|
||||||
// Surfaced under the header for a failed connect/disconnect; cleared
|
// Surfaced under each provider's own row for a failed connect/
|
||||||
// automatically after a few seconds.
|
// disconnect/settings change; cleared automatically after a few
|
||||||
property string actionStatus: ""
|
// seconds. Kept separate per provider so an error from one doesn't
|
||||||
|
// show up hanging under the other's row.
|
||||||
|
property string mullvadActionStatus: ""
|
||||||
|
property string protonActionStatus: ""
|
||||||
|
|
||||||
|
// --- Mullvad advanced options (auto-connect, relay location, multihop) --
|
||||||
|
// ProtonVPN's CLI is plain-text-only and far less predictable across
|
||||||
|
// versions, so this extra panel is Mullvad-specific rather than a
|
||||||
|
// second ProviderRow feature.
|
||||||
|
|
||||||
|
property bool mullvadOptionsOpen: false
|
||||||
|
property bool mullvadAutoConnect: false
|
||||||
|
property bool mullvadMultihop: false
|
||||||
|
// Human-readable current relay constraint, e.g. "Any" or "Sweden" —
|
||||||
|
// resolved from `relay get`'s "country xx[ city yyy]" syntax against
|
||||||
|
// mullvadCountries once that list has loaded.
|
||||||
|
property string mullvadLocationLabel: "Any"
|
||||||
|
property string mullvadEntryLabel: "Any"
|
||||||
|
// [{name, code}], parsed from `mullvad relay list` and cached for the
|
||||||
|
// life of the panel — the full relay list rarely changes and re-parsing
|
||||||
|
// ~50 countries on every open would just be wasted work.
|
||||||
|
property var mullvadCountries: []
|
||||||
|
// "" | "exit" | "entry" — which location field's picker is open, if any.
|
||||||
|
property string mullvadPickerTarget: ""
|
||||||
|
property string mullvadPickerFilter: ""
|
||||||
|
|
||||||
|
readonly property var mullvadFilteredCountries: {
|
||||||
|
const f = root.mullvadPickerFilter.trim().toLowerCase()
|
||||||
|
if (!f) return root.mullvadCountries
|
||||||
|
return root.mullvadCountries.filter(function(c) {
|
||||||
|
return c.name.toLowerCase().indexOf(f) !== -1 || c.code.indexOf(f) !== -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function shortLine(text) {
|
function shortLine(text) {
|
||||||
const line = String(text || "").trim().split("\n")[0] || ""
|
const line = String(text || "").trim().split("\n")[0] || ""
|
||||||
@@ -82,6 +114,95 @@ Item {
|
|||||||
VpnExclusivity.protonConnected = root.protonState === "connected"
|
VpnExclusivity.protonConnected = root.protonState === "connected"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyMullvadAutoConnect(text) {
|
||||||
|
const m = String(text || "").match(/autoconnect:\s*(on|off)/i)
|
||||||
|
root.mullvadAutoConnect = !!m && m[1].toLowerCase() === "on"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves relay get's "any" / "country xx" / "country xx city yyy" /
|
||||||
|
// bare-hostname syntax into a display label. Falls back to the raw
|
||||||
|
// country code (or the raw string) when mullvadCountries hasn't loaded
|
||||||
|
// yet, rather than showing nothing.
|
||||||
|
function describeMullvadLocation(raw) {
|
||||||
|
const s = String(raw || "").trim()
|
||||||
|
if (!s || s === "any") return "Any"
|
||||||
|
const m = s.match(/^country\s+([a-z]{2})\b/i)
|
||||||
|
if (!m) return s
|
||||||
|
const code = m[1].toLowerCase()
|
||||||
|
const country = root.mullvadCountries.find(function(c) { return c.code === code })
|
||||||
|
return country ? country.name : code.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMullvadRelay(text) {
|
||||||
|
const body = String(text || "")
|
||||||
|
const locMatch = body.match(/^\s*Location:\s*(.+)$/mi)
|
||||||
|
const multihopMatch = body.match(/^\s*Multihop state:\s*(\w+)/mi)
|
||||||
|
const entryMatch = body.match(/^\s*Multihop entry:\s*(.+)$/mi)
|
||||||
|
root.mullvadMultihop = !!multihopMatch && multihopMatch[1].toLowerCase() === "enabled"
|
||||||
|
root.mullvadLocationLabel = root.describeMullvadLocation(locMatch ? locMatch[1].trim() : "")
|
||||||
|
root.mullvadEntryLabel = root.describeMullvadLocation(entryMatch ? entryMatch[1].trim() : "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only country-level lines are of interest ("Sweden (se)", flush left) —
|
||||||
|
// city ("\tGothenburg (got) @ ...") and server lines are indented and
|
||||||
|
// their parenthesised code is never exactly two letters, so the regex
|
||||||
|
// alone is enough to tell them apart without checking indentation.
|
||||||
|
function applyMullvadCountries(text) {
|
||||||
|
const lines = String(text || "").split("\n")
|
||||||
|
const countries = []
|
||||||
|
for (const line of lines) {
|
||||||
|
const m = line.match(/^(\S.*) \(([a-z]{2})\)\s*$/)
|
||||||
|
if (m) countries.push({ name: m[1].trim(), code: m[2] })
|
||||||
|
}
|
||||||
|
countries.sort(function(a, b) { return a.name.localeCompare(b.name) })
|
||||||
|
root.mullvadCountries = countries
|
||||||
|
// Re-resolve any labels already showing raw codes now that names
|
||||||
|
// are available.
|
||||||
|
if (!mullvadRelayGetProc.running) mullvadRelayGetProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMullvadOptions() {
|
||||||
|
root.mullvadOptionsOpen = !root.mullvadOptionsOpen
|
||||||
|
if (root.mullvadOptionsOpen) {
|
||||||
|
if (!mullvadAutoConnectGetProc.running) mullvadAutoConnectGetProc.running = true
|
||||||
|
if (!mullvadRelayGetProc.running) mullvadRelayGetProc.running = true
|
||||||
|
if (root.mullvadCountries.length === 0 && !mullvadRelayListProc.running) mullvadRelayListProc.running = true
|
||||||
|
} else {
|
||||||
|
root.mullvadPickerTarget = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMullvadPicker(target) {
|
||||||
|
root.mullvadPickerFilter = ""
|
||||||
|
root.mullvadPickerTarget = root.mullvadPickerTarget === target ? "" : target
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickMullvadLocation(code) {
|
||||||
|
const target = root.mullvadPickerTarget
|
||||||
|
root.mullvadPickerTarget = ""
|
||||||
|
root.setMullvadLocation(target, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMullvadAutoConnect(on) {
|
||||||
|
if (mullvadAutoConnectSetProc.running) return
|
||||||
|
mullvadAutoConnectSetProc.command = ["mullvad", "auto-connect", "set", on ? "on" : "off"]
|
||||||
|
mullvadAutoConnectSetProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMullvadMultihop(on) {
|
||||||
|
if (mullvadRelaySetProc.running) return
|
||||||
|
mullvadRelaySetProc.command = ["mullvad", "relay", "set", "multihop", on ? "on" : "off"]
|
||||||
|
mullvadRelaySetProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMullvadLocation(target, code) {
|
||||||
|
if (mullvadRelaySetProc.running) return
|
||||||
|
mullvadRelaySetProc.command = target === "entry"
|
||||||
|
? ["mullvad", "relay", "set", "entry", "location", code]
|
||||||
|
: ["mullvad", "relay", "set", "location", code]
|
||||||
|
mullvadRelaySetProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
function refreshMullvad() {
|
function refreshMullvad() {
|
||||||
if (!root.mullvadInstalled) {
|
if (!root.mullvadInstalled) {
|
||||||
if (!mullvadWhichProc.running) mullvadWhichProc.running = true
|
if (!mullvadWhichProc.running) mullvadWhichProc.running = true
|
||||||
@@ -197,8 +318,8 @@ Item {
|
|||||||
stderr: StdioCollector { id: mullvadConnectErr }
|
stderr: StdioCollector { id: mullvadConnectErr }
|
||||||
onExited: (exitCode, exitStatus) => {
|
onExited: (exitCode, exitStatus) => {
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
root.actionStatus = "Mullvad: " + root.shortLine(mullvadConnectErr.text || "connect failed")
|
root.mullvadActionStatus = root.shortLine(mullvadConnectErr.text || "Connect failed")
|
||||||
actionStatusTimer.restart()
|
mullvadActionStatusTimer.restart()
|
||||||
} else if (!tailscaleDownProc.running) {
|
} else if (!tailscaleDownProc.running) {
|
||||||
tailscaleDownProc.running = true
|
tailscaleDownProc.running = true
|
||||||
}
|
}
|
||||||
@@ -213,8 +334,8 @@ Item {
|
|||||||
stderr: StdioCollector { id: mullvadDisconnectErr }
|
stderr: StdioCollector { id: mullvadDisconnectErr }
|
||||||
onExited: (exitCode, exitStatus) => {
|
onExited: (exitCode, exitStatus) => {
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
root.actionStatus = "Mullvad: " + root.shortLine(mullvadDisconnectErr.text || "disconnect failed")
|
root.mullvadActionStatus = root.shortLine(mullvadDisconnectErr.text || "Disconnect failed")
|
||||||
actionStatusTimer.restart()
|
mullvadActionStatusTimer.restart()
|
||||||
}
|
}
|
||||||
root.refreshMullvad()
|
root.refreshMullvad()
|
||||||
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
|
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
|
||||||
@@ -229,6 +350,77 @@ Item {
|
|||||||
onTriggered: if (mullvadStatusProc.running) mullvadStatusProc.running = false
|
onTriggered: if (mullvadStatusProc.running) mullvadStatusProc.running = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Mullvad advanced options: auto-connect, relay location, multihop --
|
||||||
|
// Only queried while the options section is open (see
|
||||||
|
// root.openMullvadOptions), not on the regular status poll — these
|
||||||
|
// don't change on their own the way tunnel state does.
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: mullvadAutoConnectGetProc
|
||||||
|
command: ["mullvad", "auto-connect", "get"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: mullvadAutoConnectGetCollector
|
||||||
|
onStreamFinished: root.applyMullvadAutoConnect(mullvadAutoConnectGetCollector.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: mullvadAutoConnectSetProc
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector { id: mullvadAutoConnectSetErr }
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root.mullvadActionStatus = root.shortLine(mullvadAutoConnectSetErr.text || "Auto-connect change failed")
|
||||||
|
mullvadActionStatusTimer.restart()
|
||||||
|
}
|
||||||
|
if (!mullvadAutoConnectGetProc.running) mullvadAutoConnectGetProc.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: mullvadRelayGetProc
|
||||||
|
command: ["mullvad", "relay", "get"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: mullvadRelayGetCollector
|
||||||
|
onStreamFinished: root.applyMullvadRelay(mullvadRelayGetCollector.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: mullvadRelayListProc
|
||||||
|
command: ["mullvad", "relay", "list"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: mullvadRelayListCollector
|
||||||
|
onStreamFinished: root.applyMullvadCountries(mullvadRelayListCollector.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: mullvadRelaySetProc
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector { id: mullvadRelaySetErr }
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root.mullvadActionStatus = root.shortLine(mullvadRelaySetErr.text || "Relay change failed")
|
||||||
|
mullvadActionStatusTimer.restart()
|
||||||
|
}
|
||||||
|
if (!mullvadRelayGetProc.running) mullvadRelayGetProc.running = true
|
||||||
|
// Relay constraint changes don't take effect on an already-open
|
||||||
|
// tunnel by themselves — kick a reconnect so switching location/
|
||||||
|
// multihop while connected actually moves the tunnel, instead of
|
||||||
|
// silently only applying on the next manual reconnect.
|
||||||
|
if (root.mullvadState === "connected" && !mullvadReconnectProc.running) mullvadReconnectProc.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: mullvadReconnectProc
|
||||||
|
command: ["mullvad", "reconnect"]
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {}
|
||||||
|
onExited: (exitCode, exitStatus) => root.refreshMullvad()
|
||||||
|
}
|
||||||
|
|
||||||
// --- ProtonVPN ---------------------------------------------------------
|
// --- ProtonVPN ---------------------------------------------------------
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
@@ -261,8 +453,8 @@ Item {
|
|||||||
stderr: StdioCollector { id: protonConnectErr }
|
stderr: StdioCollector { id: protonConnectErr }
|
||||||
onExited: (exitCode, exitStatus) => {
|
onExited: (exitCode, exitStatus) => {
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
root.actionStatus = "ProtonVPN: " + root.shortLine(protonConnectErr.text || "connect failed")
|
root.protonActionStatus = root.shortLine(protonConnectErr.text || "Connect failed")
|
||||||
actionStatusTimer.restart()
|
protonActionStatusTimer.restart()
|
||||||
} else if (!tailscaleDownProc.running) {
|
} else if (!tailscaleDownProc.running) {
|
||||||
tailscaleDownProc.running = true
|
tailscaleDownProc.running = true
|
||||||
}
|
}
|
||||||
@@ -277,8 +469,8 @@ Item {
|
|||||||
stderr: StdioCollector { id: protonDisconnectErr }
|
stderr: StdioCollector { id: protonDisconnectErr }
|
||||||
onExited: (exitCode, exitStatus) => {
|
onExited: (exitCode, exitStatus) => {
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
root.actionStatus = "ProtonVPN: " + root.shortLine(protonDisconnectErr.text || "disconnect failed")
|
root.protonActionStatus = root.shortLine(protonDisconnectErr.text || "Disconnect failed")
|
||||||
actionStatusTimer.restart()
|
protonActionStatusTimer.restart()
|
||||||
}
|
}
|
||||||
root.refreshProton()
|
root.refreshProton()
|
||||||
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
|
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
|
||||||
@@ -305,10 +497,17 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: actionStatusTimer
|
id: mullvadActionStatusTimer
|
||||||
interval: 4000
|
interval: 4000
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: root.actionStatus = ""
|
onTriggered: root.mullvadActionStatus = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: protonActionStatusTimer
|
||||||
|
interval: 4000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: root.protonActionStatus = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bar icon ------------------------------------------------------------
|
// --- Bar icon ------------------------------------------------------------
|
||||||
@@ -416,7 +615,13 @@ Item {
|
|||||||
property string detailText: ""
|
property string detailText: ""
|
||||||
property string state: "disconnected"
|
property string state: "disconnected"
|
||||||
property bool installed: true
|
property bool installed: true
|
||||||
|
// When true, shows a chevron that expands a provider-specific
|
||||||
|
// options section below this row (used for Mullvad's advanced
|
||||||
|
// options; ProtonVPN's row leaves this false).
|
||||||
|
property bool expandable: false
|
||||||
|
property bool expanded: false
|
||||||
signal toggle()
|
signal toggle()
|
||||||
|
signal expandToggle()
|
||||||
width: parent ? parent.width : 0
|
width: parent ? parent.width : 0
|
||||||
radius: 0
|
radius: 0
|
||||||
implicitHeight: rowContent.implicitHeight + 12
|
implicitHeight: rowContent.implicitHeight + 12
|
||||||
@@ -477,8 +682,90 @@ Item {
|
|||||||
busy: provRow.state === "connecting" || provRow.state === "disconnecting"
|
busy: provRow.state === "connecting" || provRow.state === "disconnecting"
|
||||||
onToggled: provRow.toggle()
|
onToggled: provRow.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: provRow.expandable
|
||||||
|
text: provRow.expanded ? "expand_less" : "expand_more"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 15
|
||||||
|
color: expandMouse.containsMouse ? Colors.foreground : Colors.muted
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: expandMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -5
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: provRow.expandToggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single clickable "Location: Sweden ›" row inside the Mullvad
|
||||||
|
// options section — used for both the exit location and (when multihop
|
||||||
|
// is on) the entry location. Clicking routes back to root, which opens
|
||||||
|
// the shared country picker below rather than each field owning one.
|
||||||
|
component LocationField: Item {
|
||||||
|
id: locField
|
||||||
|
property string fieldLabel: ""
|
||||||
|
property string valueText: "Any"
|
||||||
|
property bool open: false
|
||||||
|
signal clicked()
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
implicitHeight: locRow.implicitHeight
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: locRow
|
||||||
|
width: parent.width
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: locField.fieldLabel
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: locField.valueText
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: locField.open ? "expand_less" : "chevron_right"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: locField.clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A failed connect/disconnect/settings-change message, shown directly
|
||||||
|
// under the provider row it came from rather than in one shared line
|
||||||
|
// for both providers — keeps an error attributed to whichever CLI it
|
||||||
|
// actually came from.
|
||||||
|
component StatusLine: Text {
|
||||||
|
property string message: ""
|
||||||
|
visible: message !== ""
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
text: message
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
// --- Panel -----------------------------------------------------------
|
// --- Panel -----------------------------------------------------------
|
||||||
|
|
||||||
@@ -494,7 +781,18 @@ Item {
|
|||||||
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
|
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: if (visible) root.refresh()
|
// Leaving mullvadOptionsOpen itself alone across opens (it's a
|
||||||
|
// deliberate "keep this expanded" choice), but the country picker
|
||||||
|
// is transient state scoped to one visit — closing the panel with
|
||||||
|
// it open would otherwise leave a stale filter/list waiting.
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
root.refresh()
|
||||||
|
} else {
|
||||||
|
root.mullvadPickerTarget = ""
|
||||||
|
root.mullvadPickerFilter = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchor {
|
anchor {
|
||||||
window: root.barWindow
|
window: root.barWindow
|
||||||
@@ -548,28 +846,202 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: root.actionStatus !== ""
|
|
||||||
width: parent.width
|
|
||||||
text: root.actionStatus
|
|
||||||
font.family: Appearance.fontFamily
|
|
||||||
font.pixelSize: Appearance.fontSizeSmall
|
|
||||||
color: Colors.muted
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
|
// Mullvad's connect row and its advanced options share a
|
||||||
|
// single bordered card — the border wraps the whole
|
||||||
|
// section (row, status line, and options together) and
|
||||||
|
// grows to include the options/picker as they open,
|
||||||
|
// rather than the options having a separate box of
|
||||||
|
// their own below an unbordered connect row.
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
radius: 0
|
||||||
|
color: Colors.background
|
||||||
|
border.color: Colors.muted
|
||||||
|
border.width: 1
|
||||||
|
implicitHeight: mullvadSectionColumn.implicitHeight + 16
|
||||||
|
clip: true
|
||||||
|
// Without this, the card snapped to its new size
|
||||||
|
// instantly while the popup around it was still
|
||||||
|
// animating open, so the border visibly lagged
|
||||||
|
// behind (or cut off) content like the country
|
||||||
|
// picker appearing/disappearing beneath it.
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mullvadSectionColumn
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
ProviderRow {
|
ProviderRow {
|
||||||
label: "Mullvad"
|
label: "Mullvad"
|
||||||
subIcon: "gpp_good"
|
subIcon: "gpp_good"
|
||||||
state: root.mullvadState
|
state: root.mullvadState
|
||||||
detailText: root.mullvadDetail
|
detailText: root.mullvadDetail
|
||||||
installed: root.mullvadInstalled
|
installed: root.mullvadInstalled
|
||||||
|
expandable: root.mullvadInstalled
|
||||||
|
expanded: root.mullvadOptionsOpen
|
||||||
onToggle: root.toggleMullvad()
|
onToggle: root.toggleMullvad()
|
||||||
|
onExpandToggle: root.openMullvadOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusLine { message: root.mullvadActionStatus }
|
||||||
|
|
||||||
|
// Advanced options — auto-connect, exit/entry
|
||||||
|
// relay location, multihop.
|
||||||
|
Column {
|
||||||
|
id: mullvadOptionsColumn
|
||||||
|
width: parent.width
|
||||||
|
visible: root.mullvadOptionsOpen && root.mullvadInstalled
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "Auto-connect"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
ToggleSwitch {
|
||||||
|
checked: root.mullvadAutoConnect
|
||||||
|
busy: mullvadAutoConnectSetProc.running
|
||||||
|
onToggled: root.setMullvadAutoConnect(!root.mullvadAutoConnect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "Multihop"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
ToggleSwitch {
|
||||||
|
checked: root.mullvadMultihop
|
||||||
|
busy: mullvadRelaySetProc.running
|
||||||
|
onToggled: root.setMullvadMultihop(!root.mullvadMultihop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationField {
|
||||||
|
fieldLabel: root.mullvadMultihop ? "Exit" : "Location"
|
||||||
|
valueText: root.mullvadLocationLabel
|
||||||
|
open: root.mullvadPickerTarget === "exit"
|
||||||
|
onClicked: root.openMullvadPicker("exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationField {
|
||||||
|
visible: root.mullvadMultihop
|
||||||
|
fieldLabel: "Entry"
|
||||||
|
valueText: root.mullvadEntryLabel
|
||||||
|
open: root.mullvadPickerTarget === "entry"
|
||||||
|
onClicked: root.openMullvadPicker("entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared country picker for whichever LocationField
|
||||||
|
// was last clicked — filter box plus a capped,
|
||||||
|
// scrollable list ("Any" always pinned first).
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.mullvadPickerTarget !== ""
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: 24
|
||||||
|
radius: 0
|
||||||
|
color: Colors.surface
|
||||||
|
border.color: Colors.muted
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
visible: mullvadFilterInput.text.length === 0
|
||||||
|
text: "Search country…"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: mullvadFilterInput
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
color: Colors.foreground
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
clip: true
|
||||||
|
focus: root.mullvadPickerTarget !== ""
|
||||||
|
text: root.mullvadPickerFilter
|
||||||
|
onTextChanged: root.mullvadPickerFilter = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: mullvadPickerFlick
|
||||||
|
width: parent.width
|
||||||
|
height: Math.min(mullvadPickerColumn.implicitHeight, 150)
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: mullvadPickerColumn.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mullvadPickerColumn
|
||||||
|
width: mullvadPickerFlick.width
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [{ name: "Any", code: "any" }].concat(root.mullvadFilteredCountries)
|
||||||
|
Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: 22
|
||||||
|
radius: 0
|
||||||
|
color: countryMouse.containsMouse ? Colors.primary : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: 4
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: modelData.name
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: countryMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: countryMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.pickMullvadLocation(modelData.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ProviderRow {
|
ProviderRow {
|
||||||
label: "ProtonVPN"
|
label: "ProtonVPN"
|
||||||
subIcon: "gpp_good"
|
subIcon: "gpp_good"
|
||||||
@@ -578,6 +1050,8 @@ Item {
|
|||||||
installed: root.protonInstalled
|
installed: root.protonInstalled
|
||||||
onToggle: root.toggleProton()
|
onToggle: root.toggleProton()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusLine { message: root.protonActionStatus }
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|||||||
413
dotfiles/.config/quickshell/modules/bar/Weather.qml
Normal file
413
dotfiles/.config/quickshell/modules/bar/Weather.qml
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import "../../config"
|
||||||
|
|
||||||
|
// Weather indicator — modeled on omarchy's weather bar pill
|
||||||
|
// (github.com/omacom/omarchy, quattro branch,
|
||||||
|
// shell/plugins/panels/weather) but rebuilt in this bar's own style: the
|
||||||
|
// actual location/polling logic lives in the WeatherData singleton (config/),
|
||||||
|
// shared across every monitor's bar; this is just the icon + panel UI,
|
||||||
|
// same split as SystemUpdate.qml.
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
implicitWidth: row.implicitWidth + 2
|
||||||
|
implicitHeight: Appearance.barHeight
|
||||||
|
|
||||||
|
// The bar's own PanelWindow — same barWindow-anchoring convention as
|
||||||
|
// every other popup-having module here (Volume.qml, SystemUpdate.qml,
|
||||||
|
// etc.), so the panel opens in a fixed spot rather than following this
|
||||||
|
// icon's exact position in its RowLayout.
|
||||||
|
required property var barWindow
|
||||||
|
|
||||||
|
readonly property bool busy: WeatherData.locating || WeatherData.loading
|
||||||
|
|
||||||
|
// Local to this popup instance — unlike WeatherData's own state, there's
|
||||||
|
// no need to share an in-progress search across monitors' bars.
|
||||||
|
property bool searchOpen: false
|
||||||
|
property string searchQuery: ""
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: row
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: label
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 18
|
||||||
|
color: Colors.foreground
|
||||||
|
// Same spinner treatment as SystemUpdate.qml/
|
||||||
|
// BluetoothIndicator.qml's scan spinner while the location or
|
||||||
|
// conditions are still loading.
|
||||||
|
text: root.busy ? "progress_activity" : (WeatherData.hasLocation ? WeatherData.iconFor(WeatherData.weatherCode, WeatherData.isDay) : "cloud_off")
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: root.busy
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 900
|
||||||
|
onRunningChanged: if (!running) label.rotation = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
text: Math.round(WeatherData.tempC) + "°"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
WeatherData.refresh()
|
||||||
|
} else {
|
||||||
|
panel.visible = !panel.visible
|
||||||
|
grab.active = panel.visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PopupWindow {
|
||||||
|
id: panel
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
// Same contentWidth/contentPadding split as the other panels
|
||||||
|
// (Volume.qml, SystemUpdate.qml, etc.) rather than a single magic
|
||||||
|
// implicitWidth.
|
||||||
|
readonly property int contentWidth: 220
|
||||||
|
readonly property int contentPadding: 16
|
||||||
|
implicitWidth: contentWidth + contentPadding * 2
|
||||||
|
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
||||||
|
|
||||||
|
anchor {
|
||||||
|
window: root.barWindow
|
||||||
|
// Unlike anchor.item, anchor.window doesn't default rect to the
|
||||||
|
// window's own bounds — set it explicitly so edges/gravity below
|
||||||
|
// resolve against the full bar. Bottom alone (no Left/Right)
|
||||||
|
// anchors to the bar's bottom-center, which lines up with this
|
||||||
|
// icon since it sits in the bar's centered RowLayout.
|
||||||
|
rect.x: 0
|
||||||
|
rect.y: 0
|
||||||
|
rect.width: root.barWindow.width
|
||||||
|
rect.height: root.barWindow.height
|
||||||
|
edges: Edges.Bottom
|
||||||
|
gravity: Edges.Bottom
|
||||||
|
margins.bottom: -4
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: grab
|
||||||
|
windows: [panel]
|
||||||
|
onCleared: panel.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 0
|
||||||
|
color: Colors.surface
|
||||||
|
border.color: Colors.muted
|
||||||
|
border.width: 3
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: panel.contentPadding
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: label.text
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 28
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: {
|
||||||
|
if (WeatherData.locating) return "Locating…"
|
||||||
|
if (!WeatherData.hasLocation) return "Location unavailable"
|
||||||
|
return WeatherData.locationName || "Weather"
|
||||||
|
}
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
||||||
|
text: WeatherData.loading ? "Updating…" : "Feels like " + Math.round(WeatherData.feelsLikeC) + "°"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
||||||
|
text: Math.round(WeatherData.tempC) + "°"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: 28
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles the search box below — same tap target size
|
||||||
|
// and hover feedback as the picker-target rows in
|
||||||
|
// VpnIndicator.qml.
|
||||||
|
Text {
|
||||||
|
text: root.searchOpen ? "close" : "edit_location"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: editLocationMouse.containsMouse ? Colors.primary : Colors.muted
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: editLocationMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -4
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
root.searchOpen = !root.searchOpen
|
||||||
|
root.searchQuery = ""
|
||||||
|
WeatherData.searchResults = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// City search — always available regardless of how the
|
||||||
|
// current location was resolved (IP guess or a previous
|
||||||
|
// search), so a wrong auto-detected city is one click away
|
||||||
|
// from being fixed. Same search-box + result-list shape as
|
||||||
|
// VpnIndicator.qml's Mullvad location picker.
|
||||||
|
Column {
|
||||||
|
visible: root.searchOpen
|
||||||
|
width: parent.width
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: 24
|
||||||
|
radius: 0
|
||||||
|
color: Colors.surface
|
||||||
|
border.color: Colors.muted
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
visible: citySearchInput.text.length === 0
|
||||||
|
text: "Search city…"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: citySearchInput
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
color: Colors.foreground
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
clip: true
|
||||||
|
focus: root.searchOpen
|
||||||
|
text: root.searchQuery
|
||||||
|
onTextChanged: {
|
||||||
|
root.searchQuery = text
|
||||||
|
searchDebounce.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: searchDebounce
|
||||||
|
interval: 400
|
||||||
|
onTriggered: WeatherData.search(root.searchQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: WeatherData.searching
|
||||||
|
text: "Searching…"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: !WeatherData.searching && citySearchInput.text.length >= 2 && WeatherData.searchResults.length === 0
|
||||||
|
text: "No matches"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.min(resultsColumn.implicitHeight, 150)
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: resultsColumn.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: resultsColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: WeatherData.searchResults
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: resultText.implicitHeight + 8
|
||||||
|
radius: 0
|
||||||
|
color: resultMouse.containsMouse ? Colors.primary : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: resultText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: 4
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: modelData.description ? (modelData.name + " — " + modelData.description) : modelData.name
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: resultMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: resultMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
WeatherData.selectLocation(modelData.name, modelData.latitude, modelData.longitude)
|
||||||
|
root.searchOpen = false
|
||||||
|
root.searchQuery = ""
|
||||||
|
WeatherData.searchResults = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Humidity / wind — same two-stat layout, just laid out as
|
||||||
|
// a row of icon+value pairs rather than full sentences.
|
||||||
|
RowLayout {
|
||||||
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
||||||
|
width: parent.width
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 4
|
||||||
|
Text {
|
||||||
|
text: "water_drop"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: WeatherData.humidity + "%"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 4
|
||||||
|
Text {
|
||||||
|
text: "air"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: Math.round(WeatherData.windKmh) + " km/h"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: WeatherData.forecast.length > 0
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
visible: WeatherData.forecast.length > 0
|
||||||
|
width: parent.width
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: WeatherData.forecast
|
||||||
|
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
required property var modelData
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: Qt.formatDateTime(new Date(modelData.date + "T12:00:00"), "ddd")
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: WeatherData.iconFor(modelData.code, true)
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 18
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: Math.round(modelData.maxC) + "°/" + Math.round(modelData.minC) + "°"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,8 @@ RowLayout {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: Hyprland.dispatch("workspace " + modelData.name)
|
// Hyprland's Lua config expects Lua syntax from dispatch
|
||||||
|
onClicked: Hyprland.dispatch("hl.dsp.focus({ workspace = " + JSON.stringify(modelData.id > 0 ? modelData.id : modelData.name) + " })")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,13 @@ PanelWindow {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
exclusiveZone: 0
|
||||||
|
|
||||||
// Top-right, just under the bar — same fixed distance from the bar and
|
// Bottom-center: anchoring only the bottom edge lets the layer shell
|
||||||
// screen edge (Appearance.spacing) that every popup opened from the bar
|
// center it horizontally.
|
||||||
// itself uses, so this lines up with them instead of floating
|
anchors { bottom: true }
|
||||||
// bottom-center.
|
margins.bottom: Appearance.spacing * 4
|
||||||
anchors { top: true; right: true }
|
|
||||||
margins.top: Appearance.spacing
|
|
||||||
margins.right: Appearance.spacing
|
|
||||||
|
|
||||||
implicitWidth: 272
|
implicitWidth: 300
|
||||||
implicitHeight: 54
|
implicitHeight: 60
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
WlrLayershell.namespace: "quickshell:osd"
|
WlrLayershell.namespace: "quickshell:osd"
|
||||||
@@ -49,13 +46,13 @@ PanelWindow {
|
|||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 14
|
anchors.margins: 15
|
||||||
spacing: 12
|
spacing: 13
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
font.family: Appearance.iconFontFamilyRounded
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
font.variableAxes: Appearance.iconFontAxes
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
font.pixelSize: 18
|
font.pixelSize: 21
|
||||||
color: Colors.foreground
|
color: Colors.foreground
|
||||||
text: {
|
text: {
|
||||||
const audio = osd.sink ? osd.sink.audio : null
|
const audio = osd.sink ? osd.sink.audio : null
|
||||||
@@ -67,7 +64,7 @@ PanelWindow {
|
|||||||
Item {
|
Item {
|
||||||
id: slider
|
id: slider
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: 16
|
implicitHeight: 18
|
||||||
|
|
||||||
readonly property real value: osd.sink && osd.sink.audio ? osd.sink.audio.volume : 0
|
readonly property real value: osd.sink && osd.sink.audio ? osd.sink.audio.volume : 0
|
||||||
readonly property bool enabled: !!osd.sink
|
readonly property bool enabled: !!osd.sink
|
||||||
@@ -76,7 +73,7 @@ PanelWindow {
|
|||||||
id: track
|
id: track
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 4
|
height: 5
|
||||||
radius: 0
|
radius: 0
|
||||||
color: Colors.muted
|
color: Colors.muted
|
||||||
opacity: 0.35
|
opacity: 0.35
|
||||||
@@ -85,15 +82,15 @@ PanelWindow {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
width: track.width * Math.max(0, Math.min(1, slider.value))
|
width: track.width * Math.max(0, Math.min(1, slider.value))
|
||||||
height: 4
|
height: 5
|
||||||
radius: 0
|
radius: 0
|
||||||
color: Colors.primary
|
color: Colors.primary
|
||||||
opacity: slider.enabled ? 1 : 0.4
|
opacity: slider.enabled ? 1 : 0.4
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 12
|
width: 14
|
||||||
height: 12
|
height: 14
|
||||||
radius: 6
|
radius: 7
|
||||||
color: Colors.primary
|
color: Colors.primary
|
||||||
opacity: slider.enabled ? 1 : 0.4
|
opacity: slider.enabled ? 1 : 0.4
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -113,10 +110,10 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
Layout.preferredWidth: 32
|
Layout.preferredWidth: 38
|
||||||
horizontalAlignment: Text.AlignRight
|
horizontalAlignment: Text.AlignRight
|
||||||
font.family: Appearance.fontFamily
|
font.family: Appearance.fontFamily
|
||||||
font.pixelSize: Appearance.fontSizeSmall
|
font.pixelSize: 13
|
||||||
color: Colors.muted
|
color: Colors.muted
|
||||||
text: Math.round(slider.value * 100) + "%"
|
text: Math.round(slider.value * 100) + "%"
|
||||||
}
|
}
|
||||||
|
|||||||
50
dotfiles/.config/quickshell/scripts/sync-dotfiles.sh
Executable file
50
dotfiles/.config/quickshell/scripts/sync-dotfiles.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run by update-system.sh inside the update terminal, before the system
|
||||||
|
# update — pulls the dotfiles repo and re-stows it so both machines pick up
|
||||||
|
# config changes on every update.
|
||||||
|
#
|
||||||
|
# Never fails the update: a pull that can't fast-forward (local commits or
|
||||||
|
# uncommitted changes in the way) or a stow conflict just prints a warning
|
||||||
|
# and leaves things as they were, then the system update carries on.
|
||||||
|
|
||||||
|
DOTFILES="$HOME/Projects/dotfiles"
|
||||||
|
|
||||||
|
warn() { printf '\033[1;33m%s\033[0m\n' "$*"; }
|
||||||
|
info() { printf '\033[1;36m%s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
if [ ! -d "$DOTFILES/.git" ]; then
|
||||||
|
warn "Dotfiles repo not found at $DOTFILES — skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Pulling dotfiles..."
|
||||||
|
# Fetch and merge separately so the warning says which one went wrong — the
|
||||||
|
# remote is SSH via Bitwarden's agent, so a failed fetch is almost always
|
||||||
|
# Bitwarden being closed or locked.
|
||||||
|
if ! git -C "$DOTFILES" fetch; then
|
||||||
|
warn "Couldn't reach the dotfiles remote — is Bitwarden open and unlocked? Skipping stow."
|
||||||
|
echo
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# --ff-only so a diverged branch never produces a surprise merge commit;
|
||||||
|
# sort that out by hand instead.
|
||||||
|
if ! git -C "$DOTFILES" merge --ff-only; then
|
||||||
|
warn "Couldn't fast-forward dotfiles (local changes or diverged branch) — skipping stow."
|
||||||
|
echo
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v stow &> /dev/null; then
|
||||||
|
warn "stow not installed — skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Applying dotfiles with stow..."
|
||||||
|
# -R restows: drops links for files removed from the repo and adds new ones.
|
||||||
|
if stow -R -d "$DOTFILES" -t "$HOME" dotfiles; then
|
||||||
|
info "Dotfiles applied."
|
||||||
|
else
|
||||||
|
warn "stow hit a conflict — nothing was changed. Fix it and re-run:"
|
||||||
|
warn " stow -R -d $DOTFILES -t ~ dotfiles"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
66
dotfiles/.config/quickshell/scripts/update-system.sh
Executable file
66
dotfiles/.config/quickshell/scripts/update-system.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Launched by SystemUpdate.qml (top bar, next to the clock) — opens a
|
||||||
|
# terminal running whatever update command fits this distro, then notifies
|
||||||
|
# once it's done. Ported from the old waybar custom/updater module
|
||||||
|
# (.config/waybar/scripts/update-distro.sh); kept multi-distro even though
|
||||||
|
# this machine is Arch, since these dotfiles aren't.
|
||||||
|
#
|
||||||
|
# The window it opens is floated + centered by a matching Hyprland rule
|
||||||
|
# (system-update-terminal, in .config/hypr/UserConfigs/WindowRules.conf),
|
||||||
|
# same treatment as Bitwarden's window, rather than tiling into whatever
|
||||||
|
# workspace you happened to be on.
|
||||||
|
|
||||||
|
iDIR="$HOME/.config/swaync/images"
|
||||||
|
|
||||||
|
if ! command -v foot &> /dev/null; then
|
||||||
|
notify-send -i "$iDIR/error.png" "Need foot:" "foot terminal not found. Please install foot terminal."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Printed inside the terminal before the actual update command runs, so it
|
||||||
|
# reads as "updating" from the first frame rather than looking like an
|
||||||
|
# empty/stuck window while pacman/apt/etc. take a moment to start printing.
|
||||||
|
#
|
||||||
|
# foot runs this in a fresh `bash -c`, which doesn't inherit shell functions
|
||||||
|
# from this script's own environment (export -f only reaches bash processes
|
||||||
|
# forked from *this* one, not whatever foot execs) — so instead, the
|
||||||
|
# already-colored text is built once here with real escape bytes ($'...'),
|
||||||
|
# then %q-quoted into a self-contained `echo '...'` that reproduces those
|
||||||
|
# same bytes verbatim when the child bash parses it.
|
||||||
|
msg=" Updating System — please wait "
|
||||||
|
line=$(printf '=%.0s' $(seq 1 ${#msg}))
|
||||||
|
banner_text=$'\033[1;36m'"$line"$'\033[0m\n\033[1;36m'"$msg"$'\033[0m\n\033[1;36m'"$line"$'\033[0m\n\n'
|
||||||
|
BANNER="echo $(printf '%q' "$banner_text")"
|
||||||
|
|
||||||
|
if command -v paru &> /dev/null; then
|
||||||
|
CMD="paru -Syu"
|
||||||
|
LABEL="Arch-based system"
|
||||||
|
elif command -v yay &> /dev/null; then
|
||||||
|
CMD="yay -Syu"
|
||||||
|
LABEL="Arch-based system"
|
||||||
|
elif command -v dnf &> /dev/null; then
|
||||||
|
CMD="sudo dnf update --refresh -y"
|
||||||
|
LABEL="Fedora system"
|
||||||
|
elif command -v apt &> /dev/null; then
|
||||||
|
CMD="sudo apt update && sudo apt upgrade -y"
|
||||||
|
LABEL="Debian/Ubuntu system"
|
||||||
|
elif command -v zypper &> /dev/null; then
|
||||||
|
CMD="sudo zypper dup -y"
|
||||||
|
LABEL="openSUSE system"
|
||||||
|
else
|
||||||
|
notify-send -i "$iDIR/error.png" -u critical "Unsupported system" "This script does not support your distribution."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keeps the terminal open (whether the update succeeded or failed — read
|
||||||
|
# always runs regardless, since it's ; not &&) until you press Enter,
|
||||||
|
# instead of foot closing the instant the update command exits and taking
|
||||||
|
# any output/errors with it.
|
||||||
|
WAIT_PROMPT="echo; read -p 'Update finished — press Enter to close...'"
|
||||||
|
|
||||||
|
# Pull + re-stow the dotfiles first, so config changes from the other machine
|
||||||
|
# land alongside the package update.
|
||||||
|
SYNC="$(printf '%q' "$(dirname "$0")/sync-dotfiles.sh")"
|
||||||
|
|
||||||
|
foot -T update bash -c "$BANNER; $SYNC; $CMD; $WAIT_PROMPT"
|
||||||
|
notify-send -i "$iDIR/profile-pic.jpg" -u low "$LABEL" 'has been updated.'
|
||||||
@@ -4,7 +4,11 @@ import "./modules/osd"
|
|||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
Variants {
|
Variants {
|
||||||
model: Quickshell.screens
|
// Exclude the Sunshine headless virtual monitor — nobody ever sees a
|
||||||
|
// bar there, but PanelWindow's exclusiveZone still reserves space for
|
||||||
|
// it, which left a black strip cut out of Steam Big Picture's
|
||||||
|
// fullscreen area on that output.
|
||||||
|
model: Quickshell.screens.filter(s => !s.name.startsWith("HEADLESS"))
|
||||||
|
|
||||||
Bar {
|
Bar {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|||||||
5
dotfiles/.config/sunshine/.gitignore
vendored
Normal file
5
dotfiles/.config/sunshine/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Machine-local Sunshine state lives next to the stowed config; never commit it
|
||||||
|
credentials/
|
||||||
|
*.log
|
||||||
|
*.log.*
|
||||||
|
sunshine_state.json
|
||||||
47
dotfiles/.config/sunshine/apps.json
Normal file
47
dotfiles/.config/sunshine/apps.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"PATH": "$(PATH):$(HOME)/.local/bin"
|
||||||
|
},
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "Desktop",
|
||||||
|
"image-path": "desktop.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Low Res Desktop",
|
||||||
|
"image-path": "desktop.png",
|
||||||
|
"prep-cmd": [
|
||||||
|
{
|
||||||
|
"do": "xrandr --output HDMI-1 --mode 1920x1080",
|
||||||
|
"undo": "xrandr --output HDMI-1 --mode 1920x1200"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Steam Big Picture",
|
||||||
|
"detached": [
|
||||||
|
"setsid steam steam://open/bigpicture"
|
||||||
|
],
|
||||||
|
"prep-cmd": [
|
||||||
|
{
|
||||||
|
"do": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-prep.sh",
|
||||||
|
"undo": "setsid steam steam://close/bigpicture"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"image-path": "steam.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Steam Big Picture (TV)",
|
||||||
|
"detached": [
|
||||||
|
"setsid steam steam://open/bigpicture"
|
||||||
|
],
|
||||||
|
"prep-cmd": [
|
||||||
|
{
|
||||||
|
"do": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh",
|
||||||
|
"undo": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"image-path": "steam.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
dotfiles/.config/sunshine/scripts/steam-bigpicture-prep.sh
Executable file
17
dotfiles/.config/sunshine/scripts/steam-bigpicture-prep.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Run before Sunshine launches Steam Big Picture: focus the headless virtual
|
||||||
|
# monitor and park the cursor on it, so Big Picture (which spawns on whatever
|
||||||
|
# monitor currently has focus/the mouse) opens there instead of wherever the
|
||||||
|
# mouse happened to be.
|
||||||
|
set -e
|
||||||
|
hyprctl dispatch 'hl.dsp.focus({monitor = "HEADLESS-1"})'
|
||||||
|
hyprctl dispatch 'hl.dsp.cursor.move({x = 5344, y = 540})'
|
||||||
|
|
||||||
|
# Sunshine only flips the system default audio sink to sink-sunshine-stereo
|
||||||
|
# after the streaming session/client handshake completes (~500ms after this
|
||||||
|
# script runs and Steam is spawned) — see sunshine.log timestamps. If Steam's
|
||||||
|
# own audio (SDL) initializes in that window, it binds to whatever was
|
||||||
|
# default at the time (the real HDMI output) and never re-homes even after
|
||||||
|
# Sunshine's swap. Set it here first so it's already correct before Steam
|
||||||
|
# ever launches; Sunshine's own (redundant) swap right after is harmless.
|
||||||
|
pactl set-default-sink sink-sunshine-stereo || true
|
||||||
18
dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh
Executable file
18
dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Run before Sunshine launches Steam Big Picture for the 4K TV target.
|
||||||
|
# Switches the headless virtual monitor from its default 1728x1080@60
|
||||||
|
# (used for the Steam Deck target) up to 3840x2160@60, then focuses it
|
||||||
|
# and parks the cursor there so Big Picture opens on it. Reverted by
|
||||||
|
# steam-bigpicture-tv-undo.sh once the stream ends.
|
||||||
|
set -e
|
||||||
|
# NOTE: this Hyprland build uses the Lua config layer, which rejects the
|
||||||
|
# usual `hyprctl keyword monitor ...` syntax ("keyword can't work with
|
||||||
|
# non-legacy parsers. Use eval.") — hl.monitor() via `hyprctl eval` is the
|
||||||
|
# runtime equivalent of the hl.monitor() call in layout/monitors.lua.
|
||||||
|
hyprctl eval 'hl.monitor({ output = "HEADLESS-1", mode = "3840x2160@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })'
|
||||||
|
hyprctl dispatch 'hl.dsp.focus({monitor = "HEADLESS-1"})'
|
||||||
|
hyprctl dispatch 'hl.dsp.cursor.move({x = 6400, y = 1080})'
|
||||||
|
|
||||||
|
# See steam-bigpicture-prep.sh for why this runs here instead of relying on
|
||||||
|
# Sunshine's own post-handshake audio-sink swap.
|
||||||
|
pactl set-default-sink sink-sunshine-stereo || true
|
||||||
6
dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh
Executable file
6
dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Undo for steam-bigpicture-tv-prep.sh: close Big Picture and restore
|
||||||
|
# HEADLESS-1 to its default mode (the one the Steam Deck target expects).
|
||||||
|
set -e
|
||||||
|
setsid steam steam://close/bigpicture
|
||||||
|
hyprctl eval 'hl.monitor({ output = "HEADLESS-1", mode = "1728x1080@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })'
|
||||||
5
dotfiles/.config/sunshine/sunshine.conf
Normal file
5
dotfiles/.config/sunshine/sunshine.conf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
audio_sink = sink-sunshine-stereo
|
||||||
|
csrf_allowed_origins = https://xol.twhomelab.local:47990
|
||||||
|
nvenc_preset = 4
|
||||||
|
output_name = HEADLESS-1
|
||||||
|
global_prep_cmd = [{"do":"/home/thomas/.config/hypr/scripts/SunshineIdle.sh start","undo":"/home/thomas/.config/hypr/scripts/SunshineIdle.sh stop"}]
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"tooltip-format": "{ipaddr} via {gwaddr}",
|
"tooltip-format": "{ipaddr} via {gwaddr}",
|
||||||
"tooltip-format-disconnected": "Disconnected",
|
"tooltip-format-disconnected": "Disconnected",
|
||||||
"max-length": 50,
|
"max-length": 50,
|
||||||
"on-click": "orbit toggle top-right",
|
"on-click": "nm-connection-editor",
|
||||||
"on-click-right": "kitty -e nmtui",
|
"on-click-right": "kitty -e nmtui",
|
||||||
"format-icons": {
|
"format-icons": {
|
||||||
"wifi": ["", "", "", "", ""],
|
"wifi": ["", "", "", "", ""],
|
||||||
|
|||||||
18
dotfiles/.local/bin/Plex
Executable file
18
dotfiles/.local/bin/Plex
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Wrapper for the plex-desktop package's /usr/bin/Plex (-> /opt/plex-desktop/Plex.sh).
|
||||||
|
#
|
||||||
|
# QT_QUICK_CONTROLS_STYLE=org.hyprland.style is set globally in
|
||||||
|
# hypr/core/env.lua for other Qt/QML apps' theming (hyprland-qt-support), but
|
||||||
|
# Plex Desktop bundles its own private Qt/QML runtime that doesn't ship that
|
||||||
|
# style plugin — inheriting the var makes it fail to load its UI and exit
|
||||||
|
# immediately with "module \"org.hyprland.style\" is not installed" (see
|
||||||
|
# ~/.local/share/plex/Logs/Plex.log). This dir is first on $PATH, so it
|
||||||
|
# shadows /usr/bin/Plex for both a terminal launch and the .desktop entry
|
||||||
|
# (Exec=Plex is PATH-resolved) without touching the global env var.
|
||||||
|
# QT_QPA_PLATFORM: hypr env sets "wayland;xcb"; under Wayland the bundled
|
||||||
|
# QtWebEngine never runs the web client and the window stays blank.
|
||||||
|
unset QT_QUICK_CONTROLS_STYLE
|
||||||
|
export QT_QPA_PLATFORM=xcb
|
||||||
|
# Unset in the session; without it the X11 cursor in Plex is huge.
|
||||||
|
export XCURSOR_SIZE=24
|
||||||
|
exec /opt/plex-desktop/Plex.sh "$@"
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# User override of /usr/share/applications/tv.plex.PlexDesktop.desktop
|
||||||
|
# (~/.local/share/applications takes XDG precedence over /usr/share for the
|
||||||
|
# same filename, so this is picked up by app launchers instead automatically
|
||||||
|
# — no association/mimeapps changes needed).
|
||||||
|
#
|
||||||
|
# QT_QPA_PLATFORM=xcb is kept from upstream: hypr env sets "wayland;xcb", and
|
||||||
|
# under Wayland the bundled QtWebEngine never runs the web client (blank
|
||||||
|
# window, no [Web] lines in Plex.log). XCURSOR_SIZE=24 is also kept from
|
||||||
|
# upstream: it is unset in the session, so the X11 cursor comes out huge.
|
||||||
|
#
|
||||||
|
# Change from upstream: Exec unsets QT_QUICK_CONTROLS_STYLE and calls
|
||||||
|
# the package's Plex.sh directly, bypassing $PATH entirely. Plex Desktop
|
||||||
|
# bundles its own private Qt/QML runtime that doesn't ship the
|
||||||
|
# "org.hyprland.style" QQC2 style hypr/core/env.lua sets globally for other
|
||||||
|
# apps' theming — inheriting it makes Plex fail to load its UI and exit
|
||||||
|
# immediately with "module \"org.hyprland.style\" is not installed" (see
|
||||||
|
# ~/.local/share/plex/Logs/Plex.log). Going straight to the fixed install
|
||||||
|
# path (rather than relying on the .local/bin/Plex wrapper shadowing
|
||||||
|
# /usr/bin/Plex on $PATH) means this keeps working even if $PATH order ever
|
||||||
|
# changes.
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Plex
|
||||||
|
Comment=Plex client for desktop computers
|
||||||
|
Exec=env -u QT_QUICK_CONTROLS_STYLE QT_QPA_PLATFORM=xcb XCURSOR_SIZE=24 /opt/plex-desktop/Plex.sh
|
||||||
|
Icon=tv.plex.PlexDesktop
|
||||||
|
Terminal=false
|
||||||
|
Categories=AudioVideo;Player;X-Media;
|
||||||
|
X-Flatpak=tv.plex.PlexDesktop
|
||||||
|
StartupWMClass=Plex
|
||||||
|
Name[en_US]=Plex
|
||||||
14
dotfiles/.ssh/config
Normal file
14
dotfiles/.ssh/config
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Host 192.168.1.87
|
||||||
|
HostName 192.168.1.87
|
||||||
|
User thoams
|
||||||
|
ForwardAgent yes
|
||||||
|
|
||||||
|
Host 192.168.1.83
|
||||||
|
HostName 192.168.1.83
|
||||||
|
User thoams
|
||||||
|
ForwardAgent yes
|
||||||
|
|
||||||
|
Host 192.168.1.86
|
||||||
|
HostName 192.168.1.86
|
||||||
|
User thoams
|
||||||
|
ForwardAgent yes
|
||||||
@@ -23,7 +23,7 @@ source $ZSH/oh-my-zsh.sh
|
|||||||
|
|
||||||
# My alias
|
# My alias
|
||||||
|
|
||||||
alias cl='clear'
|
alias cl='clear && fastfetch'
|
||||||
alias zed='zeditor'
|
alias zed='zeditor'
|
||||||
alias ssh-gavin='ssh thoams@192.168.1.83'
|
alias ssh-gavin='ssh thoams@192.168.1.83'
|
||||||
alias ssh-raspberry-pi='ssh thoams@192.168.1.86'
|
alias ssh-raspberry-pi='ssh thoams@192.168.1.86'
|
||||||
@@ -51,3 +51,4 @@ export ZED_ALLOW_EMULATED_GPU=1
|
|||||||
fastfetch
|
fastfetch
|
||||||
|
|
||||||
eval "$(oh-my-posh init zsh --config $HOME/.config/ohmyposh/zen.toml)"
|
eval "$(oh-my-posh init zsh --config $HOME/.config/ohmyposh/zen.toml)"
|
||||||
|
export PATH="$HOME/.local/share/turritopsis/flutter-sdk/bin:$PATH"
|
||||||
|
|||||||
11
system/install.sh
Executable file
11
system/install.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Installs system-level config that stow can't manage (needs root).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# pacman: shared [options] snippet
|
||||||
|
sudo install -m644 "$repo/pacman/options.conf" /etc/pacman.d/options.conf
|
||||||
|
if ! grep -q '^Include = /etc/pacman.d/options.conf' /etc/pacman.conf; then
|
||||||
|
sudo sed -i '/^\[options\]/a Include = /etc/pacman.d/options.conf' /etc/pacman.conf
|
||||||
|
fi
|
||||||
197
system/packages.txt
Normal file
197
system/packages.txt
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Packages installed by bootstrap.sh on a new machine.
|
||||||
|
#
|
||||||
|
# - One package per line; anything after "#" is a description.
|
||||||
|
# - Lines starting with "#" are skipped (left here for reference).
|
||||||
|
# - [section] lines tell bootstrap.sh which installer to use;
|
||||||
|
# [services] lists systemd units to enable.
|
||||||
|
#
|
||||||
|
# Not listed here (bootstrap.sh handles them itself):
|
||||||
|
# - CPU microcode and GPU drivers: auto-detected from the hardware
|
||||||
|
# - yay: bootstrapped from the AUR before the [aur] section
|
||||||
|
# - svt-av1-essential-git: optional, also rebuilds ffmpeg against it
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[pacman]
|
||||||
|
adw-gtk-theme # Unofficial GTK 3 port of the libadwaita theme
|
||||||
|
# amd-ucode # Microcode update image for AMD CPUs -- auto-detected by bootstrap.sh
|
||||||
|
awww # An Answer to your Wayland Wallpaper Woes
|
||||||
|
baobab # A graphical directory tree analyzer
|
||||||
|
base # Minimal package set to define a basic Arch Linux installation
|
||||||
|
base-devel # Basic tools to build Arch Linux packages
|
||||||
|
blueman # GTK+ Bluetooth Manager
|
||||||
|
bluez # Daemons for the bluetooth protocol stack
|
||||||
|
bluez-utils # Development and debugging utilities for the bluetooth protocol stack
|
||||||
|
btop # A monitor of system resources, bpytop ported to C++
|
||||||
|
dolphin # KDE File Manager
|
||||||
|
# dunst # Customizable and lightweight notification-daemon !! you use swaync for notifications
|
||||||
|
efibootmgr # Linux user-space application to modify the EFI Boot Manager
|
||||||
|
fastfetch # A feature-rich and performance oriented neofetch like system information tool
|
||||||
|
ffmpeg # Complete solution to record, convert and stream audio and video
|
||||||
|
filezilla # Fast and reliable FTP, FTPS and SFTP client
|
||||||
|
firefox # Fast, Private & Safe Web Browser
|
||||||
|
flatpak # Linux application sandboxing and distribution framework (formerly xdg-app)
|
||||||
|
foot # Fast, lightweight, and minimalistic Wayland terminal emulator
|
||||||
|
fzf # Command-line fuzzy finder
|
||||||
|
galculator # GTK based scientific calculator
|
||||||
|
# garcon # Freedesktop.org compliant menu library !! XFCE desktop leftover?
|
||||||
|
gdu # Fast disk usage analyzer
|
||||||
|
git # the fast distributed version control system
|
||||||
|
gnome-calendar # Simple and beautiful calendar application designed to perfectly fit the GNOME desktop
|
||||||
|
gnome-disk-utility # Disk Management Utility for GNOME
|
||||||
|
gnome-font-viewer # A font viewer utility for GNOME
|
||||||
|
gnome-keyring # Stores passwords and encryption keys
|
||||||
|
gnome-text-editor # A simple text editor for the GNOME desktop
|
||||||
|
grim # Screenshot utility for Wayland
|
||||||
|
gst-plugin-pipewire # Multimedia graph framework - pipewire plugin
|
||||||
|
gvfs # Virtual filesystem implementation for GIO
|
||||||
|
gvfs-smb # Virtual filesystem implementation for GIO - SMB/CIFS backend (Windows file sharing)
|
||||||
|
htop # Interactive process viewer
|
||||||
|
hypridle # hyprland’s idle daemon
|
||||||
|
hyprland # a highly customizable dynamic tiling Wayland compositor
|
||||||
|
hyprlock # hyprland’s GPU-accelerated screen locking utility
|
||||||
|
hyprshot # Hyprland screenshot utility
|
||||||
|
imv # Image viewer for Wayland and X11
|
||||||
|
jq # Command-line JSON processor
|
||||||
|
# kitty # A modern, hackable, featureful, OpenGL-based terminal emulator !! you use foot as terminal (kitty config not in dotfiles)
|
||||||
|
libgsf # Extensible I/O abstraction library for dealing with structured file formats
|
||||||
|
libmatroska # Matroska library
|
||||||
|
libpulse # A featureful, general-purpose sound server (client library)
|
||||||
|
# libva-nvidia-driver # VA-API implementation that uses NVDEC as a backend -- auto-detected by bootstrap.sh
|
||||||
|
linux # The Linux kernel and modules
|
||||||
|
linux-firmware # Firmware files for Linux - Default set
|
||||||
|
mkinitcpio # Modular initramfs image creation utility
|
||||||
|
mkvtoolnix-cli # Set of tools to create, edit and inspect Matroska files
|
||||||
|
mpv # a free, open source, and cross-platform media player
|
||||||
|
mullvad-vpn-daemon # Mullvad VPN client (daemon and CLI)
|
||||||
|
nano # Pico editor clone with enhancements
|
||||||
|
network-manager-applet # Applet for managing network connections
|
||||||
|
networkmanager # Network connection manager and user applications
|
||||||
|
noto-fonts # Google Noto TTF fonts
|
||||||
|
noto-fonts-cjk # Google Noto CJK fonts
|
||||||
|
noto-fonts-emoji # Google Noto Color Emoji font
|
||||||
|
noto-fonts-extra # Google Noto TTF fonts - additional variants
|
||||||
|
npm # JavaScript package manager
|
||||||
|
nss-mdns # glibc plugin providing host name resolution via mDNS
|
||||||
|
# nvidia-open # NVIDIA open kernel modules -- auto-detected by bootstrap.sh
|
||||||
|
pacman-contrib # Contributed scripts and tools for pacman systems
|
||||||
|
pam-u2f # Universal 2nd Factor (U2F) PAM authentication module from Yubico
|
||||||
|
papirus-icon-theme # Papirus icon theme
|
||||||
|
pipewire # Low-latency audio/video router and processor
|
||||||
|
pipewire-alsa # Low-latency audio/video router and processor - ALSA configuration
|
||||||
|
pipewire-jack # Low-latency audio/video router and processor - JACK replacement
|
||||||
|
pipewire-pulse # Low-latency audio/video router and processor - PulseAudio replacement
|
||||||
|
pnpm # Fast, disk space efficient package manager
|
||||||
|
polkit-kde-agent # Daemon providing a polkit authentication UI for KDE
|
||||||
|
postgresql # Sophisticated object-relational DBMS
|
||||||
|
postgresql-libs # Sophisticated object-relational DBMS - Client binaries and libraries
|
||||||
|
proton-vpn-cli # Official ProtonVPN CLI Linux app
|
||||||
|
qt5-wayland # Provides APIs for Wayland
|
||||||
|
qt6-quickeffectmaker # Qt Quick Effect Maker (QQEM) for creating and editing custom shader effects
|
||||||
|
qt6-wayland # Provides APIs for Wayland
|
||||||
|
rofi # A window switcher, application launcher and dmenu replacement
|
||||||
|
sddm # QML based X11 and Wayland display manager
|
||||||
|
slurp # Select a region in a Wayland compositor
|
||||||
|
smartmontools # Control and monitor S.M.A.R.T. enabled ATA and SCSI Hard Drives
|
||||||
|
steam # Valve's digital software delivery system
|
||||||
|
stow # Manage installation of multiple softwares in the same directory tree
|
||||||
|
sudo # Give certain users the ability to run some commands as root
|
||||||
|
swaync # A simple GTK based notification daemon for Sway
|
||||||
|
tailscale # A mesh VPN that makes it easy to connect your devices, wherever they are.
|
||||||
|
thunar # Modern, fast and easy-to-use file manager for Xfce
|
||||||
|
thunar-archive-plugin # Adds archive operations to the Thunar file context menus
|
||||||
|
thunar-media-tags-plugin # Adds special features for media files to the Thunar File Manager
|
||||||
|
thunar-shares-plugin # Thunar plugin to quickly share a folder using Samba without requiring root access
|
||||||
|
thunar-volman # Automatic management of removable drives and media for Thunar
|
||||||
|
ttf-dejavu # Font family based on the Bitstream Vera Fonts with a wider range of characters
|
||||||
|
ttf-iosevka-nerd # Patched font Iosevka from nerd fonts library
|
||||||
|
ttf-jetbrains-mono # Typeface for developers, by JetBrains
|
||||||
|
ttf-jetbrains-mono-nerd # Patched font JetBrains Mono from nerd fonts library
|
||||||
|
ttf-liberation # Font family which aims at metric compatibility with Arial, Times New Roman, and Courier New
|
||||||
|
ttf-material-icons # Material Design icons by Google (deprecated classic Material Icons static style)
|
||||||
|
ttf-material-symbols-variable # Material Design icons by Google (Material Symbols - 4 axis variable)
|
||||||
|
ttf-noto-nerd # Patched font Noto from nerd fonts library
|
||||||
|
tumbler # Thumbnail service implementing the thumbnail management D-Bus specification
|
||||||
|
uv # An extremely fast Python package installer and resolver written in Rust
|
||||||
|
uwsm # A standalone Wayland session manager
|
||||||
|
vim # Vi Improved, a highly configurable, improved version of the vi text editor
|
||||||
|
# waybar # Highly customizable Wayland bar for Sway and Wlroots based compositors !! you've switched to quickshell (waybar is commented out in startup.lua)
|
||||||
|
webp-pixbuf-loader # WebM GDK Pixbuf Loader library
|
||||||
|
wget # Network utility to retrieve files from the web
|
||||||
|
wireplumber # Session / policy manager implementation for PipeWire
|
||||||
|
# wofi # launcher for wlroots-based wayland compositors !! you also have rofi — need both?
|
||||||
|
wpa_supplicant # A utility providing key negotiation for WPA wireless networks
|
||||||
|
xarchiver # GTK frontend to various command line archivers
|
||||||
|
xdg-desktop-portal-hyprland # xdg-desktop-portal backend for hyprland
|
||||||
|
xdg-utils # Command line tools that assist applications with a variety of desktop integration tasks
|
||||||
|
# xfce4-appfinder # Application launcher and finder !! XFCE desktop leftover?
|
||||||
|
# xfce4-panel # Panel for the Xfce desktop environment !! XFCE desktop leftover?
|
||||||
|
# xfce4-power-manager # Power Manager for Xfce !! XFCE desktop leftover?
|
||||||
|
# xfce4-session # Xfce's session manager !! XFCE desktop leftover?
|
||||||
|
# xfce4-settings # Xfce's Configuration System !! XFCE desktop leftover?
|
||||||
|
# xfce4-terminal # A modern terminal emulator primarily for the Xfce desktop environment !! XFCE desktop leftover?
|
||||||
|
# xfdesktop # Xfce's desktop manager !! XFCE desktop leftover?
|
||||||
|
# xfwm4 # Xfce's window manager !! XFCE desktop leftover?
|
||||||
|
zram-generator # Systemd unit generator for zram devices
|
||||||
|
zsh # A very advanced and programmable command interpreter (shell) for UNIX
|
||||||
|
|
||||||
|
# Used by the dotfiles, previously only pulled in by caelestia-shell / waybar
|
||||||
|
brightnessctl # Backlight control (quickshell, hypr keybinds)
|
||||||
|
cliphist # Clipboard history (wl-paste watcher in startup.lua)
|
||||||
|
playerctl # Media key control
|
||||||
|
power-profiles-daemon # Power profiles (quickshell battery widget)
|
||||||
|
wl-clipboard # wl-copy / wl-paste
|
||||||
|
openssh # SSH client/server (sshd service, git over ssh)
|
||||||
|
avahi # mDNS daemon (avahi-daemon service, used by nss-mdns)
|
||||||
|
|
||||||
|
[aur]
|
||||||
|
bitwarden-bin # A secure and free password manager for all of your devices.
|
||||||
|
# caelestia-shell # The desktop shell for the Caelestia dotfiles !! alternative quickshell config, not used by your shell.qml
|
||||||
|
flutter-bin # Cross platform widget toolkit for Dart (monolithic)
|
||||||
|
google-chrome # The popular web browser by Google (Stable Channel)
|
||||||
|
hyprmod # Native GTK4/libadwaita settings app for Hyprland
|
||||||
|
# jellyfin-desktop-debug # Detached debugging symbols for jellyfin-desktop !! debug symbols, not needed
|
||||||
|
minecraft-launcher # Official Minecraft Launcher
|
||||||
|
# orbit-wifi # A WiFi/Bluetooth manager for Wayland with glassmorphism UI !! removed from your config in commit 88484f8
|
||||||
|
plex-desktop # Plex desktop client for linux
|
||||||
|
plexamp-bin # Modern music client for Plex.(Prebuilt version.Use system-wide electron)
|
||||||
|
puddletag # An audio tag editor for GNU/Linux
|
||||||
|
quickshell-git # The shell your bar/OSD run on (previously a caelestia-shell dependency)
|
||||||
|
pwvucontrol # Pipewire volume control for GNOME
|
||||||
|
python-pywal # Generate and change colorschemes on the fly
|
||||||
|
raw-thumbnailer # A lightweight and fast raw image thumbnailer that can be used by file managers.
|
||||||
|
seafile-client # GUI client for synchronizing your local files with seafile server
|
||||||
|
sunshine # Self-hosted game stream host for Moonlight
|
||||||
|
# svt-av1-essential-git # A no-nonsense SVT-AV1 encoder fork with sensible defaults and Quality of Life improvements -- optional step in bootstrap.sh (rebuilds ffmpeg)
|
||||||
|
ttf-icomoon-feather # Icomoon feather font
|
||||||
|
tumbler-extra-thumbnailers # Customized thumbnailers for Tumbler
|
||||||
|
turritopsis # Desktop Jellyfin client with mpv playback
|
||||||
|
vesktop # A standalone Electron-based Discord app with Vencord & improved Linux support
|
||||||
|
visual-studio-code-bin # Visual Studio Code (vscode)
|
||||||
|
waypaper # GUI wallpaper setter for Wayland and Xorg window managers. It works as a frontend for popular wallpaper backends like swaybg, swww, wallutils, hyprpaper and feh.
|
||||||
|
# yay # Yet another yogurt. Pacman wrapper and AUR helper written in go. !! bootstrapped by the script itself anyway
|
||||||
|
# yay-debug # Detached debugging symbols for yay !! debug symbols, not needed
|
||||||
|
zen-browser-bin # Official package for Zen, a privacy-focused, feature packed Firefox-based web browser
|
||||||
|
|
||||||
|
[flatpak]
|
||||||
|
com.makemkv.MakeMKV # MakeMKV
|
||||||
|
org.jellyfin.JellyfinDesktop # Jellyfin Desktop
|
||||||
|
|
||||||
|
[installer] # official install scripts, see bootstrap.sh
|
||||||
|
claude # Claude Code CLI (official install script)
|
||||||
|
oh-my-posh # Prompt theme engine (official install script; your theme is already in dotfiles)
|
||||||
|
# ffmpeg # Static ffmpeg build — !! duplicate, ffmpeg is also installed via pacman above
|
||||||
|
# python3.13 # Standalone Python 3.13 — !! probably not needed, uv can provide Python
|
||||||
|
|
||||||
|
[services] # enabled with systemctl, start on next boot
|
||||||
|
NetworkManager.service # Networking
|
||||||
|
bluetooth.service # Bluetooth
|
||||||
|
sddm.service # Login screen
|
||||||
|
sshd.service # SSH server
|
||||||
|
avahi-daemon.service # mDNS / .local hostnames
|
||||||
|
systemd-resolved.service # DNS resolver (used by the VPNs)
|
||||||
|
systemd-timesyncd.service # Clock sync
|
||||||
|
fstrim.timer # Weekly SSD trim
|
||||||
|
tailscaled.service # Tailscale
|
||||||
|
mullvad-daemon.service # Mullvad VPN
|
||||||
|
proton.VPN.service # Proton VPN
|
||||||
5
system/pacman/options.conf
Normal file
5
system/pacman/options.conf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Shared pacman [options], included from /etc/pacman.conf.
|
||||||
|
# Installed to /etc/pacman.d/options.conf by system/install.sh.
|
||||||
|
Color
|
||||||
|
#ILoveCandy
|
||||||
|
#VerbosePkgLists
|
||||||
27
system/rebuild-ffmpeg.sh
Executable file
27
system/rebuild-ffmpeg.sh
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rebuilds Arch's ffmpeg package from source so it links against the installed
|
||||||
|
# svt-av1-essential-git instead of the repo svt-av1 it was built with.
|
||||||
|
#
|
||||||
|
# Re-run this after a `pacman -Syu` that updates ffmpeg, otherwise the repo
|
||||||
|
# build replaces this one.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! pacman -Qq svt-av1-essential-git >/dev/null 2>&1; then
|
||||||
|
echo "svt-av1-essential-git is not installed; nothing to rebuild against." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
build="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$build"' EXIT
|
||||||
|
|
||||||
|
git clone --depth 1 https://gitlab.archlinux.org/archlinux/packaging/packages/ffmpeg.git "$build/ffmpeg"
|
||||||
|
cd "$build/ffmpeg"
|
||||||
|
|
||||||
|
# The PKGBUILD verifies a signed ffmpeg tag; makepkg won't fetch the keys itself
|
||||||
|
mapfile -t keys < <(source ./PKGBUILD && printf '%s\n' "${validpgpkeys[@]}")
|
||||||
|
if (( ${#keys[@]} )); then
|
||||||
|
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys "${keys[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# No --needed: the version matches what's installed, we want it replaced anyway
|
||||||
|
makepkg --syncdeps --install --cleanbuild --noconfirm
|
||||||
Reference in New Issue
Block a user