70 Commits

Author SHA1 Message Date
03b043d9d3 Merge branch 'features/adding_quickshell' of ssh://gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles into features/adding_quickshell 2026-08-27 14:51:01 +01:00
1ff92f3b79 Merge pull request 'features/monitor_options' (#5) from features/monitor_options into features/adding_quickshell
Reviewed-on: #5
2026-08-27 14:46:07 +01:00
e5809efc6a UI: Moved Updates to bluetooth devices inside bluetooth box 2026-08-27 14:17:03 +01:00
c64cf5ff12 Fix: Restored "time to charged" 2026-08-27 14:11:27 +01:00
d6f79685f0 Feature: Extra functionablity to bluetooth actions 2026-08-27 14:07:52 +01:00
9e532023bd Fix: Corrected battery behavior 2026-08-27 14:06:21 +01:00
c2c6675f4b Feature: Added battery percentage/view 2026-08-27 13:18:46 +01:00
fcce5f304b UI: Fixed hover colours 2026-08-27 13:02:34 +01:00
4b0b5c72ba UI: Fixed padding error 2026-08-27 13:02:02 +01:00
3f4e3514a2 Fix: Icons now map correctly to device 2026-08-27 11:45:14 +01:00
622afb2100 UI: fixed selectors & added spacing 2026-08-27 11:28:57 +01:00
3ae5c57303 Feature: Added Volume Sink 2026-08-27 11:21:41 +01:00
ef06a96757 UI: new header & current connected interface 2026-08-27 11:13:09 +01:00
03ea182c8c Feature: Added wifi scanning indicator 2026-08-27 10:53:56 +01:00
ba8a565cda Feature: added scanner indicator 2026-08-27 10:45:50 +01:00
c8a9c30fc5 Fix: hover button fix 2026-08-27 10:45:36 +01:00
185b752393 Fix: removed previous border 2026-08-27 10:23:13 +01:00
8515f3f5ce Fix: Added borders to selectors 2026-08-27 10:22:51 +01:00
86921f7aab Feature: new selectors 2026-08-27 09:39:32 +01:00
cede2a1690 Feature: Password input 2026-08-27 09:18:57 +01:00
00e4368abe Fix: corrected icons 2026-08-27 09:18:42 +01:00
00d7dcb411 Fix: changed icon for power 2026-08-27 08:50:29 +01:00
4bdb590899 Feature: added connected indicator to VPN 2026-08-27 08:48:08 +01:00
44e73f5a9d Fix: corrected padding for bar icon 2026-08-27 08:17:37 +01:00
3184c61267 Fix: increased icon size 2026-08-27 08:16:51 +01:00
fb79f0a37e Fix: corrected wifi sizing 2026-08-27 08:12:00 +01:00
7a1073646a Fix: changed sizing of logo to be consistant 2026-08-27 07:49:36 +01:00
90d068a878 Fix: fixed tailscale logo sizing to match others 2026-08-27 07:48:27 +01:00
57e5c0c7b3 Fixes: removed extra padding on icon 2026-08-27 07:42:37 +01:00
b7a92c9a59 Changes: replaced icons with material icons 2026-08-27 07:40:53 +01:00
d3856ada0d utility: checking all dependencies 2026-08-27 00:38:50 +01:00
3601f52600 Merge pull request 'feature: float bitwarden extension window' (#4) from features/floating_bitwarden_extension into features/adding_quickshell
fine by me
2026-08-26 21:09:06 +01:00
98f9a77b23 feature: float bitwarden extension window 2026-08-26 21:05:03 +01:00
bc242ae956 updated to add quickshell 2026-08-26 20:21:08 +01:00
4237b2ed9e Merge pull request 'migration: lua' (#2) from features/hypr_lua_config into features/adding_quickshell
Reviewed-on: #2

pulling into changes
2026-08-26 20:17:28 +01:00
7b0cf9934b migration: lua 2026-08-26 20:14:43 +01:00
9df5731ee1 changed volume location and widget colours 2026-08-25 09:35:22 +01:00
3806935316 implimented multiple VPN connectors 2026-08-25 09:34:55 +01:00
516f8eed82 removed tray 2026-08-25 09:34:19 +01:00
369179c057 added vpn exclusitivity to allow functions to coexist 2026-08-25 09:34:04 +01:00
322804577f added vpn exclusitivity to allow functions to coexist 2026-08-25 09:33:45 +01:00
0eee14858b basic tailscale widget 2026-08-25 07:25:38 +01:00
461f4c3fff added tailscale widget 2026-08-25 07:25:27 +01:00
2f9dfda410 added initial bluetooth widget 2026-08-24 15:12:27 +01:00
e35ceb1450 added bluetooth widget 2026-08-24 15:10:55 +01:00
3d0a18866f implimented network widget 2026-08-24 15:07:18 +01:00
6c415d6fbb added starting funtionality 2026-08-24 15:06:58 +01:00
c4b4a3bd26 added quickshell config 2026-08-24 10:03:59 +01:00
866ee0bb9b Merge pull request 'features/updating_fonts' (#1) from features/updating_fonts into main
Reviewed-on: #1
2026-08-21 09:22:44 +01:00
e77c08a0a2 changed spacings for battery 2026-08-21 09:20:17 +01:00
491509e864 corrected padding so all items are verticallt alligned 2026-08-21 08:38:49 +01:00
c4604f8e0f changed spacing for date 2026-08-21 08:34:48 +01:00
30d5f4c926 adjusted spacing 2026-08-21 08:32:42 +01:00
90d7042995 changed text for alt & updated spacing 2026-08-21 08:28:16 +01:00
b6714cf20b changed spacing for bitwarden launcher 2026-08-21 08:22:28 +01:00
8877835ae0 fixed spacing for bluetooth and wireplumber 2026-08-21 08:20:40 +01:00
32dd10c83d returned wifi icons 2026-08-21 07:46:02 +01:00
f3df5e4f3d changed ethernet and wifi icons 2026-08-21 07:43:56 +01:00
74953850fb updating fonts 2026-08-21 07:21:51 +01:00
04e50bad3f reverted back to config working on laptop 2026-08-21 06:44:17 +01:00
a218026568 updated config files to lua 2026-08-21 05:48:50 +01:00
7c557bffc8 changed domains to ip addresses 2026-08-21 05:48:29 +01:00
2eb122960d docker & pnpm context added
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-08-21 04:31:20 +01:00
8c0347ee35 revert c4f220d95a
revert docker & pnpm context added

Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-08-21 04:30:06 +01:00
2935fc7cfe Update README.md 2026-08-20 07:37:58 +01:00
6f9651ccb3 added new linux screenshot location 2026-08-19 09:45:59 +01:00
c88a96406a added alias for ssh-ing 2026-08-19 09:45:26 +01:00
13c4f95e08 interval poll for updates and fixed css 2026-08-19 09:44:59 +01:00
c4f220d95a docker & pnpm context added
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-16 04:34:50 +01:00
f6d69332db Update bookmarks
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-16 04:34:28 +01:00
56 changed files with 7335 additions and 39 deletions

View File

@@ -26,6 +26,26 @@ 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
```
Once that is complete, run this command:
```console
stow -d ~/Projects/dotfiles/ -t ~/ dotfiles
```
This will sync wallpapers/config files to where they are usually located
## 📦 Programs
List of programs and tools I use.
@@ -39,7 +59,7 @@ List of programs and tools I use.
|📁 File Manager | [thunar](https://wiki.archlinux.org/title/Thunar) |
|📝 Editor | [vs code](https://github.com/microsoft/vscode) / [neovim (changing to soon)](https://github.com/neovim/neovim) |
|🌐 Browser | [zen](https://github.com/zen-browser/desktop) / [firefox](https://github.com/topics/firefox-browser) / [startpage](https://blog-bucket-personal.s3.eu-west-2.amazonaws.com/Screenshot_02-May_08-44-23_6614.png) |
|📊 Bar | [waybar](https://github.com/Alexays/Waybar) |
|📊 Bar | [waybar](https://github.com/Alexays/Waybar) / [quickshell](https://quickshell.org/) |
|🚀 Launcher | [rofi](https://github.com/davatorium/rofi) |
|🔒 Lockscreen | [hyprlock](https://github.com/hyprwm/hyprlock) |
|🚪 Login Menu | [sddm](https://github.com/sddm/sddm) |

View File

@@ -1,3 +1,4 @@
file:///home/thomas/Projects
file:///home/thomas/Documents
file:///home/thomas/Downloads
file:///home/thomas/Pictures

View 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" })

View 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

View 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 = 0.9,
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,
},
},
},
})

View File

@@ -0,0 +1,87 @@
-- 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.

View File

@@ -0,0 +1,29 @@
-- 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.
hl.config({
general = {
allow_tearing = true,
snap = {
enabled = false,
},
},
misc = {
vrr = 1,
},
})
-- Monitors
hl.monitor({ output = "DP-2", mode = "1920x1080@165.00Hz", position = "0x0", scale = 1, bitdepth = 10, cm = "wide" })
-- 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("hyprshot -o " .. os.getenv("HOME") .. "/Pictures/Screenshots-Linux -m region"))
hl.bind("SUPER + up", hl.dsp.focus({ direction = "u" }))
hl.bind("SUPER + SHIFT + Print", hl.dsp.exec_cmd("hyprshot -o " .. os.getenv("HOME") .. "/Pictures/Screenshots-Linux -m window"))

View 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" })

View File

@@ -0,0 +1,52 @@
-- 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")
-- 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
hl.exec_cmd("hypridle")
-- Float + center the Bitwarden extension pop-out (window rules can't catch it — see the script)
hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh")
hl.dispatch(hl.dsp.focus({ workspace = 3 }))
-- NOTE: the trailing "&&" here is a stray leftover from the original .conf (dangling, does nothing) — ported as-is
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")
-- 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)

View File

@@ -10,6 +10,6 @@ monitor = DP-2, 1920x1080@165.00Hz, 0x0, 1, bitdepth, 10, cm, wide
# 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

View 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")

View File

@@ -0,0 +1,176 @@
-- 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" }))
-- The following mappings use key codes to better support various keyboard layouts
-- 1 is code:10, 2 is code:11, etc
-- Switch workspaces with mainMod + [0-9]
for i, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
hl.bind(mainMod .. " + code:" .. code, hl.dsp.focus({ workspace = i })) -- code:10 = key 1 ... code:19 = key 0
end
-- Move active window and follow to workspace mainMod + SHIFT + [0-9]
for i, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
hl.bind(mainMod .. " + SHIFT + code:" .. code, 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, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
hl.bind(mainMod .. " + CTRL + code:" .. code, 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,
})

View File

@@ -0,0 +1,14 @@
-- 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 = 1, bitdepth = 10, cm = "wide" })
-- <<<<< TUI AUTO END

View 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.7", size = "monitor_w*0.7 monitor_h*0.7", match = { tag = "wallpaper*" } })
hl.window_rule({ name = "windowrule-76", float = true, opacity = "0.8 0.7", size = "monitor_w*0.7 monitor_h*0.7", match = { tag = "settings*" } })
hl.window_rule({ name = "windowrule-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.7", match = { tag = "browser*" } })
-- hl.window_rule({ opacity = "0.9 0.8", match = { tag = "projects*" } })
-- hl.window_rule({ opacity = "0.94 0.86", match = { tag = "im*" } })
-- hl.window_rule({ opacity = "0.94 0.86", match = { tag = "multimedia*" } })
-- hl.window_rule({ opacity = "0.9 0.8", match = { tag = "file-manager*" } })
hl.window_rule({ name = "windowrule-93", opacity = "0.8 0.7", match = { tag = "terminal*" } })
-- hl.window_rule({ opacity = "0.82 0.75", match = { tag = "viewer*" } })
hl.window_rule({ name = "windowrule-94", opacity = "0.8 0.7", match = { class = "^(gedit|org.gnome.TextEditor|mousepad)$" } })
hl.window_rule({ name = "windowrule-95", opacity = "0.9 0.8", match = { class = "^(deluge)$" } })
hl.window_rule({ name = "windowrule-96", opacity = "0.9 0.8", match = { class = "^(seahorse)$" } }) -- gnome-keyring gui
-- hl.window_rule({ opacity = "0.95 0.75", 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" } })

View File

@@ -0,0 +1,29 @@
-- 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 = 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" })

View 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" })

View 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" })

View 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 } })

View 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" })

View 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" })

View 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" })

View 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" })

View 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%" })

View 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%" })

View 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" })

View 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" })

View 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" })

View 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" })

View 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" })

View 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" })

View 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" })

View 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" })

View 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

View File

@@ -31,6 +31,14 @@ console_title_template = '{{ .Shell }} in {{ .Folder }}'
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'

View 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
}

View 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
}
}
}

View File

@@ -0,0 +1,16 @@
pragma Singleton
import QtQuick
// Tailscale, Mullvad, and ProtonVPN all fight over the default route/DNS if
// more than one is active at once (see VpnIndicator.qml's header comment) —
// only one of the three should ever actually be connected. This singleton
// is the shared signal between the two widget files: VpnIndicator.qml
// writes Mullvad/ProtonVPN's live connection state here as it polls them;
// TailscaleIndicator.qml reads it to back off (`tailscale down`) rather
// than stay up alongside either one, and to refuse turning Tailscale back
// on while one is still connected.
QtObject {
property bool mullvadConnected: false
property bool protonConnected: false
readonly property bool commercialVpnActive: mullvadConnected || protonConnected
}

View File

@@ -0,0 +1,4 @@
module config
singleton Colors 1.0 Colors.qml
singleton Appearance 1.0 Appearance.qml
singleton VpnExclusivity 1.0 VpnExclusivity.qml

View 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 : ""
}

View File

@@ -0,0 +1,58 @@
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 {}
}
Clock {
anchors.centerIn: parent
}
RowLayout {
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: Appearance.spacing
}
spacing: Appearance.spacing
//Tray {}
TailscaleIndicator { barWindow: bar }
VpnIndicator { barWindow: bar }
NetworkIndicator { barWindow: bar }
BluetoothIndicator { barWindow: bar }
Volume { barWindow: bar }
BatteryIndicator { barWindow: bar }
PowerButton { barWindow: bar }
}
}

View 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
}
}
}
}
}
}
}
}

View 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)
}
}
}
}
}
}
}
}
}
}

View 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")
}
}

File diff suppressed because it is too large Load Diff

View 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
}
}
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config"
RowLayout {
spacing: Appearance.spacing * 0.6
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()
}
}
}
}
}

View File

@@ -0,0 +1,496 @@
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
onClicked: {
panel.visible = !panel.visible
grab.active = panel.visible
}
onWheel: (wheel) => {
if (!root.sink || !root.sink.audio) return
const step = 0.05
const delta = wheel.angleDelta.y > 0 ? step : -step
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + delta))
}
}
// --- 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
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,594 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import "../../config"
// A switcher between commercial VPN providers, distinct from
// TailscaleIndicator.qml — Tailscale is a mesh overlay network you generally
// leave running; these are traditional single-tunnel VPN clients where only
// one makes sense to have active at a time. Connecting one here disconnects
// the other first.
//
// V1 scope: connect (provider's own "fastest"/default) and disconnect per
// provider, live status, no region/server picker yet. Provider CLIs:
// - Mullvad: official `mullvad` CLI (mullvad-vpn-daemon package), has
// real JSON status output.
// - ProtonVPN: official `protonvpn` CLI (proton-vpn-cli package), plain-
// text status only — parsed defensively since its exact wording isn't
// guaranteed across versions.
Item {
id: root
implicitWidth: label.implicitWidth + 4
implicitHeight: Appearance.barHeight
required property var barWindow
property int refreshIntervalSec: 20
readonly property int _refreshIntervalMs: Math.max(5, Math.min(3600, refreshIntervalSec)) * 1000
// Whether each CLI is on PATH at all. Starts optimistic so the panel
// doesn't flash "not installed" before the first `which` check lands.
property bool mullvadInstalled: true
property bool protonInstalled: true
// "disconnected" | "connecting" | "connected" | "disconnecting"
property string mullvadState: "disconnected"
property string mullvadDetail: ""
property string protonState: "disconnected"
property string protonDetail: ""
readonly property bool anyConnected: mullvadState === "connected" || protonState === "connected"
// Set while switching from one provider to the other: holds which one
// to connect once the current one has finished disconnecting.
property string pendingConnect: ""
// Surfaced under the header for a failed connect/disconnect; cleared
// automatically after a few seconds.
property string actionStatus: ""
function shortLine(text) {
const line = String(text || "").trim().split("\n")[0] || ""
return line.length > 80 ? line.substring(0, 77) + "…" : line
}
function applyMullvadStatus(text) {
try {
const data = JSON.parse(text)
const state = String(data.state || "disconnected")
const loc = data.details && data.details.location
root.mullvadState = state
root.mullvadDetail = loc ? [loc.city, loc.country].filter(Boolean).join(", ") : ""
} catch (e) {
root.mullvadState = "disconnected"
root.mullvadDetail = ""
}
VpnExclusivity.mullvadConnected = root.mullvadState === "connected"
}
// No --json here — ProtonVPN's CLI only prints plain text, so this
// just looks for a "Status: …" line and, best-effort, a following
// "Server:"/"Country:" line if the CLI happens to print one.
function applyProtonStatus(text) {
const body = String(text || "")
const statusMatch = body.match(/^Status:\s*(.+)$/mi)
const raw = statusMatch ? statusMatch[1].trim().toLowerCase() : ""
root.protonState = raw.indexOf("connected") === 0 ? "connected" : "disconnected"
const serverMatch = body.match(/^Server:\s*(.+)$/mi)
const countryMatch = body.match(/^Country:\s*(.+)$/mi)
root.protonDetail = serverMatch ? serverMatch[1].trim() : (countryMatch ? countryMatch[1].trim() : "")
VpnExclusivity.protonConnected = root.protonState === "connected"
}
function refreshMullvad() {
if (!root.mullvadInstalled) {
if (!mullvadWhichProc.running) mullvadWhichProc.running = true
return
}
if (!mullvadStatusProc.running) {
mullvadStatusProc.running = true
mullvadPollWatchdog.restart()
}
}
function refreshProton() {
if (!root.protonInstalled) {
if (!protonWhichProc.running) protonWhichProc.running = true
return
}
if (!protonStatusProc.running) {
protonStatusProc.running = true
protonPollWatchdog.restart()
}
}
function refresh() {
root.refreshMullvad()
root.refreshProton()
}
function connectMullvad() {
if (mullvadConnectProc.running || mullvadDisconnectProc.running) return
if (root.protonState !== "disconnected") {
root.pendingConnect = "mullvad"
root.disconnectProton()
return
}
root.pendingConnect = ""
mullvadConnectProc.running = true
}
function connectProton() {
if (protonConnectProc.running || protonDisconnectProc.running) return
if (root.mullvadState !== "disconnected") {
root.pendingConnect = "protonvpn"
root.disconnectMullvad()
return
}
root.pendingConnect = ""
protonConnectProc.running = true
}
function disconnectMullvad() {
if (mullvadDisconnectProc.running) return
mullvadDisconnectProc.running = true
}
function disconnectProton() {
if (protonDisconnectProc.running) return
protonDisconnectProc.running = true
}
function toggleMullvad() {
if (mullvadConnectProc.running || mullvadDisconnectProc.running) return
if (root.mullvadState === "connected") root.disconnectMullvad()
else root.connectMullvad()
}
function toggleProton() {
if (protonConnectProc.running || protonDisconnectProc.running) return
if (root.protonState === "connected") root.disconnectProton()
else root.connectProton()
}
// Tailscale fights Mullvad/ProtonVPN for the default route/DNS, so fire
// this right after either one finishes connecting — a harmless no-op
// if Tailscale was already down. TailscaleIndicator.qml also backs
// itself off reactively (via VpnExclusivity) as a backstop, but doing
// it here too avoids waiting out its poll interval.
Process {
id: tailscaleDownProc
command: ["tailscale", "down"]
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// --- Mullvad ---------------------------------------------------------
Process {
id: mullvadWhichProc
command: ["which", "mullvad"]
running: true
stdout: StdioCollector {}
onExited: (exitCode, exitStatus) => {
root.mullvadInstalled = exitCode === 0
if (root.mullvadInstalled) root.refreshMullvad()
}
}
Process {
id: mullvadStatusProc
command: ["mullvad", "status", "--json"]
stdout: StdioCollector {
id: mullvadStatusCollector
onStreamFinished: root.applyMullvadStatus(mullvadStatusCollector.text)
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0 && !mullvadWhichProc.running) mullvadWhichProc.running = true
}
}
Process {
id: mullvadConnectProc
command: ["mullvad", "connect", "--wait"]
stdout: StdioCollector {}
stderr: StdioCollector { id: mullvadConnectErr }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.actionStatus = "Mullvad: " + root.shortLine(mullvadConnectErr.text || "connect failed")
actionStatusTimer.restart()
} else if (!tailscaleDownProc.running) {
tailscaleDownProc.running = true
}
root.refreshMullvad()
}
}
Process {
id: mullvadDisconnectProc
command: ["mullvad", "disconnect", "--wait"]
stdout: StdioCollector {}
stderr: StdioCollector { id: mullvadDisconnectErr }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.actionStatus = "Mullvad: " + root.shortLine(mullvadDisconnectErr.text || "disconnect failed")
actionStatusTimer.restart()
}
root.refreshMullvad()
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
else if (root.pendingConnect === "protonvpn") { root.pendingConnect = ""; root.connectProton() }
}
}
Timer {
id: mullvadPollWatchdog
interval: 15000
repeat: false
onTriggered: if (mullvadStatusProc.running) mullvadStatusProc.running = false
}
// --- ProtonVPN ---------------------------------------------------------
Process {
id: protonWhichProc
command: ["which", "protonvpn"]
running: true
stdout: StdioCollector {}
onExited: (exitCode, exitStatus) => {
root.protonInstalled = exitCode === 0
if (root.protonInstalled) root.refreshProton()
}
}
Process {
id: protonStatusProc
command: ["protonvpn", "status"]
stdout: StdioCollector {
id: protonStatusCollector
onStreamFinished: root.applyProtonStatus(protonStatusCollector.text)
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0 && !protonWhichProc.running) protonWhichProc.running = true
}
}
Process {
id: protonConnectProc
command: ["protonvpn", "connect"]
stdout: StdioCollector {}
stderr: StdioCollector { id: protonConnectErr }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.actionStatus = "ProtonVPN: " + root.shortLine(protonConnectErr.text || "connect failed")
actionStatusTimer.restart()
} else if (!tailscaleDownProc.running) {
tailscaleDownProc.running = true
}
root.refreshProton()
}
}
Process {
id: protonDisconnectProc
command: ["protonvpn", "disconnect"]
stdout: StdioCollector {}
stderr: StdioCollector { id: protonDisconnectErr }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.actionStatus = "ProtonVPN: " + root.shortLine(protonDisconnectErr.text || "disconnect failed")
actionStatusTimer.restart()
}
root.refreshProton()
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
else if (root.pendingConnect === "protonvpn") { root.pendingConnect = ""; root.connectProton() }
}
}
Timer {
id: protonPollWatchdog
interval: 15000
repeat: false
onTriggered: if (protonStatusProc.running) protonStatusProc.running = false
}
// --- Shared timers -----------------------------------------------------
Timer {
id: refreshTimer
interval: root._refreshIntervalMs
repeat: true
running: true
triggeredOnStart: true
onTriggered: root.refresh()
}
Timer {
id: actionStatusTimer
interval: 4000
repeat: false
onTriggered: root.actionStatus = ""
}
// --- Bar icon ------------------------------------------------------------
// A fixed, wallpaper-independent green rather than Colors.primary — the
// glyph itself doesn't change shape between states (unlike Tailscale's
// crossed-out logo), so "connected" needs to read as a shape cue (a
// badge appearing) rather than rely on a color swap that can end up
// low-contrast depending on the current pywal palette.
readonly property color connectedBadgeColor: "#3ddc84"
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: Appearance.iconSize
color: root.anyConnected ? Colors.primary : Colors.muted
text: "vpn_lock"
}
// Small solid dot badge, bottom-right of the glyph, shown only while a
// tunnel is actually up — a shape appearing/disappearing reads at a
// glance even when the icon tint alone wouldn't stand out.
Rectangle {
visible: root.anyConnected
width: 6
height: 6
radius: 3
color: root.connectedBadgeColor
border.color: Colors.background
border.width: 1
anchors.right: label.right
anchors.bottom: label.bottom
anchors.rightMargin: 0
anchors.bottomMargin: 1
}
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
property bool busy: false
signal toggled()
implicitWidth: 34
implicitHeight: 18
opacity: toggleRoot.enabled ? 1 : 0.4
Rectangle {
anchors.fill: parent
radius: 0
color: toggleRoot.checked ? Colors.primary : Colors.muted
opacity: (toggleRoot.checked ? 1 : 0.35) * (toggleRoot.busy ? 0.6 : 1)
}
// Hover-only outline, offset outside the switch itself rather than
// drawn on its edge — the switch lags behind a click until the
// connect/disconnect process actually finishes, so this is just a
// "you can click this" cue, not click feedback. toggleMouse.containsMouse
// is already false while busy, since a disabled MouseArea doesn't
// track hover.
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
enabled: !toggleRoot.busy
hoverEnabled: true
cursorShape: toggleRoot.busy ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: toggleRoot.toggled()
}
}
// A single provider's row — icon, name, live status line, and a toggle
// that connects/disconnects it (routed back through root so it can
// enforce the one-provider-at-a-time rule).
component ProviderRow: Rectangle {
id: provRow
property string label: ""
property string subIcon: "shield"
property string detailText: ""
property string state: "disconnected"
property bool installed: true
signal toggle()
width: parent ? parent.width : 0
radius: 0
implicitHeight: rowContent.implicitHeight + 12
color: provRow.state === "connected"
? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18)
: "transparent"
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.AlignHCenter
text: provRow.subIcon
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: provRow.state === "connected" ? Colors.primary : Colors.foreground
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Text {
Layout.fillWidth: true
text: provRow.label
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
font.bold: provRow.state === "connected"
color: Colors.foreground
}
Text {
Layout.fillWidth: true
text: !provRow.installed ? "Not installed"
: provRow.state === "connecting" ? "Connecting…"
: provRow.state === "disconnecting" ? "Disconnecting…"
: provRow.state === "connected" ? (provRow.detailText || "Connected")
: "Disconnected"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
ToggleSwitch {
enabled: provRow.installed
checked: provRow.state === "connected" || provRow.state === "connecting"
busy: provRow.state === "connecting" || provRow.state === "disconnecting"
onToggled: provRow.toggle()
}
}
}
// --- Panel -----------------------------------------------------------
PopupWindow {
id: panel
visible: false
color: "transparent"
readonly property int contentWidth: 244
readonly property int contentPadding: 16
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
Behavior on implicitHeight {
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
}
onVisibleChanged: if (visible) root.refresh()
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
RowLayout {
width: parent.width
spacing: 8
Text {
text: "vpn_lock"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 22
color: root.anyConnected ? Colors.primary : Colors.foreground
}
Text {
Layout.fillWidth: true
text: "VPN"
font.family: Appearance.fontFamily
font.pixelSize: 18
font.bold: true
color: Colors.foreground
}
}
Text {
visible: root.actionStatus !== ""
width: parent.width
text: root.actionStatus
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
wrapMode: Text.WordWrap
}
Column {
width: parent.width
spacing: 2
ProviderRow {
label: "Mullvad"
subIcon: "gpp_good"
state: root.mullvadState
detailText: root.mullvadDetail
installed: root.mullvadInstalled
onToggle: root.toggleMullvad()
}
ProviderRow {
label: "ProtonVPN"
subIcon: "gpp_good"
state: root.protonState
detailText: root.protonDetail
installed: root.protonInstalled
onToggle: root.toggleProton()
}
}
Text {
width: parent.width
text: "Connecting one disconnects the other — only one VPN tunnel at a time."
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
wrapMode: Text.WordWrap
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
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
onClicked: Hyprland.dispatch("workspace " + modelData.name)
}
}
}
}

View File

@@ -0,0 +1,125 @@
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
// Top-right, just under the bar — same fixed distance from the bar and
// screen edge (Appearance.spacing) that every popup opened from the bar
// itself uses, so this lines up with them instead of floating
// bottom-center.
anchors { top: true; right: true }
margins.top: Appearance.spacing
margins.right: Appearance.spacing
implicitWidth: 272
implicitHeight: 54
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: 14
spacing: 12
Text {
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 18
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: 16
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: 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, slider.value))
height: 4
radius: 0
color: Colors.primary
opacity: slider.enabled ? 1 : 0.4
}
Rectangle {
width: 12
height: 12
radius: 6
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: 32
horizontalAlignment: Text.AlignRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
text: Math.round(slider.value * 100) + "%"
}
}
}
}

View File

@@ -0,0 +1,259 @@
#!/bin/bash
# Pre-flight check for everything this quickshell config depends on outside
# its own QML — fonts, the compositor, backing D-Bus services, and the
# external CLIs individual bar modules shell out to. Run this after
# installing/stowing quickshell on a new machine, before assuming the bar
# is just going to work.
#
# Usage: ./CheckQuickshellSetup.sh [path-to-quickshell-config]
# Defaults to $XDG_CONFIG_HOME/quickshell or ~/.config/quickshell.
set -uo pipefail
QS_DIR="${1:-${XDG_CONFIG_HOME:-$HOME/.config}/quickshell}"
PASS=0
WARN=0
FAIL=0
c_green="\033[0;32m"; c_yellow="\033[0;33m"; c_red="\033[0;31m"; c_reset="\033[0m"
pass() { printf " ${c_green}✓${c_reset} %s\n" "$1"; PASS=$((PASS+1)); }
warn() { printf " ${c_yellow}!${c_reset} %s\n" "$1"; WARN=$((WARN+1)); }
fail() { printf " ${c_red}✗${c_reset} %s\n" "$1"; FAIL=$((FAIL+1)); }
section() { printf "\n%s\n" "$1"; }
svc_state() {
# active / inactive / missing / no-systemctl. $1 = unit, $2 = --user (optional)
# NB: capture-then-here-string, not `cmd | grep -q`. Under `set -o
# pipefail`, grep -q exits the instant it finds a match, SIGPIPE-ing a
# still-writing producer — that producer's non-zero exit (not grep's
# success) becomes the pipeline's status, which reads as "not found"
# even when it was. Verified this actually happens with fc-list below.
if ! command -v systemctl >/dev/null 2>&1; then echo "no-systemctl"; return; fi
local unit_files
unit_files=$(systemctl ${2:-} list-unit-files "$1" 2>/dev/null)
if grep -q "$1" <<< "$unit_files"; then
systemctl ${2:-} is-active "$1" 2>/dev/null || echo "inactive"
else
echo "missing"
fi
}
# --- 0. Quickshell itself + config files present ----------------------------
section "Quickshell core"
if command -v quickshell >/dev/null 2>&1; then
ver=$(quickshell --version 2>&1 | head -1)
pass "quickshell found ($(command -v quickshell)) — $ver"
else
fail "quickshell binary not found on PATH — install it before anything else here matters"
fi
for f in "shell.qml" "config/Appearance.qml" "config/Colors.qml" "config/qmldir"; do
if [ -f "$QS_DIR/$f" ]; then
pass "found $f"
else
fail "missing $QS_DIR/$f — dotfiles not fully stowed/symlinked here?"
fi
done
# Nearly every module (ActiveWindow, Workspaces, PowerButton, PanelWindow's
# layer-shell surfaces, HyprlandFocusGrab) assumes a live Hyprland session.
if [ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ] && command -v hyprctl >/dev/null 2>&1 && hyprctl monitors >/dev/null 2>&1; then
pass "Hyprland is the running compositor"
else
fail "Hyprland doesn't appear to be running — Quickshell.Hyprland and the layer-shell bar will not function"
fi
# --- 1. Fonts -----------------------------------------------------------------
section "Fonts"
if command -v fc-list >/dev/null 2>&1; then
# Captured once, then grepped via here-string — see the pipefail/SIGPIPE
# note on svc_state() above; fc-list's output is exactly what triggers it.
fc_out=$(fc-list)
if grep -qi "Material Symbols Outlined" <<< "$fc_out"; then
pass "Material Symbols Outlined installed"
else
fail "Material Symbols Outlined not found — install 'ttf-material-symbols-variable' (AUR); every glyph icon in the bar/panels will render as fallback text without it"
fi
if grep -qi "CaskaydiaCove Nerd Font Mono" <<< "$fc_out"; then
pass "CaskaydiaCove Nerd Font Mono installed"
else
fail "CaskaydiaCove Nerd Font Mono not found — install a Cascadia Code Nerd Font package; clock/labels/window title use it"
fi
else
warn "fc-list not found (fontconfig missing?) — can't verify fonts"
fi
# --- 2. Backing services for native Quickshell modules ----------------------
# These modules talk to system services directly over D-Bus rather than
# shelling out — the service being up is the actual dependency, not a CLI.
section "Backing services (native QML modules)"
st=$(svc_state NetworkManager.service)
case "$st" in
active) pass "NetworkManager.service active — Quickshell.Networking (NetworkIndicator)" ;;
missing) fail "NetworkManager not installed — required by NetworkIndicator.qml" ;;
*) fail "NetworkManager.service is $st — run: sudo systemctl enable --now NetworkManager" ;;
esac
st=$(svc_state bluetooth.service)
case "$st" in
active) pass "bluetooth.service active — Quickshell.Bluetooth (BluetoothIndicator)" ;;
missing) fail "bluez not installed — required by BluetoothIndicator.qml" ;;
*) fail "bluetooth.service is $st — run: sudo systemctl enable --now bluetooth" ;;
esac
# Pipewire/wireplumber are user-session services, not system ones.
st=$(svc_state pipewire.service --user)
case "$st" in
active) pass "pipewire.service (user) active — Quickshell.Services.Pipewire (Volume/VolumeOsd)" ;;
missing) fail "pipewire not installed — required by Volume.qml/VolumeOsd.qml" ;;
*) fail "pipewire.service (user) is $st — run: systemctl --user enable --now pipewire pipewire-pulse wireplumber" ;;
esac
st=$(svc_state wireplumber.service --user)
case "$st" in
active) pass "wireplumber.service (user) active" ;;
missing) warn "wireplumber not installed — pipewire falls back to pipewire-media-session if present, otherwise volume control may misbehave" ;;
*) fail "wireplumber.service (user) is $st — run: systemctl --user enable --now wireplumber" ;;
esac
# --- 3. External CLIs shelled out to via Process ----------------------------
section "External CLIs (Quickshell.Io Process calls)"
check_bin() {
# $1 = binary, $2 = which module(s) use it, $3 = fail|warn on missing
if command -v "$1" >/dev/null 2>&1; then
pass "$1 found — used by $2"
elif [ "$3" = "warn" ]; then
warn "$1 not found — used by $2 (optional on this machine)"
else
fail "$1 not found — used by $2"
fi
}
check_bin nmcli "NetworkIndicator.qml (DNS provider panel, connection queries)" fail
check_bin ip "NetworkIndicator.qml (gateway/IP lookups)" fail
check_bin ping "NetworkIndicator.qml (latency/packet-loss stat)" fail
check_bin cat "NetworkIndicator.qml (/sys/class/net throughput counters)" fail
check_bin systemctl "PowerButton.qml (suspend/hibernate/reboot/poweroff)" fail
check_bin tailscale "TailscaleIndicator.qml / VpnExclusivity" warn
check_bin mullvad "VpnIndicator.qml / VpnExclusivity" warn
check_bin protonvpn "VpnIndicator.qml / VpnExclusivity" warn
have_tailscale=0; have_mullvad=0; have_proton=0
command -v tailscale >/dev/null 2>&1 && have_tailscale=1
command -v mullvad >/dev/null 2>&1 && have_mullvad=1
command -v protonvpn >/dev/null 2>&1 && have_proton=1
if [ $have_mullvad -eq 0 ] && [ $have_proton -eq 0 ]; then
warn "neither Mullvad nor ProtonVPN CLI is installed — VpnIndicator.qml will show both as 'Not installed'"
fi
# --- 4. VPN daemons + functional status (Mullvad/ProtonVPN/Tailscale) -------
# The exclusivity logic (VpnExclusivity.qml backing off Tailscale when a
# commercial VPN connects) is pure QML and ships with the dotfiles — it
# needs no setup of its own. What varies per machine is whether these three
# are actually installed, enabled, and callable without a password prompt.
section "VPN dual-setup (Mullvad/ProtonVPN + Tailscale)"
if [ $have_tailscale -eq 1 ]; then
st=$(svc_state tailscaled.service)
case "$st" in
active) pass "tailscaled.service is active" ;;
missing) fail "tailscaled.service not found — reinstall tailscale" ;;
*) fail "tailscaled.service is $st — run: sudo systemctl enable --now tailscaled" ;;
esac
if out=$(timeout 5 tailscale status --json 2>&1); then
if python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1 <<< "$out"; then
pass "tailscale status --json returns valid JSON"
else
warn "tailscale status --json returned non-JSON output"
fi
else
fail "tailscale status --json failed: $(echo "$out" | head -1)"
fi
fi
if [ $have_mullvad -eq 1 ]; then
st=$(svc_state mullvad-daemon.service)
case "$st" in
active) pass "mullvad-daemon.service is active" ;;
missing) fail "mullvad-daemon.service not found — reinstall the Mullvad package" ;;
*) fail "mullvad-daemon.service is $st — run: sudo systemctl enable --now mullvad-daemon" ;;
esac
if out=$(timeout 5 mullvad status --json 2>&1); then
if python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1 <<< "$out"; then
pass "mullvad status --json returns valid JSON"
else
warn "mullvad status --json returned non-JSON output"
fi
else
fail "mullvad status --json failed: $(echo "$out" | head -1)"
fi
fi
if [ $have_proton -eq 1 ]; then
# ProtonVPN's CLI drives connections through NetworkManager rather than
# its own daemon — already checked above, just re-verified here in context.
if out=$(timeout 5 protonvpn status 2>&1); then
if grep -qi "^Status:" <<< "$out"; then
pass "protonvpn status returns a parseable 'Status:' line"
else
warn "protonvpn status has no 'Status:' line — applyProtonStatus() in VpnIndicator.qml may misread it: $(echo "$out" | head -1)"
fi
else
fail "protonvpn status failed: $(echo "$out" | head -1)"
fi
fi
ts_up=0; mv_up=0; pv_up=0
# Same capture-then-here-string reasoning as above — these outputs are small
# enough that the SIGPIPE race is unlikely, but there's no upside to relying
# on that instead of just being consistent.
[ $have_tailscale -eq 1 ] && grep -q '"BackendState":"Running"' <<< "$(tailscale status --json 2>/dev/null)" && ts_up=1
[ $have_mullvad -eq 1 ] && grep -qi "^Connected" <<< "$(mullvad status 2>/dev/null)" && mv_up=1
[ $have_proton -eq 1 ] && grep -qi "^Status:\s*Connected" <<< "$(protonvpn status 2>/dev/null)" && pv_up=1
active_count=$((ts_up + mv_up + pv_up))
if [ $active_count -le 1 ]; then
pass "at most one tunnel active right now (tailscale=$ts_up mullvad=$mv_up protonvpn=$pv_up)"
else
warn "more than one tunnel is up right now (tailscale=$ts_up mullvad=$mv_up protonvpn=$pv_up) — bring extras down manually; quickshell only enforces exclusivity when you toggle from its own UI"
fi
# --- 5. System tray (currently disabled in Bar.qml) -------------------------
section "System tray (Tray.qml)"
if grep -q "^\s*//\s*Tray\s*{}" "$QS_DIR/modules/bar/Bar.qml" 2>/dev/null; then
warn "Tray {} is commented out in Bar.qml — skipping icon-theme check (nothing to render)"
elif [ -f "$QS_DIR/modules/bar/Bar.qml" ]; then
if command -v gtk-update-icon-cache >/dev/null 2>&1 || [ -d /usr/share/icons/hicolor ]; then
pass "an icon theme (at least hicolor) is present for Quickshell.Services.SystemTray icon lookups"
else
warn "no icon theme found — tray icons may render blank"
fi
fi
# --- Summary -----------------------------------------------------------------
section "Summary"
printf " ${c_green}%d passed${c_reset}, ${c_yellow}%d warnings${c_reset}, ${c_red}%d failed${c_reset}\n" "$PASS" "$WARN" "$FAIL"
if [ $FAIL -gt 0 ]; then
exit 1
fi
exit 0

