Compare commits
158 Commits
0d582329ed
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ea11fe0eb6 | |||
| 19e7d3f673 | |||
| bf86ffe258 | |||
| 8d73d2a37e | |||
| 54b91a72e2 | |||
| 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 | |||
| 03b043d9d3 | |||
| 1ff92f3b79 | |||
| e5809efc6a | |||
| c64cf5ff12 | |||
| d6f79685f0 | |||
| 9e532023bd | |||
| c2c6675f4b | |||
| fcce5f304b | |||
| 4b0b5c72ba | |||
| 3f4e3514a2 | |||
| 622afb2100 | |||
| 3ae5c57303 | |||
| ef06a96757 | |||
| 03ea182c8c | |||
| ba8a565cda | |||
| c8a9c30fc5 | |||
| 185b752393 | |||
| 8515f3f5ce | |||
| 86921f7aab | |||
| cede2a1690 | |||
| 00e4368abe | |||
| 00d7dcb411 | |||
| 4bdb590899 | |||
| 44e73f5a9d | |||
| 3184c61267 | |||
| fb79f0a37e | |||
| 7a1073646a | |||
| 90d068a878 | |||
| 57e5c0c7b3 | |||
| b7a92c9a59 | |||
| d3856ada0d | |||
| 3601f52600 | |||
| 98f9a77b23 | |||
| bc242ae956 | |||
| 4237b2ed9e | |||
| 7b0cf9934b | |||
| 9df5731ee1 | |||
| 3806935316 | |||
| 516f8eed82 | |||
| 369179c057 | |||
| 322804577f | |||
| 0eee14858b | |||
| 461f4c3fff | |||
| 2f9dfda410 | |||
| e35ceb1450 | |||
| 3d0a18866f | |||
| 6c415d6fbb | |||
| c4b4a3bd26 | |||
| 866ee0bb9b | |||
| e77c08a0a2 | |||
| 491509e864 | |||
| c4604f8e0f | |||
| 30d5f4c926 | |||
| 90d7042995 | |||
| b6714cf20b | |||
| 8877835ae0 | |||
| 32dd10c83d | |||
| f3df5e4f3d | |||
| 74953850fb | |||
| 04e50bad3f | |||
| a218026568 | |||
| 7c557bffc8 | |||
| 2eb122960d | |||
| 8c0347ee35 | |||
| 2935fc7cfe | |||
| 6f9651ccb3 | |||
| c88a96406a | |||
| 13c4f95e08 | |||
| c4f220d95a | |||
| f6d69332db | |||
| 1559da170e | |||
| ca18fe26c1 | |||
| cfd6cf2700 | |||
| 5909ed14cb | |||
| 3487732f46 | |||
| 6aff35bd5b | |||
| 176902616b | |||
| 70cda8d08e | |||
| 28c6a0c3d7 | |||
| fa2ae6f4e2 | |||
| 911182628d | |||
| 308e8440bb | |||
| fad66cf071 | |||
| d7cf9e3da3 | |||
| 03d58f73fb | |||
| a35b3b75a3 | |||
| 040cf83811 | |||
| 07c6d57e74 | |||
| d9e3ae2aee | |||
| 730eff5379 | |||
| c652800b53 | |||
| 86e5d9c95f | |||
| 852bcef6fd | |||
| 3c00586b58 | |||
| c2d6c171ce | |||
| 65aed80fdd | |||
| 9d9fcf7b8e | |||
| dc3749a734 | |||
| 30b871c3c4 | |||
| 1ddbd62ec9 | |||
| 587cc7190c | |||
| 618d3862ec | |||
| 49726e9dbc | |||
| 7f2bfb9c0e | |||
| 929bd53f90 | |||
| de505d9fd2 | |||
| 6b69af3112 | |||
| 9cc22560a5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -0,0 +1 @@
|
||||
dotfiles/.config/hypr/.initial_startup_done
|
||||
35
README.md
35
README.md
@@ -26,6 +26,39 @@ My dotfiles, a constantly evolving set of configurations which I arguably spend
|
||||
too much time tweaking, but they make the command line feel like home, so here
|
||||
we are.
|
||||
|
||||
## 🖥️ Installation
|
||||
|
||||
In order to apply these dotfiles, you must have the SSH Key stored in your password manager,
|
||||
then connect the SSH-Agent to your shell. Once that is done, you will need the application
|
||||
**Stow** in order to keep it synced.
|
||||
|
||||
After this, you will need to git clone the repo in the Projects directory
|
||||
|
||||
```console
|
||||
mkdir Projects
|
||||
git clone ssh://git@gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles.git
|
||||
```
|
||||
|
||||
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
|
||||
stow -d ~/Projects/dotfiles/ -t ~/ dotfiles
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
List of programs and tools I use.
|
||||
@@ -39,7 +72,7 @@ List of programs and tools I use.
|
||||
|📁 File Manager | [thunar](https://wiki.archlinux.org/title/Thunar) |
|
||||
|📝 Editor | [vs code](https://github.com/microsoft/vscode) / [neovim (changing to soon)](https://github.com/neovim/neovim) |
|
||||
|🌐 Browser | [zen](https://github.com/zen-browser/desktop) / [firefox](https://github.com/topics/firefox-browser) / [startpage](https://blog-bucket-personal.s3.eu-west-2.amazonaws.com/Screenshot_02-May_08-44-23_6614.png) |
|
||||
|📊 Bar | [waybar](https://github.com/Alexays/Waybar) |
|
||||
|📊 Bar | [waybar](https://github.com/Alexays/Waybar) / [quickshell](https://quickshell.org/) |
|
||||
|🚀 Launcher | [rofi](https://github.com/davatorium/rofi) |
|
||||
|🔒 Lockscreen | [hyprlock](https://github.com/hyprwm/hyprlock) |
|
||||
|🚪 Login Menu | [sddm](https://github.com/sddm/sddm) |
|
||||
|
||||
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
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
|
||||
"logo": {
|
||||
@@ -49,12 +48,12 @@
|
||||
{
|
||||
"type": "cursor",
|
||||
"key": "│ ├",
|
||||
"keyColor": "blue",
|
||||
"keyColor": "blue"
|
||||
},
|
||||
{
|
||||
"type": "terminalfont",
|
||||
"key": "│ ├",
|
||||
"keyColor": "blue",
|
||||
"keyColor": "blue"
|
||||
},
|
||||
{
|
||||
"type": "terminal",
|
||||
@@ -123,6 +122,6 @@
|
||||
"type": "custom",
|
||||
"format": "\u001b[90m \u001b[31m \u001b[32m \u001b[33m \u001b[34m \u001b[35m \u001b[36m \u001b[37m \u001b[38m \u001b[39m \u001b[39m \u001b[38m \u001b[37m \u001b[36m \u001b[35m \u001b[34m \u001b[33m \u001b[32m \u001b[31m \u001b[90m "
|
||||
},
|
||||
"break",
|
||||
"break"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -47,3 +47,11 @@ regular4=53BDFA # blue
|
||||
regular5=FAE994 # magenta
|
||||
regular6=99faf2 # cyan
|
||||
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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
file:///home/thomas/Projects
|
||||
file:///home/thomas/Documents
|
||||
file:///home/thomas/Downloads
|
||||
file:///home/thomas/Pictures
|
||||
file:///home/thomas/Music
|
||||
file:///home/thomas/Videos
|
||||
smb://192.168.1.84/media/
|
||||
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
|
||||
# See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
|
||||
|
||||
#env = LIBVA_DRIVER_NAME,nvidia
|
||||
#env = __GLX_VENDOR_LIBRARY_NAME,nvidia
|
||||
#env = NVD_BACKEND,direct
|
||||
#env = GSK_RENDERER,ngl
|
||||
env = LIBVA_DRIVER_NAME,nvidia
|
||||
env = __GLX_VENDOR_LIBRARY_NAME,nvidia
|
||||
env = NVD_BACKEND,direct
|
||||
env = GSK_RENDERER,ngl
|
||||
|
||||
# additional ENV's for nvidia. Caution, activate with care
|
||||
#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_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.
|
||||
|
||||
# Bitwarden SSH agent (so GUI apps like VS Code see it, not just shells)
|
||||
env = SSH_AUTH_SOCK,/home/thomas/.bitwarden-ssh-agent.sock
|
||||
|
||||
@@ -74,7 +74,6 @@ bind = $mainMod, K, layoutmsg, cycleprev
|
||||
bind = $mainMod CTRL, Return, layoutmsg, swapwithmaster
|
||||
|
||||
# Dwindle Layout
|
||||
bind = $mainMod SHIFT, I, togglesplit # only works on dwindle layout
|
||||
bind = $mainMod, P, pseudo, # dwindle
|
||||
|
||||
# Special Keys / Hot Keys
|
||||
|
||||
@@ -21,7 +21,7 @@ decoration {
|
||||
rounding = 10
|
||||
|
||||
active_opacity = 1.0
|
||||
inactive_opacity = 0.9
|
||||
inactive_opacity = 1.0
|
||||
fullscreen_opacity = 1.0
|
||||
|
||||
dim_inactive = true
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
# NOTE: some settings are in ~/.config/hypr/UserConfigs/UserDecorAnimations.conf
|
||||
|
||||
dwindle {
|
||||
pseudotile = true
|
||||
preserve_split = true
|
||||
#smart_split = true
|
||||
special_scale_factor = 1
|
||||
@@ -34,7 +33,7 @@ input {
|
||||
kb_rules =
|
||||
repeat_rate = 50
|
||||
repeat_delay = 300
|
||||
sensitivity = 0 #mouse sensitivity
|
||||
sensitivity = 0.6 #mouse sensitivity
|
||||
#accel_profile = # flat or adaptive or blank or EMPTY means libinput’s default mode
|
||||
numlock_by_default = true
|
||||
left_handed = false
|
||||
@@ -77,7 +76,6 @@ gestures {
|
||||
misc {
|
||||
disable_hyprland_logo = true
|
||||
disable_splash_rendering = true
|
||||
vfr = true
|
||||
vrr = 2
|
||||
mouse_move_enables_dpms = true
|
||||
enable_swallow = off
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
# <<<<< TUI AUTO START
|
||||
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: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
|
||||
|
||||
34
dotfiles/.config/hypr/appearance/animations.lua
Normal file
34
dotfiles/.config/hypr/appearance/animations.lua
Normal file
@@ -0,0 +1,34 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- Ported from UserConfigs/UserAnimations.conf (the active preset)
|
||||
--
|
||||
-- The 14 alternative presets from animations/*.conf were also ported, flat, as
|
||||
-- anim-*.lua (see the plan) — none of them are wired in. They now live in ../presets/.
|
||||
-- To switch styles, replace the body of this file with `require("presets.anim-<preset>")`,
|
||||
-- e.g. require("presets.anim-ml4w-classic").
|
||||
|
||||
hl.config({
|
||||
animations = {
|
||||
enabled = true,
|
||||
},
|
||||
})
|
||||
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("smoothOut", { type = "bezier", points = { { 0.5, 0 }, { 0.99, 0.99 } } })
|
||||
hl.curve("smoothIn", { type = "bezier", points = { { 0.5, -0.5 }, { 0.68, 1.5 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "smoothOut", style = "slide" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 100, bezier = "liner", style = "loop" }) -- used by rainbow borders and rotating colors; speed clamped from 180 -> Lua hl.animation caps speed at 100
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "smoothOut" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "overshot" })
|
||||
|
||||
-- animations for -git or version >0.42.0
|
||||
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
|
||||
26
dotfiles/.config/hypr/appearance/colors.lua
Normal file
26
dotfiles/.config/hypr/appearance/colors.lua
Normal file
@@ -0,0 +1,26 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- HyprFlux static color palette
|
||||
-- Ported from colours/hyprflux-colors.conf
|
||||
|
||||
local M = {}
|
||||
|
||||
M.background = "rgb(010102)"
|
||||
M.foreground = "rgb(FDF8FE)"
|
||||
M.color0 = "rgb(313131)"
|
||||
M.color1 = "rgb(09050C)"
|
||||
M.color2 = "rgb(221647)"
|
||||
M.color3 = "rgb(2C1A40)"
|
||||
M.color4 = "rgb(5E3887)"
|
||||
M.color5 = "rgb(7344A6)"
|
||||
M.color6 = "rgb(BAB0BD)"
|
||||
M.color7 = "rgb(F3ECF5)"
|
||||
M.color8 = "rgb(AAA5AC)"
|
||||
M.color9 = "rgb(0B0711)"
|
||||
M.color10 = "rgb(2D1D5F)"
|
||||
M.color11 = "rgb(3B2355)"
|
||||
M.color12 = "rgb(7D4AB4)"
|
||||
M.color13 = "rgb(9A5BDD)"
|
||||
M.color14 = "rgb(F8EAFC)"
|
||||
M.color15 = "rgb(F3ECF5)"
|
||||
|
||||
return M
|
||||
65
dotfiles/.config/hypr/appearance/decorations.lua
Normal file
65
dotfiles/.config/hypr/appearance/decorations.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- Decoration Settings
|
||||
-- Ported from UserConfigs/UserDecorations.conf
|
||||
--
|
||||
-- Hyprland Wiki Links
|
||||
-- Animation - https://wiki.hyprland.org/Configuring/Animations/
|
||||
-- Decoration - https://wiki.hyprland.org/Configuring/Variables/#decoration
|
||||
|
||||
local colors = require("appearance.colors")
|
||||
|
||||
hl.config({
|
||||
general = {
|
||||
border_size = 2,
|
||||
gaps_in = 2,
|
||||
gaps_out = 4,
|
||||
|
||||
col = {
|
||||
active_border = colors.color12,
|
||||
inactive_border = colors.color10,
|
||||
},
|
||||
},
|
||||
|
||||
decoration = {
|
||||
rounding = 10,
|
||||
|
||||
active_opacity = 1.0,
|
||||
inactive_opacity = 1.0,
|
||||
fullscreen_opacity = 1.0,
|
||||
|
||||
dim_inactive = true,
|
||||
dim_strength = 0.1,
|
||||
dim_special = 0.8,
|
||||
|
||||
shadow = {
|
||||
enabled = false,
|
||||
range = 3,
|
||||
render_power = 1,
|
||||
|
||||
color = colors.color12,
|
||||
color_inactive = colors.color10,
|
||||
},
|
||||
|
||||
blur = {
|
||||
enabled = true,
|
||||
size = 6,
|
||||
passes = 2,
|
||||
ignore_opacity = true,
|
||||
new_optimizations = true,
|
||||
special = true,
|
||||
popups = true,
|
||||
},
|
||||
},
|
||||
|
||||
group = {
|
||||
col = {
|
||||
border_active = colors.color15,
|
||||
},
|
||||
|
||||
groupbar = {
|
||||
col = {
|
||||
active = colors.color0,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
90
dotfiles/.config/hypr/core/env.lua
Normal file
90
dotfiles/.config/hypr/core/env.lua
Normal file
@@ -0,0 +1,90 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- Environment variables. See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Environment-variables/
|
||||
-- Ported from UserConfigs/ENVariables.conf
|
||||
|
||||
-- Toolkit Backend Variables
|
||||
hl.env("GDK_BACKEND", "wayland,x11,*")
|
||||
hl.env("QT_QPA_PLATFORM", "wayland;xcb")
|
||||
hl.env("CLUTTER_BACKEND", "wayland")
|
||||
|
||||
-- Run SDL2 applications on Wayland.
|
||||
-- Remove or set to x11 if games that provide older versions of SDL cause compatibility issues
|
||||
-- hl.env("SDL_VIDEODRIVER", "wayland")
|
||||
|
||||
-- xdg Specifications
|
||||
hl.env("XDG_CURRENT_DESKTOP", "Hyprland")
|
||||
hl.env("XDG_SESSION_DESKTOP", "Hyprland")
|
||||
hl.env("XDG_SESSION_TYPE", "wayland")
|
||||
|
||||
-- QT Variables
|
||||
hl.env("QT_AUTO_SCREEN_SCALE_FACTOR", "1")
|
||||
hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1")
|
||||
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
|
||||
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct")
|
||||
|
||||
-- hyprland-qt-support
|
||||
hl.env("QT_QUICK_CONTROLS_STYLE", "org.hyprland.style")
|
||||
|
||||
-- xwayland apps scale fix (useful if you use monitor scaling).
|
||||
-- Set same value if you use scaling in monitors.lua
|
||||
-- 1 is 100% 1.5 is 150%
|
||||
-- see https://wiki.hyprland.org/Configuring/XWayland/
|
||||
hl.env("GDK_SCALE", "1")
|
||||
hl.env("QT_SCALE_FACTOR", "1")
|
||||
|
||||
-- Bibata-Modern-Ice-Cursor
|
||||
-- NOTE! You must have the hyprcursor version to activate this.
|
||||
-- https://wiki.hyprland.org/Hypr-Ecosystem/hyprcursor/
|
||||
hl.env("HYPRCURSOR_THEME", "Bibata-Modern-Classic")
|
||||
hl.env("HYPRCURSOR_SIZE", "24")
|
||||
|
||||
-- firefox
|
||||
hl.env("MOZ_ENABLE_WAYLAND", "1")
|
||||
|
||||
-- electron >28 apps (may help) ##
|
||||
-- https://www.electronjs.org/docs/latest/api/environment-variables
|
||||
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") -- auto selects Wayland if possible, X11 otherwise
|
||||
|
||||
-- NVIDIA
|
||||
-- This is from the Hyprland wiki. Uncomment below if an nvidia GPU is detected.
|
||||
-- See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
|
||||
|
||||
hl.env("LIBVA_DRIVER_NAME", "nvidia")
|
||||
hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia")
|
||||
hl.env("NVD_BACKEND", "direct")
|
||||
hl.env("GSK_RENDERER", "ngl")
|
||||
|
||||
-- additional ENV's for nvidia. Caution, activate with care
|
||||
-- hl.env("GBM_BACKEND", "nvidia-drm")
|
||||
|
||||
-- hl.env("__GL_GSYNC_ALLOWED", "1") -- adaptive Vsync
|
||||
-- hl.env("__NV_PRIME_RENDER_OFFLOAD", "1")
|
||||
-- hl.env("__VK_LAYER_NV_optimus", "NVIDIA_only")
|
||||
-- hl.env("WLR_DRM_NO_ATOMIC", "1")
|
||||
|
||||
-- FOR VM and POSSIBLY NVIDIA
|
||||
-- LIBGL_ALWAYS_SOFTWARE software mesa rendering
|
||||
-- hl.env("LIBGL_ALWAYS_SOFTWARE", "1") -- Warning. May cause hyprland to crash
|
||||
-- hl.env("WLR_RENDERER_ALLOW_SOFTWARE", "1")
|
||||
|
||||
-- nvidia firefox (for hardware acceleration on FF)?
|
||||
-- check this post https://github.com/elFarto/nvidia-vaapi-driver#configuration
|
||||
-- hl.env("MOZ_DISABLE_RDD_SANDBOX", "1")
|
||||
-- hl.env("EGL_PLATFORM", "wayland")
|
||||
|
||||
---- Aquamarine Environment Variables ---- ( Hyprland > 0.45 )
|
||||
-- https://wiki.hyprland.org/Configuring/Environment-variables/#aquamarine-environment-variables
|
||||
-- hl.env("AQ_TRACE", "1") -- Enables more verbose logging.
|
||||
-- hl.env("AQ_DRM_DEVICES", "/dev/dri/card1:/dev/dri/card0") -- explicit list of DRM devices (GPUs) to use.
|
||||
-- hl.env("AQ_MGPU_NO_EXPLICIT", "1") -- Disables explicit syncing on mgpu buffers
|
||||
-- hl.env("AQ_NO_MODIFIERS", "1") -- Disables modifiers for DRM buffers
|
||||
|
||||
---- Hyprland Environment Variables ----
|
||||
-- https://wiki.hyprland.org/Configuring/Environment-variables/#hyprland-environment-variables
|
||||
-- hl.env("HYPRLAND_TRACE", "1") -- Enables more verbose logging.
|
||||
-- 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_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")
|
||||
36
dotfiles/.config/hypr/core/hyprmod.lua
Normal file
36
dotfiles/.config/hypr/core/hyprmod.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
-- Generated by HyprMod
|
||||
-- Ported from hyprland-gui.conf
|
||||
--
|
||||
-- HyprMod isn't currently installed on this machine, so this is a straight syntax port of
|
||||
-- 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.
|
||||
|
||||
-- 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({
|
||||
general = {
|
||||
allow_tearing = true,
|
||||
snap = {
|
||||
enabled = false,
|
||||
},
|
||||
},
|
||||
misc = {
|
||||
vrr = 0,
|
||||
},
|
||||
})
|
||||
|
||||
-- Monitors
|
||||
hl.monitor({ output = "DP-2", mode = "1920x1080@165.00Hz", position = "0x0", scale = 1, bitdepth = 10, cm = "wide", vrr = 0 })
|
||||
|
||||
-- Keybinds
|
||||
-- (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
|
||||
-- to the one already in keybinds.lua, kept here too for parity with the original file)
|
||||
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 + 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)
|
||||
120
dotfiles/.config/hypr/core/settings.lua
Normal file
120
dotfiles/.config/hypr/core/settings.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- User Settings
|
||||
-- Ported from UserConfigs/UserSettings.conf
|
||||
--
|
||||
-- refer to Hyprland wiki for more info https://wiki.hyprland.org/Configuring/Variables/
|
||||
-- NOTE: some settings are in animations.lua / decorations.lua
|
||||
|
||||
hl.config({
|
||||
dwindle = {
|
||||
preserve_split = true,
|
||||
-- smart_split = true,
|
||||
special_scale_factor = 1,
|
||||
},
|
||||
|
||||
master = {
|
||||
new_status = "master",
|
||||
new_on_top = true,
|
||||
mfact = 0.5,
|
||||
},
|
||||
|
||||
general = {
|
||||
resize_on_border = true,
|
||||
layout = "dwindle",
|
||||
},
|
||||
|
||||
input = {
|
||||
kb_layout = "gb",
|
||||
kb_variant = "",
|
||||
kb_model = "",
|
||||
kb_options = "",
|
||||
kb_rules = "",
|
||||
repeat_rate = 50,
|
||||
repeat_delay = 300,
|
||||
sensitivity = 0.6, -- mouse sensitivity
|
||||
-- accel_profile = "", -- flat or adaptive or blank or EMPTY means libinput's default mode
|
||||
numlock_by_default = true,
|
||||
left_handed = false,
|
||||
follow_mouse = 1,
|
||||
float_switch_override_focus = false,
|
||||
|
||||
touchpad = {
|
||||
disable_while_typing = true,
|
||||
natural_scroll = true,
|
||||
clickfinger_behavior = false,
|
||||
middle_button_emulation = false,
|
||||
tap_to_click = true,
|
||||
drag_lock = false,
|
||||
},
|
||||
|
||||
-- below for devices with touchdevice ie. touchscreen
|
||||
touchdevice = {
|
||||
enabled = true,
|
||||
},
|
||||
|
||||
-- below is for tablet, see link above for proper variables
|
||||
tablet = {
|
||||
transform = 0,
|
||||
left_handed = false,
|
||||
},
|
||||
},
|
||||
|
||||
-- "gestures" here only holds the tuning knobs. workspace_swipe/workspace_swipe_fingers
|
||||
-- were removed from this category upstream in favor of hl.gesture() (see below) — and
|
||||
-- were already commented out in the source .conf, so the swipe gesture itself stays
|
||||
-- disabled here too, ported as-is.
|
||||
gestures = {
|
||||
workspace_swipe_distance = 500,
|
||||
workspace_swipe_invert = true,
|
||||
workspace_swipe_min_speed_to_force = 30,
|
||||
workspace_swipe_cancel_ratio = 0.5,
|
||||
workspace_swipe_create_new = true,
|
||||
workspace_swipe_forever = true,
|
||||
-- workspace_swipe_use_r = true, -- uncomment if wanted a forever create a new workspace with swipe right
|
||||
},
|
||||
|
||||
misc = {
|
||||
disable_hyprland_logo = true,
|
||||
disable_splash_rendering = true,
|
||||
vrr = 2,
|
||||
mouse_move_enables_dpms = true,
|
||||
enable_swallow = false,
|
||||
swallow_regex = "^(kitty)$",
|
||||
focus_on_activate = false,
|
||||
initial_workspace_tracking = 0,
|
||||
middle_click_paste = false,
|
||||
enable_anr_dialog = true, -- Application not Responding (ANR)
|
||||
anr_missed_pings = 15, -- ANR Threshold default 1 is too low
|
||||
},
|
||||
|
||||
-- opengl = {
|
||||
-- nvidia_anti_flicker = true,
|
||||
-- },
|
||||
|
||||
binds = {
|
||||
workspace_back_and_forth = true,
|
||||
allow_workspace_cycles = true,
|
||||
pass_mouse_when_bound = false,
|
||||
},
|
||||
|
||||
-- Could help when scaling and not pixelating
|
||||
xwayland = {
|
||||
enabled = true,
|
||||
force_zero_scaling = true,
|
||||
},
|
||||
|
||||
render = {
|
||||
direct_scanout = false,
|
||||
},
|
||||
|
||||
cursor = {
|
||||
sync_gsettings_theme = true,
|
||||
no_hardware_cursors = 2, -- change to 1 if you want to disable
|
||||
enable_hyprcursor = true,
|
||||
warp_on_change_workspace = 2,
|
||||
no_warps = true,
|
||||
},
|
||||
})
|
||||
|
||||
-- Uncomment to actually enable the touchpad workspace-swipe gesture (see the note above):
|
||||
-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
||||
62
dotfiles/.config/hypr/core/startup.lua
Normal file
62
dotfiles/.config/hypr/core/startup.lua
Normal file
@@ -0,0 +1,62 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- Commands and Apps to be executed at launch
|
||||
-- Ported from UserConfigs/Startup_Apps.conf
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Autostart/
|
||||
|
||||
local scriptsDir = os.getenv("HOME") .. "/.config/hypr/scripts"
|
||||
|
||||
hl.on("hyprland.start", function()
|
||||
-- wallpaper stuff
|
||||
hl.exec_cmd("awww-daemon --format xrgb")
|
||||
-- hl.exec_cmd("mpvpaper '*' -o \"load-scripts=no no-audio --loop\" $livewallpaper")
|
||||
|
||||
-- Startup
|
||||
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")
|
||||
-- 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)
|
||||
hl.exec_cmd(scriptsDir .. "/Polkit.sh")
|
||||
|
||||
-- startup apps
|
||||
hl.exec_cmd("nm-applet --indicator")
|
||||
hl.exec_cmd("swaync")
|
||||
-- hl.exec_cmd("ags")
|
||||
-- hl.exec_cmd("blueman-applet")
|
||||
-- hl.exec_cmd("rog-control-center")
|
||||
-- hl.exec_cmd("waybar")
|
||||
hl.exec_cmd("qs") -- quickshell AGS Desktop Overview alternative
|
||||
|
||||
-- clipboard manager
|
||||
hl.exec_cmd("wl-paste --type text --watch cliphist store")
|
||||
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
||||
|
||||
-- Starting hypridle to start hyprlock
|
||||
-- Sunshine streams hold an idle inhibitor (scripts/SunshineIdle.sh) so this never locks mid-stream
|
||||
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)
|
||||
hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh")
|
||||
|
||||
hl.dispatch(hl.dsp.focus({ workspace = 3 }))
|
||||
|
||||
-- NOTE: the trailing "&&" here is a stray leftover from the original .conf (dangling, does nothing) — ported as-is
|
||||
hl.exec_cmd("/usr/bin/Plexamp.AppImage &&")
|
||||
hl.exec_cmd("sleep 3 && vesktop")
|
||||
hl.dispatch(hl.dsp.focus({ workspace = 1 }))
|
||||
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
|
||||
-- hl.exec_cmd("awww-daemon --format xrgb && awww img " .. os.getenv("HOME") .. "/Pictures/wallpapers/mecha-nostalgia.png") -- persistent wallpaper
|
||||
|
||||
-- xdg-desktop-portal-hyprland (should be auto starting. However, you can force to start)
|
||||
-- hl.exec_cmd(scriptsDir .. "/PortalHyprland.sh")
|
||||
end)
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
general:allow_tearing = 1
|
||||
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
|
||||
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
|
||||
unbind = SUPER SHIFT, Print
|
||||
unbind = SUPER, up
|
||||
bind = SUPER, Print, exec, hyprshot -o $HOME/Pictures/Screenshots -m region
|
||||
bind = SUPER, Print, exec, hyprshot -o $HOME/Pictures/Screenshots-Linux -m region
|
||||
bind = SUPER, up, movefocus, u
|
||||
bind = SUPER SHIFT, Print, exec, hyprshot -o $HOME/Pictures/Screenshots -m window
|
||||
bind = SUPER SHIFT, Print, exec, hyprshot -o $HOME/Pictures/Screenshots-Linux -m window
|
||||
|
||||
@@ -31,4 +31,4 @@ source= $UserConfigs/workspaces.conf
|
||||
exec = gsettings set org.gnome.desktop.interface cursor-theme Future-black Cursors
|
||||
|
||||
# HyprMod managed settings
|
||||
source = /home/thomas/.config/hypr/hyprland-gui.conf
|
||||
source = $HOME/.config/hypr/hyprland-gui.conf
|
||||
|
||||
42
dotfiles/.config/hypr/hyprland.lua
Normal file
42
dotfiles/.config/hypr/hyprland.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- Ported from hyprland.conf — see https://wiki.hyprland.org/
|
||||
--
|
||||
-- This mirrors the old source= chain 1:1, just as require()'d Lua modules instead
|
||||
-- of hyprlang includes. See each file for its origin. The presets/ preset library
|
||||
-- (anim-*.lua) is also ported but intentionally not required from here — only the
|
||||
-- currently-active style (appearance/animations.lua) is wired in, exactly like before.
|
||||
--
|
||||
-- Modules live in subfolders sorted by concern (core/, appearance/, layout/, input/,
|
||||
-- presets/) — require() paths below use dotted module names, which Lua resolves to
|
||||
-- the matching subfolder (e.g. require("core.env") -> core/env.lua).
|
||||
|
||||
-- Initial boot script: applies initial wallpapers, theming, new settings etc.
|
||||
-- suggest not changing/deleting this, including the reference file at ~/.config/hypr/.initial_startup_done
|
||||
-- as long as that reference file is present, initial-boot.sh will not execute
|
||||
hl.on("hyprland.start", function()
|
||||
hl.exec_cmd(os.getenv("HOME") .. "/.config/hypr/initial-boot.sh")
|
||||
end)
|
||||
|
||||
-- ## This is where you want to start tinkering
|
||||
require("core.startup") -- put your start-up packages in core/startup.lua
|
||||
require("core.env") -- Environment variables to load
|
||||
require("layout.windowrules") -- all about Hyprland Window Rules and Layer Rules
|
||||
require("appearance.decorations") -- Decorations config
|
||||
require("appearance.animations") -- Animation config
|
||||
require("input.keybinds") -- Put your own keybinds here
|
||||
require("core.settings") -- Main Hyprland Settings
|
||||
|
||||
-- nwg-displays / hyprdynamicmonitors
|
||||
require("layout.monitors")
|
||||
require("layout.workspaces")
|
||||
|
||||
-- NOTE: the original hyprland.conf used a plain `exec` (not `exec-once`) here, so it
|
||||
-- re-ran on every `hyprctl reload`, not just at startup. hl.on("hyprland.start", ...)
|
||||
-- only fires once at actual compositor start — close enough for a one-shot gsettings
|
||||
-- call, but worth knowing if you relied on the reload-time re-apply.
|
||||
hl.on("hyprland.start", function()
|
||||
hl.exec_cmd("gsettings set org.gnome.desktop.interface cursor-theme Future-black Cursors")
|
||||
end)
|
||||
|
||||
-- HyprMod managed settings
|
||||
require("core.hyprmod")
|
||||
@@ -22,7 +22,7 @@ image {
|
||||
path = $HOME/.config/hypr/hyprlock/profile-picture.jpg
|
||||
border_size = 2
|
||||
border_color = rgba(255, 255, 255, 0)
|
||||
size = 300
|
||||
size = 430
|
||||
rounding = -1
|
||||
rotate = 0
|
||||
reload_time = -1
|
||||
@@ -35,11 +35,11 @@ image {
|
||||
# Day-Month-Date
|
||||
label {
|
||||
monitor = DP-2
|
||||
text = cmd[update:1000] echo -e "$(LC_TIME=en_US.UTF-8 date +"%A, %B %d")"
|
||||
text = cmd[update:1000] echo -e "$(LC_TIME=en_US.UTF-8 date +"%A, %d %B %Y")"
|
||||
color = rgba(216, 222, 233, 0.90)
|
||||
font_size = 25
|
||||
font_family = SF Pro Display Semibold
|
||||
position = 0, 400
|
||||
position = 0, 390
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
@@ -51,7 +51,7 @@ label {
|
||||
color = rgba(216, 222, 233, 0.90)
|
||||
font_size = 120
|
||||
font_family = SF Pro Display Bold
|
||||
position = 0, 280
|
||||
position = 0, 300
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
@@ -66,7 +66,7 @@ shape {
|
||||
border_color = rgba(253, 198, 135, 0)
|
||||
rotate = 0
|
||||
xray = false # if true, make a "hole" in the background (rectangle of specified size, no rotation)
|
||||
position = 0, -250
|
||||
position = 0, -280
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
@@ -82,7 +82,7 @@ label {
|
||||
dots_center = true
|
||||
font_size = 18
|
||||
font_family = SF Pro Display Bold
|
||||
position = 0, -250
|
||||
position = 0, -280
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
@@ -102,7 +102,7 @@ input-field {
|
||||
font_family = SF Pro Display Bold
|
||||
placeholder_text = <i><span foreground="##ffffff99">🔒 Enter Pass</span></i>
|
||||
hide_input = false
|
||||
position = 0, -310
|
||||
position = 0, -340
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
@@ -118,3 +118,111 @@ label {
|
||||
halign = center
|
||||
valign = bottom
|
||||
}
|
||||
|
||||
####################################################################################################################
|
||||
# Laptop Screen
|
||||
##################################################################################################################################
|
||||
|
||||
|
||||
# Profie-Photo
|
||||
image {
|
||||
monitor = eDP-1
|
||||
path = $HOME/.config/hypr/hyprlock/profile-picture.jpg
|
||||
border_size = 2
|
||||
border_color = rgba(255, 255, 255, 0)
|
||||
size = 600
|
||||
rounding = -1
|
||||
rotate = 0
|
||||
reload_time = -1
|
||||
reload_cmd =
|
||||
position = 0, 0
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
|
||||
# Day-Month-Date
|
||||
label {
|
||||
monitor = eDP-1
|
||||
text = cmd[update:1000] echo -e "$(LC_TIME=en_US.UTF-8 date +"%A, %d %B %Y")"
|
||||
color = rgba(216, 222, 233, 0.90)
|
||||
font_size = 35
|
||||
font_family = SF Pro Display Semibold
|
||||
position = 0, 550
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
|
||||
# Time
|
||||
label {
|
||||
monitor = eDP-1
|
||||
text = cmd[update:1000] echo "<span>$(date +"%I:%M")</span>"
|
||||
color = rgba(216, 222, 233, 0.90)
|
||||
font_size = 150
|
||||
font_family = SF Pro Display Bold
|
||||
position = 0, 430
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
|
||||
# USER-BOX
|
||||
shape {
|
||||
monitor = eDP-1
|
||||
size = 400, 90
|
||||
color = rgba(255, 255, 255, .1)
|
||||
rounding = -1
|
||||
border_size = 0
|
||||
border_color = rgba(253, 198, 135, 0)
|
||||
rotate = 0
|
||||
xray = false # if true, make a "hole" in the background (rectangle of specified size, no rotation)
|
||||
position = 0, -400
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
|
||||
# USER
|
||||
label {
|
||||
monitor = eDP-1
|
||||
text = $USER
|
||||
color = rgba(216, 222, 233, 0.80)
|
||||
outline_thickness = 2
|
||||
dots_size = 0.2 # Scale of input-field height, 0.2 - 0.8
|
||||
dots_spacing = 0.2 # Scale of dots' absolute size, 0.0 - 1.0
|
||||
dots_center = true
|
||||
font_size = 28
|
||||
font_family = SF Pro Display Bold
|
||||
position = 0, -400
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
|
||||
# INPUT FIELD
|
||||
input-field {
|
||||
monitor = eDP-1
|
||||
size = 400, 90
|
||||
outline_thickness = 2
|
||||
dots_size = 0.2 # Scale of input-field height, 0.2 - 0.8
|
||||
dots_spacing = 0.2 # Scale of dots' absolute size, 0.0 - 1.0
|
||||
dots_center = true
|
||||
outer_color = rgba(0, 0, 0, 0)
|
||||
inner_color = rgba(255, 255, 255, 0.1)
|
||||
font_color = rgb(200, 200, 200)
|
||||
fade_on_empty = false
|
||||
font_family = SF Pro Display Bold
|
||||
placeholder_text = <i><span foreground="##ffffff99">🔒 Enter Pass</span></i>
|
||||
hide_input = false
|
||||
position = 0, -500
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
|
||||
# CURRENT SONG
|
||||
label {
|
||||
monitor = eDP-1
|
||||
text = cmd[update:1000] echo "$(~/.config/hypr/hyprlock/Scripts/songdetail.sh)"
|
||||
color = rgba(255, 255, 255, 0.6)
|
||||
font_size = 28
|
||||
font_family = JetBrains Mono Nerd, SF Pro Display Bold
|
||||
position = 0, 90
|
||||
halign = center
|
||||
valign = bottom
|
||||
}
|
||||
175
dotfiles/.config/hypr/input/keybinds.lua
Normal file
175
dotfiles/.config/hypr/input/keybinds.lua
Normal file
@@ -0,0 +1,175 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- Ported from UserConfigs/Keybinds.conf
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Binds/ and .../Basics/Dispatchers/
|
||||
|
||||
local mainMod = "SUPER"
|
||||
local scriptsDir = os.getenv("HOME") .. "/.config/hypr/scripts"
|
||||
local userScripts = os.getenv("HOME") .. "/.config/hypr/UserScripts"
|
||||
|
||||
-- settings for user default apps
|
||||
local edit = os.getenv("EDITOR") or "nvim"
|
||||
|
||||
-- These two are for UserKeybinds.conf & Waybar Modules
|
||||
local term = "foot" -- Terminal
|
||||
local files = "thunar" -- File Manager
|
||||
|
||||
-- Default Search Engine for ROFI Search (SUPER S)
|
||||
local searchEngine = "https://www.google.com/search?q={}"
|
||||
|
||||
-- common shortcuts
|
||||
hl.bind(mainMod .. " + SPACE", hl.dsp.exec_cmd(".config/rofi/launchers/type-4/launcher.sh")) -- desktop overview (if installed)
|
||||
hl.bind(mainMod .. " + Q", hl.dsp.window.close()) -- close active (not kill)
|
||||
hl.bind(mainMod .. " + SHIFT + Q", hl.dsp.exec_cmd(scriptsDir .. "/KillActiveProcess.sh")) -- Kill active process
|
||||
hl.bind(mainMod .. " + Return", hl.dsp.exec_cmd(term)) -- terminal
|
||||
hl.bind(mainMod .. " + F", hl.dsp.exec_cmd(files)) -- file manager
|
||||
hl.bind(mainMod .. " + B", hl.dsp.exec_cmd("zen-browser")) -- Launch Zen Browser New Everytime
|
||||
hl.bind(mainMod .. " + V", hl.dsp.exec_cmd(scriptsDir .. "/ClipManager.sh")) -- Clipboard Manager
|
||||
hl.bind(mainMod .. " + C", hl.dsp.exec_cmd("code --ozone-platform=x11")) -- Visual Studio Code
|
||||
hl.bind(mainMod .. " + O", hl.dsp.exec_cmd("obsidian --ozone-platform=x11")) -- Obsidian
|
||||
hl.bind(mainMod .. " + X", hl.dsp.exec_cmd("vesktop")) -- Vesktop(Discord)
|
||||
hl.bind(mainMod .. " + M", hl.dsp.exec_cmd("fdm")) -- Free Download Manager
|
||||
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd("waypaper")) -- Wayland Wallpaper Setter
|
||||
hl.bind(mainMod .. " + L", hl.dsp.exec_cmd("hyprlock")) -- Screen Locker
|
||||
hl.bind(mainMod .. " + DELETE", hl.dsp.exec_cmd("~/.config/rofi/powermenu/type-2/powermenu.sh")) -- Power Menu
|
||||
hl.bind(mainMod .. " + S", hl.dsp.exec_cmd("cd /home/thomas/hyprmod && uv run hyprmod")) -- Hyprmod - A Hyprland settings manager (if installed)
|
||||
hl.bind(mainMod .. " + SHIFT + R", hl.dsp.exec_cmd(scriptsDir .. "/Refresh.sh")) -- Refresh waybar, swaync, rofirrr
|
||||
hl.bind(mainMod .. " + N", hl.dsp.exec_cmd("swaync-client -t -sw")) -- swayNC notification panel
|
||||
|
||||
-- FEATURES / EXTRAS
|
||||
hl.bind(mainMod .. " + SHIFT + F", hl.dsp.window.fullscreen({ mode = "fullscreen" })) -- whole full screen
|
||||
hl.bind(mainMod .. " + SHIFT + SPACE", hl.dsp.window.float({ action = "toggle" })) -- Float Mode
|
||||
|
||||
-- Desktop zooming or magnifier
|
||||
hl.bind(mainMod .. " + ALT + mouse_down", hl.dsp.exec_cmd([[hyprctl keyword cursor:zoom_factor "$(hyprctl getoption cursor:zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor * 2.0}')"]]))
|
||||
hl.bind(mainMod .. " + ALT + mouse_up", hl.dsp.exec_cmd([[hyprctl keyword cursor:zoom_factor "$(hyprctl getoption cursor:zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor / 2.0}')"]]))
|
||||
|
||||
-- Waybar / Bar related
|
||||
hl.bind(mainMod .. " + CTRL + ALT + B", hl.dsp.exec_cmd("pkill -SIGUSR1 waybar")) -- Toggle hide/show waybar
|
||||
|
||||
-- FEATURES / EXTRAS (UserScripts)
|
||||
hl.bind(mainMod .. " + SHIFT + A", hl.dsp.exec_cmd(scriptsDir .. "/Animations.sh")) -- hyprland animations menu
|
||||
hl.bind(mainMod .. " + SHIFT + Z", hl.dsp.exec_cmd(userScripts .. "/ZshChangeTheme.sh")) -- Change oh-my-zsh theme
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
-- Default Keybinds
|
||||
--------------------------------------------------------------------------------------
|
||||
|
||||
hl.bind("CTRL + ALT + Delete", hl.dsp.exit()) -- exit Hyprland
|
||||
|
||||
-- Master Layout
|
||||
hl.bind(mainMod .. " + CTRL + D", hl.dsp.layout("removemaster"))
|
||||
hl.bind(mainMod .. " + I", hl.dsp.layout("addmaster"))
|
||||
hl.bind(mainMod .. " + J", hl.dsp.layout("cyclenext"))
|
||||
hl.bind(mainMod .. " + K", hl.dsp.layout("cycleprev"))
|
||||
hl.bind(mainMod .. " + CTRL + Return", hl.dsp.layout("swapwithmaster"))
|
||||
|
||||
-- Dwindle Layout
|
||||
hl.bind(mainMod .. " + P", hl.dsp.window.pseudo()) -- dwindle
|
||||
|
||||
-- Special Keys / Hot Keys
|
||||
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd(scriptsDir .. "/Volume.sh --inc"), { repeating = true, locked = true }) -- volume up
|
||||
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd(scriptsDir .. "/Volume.sh --dec"), { repeating = true, locked = true }) -- volume down
|
||||
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd(scriptsDir .. "/Volume.sh --toggle-mic"), { locked = true }) -- mic mute
|
||||
hl.bind("XF86AudioMute", hl.dsp.exec_cmd(scriptsDir .. "/Volume.sh --toggle"), { locked = true }) -- mute
|
||||
hl.bind("XF86Sleep", hl.dsp.exec_cmd("systemctl suspend"), { locked = true }) -- sleep button
|
||||
hl.bind("XF86RFKill", hl.dsp.exec_cmd(scriptsDir .. "/AirplaneMode.sh"), { locked = true }) -- Airplane mode
|
||||
|
||||
-- media controls using keyboards
|
||||
hl.bind("XF86MediaPlayPause", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --pause"), { locked = true })
|
||||
hl.bind("XF86AudioPause", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --pause"), { locked = true })
|
||||
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --pause"), { locked = true })
|
||||
hl.bind("XF86AudioNext", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --nxt"), { locked = true })
|
||||
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --prv"), { locked = true })
|
||||
hl.bind("XF86AudioStop", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --stop"), { locked = true })
|
||||
|
||||
-- Resize windows
|
||||
hl.bind(mainMod .. " + SHIFT + left", hl.dsp.window.resize({ x = -50, y = 0, relative = true }), { repeating = true })
|
||||
hl.bind(mainMod .. " + SHIFT + right", hl.dsp.window.resize({ x = 50, y = 0, relative = true }), { repeating = true })
|
||||
hl.bind(mainMod .. " + SHIFT + up", hl.dsp.window.resize({ x = 0, y = -50, relative = true }), { repeating = true })
|
||||
hl.bind(mainMod .. " + SHIFT + down", hl.dsp.window.resize({ x = 0, y = 50, relative = true }), { repeating = true })
|
||||
|
||||
-- Move windows
|
||||
hl.bind(mainMod .. " + CTRL + left", hl.dsp.window.move({ direction = "l" }))
|
||||
hl.bind(mainMod .. " + CTRL + right", hl.dsp.window.move({ direction = "r" }))
|
||||
hl.bind(mainMod .. " + CTRL + up", hl.dsp.window.move({ direction = "u" }))
|
||||
hl.bind(mainMod .. " + CTRL + down", hl.dsp.window.move({ direction = "d" }))
|
||||
|
||||
-- Swap windows
|
||||
hl.bind(mainMod .. " + ALT + left", hl.dsp.window.swap({ direction = "l" }))
|
||||
hl.bind(mainMod .. " + ALT + right", hl.dsp.window.swap({ direction = "r" }))
|
||||
hl.bind(mainMod .. " + ALT + up", hl.dsp.window.swap({ direction = "u" }))
|
||||
hl.bind(mainMod .. " + ALT + down", hl.dsp.window.swap({ direction = "d" }))
|
||||
|
||||
-- Move focus with mainMod + arrow keys
|
||||
hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "l" }))
|
||||
hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "r" }))
|
||||
hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "u" }))
|
||||
hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "d" }))
|
||||
|
||||
-- Workspaces related
|
||||
hl.bind(mainMod .. " + tab", hl.dsp.focus({ workspace = "m+1" }))
|
||||
hl.bind(mainMod .. " + SHIFT + tab", hl.dsp.focus({ workspace = "m-1" }))
|
||||
|
||||
-- Special workspace
|
||||
-- hl.bind(mainMod .. " + SHIFT + U", hl.dsp.window.move({ workspace = "special" }))
|
||||
-- hl.bind(mainMod .. " + U", hl.dsp.workspace.toggle_special())
|
||||
hl.bind(mainMod .. " + U", hl.dsp.workspace.toggle_special("nyx"))
|
||||
hl.bind(mainMod .. " + SHIFT + U", hl.dsp.window.move({ workspace = "special:nyx" }))
|
||||
|
||||
-- Number-row keys (the Lua bind parser does not accept the code:NN keycode syntax)
|
||||
-- Switch workspaces with mainMod + [0-9]
|
||||
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
|
||||
hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }))
|
||||
end
|
||||
|
||||
-- Move active window and follow to workspace mainMod + SHIFT + [0-9]
|
||||
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
|
||||
hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i, follow = true }))
|
||||
end
|
||||
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 ]
|
||||
|
||||
-- Move active window to a workspace silently mainMod + CTRL + [0-9]
|
||||
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
|
||||
hl.bind(mainMod .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = i, follow = false }))
|
||||
end
|
||||
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 ]
|
||||
|
||||
-- Scroll through existing workspaces with mainMod + scroll
|
||||
hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
|
||||
hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
|
||||
hl.bind(mainMod .. " + period", hl.dsp.focus({ workspace = "e+1" }))
|
||||
hl.bind(mainMod .. " + comma", hl.dsp.focus({ workspace = "e-1" }))
|
||||
|
||||
-- Move/resize windows with mainMod + LMB/RMB and dragging
|
||||
hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) -- mouse:272 = left click
|
||||
hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) -- mouse:273 = right click
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
-- Laptop specific keybinds
|
||||
--------------------------------------------------------------------------------------
|
||||
|
||||
-- for disabling Touchpad. hyprctl devices to get device name.
|
||||
local touchpadDevice = "asue1209:00-04f3:319f-touchpad"
|
||||
|
||||
hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd(scriptsDir .. "/BrightnessKbd.sh --dec"), { repeating = true }) -- decrease keyboard brightness
|
||||
hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd(scriptsDir .. "/BrightnessKbd.sh --inc"), { repeating = true }) -- increase keyboard brightness
|
||||
hl.bind("XF86Launch1", hl.dsp.exec_cmd("rog-control-center")) -- ASUS Armory crate button
|
||||
hl.bind("XF86Launch3", hl.dsp.exec_cmd("asusctl led-mode -n")) -- FN+F4 Switch keyboard RGB profile
|
||||
hl.bind("XF86Launch4", hl.dsp.exec_cmd("asusctl profile -n")) -- FN+F5 change of fan profiles (Quiet, Balance, Performance)
|
||||
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd(scriptsDir .. "/Brightness.sh --dec"), { repeating = true }) -- decrease monitor brightness
|
||||
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(scriptsDir .. "/Brightness.sh --inc"), { repeating = true }) -- increase monitor brightness
|
||||
hl.bind("XF86TouchpadToggle", hl.dsp.exec_cmd(scriptsDir .. "/TouchPad.sh")) -- disable touchpad
|
||||
|
||||
-- Screenshot keybindings using F6 (no PrintSrc button)
|
||||
-- hl.bind(mainMod .. " + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --now")) -- screenshot
|
||||
-- hl.bind(mainMod .. " + SHIFT + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --area")) -- screenshot (area)
|
||||
-- hl.bind(mainMod .. " + CTRL + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --in5")) -- screenshot (5 secs delay)
|
||||
-- hl.bind(mainMod .. " + ALT + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --in10")) -- screenshot (10 secs delay)
|
||||
-- hl.bind("ALT + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --active")) -- screenshot (active window only)
|
||||
|
||||
hl.device({
|
||||
name = touchpadDevice,
|
||||
enabled = true,
|
||||
})
|
||||
15
dotfiles/.config/hypr/layout/monitors.lua
Normal file
15
dotfiles/.config/hypr/layout/monitors.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Generated with hyprdynamicmonitors freeze.
|
||||
-- This is a template that you can edit, it is just a starter that pulled your current monitor setup,
|
||||
-- adjust as needed.
|
||||
-- You can use templates here etc, see https://github.com/fiffeek/hyprdynamicmonitors/blob/main/examples/basic/hyprconfigs/template.go.tmpl.
|
||||
-- Monitors are given arbitrary tags (the "monitor" prefix and the ID coming from hyprland).
|
||||
-- If you are using the TUI to edit, leave this at the end of the file (the last monitor config applies)
|
||||
-- and leave the markers.
|
||||
-- Ported from UserConfigs/monitors.conf — hyprdynamicmonitors already supports writing this file
|
||||
-- directly as monitors.lua once hyprland.lua is active, so re-running it should stay compatible.
|
||||
-- <<<<< 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: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 = 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
|
||||
203
dotfiles/.config/hypr/layout/windowrules.lua
Normal file
203
dotfiles/.config/hypr/layout/windowrules.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- For window rules and layer rules
|
||||
-- Ported from UserConfigs/WindowRules.conf
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Window-Rules/
|
||||
|
||||
-- windowrule - tags - add apps under appropriate tag to use the same settings
|
||||
-- browser tags
|
||||
hl.window_rule({ name = "windowrule-1", tag = "+browser", match = { class = "^([Ff]irefox|org.mozilla.firefox|[Ff]irefox-esr|[Ff]irefox-bin)$" } })
|
||||
hl.window_rule({ name = "rustdesk", tag = "+rustdesk", match = { class = "^(rustdesk)$", xwayland = true } })
|
||||
hl.window_rule({ name = "windowrule-2", tag = "+browser", match = { class = "^([Gg]oogle-chrome(-beta|-dev|-unstable)?)$" } })
|
||||
hl.window_rule({ name = "windowrule-3", tag = "+browser", match = { class = "^(chrome-.+-Default)$" } }) -- Chrome PWAs
|
||||
-- hl.window_rule({ name = "browser-chromium", tag = "+browser", match = { class = "^([Cc]hromium)$" } })
|
||||
hl.window_rule({ name = "windowrule-4", tag = "+browser", match = { class = "^([Mm]icrosoft-edge(-stable|-beta|-dev|-unstable))$" } })
|
||||
hl.window_rule({ name = "windowrule-5", tag = "+browser", match = { class = "^(Brave-browser(-beta|-dev|-unstable)?)$" } })
|
||||
hl.window_rule({ name = "windowrule-6", tag = "+browser", match = { class = "^([Tt]horium-browser|[Cc]achy-browser)$" } })
|
||||
hl.window_rule({ name = "windowrule-7", tag = "+browser", match = { class = "^(zen-alpha|zen)$" } })
|
||||
|
||||
-- notif tags
|
||||
hl.window_rule({ name = "windowrule-8", tag = "+notif", match = { class = "^(swaync-control-center|swaync-notification-window|swaync-client|class)$" } })
|
||||
|
||||
-- HyprFlux settings tag
|
||||
hl.window_rule({ name = "windowrule-9", tag = "+HyprFlux_Cheat", match = { title = "^(HyprFlux Quick Cheat Sheet)$" } })
|
||||
hl.window_rule({ name = "windowrule-10", tag = "+HyprFlux_Settings", match = { title = "^(HyprFlux Settings)$" } })
|
||||
hl.window_rule({ name = "windowrule-11", tag = "+HyprFlux-Settings", match = { class = "^(nwg-displays|nwg-look)$" } })
|
||||
|
||||
-- terminal tags
|
||||
hl.window_rule({ name = "windowrule-12", tag = "+terminal", match = { class = "^(Alacritty|kitty|kitty-dropterm)$" } })
|
||||
|
||||
-- email tags
|
||||
hl.window_rule({ name = "windowrule-13", tag = "+email", match = { class = "^([Tt]hunderbird|org.gnome.Evolution)$" } })
|
||||
hl.window_rule({ name = "windowrule-14", tag = "+email", match = { class = "^(eu.betterbird.Betterbird)$" } })
|
||||
|
||||
-- project tags
|
||||
hl.window_rule({ name = "windowrule-15", tag = "+projects", match = { class = "^(codium|codium-url-handler|VSCodium)$" } })
|
||||
hl.window_rule({ name = "windowrule-16", tag = "+projects", match = { class = "^(VSCode|code-url-handler)$" } })
|
||||
hl.window_rule({ name = "windowrule-17", tag = "+projects", match = { class = "^(jetbrains-.+)$" } }) -- JetBrains IDEs
|
||||
|
||||
-- screenshare tags
|
||||
hl.window_rule({ name = "windowrule-18", tag = "+screenshare", match = { class = "^(com.obsproject.Studio)$" } })
|
||||
|
||||
-- IM tags
|
||||
-- hl.window_rule({ tag = "+im", match = { class = "^([Dd]iscord|[Ww]ebCord|[Vv]esktop)$" } })
|
||||
hl.window_rule({
|
||||
name = "windowrule-19",
|
||||
tag = "+im",
|
||||
center = true,
|
||||
float = true,
|
||||
size = "monitor_w*0.6 monitor_h*0.7",
|
||||
match = { class = "^([Ff]erdium)$" },
|
||||
})
|
||||
hl.window_rule({ name = "windowrule-20", tag = "+im", match = { class = "^([Ww]hatsapp-for-linux)$" } })
|
||||
hl.window_rule({ name = "windowrule-21", tag = "+im", match = { class = "^(ZapZap|com.rtosta.zapzap)$" } })
|
||||
hl.window_rule({ name = "windowrule-22", tag = "+im", match = { class = "^(org.telegram.desktop|io.github.tdesktop_x64.TDesktop)$" } })
|
||||
hl.window_rule({ name = "windowrule-23", tag = "+im", match = { class = "^(teams-for-linux)$" } })
|
||||
hl.window_rule({ name = "windowrule-24", tag = "+im", match = { class = "^(im.riot.Riot|Element)$" } }) -- Element Matrix client
|
||||
|
||||
-- game tags
|
||||
hl.window_rule({ name = "windowrule-25", tag = "+games", match = { class = "^(gamescope)$" } })
|
||||
hl.window_rule({ name = "windowrule-26", tag = "+games", match = { class = "^(steam_app_\\d+)$" } })
|
||||
|
||||
-- gamestore tags
|
||||
hl.window_rule({ name = "windowrule-27", tag = "+gamestore", match = { class = "^([Ss]team)$" } })
|
||||
hl.window_rule({ name = "windowrule-28", tag = "+gamestore", match = { title = "^([Ll]utris)$" } })
|
||||
hl.window_rule({ name = "windowrule-29", tag = "+gamestore", match = { class = "^(com.heroicgameslauncher.hgl)$" } })
|
||||
|
||||
-- file-manager tags
|
||||
hl.window_rule({ name = "windowrule-30", tag = "+file-manager", match = { class = "^([Tt]hunar|org.gnome.Nautilus|[Pp]cmanfm-qt)$" } })
|
||||
hl.window_rule({ name = "windowrule-31", tag = "+file-manager", match = { class = "^(app.drey.Warp)$" } })
|
||||
|
||||
-- wallpaper tags
|
||||
hl.window_rule({ name = "windowrule-32", tag = "+wallpaper", match = { class = "^([Ww]aytrogen)$" } })
|
||||
|
||||
-- multimedia tags
|
||||
hl.window_rule({ name = "windowrule-33", tag = "+multimedia", match = { class = "^([Aa]udacious)$" } })
|
||||
|
||||
-- multimedia-video tags
|
||||
hl.window_rule({ name = "windowrule-34", tag = "+multimedia_video", match = { class = "^([Mm]pv|vlc)$" } })
|
||||
|
||||
-- settings tags
|
||||
hl.window_rule({ name = "windowrule-35", tag = "+settings", center = true, match = { title = "^(ROG Control)$" } })
|
||||
hl.window_rule({ name = "windowrule-36", tag = "+settings", match = { class = "^(wihotspot(-gui)?)$" } }) -- wifi hotspot
|
||||
hl.window_rule({ name = "windowrule-37", tag = "+settings", match = { class = "^([Bb]aobab|org.gnome.[Bb]aobab)$" } }) -- Disk usage analyzer
|
||||
hl.window_rule({ name = "windowrule-38", tag = "+settings", match = { class = "^(gnome-disks|wihotspot(-gui)?)$" } })
|
||||
hl.window_rule({ name = "windowrule-39", tag = "+settings", match = { title = "(Kvantum Manager)" } })
|
||||
hl.window_rule({ name = "windowrule-40", tag = "+settings", match = { class = "^(file-roller|org.gnome.FileRoller)$" } }) -- archive manager
|
||||
hl.window_rule({ name = "windowrule-41", tag = "+settings", match = { class = "^(nm-applet|nm-connection-editor|blueman-manager)$" } })
|
||||
hl.window_rule({ name = "windowrule-42", tag = "+settings", center = true, match = { class = "^(pavucontrol|org.pulseaudio.pavucontrol|com.saivert.pwvucontrol)$" } })
|
||||
hl.window_rule({ name = "windowrule-43", tag = "+settings", match = { class = "^(qt5ct|qt6ct|[Yy]ad)$" } })
|
||||
hl.window_rule({ name = "windowrule-44", tag = "+settings", match = { class = "(xdg-desktop-portal-gtk)" } })
|
||||
hl.window_rule({ name = "windowrule-45", tag = "+settings", match = { class = "^(org.kde.polkit-kde-authentication-agent-1)$" } })
|
||||
hl.window_rule({ name = "windowrule-46", tag = "+settings", match = { class = "^([Rr]ofi)$" } })
|
||||
|
||||
-- Custom rule
|
||||
hl.window_rule({ name = "windowrule-47", tile = true, match = { class = "^([Cc]hromium)$" } })
|
||||
hl.window_rule({ name = "windowrule-48", size = "monitor_w*0.25 monitor_h*0.55", float = true, match = { class = "^(com.network.manager)$" } })
|
||||
hl.window_rule({ name = "windowrule-49", workspace = "special:nyx", match = { class = "^([Vv]esktop|[Dd]iscord|[Ss]potify)$" } })
|
||||
hl.window_rule({ name = "windowrule-255", size = "monitor_w*0.75 monitor_h*0.80", float = true, match = { class = "^([Bb]itwarden)$" } })
|
||||
|
||||
-- viewer tags
|
||||
hl.window_rule({ name = "windowrule-50", tag = "+viewer", match = { class = "^(gnome-system-monitor|org.gnome.SystemMonitor|io.missioncenter.MissionCenter)$" } }) -- system monitor
|
||||
hl.window_rule({ name = "windowrule-51", tag = "+viewer", match = { class = "^(evince)$" } }) -- document viewer
|
||||
hl.window_rule({ name = "windowrule-52", tag = "+viewer", match = { class = "^(eog|org.gnome.Loupe)$" } }) -- image viewer
|
||||
|
||||
-- Some special override rules
|
||||
hl.window_rule({ name = "windowrule-53", no_blur = true, opacity = "1.0", match = { tag = "multimedia_video*" } })
|
||||
|
||||
-- POSITION
|
||||
-- hl.window_rule({ center = true, match = { float = true } }) -- warning, it cause even the menu to float and center.
|
||||
hl.window_rule({ name = "windowrule-54", center = true, float = true, size = "monitor_w*0.65 monitor_h*0.9", match = { tag = "HyprFlux_Cheat*" } })
|
||||
hl.window_rule({ name = "windowrule-55", center = true, float = true, match = { class = "([Tt]hunar)", title = "negative:(.*[Tt]hunar.*)" } })
|
||||
hl.window_rule({ name = "windowrule-56", center = true, float = true, match = { tag = "HyprFlux-Settings*" } })
|
||||
hl.window_rule({ name = "windowrule-57", center = true, match = { title = "^(Keybindings)$" } })
|
||||
hl.window_rule({ name = "windowrule-58", center = true, size = "monitor_w*0.6 monitor_h*0.7", match = { class = "^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$" } })
|
||||
hl.window_rule({
|
||||
name = "windowrule-59",
|
||||
move = "monitor_w*0.61 monitor_h*0.07",
|
||||
float = true,
|
||||
size = "monitor_w*0.38 monitor_h*0.38",
|
||||
pin = true,
|
||||
keep_aspect_ratio = true,
|
||||
match = { title = "^(Picture-in-Picture)$" },
|
||||
})
|
||||
|
||||
-- hl.window_rule({ move = "72% 7%", match = { title = "^(Firefox)$" } })
|
||||
|
||||
-- windowrule to avoid idle for fullscreen apps
|
||||
-- hl.window_rule({ idle_inhibit = "fullscreen", match = { class = "^(*)$" } })
|
||||
-- hl.window_rule({ idle_inhibit = "fullscreen", match = { title = "^(*)$" } })
|
||||
hl.window_rule({ name = "windowrule-60", idle_inhibit = "fullscreen", match = { fullscreen = true } })
|
||||
|
||||
-- Discord to workspace 3
|
||||
-- NOTE: these two used the old single-line windowrule syntax in the source .conf (match packed
|
||||
-- oddly with the rule); windowrule-49 above already sends the same apps to special:nyx, so this
|
||||
-- looks like a pre-existing conflict in the original config rather than something introduced here —
|
||||
-- ported literally, last-defined-wins as before.
|
||||
hl.window_rule({ workspace = "3", match = { class = "^([Vv]esktop|[Dd]iscord|[Ss]potify|[Pp]lexamp)$" } })
|
||||
hl.window_rule({ size = "20% 20%", match = { class = "^([Pp]lexamp)" } })
|
||||
|
||||
-- FLOAT
|
||||
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.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-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-80", float = true, match = { class = "^(mpv|com.github.rafostar.Clapper)$" } })
|
||||
hl.window_rule({ name = "windowrule-81", float = true, match = { class = "^([Qq]alculate-gtk)$" } })
|
||||
-- hl.window_rule({ float = true, match = { class = "^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$" } })
|
||||
-- hl.window_rule({ float = true, match = { title = "^(Firefox)$" } })
|
||||
hl.window_rule({ name = "windowrule-82", float = true, match = { class = "^(fdm|freedownloadmanager)$" } })
|
||||
hl.window_rule({ name = "windowrule-83", float = true, match = { class = "^(proton-authenticator)$" } })
|
||||
hl.window_rule({ name = "windowrule-84", float = true, match = { class = "^([Ww]indscribe)$" } })
|
||||
|
||||
-- windowrule - ######### float popups and dialogue #######
|
||||
hl.window_rule({ name = "windowrule-85", float = true, center = true, match = { title = "^(Authentication Required)$" } })
|
||||
hl.window_rule({ name = "windowrule-86", float = true, match = { class = "(codium|codium-url-handler|VSCodium)", title = "negative:(.*codium.*|.*VSCodium.*)" } })
|
||||
hl.window_rule({ name = "windowrule-87", float = true, match = { class = "^(com.heroicgameslauncher.hgl)$", title = "negative:(Heroic Games Launcher)" } })
|
||||
hl.window_rule({ name = "windowrule-88", float = true, match = { class = "^([Ss]team)$", title = "negative:^([Ss]team)$" } })
|
||||
hl.window_rule({ name = "windowrule-89", float = true, size = "monitor_w*0.7 monitor_h*0.6", center = true, match = { title = "^(Add Folder to Workspace)$" } })
|
||||
hl.window_rule({ name = "windowrule-90", float = true, size = "monitor_w*0.7 monitor_h*0.6", center = true, match = { title = "^(Save As)$" } })
|
||||
hl.window_rule({ name = "windowrule-91", float = true, size = "monitor_w*0.7 monitor_h*0.6", match = { initial_title = "(Open Files)" } })
|
||||
hl.window_rule({ name = "windowrule-92", float = true, center = true, size = "monitor_w*0.16 monitor_h*0.12", match = { title = "^(SDDM Background)$" } }) -- HyprFlux YAD for setting SDDM background
|
||||
-- END of float popups and dialogue #######
|
||||
|
||||
-- OPACITY
|
||||
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "browser*" } })
|
||||
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "projects*" } })
|
||||
-- hl.window_rule({ opacity = "0.94 0.94", match = { tag = "im*" } })
|
||||
-- hl.window_rule({ opacity = "0.94 0.94", match = { tag = "multimedia*" } })
|
||||
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "file-manager*" } })
|
||||
hl.window_rule({ name = "windowrule-93", opacity = "0.8 0.8", match = { tag = "terminal*" } })
|
||||
-- hl.window_rule({ opacity = "0.82 0.82", match = { tag = "viewer*" } })
|
||||
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.9", match = { class = "^(deluge)$" } })
|
||||
hl.window_rule({ name = "windowrule-96", opacity = "0.9 0.9", match = { class = "^(seahorse)$" } }) -- gnome-keyring gui
|
||||
-- hl.window_rule({ opacity = "0.95 0.95", match = { title = "^(Picture-in-Picture)$" } })
|
||||
|
||||
-- SIZE
|
||||
-- hl.window_rule({ size = "25% 25%", match = { title = "^(Firefox)$" } })
|
||||
|
||||
-- PINNING
|
||||
-- hl.window_rule({ pin = true, match = { title = "^(Firefox)$" } })
|
||||
|
||||
-- windowrule - extras
|
||||
|
||||
-- BLUR & FULLSCREEN
|
||||
-- hl.window_rule({ active_border_color = "rgb(EE4B55)", inactive_border_color = "rgb(880808)", match = { fullscreen = true } })
|
||||
-- hl.window_rule({ active_border_color = "rgb(282737)", inactive_border_color = "rgb(1E1D2D)", match = { float = true } })
|
||||
-- hl.window_rule({ opacity = "0.8 0.8", match = { pin = true } })
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
-- LAYER RULES
|
||||
--------------------------------------------------------------------------------------
|
||||
|
||||
hl.layer_rule({ name = "layerrule-1", blur = true, ignore_alpha = 0, match = { namespace = "rofi" } })
|
||||
hl.layer_rule({ name = "layerrule-2", blur = true, ignore_alpha = 0, match = { namespace = "notifications" } })
|
||||
hl.layer_rule({ name = "layerrule-3", blur = true, ignore_alpha = 0.5, match = { namespace = "quickshell:overview" } })
|
||||
|
||||
-- hl.layer_rule({ ignore_alpha = 0.5, match = { tag = "notif*" } })
|
||||
-- hl.layer_rule({ no_anim = true (unset), match = { class = "^([Rr]ofi)$" } })
|
||||
-- hl.layer_rule({ blur = true, match = { class = "^([Rr]ofi)$" } })
|
||||
-- hl.layer_rule({ ignore_alpha = 0, match = { class = "<rofi>" } })
|
||||
-- hl.layer_rule({ ignore_alpha = 0, match = { namespace = "overview" } })
|
||||
-- hl.layer_rule({ blur = true, match = { namespace = "overview" } })
|
||||
30
dotfiles/.config/hypr/layout/workspaces.lua
Normal file
30
dotfiles/.config/hypr/layout/workspaces.lua
Normal file
@@ -0,0 +1,30 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- NOTE: This will be overwritten by NWG-Displays
|
||||
-- once you use it and click apply.
|
||||
-- (nwg-displays already supports writing this file directly as workspaces.lua once
|
||||
-- hyprland.lua is active, so re-running it should stay compatible.)
|
||||
--
|
||||
-- Ported from UserConfigs/workspaces.conf
|
||||
-- You can set workspace rules to achieve workspace-specific behaviors.
|
||||
-- For instance, you can define a workspace where all windows are drawn without borders or gaps.
|
||||
-- https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/
|
||||
|
||||
-- Assigning workspace to a certain monitor. Below are just examples
|
||||
hl.workspace_rule({ workspace = 1, monitor = "DP-2", 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 = 9, monitor = "HEADLESS-1", default = true })
|
||||
-- hl.workspace_rule({ workspace = 4, monitor = "eDP-1" })
|
||||
-- hl.workspace_rule({ workspace = 5, monitor = "DP-2" })
|
||||
-- hl.workspace_rule({ workspace = 6, monitor = "DP-2" })
|
||||
-- hl.workspace_rule({ workspace = 7, monitor = "DP-2" })
|
||||
-- hl.workspace_rule({ workspace = 8, monitor = "DP-2" })
|
||||
|
||||
-- example rules (from wiki)
|
||||
-- hl.workspace_rule({ workspace = 3, rounding = false, decorate = false })
|
||||
-- hl.workspace_rule({ workspace = "name:coding", rounding = false, decorate = false, gaps_in = 0, gaps_out = 0, border = false, monitor = "DP-1" })
|
||||
-- hl.workspace_rule({ workspace = 8, border_size = 8 })
|
||||
-- hl.workspace_rule({ workspace = "name:Hello", monitor = "DP-1", default = true })
|
||||
-- hl.workspace_rule({ workspace = "name:gaming", monitor = "desc:Chimei Innolux Corporation 0x150C", default = true })
|
||||
-- hl.workspace_rule({ workspace = 5, on_created_empty = "[float] firefox" })
|
||||
-- hl.workspace_rule({ workspace = "special:scratchpad", on_created_empty = "foot" })
|
||||
22
dotfiles/.config/hypr/presets/anim-00-default.lua
Normal file
22
dotfiles/.config/hypr/presets/anim-00-default.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- Ported from animations/00-default.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "smoothOut", style = "slide" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 100, bezier = "liner", style = "loop" }) -- used by rainbow borders and rotating colors; speed clamped from 180 -> Lua hl.animation caps speed at 100
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "smoothOut" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "overshot" })
|
||||
|
||||
-- animations for -git or version >0.42.0
|
||||
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
|
||||
25
dotfiles/.config/hypr/presets/anim-01-default-v2.lua
Normal file
25
dotfiles/.config/hypr/presets/anim-01-default-v2.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- old animations
|
||||
-- Ported from animations/01-default - v2.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("myBezier", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("linear", { type = "bezier", points = { { 0.0, 0.0 }, { 1.0, 1.0 } } })
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("slow", { type = "bezier", points = { { 0, 0.85 }, { 0.3, 1 } } })
|
||||
hl.curve("overshot", { type = "bezier", points = { { 0.7, 0.6 }, { 0.1, 1.1 } } })
|
||||
hl.curve("bounce", { type = "bezier", points = { { 1.1, 1.6 }, { 0.1, 0.85 } } })
|
||||
hl.curve("sligshot", { type = "bezier", points = { { 1, -1 }, { 0.15, 1.25 } } })
|
||||
hl.curve("nice", { type = "bezier", points = { { 0, 6.9 }, { 0.5, -4.20 } } })
|
||||
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "slow", style = "popin" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "popin" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "linear" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 100, bezier = "linear", style = "loop" }) -- used by rainbow borders and rotating colors; speed clamped from 180 -> Lua hl.animation caps speed at 100
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 5, bezier = "overshot" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 5, bezier = "bounce", style = "popin" })
|
||||
4
dotfiles/.config/hypr/presets/anim-03-disable.lua
Normal file
4
dotfiles/.config/hypr/presets/anim-03-disable.lua
Normal file
@@ -0,0 +1,4 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- Ported from animations/03- Disable Animation.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = false } })
|
||||
39
dotfiles/.config/hypr/presets/anim-end-4.lua
Normal file
39
dotfiles/.config/hypr/presets/anim-end-4.lua
Normal file
@@ -0,0 +1,39 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "End-4"
|
||||
-- credit https://github.com/end-4/dots-hyprland
|
||||
-- Ported from animations/END-4.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
-- Animation curves
|
||||
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
|
||||
hl.curve("md3_standard", { type = "bezier", points = { { 0.2, 0 }, { 0, 1 } } })
|
||||
hl.curve("md3_decel", { type = "bezier", points = { { 0.05, 0.7 }, { 0.1, 1 } } })
|
||||
hl.curve("md3_accel", { type = "bezier", points = { { 0.3, 0 }, { 0.8, 0.15 } } })
|
||||
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.1 } } })
|
||||
hl.curve("crazyshot", { type = "bezier", points = { { 0.1, 1.5 }, { 0.76, 0.92 } } })
|
||||
hl.curve("hyprnostretch", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.0 } } })
|
||||
hl.curve("menu_decel", { type = "bezier", points = { { 0.1, 1 }, { 0, 1 } } })
|
||||
hl.curve("menu_accel", { type = "bezier", points = { { 0.38, 0.04 }, { 1, 0.07 } } })
|
||||
hl.curve("easeInOutCirc", { type = "bezier", points = { { 0.85, 0 }, { 0.15, 1 } } })
|
||||
hl.curve("easeOutCirc", { type = "bezier", points = { { 0, 0.55 }, { 0.45, 1 } } })
|
||||
hl.curve("easeOutExpo", { type = "bezier", points = { { 0.16, 1 }, { 0.3, 1 } } })
|
||||
hl.curve("softAcDecel", { type = "bezier", points = { { 0.26, 0.26 }, { 0.15, 1 } } })
|
||||
hl.curve("md2", { type = "bezier", points = { { 0.4, 0 }, { 0.2, 1 } } }) -- use with .2s duration
|
||||
|
||||
-- Animation configs
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 3, bezier = "md3_decel", style = "popin 60%" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 3, bezier = "md3_decel", style = "popin 60%" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "md3_accel", style = "popin 60%" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "md3_decel" })
|
||||
-- hl.animation({ leaf = "layers", enabled = true, speed = 2, bezier = "md3_decel", style = "slide" })
|
||||
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "menu_decel", style = "slide" })
|
||||
hl.animation({ leaf = "layersOut", enabled = true, speed = 1.6, bezier = "menu_accel" })
|
||||
hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 2, bezier = "menu_decel" })
|
||||
hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 4.5, bezier = "menu_accel" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 7, bezier = "menu_decel", style = "slide" })
|
||||
-- hl.animation({ leaf = "workspaces", enabled = true, speed = 2.5, bezier = "softAcDecel", style = "slide" })
|
||||
-- hl.animation({ leaf = "workspaces", enabled = true, speed = 7, bezier = "menu_decel", style = "slidefade 15%" })
|
||||
-- hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "md3_decel", style = "slidefadevert 15%" })
|
||||
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "md3_decel", style = "slidevert" })
|
||||
21
dotfiles/.config/hypr/presets/anim-hyde-default.lua
Normal file
21
dotfiles/.config/hypr/presets/anim-hyde-default.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Default"
|
||||
-- credit https://github.com/prasanthrangan/hyprdots
|
||||
-- Ported from animations/HYDE - default.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "once" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
|
||||
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 5, bezier = "wind", style = "slidevert" })
|
||||
20
dotfiles/.config/hypr/presets/anim-hyde-minimal-1.lua
Normal file
20
dotfiles/.config/hypr/presets/anim-hyde-minimal-1.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Minimal-1"
|
||||
-- credit https://github.com/prasanthrangan/hyprdots
|
||||
-- Ported from animations/HYDE - minimal-1.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "once" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
|
||||
14
dotfiles/.config/hypr/presets/anim-hyde-minimal-2.lua
Normal file
14
dotfiles/.config/hypr/presets/anim-hyde-minimal-2.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Minimal-2"
|
||||
-- credit https://github.com/prasanthrangan/hyprdots
|
||||
-- Ported from animations/HYDE - minimal-2.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("quart", { type = "bezier", points = { { 0.25, 1 }, { 0.5, 1 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "quart", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 6, bezier = "quart" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 6, bezier = "quart" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 6, bezier = "quart" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "quart" })
|
||||
40
dotfiles/.config/hypr/presets/anim-hyde-optimized.lua
Normal file
40
dotfiles/.config/hypr/presets/anim-hyde-optimized.lua
Normal file
@@ -0,0 +1,40 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Optimized"
|
||||
-- credit https://github.com/prasanthrangan/hyprdots
|
||||
-- Ported from animations/HYDE - optimized.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.85 }, { 0.03, 0.97 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.07, 0.88 }, { 0.04, 0.99 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.20, -0.15 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
hl.curve("md3_standard", { type = "bezier", points = { { 0.12, 0 }, { 0, 1 } } })
|
||||
hl.curve("md3_decel", { type = "bezier", points = { { 0.05, 0.80 }, { 0.10, 0.97 } } })
|
||||
hl.curve("md3_accel", { type = "bezier", points = { { 0.20, 0 }, { 0.80, 0.08 } } })
|
||||
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.85 }, { 0.07, 1.04 } } })
|
||||
hl.curve("crazyshot", { type = "bezier", points = { { 0.1, 1.22 }, { 0.68, 0.98 } } })
|
||||
hl.curve("hyprnostretch", { type = "bezier", points = { { 0.05, 0.82 }, { 0.03, 0.94 } } })
|
||||
hl.curve("menu_decel", { type = "bezier", points = { { 0.05, 0.82 }, { 0, 1 } } })
|
||||
hl.curve("menu_accel", { type = "bezier", points = { { 0.20, 0 }, { 0.82, 0.10 } } })
|
||||
hl.curve("easeInOutCirc", { type = "bezier", points = { { 0.75, 0 }, { 0.15, 1 } } })
|
||||
hl.curve("easeOutCirc", { type = "bezier", points = { { 0, 0.48 }, { 0.38, 1 } } })
|
||||
hl.curve("easeOutExpo", { type = "bezier", points = { { 0.10, 0.94 }, { 0.23, 0.98 } } })
|
||||
hl.curve("softAcDecel", { type = "bezier", points = { { 0.20, 0.20 }, { 0.15, 1 } } })
|
||||
hl.curve("md2", { type = "bezier", points = { { 0.30, 0 }, { 0.15, 1 } } })
|
||||
|
||||
hl.curve("OutBack", { type = "bezier", points = { { 0.28, 1.40 }, { 0.58, 1 } } })
|
||||
hl.curve("easeInOutCirc", { type = "bezier", points = { { 0.78, 0 }, { 0.15, 1 } } })
|
||||
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1.6, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 82, bezier = "liner", style = "once" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 3.2, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 2.8, bezier = "easeOutCirc" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 3.0, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 1.8, bezier = "md3_decel" })
|
||||
hl.animation({ leaf = "layersIn", enabled = true, speed = 1.8, bezier = "menu_decel", style = "slide" })
|
||||
hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "menu_accel" })
|
||||
hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.6, bezier = "menu_decel" })
|
||||
hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.8, bezier = "menu_accel" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 4.0, bezier = "menu_decel", style = "slide" })
|
||||
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 2.3, bezier = "md3_decel", style = "slidefadevert 15%" })
|
||||
32
dotfiles/.config/hypr/presets/anim-hyde-vertical.lua
Normal file
32
dotfiles/.config/hypr/presets/anim-hyde-vertical.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Vertical"
|
||||
-- credit https://github.com/prasanthrangan/hyprdots
|
||||
-- Ported from animations/HYDE - Vertical.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("fluent_decel", { type = "bezier", points = { { 0, 0.2 }, { 0.4, 1 } } })
|
||||
hl.curve("easeOutCirc", { type = "bezier", points = { { 0, 0.55 }, { 0.45, 1 } } })
|
||||
hl.curve("easeOutCubic", { type = "bezier", points = { { 0.33, 1 }, { 0.68, 1 } } })
|
||||
hl.curve("easeinoutsine", { type = "bezier", points = { { 0.37, 0 }, { 0.63, 1 } } })
|
||||
|
||||
-- Windows
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 1.5, bezier = "easeinoutsine", style = "popin 60%" }) -- window open
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.5, bezier = "easeOutCubic", style = "popin 60%" }) -- window close.
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 1.5, bezier = "easeinoutsine", style = "slide" }) -- everything in between, moving, dragging, resizing.
|
||||
|
||||
-- Fading
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 2.5, bezier = "fluent_decel" })
|
||||
|
||||
hl.animation({ leaf = "fadeLayersIn", enabled = false })
|
||||
hl.animation({ leaf = "border", enabled = false })
|
||||
|
||||
-- Layers
|
||||
hl.animation({ leaf = "layers", enabled = true, speed = 1.5, bezier = "easeinoutsine", style = "popin" })
|
||||
|
||||
-- Workspaces
|
||||
-- styles: slide, slidevert, fade, slidefade, slidefadevert
|
||||
-- hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "fluent_decel", style = "slidefade 30%" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "fluent_decel", style = "slidefadevert 30%" })
|
||||
|
||||
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 2, bezier = "fluent_decel", style = "slidefade 10%" })
|
||||
52
dotfiles/.config/hypr/presets/anim-mahaveer-1.lua
Normal file
52
dotfiles/.config/hypr/presets/anim-mahaveer-1.lua
Normal file
@@ -0,0 +1,52 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Me-1"
|
||||
-- credit https://github.com/mahaveergurjar
|
||||
-- Ported from animations/Mahaveer - me-1.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
-- Animation curves
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
-- hl.curve("linear", { type = "bezier", points = { {0,0}, {1,1} } })
|
||||
hl.curve("md3_standard", { type = "bezier", points = { { 0.2, 0 }, { 0, 1 } } })
|
||||
hl.curve("md3_decel", { type = "bezier", points = { { 0.05, 0.7 }, { 0.1, 1 } } })
|
||||
hl.curve("md3_accel", { type = "bezier", points = { { 0.3, 0 }, { 0.8, 0.15 } } })
|
||||
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.1 } } })
|
||||
hl.curve("crazyshot", { type = "bezier", points = { { 0.1, 1.5 }, { 0.76, 0.92 } } })
|
||||
hl.curve("hyprnostretch", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.0 } } })
|
||||
hl.curve("menu_decel", { type = "bezier", points = { { 0.1, 1 }, { 0, 1 } } })
|
||||
hl.curve("menu_accel", { type = "bezier", points = { { 0.38, 0.04 }, { 1, 0.07 } } })
|
||||
hl.curve("easeInOutCirc", { type = "bezier", points = { { 0.85, 0 }, { 0.15, 1 } } })
|
||||
hl.curve("easeOutCirc", { type = "bezier", points = { { 0, 0.55 }, { 0.45, 1 } } })
|
||||
hl.curve("easeOutExpo", { type = "bezier", points = { { 0.16, 1 }, { 0.3, 1 } } })
|
||||
hl.curve("softAcDecel", { type = "bezier", points = { { 0.26, 0.26 }, { 0.15, 1 } } })
|
||||
hl.curve("md2", { type = "bezier", points = { { 0.4, 0 }, { 0.2, 1 } } }) -- use with .2s duration
|
||||
|
||||
-- Animation configs
|
||||
-- hl.animation({ leaf = "windows", enabled = true, speed = 3, bezier = "md3_decel", style = "popin 60%" })
|
||||
-- hl.animation({ leaf = "windowsIn", enabled = true, speed = 3, bezier = "md3_decel", style = "popin 60%" })
|
||||
-- hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "md3_accel", style = "popin 60%" })
|
||||
-- hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "once" })
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
|
||||
-- hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "md3_decel" })
|
||||
-- hl.animation({ leaf = "layers", enabled = true, speed = 2, bezier = "md3_decel", style = "slide" })
|
||||
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "menu_decel", style = "slide" })
|
||||
hl.animation({ leaf = "layersOut", enabled = true, speed = 1.6, bezier = "menu_accel" })
|
||||
hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 2, bezier = "menu_decel" })
|
||||
hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 4.5, bezier = "menu_accel" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 7, bezier = "menu_decel", style = "slide" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
|
||||
-- hl.animation({ leaf = "workspaces", enabled = true, speed = 2.5, bezier = "softAcDecel", style = "slide" })
|
||||
-- hl.animation({ leaf = "workspaces", enabled = true, speed = 7, bezier = "menu_decel", style = "slidefade 15%" })
|
||||
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "md3_decel", style = "slidefadevert 15%" })
|
||||
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "md3_decel", style = "slidevert" })
|
||||
27
dotfiles/.config/hypr/presets/anim-mahaveer-2.lua
Normal file
27
dotfiles/.config/hypr/presets/anim-mahaveer-2.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Me-2"
|
||||
-- credit https://github.com/mahaveergurjar
|
||||
-- Ported from animations/Mahaveer - me-2.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("smoothOut", { type = "bezier", points = { { 0.5, 0 }, { 0.99, 0.99 } } })
|
||||
hl.curve("smoothIn", { type = "bezier", points = { { 0.5, -0.5 }, { 0.68, 1.5 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "smoothOut", style = "slide" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 100, bezier = "liner", style = "loop" }) -- used by rainbow borders and rotating colors; speed clamped from 180 -> Lua hl.animation caps speed at 100
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "smoothOut" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "overshot" })
|
||||
|
||||
-- animations for -git or version >0.42.0
|
||||
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
|
||||
15
dotfiles/.config/hypr/presets/anim-ml4w-classic.lua
Normal file
15
dotfiles/.config/hypr/presets/anim-ml4w-classic.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Classic"
|
||||
-- credit https://github.com/mylinuxforwork/dotfiles
|
||||
-- Ported from animations/ML4W - classic.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("myBezier", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 7, bezier = "myBezier" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 7, bezier = "default", style = "popin 80%" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 8, bezier = "default" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 7, bezier = "default" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "default" })
|
||||
20
dotfiles/.config/hypr/presets/anim-ml4w-dynamic.lua
Normal file
20
dotfiles/.config/hypr/presets/anim-ml4w-dynamic.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Dynamic"
|
||||
-- credit https://github.com/mylinuxforwork/dotfiles
|
||||
-- Ported from animations/ML4W - dynamic.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "loop" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
|
||||
24
dotfiles/.config/hypr/presets/anim-ml4w-fast.lua
Normal file
24
dotfiles/.config/hypr/presets/anim-ml4w-fast.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Fast"
|
||||
-- credit https://github.com/mylinuxforwork/dotfiles
|
||||
-- Ported from animations/ML4W - fast.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
|
||||
hl.curve("md3_standard", { type = "bezier", points = { { 0.2, 0 }, { 0, 1 } } })
|
||||
hl.curve("md3_decel", { type = "bezier", points = { { 0.05, 0.7 }, { 0.1, 1 } } })
|
||||
hl.curve("md3_accel", { type = "bezier", points = { { 0.3, 0 }, { 0.8, 0.15 } } })
|
||||
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.1 } } })
|
||||
hl.curve("crazyshot", { type = "bezier", points = { { 0.1, 1.5 }, { 0.76, 0.92 } } })
|
||||
hl.curve("hyprnostretch", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.0 } } })
|
||||
hl.curve("fluent_decel", { type = "bezier", points = { { 0.1, 1 }, { 0, 1 } } })
|
||||
hl.curve("easeInOutCirc", { type = "bezier", points = { { 0.85, 0 }, { 0.15, 1 } } })
|
||||
hl.curve("easeOutCirc", { type = "bezier", points = { { 0, 0.55 }, { 0.45, 1 } } })
|
||||
hl.curve("easeOutExpo", { type = "bezier", points = { { 0.16, 1 }, { 0.3, 1 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 3, bezier = "md3_decel", style = "popin 60%" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 2.5, bezier = "md3_decel" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 3.5, bezier = "easeOutExpo", style = "slide" })
|
||||
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "md3_decel", style = "slidevert" })
|
||||
20
dotfiles/.config/hypr/presets/anim-ml4w-high.lua
Normal file
20
dotfiles/.config/hypr/presets/anim-ml4w-high.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "High"
|
||||
-- credit https://github.com/mylinuxforwork/dotfiles
|
||||
-- Ported from animations/ML4W - high.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
|
||||
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
|
||||
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "once" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
|
||||
19
dotfiles/.config/hypr/presets/anim-ml4w-moving.lua
Normal file
19
dotfiles/.config/hypr/presets/anim-ml4w-moving.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Moving"
|
||||
-- credit https://github.com/mylinuxforwork/dotfiles
|
||||
-- Ported from animations/ML4W - moving.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
hl.curve("smoothOut", { type = "bezier", points = { { 0.5, 0 }, { 0.99, 0.99 } } })
|
||||
hl.curve("smoothIn", { type = "bezier", points = { { 0.5, -0.5 }, { 0.68, 1.5 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 5, bezier = "overshot", style = "slide" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "smoothOut" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 3, bezier = "smoothOut" })
|
||||
hl.animation({ leaf = "windowsMove", enabled = true, speed = 4, bezier = "smoothIn", style = "slide" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 5, bezier = "default" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 5, bezier = "smoothIn" })
|
||||
hl.animation({ leaf = "fadeDim", enabled = true, speed = 5, bezier = "smoothIn" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "default" })
|
||||
15
dotfiles/.config/hypr/presets/anim-ml4w-standard.lua
Normal file
15
dotfiles/.config/hypr/presets/anim-ml4w-standard.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
-- name "Standard"
|
||||
-- credit https://github.com/mylinuxforwork/dotfiles
|
||||
-- Ported from animations/ML4W - standard.conf (unused preset — not required by anything)
|
||||
|
||||
hl.config({ animations = { enabled = true } })
|
||||
|
||||
hl.curve("myBezier", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 7, bezier = "myBezier" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 7, bezier = "default", style = "popin 80%" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 8, bezier = "default" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 7, bezier = "default" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "default" })
|
||||
73
dotfiles/.config/hypr/scripts/FloatBitwardenPopup.sh
Executable file
73
dotfiles/.config/hypr/scripts/FloatBitwardenPopup.sh
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Float + resize (to 781x702) + center the Bitwarden browser-extension pop-out window.
|
||||
#
|
||||
# Why this exists instead of a window rule: Hyprland's `float`/`center`/`size`
|
||||
# rules are "static" — evaluated exactly once, at the instant a window is
|
||||
# created — and this popup opens with the generic title "Zen Browser" (same
|
||||
# initialTitle/initialClass as the main browser window), only updating to
|
||||
# "Extension: (Bitwarden Password Manager) - Bitwarden..." a moment after
|
||||
# it's already mapped. By then the static rule has already missed its one
|
||||
# chance and there's no field available at creation time to tell the popup
|
||||
# apart from a normal browser window. See layout/windowrules.lua and
|
||||
# https://github.com/hyprwm/Hyprland/issues/7402 for the same problem class.
|
||||
#
|
||||
# No socat/ncat is installed, so instead of an event-driven IPC listener this
|
||||
# just polls `hyprctl clients` at a short interval with jq (already
|
||||
# installed).
|
||||
#
|
||||
# Dispatch syntax note: Hyprland 0.55+'s Lua config changed `hyprctl dispatch`
|
||||
# to take a literal Lua expression calling hl.dsp.* (e.g.
|
||||
# `hyprctl dispatch 'hl.dsp.window.center()'`), not the old
|
||||
# "centerwindow"-style dispatcher strings. Verified live against the real
|
||||
# API in /usr/share/hypr/stubs/hl.meta.lua before wiring this in.
|
||||
#
|
||||
# Why it re-asserts for a few seconds instead of firing once: right after the
|
||||
# popup leaves the tiled layout, Hyprland briefly re-tiles the sibling window
|
||||
# to fill the freed space, and during that reflow this popup was observed
|
||||
# reporting `floating: false` again for one poll tick — snapping it back to
|
||||
# its old tiled geometry. Re-asserting float+center for a short settle window
|
||||
# after first detection rides that out. After the settle window it's left
|
||||
# alone, so a manual re-tile/move by you later isn't fought.
|
||||
|
||||
TITLE_PATTERN='Extension: \(Bitwarden Password Manager\)'
|
||||
POLL_INTERVAL=0.3
|
||||
SETTLE_TICKS=6 # ~2s of re-assertion after first detection, at POLL_INTERVAL
|
||||
TARGET_WIDTH=781
|
||||
TARGET_HEIGHT=702
|
||||
|
||||
declare -A ticks_left # address -> remaining re-assert ticks
|
||||
|
||||
while true; do
|
||||
sleep "$POLL_INTERVAL"
|
||||
|
||||
# Forget any tracked window that's no longer open.
|
||||
open_addresses=$(hyprctl clients -j | jq -r '.[].address')
|
||||
for addr in "${!ticks_left[@]}"; do
|
||||
grep -qF "$addr" <<<"$open_addresses" || unset 'ticks_left[$addr]'
|
||||
done
|
||||
|
||||
# Process substitution (not a pipe) so the while loop runs in this shell
|
||||
# and its writes to ticks_left actually persist across iterations.
|
||||
while IFS= read -r win; do
|
||||
title=$(jq -r '.title' <<<"$win")
|
||||
[[ "$title" =~ $TITLE_PATTERN ]] || continue
|
||||
|
||||
address=$(jq -r '.address' <<<"$win")
|
||||
floating=$(jq -r '.floating' <<<"$win")
|
||||
|
||||
# First time we've seen this window: start its settle window.
|
||||
[[ -v ticks_left[$address] ]] || ticks_left[$address]=$SETTLE_TICKS
|
||||
|
||||
# Already floating and past its settle window: leave it alone.
|
||||
[[ "$floating" == "true" && ${ticks_left[$address]} -le 0 ]] && continue
|
||||
|
||||
hyprctl dispatch "hl.dsp.focus({ window = \"address:$address\" })" >/dev/null
|
||||
hyprctl dispatch 'hl.dsp.window.float({ action = "enable" })' >/dev/null
|
||||
# resize({x=W,y=H}) sets the absolute size (not a delta) — verified live.
|
||||
# Must happen before center(), since resizing shifts what "centered" means.
|
||||
hyprctl dispatch "hl.dsp.window.resize({ x = $TARGET_WIDTH, y = $TARGET_HEIGHT })" >/dev/null
|
||||
hyprctl dispatch 'hl.dsp.window.center()' >/dev/null
|
||||
|
||||
ticks_left[$address]=$((ticks_left[$address] - 1))
|
||||
done < <(hyprctl clients -j | jq -c '.[] | {address, title, floating}')
|
||||
done
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
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_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
|
||||
# HyprFlux — https://github.com/ahmad9059/HyprFlux
|
||||
# Scripts for volume controls for audio and mic
|
||||
# Volume controls for audio and mic via wpctl (PipeWire).
|
||||
# Quickshell's VolumeOsd watches PipeWire directly, so no notifications here.
|
||||
|
||||
iDIR="$HOME/.config/swaync/icons"
|
||||
sDIR="$HOME/.config/hypr/scripts"
|
||||
SINK="@DEFAULT_AUDIO_SINK@"
|
||||
SOURCE="@DEFAULT_AUDIO_SOURCE@"
|
||||
STEP="5%"
|
||||
|
||||
# Get Volume
|
||||
# Print volume as an integer percent, or "Muted"
|
||||
get_volume() {
|
||||
volume=$(pamixer --get-volume)
|
||||
if [[ "$volume" -eq "0" ]]; then
|
||||
local out
|
||||
out=$(wpctl get-volume "$1")
|
||||
if [[ "$out" == *MUTED* ]]; then
|
||||
echo "Muted"
|
||||
else
|
||||
echo "$volume %"
|
||||
awk '{ printf "%d %%\n", $2 * 100 + 0.5 }' <<< "$out"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get icons
|
||||
get_icon() {
|
||||
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
|
||||
is_muted() {
|
||||
[[ "$(wpctl get-volume "$1")" == *MUTED* ]]
|
||||
}
|
||||
|
||||
# Notify
|
||||
notify_user() {
|
||||
if [[ "$(get_volume)" == "Muted" ]]; then
|
||||
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
|
||||
}
|
||||
# Raising/lowering while muted unmutes first, matching the old behaviour
|
||||
inc() { is_muted "$1" && wpctl set-mute "$1" 0 || wpctl set-volume -l 1.0 "$1" "$STEP+"; }
|
||||
dec() { is_muted "$1" && wpctl set-mute "$1" 0 || wpctl set-volume "$1" "$STEP-"; }
|
||||
|
||||
# Increase Volume
|
||||
inc_volume() {
|
||||
if [ "$(pamixer --get-mute)" == "true" ]; then
|
||||
toggle_mute
|
||||
else
|
||||
pamixer -i 5 --allow-boost --set-limit 150 && notify_user
|
||||
fi
|
||||
}
|
||||
|
||||
# Decrease Volume
|
||||
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
|
||||
case "$1" in
|
||||
--inc) inc "$SINK" ;;
|
||||
--dec) dec "$SINK" ;;
|
||||
--toggle) wpctl set-mute "$SINK" toggle ;;
|
||||
--toggle-mic) wpctl set-mute "$SOURCE" toggle ;;
|
||||
--mic-inc) inc "$SOURCE" ;;
|
||||
--mic-dec) dec "$SOURCE" ;;
|
||||
--get-mic) get_volume "$SOURCE" ;;
|
||||
*) get_volume "$SINK" ;;
|
||||
esac
|
||||
|
||||
93
dotfiles/.config/ohmyposh/zen.toml
Normal file
93
dotfiles/.config/ohmyposh/zen.toml
Normal file
@@ -0,0 +1,93 @@
|
||||
#:schema https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json
|
||||
|
||||
version = 2
|
||||
final_space = true
|
||||
console_title_template = '{{ .Shell }} in {{ .Folder }}'
|
||||
|
||||
[[blocks]]
|
||||
type = 'prompt'
|
||||
alignment = 'left'
|
||||
newline = true
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'path'
|
||||
style = 'plain'
|
||||
background = 'transparent'
|
||||
foreground = 'blue'
|
||||
template = '{{ .Path }}'
|
||||
|
||||
[blocks.segments.properties]
|
||||
style = 'full'
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'git'
|
||||
style = 'plain'
|
||||
foreground = 'p:grey'
|
||||
background = 'transparent'
|
||||
template = ' {{ .HEAD }}{{ if or (.Working.Changed) (.Staging.Changed) }}*{{ end }} <cyan>{{ if gt .Behind 0 }}⇣{{ end }}{{ if gt .Ahead 0 }}⇡{{ end }}</>'
|
||||
|
||||
[blocks.segments.properties]
|
||||
branch_icon = ''
|
||||
commit_icon = '@'
|
||||
fetch_status = true
|
||||
|
||||
[[blocks.segments]]
|
||||
type = "pnpm"
|
||||
style = "powerline"
|
||||
powerline_symbol = "❯"
|
||||
foreground = "blue"
|
||||
background = "transparent"
|
||||
template = " {{ .Full }} "
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'docker'
|
||||
style = 'powerline'
|
||||
foreground = 'blue'
|
||||
background = 'transparent'
|
||||
template = ' {{ .Context }} '
|
||||
|
||||
[blocks.segments.options]
|
||||
display_mode = 'files'
|
||||
|
||||
[[blocks]]
|
||||
type = 'rprompt'
|
||||
overflow = 'hidden'
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'executiontime'
|
||||
style = 'plain'
|
||||
foreground = 'yellow'
|
||||
background = 'transparent'
|
||||
template = '{{ .FormattedMs }}'
|
||||
|
||||
[blocks.segments.properties]
|
||||
threshold = 5000
|
||||
|
||||
[[blocks]]
|
||||
type = 'prompt'
|
||||
alignment = 'left'
|
||||
newline = true
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'text'
|
||||
style = 'plain'
|
||||
foreground_templates = [
|
||||
"{{if gt .Code 0}}red{{end}}",
|
||||
"{{if eq .Code 0}}magenta{{end}}",
|
||||
]
|
||||
background = 'transparent'
|
||||
template = '❯'
|
||||
|
||||
|
||||
[transient_prompt]
|
||||
foreground_templates = [
|
||||
"{{if gt .Code 0}}red{{end}}",
|
||||
"{{if eq .Code 0}}magenta{{end}}",
|
||||
]
|
||||
background = 'transparent'
|
||||
template = '❯ '
|
||||
|
||||
[secondary_prompt]
|
||||
foreground = 'magenta'
|
||||
background = 'transparent'
|
||||
template = '❯❯ '
|
||||
@@ -1,5 +0,0 @@
|
||||
accent_primary = "#007acc"
|
||||
accent_secondary = "#06b6d4"
|
||||
background = "#1e1e1e"
|
||||
foreground = "#d4d4d8"
|
||||
opacity = 0.91
|
||||
4
dotfiles/.config/qt5ct/colors/Catppuccin-Latte.conf
Normal file
4
dotfiles/.config/qt5ct/colors/Catppuccin-Latte.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
[ColorScheme]
|
||||
active_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff1e66f5, #ffeff1f5, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
|
||||
disabled_colors=#ff6c6f85, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff6c6f85, #ff6c6f85, #ff6c6f85, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff9ca0b0, #ff5c5f77, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
|
||||
inactive_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ffccd0da, #ff6c6f85, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
|
||||
4
dotfiles/.config/qt5ct/colors/Catppuccin-Mocha.conf
Normal file
4
dotfiles/.config/qt5ct/colors/Catppuccin-Mocha.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
[ColorScheme]
|
||||
active_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ff1e1e2e, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
|
||||
disabled_colors=#ffa6adc8, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffa6adc8, #ffa6adc8, #ffa6adc8, #ff1e1e2e, #ff11111b, #ff7f849c, #ff89b4fa, #ff45475a, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
|
||||
inactive_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ffa6adc8, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
|
||||
32
dotfiles/.config/qt5ct/qt5ct.conf
Normal file
32
dotfiles/.config/qt5ct/qt5ct.conf
Normal file
@@ -0,0 +1,32 @@
|
||||
[Appearance]
|
||||
color_scheme_path=
|
||||
custom_palette=true
|
||||
icon_theme=Papirus-Dark
|
||||
standard_dialogs=default
|
||||
style=Fusion
|
||||
|
||||
[Fonts]
|
||||
fixed="Adwaita Sans,10,-1,5,50,0,0,0,0,0,Regular"
|
||||
general="Adwaita Sans,12,-1,5,50,0,0,0,0,0,Regular"
|
||||
|
||||
[Interface]
|
||||
activate_item_on_single_click=1
|
||||
buttonbox_layout=0
|
||||
cursor_flash_time=1000
|
||||
dialog_buttons_have_icons=1
|
||||
double_click_interval=400
|
||||
gui_effects=General, AnimateMenu, AnimateCombo, AnimateTooltip, AnimateToolBox
|
||||
keyboard_scheme=2
|
||||
menus_have_icons=true
|
||||
show_shortcuts_in_context_menus=true
|
||||
stylesheets=@Invalid()
|
||||
toolbutton_style=4
|
||||
underline_shortcut=1
|
||||
wheel_scroll_lines=3
|
||||
|
||||
[SettingsWindow]
|
||||
geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3\0\0\0\0\x2\0\0\0\a\x80\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3)
|
||||
|
||||
[Troubleshooting]
|
||||
force_raster_widgets=1
|
||||
ignored_applications=@Invalid()
|
||||
4
dotfiles/.config/qt6ct/colors/Catppuccin-Latte.conf
Normal file
4
dotfiles/.config/qt6ct/colors/Catppuccin-Latte.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
[ColorScheme]
|
||||
active_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff1e66f5, #ffeff1f5, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
|
||||
disabled_colors=#ff6c6f85, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff6c6f85, #ff6c6f85, #ff6c6f85, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff9ca0b0, #ff5c5f77, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
|
||||
inactive_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ffccd0da, #ff6c6f85, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
|
||||
4
dotfiles/.config/qt6ct/colors/Catppuccin-Mocha.conf
Normal file
4
dotfiles/.config/qt6ct/colors/Catppuccin-Mocha.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
[ColorScheme]
|
||||
active_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ff1e1e2e, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
|
||||
disabled_colors=#ffa6adc8, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffa6adc8, #ffa6adc8, #ffa6adc8, #ff1e1e2e, #ff11111b, #ff7f849c, #ff89b4fa, #ff45475a, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
|
||||
inactive_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ffa6adc8, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
|
||||
32
dotfiles/.config/qt6ct/qt6ct.conf
Normal file
32
dotfiles/.config/qt6ct/qt6ct.conf
Normal file
@@ -0,0 +1,32 @@
|
||||
[Appearance]
|
||||
color_scheme_path=
|
||||
custom_palette=true
|
||||
icon_theme=Papirus-Dark
|
||||
standard_dialogs=default
|
||||
style=Fusion
|
||||
|
||||
[Fonts]
|
||||
fixed="Adwaita Sans,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1,Regular"
|
||||
general="Adwaita Sans,12,-1,5,400,0,0,0,0,0,0,0,0,0,0,1,Regular"
|
||||
|
||||
[Interface]
|
||||
activate_item_on_single_click=1
|
||||
buttonbox_layout=0
|
||||
cursor_flash_time=1000
|
||||
dialog_buttons_have_icons=1
|
||||
double_click_interval=400
|
||||
gui_effects=General, AnimateMenu, AnimateCombo, AnimateTooltip, AnimateToolBox
|
||||
keyboard_scheme=2
|
||||
menus_have_icons=true
|
||||
show_shortcuts_in_context_menus=true
|
||||
stylesheets=@Invalid()
|
||||
toolbutton_style=4
|
||||
underline_shortcut=1
|
||||
wheel_scroll_lines=3
|
||||
|
||||
[SettingsWindow]
|
||||
geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3\0\0\0\0\x2\0\0\0\a\x80\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3)
|
||||
|
||||
[Troubleshooting]
|
||||
force_raster_widgets=1
|
||||
ignored_applications=@Invalid()
|
||||
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 |
29
dotfiles/.config/quickshell/config/Appearance.qml
Normal file
29
dotfiles/.config/quickshell/config/Appearance.qml
Normal file
@@ -0,0 +1,29 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
readonly property string fontFamily: "CaskaydiaCove Nerd Font Mono"
|
||||
readonly property string iconFontFamily: "Material Symbols Outlined"
|
||||
// Same glyph set, softer/rounded terminals — used where a module wants
|
||||
// a visually distinct icon style from the rest of the bar (currently
|
||||
// just BluetoothIndicator.qml) without switching every icon over.
|
||||
readonly property string iconFontFamilyRounded: "Material Symbols Rounded"
|
||||
// Material Symbols is a variable font. Left unset, glyphs fall back to the
|
||||
// font's default optical size (24px), which is tuned for larger rendering
|
||||
// and makes some icons (e.g. "lock") look heavier/bigger than others when
|
||||
// drawn small — pin "opsz" near the actual render size to keep icons
|
||||
// visually consistent. See Appearance.iconSize for the matching pixelSize.
|
||||
readonly property var iconFontAxes: ({ "FILL": 0, "wght": 400, "GRAD": 0, "opsz": 20 })
|
||||
readonly property int iconSize: 16
|
||||
|
||||
readonly property int fontSize: 13
|
||||
readonly property int fontSizeSmall: 11
|
||||
|
||||
readonly property int radius: 14
|
||||
readonly property int radiusSmall: 8
|
||||
|
||||
readonly property int barHeight: 34
|
||||
readonly property int spacing: 10
|
||||
|
||||
readonly property int animationDuration: 150
|
||||
}
|
||||
62
dotfiles/.config/quickshell/config/Colors.qml
Normal file
62
dotfiles/.config/quickshell/config/Colors.qml
Normal file
@@ -0,0 +1,62 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
// Wallpaper-driven palette, sourced from pywal's ~/.cache/wal/colors.json.
|
||||
// Run `wal -i /path/to/wallpaper.png` to (re)generate it — this file watches
|
||||
// the cache and updates live, no shell restart needed. Until that's been run
|
||||
// once, everything falls back to a hand-picked dark palette below.
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property bool active: _parsed !== null
|
||||
property var _parsed: null
|
||||
|
||||
// --- Fallback palette (used until pywal has run) ---
|
||||
readonly property color fallbackBg: "#1a1b26"
|
||||
readonly property color fallbackFg: "#c0caf5"
|
||||
readonly property var fallbackPalette: [
|
||||
"#15161e", "#f7768e", "#9ece6a", "#e0af68",
|
||||
"#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6",
|
||||
"#414868", "#f7768e", "#9ece6a", "#e0af68",
|
||||
"#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"
|
||||
]
|
||||
|
||||
function paletteColor(i) {
|
||||
if (_parsed && _parsed.colors)
|
||||
return _parsed.colors["color" + i] || fallbackPalette[i]
|
||||
return fallbackPalette[i]
|
||||
}
|
||||
|
||||
readonly property color background: _parsed && _parsed.special ? _parsed.special.background : fallbackBg
|
||||
readonly property color foreground: _parsed && _parsed.special ? _parsed.special.foreground : fallbackFg
|
||||
readonly property color primary: paletteColor(4)
|
||||
// Named primaryText (not "onPrimary") because QML treats any "onXxx"
|
||||
// identifier as a signal handler slot, not a regular property.
|
||||
readonly property color primaryText: background
|
||||
readonly property color urgent: paletteColor(1)
|
||||
// paletteColor(8) alone renders quite dark/low-contrast for text (it's
|
||||
// meant as a muted *background* tone in most wal palettes) — lighten it
|
||||
// so muted labels stay legible against the bar's dark background.
|
||||
readonly property color muted: Qt.lighter(paletteColor(8), 2.2)
|
||||
readonly property color surface: Qt.lighter(background, 1.35)
|
||||
|
||||
// QtObject has no default property, so the FileView is attached via a
|
||||
// property assignment rather than as a plain child element.
|
||||
readonly property FileView _walFile: FileView {
|
||||
path: Quickshell.env("HOME") + "/.cache/wal/colors.json"
|
||||
watchChanges: true
|
||||
onLoaded: {
|
||||
try {
|
||||
root._parsed = JSON.parse(text())
|
||||
} catch (e) {
|
||||
console.warn("Colors: failed to parse pywal cache:", e)
|
||||
root._parsed = null
|
||||
}
|
||||
}
|
||||
onLoadFailed: (error) => {
|
||||
root._parsed = null
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
17
dotfiles/.config/quickshell/config/VpnExclusivity.qml
Normal file
17
dotfiles/.config/quickshell/config/VpnExclusivity.qml
Normal file
@@ -0,0 +1,17 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
// Tailscale, Mullvad, ProtonVPN, and the home UniFi WireGuard tunnel all
|
||||
// fight over the default route/DNS if more than one is active at once (see
|
||||
// VpnIndicator.qml's header comment) — only one should ever actually be
|
||||
// connected. This singleton is the shared signal between the two widget
|
||||
// files: VpnIndicator.qml writes each tunnel's live connection state here
|
||||
// as it polls them; TailscaleIndicator.qml reads it to back off (`tailscale
|
||||
// down`) rather than stay up alongside one, and to refuse turning Tailscale
|
||||
// back on while one is still connected.
|
||||
QtObject {
|
||||
property bool mullvadConnected: false
|
||||
property bool protonConnected: false
|
||||
property bool homeWgConnected: false
|
||||
readonly property bool commercialVpnActive: mullvadConnected || protonConnected || homeWgConnected
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
6
dotfiles/.config/quickshell/config/qmldir
Normal file
6
dotfiles/.config/quickshell/config/qmldir
Normal file
@@ -0,0 +1,6 @@
|
||||
module config
|
||||
singleton Colors 1.0 Colors.qml
|
||||
singleton Appearance 1.0 Appearance.qml
|
||||
singleton VpnExclusivity 1.0 VpnExclusivity.qml
|
||||
singleton SystemUpdates 1.0 SystemUpdates.qml
|
||||
singleton WeatherData 1.0 WeatherData.qml
|
||||
13
dotfiles/.config/quickshell/modules/bar/ActiveWindow.qml
Normal file
13
dotfiles/.config/quickshell/modules/bar/ActiveWindow.qml
Normal file
@@ -0,0 +1,13 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import "../../config"
|
||||
|
||||
Text {
|
||||
Layout.maximumWidth: 320
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
text: Hyprland.activeToplevel ? Hyprland.activeToplevel.title : ""
|
||||
}
|
||||
62
dotfiles/.config/quickshell/modules/bar/Bar.qml
Normal file
62
dotfiles/.config/quickshell/modules/bar/Bar.qml
Normal file
@@ -0,0 +1,62 @@
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../config"
|
||||
|
||||
PanelWindow {
|
||||
id: bar
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
implicitHeight: Appearance.barHeight
|
||||
exclusiveZone: implicitHeight
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "quickshell:bar"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.background
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: Appearance.spacing
|
||||
}
|
||||
spacing: Appearance.spacing
|
||||
Workspaces { screen: bar.screen }
|
||||
ActiveWindow {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing
|
||||
Weather { barWindow: bar }
|
||||
Clock {}
|
||||
SystemUpdate { barWindow: bar }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
rightMargin: Appearance.spacing
|
||||
}
|
||||
spacing: Appearance.spacing
|
||||
PinnedTray {}
|
||||
TailscaleIndicator { barWindow: bar }
|
||||
VpnIndicator { barWindow: bar }
|
||||
NetworkIndicator { barWindow: bar }
|
||||
BluetoothIndicator { barWindow: bar }
|
||||
Volume { barWindow: bar }
|
||||
BatteryIndicator { barWindow: bar }
|
||||
PowerButton { barWindow: bar }
|
||||
}
|
||||
}
|
||||
605
dotfiles/.config/quickshell/modules/bar/BatteryIndicator.qml
Normal file
605
dotfiles/.config/quickshell/modules/bar/BatteryIndicator.qml
Normal file
@@ -0,0 +1,605 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import "../../config"
|
||||
|
||||
// Battery + power-profile panel, ported from omarchy's power panel
|
||||
// (github.com/basecamp/omarchy shell/plugins/panels/power/{Model.js,Panel.qml})
|
||||
// onto this project's own conventions: Quickshell's native UPower/PowerProfiles
|
||||
// bindings instead of omarchy's `omarchy-battery-status` /
|
||||
// `omarchy-powerprofiles-list` / `omarchy-powerprofiles-set` helper scripts
|
||||
// (which don't exist outside omarchy), and Material Symbols glyphs instead of
|
||||
// Nerd Font ones. The charge-threshold ("Holding"/limited-charge) concept from
|
||||
// the original is hardware-specific (vendor charge-limit sysfs knobs) and has
|
||||
// no equivalent in Quickshell's UPower binding, so it's dropped rather than
|
||||
// guessed at; "Battery health" (computed from sysfs energy_full/design) is shown in its
|
||||
// place, using data UPower actually reports.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property var device: UPower.displayDevice
|
||||
// isLaptopBattery (type === Battery && powerSupply === true) is the
|
||||
// Quickshell-recommended check for "is this actually a battery" — more
|
||||
// precise than isPresent, which the docs note is only meaningful once
|
||||
// you already know the device is of Battery type.
|
||||
readonly property bool batteryPresent: !!(root.device && root.device.ready && root.device.isLaptopBattery)
|
||||
|
||||
// Hidden entirely on a desktop with no battery — RowLayout skips
|
||||
// invisible children for both sizing and spacing, so this just vanishes
|
||||
// from the bar rather than leaving a gap.
|
||||
visible: root.batteryPresent
|
||||
implicitWidth: root.batteryPresent ? label.implicitWidth : 0
|
||||
implicitHeight: Appearance.barHeight
|
||||
|
||||
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
|
||||
// anchor to the bar itself rather than to this icon, keeping it a fixed
|
||||
// distance from the bar and the screen edge regardless of where this
|
||||
// icon sits in the row.
|
||||
required property var barWindow
|
||||
|
||||
readonly property bool onBattery: UPower.onBattery
|
||||
// Named deviceState, not "state" — Item already has a built-in `state`
|
||||
// property (the property-based state-machine one) that this would
|
||||
// otherwise collide with.
|
||||
readonly property int deviceState: root.batteryPresent ? root.device.state : UPowerDeviceState.Unknown
|
||||
readonly property bool fullyCharged: root.deviceState === UPowerDeviceState.FullyCharged
|
||||
readonly property bool charging: root.batteryPresent && !root.onBattery && !root.fullyCharged
|
||||
// device.percentage is a 0..1 fraction despite the name (same convention
|
||||
// Volume.qml relies on for Pipewire's audio.volume).
|
||||
readonly property real fraction: root.batteryPresent ? Math.max(0, Math.min(1, root.device.percentage)) : 0
|
||||
readonly property bool low: root.batteryPresent && root.fraction <= 0.15 && !root.charging
|
||||
|
||||
// Quickshell's UPower binding has no charge-cycle property (see the
|
||||
// file-header comment on why the old charge-until threshold was
|
||||
// dropped) — it comes straight from sysfs instead.
|
||||
//
|
||||
// device.nativePath turned out to be empty on real hardware (tested
|
||||
// live), so rather than build a path from it, a shell glob finds
|
||||
// whichever BAT* directory actually exists — same shell-out-to-a-CLI
|
||||
// convention TailscaleIndicator.qml uses for its own live stats.
|
||||
// This barely changes during a session, so one read at startup is
|
||||
// enough; no polling timer.
|
||||
property int cycleCount: -1
|
||||
|
||||
Process {
|
||||
id: cycleCountProc
|
||||
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/cycle_count\" 2>/dev/null"]
|
||||
running: root.batteryPresent
|
||||
stdout: StdioCollector {
|
||||
id: cycleCountCollector
|
||||
onStreamFinished: {
|
||||
const n = parseInt(cycleCountCollector.text.trim(), 10)
|
||||
root.cycleCount = (isNaN(n) || n <= 0) ? -1 : n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// device.healthPercentage/healthSupported come from upowerd over D-Bus,
|
||||
// and upowerd has been observed reporting energy-full as a flat 0 (a
|
||||
// live daemon glitch, `upower -i` shows it too) while sysfs still has
|
||||
// the real numbers underneath — same class of unreliability that put
|
||||
// cycle_count on sysfs above instead of trusting upowerd's own
|
||||
// charge-cycles field. Health is computed the same way UPower computes
|
||||
// it (current full-charge capacity over the battery's original design
|
||||
// capacity), just read directly so a daemon hiccup doesn't blank it.
|
||||
property real energyFullWh: -1
|
||||
property real energyFullDesignWh: -1
|
||||
readonly property real healthPercent: (root.energyFullWh > 0 && root.energyFullDesignWh > 0)
|
||||
? Math.min(100, (root.energyFullWh / root.energyFullDesignWh) * 100) : -1
|
||||
|
||||
Process {
|
||||
id: energyFullProc
|
||||
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_full\" 2>/dev/null"]
|
||||
running: root.batteryPresent
|
||||
stdout: StdioCollector {
|
||||
id: energyFullCollector
|
||||
onStreamFinished: {
|
||||
const n = parseFloat(energyFullCollector.text.trim())
|
||||
root.energyFullWh = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: energyFullDesignProc
|
||||
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_full_design\" 2>/dev/null"]
|
||||
running: root.batteryPresent
|
||||
stdout: StdioCollector {
|
||||
id: energyFullDesignCollector
|
||||
onStreamFinished: {
|
||||
const n = parseFloat(energyFullDesignCollector.text.trim())
|
||||
root.energyFullDesignWh = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// device.timeToFull/timeToEmpty are computed by Quickshell from
|
||||
// energy/energyCapacity/changeRate — when upowerd's EnergyFull glitches
|
||||
// to 0 (the same daemon flake HEALTH routes around above), that formula
|
||||
// divides by a bogus capacity and comes out negative rather than a
|
||||
// real estimate (confirmed live: timeToFull sat at -3751 while `upower
|
||||
// -i` itself still reported a normal 26.7 minutes). This recomputes
|
||||
// the same estimate straight from sysfs as a fallback, only used when
|
||||
// Quickshell's own value isn't positive. Unlike cycle_count/design
|
||||
// capacity above, energy_now and power_now change continuously while
|
||||
// (dis)charging, so this polls on a timer instead of reading once.
|
||||
property real sysfsEnergyNowWh: -1
|
||||
property real sysfsPowerNowW: -1
|
||||
|
||||
readonly property real fallbackTimeToFullSeconds: (root.sysfsPowerNowW > 0 && root.energyFullWh > 0 && root.sysfsEnergyNowWh >= 0)
|
||||
? Math.max(0, (root.energyFullWh - root.sysfsEnergyNowWh) / root.sysfsPowerNowW * 3600) : -1
|
||||
readonly property real fallbackTimeToEmptySeconds: (root.sysfsPowerNowW > 0 && root.sysfsEnergyNowWh > 0)
|
||||
? (root.sysfsEnergyNowWh / root.sysfsPowerNowW * 3600) : -1
|
||||
|
||||
// Prefers upowerd's own live estimate when it's sane; only falls back
|
||||
// to the sysfs-computed one when upowerd's has gone zero/negative.
|
||||
readonly property real timeRemainingSeconds: {
|
||||
const primary = root.onBattery ? root.device.timeToEmpty : root.device.timeToFull
|
||||
if (primary > 0) return primary
|
||||
return root.onBattery ? root.fallbackTimeToEmptySeconds : root.fallbackTimeToFullSeconds
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: sysfsRateTimer
|
||||
interval: 15000
|
||||
repeat: true
|
||||
running: root.batteryPresent
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
if (!sysfsEnergyNowProc.running) sysfsEnergyNowProc.running = true
|
||||
if (!sysfsPowerNowProc.running) sysfsPowerNowProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: sysfsEnergyNowProc
|
||||
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_now\" 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
id: sysfsEnergyNowCollector
|
||||
onStreamFinished: {
|
||||
const n = parseFloat(sysfsEnergyNowCollector.text.trim())
|
||||
root.sysfsEnergyNowWh = (isNaN(n) || n < 0) ? -1 : (n / 1000000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: sysfsPowerNowProc
|
||||
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/power_now\" 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
id: sysfsPowerNowCollector
|
||||
onStreamFinished: {
|
||||
const n = parseFloat(sysfsPowerNowCollector.text.trim())
|
||||
root.sysfsPowerNowW = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Raw UPower state string (Charging/Discharging/PendingCharge/…) — more
|
||||
// granular than modeLabel below, which collapses those down to the
|
||||
// three phrases the hero text and STATE row show.
|
||||
readonly property string rawStateLabel: root.batteryPresent
|
||||
? UPowerDeviceState.toString(root.deviceState) : "—"
|
||||
|
||||
// Material Symbols' charging glyphs aren't an evenly-spaced "_bar"
|
||||
// scale like the non-charging ones below — they're fixed at these
|
||||
// seven breakpoints (confirmed against the installed font's ligature
|
||||
// table), so this picks the highest one at or below the current
|
||||
// percentage rather than rounding to an index.
|
||||
function chargingIcon(fraction) {
|
||||
const percent = fraction * 100
|
||||
const thresholds = [20, 30, 50, 60, 80, 90, 100]
|
||||
const icons = [
|
||||
"battery_charging_20", "battery_charging_30", "battery_charging_50", "battery_charging_60",
|
||||
"battery_charging_80", "battery_charging_90", "battery_charging_full"
|
||||
]
|
||||
for (let i = thresholds.length - 1; i >= 0; i--) {
|
||||
if (percent >= thresholds[i]) return icons[i]
|
||||
}
|
||||
// Below 20% there's no lower charging glyph — it's still the
|
||||
// truthful floor of what's available while current is flowing in.
|
||||
return icons[0]
|
||||
}
|
||||
|
||||
function batteryIcon() {
|
||||
if (!root.batteryPresent) return "battery_unknown"
|
||||
if (root.fullyCharged) return "battery_full"
|
||||
if (root.charging) return root.chargingIcon(root.fraction)
|
||||
if (root.fraction <= 0.1) return "battery_alert"
|
||||
const bars = ["battery_0_bar", "battery_1_bar", "battery_2_bar", "battery_3_bar", "battery_4_bar", "battery_5_bar", "battery_full"]
|
||||
return bars[Math.max(0, Math.min(bars.length - 1, Math.round(root.fraction * (bars.length - 1))))]
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const s = Math.max(0, Math.round(seconds || 0))
|
||||
if (s <= 0) return "—"
|
||||
const h = Math.floor(s / 3600)
|
||||
const m = Math.round((s % 3600) / 60)
|
||||
return h > 0 ? (h + "h " + m + "m") : (m + "m")
|
||||
}
|
||||
|
||||
readonly property string modeLabel: {
|
||||
if (!root.batteryPresent) return ""
|
||||
if (root.fullyCharged) return "Fully charged"
|
||||
return root.onBattery ? "On battery" : "Charging"
|
||||
}
|
||||
|
||||
// Info grid rows — same shape as NetworkIndicator.qml's statRows/
|
||||
// statCells: a flat label/value list, flattened again into individual
|
||||
// grid cells (label, value, label, value, …) so a 4-column GridLayout
|
||||
// lays them out as two label/value pairs per row.
|
||||
readonly property var statRows: [
|
||||
{
|
||||
label: "HEALTH",
|
||||
value: root.healthPercent > 0 ? Math.round(root.healthPercent) + "%" : "—"
|
||||
},
|
||||
{
|
||||
label: root.onBattery ? "TIME LEFT" : "TIME TO FULL",
|
||||
value: root.batteryPresent ? root.formatDuration(root.timeRemainingSeconds) : "—"
|
||||
},
|
||||
{ label: "STATUS", value: root.rawStateLabel },
|
||||
{ label: "CYCLES", value: root.cycleCount > 0 ? String(root.cycleCount) : "—" }
|
||||
]
|
||||
|
||||
readonly property var statCells: {
|
||||
const cells = []
|
||||
for (let i = 0; i < root.statRows.length; i++) {
|
||||
cells.push({ text: root.statRows[i].label, isLabel: true })
|
||||
cells.push({ text: root.statRows[i].value, isLabel: false })
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
// Cute rotating status phrases while current is actually flowing —
|
||||
// ported as-is from omarchy's power panel, which pairs the same phrase
|
||||
// sets with the same charging/on-battery states.
|
||||
readonly property var chargingPhrases: [
|
||||
"Pumping power", "Injecting electrons", "Pouring juice", "Amassing watts",
|
||||
"Hoarding joules", "Sucking volts", "Topping reserves", "Soaking amps", "Inhaling kilowatts"
|
||||
]
|
||||
readonly property var onBatteryPhrases: [
|
||||
"Slurping power", "Spending joules", "Draining watts", "Burning electrons",
|
||||
"Sipping juice", "Spending coulombs", "Bleeding amps", "Guzzling volts", "Munching reserves"
|
||||
]
|
||||
readonly property var activePhrases: root.fullyCharged ? []
|
||||
: (root.charging ? root.chargingPhrases : (root.onBattery ? root.onBatteryPhrases : []))
|
||||
readonly property bool rotatingPhrases: root.activePhrases.length > 0
|
||||
property int phraseIndex: 0
|
||||
readonly property string heroStatusText: root.rotatingPhrases
|
||||
? root.activePhrases[root.phraseIndex % root.activePhrases.length]
|
||||
: root.modeLabel
|
||||
|
||||
// Only rotates while the panel is open and actually charging/discharging.
|
||||
Timer {
|
||||
id: phraseTimer
|
||||
interval: 2800
|
||||
running: panel.visible && root.rotatingPhrases
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
onTriggered: phraseSwap.restart()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: phraseSwap
|
||||
PropertyAnimation { target: heroStatus; property: "opacity"; to: 0.0; duration: 180; easing.type: Easing.OutQuad }
|
||||
ScriptAction {
|
||||
script: {
|
||||
const n = root.activePhrases.length
|
||||
if (n > 0) root.phraseIndex = (root.phraseIndex + 1) % n
|
||||
}
|
||||
}
|
||||
PropertyAnimation { target: heroStatus; property: "opacity"; to: 1.0; duration: 260; easing.type: Easing.InQuad }
|
||||
}
|
||||
|
||||
// If we leave a rotating state mid-swap (e.g. unplugged mid-fade),
|
||||
// snap back to full opacity so the new state reads immediately.
|
||||
Connections {
|
||||
target: root
|
||||
function onRotatingPhrasesChanged() {
|
||||
if (!root.rotatingPhrases) {
|
||||
phraseSwap.stop()
|
||||
heroStatus.opacity = 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Power profile list ------------------------------------------------
|
||||
// power-profiles-daemon only ever exposes these three fixed profiles;
|
||||
// Performance is conditional on hasPerformanceProfile (e.g. absent on
|
||||
// battery-only/thermally-limited hardware) — same gating omarchy's own
|
||||
// Model.js applies via its profile-list script.
|
||||
readonly property var profileEntries: {
|
||||
const entries = [
|
||||
{ value: PowerProfile.PowerSaver, label: "Saver", icon: "eco" },
|
||||
{ value: PowerProfile.Balanced, label: "Balanced", icon: "balance" }
|
||||
]
|
||||
if (PowerProfiles.hasPerformanceProfile)
|
||||
entries.push({ value: PowerProfile.Performance, label: "Performance", icon: "bolt" })
|
||||
return entries
|
||||
}
|
||||
|
||||
// --- Automatic power profile --------------------------------------
|
||||
// Battery-state-driven, on top of the manual picker above: below 20%
|
||||
// → Power Saver (protect what's left), plugged in → Performance,
|
||||
// unplugged and healthy → Balanced. Low battery wins even if a weak
|
||||
// charger is still plugged in — running low is the more urgent signal.
|
||||
// Performance only gets requested when the daemon actually reports it
|
||||
// as available, falling back to Balanced instead of erroring on
|
||||
// hardware without a performance profile (e.g. the ERROR this exact
|
||||
// widget hit before power-profiles-daemon was installed).
|
||||
//
|
||||
// This only ever writes when the *target* changes (crossing the 20%
|
||||
// line, plugging/unplugging) rather than on every poll, so a manual
|
||||
// pick from the buttons above still sticks until the next real
|
||||
// transition instead of being fought immediately.
|
||||
readonly property int desiredProfile: {
|
||||
if (root.fraction <= 0.20) return PowerProfile.PowerSaver
|
||||
if (!root.onBattery) return PowerProfiles.hasPerformanceProfile ? PowerProfile.Performance : PowerProfile.Balanced
|
||||
return PowerProfile.Balanced
|
||||
}
|
||||
|
||||
function applyDesiredProfile() {
|
||||
if (!root.batteryPresent) return
|
||||
if (PowerProfiles.profile !== root.desiredProfile) PowerProfiles.profile = root.desiredProfile
|
||||
}
|
||||
|
||||
onDesiredProfileChanged: root.applyDesiredProfile()
|
||||
Component.onCompleted: Qt.callLater(root.applyDesiredProfile)
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 16
|
||||
color: root.low ? Colors.urgent : Colors.foreground
|
||||
text: root.batteryIcon()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
panel.visible = !panel.visible
|
||||
grab.active = panel.visible
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reusable pieces -----------------------------------------------
|
||||
|
||||
component ProfileButton: Rectangle {
|
||||
id: profBtn
|
||||
required property string label
|
||||
required property string icon
|
||||
required property bool active
|
||||
signal activate()
|
||||
width: parent ? parent.width : 0
|
||||
radius: 0
|
||||
implicitHeight: 44
|
||||
color: mouse.containsMouse ? Colors.primary
|
||||
: (profBtn.active ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
|
||||
|
||||
// Hover-only outline, offset outside the button rather than drawn on
|
||||
// its own edge — same treatment as this project's other selectable
|
||||
// rows (e.g. Volume.qml's DeviceRow).
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: mouse.containsMouse ? 1 : 0
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: profBtn.icon
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 18
|
||||
color: mouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: profBtn.label
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
font.bold: profBtn.active
|
||||
color: mouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: profBtn.activate()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Panel -----------------------------------------------------------
|
||||
|
||||
PopupWindow {
|
||||
id: panel
|
||||
visible: false
|
||||
color: "transparent"
|
||||
// Wider than before (244) to give the 2-column info grid's
|
||||
// label/value pairs enough room per half without eliding —
|
||||
// same reasoning as NetworkIndicator.qml's own wider panel.
|
||||
readonly property int contentWidth: 280
|
||||
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 (and thus the true screen edges)
|
||||
// instead of a near-zero rect at the window's origin.
|
||||
rect.x: 0
|
||||
rect.y: 0
|
||||
rect.width: root.barWindow.width
|
||||
rect.height: root.barWindow.height
|
||||
edges: Edges.Bottom | Edges.Right
|
||||
gravity: Edges.Bottom | Edges.Left
|
||||
margins.bottom: -4
|
||||
margins.right: 0
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Hero: icon · title/status · percentage
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: root.batteryIcon()
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 30
|
||||
color: root.low ? Colors.urgent : Colors.foreground
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
Text {
|
||||
width: parent.width
|
||||
text: "Battery"
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: Colors.foreground
|
||||
}
|
||||
Text {
|
||||
id: heroStatus
|
||||
width: parent.width
|
||||
text: root.heroStatusText
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Math.round(root.fraction * 100) + "%"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
color: Colors.foreground
|
||||
}
|
||||
}
|
||||
|
||||
// Charge level bar
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 4
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 0
|
||||
color: Colors.muted
|
||||
opacity: 0.35
|
||||
}
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
radius: 0
|
||||
color: root.low ? Colors.urgent : Colors.primary
|
||||
width: Math.max(height, parent.width * root.fraction)
|
||||
Behavior on width { NumberAnimation { duration: 320; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
}
|
||||
|
||||
// Stats — a 2-column, 2-row info grid, same layout
|
||||
// NetworkIndicator.qml uses for its own live stats: a
|
||||
// 4-column GridLayout fed a flattened label/value/label/value
|
||||
// list, so each row holds two label/value pairs side by side
|
||||
// with the values right-aligned against their own half.
|
||||
GridLayout {
|
||||
width: parent.width
|
||||
columns: 4
|
||||
columnSpacing: 12
|
||||
rowSpacing: 5
|
||||
|
||||
Repeater {
|
||||
model: root.statCells
|
||||
Text {
|
||||
required property var modelData
|
||||
Layout.fillWidth: !modelData.isLabel
|
||||
horizontalAlignment: modelData.isLabel ? Text.AlignLeft : Text.AlignRight
|
||||
text: modelData.text
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: modelData.isLabel ? Colors.muted : Colors.foreground
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power profile picker
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: "POWER PROFILE"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
|
||||
Row {
|
||||
id: profileRow
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
readonly property real cellWidth: root.profileEntries.length > 0
|
||||
? (width - spacing * (root.profileEntries.length - 1)) / root.profileEntries.length
|
||||
: 0
|
||||
|
||||
Repeater {
|
||||
model: root.profileEntries
|
||||
ProfileButton {
|
||||
required property var modelData
|
||||
width: profileRow.cellWidth
|
||||
label: modelData.label
|
||||
icon: modelData.icon
|
||||
active: PowerProfiles.profile === modelData.value
|
||||
onActivate: PowerProfiles.profile = modelData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
795
dotfiles/.config/quickshell/modules/bar/BluetoothIndicator.qml
Normal file
795
dotfiles/.config/quickshell/modules/bar/BluetoothIndicator.qml
Normal file
@@ -0,0 +1,795 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Bluetooth
|
||||
import "../../config"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Independent left/right breathing room around the glyph. Material
|
||||
// Symbols icons aren't monospaced — each one's font-reported advance
|
||||
// width (label.implicitWidth) bakes in its own side bearings, so the
|
||||
// same "+4, centered" math that every other bar icon uses can still
|
||||
// look tighter/looser icon-to-icon. Tune these two by eye instead of
|
||||
// the icon's own bounding box to even it out against its neighbours.
|
||||
property real iconLeftPadding: 0
|
||||
property real iconRightPadding: 0
|
||||
implicitWidth: root.iconLeftPadding + label.implicitWidth + root.iconRightPadding
|
||||
implicitHeight: Appearance.barHeight
|
||||
|
||||
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
|
||||
// anchor to the bar itself rather than to this icon, keeping it a fixed
|
||||
// distance from the bar and the screen edge regardless of where this
|
||||
// icon sits in the row.
|
||||
required property var barWindow
|
||||
|
||||
readonly property var adapter: Bluetooth.defaultAdapter
|
||||
|
||||
// Scan only while the panel is open — same convention as the WiFi
|
||||
// scannerEnabled binding in NetworkIndicator.qml.
|
||||
Binding {
|
||||
target: root.adapter
|
||||
property: "discovering"
|
||||
value: panel.visible && root.adapter.enabled
|
||||
when: root.adapter !== null
|
||||
}
|
||||
|
||||
// --- Device row helpers ----------------------------------------------
|
||||
// Rows are plain objects rather than the live BluetoothDevice QObject —
|
||||
// BlueZ churn (pairing timeouts, a device dropping out of range mid-scan)
|
||||
// can destroy the object while a delegate still holds it, the same
|
||||
// crash risk noted for WiFi networks in NetworkIndicator.qml. The actual
|
||||
// object is re-resolved by address only when an action needs it, via
|
||||
// deviceForAddress().
|
||||
function deviceLabel(d) {
|
||||
return (d.deviceName || d.name || d.address || "").trim()
|
||||
}
|
||||
|
||||
// BlueZ's Icon property is a freedesktop icon-theme name (verified
|
||||
// against a real paired device on this machine: "audio-headset") — map
|
||||
// the common categories to a Material Symbols glyph, falling back to a
|
||||
// generic "devices_other" for anything unrecognized.
|
||||
function deviceIcon(icon) {
|
||||
const i = icon || ""
|
||||
if (i.indexOf("headset") !== -1 || i.indexOf("headphone") !== -1) return "headphones"
|
||||
if (i.indexOf("mouse") !== -1) return "mouse"
|
||||
if (i.indexOf("keyboard") !== -1) return "keyboard"
|
||||
if (i.indexOf("gaming") !== -1 || i.indexOf("joystick") !== -1) return "sports_esports"
|
||||
if (i.indexOf("tablet") !== -1) return "tablet"
|
||||
if (i.indexOf("camera") !== -1) return "videocam"
|
||||
if (i.indexOf("printer") !== -1) return "print"
|
||||
if (i.indexOf("watch") !== -1) return "watch"
|
||||
if (i.indexOf("tv") !== -1) return "tv"
|
||||
if (i.indexOf("computer") !== -1) return "computer"
|
||||
if (i.indexOf("card") !== -1) return "speaker"
|
||||
return "devices_other"
|
||||
}
|
||||
|
||||
function deviceRow(d) {
|
||||
if (!d) return null
|
||||
const label = root.deviceLabel(d)
|
||||
if (!label) return null
|
||||
return {
|
||||
address: d.address || "",
|
||||
label: label,
|
||||
icon: root.deviceIcon(d.icon),
|
||||
connected: !!d.connected,
|
||||
paired: !!d.paired,
|
||||
bonded: !!d.bonded,
|
||||
trusted: !!d.trusted,
|
||||
pairing: !!d.pairing,
|
||||
state: d.state,
|
||||
batteryAvailable: !!d.batteryAvailable,
|
||||
battery: d.battery || 0
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var deviceRows: root.adapter
|
||||
? root.adapter.devices.values.map(root.deviceRow).filter(function(r) { return r !== null })
|
||||
: []
|
||||
|
||||
readonly property var connectedRows: root.deviceRows.filter(function(r) { return r.connected })
|
||||
readonly property var knownRows: root.deviceRows.filter(function(r) {
|
||||
return !r.connected && (r.paired || r.bonded || r.trusted)
|
||||
})
|
||||
readonly property var discoveredRows: root.deviceRows.filter(function(r) {
|
||||
return !r.connected && !r.paired && !r.bonded && !r.trusted
|
||||
})
|
||||
|
||||
function deviceForAddress(address) {
|
||||
if (!root.adapter) return null
|
||||
const devs = root.adapter.devices.values
|
||||
for (let i = 0; i < devs.length; i++) {
|
||||
if (devs[i].address === address) return devs[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Action feedback ---------------------------------------------------
|
||||
// BlueZ operations here are fire-and-forget from QML's side — connect()/
|
||||
// disconnect()/pair()/forget() report failure only via a C++-side log
|
||||
// line (see quickshell's bluetooth/device.cpp), nothing reaches QML. So
|
||||
// "did it work" has to be inferred from the device's own properties
|
||||
// instead of a success/failure callback. Shown as a line right under the
|
||||
// specific row it's about (DeviceRow.statusText below) rather than a
|
||||
// panel-wide banner — actionStatusAddress is how a row knows a message
|
||||
// is meant for it; since it's address-keyed rather than tied to a
|
||||
// particular DeviceRow instance, the message still finds its row even
|
||||
// when the device jumps sections (Paired → Connected on a successful
|
||||
// connect, say), because that's a new DeviceRow for the same address.
|
||||
// - busyStatus is purely reactive — it just reads pairing/state off
|
||||
// whichever row is currently mid-operation, however that operation
|
||||
// was triggered. It's not shown directly (each row already derives
|
||||
// the same "Connecting…"/"Disconnecting…" off its own dev.state);
|
||||
// it only exists here to tell resolvePendingAction when to fire.
|
||||
// - pendingAction+the busyStatus watcher below is how a *specific*
|
||||
// click gets its own outcome message: it snapshots what was asked
|
||||
// for, waits for busyStatus to clear, then checks whether the
|
||||
// expected end state (connected/paired) actually landed.
|
||||
readonly property string busyStatus: {
|
||||
for (const r of root.deviceRows) {
|
||||
if (r.pairing) return "pairing:" + r.address
|
||||
if (r.state === BluetoothDeviceState.Connecting) return "connecting:" + r.address
|
||||
if (r.state === BluetoothDeviceState.Disconnecting) return "disconnecting:" + r.address
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
property var pendingAction: null
|
||||
property string actionStatus: ""
|
||||
property string actionStatusAddress: ""
|
||||
|
||||
// Checks whether pendingAction's expected end state actually landed and
|
||||
// reports it, then clears pendingAction. Called both when busyStatus
|
||||
// clears normally and from pendingTimeoutTimer below — an operation
|
||||
// that resolves inside a single binding re-evaluation (a device already
|
||||
// in range reconnecting near-instantly, say) can flip busyStatus from ""
|
||||
// straight back to "" without ever visibly changing, which would
|
||||
// otherwise leave pendingAction stuck forever and misattribute whatever
|
||||
// busyStatus transition happens to come along next.
|
||||
function resolvePendingAction() {
|
||||
if (!root.pendingAction) return
|
||||
const p = root.pendingAction
|
||||
root.pendingAction = null
|
||||
const d = root.deviceForAddress(p.address)
|
||||
root.actionStatusAddress = p.address
|
||||
if (p.kind === "disconnect") {
|
||||
root.actionStatus = (d && !d.connected) ? "Disconnected" : "Couldn't disconnect"
|
||||
} else if (p.kind === "pair") {
|
||||
root.actionStatus = (d && d.paired) ? "Paired" : "Couldn't pair"
|
||||
} else {
|
||||
root.actionStatus = (d && d.connected) ? "Connected" : "Couldn't connect"
|
||||
}
|
||||
actionStatusTimer.restart()
|
||||
}
|
||||
|
||||
onBusyStatusChanged: if (root.busyStatus === "") root.resolvePendingAction()
|
||||
|
||||
Timer {
|
||||
id: actionStatusTimer
|
||||
interval: 3500
|
||||
repeat: false
|
||||
onTriggered: root.actionStatus = ""
|
||||
}
|
||||
|
||||
// Backstop for the "resolved before busyStatus ever visibly changed"
|
||||
// case above, and a genuine timeout for an operation that's just stuck
|
||||
// (device out of range, etc.) — either way, restarted whenever
|
||||
// pendingAction is (re)set, so it always reflects the latest click.
|
||||
Timer {
|
||||
id: pendingTimeoutTimer
|
||||
interval: 8000
|
||||
repeat: false
|
||||
onTriggered: root.resolvePendingAction()
|
||||
}
|
||||
|
||||
function connectDevice(row) {
|
||||
const d = root.deviceForAddress(row.address)
|
||||
if (!d) return
|
||||
// Already-paired devices just reconnect; a fresh discovery result
|
||||
// has to pair first (BlueZ's own agent handles the SSP/passkey
|
||||
// handshake — there's no PIN-entry surface exposed by this API).
|
||||
if (row.paired || row.bonded || row.trusted) {
|
||||
root.pendingAction = { address: row.address, kind: "connect" }
|
||||
pendingTimeoutTimer.restart()
|
||||
d.connect()
|
||||
} else {
|
||||
root.pendingAction = { address: row.address, kind: "pair" }
|
||||
pendingTimeoutTimer.restart()
|
||||
d.pair()
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectDevice(row) {
|
||||
const d = root.deviceForAddress(row.address)
|
||||
if (!d) return
|
||||
root.pendingAction = { address: row.address, kind: "disconnect" }
|
||||
pendingTimeoutTimer.restart()
|
||||
d.disconnect()
|
||||
}
|
||||
|
||||
function forgetDevice(row) {
|
||||
const d = root.deviceForAddress(row.address)
|
||||
if (!d) return
|
||||
if (d.connected) d.disconnect()
|
||||
d.forget()
|
||||
// No feedback line here — forgetting removes this row from every
|
||||
// list immediately, so there's nothing left to show a message
|
||||
// under. The row vanishing is the confirmation.
|
||||
}
|
||||
|
||||
function setTrusted(row, trusted) {
|
||||
const d = root.deviceForAddress(row.address)
|
||||
if (!d) return
|
||||
d.trusted = trusted
|
||||
root.actionStatusAddress = row.address
|
||||
root.actionStatus = trusted ? "Trusting" : "No longer trusting"
|
||||
actionStatusTimer.restart()
|
||||
}
|
||||
|
||||
readonly property bool anyConnected: root.connectedRows.length > 0
|
||||
|
||||
// Caps how tall the Paired/Available list gets before it scrolls
|
||||
// instead of continuing to grow the popup — same convention as
|
||||
// NetworkIndicator.qml's Known/Other Networks list.
|
||||
readonly property int maxDeviceListHeight: 260
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.iconLeftPadding
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 16
|
||||
color: (root.adapter && root.adapter.enabled) ? Colors.foreground : Colors.muted
|
||||
text: {
|
||||
if (!root.adapter || !root.adapter.enabled) return "bluetooth_disabled"
|
||||
return root.anyConnected ? "bluetooth_connected" : "bluetooth"
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
panel.visible = !panel.visible
|
||||
grab.active = panel.visible
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reusable pieces ---------------------------------------------------
|
||||
|
||||
component ToggleSwitch: Item {
|
||||
id: toggleRoot
|
||||
property bool checked: false
|
||||
signal toggled()
|
||||
implicitWidth: 34
|
||||
implicitHeight: 18
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 0
|
||||
color: toggleRoot.checked ? Colors.primary : Colors.muted
|
||||
opacity: toggleRoot.checked ? 1 : 0.35
|
||||
}
|
||||
// Hover-only outline, offset outside the switch itself rather than
|
||||
// drawn on its edge — the switch lags behind a click until the
|
||||
// backend confirms it (e.g. the adapter's enabled state), so this
|
||||
// is just a "you can click this" cue, not click feedback.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -4
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: toggleMouse.containsMouse ? 1 : 0
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.height - 4
|
||||
height: parent.height - 4
|
||||
radius: 0
|
||||
color: Colors.background
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: toggleRoot.checked ? parent.width - width - 2 : 2
|
||||
Behavior on x { NumberAnimation { duration: Appearance.animationDuration } }
|
||||
}
|
||||
MouseArea {
|
||||
id: toggleMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: toggleRoot.toggled()
|
||||
}
|
||||
}
|
||||
|
||||
// One device row. `showForget` gates the whole trailing action cluster —
|
||||
// trust toggle, disconnect, forget — for discovered-but-unpaired devices,
|
||||
// which have none of those to act on yet (a plain click there pairs
|
||||
// instead, via activate()). For a paired/connected row, the row body
|
||||
// itself still connects/disconnects (activate()) same as before; the
|
||||
// actions below are the explicit, discoverable versions of that plus
|
||||
// the two this row previously had no way to reach at all.
|
||||
component DeviceRow: Rectangle {
|
||||
id: devRow
|
||||
required property var dev
|
||||
property bool showForget: true
|
||||
signal activate()
|
||||
width: parent ? parent.width : 0
|
||||
radius: 0
|
||||
implicitHeight: rowContent.implicitHeight + 12
|
||||
|
||||
// Which action icon (if any) the pointer is currently over — drives
|
||||
// both that icon's own hover color below and rowMouse's click
|
||||
// dispatch. Derived from rowMouse.mouseX/containsMouse rather than a
|
||||
// MouseArea on each icon: those icons live inside rowContent, which
|
||||
// sits underneath rowMouse in stacking order, so a nested MouseArea
|
||||
// there would just be shadowed by it (same reason
|
||||
// NetworkIndicator.qml's NetworkRow dispatches by position too).
|
||||
// Checked right-to-left since that's the order the icons are laid
|
||||
// out in, so an earlier icon's "from here to the row's right edge"
|
||||
// zone doesn't swallow the ones after it.
|
||||
readonly property string hoveredAction: {
|
||||
if (!rowMouse.containsMouse) return ""
|
||||
if (devRow.showForget && forgetIcon.visible
|
||||
&& rowMouse.mouseX >= devRow.mapFromItem(forgetIcon, 0, 0).x - 8) return "forget"
|
||||
if (devRow.showForget && trustIcon.visible
|
||||
&& rowMouse.mouseX >= devRow.mapFromItem(trustIcon, 0, 0).x - 8) return "trust"
|
||||
if (disconnectIcon.visible
|
||||
&& rowMouse.mouseX >= devRow.mapFromItem(disconnectIcon, 0, 0).x - 8) return "disconnect"
|
||||
return ""
|
||||
}
|
||||
// Hover checked first (same ordering as Volume.qml's DeviceRow):
|
||||
// hovering always shows a solid Colors.primary fill, even for an
|
||||
// already-connected row, so the primaryText used below never has to
|
||||
// sit on top of the dimmer translucent tint instead.
|
||||
color: rowMouse.containsMouse ? Colors.primary
|
||||
: (devRow.dev.connected ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
|
||||
|
||||
// Hover-only outline, offset outside the row rather than drawn on
|
||||
// its own edge — same treatment as NetworkIndicator.qml's NetworkRow.
|
||||
// The lists this feeds are inset by 3px (see deviceListColumn and
|
||||
// the Connected section below) so this has room without being
|
||||
// cropped by deviceListFlick's clip.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: rowMouse.containsMouse ? 1 : 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: 6
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
Layout.preferredWidth: 20
|
||||
Layout.minimumWidth: 20
|
||||
Layout.maximumWidth: 20
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: devRow.dev.icon
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 14
|
||||
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: devRow.dev.label
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSize
|
||||
font.bold: devRow.dev.connected
|
||||
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
Text {
|
||||
readonly property string statusText: {
|
||||
if (devRow.dev.pairing) return "Pairing…"
|
||||
if (devRow.dev.state === BluetoothDeviceState.Connecting) return "Connecting…"
|
||||
if (devRow.dev.state === BluetoothDeviceState.Disconnecting) return "Disconnecting…"
|
||||
// The outcome of the last action taken on this
|
||||
// specific device (connect/disconnect/pair/trust) —
|
||||
// see the "Action feedback" comment above
|
||||
// root.busyStatus. Matched by address rather than by
|
||||
// this being "the same row" that was clicked, since
|
||||
// a successful connect/disconnect moves the device
|
||||
// to a different section (a new DeviceRow entirely).
|
||||
if (root.actionStatusAddress === devRow.dev.address && root.actionStatus !== "")
|
||||
return root.actionStatus
|
||||
if (devRow.dev.connected && devRow.dev.batteryAvailable)
|
||||
return Math.round(devRow.dev.battery * 100) + "%"
|
||||
if (devRow.dev.connected) return "Connected"
|
||||
return ""
|
||||
}
|
||||
visible: statusText !== ""
|
||||
text: statusText
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
|
||||
}
|
||||
}
|
||||
|
||||
// Each action below is a fixed-size slot with its own
|
||||
// hover-halo outline — offset outside the slot rather than drawn
|
||||
// on its own edge, same treatment as devRow's own outline above
|
||||
// and every other hoverable box in this file (ToggleSwitch,
|
||||
// DeviceRow itself, etc.) — instead of just the color swap the
|
||||
// row's own icons get.
|
||||
|
||||
// Disconnect without forgetting — same effect as clicking the
|
||||
// row body on a Connected row (activate() below calls
|
||||
// disconnectDevice too), just given its own explicit, always-
|
||||
// visible icon instead of relying on that being guessed.
|
||||
Item {
|
||||
id: disconnectIcon
|
||||
visible: devRow.showForget && devRow.dev.connected
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: devRow.hoveredAction === "disconnect" ? 1 : 0
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "link_off"
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 14
|
||||
// Stays primaryText (dark) rather than switching to
|
||||
// Colors.primary while targeted — devRow's own fill is
|
||||
// already Colors.primary on hover, so that would've put
|
||||
// a primary-colored glyph on a primary-colored
|
||||
// background. The outline Rectangle above is what shows
|
||||
// which slot is targeted.
|
||||
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
|
||||
}
|
||||
}
|
||||
|
||||
// Trust toggle — full brightness when trusted, dimmed when not,
|
||||
// same "state via opacity" convention PowerButton.qml uses for
|
||||
// its disabled entries.
|
||||
Item {
|
||||
id: trustIcon
|
||||
visible: devRow.showForget
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: devRow.hoveredAction === "trust" ? 1 : 0
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "verified_user"
|
||||
opacity: devRow.dev.trusted ? 1 : 0.4
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 14
|
||||
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: forgetIcon
|
||||
visible: devRow.showForget
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: devRow.hoveredAction === "forget" ? 1 : 0
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 14
|
||||
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowMouse
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: rowContent.height + 12
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// Dispatches on devRow.hoveredAction rather than its own
|
||||
// MouseArea per icon — see that property's comment above.
|
||||
onClicked: {
|
||||
switch (devRow.hoveredAction) {
|
||||
case "forget": root.forgetDevice(devRow.dev); break
|
||||
case "trust": root.setTrusted(devRow.dev, !devRow.dev.trusted); break
|
||||
case "disconnect": root.disconnectDevice(devRow.dev); break
|
||||
default: devRow.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Panel -------------------------------------------------------------
|
||||
|
||||
PopupWindow {
|
||||
id: panel
|
||||
visible: false
|
||||
color: "transparent"
|
||||
// Wider than the other panels' usual 244 — each device row now ends
|
||||
// in a row of three 20px action slots (disconnect/trust/forget)
|
||||
// instead of a single close icon, and the name column needs room to
|
||||
// not immediately start eliding against that.
|
||||
readonly property int contentWidth: 300
|
||||
readonly property int contentPadding: 16
|
||||
implicitWidth: contentWidth + contentPadding * 2
|
||||
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
||||
// Connected/paired/discovered devices come and go while the panel
|
||||
// is open — animate the resize instead of snapping, same as the
|
||||
// network panel's WiFi list.
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
anchor {
|
||||
window: root.barWindow
|
||||
rect.x: 0
|
||||
rect.y: 0
|
||||
rect.width: root.barWindow.width
|
||||
rect.height: root.barWindow.height
|
||||
edges: Edges.Bottom | Edges.Right
|
||||
gravity: Edges.Bottom | Edges.Left
|
||||
margins.bottom: -4
|
||||
margins.right: 0
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: label.text
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 21
|
||||
color: Colors.foreground
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "Bluetooth"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: Colors.foreground
|
||||
}
|
||||
|
||||
// Spins continuously while root.adapter.discovering is
|
||||
// true — a persistent "still searching" cue independent
|
||||
// of the list itself, unlike the "Scanning for devices…"
|
||||
// text below (which only shows while the device list is
|
||||
// still empty).
|
||||
Text {
|
||||
id: scanSpinner
|
||||
visible: root.adapter !== null && root.adapter.discovering
|
||||
text: "progress_activity"
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 16
|
||||
color: Colors.muted
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: scanSpinner.visible
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 900
|
||||
}
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
visible: root.adapter !== null
|
||||
checked: root.adapter !== null && root.adapter.enabled
|
||||
onToggled: root.adapter.enabled = !root.adapter.enabled
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.adapter === null
|
||||
text: "No Bluetooth adapter"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.adapter !== null && !root.adapter.enabled
|
||||
text: "Turn Bluetooth on to see devices"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
|
||||
// Connected — always shown in full above the scrollable
|
||||
// list, mirroring the network panel's layout.
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
visible: root.connectedRows.length > 0
|
||||
|
||||
Text {
|
||||
text: "Connected"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
Column {
|
||||
// Inset 3px on each side to match deviceListColumn
|
||||
// below, so DeviceRow's hover halo has room here too.
|
||||
x: 3
|
||||
width: parent.width - 6
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: root.connectedRows
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
dev: modelData
|
||||
showForget: true
|
||||
onActivate: root.disconnectDevice(dev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paired — always shown in full, same as Connected above and
|
||||
// for the same reason: these are one click away from
|
||||
// reconnecting (root.connectDevice reconnects rather than
|
||||
// re-pairing whenever a row is already paired/bonded/trusted),
|
||||
// so they shouldn't be buried in the capped-height scroll area
|
||||
// below or hidden behind an active scan turning up other
|
||||
// nearby devices.
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
visible: root.knownRows.length > 0
|
||||
|
||||
Text {
|
||||
text: "Paired Devices"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
Column {
|
||||
// Inset 3px on each side to match deviceListColumn
|
||||
// below, so DeviceRow's hover halo has room here too.
|
||||
x: 3
|
||||
width: parent.width - 6
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: root.knownRows
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
dev: modelData
|
||||
showForget: true
|
||||
onActivate: root.connectDevice(dev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.adapter !== null && root.adapter.enabled && root.deviceRows.length === 0
|
||||
text: (root.adapter && root.adapter.discovering) ? "Scanning for devices…" : "No devices found"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
|
||||
// Available — freshly discovered, unpaired devices only.
|
||||
// Capped height, scrolls once there are more than fit rather
|
||||
// than growing the popup indefinitely.
|
||||
Flickable {
|
||||
id: deviceListFlick
|
||||
width: parent.width
|
||||
height: Math.min(deviceListColumn.implicitHeight, root.maxDeviceListHeight)
|
||||
contentWidth: width
|
||||
contentHeight: deviceListColumn.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
visible: deviceListColumn.implicitHeight > 0
|
||||
|
||||
Column {
|
||||
id: deviceListColumn
|
||||
// Inset 3px on every side so DeviceRow's hover halo
|
||||
// has room without being cropped by deviceListFlick's
|
||||
// clip. Left/right needs the manual x/width shift
|
||||
// (children bind width: parent.width, which ignores
|
||||
// Column's own padding); top/bottom just needs
|
||||
// padding, since that's along Column's own layout
|
||||
// axis and Flickable's contentHeight tracks it.
|
||||
x: 3
|
||||
width: deviceListFlick.width - 6
|
||||
topPadding: 3
|
||||
bottomPadding: 3
|
||||
spacing: 6
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
visible: root.adapter !== null && root.adapter.discovering && root.discoveredRows.length > 0
|
||||
|
||||
Text {
|
||||
text: "Available Devices"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: root.discoveredRows
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
dev: modelData
|
||||
showForget: false
|
||||
onActivate: root.connectDevice(dev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
dotfiles/.config/quickshell/modules/bar/Clock.qml
Normal file
18
dotfiles/.config/quickshell/modules/bar/Clock.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import "../../config"
|
||||
|
||||
Text {
|
||||
id: root
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSize
|
||||
font.bold: true
|
||||
color: Colors.foreground
|
||||
text: Qt.formatDateTime(new Date(), "ddd d MMM hh:mm")
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.text = Qt.formatDateTime(new Date(), "ddd d MMM hh:mm")
|
||||
}
|
||||
}
|
||||
1538
dotfiles/.config/quickshell/modules/bar/NetworkIndicator.qml
Normal file
1538
dotfiles/.config/quickshell/modules/bar/NetworkIndicator.qml
Normal file
File diff suppressed because it is too large
Load Diff
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
200
dotfiles/.config/quickshell/modules/bar/PowerButton.qml
Normal file
200
dotfiles/.config/quickshell/modules/bar/PowerButton.qml
Normal file
@@ -0,0 +1,200 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import "../../config"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitWidth: label.implicitWidth + 2
|
||||
implicitHeight: Appearance.barHeight
|
||||
|
||||
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
|
||||
// anchor to the bar itself rather than to this icon, keeping it a fixed
|
||||
// distance from the bar and the screen edge regardless of where this
|
||||
// icon sits in the row.
|
||||
required property var barWindow
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 16
|
||||
color: Colors.foreground
|
||||
text: "settings_power"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
menu.visible = !menu.visible
|
||||
grab.active = menu.visible
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: proc }
|
||||
|
||||
PopupWindow {
|
||||
id: menu
|
||||
visible: false
|
||||
color: "transparent"
|
||||
// Same contentWidth/contentPadding split as the other panels
|
||||
// (Volume.qml, NetworkIndicator.qml, etc.) rather than a single
|
||||
// magic implicitWidth.
|
||||
readonly property int contentWidth: 178
|
||||
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 (and thus the true screen edges)
|
||||
// instead of a near-zero rect at the window's origin.
|
||||
rect.x: 0
|
||||
rect.y: 0
|
||||
rect.width: root.barWindow.width
|
||||
rect.height: root.barWindow.height
|
||||
edges: Edges.Bottom | Edges.Right
|
||||
gravity: Edges.Bottom | Edges.Left
|
||||
margins.bottom: -4
|
||||
margins.right: 0
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [menu]
|
||||
onCleared: menu.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: menu.contentPadding
|
||||
spacing: 10
|
||||
|
||||
// Header — same title layout as Volume.qml's panel: the bar
|
||||
// icon large, plus a bold title.
|
||||
RowLayout {
|
||||
id: header
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: label.text
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 23
|
||||
color: Colors.foreground
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "Power"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: Colors.foreground
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
id: list
|
||||
model: [
|
||||
{ label: "Lock", icon: "lock", cmd: ["hyprlock"] },
|
||||
{ label: "Suspend", icon: "bedtime", cmd: ["systemctl", "suspend"] },
|
||||
{ label: "Hibernate", icon: "ac_unit", cmd: ["systemctl", "hibernate"] },
|
||||
{ label: "Log out", icon: "logout", cmd: ["hyprctl", "dispatch", "exit"] },
|
||||
{ label: "Reboot", icon: "restart_alt", cmd: ["systemctl", "reboot"] },
|
||||
{ label: "Shut down", icon: "power_settings_new", cmd: ["systemctl", "poweroff"] }
|
||||
]
|
||||
|
||||
// Same shape as this project's other selectable rows
|
||||
// (e.g. VpnIndicator.qml's ProviderRow,
|
||||
// TailscaleIndicator.qml's ExitNodeRow): radius 0, a
|
||||
// fixed-width icon column followed by a fillWidth
|
||||
// label, and an offset hover halo rather than one
|
||||
// drawn on the row's own edge.
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
required property var modelData
|
||||
width: parent ? parent.width : 0
|
||||
radius: 0
|
||||
implicitHeight: entryContent.implicitHeight + 12
|
||||
color: (!entry.modelData.disabled && entryMouse.containsMouse) ? Colors.primary : "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: entryMouse.containsMouse ? 1 : 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: entryContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: 6
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
Layout.preferredWidth: 20
|
||||
Layout.minimumWidth: 20
|
||||
Layout.maximumWidth: 20
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: entry.modelData.icon
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 14
|
||||
opacity: entry.modelData.disabled ? 0.4 : 1
|
||||
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: entry.modelData.label
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSize
|
||||
opacity: entry.modelData.disabled ? 0.4 : 1
|
||||
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: entryMouse
|
||||
anchors.fill: parent
|
||||
enabled: !entry.modelData.disabled
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
proc.command = entry.modelData.cmd
|
||||
proc.running = true
|
||||
menu.visible = false
|
||||
grab.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1175
dotfiles/.config/quickshell/modules/bar/TailscaleIndicator.qml
Normal file
1175
dotfiles/.config/quickshell/modules/bar/TailscaleIndicator.qml
Normal file
File diff suppressed because it is too large
Load Diff
32
dotfiles/.config/quickshell/modules/bar/Tray.qml
Normal file
32
dotfiles/.config/quickshell/modules/bar/Tray.qml
Normal file
@@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import "../../config"
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: IconImage {
|
||||
id: trayIcon
|
||||
required property var modelData
|
||||
implicitWidth: 16
|
||||
implicitHeight: 16
|
||||
source: modelData.icon
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton)
|
||||
trayIcon.modelData.activate()
|
||||
else
|
||||
trayIcon.modelData.secondaryActivate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
506
dotfiles/.config/quickshell/modules/bar/Volume.qml
Normal file
506
dotfiles/.config/quickshell/modules/bar/Volume.qml
Normal file
@@ -0,0 +1,506 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Pipewire
|
||||
import "../../config"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitWidth: label.implicitWidth
|
||||
implicitHeight: Appearance.barHeight
|
||||
|
||||
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
|
||||
// anchor to the bar itself rather than to this icon, keeping it a fixed
|
||||
// distance from the bar and the screen edge regardless of where this
|
||||
// icon sits in the row.
|
||||
required property var barWindow
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property var source: Pipewire.defaultAudioSource
|
||||
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
|
||||
|
||||
readonly property real outputVolume: (root.sink && root.sink.audio) ? root.sink.audio.volume : 0
|
||||
readonly property bool outputMuted: (root.sink && root.sink.audio) ? root.sink.audio.muted : false
|
||||
readonly property real inputVolume: (root.source && root.source.audio) ? root.source.audio.volume : 0
|
||||
readonly property bool inputMuted: (root.source && root.source.audio) ? root.source.audio.muted : false
|
||||
|
||||
readonly property var candidateSinks: root.nodes.filter(function(n) { return n && n.isSink && !n.isStream })
|
||||
readonly property var candidateSources: root.nodes.filter(function(n) { return n && !n.isSink && !n.isStream && n.audio })
|
||||
|
||||
PwObjectTracker { objects: root.candidateSinks }
|
||||
PwObjectTracker { objects: root.candidateSources }
|
||||
|
||||
// Live mic level for the input gauge below — separate from inputVolume
|
||||
// (the configured gain), this tracks the actual incoming signal.
|
||||
// Only runs while the panel is open, to skip metering when unused.
|
||||
PwNodePeakMonitor {
|
||||
id: inputPeak
|
||||
node: root.source
|
||||
enabled: panel.visible
|
||||
}
|
||||
|
||||
PwNodePeakMonitor {
|
||||
id: outputPeak
|
||||
node: root.sink
|
||||
enabled: panel.visible
|
||||
}
|
||||
|
||||
// --- Device label/icon heuristics, ported from omarchy's audio panel
|
||||
// Model.js (github.com/basecamp/omarchy shell/plugins/panels/audio/Model.js),
|
||||
// adapted to this project's Material Symbols icon set instead of Nerd Font.
|
||||
function friendlyDeviceLabel(text) {
|
||||
var out = String(text || "").trim()
|
||||
out = out.replace(/^sof-soundwire\s+/i, "")
|
||||
out = out.replace(/^built-?in audio\s+/i, "")
|
||||
out = out.replace(/\s+Output$/i, "")
|
||||
out = out.replace(/\s+Input$/i, "")
|
||||
out = out.replace(/Microphones/gi, "Microphone")
|
||||
return out
|
||||
}
|
||||
|
||||
function deviceLabel(node) {
|
||||
if (!node) return "Unknown"
|
||||
var nick = friendlyDeviceLabel(node.nickname || "")
|
||||
if (nick) return nick
|
||||
return friendlyDeviceLabel(node.description || node.name || "Unknown")
|
||||
}
|
||||
|
||||
function deviceBlob(node) {
|
||||
if (!node) return ""
|
||||
var p = node.properties || {}
|
||||
return [node.name, node.description, node.nickname,
|
||||
p["device.icon-name"] || "", p["device.product.name"] || ""].join(" ").toLowerCase()
|
||||
}
|
||||
|
||||
// Quickshell's Pipewire API only exposes a node's own properties, not
|
||||
// its parent Device — so device.api (unlike device.form-factor or the
|
||||
// richer device.icon-name, which only live on the Device object) is the
|
||||
// one reliable Bluetooth signal actually present here: it's set on
|
||||
// every node PipeWire hands off to a bluez5-backed sink/source. Checking
|
||||
// it directly beats sniffing the word "bluetooth" out of node names,
|
||||
// which usually just say "bluez_output..." and never match.
|
||||
function isBluetoothNode(node) {
|
||||
var p = (node && node.properties) || {}
|
||||
return p["device.api"] === "bluez5"
|
||||
}
|
||||
|
||||
// PipeWire's own device.form-factor (which would call ACCENTUM a
|
||||
// "headset" outright) isn't reachable from here for the same reason —
|
||||
// it lives on the Device object, not the node. And some devices (a
|
||||
// generic USB DAC like an EKSA headset) have no form-factor anywhere in
|
||||
// PipeWire at all, headset or not, so no property-based check can catch
|
||||
// them. This override list is the deliberate manual escape hatch for
|
||||
// exactly that case — add a distinctive substring from the product name
|
||||
// (device.product.name/description, matched case-insensitively via
|
||||
// deviceBlob) whenever a new no-metadata device shows up misclassified.
|
||||
readonly property var headphoneNameOverrides: ["e900plus"]
|
||||
|
||||
function isHeadphoneNode(node) {
|
||||
var blob = deviceBlob(node)
|
||||
if (blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1
|
||||
|| blob.indexOf("earbud") !== -1 || blob.indexOf("earphone") !== -1
|
||||
|| blob.indexOf("airpod") !== -1)
|
||||
return true
|
||||
return root.headphoneNameOverrides.some(function(needle) { return blob.indexOf(needle) !== -1 })
|
||||
}
|
||||
|
||||
function sinkIcon(node) {
|
||||
if (!node) return "speaker"
|
||||
if (isHeadphoneNode(node)) return "headphones"
|
||||
if (isBluetoothNode(node)) return "bluetooth_audio"
|
||||
var blob = deviceBlob(node)
|
||||
if (blob.indexOf("hdmi") !== -1 || blob.indexOf("display") !== -1) return "tv"
|
||||
return "speaker"
|
||||
}
|
||||
|
||||
function sourceIcon(node) {
|
||||
if (!node) return "mic"
|
||||
if (isHeadphoneNode(node)) return "headphones"
|
||||
if (isBluetoothNode(node)) return "bluetooth_audio"
|
||||
var blob = deviceBlob(node)
|
||||
if (blob.indexOf("webcam") !== -1 || blob.indexOf("camera") !== -1) return "videocam"
|
||||
return "mic"
|
||||
}
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 18
|
||||
color: Colors.foreground
|
||||
text: (root.outputMuted || root.outputVolume <= 0) ? "volume_off"
|
||||
: (root.outputVolume < 0.5 ? "volume_down" : "volume_up")
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
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: {
|
||||
panel.visible = !panel.visible
|
||||
grab.active = panel.visible
|
||||
}
|
||||
onWheel: (wheel) => {
|
||||
if (!root.sink || !root.sink.audio) return
|
||||
wheelAccum += wheel.angleDelta.y
|
||||
const steps = Math.trunc(wheelAccum / unitsPerStep)
|
||||
if (steps === 0) return
|
||||
wheelAccum -= steps * unitsPerStep
|
||||
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + steps * volumeStep))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reusable pieces -----------------------------------------------
|
||||
|
||||
component VolumeSlider: Item {
|
||||
id: sliderRoot
|
||||
property real value: 0
|
||||
property bool enabled: true
|
||||
signal moved(real newValue)
|
||||
implicitHeight: 16
|
||||
|
||||
Rectangle {
|
||||
id: track
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: 4
|
||||
radius: 0
|
||||
color: Colors.muted
|
||||
opacity: 0.35
|
||||
}
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: track.width * Math.max(0, Math.min(1, sliderRoot.value))
|
||||
height: 4
|
||||
radius: 0
|
||||
color: Colors.primary
|
||||
opacity: sliderRoot.enabled ? 1 : 0.4
|
||||
}
|
||||
Rectangle {
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: Colors.primary
|
||||
opacity: sliderRoot.enabled ? 1 : 0.4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Math.max(0, Math.min(track.width - width, track.width * sliderRoot.value - width / 2))
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: sliderRoot.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
function updateFromX(mx) {
|
||||
sliderRoot.moved(Math.max(0, Math.min(1, mx / track.width)))
|
||||
}
|
||||
onPressed: (mouse) => updateFromX(mouse.x)
|
||||
onPositionChanged: (mouse) => { if (pressed) updateFromX(mouse.x) }
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only live level gauge — same track styling as VolumeSlider but no
|
||||
// handle/interaction, filled from an actual signal level instead of a
|
||||
// configured value.
|
||||
component LevelMeter: Item {
|
||||
id: meterRoot
|
||||
property real level: 0
|
||||
implicitHeight: 4
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: 4
|
||||
radius: 0
|
||||
color: Colors.muted
|
||||
opacity: 0.25
|
||||
}
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
height: 4
|
||||
radius: 0
|
||||
color: Colors.paletteColor(2)
|
||||
width: parent.width * Math.max(0, Math.min(1, meterRoot.level))
|
||||
Behavior on width { NumberAnimation { duration: 80 } }
|
||||
}
|
||||
}
|
||||
|
||||
component ToggleSwitch: Item {
|
||||
id: toggleRoot
|
||||
property bool checked: false
|
||||
signal toggled()
|
||||
implicitWidth: 34
|
||||
implicitHeight: 18
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 0
|
||||
color: toggleRoot.checked ? Colors.primary : Colors.muted
|
||||
opacity: toggleRoot.checked ? 1 : 0.35
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.height - 4
|
||||
height: parent.height - 4
|
||||
radius: 0
|
||||
color: Colors.background
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: toggleRoot.checked ? parent.width - width - 2 : 2
|
||||
Behavior on x { NumberAnimation { duration: Appearance.animationDuration } }
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: toggleRoot.toggled()
|
||||
}
|
||||
}
|
||||
|
||||
component DeviceRow: Rectangle {
|
||||
id: deviceRow
|
||||
required property var node
|
||||
required property string icon
|
||||
required property bool active
|
||||
signal activate()
|
||||
width: parent ? parent.width : 0
|
||||
height: 30
|
||||
radius: 0
|
||||
color: rowMouse.containsMouse ? Colors.primary
|
||||
: (deviceRow.active ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
|
||||
|
||||
// Hover-only outline, offset outside the row rather than drawn on
|
||||
// its own edge — same treatment as the other bar widgets' selectable
|
||||
// rows (e.g. NetworkIndicator.qml's NetworkRow). Not inside a
|
||||
// clipped Flickable here, so no inset needed on its container.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: rowMouse.containsMouse ? 1 : 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
Text {
|
||||
Layout.preferredWidth: 18
|
||||
Layout.minimumWidth: 18
|
||||
Layout.maximumWidth: 18
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: deviceRow.icon
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 14
|
||||
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.deviceLabel(deviceRow.node)
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSize
|
||||
font.bold: deviceRow.active
|
||||
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: rowMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: deviceRow.activate()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Panel -----------------------------------------------------------
|
||||
|
||||
PopupWindow {
|
||||
id: panel
|
||||
visible: false
|
||||
color: "transparent"
|
||||
// Padding between the panel's border and its content. implicitWidth
|
||||
// is contentWidth + padding on both sides, so the content area
|
||||
// itself stays a fixed size regardless of how much padding surrounds it.
|
||||
readonly property int contentWidth: 244
|
||||
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 (and thus the true screen edges)
|
||||
// instead of a near-zero rect at the window's origin.
|
||||
rect.x: 0
|
||||
rect.y: 0
|
||||
rect.width: root.barWindow.width
|
||||
rect.height: root.barWindow.height
|
||||
edges: Edges.Bottom | Edges.Right
|
||||
gravity: Edges.Bottom | Edges.Left
|
||||
margins.bottom: -4
|
||||
margins.right: 0
|
||||
}
|
||||
|
||||
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 - 2
|
||||
spacing: 10
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: label.text
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 30
|
||||
color: Colors.foreground
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "Audio"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: Colors.foreground
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
checked: !root.outputMuted
|
||||
onToggled: if (root.sink && root.sink.audio) root.sink.audio.muted = !root.sink.audio.muted
|
||||
}
|
||||
}
|
||||
|
||||
// Output
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "OUTPUT"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
Text {
|
||||
text: Math.round(root.outputVolume * 100) + "%"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
}
|
||||
|
||||
VolumeSlider {
|
||||
width: parent.width
|
||||
value: root.outputVolume
|
||||
enabled: !!root.sink
|
||||
onMoved: (v) => { if (root.sink && root.sink.audio) root.sink.audio.volume = v }
|
||||
}
|
||||
|
||||
LevelMeter {
|
||||
width: parent.width
|
||||
level: outputPeak.peak
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
Repeater {
|
||||
model: root.candidateSinks
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
node: modelData
|
||||
icon: root.sinkIcon(modelData)
|
||||
active: !!(root.sink && modelData && root.sink.id === modelData.id)
|
||||
onActivate: Pipewire.preferredDefaultAudioSink = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "INPUT"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
Text {
|
||||
text: Math.round(root.inputVolume * 100) + "%"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: Colors.muted
|
||||
}
|
||||
}
|
||||
|
||||
VolumeSlider {
|
||||
width: parent.width
|
||||
value: root.inputVolume
|
||||
enabled: !!root.source
|
||||
onMoved: (v) => { if (root.source && root.source.audio) root.source.audio.volume = v }
|
||||
}
|
||||
|
||||
LevelMeter {
|
||||
width: parent.width
|
||||
level: inputPeak.peak
|
||||
}
|
||||
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
Repeater {
|
||||
model: root.candidateSources
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
node: modelData
|
||||
icon: root.sourceIcon(modelData)
|
||||
active: !!(root.source && modelData && root.source.id === modelData.id)
|
||||
onActivate: Pipewire.preferredDefaultAudioSource = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1360
dotfiles/.config/quickshell/modules/bar/VpnIndicator.qml
Normal file
1360
dotfiles/.config/quickshell/modules/bar/VpnIndicator.qml
Normal file
File diff suppressed because it is too large
Load Diff
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
dotfiles/.config/quickshell/modules/bar/Workspaces.qml
Normal file
45
dotfiles/.config/quickshell/modules/bar/Workspaces.qml
Normal file
@@ -0,0 +1,45 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import "../../config"
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
property var screen
|
||||
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: Hyprland.workspaces
|
||||
|
||||
delegate: Rectangle {
|
||||
id: pill
|
||||
required property var modelData
|
||||
visible: modelData.monitor && root.screen && modelData.monitor.name === root.screen.name
|
||||
|
||||
implicitWidth: 26
|
||||
implicitHeight: 22
|
||||
radius: Appearance.radiusSmall
|
||||
color: modelData.urgent ? Colors.urgent
|
||||
: modelData.active ? Colors.primary
|
||||
: "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Appearance.animationDuration } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.name
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: modelData.active ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// 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) + " })")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
dotfiles/.config/quickshell/modules/osd/VolumeOsd.qml
Normal file
122
dotfiles/.config/quickshell/modules/osd/VolumeOsd.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pipewire
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../config"
|
||||
|
||||
PanelWindow {
|
||||
id: osd
|
||||
visible: false
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
|
||||
// Bottom-center: anchoring only the bottom edge lets the layer shell
|
||||
// center it horizontally.
|
||||
anchors { bottom: true }
|
||||
margins.bottom: Appearance.spacing * 4
|
||||
|
||||
implicitWidth: 300
|
||||
implicitHeight: 60
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "quickshell:osd"
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
PwObjectTracker { objects: osd.sink ? [osd.sink] : [] }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 1400
|
||||
onTriggered: osd.visible = false
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: osd.sink ? osd.sink.audio : null
|
||||
function onVolumeChanged() { osd.visible = true; hideTimer.restart() }
|
||||
function onMutedChanged() { osd.visible = true; hideTimer.restart() }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.radius
|
||||
color: Colors.surface
|
||||
border.color: Colors.muted
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 15
|
||||
spacing: 13
|
||||
|
||||
Text {
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 21
|
||||
color: Colors.foreground
|
||||
text: {
|
||||
const audio = osd.sink ? osd.sink.audio : null
|
||||
if (!audio || audio.muted || audio.volume <= 0) return "volume_off"
|
||||
return audio.volume < 0.5 ? "volume_down" : "volume_up"
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: slider
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 18
|
||||
|
||||
readonly property real value: osd.sink && osd.sink.audio ? osd.sink.audio.volume : 0
|
||||
readonly property bool enabled: !!osd.sink
|
||||
|
||||
Rectangle {
|
||||
id: track
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: 5
|
||||
radius: 0
|
||||
color: Colors.muted
|
||||
opacity: 0.35
|
||||
}
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: track.width * Math.max(0, Math.min(1, slider.value))
|
||||
height: 5
|
||||
radius: 0
|
||||
color: Colors.primary
|
||||
opacity: slider.enabled ? 1 : 0.4
|
||||
}
|
||||
Rectangle {
|
||||
width: 14
|
||||
height: 14
|
||||
radius: 7
|
||||
color: Colors.primary
|
||||
opacity: slider.enabled ? 1 : 0.4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Math.max(0, Math.min(track.width - width, track.width * slider.value - width / 2))
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: slider.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
function updateFromX(mx) {
|
||||
if (!osd.sink || !osd.sink.audio) return
|
||||
osd.sink.audio.volume = Math.max(0, Math.min(1, mx / track.width))
|
||||
}
|
||||
onPressed: (mouse) => updateFromX(mouse.x)
|
||||
onPositionChanged: (mouse) => { if (pressed) updateFromX(mouse.x) }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.preferredWidth: 38
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: 13
|
||||
color: Colors.muted
|
||||
text: Math.round(slider.value * 100) + "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
274
dotfiles/.config/quickshell/scripts/CheckQuickshellSetup.sh
Executable file
274
dotfiles/.config/quickshell/scripts/CheckQuickshellSetup.sh
Executable file
@@ -0,0 +1,274 @@
|
||||
#!/bin/bash
|
||||
# Pre-flight check for everything this quickshell config depends on outside
|
||||
# its own QML — fonts, the compositor, backing D-Bus services, and the
|
||||
# external CLIs individual bar modules shell out to. Run this after
|
||||
# installing/stowing quickshell on a new machine, before assuming the bar
|
||||
# is just going to work.
|
||||
#
|
||||
# Usage: ./CheckQuickshellSetup.sh [path-to-quickshell-config]
|
||||
# Defaults to $XDG_CONFIG_HOME/quickshell or ~/.config/quickshell.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
QS_DIR="${1:-${XDG_CONFIG_HOME:-$HOME/.config}/quickshell}"
|
||||
|
||||
PASS=0
|
||||
WARN=0
|
||||
FAIL=0
|
||||
|
||||
c_green="\033[0;32m"; c_yellow="\033[0;33m"; c_red="\033[0;31m"; c_reset="\033[0m"
|
||||
|
||||
pass() { printf " ${c_green}✓${c_reset} %s\n" "$1"; PASS=$((PASS+1)); }
|
||||
warn() { printf " ${c_yellow}!${c_reset} %s\n" "$1"; WARN=$((WARN+1)); }
|
||||
fail() { printf " ${c_red}✗${c_reset} %s\n" "$1"; FAIL=$((FAIL+1)); }
|
||||
section() { printf "\n%s\n" "$1"; }
|
||||
|
||||
svc_state() {
|
||||
# active / inactive / missing / no-systemctl. $1 = unit, $2 = --user (optional)
|
||||
# NB: capture-then-here-string, not `cmd | grep -q`. Under `set -o
|
||||
# pipefail`, grep -q exits the instant it finds a match, SIGPIPE-ing a
|
||||
# still-writing producer — that producer's non-zero exit (not grep's
|
||||
# success) becomes the pipeline's status, which reads as "not found"
|
||||
# even when it was. Verified this actually happens with fc-list below.
|
||||
if ! command -v systemctl >/dev/null 2>&1; then echo "no-systemctl"; return; fi
|
||||
local unit_files
|
||||
unit_files=$(systemctl ${2:-} list-unit-files "$1" 2>/dev/null)
|
||||
if grep -q "$1" <<< "$unit_files"; then
|
||||
systemctl ${2:-} is-active "$1" 2>/dev/null || echo "inactive"
|
||||
else
|
||||
echo "missing"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 0. Quickshell itself + config files present ----------------------------
|
||||
|
||||
section "Quickshell core"
|
||||
|
||||
if command -v quickshell >/dev/null 2>&1; then
|
||||
ver=$(quickshell --version 2>&1 | head -1)
|
||||
pass "quickshell found ($(command -v quickshell)) — $ver"
|
||||
else
|
||||
fail "quickshell binary not found on PATH — install it before anything else here matters"
|
||||
fi
|
||||
|
||||
for f in "shell.qml" "config/Appearance.qml" "config/Colors.qml" "config/qmldir"; do
|
||||
if [ -f "$QS_DIR/$f" ]; then
|
||||
pass "found $f"
|
||||
else
|
||||
fail "missing $QS_DIR/$f — dotfiles not fully stowed/symlinked here?"
|
||||
fi
|
||||
done
|
||||
|
||||
# Nearly every module (ActiveWindow, Workspaces, PowerButton, PanelWindow's
|
||||
# layer-shell surfaces, HyprlandFocusGrab) assumes a live Hyprland session.
|
||||
if [ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ] && command -v hyprctl >/dev/null 2>&1 && hyprctl monitors >/dev/null 2>&1; then
|
||||
pass "Hyprland is the running compositor"
|
||||
else
|
||||
fail "Hyprland doesn't appear to be running — Quickshell.Hyprland and the layer-shell bar will not function"
|
||||
fi
|
||||
|
||||
# --- 1. Fonts -----------------------------------------------------------------
|
||||
|
||||
section "Fonts"
|
||||
|
||||
if command -v fc-list >/dev/null 2>&1; then
|
||||
# Captured once, then grepped via here-string — see the pipefail/SIGPIPE
|
||||
# note on svc_state() above; fc-list's output is exactly what triggers it.
|
||||
fc_out=$(fc-list)
|
||||
|
||||
if grep -qi "Material Symbols Outlined" <<< "$fc_out"; then
|
||||
pass "Material Symbols Outlined installed"
|
||||
else
|
||||
fail "Material Symbols Outlined not found — install 'ttf-material-symbols-variable' (AUR); every glyph icon in the bar/panels will render as fallback text without it"
|
||||
fi
|
||||
|
||||
if grep -qi "CaskaydiaCove Nerd Font Mono" <<< "$fc_out"; then
|
||||
pass "CaskaydiaCove Nerd Font Mono installed"
|
||||
else
|
||||
fail "CaskaydiaCove Nerd Font Mono not found — install a Cascadia Code Nerd Font package; clock/labels/window title use it"
|
||||
fi
|
||||
else
|
||||
warn "fc-list not found (fontconfig missing?) — can't verify fonts"
|
||||
fi
|
||||
|
||||
# --- 2. Backing services for native Quickshell modules ----------------------
|
||||
# These modules talk to system services directly over D-Bus rather than
|
||||
# shelling out — the service being up is the actual dependency, not a CLI.
|
||||
|
||||
section "Backing services (native QML modules)"
|
||||
|
||||
st=$(svc_state NetworkManager.service)
|
||||
case "$st" in
|
||||
active) pass "NetworkManager.service active — Quickshell.Networking (NetworkIndicator)" ;;
|
||||
missing) fail "NetworkManager not installed — required by NetworkIndicator.qml" ;;
|
||||
*) fail "NetworkManager.service is $st — run: sudo systemctl enable --now NetworkManager" ;;
|
||||
esac
|
||||
|
||||
st=$(svc_state bluetooth.service)
|
||||
case "$st" in
|
||||
active) pass "bluetooth.service active — Quickshell.Bluetooth (BluetoothIndicator)" ;;
|
||||
missing) fail "bluez not installed — required by BluetoothIndicator.qml" ;;
|
||||
*) fail "bluetooth.service is $st — run: sudo systemctl enable --now bluetooth" ;;
|
||||
esac
|
||||
|
||||
# Pipewire/wireplumber are user-session services, not system ones.
|
||||
st=$(svc_state pipewire.service --user)
|
||||
case "$st" in
|
||||
active) pass "pipewire.service (user) active — Quickshell.Services.Pipewire (Volume/VolumeOsd)" ;;
|
||||
missing) fail "pipewire not installed — required by Volume.qml/VolumeOsd.qml" ;;
|
||||
*) fail "pipewire.service (user) is $st — run: systemctl --user enable --now pipewire pipewire-pulse wireplumber" ;;
|
||||
esac
|
||||
|
||||
st=$(svc_state wireplumber.service --user)
|
||||
case "$st" in
|
||||
active) pass "wireplumber.service (user) active" ;;
|
||||
missing) warn "wireplumber not installed — pipewire falls back to pipewire-media-session if present, otherwise volume control may misbehave" ;;
|
||||
*) fail "wireplumber.service (user) is $st — run: systemctl --user enable --now wireplumber" ;;
|
||||
esac
|
||||
|
||||
# --- 3. External CLIs shelled out to via Process ----------------------------
|
||||
|
||||
section "External CLIs (Quickshell.Io Process calls)"
|
||||
|
||||
check_bin() {
|
||||
# $1 = binary, $2 = which module(s) use it, $3 = fail|warn on missing
|
||||
if command -v "$1" >/dev/null 2>&1; then
|
||||
pass "$1 found — used by $2"
|
||||
elif [ "$3" = "warn" ]; then
|
||||
warn "$1 not found — used by $2 (optional on this machine)"
|
||||
else
|
||||
fail "$1 not found — used by $2"
|
||||
fi
|
||||
}
|
||||
|
||||
check_bin nmcli "NetworkIndicator.qml (DNS provider panel, connection queries)" fail
|
||||
check_bin ip "NetworkIndicator.qml (gateway/IP lookups)" fail
|
||||
check_bin ping "NetworkIndicator.qml (latency/packet-loss stat)" fail
|
||||
check_bin cat "NetworkIndicator.qml (/sys/class/net throughput counters)" fail
|
||||
check_bin systemctl "PowerButton.qml (suspend/hibernate/reboot/poweroff)" fail
|
||||
check_bin tailscale "TailscaleIndicator.qml / VpnExclusivity" warn
|
||||
check_bin mullvad "VpnIndicator.qml / VpnExclusivity" warn
|
||||
check_bin protonvpn "VpnIndicator.qml / VpnExclusivity" warn
|
||||
|
||||
have_tailscale=0; have_mullvad=0; have_proton=0
|
||||
command -v tailscale >/dev/null 2>&1 && have_tailscale=1
|
||||
command -v mullvad >/dev/null 2>&1 && have_mullvad=1
|
||||
command -v protonvpn >/dev/null 2>&1 && have_proton=1
|
||||
|
||||
if [ $have_mullvad -eq 0 ] && [ $have_proton -eq 0 ]; then
|
||||
warn "neither Mullvad nor ProtonVPN CLI is installed — VpnIndicator.qml will show both as 'Not installed'"
|
||||
fi
|
||||
|
||||
# Keep in sync with homeWgConnection in VpnIndicator.qml.
|
||||
if nmcli -g connection.type connection show unifi-home >/dev/null 2>&1; then
|
||||
pass "NetworkManager profile 'unifi-home' exists — used by VpnIndicator.qml (Home/UniFi row)"
|
||||
else
|
||||
warn "no NetworkManager profile 'unifi-home' — import one from the VPN panel's 'Add config' button (optional)"
|
||||
fi
|
||||
|
||||
# The 'Add config' button's file chooser (scripts/PickFile.py) talks to
|
||||
# xdg-desktop-portal over D-Bus via PyGObject.
|
||||
if python3 -c "from gi.repository import Gio" >/dev/null 2>&1; then
|
||||
pass "PyGObject available — used by PickFile.py (VPN 'Add config' file chooser)"
|
||||
else
|
||||
warn "PyGObject (python-gobject) missing — the VPN panel's 'Add config' button can't open a file chooser"
|
||||
fi
|
||||
|
||||
# --- 4. VPN daemons + functional status (Mullvad/ProtonVPN/Tailscale) -------
|
||||
# The exclusivity logic (VpnExclusivity.qml backing off Tailscale when a
|
||||
# commercial VPN connects) is pure QML and ships with the dotfiles — it
|
||||
# needs no setup of its own. What varies per machine is whether these three
|
||||
# are actually installed, enabled, and callable without a password prompt.
|
||||
|
||||
section "VPN dual-setup (Mullvad/ProtonVPN + Tailscale)"
|
||||
|
||||
if [ $have_tailscale -eq 1 ]; then
|
||||
st=$(svc_state tailscaled.service)
|
||||
case "$st" in
|
||||
active) pass "tailscaled.service is active" ;;
|
||||
missing) fail "tailscaled.service not found — reinstall tailscale" ;;
|
||||
*) fail "tailscaled.service is $st — run: sudo systemctl enable --now tailscaled" ;;
|
||||
esac
|
||||
|
||||
if out=$(timeout 5 tailscale status --json 2>&1); then
|
||||
if python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1 <<< "$out"; then
|
||||
pass "tailscale status --json returns valid JSON"
|
||||
else
|
||||
warn "tailscale status --json returned non-JSON output"
|
||||
fi
|
||||
else
|
||||
fail "tailscale status --json failed: $(echo "$out" | head -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $have_mullvad -eq 1 ]; then
|
||||
st=$(svc_state mullvad-daemon.service)
|
||||
case "$st" in
|
||||
active) pass "mullvad-daemon.service is active" ;;
|
||||
missing) fail "mullvad-daemon.service not found — reinstall the Mullvad package" ;;
|
||||
*) fail "mullvad-daemon.service is $st — run: sudo systemctl enable --now mullvad-daemon" ;;
|
||||
esac
|
||||
|
||||
if out=$(timeout 5 mullvad status --json 2>&1); then
|
||||
if python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1 <<< "$out"; then
|
||||
pass "mullvad status --json returns valid JSON"
|
||||
else
|
||||
warn "mullvad status --json returned non-JSON output"
|
||||
fi
|
||||
else
|
||||
fail "mullvad status --json failed: $(echo "$out" | head -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $have_proton -eq 1 ]; then
|
||||
# ProtonVPN's CLI drives connections through NetworkManager rather than
|
||||
# its own daemon — already checked above, just re-verified here in context.
|
||||
if out=$(timeout 5 protonvpn status 2>&1); then
|
||||
if grep -qi "^Status:" <<< "$out"; then
|
||||
pass "protonvpn status returns a parseable 'Status:' line"
|
||||
else
|
||||
warn "protonvpn status has no 'Status:' line — applyProtonStatus() in VpnIndicator.qml may misread it: $(echo "$out" | head -1)"
|
||||
fi
|
||||
else
|
||||
fail "protonvpn status failed: $(echo "$out" | head -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
ts_up=0; mv_up=0; pv_up=0
|
||||
# Same capture-then-here-string reasoning as above — these outputs are small
|
||||
# enough that the SIGPIPE race is unlikely, but there's no upside to relying
|
||||
# on that instead of just being consistent.
|
||||
[ $have_tailscale -eq 1 ] && grep -q '"BackendState":"Running"' <<< "$(tailscale status --json 2>/dev/null)" && ts_up=1
|
||||
[ $have_mullvad -eq 1 ] && grep -qi "^Connected" <<< "$(mullvad status 2>/dev/null)" && mv_up=1
|
||||
[ $have_proton -eq 1 ] && grep -qi "^Status:\s*Connected" <<< "$(protonvpn status 2>/dev/null)" && pv_up=1
|
||||
|
||||
active_count=$((ts_up + mv_up + pv_up))
|
||||
if [ $active_count -le 1 ]; then
|
||||
pass "at most one tunnel active right now (tailscale=$ts_up mullvad=$mv_up protonvpn=$pv_up)"
|
||||
else
|
||||
warn "more than one tunnel is up right now (tailscale=$ts_up mullvad=$mv_up protonvpn=$pv_up) — bring extras down manually; quickshell only enforces exclusivity when you toggle from its own UI"
|
||||
fi
|
||||
|
||||
# --- 5. System tray (currently disabled in Bar.qml) -------------------------
|
||||
|
||||
section "System tray (Tray.qml)"
|
||||
|
||||
if grep -q "^\s*//\s*Tray\s*{}" "$QS_DIR/modules/bar/Bar.qml" 2>/dev/null; then
|
||||
warn "Tray {} is commented out in Bar.qml — skipping icon-theme check (nothing to render)"
|
||||
elif [ -f "$QS_DIR/modules/bar/Bar.qml" ]; then
|
||||
if command -v gtk-update-icon-cache >/dev/null 2>&1 || [ -d /usr/share/icons/hicolor ]; then
|
||||
pass "an icon theme (at least hicolor) is present for Quickshell.Services.SystemTray icon lookups"
|
||||
else
|
||||
warn "no icon theme found — tray icons may render blank"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Summary -----------------------------------------------------------------
|
||||
|
||||
section "Summary"
|
||||
printf " ${c_green}%d passed${c_reset}, ${c_yellow}%d warnings${c_reset}, ${c_red}%d failed${c_reset}\n" "$PASS" "$WARN" "$FAIL"
|
||||
|
||||
if [ $FAIL -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
84
dotfiles/.config/quickshell/scripts/ImportUnifiWireguard.sh
Executable file
84
dotfiles/.config/quickshell/scripts/ImportUnifiWireguard.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# Imports a UniFi WireGuard VPN Server client config into NetworkManager
|
||||
# under the profile name VpnIndicator.qml expects (homeWgConnection,
|
||||
# "unifi-home" by default), so the bar's VPN panel can toggle it.
|
||||
#
|
||||
# Get the .conf from UniFi Network: Settings → VPN → VPN Server → your
|
||||
# WireGuard server → Add Client → Download.
|
||||
#
|
||||
# Usage: ./ImportUnifiWireguard.sh <client.conf> [profile-name] [--replace]
|
||||
# ./ImportUnifiWireguard.sh --pick [profile-name] [--replace]
|
||||
#
|
||||
# --pick opens a file chooser (PickFile.py) instead of taking a path — this
|
||||
# is what the VPN panel's "Add config" button runs. Exits 3 if cancelled.
|
||||
#
|
||||
# NetworkManager names both the profile and the interface after the file's
|
||||
# basename, so the config is copied to <profile-name>.conf first (interface
|
||||
# names max out at 15 chars). The private key ends up in a root-only file
|
||||
# under /etc/NetworkManager/system-connections — delete the downloaded
|
||||
# .conf afterwards.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
replace=0
|
||||
pick=0
|
||||
args=()
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--replace) replace=1 ;;
|
||||
--pick) pick=1 ;;
|
||||
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) args+=("$a") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ $pick -eq 1 ]; then
|
||||
conf=""
|
||||
name="${args[0]:-unifi-home}"
|
||||
rc=0
|
||||
conf=$("$(dirname "$0")/PickFile.py" "Select UniFi WireGuard config" "*.conf") || rc=$?
|
||||
if [ $rc -eq 1 ]; then echo "Cancelled" >&2; exit 3; fi
|
||||
if [ $rc -ne 0 ]; then echo "Couldn't open a file chooser" >&2; exit 1; fi
|
||||
else
|
||||
conf="${args[0]:-}"
|
||||
name="${args[1]:-unifi-home}"
|
||||
fi
|
||||
|
||||
if [ -z "$conf" ] || [ ! -f "$conf" ]; then
|
||||
echo "Usage: $0 <client.conf> [profile-name] [--replace]" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! [[ "$name" =~ ^[A-Za-z0-9_-]{1,15}$ ]]; then
|
||||
echo "Profile name must be 1-15 chars of [A-Za-z0-9_-] (it doubles as the interface name)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if nmcli -g connection.id connection show "$name" >/dev/null 2>&1; then
|
||||
if [ $replace -eq 1 ]; then
|
||||
nmcli connection delete "$name" >/dev/null
|
||||
else
|
||||
echo "A NetworkManager profile named '$name' already exists — rerun with --replace to overwrite it" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
install -m 600 "$conf" "$tmpdir/$name.conf"
|
||||
|
||||
if ! grep -qi '^\[Interface\]' "$tmpdir/$name.conf" || ! grep -qi '^\[Peer\]' "$tmpdir/$name.conf"; then
|
||||
echo "$(basename "$conf") doesn't look like a WireGuard config" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
nmcli connection import type wireguard file "$tmpdir/$name.conf"
|
||||
# Only ever brought up deliberately from the bar — never on boot or on
|
||||
# joining a network, where it'd silently fight Tailscale/Mullvad/ProtonVPN.
|
||||
nmcli connection modify "$name" connection.autoconnect no
|
||||
# Import itself defaults to autoconnect=yes, so NetworkManager may already
|
||||
# have started bringing it up in the moment before the modify above.
|
||||
nmcli connection down "$name" >/dev/null 2>&1 || true
|
||||
|
||||
echo
|
||||
echo "Imported as '$name'. Toggle it from the VPN panel, or: nmcli connection up $name"
|
||||
echo "You can now delete $conf"
|
||||
74
dotfiles/.config/quickshell/scripts/PickFile.py
Executable file
74
dotfiles/.config/quickshell/scripts/PickFile.py
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
# Opens the xdg-desktop-portal file chooser and prints the picked file's
|
||||
# path. Quickshell has no usable file dialog of its own (QtQuick.Dialogs'
|
||||
# fallback renders inside the calling window, i.e. the tiny bar popup), and
|
||||
# no zenity/kdialog is installed — the portal goes through
|
||||
# xdg-desktop-portal-gtk, which is.
|
||||
#
|
||||
# Usage: PickFile.py [title] [glob ...]
|
||||
# Exits 0 and prints the path on a pick, 1 on cancel, 2 on error.
|
||||
|
||||
import os
|
||||
import sys
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
title = sys.argv[1] if len(sys.argv) > 1 else "Open File"
|
||||
globs = sys.argv[2:]
|
||||
|
||||
try:
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
except GLib.Error as e:
|
||||
print(f"No session bus: {e.message}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# The portal answers on a Request object whose path is derived from our
|
||||
# unique bus name and handle_token, so subscribe to it before calling —
|
||||
# otherwise a fast response could arrive before we're listening.
|
||||
token = f"quickshell_{os.getpid()}"
|
||||
sender = bus.get_unique_name().lstrip(":").replace(".", "_")
|
||||
handle = f"/org/freedesktop/portal/desktop/request/{sender}/{token}"
|
||||
|
||||
loop = GLib.MainLoop()
|
||||
result = {"code": 2, "uris": []}
|
||||
|
||||
|
||||
def on_response(_conn, _sender, _path, _iface, _signal, params):
|
||||
code, results = params.unpack()
|
||||
result["code"] = code
|
||||
result["uris"] = results.get("uris", [])
|
||||
loop.quit()
|
||||
|
||||
|
||||
bus.signal_subscribe("org.freedesktop.portal.Desktop", "org.freedesktop.portal.Request",
|
||||
"Response", handle, None, Gio.DBusSignalFlags.NONE, on_response)
|
||||
|
||||
options = {
|
||||
"handle_token": GLib.Variant("s", token),
|
||||
"modal": GLib.Variant("b", True),
|
||||
}
|
||||
filters = []
|
||||
if globs:
|
||||
filters.append((", ".join(globs), [(0, g) for g in globs]))
|
||||
filters.append(("All files", [(0, "*")]))
|
||||
options["filters"] = GLib.Variant("a(sa(us))", filters)
|
||||
downloads = os.path.expanduser("~/Downloads")
|
||||
if os.path.isdir(downloads):
|
||||
options["current_folder"] = GLib.Variant("ay", downloads.encode() + b"\0")
|
||||
|
||||
try:
|
||||
bus.call_sync("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.FileChooser", "OpenFile",
|
||||
GLib.Variant("(ssa{sv})", ("", title, options)),
|
||||
GLib.VariantType("(o)"), Gio.DBusCallFlags.NONE, -1, None)
|
||||
except GLib.Error as e:
|
||||
print(f"File chooser portal unavailable: {e.message}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
loop.run()
|
||||
|
||||
# Response codes: 0 = picked, 1 = cancelled, 2 = other failure.
|
||||
if result["code"] != 0 or not result["uris"]:
|
||||
sys.exit(1 if result["code"] == 1 else 2)
|
||||
print(unquote(urlparse(result["uris"][0]).path))
|
||||
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.'
|
||||
20
dotfiles/.config/quickshell/shell.qml
Normal file
20
dotfiles/.config/quickshell/shell.qml
Normal file
@@ -0,0 +1,20 @@
|
||||
import Quickshell
|
||||
import "./modules/bar"
|
||||
import "./modules/osd"
|
||||
|
||||
ShellRoot {
|
||||
Variants {
|
||||
// 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 {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
}
|
||||
}
|
||||
|
||||
VolumeOsd {}
|
||||
}
|
||||
102
dotfiles/.config/rofi/applets/bin/appasroot.sh
Executable file
102
dotfiles/.config/rofi/applets/bin/appasroot.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : Run Applications as Root
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Theme Elements
|
||||
prompt='Applications'
|
||||
mesg='Run Applications as Root'
|
||||
|
||||
if [[ "$theme" == *'type-1'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='400px'
|
||||
elif [[ "$theme" == *'type-3'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='120px'
|
||||
elif [[ "$theme" == *'type-5'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='520px'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='5'
|
||||
list_row='1'
|
||||
win_width='670px'
|
||||
fi
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
option_1=" Alacritty"
|
||||
option_2=" Thunar"
|
||||
option_3=" Geany"
|
||||
option_4=" Ranger"
|
||||
option_5=" Vim"
|
||||
else
|
||||
option_1=""
|
||||
option_2=""
|
||||
option_3=""
|
||||
option_4=""
|
||||
option_5=""
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "window {width: $win_width;}" \
|
||||
-theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str 'textbox-prompt-colon {str: "";}' \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4\n$option_5" | rofi_cmd
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
polkit_cmd="pkexec env PATH=$PATH DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY"
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
${polkit_cmd} alacritty
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
${polkit_cmd} dbus-run-session thunar
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
${polkit_cmd} geany
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
${polkit_cmd} alacritty -e ranger
|
||||
elif [[ "$1" == '--opt5' ]]; then
|
||||
${polkit_cmd} alacritty -e vim
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
$option_5)
|
||||
run_cmd --opt5
|
||||
;;
|
||||
esac
|
||||
|
||||
104
dotfiles/.config/rofi/applets/bin/apps.sh
Executable file
104
dotfiles/.config/rofi/applets/bin/apps.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : Favorite Applications
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Theme Elements
|
||||
prompt='Applications'
|
||||
mesg="Installed Packages : `pacman -Q | wc -l` (pacman)"
|
||||
|
||||
if [[ ( "$theme" == *'type-1'* ) || ( "$theme" == *'type-3'* ) || ( "$theme" == *'type-5'* ) ]]; then
|
||||
list_col='1'
|
||||
list_row='6'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='6'
|
||||
list_row='1'
|
||||
fi
|
||||
|
||||
# CMDs (add your apps here)
|
||||
term_cmd='alacritty'
|
||||
file_cmd='thunar'
|
||||
text_cmd='geany'
|
||||
web_cmd='firefox'
|
||||
music_cmd='alacritty -e ncmpcpp'
|
||||
setting_cmd='xfce4-settings-manager'
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
option_1=" Terminal <span weight='light' size='small'><i>($term_cmd)</i></span>"
|
||||
option_2=" Files <span weight='light' size='small'><i>($file_cmd)</i></span>"
|
||||
option_3=" Editor <span weight='light' size='small'><i>($text_cmd)</i></span>"
|
||||
option_4=" Browser <span weight='light' size='small'><i>($web_cmd)</i></span>"
|
||||
option_5=" Music <span weight='light' size='small'><i>($music_cmd)</i></span>"
|
||||
option_6=" Settings <span weight='light' size='small'><i>($setting_cmd)</i></span>"
|
||||
else
|
||||
option_1=""
|
||||
option_2=""
|
||||
option_3=""
|
||||
option_4=""
|
||||
option_5=""
|
||||
option_6=""
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str 'textbox-prompt-colon {str: "";}' \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4\n$option_5\n$option_6" | rofi_cmd
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
${term_cmd}
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
${file_cmd}
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
${text_cmd}
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
${web_cmd}
|
||||
elif [[ "$1" == '--opt5' ]]; then
|
||||
${music_cmd}
|
||||
elif [[ "$1" == '--opt6' ]]; then
|
||||
${setting_cmd}
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
$option_5)
|
||||
run_cmd --opt5
|
||||
;;
|
||||
$option_6)
|
||||
run_cmd --opt6
|
||||
;;
|
||||
esac
|
||||
134
dotfiles/.config/rofi/applets/bin/battery.sh
Executable file
134
dotfiles/.config/rofi/applets/bin/battery.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : Battery
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Battery Info
|
||||
battery="`acpi -b | cut -d',' -f1 | cut -d':' -f1`"
|
||||
status="`acpi -b | cut -d',' -f1 | cut -d':' -f2 | tr -d ' '`"
|
||||
percentage="`acpi -b | cut -d',' -f2 | tr -d ' ',\%`"
|
||||
time="`acpi -b | cut -d',' -f3`"
|
||||
|
||||
if [[ -z "$time" ]]; then
|
||||
time=' Fully Charged'
|
||||
fi
|
||||
|
||||
# Theme Elements
|
||||
prompt="$status"
|
||||
mesg="${battery}: ${percentage}%,${time}"
|
||||
|
||||
if [[ "$theme" == *'type-1'* ]]; then
|
||||
list_col='1'
|
||||
list_row='4'
|
||||
win_width='400px'
|
||||
elif [[ "$theme" == *'type-3'* ]]; then
|
||||
list_col='1'
|
||||
list_row='4'
|
||||
win_width='120px'
|
||||
elif [[ "$theme" == *'type-5'* ]]; then
|
||||
list_col='1'
|
||||
list_row='4'
|
||||
win_width='500px'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='4'
|
||||
list_row='1'
|
||||
win_width='550px'
|
||||
fi
|
||||
|
||||
# Charging Status
|
||||
active=""
|
||||
urgent=""
|
||||
if [[ $status = *"Charging"* ]]; then
|
||||
active="-a 1"
|
||||
ICON_CHRG=""
|
||||
elif [[ $status = *"Full"* ]]; then
|
||||
active="-u 1"
|
||||
ICON_CHRG=""
|
||||
else
|
||||
urgent="-u 1"
|
||||
ICON_CHRG=""
|
||||
fi
|
||||
|
||||
# Discharging
|
||||
if [[ $percentage -ge 5 ]] && [[ $percentage -le 19 ]]; then
|
||||
ICON_DISCHRG=""
|
||||
elif [[ $percentage -ge 20 ]] && [[ $percentage -le 39 ]]; then
|
||||
ICON_DISCHRG=""
|
||||
elif [[ $percentage -ge 40 ]] && [[ $percentage -le 59 ]]; then
|
||||
ICON_DISCHRG=""
|
||||
elif [[ $percentage -ge 60 ]] && [[ $percentage -le 79 ]]; then
|
||||
ICON_DISCHRG=""
|
||||
elif [[ $percentage -ge 80 ]] && [[ $percentage -le 100 ]]; then
|
||||
ICON_DISCHRG=""
|
||||
fi
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
option_1=" Remaining ${percentage}%"
|
||||
option_2=" $status"
|
||||
option_3=" Power Manager"
|
||||
option_4=" Diagnose"
|
||||
else
|
||||
option_1="$ICON_DISCHRG"
|
||||
option_2="$ICON_CHRG"
|
||||
option_3=""
|
||||
option_4=""
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "window {width: $win_width;}" \
|
||||
-theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str "textbox-prompt-colon {str: \"$ICON_DISCHRG\";}" \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
${active} ${urgent} \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4" | rofi_cmd
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
polkit_cmd="pkexec env PATH=$PATH DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY"
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
notify-send -u low " Remaining : ${percentage}%"
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
notify-send -u low "$ICON_CHRG Status : $status"
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
xfce4-power-manager-settings
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
${polkit_cmd} alacritty -e powertop
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
107
dotfiles/.config/rofi/applets/bin/brightness.sh
Executable file
107
dotfiles/.config/rofi/applets/bin/brightness.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : Brightness
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Brightness Info
|
||||
backlight="$(printf "%.0f\n" `light -G`)"
|
||||
card="`light -L | grep 'backlight' | head -n1 | cut -d'/' -f3`"
|
||||
|
||||
if [[ $backlight -ge 0 ]] && [[ $backlight -le 29 ]]; then
|
||||
level="Low"
|
||||
elif [[ $backlight -ge 30 ]] && [[ $backlight -le 49 ]]; then
|
||||
level="Optimal"
|
||||
elif [[ $backlight -ge 50 ]] && [[ $backlight -le 69 ]]; then
|
||||
level="High"
|
||||
elif [[ $backlight -ge 70 ]] && [[ $backlight -le 100 ]]; then
|
||||
level="Peak"
|
||||
fi
|
||||
|
||||
# Theme Elements
|
||||
prompt="${backlight}%"
|
||||
mesg="Device: ${card}, Level: $level"
|
||||
|
||||
if [[ "$theme" == *'type-1'* ]]; then
|
||||
list_col='1'
|
||||
list_row='4'
|
||||
win_width='400px'
|
||||
elif [[ "$theme" == *'type-3'* ]]; then
|
||||
list_col='1'
|
||||
list_row='4'
|
||||
win_width='120px'
|
||||
elif [[ "$theme" == *'type-5'* ]]; then
|
||||
list_col='1'
|
||||
list_row='4'
|
||||
win_width='425px'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='4'
|
||||
list_row='1'
|
||||
win_width='550px'
|
||||
fi
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
option_1=" Increase"
|
||||
option_2=" Optimal"
|
||||
option_3=" Decrease"
|
||||
option_4=" Settings"
|
||||
else
|
||||
option_1=""
|
||||
option_2=""
|
||||
option_3=""
|
||||
option_4=""
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "window {width: $win_width;}" \
|
||||
-theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str 'textbox-prompt-colon {str: "";}' \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4" | rofi_cmd
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
light -A 5
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
light -S 25
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
light -U 5
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
xfce4-power-manager-settings
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
esac
|
||||
131
dotfiles/.config/rofi/applets/bin/mpd.sh
Executable file
131
dotfiles/.config/rofi/applets/bin/mpd.sh
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : MPD (music)
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Theme Elements
|
||||
status="`mpc status`"
|
||||
if [[ -z "$status" ]]; then
|
||||
prompt='Offline'
|
||||
mesg="MPD is Offline"
|
||||
else
|
||||
prompt="`mpc -f "%artist%" current`"
|
||||
mesg="`mpc -f "%title%" current` :: `mpc status | grep "#" | awk '{print $3}'`"
|
||||
fi
|
||||
|
||||
if [[ ( "$theme" == *'type-1'* ) || ( "$theme" == *'type-3'* ) || ( "$theme" == *'type-5'* ) ]]; then
|
||||
list_col='1'
|
||||
list_row='6'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='6'
|
||||
list_row='1'
|
||||
fi
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
if [[ ${status} == *"[playing]"* ]]; then
|
||||
option_1=" Pause"
|
||||
else
|
||||
option_1=" Play"
|
||||
fi
|
||||
option_2=" Stop"
|
||||
option_3=" Previous"
|
||||
option_4=" Next"
|
||||
option_5=" Repeat"
|
||||
option_6=" Random"
|
||||
else
|
||||
if [[ ${status} == *"[playing]"* ]]; then
|
||||
option_1=""
|
||||
else
|
||||
option_1=""
|
||||
fi
|
||||
option_2=""
|
||||
option_3=""
|
||||
option_4=""
|
||||
option_5=""
|
||||
option_6=""
|
||||
fi
|
||||
|
||||
# Toggle Actions
|
||||
active=''
|
||||
urgent=''
|
||||
# Repeat
|
||||
if [[ ${status} == *"repeat: on"* ]]; then
|
||||
active="-a 4"
|
||||
elif [[ ${status} == *"repeat: off"* ]]; then
|
||||
urgent="-u 4"
|
||||
else
|
||||
option_5=" Parsing Error"
|
||||
fi
|
||||
# Random
|
||||
if [[ ${status} == *"random: on"* ]]; then
|
||||
[ -n "$active" ] && active+=",5" || active="-a 5"
|
||||
elif [[ ${status} == *"random: off"* ]]; then
|
||||
[ -n "$urgent" ] && urgent+=",5" || urgent="-u 5"
|
||||
else
|
||||
option_6=" Parsing Error"
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str 'textbox-prompt-colon {str: "";}' \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
${active} ${urgent} \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4\n$option_5\n$option_6" | rofi_cmd
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
mpc -q toggle && notify-send -u low -t 1000 " `mpc current`"
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
mpc -q stop
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
mpc -q prev && notify-send -u low -t 1000 " `mpc current`"
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
mpc -q next && notify-send -u low -t 1000 " `mpc current`"
|
||||
elif [[ "$1" == '--opt5' ]]; then
|
||||
mpc -q repeat
|
||||
elif [[ "$1" == '--opt6' ]]; then
|
||||
mpc -q random
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
$option_5)
|
||||
run_cmd --opt5
|
||||
;;
|
||||
$option_6)
|
||||
run_cmd --opt6
|
||||
;;
|
||||
esac
|
||||
129
dotfiles/.config/rofi/applets/bin/powermenu.sh
Executable file
129
dotfiles/.config/rofi/applets/bin/powermenu.sh
Executable file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : Power Menu
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Theme Elements
|
||||
prompt="`hostname`"
|
||||
mesg="Uptime : `uptime -p | sed -e 's/up //g'`"
|
||||
|
||||
if [[ ( "$theme" == *'type-1'* ) || ( "$theme" == *'type-3'* ) || ( "$theme" == *'type-5'* ) ]]; then
|
||||
list_col='1'
|
||||
list_row='6'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='6'
|
||||
list_row='1'
|
||||
fi
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
option_1=" Lock"
|
||||
option_2=" Logout"
|
||||
option_3=" Suspend"
|
||||
option_4=" Hibernate"
|
||||
option_5=" Reboot"
|
||||
option_6=" Shutdown"
|
||||
yes=' Yes'
|
||||
no=' No'
|
||||
else
|
||||
option_1=""
|
||||
option_2=""
|
||||
option_3=""
|
||||
option_4=""
|
||||
option_5=""
|
||||
option_6=""
|
||||
yes=''
|
||||
no=''
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str 'textbox-prompt-colon {str: "";}' \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4\n$option_5\n$option_6" | rofi_cmd
|
||||
}
|
||||
|
||||
# Confirmation CMD
|
||||
confirm_cmd() {
|
||||
rofi -theme-str 'window {location: center; anchor: center; fullscreen: false; width: 350px;}' \
|
||||
-theme-str 'mainbox {orientation: vertical; children: [ "message", "listview" ];}' \
|
||||
-theme-str 'listview {columns: 2; lines: 1;}' \
|
||||
-theme-str 'element-text {horizontal-align: 0.5;}' \
|
||||
-theme-str 'textbox {horizontal-align: 0.5;}' \
|
||||
-dmenu \
|
||||
-p 'Confirmation' \
|
||||
-mesg 'Are you Sure?' \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Ask for confirmation
|
||||
confirm_exit() {
|
||||
echo -e "$yes\n$no" | confirm_cmd
|
||||
}
|
||||
|
||||
# Confirm and execute
|
||||
confirm_run () {
|
||||
selected="$(confirm_exit)"
|
||||
if [[ "$selected" == "$yes" ]]; then
|
||||
${1} && ${2} && ${3}
|
||||
else
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
betterlockscreen -l
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
confirm_run 'kill -9 -1'
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
confirm_run 'mpc -q pause' 'amixer set Master mute' 'systemctl suspend'
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
confirm_run 'systemctl hibernate'
|
||||
elif [[ "$1" == '--opt5' ]]; then
|
||||
confirm_run 'systemctl reboot'
|
||||
elif [[ "$1" == '--opt6' ]]; then
|
||||
confirm_run 'systemctl poweroff'
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
$option_5)
|
||||
run_cmd --opt5
|
||||
;;
|
||||
$option_6)
|
||||
run_cmd --opt6
|
||||
;;
|
||||
esac
|
||||
|
||||
103
dotfiles/.config/rofi/applets/bin/quicklinks.sh
Executable file
103
dotfiles/.config/rofi/applets/bin/quicklinks.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : Quick Links
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Theme Elements
|
||||
prompt='Quick Links'
|
||||
mesg="Using '$BROWSER' as web browser"
|
||||
|
||||
if [[ ( "$theme" == *'type-1'* ) || ( "$theme" == *'type-3'* ) || ( "$theme" == *'type-5'* ) ]]; then
|
||||
list_col='1'
|
||||
list_row='6'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='6'
|
||||
list_row='1'
|
||||
fi
|
||||
|
||||
if [[ ( "$theme" == *'type-1'* ) || ( "$theme" == *'type-5'* ) ]]; then
|
||||
efonts="JetBrains Mono Nerd Font 10"
|
||||
else
|
||||
efonts="JetBrains Mono Nerd Font 28"
|
||||
fi
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
option_1=" Google"
|
||||
option_2=" Gmail"
|
||||
option_3=" Youtube"
|
||||
option_4=" Github"
|
||||
option_5=" Reddit"
|
||||
option_6=" Twitter"
|
||||
else
|
||||
option_1=""
|
||||
option_2=""
|
||||
option_3=""
|
||||
option_4=""
|
||||
option_5=""
|
||||
option_6=""
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str 'textbox-prompt-colon {str: "";}' \
|
||||
-theme-str "element-text {font: \"$efonts\";}" \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4\n$option_5\n$option_6" | rofi_cmd
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
xdg-open 'https://www.google.com/'
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
xdg-open 'https://mail.google.com/'
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
xdg-open 'https://www.youtube.com/'
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
xdg-open 'https://www.github.com/'
|
||||
elif [[ "$1" == '--opt5' ]]; then
|
||||
xdg-open 'https://www.reddit.com/'
|
||||
elif [[ "$1" == '--opt6' ]]; then
|
||||
xdg-open 'https://www.twitter.com/'
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
$option_5)
|
||||
run_cmd --opt5
|
||||
;;
|
||||
$option_6)
|
||||
run_cmd --opt6
|
||||
;;
|
||||
esac
|
||||
165
dotfiles/.config/rofi/applets/bin/screenshot.sh
Executable file
165
dotfiles/.config/rofi/applets/bin/screenshot.sh
Executable file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : Screenshot
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Theme Elements
|
||||
prompt='Screenshot'
|
||||
mesg="DIR: `xdg-user-dir PICTURES`/Screenshots"
|
||||
|
||||
if [[ "$theme" == *'type-1'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='400px'
|
||||
elif [[ "$theme" == *'type-3'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='120px'
|
||||
elif [[ "$theme" == *'type-5'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='520px'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='5'
|
||||
list_row='1'
|
||||
win_width='670px'
|
||||
fi
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
option_1=" Capture Desktop"
|
||||
option_2=" Capture Area"
|
||||
option_3=" Capture Window"
|
||||
option_4=" Capture in 5s"
|
||||
option_5=" Capture in 10s"
|
||||
else
|
||||
option_1=""
|
||||
option_2=""
|
||||
option_3=""
|
||||
option_4=""
|
||||
option_5=""
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "window {width: $win_width;}" \
|
||||
-theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str 'textbox-prompt-colon {str: "";}' \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4\n$option_5" | rofi_cmd
|
||||
}
|
||||
|
||||
# Screenshot
|
||||
time=`date +%Y-%m-%d-%H-%M-%S`
|
||||
geometry=`xrandr | grep 'current' | head -n1 | cut -d',' -f2 | tr -d '[:blank:],current'`
|
||||
dir="`xdg-user-dir PICTURES`/Screenshots"
|
||||
file="Screenshot_${time}_${geometry}.png"
|
||||
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
mkdir -p "$dir"
|
||||
fi
|
||||
|
||||
# notify and view screenshot
|
||||
notify_view() {
|
||||
notify_cmd_shot='dunstify -u low --replace=699'
|
||||
${notify_cmd_shot} "Copied to clipboard."
|
||||
viewnior ${dir}/"$file"
|
||||
if [[ -e "$dir/$file" ]]; then
|
||||
${notify_cmd_shot} "Screenshot Saved."
|
||||
else
|
||||
${notify_cmd_shot} "Screenshot Deleted."
|
||||
fi
|
||||
}
|
||||
|
||||
# Copy screenshot to clipboard
|
||||
copy_shot () {
|
||||
tee "$file" | xclip -selection clipboard -t image/png
|
||||
}
|
||||
|
||||
# countdown
|
||||
countdown () {
|
||||
for sec in `seq $1 -1 1`; do
|
||||
dunstify -t 1000 --replace=699 "Taking shot in : $sec"
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# take shots
|
||||
shotnow () {
|
||||
cd ${dir} && sleep 0.5 && maim -u -f png | copy_shot
|
||||
notify_view
|
||||
}
|
||||
|
||||
shot5 () {
|
||||
countdown '5'
|
||||
sleep 1 && cd ${dir} && maim -u -f png | copy_shot
|
||||
notify_view
|
||||
}
|
||||
|
||||
shot10 () {
|
||||
countdown '10'
|
||||
sleep 1 && cd ${dir} && maim -u -f png | copy_shot
|
||||
notify_view
|
||||
}
|
||||
|
||||
shotwin () {
|
||||
cd ${dir} && maim -u -f png -i `xdotool getactivewindow` | copy_shot
|
||||
notify_view
|
||||
}
|
||||
|
||||
shotarea () {
|
||||
cd ${dir} && maim -u -f png -s -b 2 -c 0.35,0.55,0.85,0.25 -l | copy_shot
|
||||
notify_view
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
shotnow
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
shotarea
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
shotwin
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
shot5
|
||||
elif [[ "$1" == '--opt5' ]]; then
|
||||
shot10
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
$option_5)
|
||||
run_cmd --opt5
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
134
dotfiles/.config/rofi/applets/bin/volume.sh
Executable file
134
dotfiles/.config/rofi/applets/bin/volume.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Author : Aditya Shakya (adi1090x)
|
||||
## Github : @adi1090x
|
||||
#
|
||||
## Applets : Volume
|
||||
|
||||
# Import Current Theme
|
||||
source "$HOME"/.config/rofi/applets/shared/theme.bash
|
||||
theme="$type/$style"
|
||||
|
||||
# Volume Info
|
||||
mixer="`amixer info Master | grep 'Mixer name' | cut -d':' -f2 | tr -d \',' '`"
|
||||
speaker="`amixer get Master | tail -n1 | awk -F ' ' '{print $5}' | tr -d '[]'`"
|
||||
mic="`amixer get Capture | tail -n1 | awk -F ' ' '{print $5}' | tr -d '[]'`"
|
||||
|
||||
active=""
|
||||
urgent=""
|
||||
|
||||
# Speaker Info
|
||||
amixer get Master | grep '\[on\]' &>/dev/null
|
||||
if [[ "$?" == 0 ]]; then
|
||||
active="-a 1"
|
||||
stext='Unmute'
|
||||
sicon=''
|
||||
else
|
||||
urgent="-u 1"
|
||||
stext='Mute'
|
||||
sicon=''
|
||||
fi
|
||||
|
||||
# Microphone Info
|
||||
amixer get Capture | grep '\[on\]' &>/dev/null
|
||||
if [[ "$?" == 0 ]]; then
|
||||
[ -n "$active" ] && active+=",3" || active="-a 3"
|
||||
mtext='Unmute'
|
||||
micon=''
|
||||
else
|
||||
[ -n "$urgent" ] && urgent+=",3" || urgent="-u 3"
|
||||
mtext='Mute'
|
||||
micon=''
|
||||
fi
|
||||
|
||||
# Theme Elements
|
||||
prompt="S:$stext, M:$mtext"
|
||||
mesg="$mixer - Speaker: $speaker, Mic: $mic"
|
||||
|
||||
if [[ "$theme" == *'type-1'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='400px'
|
||||
elif [[ "$theme" == *'type-3'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='120px'
|
||||
elif [[ "$theme" == *'type-5'* ]]; then
|
||||
list_col='1'
|
||||
list_row='5'
|
||||
win_width='520px'
|
||||
elif [[ ( "$theme" == *'type-2'* ) || ( "$theme" == *'type-4'* ) ]]; then
|
||||
list_col='5'
|
||||
list_row='1'
|
||||
win_width='670px'
|
||||
fi
|
||||
|
||||
# Options
|
||||
layout=`cat ${theme} | grep 'USE_ICON' | cut -d'=' -f2`
|
||||
if [[ "$layout" == 'NO' ]]; then
|
||||
option_1=" Increase"
|
||||
option_2="$sicon $stext"
|
||||
option_3=" Decrese"
|
||||
option_4="$micon $mtext"
|
||||
option_5=" Settings"
|
||||
else
|
||||
option_1=""
|
||||
option_2="$sicon"
|
||||
option_3=""
|
||||
option_4="$micon"
|
||||
option_5=""
|
||||
fi
|
||||
|
||||
# Rofi CMD
|
||||
rofi_cmd() {
|
||||
rofi -theme-str "window {width: $win_width;}" \
|
||||
-theme-str "listview {columns: $list_col; lines: $list_row;}" \
|
||||
-theme-str 'textbox-prompt-colon {str: "";}' \
|
||||
-dmenu \
|
||||
-p "$prompt" \
|
||||
-mesg "$mesg" \
|
||||
${active} ${urgent} \
|
||||
-markup-rows \
|
||||
-theme ${theme}
|
||||
}
|
||||
|
||||
# Pass variables to rofi dmenu
|
||||
run_rofi() {
|
||||
echo -e "$option_1\n$option_2\n$option_3\n$option_4\n$option_5" | rofi_cmd
|
||||
}
|
||||
|
||||
# Execute Command
|
||||
run_cmd() {
|
||||
if [[ "$1" == '--opt1' ]]; then
|
||||
amixer -Mq set Master,0 5%+ unmute
|
||||
elif [[ "$1" == '--opt2' ]]; then
|
||||
amixer set Master toggle
|
||||
elif [[ "$1" == '--opt3' ]]; then
|
||||
amixer -Mq set Master,0 5%- unmute
|
||||
elif [[ "$1" == '--opt4' ]]; then
|
||||
amixer set Capture toggle
|
||||
elif [[ "$1" == '--opt5' ]]; then
|
||||
pavucontrol
|
||||
fi
|
||||
}
|
||||
|
||||
# Actions
|
||||
chosen="$(run_rofi)"
|
||||
case ${chosen} in
|
||||
$option_1)
|
||||
run_cmd --opt1
|
||||
;;
|
||||
$option_2)
|
||||
run_cmd --opt2
|
||||
;;
|
||||
$option_3)
|
||||
run_cmd --opt3
|
||||
;;
|
||||
$option_4)
|
||||
run_cmd --opt4
|
||||
;;
|
||||
$option_5)
|
||||
run_cmd --opt5
|
||||
;;
|
||||
esac
|
||||
|
||||
18
dotfiles/.config/rofi/applets/shared/colors.rasi
Normal file
18
dotfiles/.config/rofi/applets/shared/colors.rasi
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
*
|
||||
* Author : Aditya Shakya (adi1090x)
|
||||
* Github : @adi1090x
|
||||
*
|
||||
* Colors
|
||||
*
|
||||
* Available Colors Schemes
|
||||
*
|
||||
* adapta catppuccin everforest navy paper
|
||||
* arc cyberpunk gruvbox nord solarized
|
||||
* black dracula lovelace onedark yousai
|
||||
*
|
||||
**/
|
||||
|
||||
/* Import color-scheme from `colors` directory */
|
||||
|
||||
@import "~/.config/rofi/colors/onedark.rasi"
|
||||
12
dotfiles/.config/rofi/applets/shared/fonts.rasi
Normal file
12
dotfiles/.config/rofi/applets/shared/fonts.rasi
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
*
|
||||
* Author : Aditya Shakya (adi1090x)
|
||||
* Github : @adi1090x
|
||||
*
|
||||
* Fonts
|
||||
*
|
||||
**/
|
||||
|
||||
* {
|
||||
font: "JetBrains Mono Nerd Font 10";
|
||||
}
|
||||
4
dotfiles/.config/rofi/applets/shared/theme.bash
Executable file
4
dotfiles/.config/rofi/applets/shared/theme.bash
Executable file
@@ -0,0 +1,4 @@
|
||||
## Current Theme
|
||||
|
||||
type="$HOME/.config/rofi/applets/type-1"
|
||||
style='style-1.rasi'
|
||||
152
dotfiles/.config/rofi/applets/type-1/style-1.rasi
Normal file
152
dotfiles/.config/rofi/applets/type-1/style-1.rasi
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
*
|
||||
* Author : Aditya Shakya (adi1090x)
|
||||
* Github : @adi1090x
|
||||
*
|
||||
* Rofi Theme File
|
||||
* Rofi Version: 1.7.3
|
||||
**/
|
||||
|
||||
/*****----- Configuration -----*****/
|
||||
configuration {
|
||||
show-icons: false;
|
||||
}
|
||||
|
||||
/*****----- Global Properties -----*****/
|
||||
@import "../shared/colors.rasi"
|
||||
@import "../shared/fonts.rasi"
|
||||
|
||||
/*
|
||||
USE_ICON=NO
|
||||
*/
|
||||
|
||||
/*****----- Main Window -----*****/
|
||||
window {
|
||||
transparency: "real";
|
||||
location: center;
|
||||
anchor: center;
|
||||
fullscreen: false;
|
||||
width: 400px;
|
||||
x-offset: 0px;
|
||||
y-offset: 0px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border: 1px solid;
|
||||
border-radius: 0px;
|
||||
border-color: @selected;
|
||||
cursor: "default";
|
||||
background-color: @background;
|
||||
}
|
||||
|
||||
/*****----- Main Box -----*****/
|
||||
mainbox {
|
||||
enabled: true;
|
||||
spacing: 10px;
|
||||
margin: 0px;
|
||||
padding: 20px;
|
||||
background-color: transparent;
|
||||
children: [ "inputbar", "message", "listview" ];
|
||||
}
|
||||
|
||||
/*****----- Inputbar -----*****/
|
||||
inputbar {
|
||||
enabled: true;
|
||||
spacing: 10px;
|
||||
padding: 0px;
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
border-color: @selected;
|
||||
background-color: transparent;
|
||||
text-color: @foreground;
|
||||
children: [ "textbox-prompt-colon", "prompt"];
|
||||
}
|
||||
|
||||
textbox-prompt-colon {
|
||||
enabled: true;
|
||||
expand: false;
|
||||
str: "";
|
||||
padding: 10px 13px;
|
||||
border-radius: 0px;
|
||||
background-color: @urgent;
|
||||
text-color: @background;
|
||||
}
|
||||
prompt {
|
||||
enabled: true;
|
||||
padding: 10px;
|
||||
border-radius: 0px;
|
||||
background-color: @active;
|
||||
text-color: @background;
|
||||
}
|
||||
|
||||
/*****----- Message -----*****/
|
||||
message {
|
||||
enabled: true;
|
||||
margin: 0px;
|
||||
padding: 10px;
|
||||
border: 0px solid;
|
||||
border-radius: 0px;
|
||||
border-color: @selected;
|
||||
background-color: @background-alt;
|
||||
text-color: @foreground;
|
||||
}
|
||||
textbox {
|
||||
background-color: inherit;
|
||||
text-color: inherit;
|
||||
vertical-align: 0.5;
|
||||
horizontal-align: 0.0;
|
||||
}
|
||||
|
||||
/*****----- Listview -----*****/
|
||||
listview {
|
||||
enabled: true;
|
||||
columns: 1;
|
||||
lines: 6;
|
||||
cycle: true;
|
||||
scrollbar: false;
|
||||
layout: vertical;
|
||||
|
||||
spacing: 5px;
|
||||
background-color: transparent;
|
||||
cursor: "default";
|
||||
}
|
||||
|
||||
/*****----- Elements -----*****/
|
||||
element {
|
||||
enabled: true;
|
||||
padding: 10px;
|
||||
border: 0px solid;
|
||||
border-radius: 0px;
|
||||
border-color: @selected;
|
||||
background-color: transparent;
|
||||
text-color: @foreground;
|
||||
cursor: pointer;
|
||||
}
|
||||
element-text {
|
||||
background-color: transparent;
|
||||
text-color: inherit;
|
||||
cursor: inherit;
|
||||
vertical-align: 0.5;
|
||||
horizontal-align: 0.0;
|
||||
}
|
||||
|
||||
element normal.normal,
|
||||
element alternate.normal {
|
||||
background-color: var(background);
|
||||
text-color: var(foreground);
|
||||
}
|
||||
element normal.urgent,
|
||||
element alternate.urgent,
|
||||
element selected.active {
|
||||
background-color: var(urgent);
|
||||
text-color: var(background);
|
||||
}
|
||||
element normal.active,
|
||||
element alternate.active,
|
||||
element selected.urgent {
|
||||
background-color: var(active);
|
||||
text-color: var(background);
|
||||
}
|
||||
element selected.normal {
|
||||
background-color: var(selected);
|
||||
text-color: var(background);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user