View File

@@ -0,0 +1,16 @@
import Quickshell
import "./modules/bar"
import "./modules/osd"
ShellRoot {
Variants {
model: Quickshell.screens
Bar {
required property var modelData
screen: modelData
}
}
VolumeOsd {}
}

View File

@@ -1,15 +1,15 @@
{
"modules-left": ["custom/notification", "battery", "clock#date", "clock","custom/updater", "custom/launcher"],
"modules-center": ["hyprland/workspaces"],
"modules-right": ["network", "bluetooth", "pulseaudio", "custom/power"],
"modules-right": ["network", "bluetooth", "wireplumber", "wireplumber#microphone", "custom/power"],
"sway/mode": {
"format": "<span style=\"italic\">{}</span>"
},
"network": {
"format-wifi": "<span color='#962fb5'>{icon} </span>{essid} ({signalStrength}%)",
"format-ethernet": "<span color='#962fb5'> </span>Ethernet",
"format-disconnected": "󰅛 Not Connected",
"format-wifi": "<span color='#962fb5'>{icon}<span letter_spacing='500'> </span></span>{essid}({signalStrength}%)",
"format-ethernet": "<span color='#962fb5'font='bold'>{icon}<span letter_spacing='500'> </span></span><span rise='1000'>Ethernet</span>",
"format-disconnected": "󰅛<span letter_spacing='500'> </span><span rise='1000'>Not Connected</span>",
"tooltip-format": "{ipaddr} via {gwaddr}",
"tooltip-format-disconnected": "Disconnected",
"max-length": 50,
@@ -17,7 +17,7 @@
"on-click-right": "kitty -e nmtui",
"format-icons": {
"wifi": ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"],
"ethernet": "",
"ethernet": "󰈀",
"disconnected": "󰅛"
}
},
@@ -33,10 +33,10 @@
"spacing": 10
},
"clock": {
"format": " {:%H:%M}"
"format": " <span rise='300'>{:%H:%M}</span>"
},
"clock#date": {
"format": " {:%A, %B %d, %Y}",
"format": " <span rise='300'>{:%A, %B %d, %Y}</span>",
"tooltip-format": "<tt><small>{calendar}</small></tt>",
"calendar": {
"mode" : "year",
@@ -55,27 +55,30 @@
"on-click-right": "mode"
}
},
"pulseaudio": {
"format": "{icon} {volume}% {format_source}",
"format-muted": " {format_source}",
"format-bluetooth": "{icon} {volume}% {format_source}",
"format-bluetooth-muted": " {format_source}",
"format-source": " {volume}%",
"format-source-muted": " {volume}%",
"format-icons": {
"default": [
"<span color='#808080'></span>",
"<span color='#ffff66'></span>",
"<span color='#00ff7f'> </span>"]
},
"on-click": "pavucontrol"
"wireplumber": {
"format": "<span rise='300'>{icon}</span>{volume}%",
"format-muted": "",
"format-bluetooth": "{icon}{volume}%",
"format-bluetooth-muted": "",
"format-icons": [
"<span color='#808080'><span letter_spacing='500'> </span></span>",
"<span color='#ffff66'><span letter_spacing='500'> </span></span>",
"<span color='#00ff7f'><span letter_spacing='3000'> </span></span>"
],
"on-click": "pwvucontrol"
},
"wireplumber#microphone": {
"node-type": "Audio/Source",
"format": "<span letter_spacing='100'> </span><span rise='300'>{volume}%</span>",
"format-muted": "<span letter_spacing='2000'> </span>{volume}%",
"on-click": "pwvucontrol"
},
"bluetooth": {
"format-on": "<span color='#00ff7f'></span> On",
"format-on": "<span color='#00ff7f'><span letter_spacing='500'> </span></span>On",
"format-off": "<span color='#888888'>󰂲 Off</span>",
"format-disabled": "<span color='#888888'>󰂲 Off</span>",
"format-connected": "<span color='#0478d6'>󰂱</span> {device_alias}",
"format-connected-battery": "<span color='#0478d6'>󰂱 </span> {device_alias} <span color='#0478d6'>󰥈 </span> {device_battery_percentage}%",
"format-connected": "<span color='#0478d6'>󰂱<span letter_spacing='500'> </span></span>{device_alias}",
"format-connected-battery": "<span color='#0478d6'>󰂱<span letter_spacing='500'> </span></span>{device_alias}<span letter_spacing='500'> </span><span color='#0478d6'>󰥈<span letter_spacing='500'> </span></span>{device_battery_percentage}%",
"tooltip-format": "{controller_alias}\t{controller_address}\n\n{num_connections} connected",
"tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}",
"tooltip-format-enumerate-connected": "{device_alias}\t{device_address}",
@@ -97,7 +100,7 @@
"on-click-right": "swaymsg 'move scratchpad'"
},
"custom/launcher": {
"format":" ",
"format":" ",
"tooltip-format": "Click To Launch Bitwarden",
"on-click": "exec bitwarden-desktop"
},
@@ -106,11 +109,11 @@
"warning": 30,
"critical": 15
},
"format": "{icon} {capacity}%",
"format-charging": "{icon} {capacity}%",
"format-plugged": "{icon} {capacity}%",
"format-alt": "{time} {icon}",
"format-full": "󰁹 {capacity}%",
"format": "{icon}<span letter_spacing='1'> </span>{capacity}%",
"format-charging": "{icon}<span letter_spacing='1'> </span>{capacity}%",
"format-plugged": "{icon}<span letter_spacing='1'> </span>{capacity}%",
"format-alt": "{time}<span letter_spacing='1'> </span>{icon}",
"format-full": "󰁹<span letter_spacing='1'> </span>{capacity}%",
"format-icons": {
"default": ["󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂"],
"charging": ["󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋"],
@@ -137,13 +140,13 @@
"sort-by-number": true
},
"custom/updater": {
"format": "󰏖 {}",
"format": "󰏖 <span rise='300'>{}</span>",
"exec": "checkupdates | wc -l",
"exec-if": "[[ $(checkupdates | wc -l) ]]",
"interval": 5,
"interval": 1300,
"on-click": "$HOME/.config/waybar/scripts/update-distro.sh",
"tooltip": true,
"tooltip-format": "Left Click: Update System\nArch (w/ notification)\nFedora, OpenSuse, Debian/Ubuntu click to update"
"tooltip-format": "Left Click: Update System\nRight Click: Update Package Count\nArch (w/ notification)"
},
"custom/notification": {

View File

@@ -3,6 +3,9 @@
font-family:
Font Awesome,
Roboto,
"NotoSansM Nerd Font",
"NotoSansMono Nerd Font",
"Font Awesome 6 Free",
Arial,
sans-serif;
font-size: 13px;
@@ -16,10 +19,11 @@ window#waybar {
/*-----module groups----*/
.modules-right {
margin: 2px 10px 0 0;
padding-top: 1px;
}
#custom-notification {
font-family: "NotoSansMono Nerd Font";
font-family: "NotoSansM Nerd Font", "NotoSansMono Nerd Font";
font-size: 13px;
background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px;
@@ -37,6 +41,7 @@ window#waybar {
.modules-left {
margin: 2px 0 0 5px;
padding-top: 1px;
}
/*-----modules indv----*/
#workspaces button {
@@ -68,7 +73,8 @@ window#waybar {
border-radius: 20px;
margin: 5px 3px;
}
#pulseaudio {
#wireplumber,
#wireplumber-microphone {
padding: 5px 10px 5px 15px;
background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px;
@@ -118,7 +124,8 @@ window#waybar {
#idle_inhibitor.activated {
color: #2dcc36;
}
#pulseaudio.muted {
#wireplumber.muted,
#wireplumber-microphone.muted {
color: #cc3436;
}
#battery.charging {

View File

@@ -25,6 +25,9 @@ source $ZSH/oh-my-zsh.sh
alias cl='clear'
alias zed='zeditor'
alias ssh-gavin='ssh thoams@192.168.1.83'
alias ssh-raspberry-pi='ssh thoams@192.168.1.86'
alias ssh-ted='ssh thoams@192.168.1.87'
# Set-up FZF key bindings (CTRL R for fuzzy history finder)
source <(fzf --zsh)
@@ -43,6 +46,8 @@ export LC_ALL=C.UTF-8
export SSH_AUTH_SOCK=$HOME/.bitwarden-ssh-agent.sock
export ZED_ALLOW_EMULATED_GPU=1
fastfetch
eval "$(oh-my-posh init zsh --config $HOME/.config/ohmyposh/zen.toml)"