111 Commits

Author SHA1 Message Date
81b051c95d Removal: Drop QT_QUICK_CONTROLS_STYLE (hyprland-qt-support) env
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:24:49 +01:00
e563ec22d2 Feature: Shorter bar labels on narrow screens
Bar.qml switches ActiveWindow and MediaPlayer to tighter truncation
limits when the bar is narrower than compactBarWidth (1600px), so on
the laptop's eDP-1 they don't crowd the centre clock/weather group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:24:49 +01:00
7d9905f73b Fix: Make system/install.sh report what it did and stop adding blank lines
Prints whether options.conf and the pacman.conf Include were installed or
already in place, then the options pacman ends up using. Dropping an old
Include now also drops the blank line written after it, so re-runs leave
pacman.conf untouched instead of adding a blank line and rewriting it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:57:35 +01:00
7f11abf50f Feature: Pac-Man progress bars, version table and 10 parallel downloads in pacman
Turns on ILoveCandy and VerbosePkgLists and raises ParallelDownloads to 10
in system/pacman/options.conf. install.sh now puts the Include at the end of
[options] instead of straight after the header, so these override
pacman.conf's own defaults (its ParallelDownloads = 5 would otherwise win),
and moves an existing Include there when re-run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:53:36 +01:00
37c03fa03c Feature: Make Ghostty the default terminal
Super+Return, $TERMINAL (rofi-sensible-terminal and scripts) and Thunar's
"Open Terminal Here" (exo helper + helpers.rc) now open Ghostty; foot stays
installed as the fallback. Also replaces Ghostty's deprecated
gtk-tabs-location = hidden with window-show-tab-bar = never.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:50:20 +01:00
20873c1ca6 Merge pull request 'Fix: Known issues from the dotfiles documentation pass' (#8) from thomas/fixes/known_issues into main
Reviewed-on: #8
2026-10-22 08:29:48 +01:00
6e1ea8ffc4 Removal: Delete pre-Lua Hyprland config (hyprland.conf, UserConfigs, colours, animations)
hyprland.lua and its modules replaced all of these in the Lua migration;
nothing loads or reads them any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:19:12 +01:00
3eb8f6027b Fix: Toggle touchpad via hyprctl eval and detect the device automatically
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:18:39 +01:00
9e7d6284eb Fix: Update README program list and link Outline docs, fix fastfetch v2 logo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:25 +01:00
0e9dbf0abc Fix: Stop Refresh.sh relaunching Waybar, drop dead special:nyx rule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:25 +01:00
aefa68f24c Fix: Bootstrap oh-my-zsh, its plugins and the bar's Cascadia font on new installs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:25 +01:00
02adb17b4e Fix: Install hyprlock's SF Pro fonts through stow so fontconfig finds them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:19 +01:00
353d77c4fb Fix: Only set NVIDIA env vars when the NVIDIA driver is loaded, drop duplicate Qt theme var
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:19 +01:00
3931597aca Fix: Use Bibata-Modern-Classic everywhere and add it to packages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:13 +01:00
206bf15bf4 Fix: Style clipboard menu with the launcher theme
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:13 +01:00
818971b29c Fix: Rewrite animation picker to switch Lua presets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:13 +01:00
f861c7c31c Fix: Autostart Plexamp from plexamp-bin's real binary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:13 +01:00
6a8c8c92d8 Fix: Set zoom via hyprctl eval and launch installed HyprMod from keybinds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:13 +01:00
e23ce6647f Fix: Use Lua dispatch syntax for log out and DPMS, repair swaync power/lock buttons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:17:13 +01:00
5c6dad3e91 Feature: Run system updater in Ghostty with floating window rule
Open the update terminal in Ghostty, offering to install it via pkexec
when missing and falling back to foot if declined. Add the
system-update-terminal window rule the script already referred to, so
the window floats centred at 1000x650 instead of tiling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:23:24 +01:00
5827f18d05 Feature: Add Ghostty config ported from foot
Same font, padding, colours, cursor and Ctrl+C/Ctrl+Shift+C bindings as
foot.ini, so the two can be tried side by side. Ctrl+C copies only with
a selection and interrupts otherwise; TERM falls back to xterm-256color
only over SSH. foot stays the default $term.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:23:24 +01:00
561312c234 Feature: Show public IP in network panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:21:09 +01:00
6e567dd1e1 Feature: Open Steam games on headless output while Sunshine streams
Disabled-by-default window rule that sends steam_app windows fullscreen
to workspace 9 (HEADLESS-1), toggled by sunshine-games.sh, so old games
like KOTOR start at the stream's resolution.
2026-10-22 07:11:14 +01:00
9af1beeaae Feature: Add now-playing media player to bar
Show the current MPRIS track after the workspaces, with a per-source
brand icon (YouTube, Spotify, Plexamp, Twitch...) that greys out while
paused. Clicking it opens a panel with album art, a seek bar, transport
controls and shuffle/repeat.

Also cap the window title and track text by character count rather
than pixel width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:11:14 +01:00
16a0d12b8f Feature: Add compatibility for kotor 2026-10-22 06:41:45 +01:00
e8113f4366 Fix: Adding all/most hosts 2026-10-22 06:40:05 +01:00
ea11fe0eb6 Feature: Make home UniFi VPN row a collapsible card
Put the Replace config option behind an expand chevron, like Mullvad's
options, and frame the card while it's expanded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:07:01 +01:00
19e7d3f673 Feature: Add config import button for home UniFi VPN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:45:12 +01:00
bf86ffe258 Feature: Add gavin homelab host to SSH config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:41:26 +01:00
8d73d2a37e Feature: Add home UniFi WireGuard tunnel to VPN panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:40:23 +01:00
54b91a72e2 Feature: Add new-install package review list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:24:30 +01:00
788d4939f0 Fix: Drop plain Ctrl+V paste binding in foot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:23:41 +01:00
9a038b74c3 Fix: Restart portals after env import so screen sharing works
xdg-desktop-portal-hyprland could start before Hyprland imported
WAYLAND_DISPLAY/XDG_CURRENT_DESKTOP, leaving no ScreenCast backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:23:41 +01:00
954dedf7cd Fix: Scale bar volume scroll by distance for touchpads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:40:27 +01:00
90d0e2bed0 Feature: Show Plexamp icon in media notifications
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:37:01 +01:00
14f72af3f2 Fix: Fall back to grim/slurp for screenshots without hyprshot
Super+Print and Super+Shift+Print use hyprshot when installed and
grim/slurp otherwise. Add Super+Shift+S (area) and Super+F6 (full
screen) for keyboards without a Print key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:31:57 +01:00
836848dedd Fix: Bind workspace number keys by name in Lua config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:26:10 +01:00
fbaffc0816 Fix: Use Lua dispatch syntax for bar workspace buttons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:26:10 +01:00
ccae379490 Feature: Ctrl+C/Ctrl+V copy and paste in foot
Ctrl+C and Ctrl+V now copy and paste like in other apps. The interrupt
signal (^C) moves to Ctrl+Shift+C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:33:49 +01:00
7f0fad5922 Fix: Run Plex under XWayland to stop a blank window
hypr env sets QT_QPA_PLATFORM="wayland;xcb", and under Wayland Plex's
bundled QtWebEngine never loads the web client, so the window stays
blank. Both the PATH wrapper and the .desktop entry now force xcb, as
upstream does, and set XCURSOR_SIZE=24 so the X11 cursor isn't huge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:33:12 +01:00
b40d4a9fa4 Fix: Point Sunshine tray link at new tailnet domain
The tailnet's MagicDNS suffix changed from twhomelab.local to
twhomelab.tailnet, so the Sunshine web UI link now opens
xol.twhomelab.tailnet:47990.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:33:12 +01:00
19d6936aa6 Feature: Enable hypridle without locking during Sunshine streams
Sunshine's global_prep_cmd now runs hypr/scripts/SunshineIdle.sh, which
holds a systemd idle inhibitor for the length of a stream (hypridle
respects it) and unlocks hyprlock if the session was already locked.
With that in place hypridle starts at login again.

Sunshine's config (sunshine.conf, apps.json, prep scripts) moves into
the dotfiles; credentials, logs and state stay local and are gitignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:14:44 +01:00
4a57e9902f Feature: Bootstrap script for new installs
bootstrap.sh installs everything listed in system/packages.txt (pacman,
AUR via yay, flatpak, official install scripts), auto-detects CPU
microcode and GPU drivers, enables services, links dotfiles with stow
and runs system/install.sh. Optional --svt-av1 installs
svt-av1-essential-git and rebuilds ffmpeg against it via
system/rebuild-ffmpeg.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:10:27 +01:00
1201e6a4cc Feature: Sync dotfiles on system update
update-system.sh now runs sync-dotfiles.sh first, which pulls the repo
(fast-forward only) and re-stows it so both machines pick up config changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:34:07 +01:00
626c7dbcc1 Feature: Move volume OSD to bottom-center and enlarge slightly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:28:55 +01:00
35374800bf Fix: Volume keys use wpctl instead of missing pamixer
Quickshell's VolumeOsd now shows changes, so drop notify-send popups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:28:55 +01:00
7cb7f10ff6 Fix: Only create Sunshine headless output on desktop (no battery)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:10:21 +01:00
88484f8495 Remove: Drop orbit, network click opens nm-connection-editor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:10:21 +01:00
1babf2e334 Feature: Shared pacman options installed via system/install.sh
Enables Color through an Include snippet in /etc/pacman.d so pacman
options stay in sync between machines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:10:21 +01:00
cc78daea77 Merge branch 'main' of ssh://gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles 2026-10-11 16:52:33 +01:00
0415804b67 applying changes 2026-10-11 16:50:54 +01:00
75aa6b945b UI: Remove reduced opacity on unfocused windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:46:06 +01:00
8b65136048 Feature: Add ForwardAgent for passing SSH Keys 2026-10-11 02:05:54 +01:00
4bd3d327c6 Feature: Seafile starts on login and is pinned in the tray
Launches seafile-applet from startup.lua and adds it to PinnedTray between Bitwarden and Sunshine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:49:04 +01:00
786a3d191a Fix: Typo in Hyprland startup.lua stopped startup apps launching
The typo caused a Lua error on login, so the rest of the startup handler never ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:38:24 +01:00
ad5efc9597 Feature: cl alias now runs fastfetch after clear
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:53:56 +01:00
850da708c2 UI: Update icon no longer highlights when updates are available
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:53:56 +01:00
de39c40460 Feature: Added Videos bookmark
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:53:56 +01:00
72db6ca784 Fix: Disabled VRR to stop flickering on Acer monitor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:53:56 +01:00
84828e58bc Feature: Use Bitwarden SSH agent for GUI apps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:53:56 +01:00
26b7bf4ecc Feature: Enabled NVIDIA environment variables
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:53:56 +01:00
5b58c27e80 Fix: Removed Comments 2026-09-06 02:06:00 +01:00
82be653c04 UI: Corrected Sizing 2026-09-06 01:02:45 +01:00
d395a155af Feature: Added Weather widget 2026-09-06 01:00:25 +01:00
243c90a809 Features: Added Update functionality 2026-09-06 00:35:20 +01:00
38df0301f2 Feature: Icon does not appear unless installed 2026-09-05 23:10:26 +01:00
714fadc997 Fix: Corrected opened URL and Added assets for Sunshine 2026-09-05 23:03:02 +01:00
99e2225b51 UI: Made Bitwarden correct size 2026-09-05 22:58:20 +01:00
4aaa1d2d98 UI: Fixes Sizes 2026-09-05 22:16:10 +01:00
14b7e748d4 Feature: Add pinned tray for Bitwarden and Sunshine
Replaces the disabled generic system tray with a curated PinnedTray
showing just Bitwarden and Sunshine. Each shows its live tray icon
(and forwards clicks to it) while running; when not running it shows
a dimmed static icon and launches the app on click instead.

Right-clicking Sunshine specifically opens its web UI in the browser
rather than forwarding to secondaryActivate(), since that's how it's
actually configured/used.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 22:10:21 +01:00
a0b9ea29fe Feature: Added options for mullvad vpn 2026-09-05 21:56:42 +01:00
007843db6e Fix: Not to render on display with HEADLESS 2026-09-05 21:55:57 +01:00
764652d826 Feature: Added new window for steam big picture 2026-09-05 21:55:16 +01:00
8dedfe30fa Merge pull request 'Removal: Stopped Waybar from starting' (#7) from maintinance/remove_waybar into main
Reviewed-on: #7

granted
2026-08-27 15:02:44 +01:00
bc54a4b5b7 Removal: Stopped Waybar from starting 2026-08-27 15:01:45 +01:00
d306270dc8 Merge pull request 'features/adding_quickshell' (#6) from features/adding_quickshell into main
Reviewed-on: #6

tested on home laptop
tested in home PC

will need to make install scripts for process

approved
2026-08-27 14:55:29 +01:00
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
118 changed files with 6087 additions and 3126 deletions

View File

@@ -39,13 +39,29 @@ mkdir Projects
git clone ssh://git@gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles.git
```
Once that is complete, run this command:
On a fresh install, run the bootstrap script. It installs drivers (auto-detected),
every package in `system/packages.txt`, yay, flatpaks, then links the dotfiles and
system config below:
```console
~/Projects/dotfiles/bootstrap.sh # asks about the optional SVT-AV1 + ffmpeg rebuild
~/Projects/dotfiles/bootstrap.sh --svt-av1 # or --no-svt-av1 to skip the question
```
To only link the dotfiles, run this command:
```console
stow -d ~/Projects/dotfiles/ -t ~/ dotfiles
```
This will sync wallpapers/config files to where they are usually located
System-level config (e.g. pacman options) lives in `system/` and is installed with sudo:
```console
~/Projects/dotfiles/system/install.sh
```
Full documentation (every config, keybinding and script) lives in the Outline wiki's
**Dotfiles** collection: https://outline.twhomelab.co.uk/collection/dotfiles-frMIfJi2Hf
## 📦 Programs
List of programs and tools I use.
@@ -54,12 +70,12 @@ List of programs and tools I use.
| Component | Program |
|-------------------|------------|
|🪟 Windows Manager | [hyprland](https://github.com/hyprwm/Hyprland) |
|🖥️ Terminal | [foot](https://codeberg.org/dnkl/foot) |
|🐚 Shell | [zsh](https://github.com/ohmyzsh/ohmyzsh/wiki/Installing-ZSH) / [theme](https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/refined.zsh-theme) |
|🖥️ Terminal | [ghostty](https://ghostty.org/) / [foot](https://codeberg.org/dnkl/foot) (fallback) |
|🐚 Shell | [zsh](https://github.com/ohmyzsh/ohmyzsh/wiki/Installing-ZSH) + oh-my-zsh / [oh-my-posh](https://ohmyposh.dev/) prompt (`.config/ohmyposh/zen.toml`) |
|📁 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 | [quickshell](https://quickshell.org/) (waybar retired, config kept) |
|🚀 Launcher | [rofi](https://github.com/davatorium/rofi) |
|🔒 Lockscreen | [hyprlock](https://github.com/hyprwm/hyprlock) |
|🚪 Login Menu | [sddm](https://github.com/sddm/sddm) |

189
bootstrap.sh Executable file
View File

@@ -0,0 +1,189 @@
#!/usr/bin/env bash
# Sets up a fresh Arch install: drivers, packages, dotfiles and system config.
# Safe to re-run; anything already installed is skipped.
#
# Usage: ./bootstrap.sh [--svt-av1 | --no-svt-av1]
# --svt-av1 also install svt-av1-essential-git and rebuild ffmpeg against it
# --no-svt-av1 skip that without asking
# (neither) ask
set -euo pipefail
repo="$(cd "$(dirname "$0")" && pwd)"
list="$repo/system/packages.txt"
svt_av1=ask
for arg in "$@"; do
case "$arg" in
--svt-av1) svt_av1=yes ;;
--no-svt-av1) svt_av1=no ;;
*) echo "Unknown option: $arg" >&2; exit 1 ;;
esac
done
if (( EUID == 0 )); then
echo "Run this as your normal user, not root (it uses sudo where needed)." >&2
exit 1
fi
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
# Prints the packages under a [section] of packages.txt, skipping comments
packages() {
awk -v want="[$1]" '
/^\[/ { section = $1; next }
section == want { sub(/#.*/, ""); if (NF) print $1 }
' "$list"
}
# --- System config -----------------------------------------------------------
step "System config (pacman options)"
"$repo/system/install.sh"
step "Enabling multilib (needed for Steam and 32-bit drivers)"
if ! grep -q '^\[multilib\]' /etc/pacman.conf; then
sudo sed -i '/^#\[multilib\]/,/^#Include/ s/^#//' /etc/pacman.conf
fi
sudo pacman -Syu --needed --noconfirm pciutils git base-devel
# --- Hardware ----------------------------------------------------------------
step "Detecting CPU and GPU"
hw=()
case "$(grep -m1 '^vendor_id' /proc/cpuinfo | awk '{print $3}')" in
AuthenticAMD) hw+=(amd-ucode); echo "CPU: AMD" ;;
GenuineIntel) hw+=(intel-ucode); echo "CPU: Intel" ;;
*) echo "CPU: unknown vendor, skipping microcode" ;;
esac
# Vendor column of every display controller, e.g. "NVIDIA Corporation"
gpus="$(lspci -mm | awk -F'"' '$2 ~ /VGA|3D|Display/ { print $4 }')"
if grep -q NVIDIA <<<"$gpus"; then
echo "GPU: NVIDIA"
# nvidia-open only ships modules for the stock `linux` kernel; any other
# kernel needs the DKMS build. Supports Turing (GTX 16xx / RTX 20xx) and newer.
if pacman -Qq linux-lts linux-zen linux-hardened >/dev/null 2>&1; then
hw+=(nvidia-open-dkms linux-headers)
else
hw+=(nvidia-open)
fi
hw+=(nvidia-utils lib32-nvidia-utils libva-nvidia-driver)
fi
if grep -q 'Advanced Micro Devices' <<<"$gpus"; then
echo "GPU: AMD"
hw+=(mesa lib32-mesa vulkan-radeon lib32-vulkan-radeon)
fi
if grep -q 'Intel Corporation' <<<"$gpus"; then
echo "GPU: Intel"
hw+=(mesa lib32-mesa vulkan-intel lib32-vulkan-intel intel-media-driver)
fi
# Installed before the main list so Steam's 32-bit Vulkan dependency is
# already satisfied by the right driver
if (( ${#hw[@]} )); then
sudo pacman -S --needed --noconfirm "${hw[@]}"
fi
# --- Packages ----------------------------------------------------------------
step "Official repo packages"
mapfile -t pkgs < <(packages pacman)
sudo pacman -S --needed --noconfirm "${pkgs[@]}"
step "yay (AUR helper)"
if ! command -v yay >/dev/null; then
build="$(mktemp -d)"
git clone https://aur.archlinux.org/yay-bin.git "$build/yay-bin"
(cd "$build/yay-bin" && makepkg -si --noconfirm)
rm -rf "$build"
else
echo "already installed"
fi
step "AUR packages"
mapfile -t pkgs < <(packages aur)
yay -S --needed --noconfirm "${pkgs[@]}"
step "Flatpak apps"
sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
mapfile -t pkgs < <(packages flatpak)
sudo flatpak install -y --noninteractive flathub "${pkgs[@]}"
step "Apps from official install scripts"
mkdir -p "$HOME/.local/bin"
for app in $(packages installer); do
if command -v "$app" >/dev/null || [[ -x "$HOME/.local/bin/$app" ]]; then
echo "$app: already installed"
continue
fi
case "$app" in
claude) curl -fsSL https://claude.ai/install.sh | bash ;;
oh-my-posh) curl -fsSL https://ohmyposh.dev/install.sh | bash -s -- -d "$HOME/.local/bin" ;;
*) echo "$app: no install recipe in bootstrap.sh, skipping" >&2 ;;
esac
done
step "oh-my-zsh and plugins"
# .zshrc sources ~/.oh-my-zsh/oh-my-zsh.sh and loads two custom plugins from it.
# A plain clone is all oh-my-zsh needs; its installer would also write a template
# ~/.zshrc, which then conflicts with the stowed one.
if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
git clone --depth 1 https://github.com/ohmyzsh/ohmyzsh.git "$HOME/.oh-my-zsh"
else
echo "oh-my-zsh: already installed"
fi
zsh_custom="$HOME/.oh-my-zsh/custom/plugins"
for plugin in zsh-autosuggestions zsh-syntax-highlighting; do
if [[ -d "$zsh_custom/$plugin" ]]; then
echo "$plugin: already installed"
else
git clone --depth 1 "https://github.com/zsh-users/$plugin" "$zsh_custom/$plugin"
fi
done
# --- Optional: SVT-AV1 essential + ffmpeg rebuild ----------------------------
if [[ $svt_av1 == ask ]]; then
read -rp $'\nInstall svt-av1-essential-git and rebuild ffmpeg against it? (slow) [y/N] ' reply
[[ $reply == [yY]* ]] && svt_av1=yes || svt_av1=no
fi
if [[ $svt_av1 == yes ]]; then
step "svt-av1-essential-git"
# Interactive on purpose: pacman asks to replace the conflicting svt-av1,
# and --noconfirm would answer no
yay -S --needed svt-av1-essential-git
step "Rebuilding ffmpeg against svt-av1-essential-git"
"$repo/system/rebuild-ffmpeg.sh"
fi
# --- Services ----------------------------------------------------------------
step "Enabling services"
# Not started now: sddm would take over the screen mid-script. They come up on reboot.
mapfile -t units < <(packages services)
sudo systemctl enable "${units[@]}"
# --- Dotfiles ----------------------------------------------------------------
step "Linking dotfiles with stow"
# Create real parent dirs first, otherwise stow symlinks the whole directory
# (e.g. ~/.config) into the repo and every app's files end up in git
mkdir -p "$HOME/.config" "$HOME/.local/bin" "$HOME/.local/share/fonts" "$HOME/Pictures"
# Sunshine keeps credentials and logs beside its config; those must stay out of git
mkdir -p "$HOME/.config/sunshine"
mkdir -p -m 700 "$HOME/.ssh"
if ! stow -d "$repo" -t "$HOME" --restow dotfiles; then
echo "stow hit conflicts (listed above). Move those files aside, then run:" >&2
echo " stow -d \"$repo\" -t \"\$HOME\" dotfiles" >&2
fi
if [[ "$(getent passwd "$USER" | cut -d: -f7)" != */zsh ]]; then
step "Setting zsh as login shell"
chsh -s /usr/bin/zsh
fi
# Picks up the stowed fonts in ~/.local/share/fonts (SF Pro Display for hyprlock)
fc-cache -f "$HOME/.local/share/fonts" >/dev/null
step "Done — reboot to load drivers and start Hyprland"

View File

@@ -0,0 +1 @@
SSH_AUTH_SOCK=${HOME}/.bitwarden-ssh-agent.sock

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
"logo": {
//"source": "~/.config/fastfetch/nixos.png",
//"source": "~/.config/fastfetch/arch.png",
//"type": "kitty-direct",
"height": 15,
"width": 30,

View File

@@ -47,3 +47,11 @@ regular4=53BDFA # blue
regular5=FAE994 # magenta
regular6=99faf2 # cyan
regular7=C7C7C7 # white
[key-bindings]
clipboard-copy=Control+c XF86Copy
clipboard-paste=Control+Shift+v XF86Paste
[text-bindings]
# Ctrl+C now copies, so send the interrupt signal (^C) from Ctrl+Shift+C instead
\x03=Control+Shift+c

View File

@@ -0,0 +1,67 @@
# Ported from foot/foot.ini — same look and bindings, so the two can be
# tried side by side. Ghostty is now the default terminal ($term in
# hypr/input/keybinds.lua). Check edits with `ghostty +validate-config`.
# --- Font / window ---
font-family = JetBrainsMono Nerd Font
font-size = 9
# foot: pad=12x12 center
window-padding-x = 12
window-padding-y = 12
window-padding-balance = true
# foot: initial-window-size-pixels=1000x520. Ghostty sizes in cells, not
# pixels — this is roughly the same at this font size. Only matters for
# floating windows; tiled ones fill their slot either way.
window-width = 135
window-height = 31
# No title bar or tab strip — Hyprland draws the borders. (window-show-tab-bar
# replaces the deprecated gtk-tabs-location = hidden.)
window-decoration = none
window-show-tab-bar = never
# --- Terminal type ---
# foot forced TERM=xterm-256color everywhere so SSH'd hosts wouldn't lack
# terminfo. Ghostty keeps its own richer TERM locally and only falls back
# to xterm-256color (or installs its terminfo) when you ssh somewhere.
shell-integration-features = ssh-env,ssh-terminfo
# --- Scrollback / mouse ---
# foot kept 5000 lines; Ghostty's limit is in bytes and its 10MB default
# is already well beyond that, so it's left alone.
mouse-scroll-multiplier = 2
mouse-hide-while-typing = false
# URLs: Ctrl+click opens them with xdg-open by default, like foot's url-mode.
# --- Cursor ---
cursor-style = bar
cursor-style-blink = true
adjust-cursor-thickness = 1
# --- Colours ---
background-opacity = 0.80
background = #0a0e14
foreground = #fffbf6
selection-foreground = #b3b1ad
selection-background = #b3b1ad
palette = 0=#01060e
palette = 1=#eb4129
palette = 2=#abe047
palette = 3=#f9af4f
palette = 4=#53bdfa
palette = 5=#fae994
palette = 6=#99faf2
palette = 7=#c7c7c7
# --- Keybindings ---
# foot: Ctrl+C copies, Ctrl+Shift+C sends the interrupt. "performable:"
# makes Ctrl+C copy only when there's a selection — with nothing selected
# it falls through and interrupts as normal. Ctrl+Shift+C still sends ^C
# too, for muscle memory.
keybind = performable:ctrl+c=copy_to_clipboard
keybind = ctrl+shift+c=text:\x03
# Ctrl+Shift+V paste is Ghostty's default already.

View File

@@ -3,5 +3,6 @@ file:///home/thomas/Documents
file:///home/thomas/Downloads
file:///home/thomas/Pictures
file:///home/thomas/Music
file:///home/thomas/Videos
smb://192.168.1.84/media/
smb://192.168.1.85/music/

View File

@@ -2,7 +2,7 @@
gtk-theme-name=HyprFlux-Compact
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-cursor-theme-name=Future-black Cursors
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-toolbar-style=GTK_TOOLBAR_ICONS
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR

View File

@@ -2,6 +2,6 @@
gtk-theme-name=HyprFlux-Compact
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-cursor-theme-name=Future-black Cursors
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1

View File

@@ -1,6 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# HyprFlux Dots
Settings have been massivily modified as to work in a more simple mannor
I will update the Hyprland-Dots wiki for guidance. Make sure to check out changelogs as well

View File

@@ -1,89 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# Environment variables. See https://wiki.hyprland.org/Configuring/Environment-variables/
# Set your defaults editor through ENV in ~/.config/hypr/UserConfigs/01-UserDefaults.conf
# environment-variables
# Toolkit Backend Variables
env = GDK_BACKEND,wayland,x11,*
env = QT_QPA_PLATFORM,wayland;xcb
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
#env = SDL_VIDEODRIVER,wayland
# xdg Specifications
env = XDG_CURRENT_DESKTOP,Hyprland
env = XDG_SESSION_DESKTOP,Hyprland
env = XDG_SESSION_TYPE,wayland
# QT Variables
env = QT_AUTO_SCREEN_SCALE_FACTOR,1
env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1
env = QT_QPA_PLATFORMTHEME,qt5ct
env = QT_QPA_PLATFORMTHEME,qt6ct
# hyprland-qt-support
env = QT_QUICK_CONTROLS_STYLE,org.hyprland.style
# xwayland apps scale fix (useful if you are use monitor scaling).
# Set same value if you use scaling in Monitors.conf
# 1 is 100% 1.5 is 150%
# see https://wiki.hyprland.org/Configuring/XWayland/
env = GDK_SCALE,1
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/
env = HYPRCURSOR_THEME,Bibata-Modern-Classic
env = HYPRCURSOR_SIZE,24
# firefox
env = MOZ_ENABLE_WAYLAND,1
# electron >28 apps (may help) ##
# https://www.electronjs.org/docs/latest/api/environment-variables
env = ELECTRON_OZONE_PLATFORM_HINT,auto # auto selects Wayland if possible, X11 otherwise
# NVIDIA
# This is from Hyprland Wiki. Below will be activated nvidia gpu detected
# See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
#env = LIBVA_DRIVER_NAME,nvidia
#env = __GLX_VENDOR_LIBRARY_NAME,nvidia
#env = NVD_BACKEND,direct
#env = GSK_RENDERER,ngl
# additional ENV's for nvidia. Caution, activate with care
#env = GBM_BACKEND,nvidia-drm
#env = __GL_GSYNC_ALLOWED,1 #adaptive Vsync
#env = __NV_PRIME_RENDER_OFFLOAD,1
#env = __VK_LAYER_NV_optimus,NVIDIA_only
#env = WLR_DRM_NO_ATOMIC,1
# FOR VM and POSSIBLY NVIDIA
# LIBGL_ALWAYS_SOFTWARE software mesa rendering
#env = LIBGL_ALWAYS_SOFTWARE,1 # Warning. May cause hyprland to crash
#env = WLR_RENDERER_ALLOW_SOFTWARE,1
# nvidia firefox (for hardware acceleration on FF)?
# check this post https://github.com/elFarto/nvidia-vaapi-driver#configuration
#env = MOZ_DISABLE_RDD_SANDBOX,1
#env = EGL_PLATFORM,wayland
#### Aquamarine Environment Variables #### ( Hyprland > 0.45 )
# https://wiki.hyprland.org/Configuring/Environment-variables/#aquamarine-environment-variables----ref-httpsgithubcomhyprwmaquamarineblobmaindocsenvmd---
# env = AQ_TRACE,1 # Enables more verbose logging.
# env = AQ_DRM_DEVICES,/dev/dri/card1:/dev/dri/card0 # Set an explicit list of DRM devices (GPUs) to use. It’s a colon-separated list of paths, with the first being the primary. E.g. /dev/dri/card1:/dev/dri/card0
# env = AQ_MGPU_NO_EXPLICIT,1 # Disables explicit syncing on mgpu buffers
# env = AQ_NO_MODIFIERS,1 # Disables modifiers for DRM buffers
#### Hyprland Environment Variables ####
# https://wiki.hyprland.org/Configuring/Environment-variables/#hyprland-environment-variables
# env = HYPRLAND_TRACE,1 # Enables more verbose logging.
# env = HYPRLAND_NO_RT,1 # Disables realtime priority setting by Hyprland.
# env = HYPRLAND_NO_SD_NOTIFY,1 # If systemd, disables the 'sd_notify' calls.
# env = HYPRLAND_NO_SD_VARS,1 # Disables management of variables in systemd and dbus activation environments.

View File

@@ -1,209 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# This is where you put your own keybinds. Be Mindful to check as well ~/.config/hypr/configs/Keybinds.conf to avoid conflict
# if you think I should replace the Pre-defined Keybinds in ~/.config/hypr/configs/Keybinds.conf , submit an issue or let me know in DC and present me a valid reason as to why, such as conflicting with global shortcuts, etc etc
# See https://wiki.hyprland.org/Configuring/Keywords/ for more settings and variables
# See also Laptops.conf for laptops keybinds
# /* ---- ✴️ Variables ✴️ ---- */ #
$mainMod = SUPER
$scriptsDir = $HOME/.config/hypr/scripts
$UserScripts = $HOME/.config/hypr/UserScripts
# settings for User defaults apps - set your default terminal and file manager on this file
$edit=${EDITOR:-nvim}
# These two are for UserKeybinds.conf & Waybar Modules
$term = foot # Terminal
$files = thunar # File Manager
# Default Search Engine for ROFI Search (SUPER S)
$Search_Engine = "https://www.google.com/search?q={}"
# common shortcuts
bind = $mainMod, SPACE, exec, .config/rofi/launchers/type-4/launcher.sh # desktop overview (if installed)
bind = $mainMod, Q, killactive, # close active (not kill)
bind = $mainMod SHIFT, Q, exec, $scriptsDir/KillActiveProcess.sh # Kill active process
bind = $mainMod, Return, exec, $term #terminal
bind = $mainMod, F, exec, $files #file manager
bind = $mainMod, B, exec, zen-browser # Launch Zen Browser New Everytime
bind = $mainMod, V, exec, $scriptsDir/ClipManager.sh # Clipboard Manager
bind = $mainMod, C, exec, code --ozone-platform=x11 # Visual Studio Code
bind = $mainMod, O, exec, obsidian --ozone-platform=x11 # Obsidian
bind = $mainMod, X, exec, vesktop # Vesktop(Discord)
bind = $mainMod, M, exec, fdm # Free Downlaod Manager
bind = $mainMod, W, exec, waypaper # Wayland Wallpaper Setter
bind = $mainMod, L, exec, hyprlock # Screen Locker
bind = $mainMod, DELETE, exec, ~/.config/rofi/powermenu/type-2/powermenu.sh # Power Menu
bind = $mainMod, S, exec, cd /home/thomas/hyprmod && uv run hyprmod # Hyprmod - A Hyprland settings manager (if installed)
bind = $mainMod SHIFT, R, exec, $scriptsDir/Refresh.sh # Refresh waybar, swaync, rofirrr
bind = $mainMod, N, exec, swaync-client -t -sw # swayNC notification panel
# FEATURES / EXTRAS
bind = $mainMod SHIFT, F, fullscreen # whole full screen
bind = $mainMod SHIFT, SPACE, togglefloating, #Float Mode
# Desktop zooming or magnifier
bind = $mainMod ALT, mouse_down, exec, hyprctl keyword cursor:zoom_factor "$(hyprctl getoption cursor:zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor * 2.0}')"
bind = $mainMod ALT, mouse_up, exec, hyprctl keyword cursor:zoom_factor "$(hyprctl getoption cursor:zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor / 2.0}')"
## NOTES for ja (Hyprland version 0.39 (Ubuntu 24.04))
#bind = $mainMod ALT, mouse_down, exec, hyprctl keyword misc:cursor_zoom_factor "$(hyprctl getoption misc:cursor_zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor * 2.0}')"
#bind = $mainMod ALT, mouse_up, exec, hyprctl keyword misc:cursor_zoom_factor "$(hyprctl getoption misc:cursor_zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor / 2.0}')"
# Waybar / Bar related
bind = $mainMod CTRL ALT, B, exec, pkill -SIGUSR1 waybar # Toggle hide/show waybar
# FEATURES / EXTRAS (UserScripts)
bind = $mainMod SHIFT, A, exec, $scriptsDir/Animations.sh #hyprland animations menu
bind = $mainMod SHIFT, Z, exec, $UserScripts/ZshChangeTheme.sh # Change oh-my-zsh theme
#######################################################################################################################################
#Default Keybinds#
#######################################################################################################################################
bind = CTRL ALT, Delete, exec, hyprctl dispatch exit 0 # exit Hyprland
# Master Layout
bind = $mainMod CTRL, D, layoutmsg, removemaster
bind = $mainMod, I, layoutmsg, addmaster
bind = $mainMod, J, layoutmsg, cyclenext
bind = $mainMod, K, layoutmsg, cycleprev
bind = $mainMod CTRL, Return, layoutmsg, swapwithmaster
# Dwindle Layout
bind = $mainMod, P, pseudo, # dwindle
# Special Keys / Hot Keys
bindel = , xf86audioraisevolume, exec, $scriptsDir/Volume.sh --inc # volume up
bindel = , xf86audiolowervolume, exec, $scriptsDir/Volume.sh --dec # volume down
bindl = , xf86AudioMicMute, exec, $scriptsDir/Volume.sh --toggle-mic # mic mute
bindl = , xf86audiomute, exec, $scriptsDir/Volume.sh --toggle # mute
bindl = , xf86Sleep, exec, systemctl suspend # sleep button
bindl = , xf86Rfkill, exec, $scriptsDir/AirplaneMode.sh # Airplane mode
# media controls using keyboards
bindl = , xf86AudioPlayPause, exec, $scriptsDir/MediaCtrl.sh --pause
bindl = , xf86AudioPause, exec, $scriptsDir/MediaCtrl.sh --pause
bindl = , xf86AudioPlay, exec, $scriptsDir/MediaCtrl.sh --pause
bindl = , xf86AudioNext, exec, $scriptsDir/MediaCtrl.sh --nxt
bindl = , xf86AudioPrev, exec, $scriptsDir/MediaCtrl.sh --prv
bindl = , xf86audiostop, exec, $scriptsDir/MediaCtrl.sh --stop
# Resize windows
binde = $mainMod SHIFT, left, resizeactive,-50 0
binde = $mainMod SHIFT, right, resizeactive,50 0
binde = $mainMod SHIFT, up, resizeactive,0 -50
binde = $mainMod SHIFT, down, resizeactive,0 50
# Move windows
bind = $mainMod CTRL, left, movewindow, l
bind = $mainMod CTRL, right, movewindow, r
bind = $mainMod CTRL, up, movewindow, u
bind = $mainMod CTRL, down, movewindow, d
# Swap windows
bind = $mainMod ALT, left, swapwindow, l
bind = $mainMod ALT, right, swapwindow, r
bind = $mainMod ALT, up, swapwindow, u
bind = $mainMod ALT, down, swapwindow, d
# Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l
bind = $mainMod, right, movefocus, r
bind = $mainMod, up, movefocus, u
bind = $mainMod, down, movefocus, d
# Workspaces related
bind = $mainMod, tab, workspace, m+1
bind = $mainMod SHIFT, tab, workspace, m-1
# Special workspace
# bind = $mainMod SHIFT, U, movetoworkspace, special
# bind = $mainMod, U, togglespecialworkspace,
bind = $mainMod, U, togglespecialworkspace, nyx
bind = $mainMod SHIFT, U, movetoworkspace, special:nyx
# The following mappings use the key codes to better support various keyboard layouts
# 1 is code:10, 2 is code 11, etc
# Switch workspaces with mainMod + [0-9]
bind = $mainMod, code:10, workspace, 1 # NOTE: code:10 = key 1
bind = $mainMod, code:11, workspace, 2 # NOTE: code:11 = key 2
bind = $mainMod, code:12, workspace, 3 # NOTE: code:12 = key 3
bind = $mainMod, code:13, workspace, 4 # NOTE: code:13 = key 4
bind = $mainMod, code:14, workspace, 5 # NOTE: code:14 = key 5
bind = $mainMod, code:15, workspace, 6 # NOTE: code:15 = key 6
bind = $mainMod, code:16, workspace, 7 # NOTE: code:16 = key 7
bind = $mainMod, code:17, workspace, 8 # NOTE: code:17 = key 8
bind = $mainMod, code:18, workspace, 9 # NOTE: code:18 = key 9
bind = $mainMod, code:19, workspace, 10 # NOTE: code:19 = key 0
# Move active window and follow to workspace mainMod + SHIFT [0-9]
bind = $mainMod SHIFT, code:10, movetoworkspace, 1 # NOTE: code:10 = key 1
bind = $mainMod SHIFT, code:11, movetoworkspace, 2 # NOTE: code:11 = key 2
bind = $mainMod SHIFT, code:12, movetoworkspace, 3 # NOTE: code:12 = key 3
bind = $mainMod SHIFT, code:13, movetoworkspace, 4 # NOTE: code:13 = key 4
bind = $mainMod SHIFT, code:14, movetoworkspace, 5 # NOTE: code:14 = key 5
bind = $mainMod SHIFT, code:15, movetoworkspace, 6 # NOTE: code:15 = key 6
bind = $mainMod SHIFT, code:16, movetoworkspace, 7 # NOTE: code:16 = key 7
bind = $mainMod SHIFT, code:17, movetoworkspace, 8 # NOTE: code:17 = key 8
bind = $mainMod SHIFT, code:18, movetoworkspace, 9 # NOTE: code:18 = key 9
bind = $mainMod SHIFT, code:19, movetoworkspace, 10 # NOTE: code:19 = key 0
bind = $mainMod SHIFT, bracketleft, movetoworkspace, -1 # brackets [
bind = $mainMod SHIFT, bracketright, movetoworkspace, +1 # brackets ]
# Move active window to a workspace silently mainMod + CTRL [0-9]
bind = $mainMod CTRL, code:10, movetoworkspacesilent, 1 # NOTE: code:10 = key 1
bind = $mainMod CTRL, code:11, movetoworkspacesilent, 2 # NOTE: code:11 = key 2
bind = $mainMod CTRL, code:12, movetoworkspacesilent, 3 # NOTE: code:12 = key 3
bind = $mainMod CTRL, code:13, movetoworkspacesilent, 4 # NOTE: code:13 = key 4
bind = $mainMod CTRL, code:14, movetoworkspacesilent, 5 # NOTE: code:14 = key 5
bind = $mainMod CTRL, code:15, movetoworkspacesilent, 6 # NOTE: code:15 = key 6
bind = $mainMod CTRL, code:16, movetoworkspacesilent, 7 # NOTE: code:16 = key 7
bind = $mainMod CTRL, code:17, movetoworkspacesilent, 8 # NOTE: code:17 = key 8
bind = $mainMod CTRL, code:18, movetoworkspacesilent, 9 # NOTE: code:18 = key 9
bind = $mainMod CTRL, code:19, movetoworkspacesilent, 10 # NOTE: code:19 = key 0
bind = $mainMod CTRL, bracketleft, movetoworkspacesilent, -1 # brackets [
bind = $mainMod CTRL, bracketright, movetoworkspacesilent, +1 # brackets ]
# Scroll through existing workspaces with mainMod + scroll
bind = $mainMod, mouse_down, workspace, e+1
bind = $mainMod, mouse_up, workspace, e-1
bind = $mainMod, period, workspace, e+1
bind = $mainMod, comma, workspace, e-1
# Move/resize windows with mainMod + LMB/RMB and dragging
bindm = $mainMod, mouse:272, movewindow # NOTE: mouse:272 = left click
bindm = $mainMod, mouse:273, resizewindow # NOTE: mouse:272 = right click
#########################################################################################################
# Laptop specific keybinds
#########################################################################################################
# for disabling Touchpad. hyprctl devices to get device name.
$Touchpad_Device=asue1209:00-04f3:319f-touchpad
binde = , xf86KbdBrightnessDown, exec, $scriptsDir/BrightnessKbd.sh --dec # decrease keyboard brightness
binde = , xf86KbdBrightnessUp, exec, $scriptsDir/BrightnessKbd.sh --inc # increase keyboard brightness
bind = , xf86Launch1, exec, rog-control-center # ASUS Armory crate button
bind = , xf86Launch3, exec, asusctl led-mode -n # FN+F4 Switch keyboard RGB profile
bind = , xf86Launch4, exec, asusctl profile -n # FN+F5 change of fan profiles (Quite, Balance, Performance)
binde = , xf86MonBrightnessDown, exec, $scriptsDir/Brightness.sh --dec # decrease monitor brightness
binde = , xf86MonBrightnessUp, exec, $scriptsDir/Brightness.sh --inc # increase monitor brightness
bind = , xf86TouchpadToggle, exec, $scriptsDir/TouchPad.sh # disable touchpad
# Screenshot keybindings using F6 (no PrinSrc button)
#bind = $mainMod, F6, exec, $scriptsDir/ScreenShot.sh --now # screenshot
#bind = $mainMod SHIFT, F6, exec, $scriptsDir/ScreenShot.sh --area # screenshot (area)
#bind = $mainMod CTRL, F6, exec, $scriptsDir/ScreenShot.sh --in5 # # screenshot (5 secs delay)
#bind = $mainMod ALT, F6, exec, $scriptsDir/ScreenShot.sh --in10 # screenshot (10 secs delay)
#bind = ALT, F6, exec, $scriptsDir/ScreenShot.sh --active # screenshot (active window only)
$TOUCHPAD_ENABLED = true
device {
name = $Touchpad_Device
enabled = $TOUCHPAD_ENABLED
}

View File

@@ -1,4 +0,0 @@
# NOTE, THIS FILE IS BEING USED by disabling Laptop display monitor behaviour when closing lid.
# See notes on Laptops.conf
#monitor = eDP-1, preferred, auto, 1

View File

@@ -1,31 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# See https://wiki.hyprland.org/Configuring/Keywords/ for more variable settings
# These configs are mostly for laptops. This is addemdum to Keybinds.conf
$mainMod = SUPER
$scriptsDir = $HOME/.config/hypr/scripts
$UserConfigs = $HOME/.config/hypr/UserConfigs
# Below are useful when you are connecting your laptop in external display
# Suggest you edit below for your laptop display
# From WIKI This is to disable laptop monitor when lid is closed.
# consult https://wiki.hyprland.org/hyprland-wiki/pages/Configuring/Binds/#switches
#bindl = , switch:off:Lid Switch,exec,hyprctl keyword monitor "eDP-1, preferred, auto, 1"
#bindl = , switch:on:Lid Switch,exec,hyprctl keyword monitor "eDP-1, disable"
# WARNING! Using this method has some caveats!! USE THIS PART WITH SOME CAUTION!
# CONS of doing this, is that you need to set up your wallpaper (SUPER W) and choose wallpaper.
# CAVEATS! Sometimes the Main Laptop Monitor DOES NOT have display that it needs to re-connect your external monitor
# One work around is to ensure that before shutting down laptop, MAKE SURE your laptop lid is OPEN!!
# Make sure to comment (put # on the both the bindl = , switch ......) above
# NOTE: Display for laptop are being generated into LaptopDisplay.conf
# This part is to be use if you do not want your main laptop monitor to wake up during say wallpaper change etc
#bindl = , switch:off:Lid Switch,exec,echo "monitor = eDP-1, preferred, auto, 1" > $UserConfigs/LaptopDisplay.conf
#bindl = , switch:on:Lid Switch,exec,echo "monitor = eDP-1, disable" > $UserConfigs/LaptopDisplay.conf
# for laptop-lid action (to erase the last entry)
#exec-once = echo "monitor = eDP-1, preferred, auto, 1" > $HOME/.config/hypr/UserConfigs/LaptopDisplay.conf
#

View File

@@ -1,43 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# Commands and Apps to be executed at launch
$scriptsDir = $HOME/.config/hypr/scripts
# wallpaper stuff
exec-once = awww-daemon --format xrgb
#exec-once = mpvpaper '*' -o "load-scripts=no no-audio --loop" $livewallpaper
# Startup
exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
# Polkit (Polkit Gnome / KDE)
exec-once = $scriptsDir/Polkit.sh
# starup apps
exec-once = nm-applet --indicator
exec-once = swaync
#exec-once = ags
#exec-once = blueman-applet
#exec-once = rog-control-center
exec-once = waybar
exec-once = qs # quickshell AGS Desktop Overview alternative
#clipboard manager
exec-once = wl-paste --type text --watch cliphist store
exec-once = wl-paste --type image --watch cliphist store
# Starting hypridle to start hyprlock
exec-once = hypridle
exec-once = hyprctl dispatch workspace 3
exec-once = /usr/bin/Plexamp.AppImage &&
exec-once = sleep 3 && vesktop
exec-once = hyprctl dispatch workspace 1
exec-once = bitwarden-desktop --ozone-platform-hint=auto --silent
# Here are list of features available but disabled by default
# exec-once = awww-daemon --format xrgb && awww img $HOME/Pictures/wallpapers/mecha-nostalgia.png # persistent wallpaper
# xdg-desktop-portal-hyprland (should be auto starting. However, you can force to start)
#exec-once = $scriptsDir/PortalHyprland.sh

View File

@@ -1,26 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
animations {
enabled = yes
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
bezier = overshot, 0.05, 0.9, 0.1, 1.05
bezier = smoothOut, 0.5, 0, 0.99, 0.99
bezier = smoothIn, 0.5, -0.5, 0.68, 1.5
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 5, winIn, slide
animation = windowsOut, 1, 3, smoothOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 180, liner, loop #used by rainbow borders and rotating colors
animation = fade, 1, 3, smoothOut
animation = workspaces, 1, 5, overshot
# animations for -git or version >0.42.0
animation = workspacesIn, 1, 5, winIn, slide
animation = workspacesOut, 1, 5, winOut, slide
}

View File

@@ -1,58 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# Decoration Settings
# Hyprland Wiki Links
# Animation - https://wiki.hyprland.org/Configuring/Animations/
# Decoration - https://wiki.hyprland.org/Configuring/Variables/#decoration
# HyprFlux static color palette
source = $HOME/.config/hypr/colours/hyprflux-colors.conf
general {
border_size = 0
gaps_in = 2
gaps_out = 4
# col.active_border = $color12
# col.inactive_border = $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 = $color12
color_inactive = $color10
}
blur {
enabled = true
size = 6
passes = 2
ignore_opacity = true
new_optimizations = true
special = true
popups = true
}
}
group {
col.border_active = $color15
groupbar {
col.active = $color0
}
}

View File

@@ -1,116 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# User Settings
# This is where you put your own settings as this will not be touched during update
# if the upgrade.sh is used.
# refer to Hyprland wiki for more info https://wiki.hyprland.org/Configuring/Variables/
# NOTE: some settings are in ~/.config/hypr/UserConfigs/UserDecorAnimations.conf
dwindle {
preserve_split = true
#smart_split = true
special_scale_factor = 1
}
master {
new_status = master
new_on_top = 1
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 table see link above for proper variables
tablet {
transform = 0
left_handed = 0
}
}
gestures {
# workspace_swipe = true
# workspace_swipe_fingers = 3
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 = off
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 = 0
}
cursor {
sync_gsettings_theme = true
no_hardware_cursors = 2 # change to 1 if want to disable
enable_hyprcursor = true
warp_on_change_workspace = 2
no_warps = true
}

View File

@@ -1,744 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# For window rules and layerrules
# See https://wiki.hyprland.org/Configuring/Window-Rules/ for more
# NOTES: This is only for Hyprland > 0.48
# note for ja: This should NOT be implemented on Debian and Ubuntu
# windowrule - tags - add apps under appropriate tag to use the same settings
# browser tags
windowrule {
name = windowrule-1
tag = +browser
match:class = ^([Ff]irefox|org.mozilla.firefox|[Ff]irefox-esr|[Ff]irefox-bin)$
}
windowrule {
name = windowrule-2
tag = +browser
match:class = ^([Gg]oogle-chrome(-beta|-dev|-unstable)?)$
}
windowrule {
name = windowrule-3
tag = +browser
match:class = ^(chrome-.+-Default)$ # Chrome PWAs
}
# windowrule = tag +browser, class:^([Cc]hromium)$
windowrule {
name = windowrule-4
tag = +browser
match:class = ^([Mm]icrosoft-edge(-stable|-beta|-dev|-unstable))$
}
windowrule {
name = windowrule-5
tag = +browser
match:class = ^(Brave-browser(-beta|-dev|-unstable)?)$
}
windowrule {
name = windowrule-6
tag = +browser
match:class = ^([Tt]horium-browser|[Cc]achy-browser)$
}
windowrule {
name = windowrule-7
tag = +browser
match:class = ^(zen-alpha|zen)$
}
# notif tags
windowrule {
name = windowrule-8
tag = +notif
match:class = ^(swaync-control-center|swaync-notification-window|swaync-client|class)$
}
# HyprFlux settings tag
windowrule {
name = windowrule-9
tag = +HyprFlux_Cheat
match:title = ^(HyprFlux Quick Cheat Sheet)$
}
windowrule {
name = windowrule-10
tag = +HyprFlux_Settings
match:title = ^(HyprFlux Settings)$
}
windowrule {
name = windowrule-11
tag = +HyprFlux-Settings
match:class = ^(nwg-displays|nwg-look)$
}
# terminal tags
windowrule {
name = windowrule-12
tag = +terminal
match:class = ^(Alacritty|kitty|kitty-dropterm)$
}
# email tags
windowrule {
name = windowrule-13
tag = +email
match:class = ^([Tt]hunderbird|org.gnome.Evolution)$
}
windowrule {
name = windowrule-14
tag = +email
match:class = ^(eu.betterbird.Betterbird)$
}
# project tags
windowrule {
name = windowrule-15
tag = +projects
match:class = ^(codium|codium-url-handler|VSCodium)$
}
windowrule {
name = windowrule-16
tag = +projects
match:class = ^(VSCode|code-url-handler)$
}
windowrule {
name = windowrule-17
tag = +projects
match:class = ^(jetbrains-.+)$ # JetBrains IDEs
}
# screenshare tags
windowrule {
name = windowrule-18
tag = +screenshare
match:class = ^(com.obsproject.Studio)$
}
# IM tags
# windowrule = tag +im, class:^([Dd]iscord|[Ww]ebCord|[Vv]esktop)$
windowrule {
name = windowrule-19
tag = +im
center = on
float = on
size = (monitor_w*0.6) (monitor_h*0.7)
match:class = ^([Ff]erdium)$
}
windowrule {
name = windowrule-20
tag = +im
match:class = ^([Ww]hatsapp-for-linux)$
}
windowrule {
name = windowrule-21
tag = +im
match:class = ^(ZapZap|com.rtosta.zapzap)$
}
windowrule {
name = windowrule-22
tag = +im
match:class = ^(org.telegram.desktop|io.github.tdesktop_x64.TDesktop)$
}
windowrule {
name = windowrule-23
tag = +im
match:class = ^(teams-for-linux)$
}
windowrule {
name = windowrule-24
tag = +im
match:class = ^(im.riot.Riot|Element)$ # Element Matrix client
}
# game tags
windowrule {
name = windowrule-25
tag = +games
match:class = ^(gamescope)$
}
windowrule {
name = windowrule-26
tag = +games
match:class = ^(steam_app_\d+)$
}
# gamestore tags
windowrule {
name = windowrule-27
tag = +gamestore
match:class = ^([Ss]team)$
}
windowrule {
name = windowrule-28
tag = +gamestore
match:title = ^([Ll]utris)$
}
windowrule {
name = windowrule-29
tag = +gamestore
match:class = ^(com.heroicgameslauncher.hgl)$
}
# file-manager tags
windowrule {
name = windowrule-30
tag = +file-manager
match:class = ^([Tt]hunar|org.gnome.Nautilus|[Pp]cmanfm-qt)$
}
windowrule {
name = windowrule-31
tag = +file-manager
match:class = ^(app.drey.Warp)$
}
# wallpaper tags
windowrule {
name = windowrule-32
tag = +wallpaper
match:class = ^([Ww]aytrogen)$
}
# multimedia tags
windowrule {
name = windowrule-33
tag = +multimedia
match:class = ^([Aa]udacious)$
}
# multimedia-video tags
windowrule {
name = windowrule-34
tag = +multimedia_video
match:class = ^([Mm]pv|vlc)$
}
# settings tags
windowrule {
name = windowrule-35
tag = +settings
center = on
match:title = ^(ROG Control)$
}
windowrule {
name = windowrule-36
tag = +settings
match:class = ^(wihotspot(-gui)?)$ # wifi hotspot
}
windowrule {
name = windowrule-37
tag = +settings
match:class = ^([Bb]aobab|org.gnome.[Bb]aobab)$ # Disk usage analyzer
}
windowrule {
name = windowrule-38
tag = +settings
match:class = ^(gnome-disks|wihotspot(-gui)?)$
}
windowrule {
name = windowrule-39
tag = +settings
match:title = (Kvantum Manager)
}
windowrule {
name = windowrule-40
tag = +settings
match:class = ^(file-roller|org.gnome.FileRoller)$ # archive manager
}
windowrule {
name = windowrule-41
tag = +settings
match:class = ^(nm-applet|nm-connection-editor|blueman-manager)$
}
windowrule {
name = windowrule-42
tag = +settings
center = on
match:class = ^(pavucontrol|org.pulseaudio.pavucontrol|com.saivert.pwvucontrol)$
}
windowrule {
name = windowrule-43
tag = +settings
match:class = ^(qt5ct|qt6ct|[Yy]ad)$
}
windowrule {
name = windowrule-44
tag = +settings
match:class = (xdg-desktop-portal-gtk)
}
windowrule {
name = windowrule-45
tag = +settings
match:class = ^(org.kde.polkit-kde-authentication-agent-1)$
}
windowrule {
name = windowrule-46
tag = +settings
match:class = ^([Rr]ofi)$
}
# Custom rule
windowrule {
name = windowrule-47
tile = on
match:class = ^([Cc]hromium)$
}
windowrule {
name = windowrule-48
size = (monitor_w*0.25) (monitor_h*0.55)
float = on
match:class = ^(com.network.manager)$
}
windowrule {
name = windowrule-49
workspace = special:nyx
match:class = ^([Vv]esktop|[Dd]iscord|[Ss]potify)$
}
# viewer tags
windowrule {
name = windowrule-50
tag = +viewer
match:class = ^(gnome-system-monitor|org.gnome.SystemMonitor|io.missioncenter.MissionCenter)$ # system monitor
}
windowrule {
name = windowrule-51
tag = +viewer
match:class = ^(evince)$ # document viewer
}
windowrule {
name = windowrule-52
tag = +viewer
match:class = ^(eog|org.gnome.Loupe)$ # image viewer
}
# Some special override rules
windowrule {
name = windowrule-53
no_blur = on
opacity = 1.0
match:tag = multimedia_video*
}
# POSITION
# windowrule = center,floating:1 # warning, it cause even the menu to float and center.
windowrule {
name = windowrule-54
center = on
float = on
size = (monitor_w*0.65) (monitor_h*0.9)
match:tag = HyprFlux_Cheat*
}
windowrule {
name = windowrule-55
center = on
float = on
match:class = ([Tt]hunar)
match:title = negative:(.*[Tt]hunar.*)
}
windowrule {
name = windowrule-56
center = on
float = on
match:tag = HyprFlux-Settings*
}
windowrule {
name = windowrule-57
center = on
match:title = ^(Keybindings)$
}
windowrule {
name = windowrule-58
center = on
size = (monitor_w*0.6) (monitor_h*0.7)
match:class = ^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$
}
windowrule {
name = windowrule-59
move = ((monitor_w*0.61)) ((monitor_h*0.07))
float = on
size = (monitor_w*0.38) (monitor_h*0.38)
pin = on
keep_aspect_ratio = on
match:title = ^(Picture-in-Picture)$
}
#windowrule = move 72% 7%,title:^(Firefox)$
# windowrule to avoid idle for fullscreen apps
#windowrule = idleinhibit fullscreen, class:^(*)$
#windowrule = idleinhibit fullscreen, title:^(*)$
windowrule {
name = windowrule-60
idle_inhibit = fullscreen
match:fullscreen = 1
}
# windowrule move to workspace
windowrule {
name = windowrule-61
workspace = 1
match:tag = projects*
}
windowrule {
name = windowrule-62
workspace = 1
match:tag = email*
}
windowrule {
name = windowrule-63
workspace = 2
match:tag = browser*
}
windowrule {
name = windowrule-64
workspace = 3
match:class = ^([Tt]hunar)$
}
windowrule {
name = windowrule-65
workspace = 4
match:tag = im*
}
windowrule {
name = windowrule-66
workspace = 5
match:tag = gamestore*
}
windowrule {
name = windowrule-67
workspace = 8
no_blur = on
fullscreen = on
match:tag = games*
}
windowrule {
name = windowrule-68
workspace = 1
match:class = ^(kitty)$
match:title = ^(tmuxifier)$
}
windowrule {
name = windowrule-69
workspace = 9
match:class = ^(virt-viewer)$
}
windowrule {
name = windowrule-70
workspace = 10
match:class = ^([Oo]bsidian)$
}
# windowrule move to workspace (silent)
windowrule {
name = windowrule-71
workspace = 4 silent
match:tag = screenshare*
}
windowrule {
name = windowrule-72
workspace = 9 silent
match:class = ^(virt-manager)$
}
windowrule {
name = windowrule-73
workspace = 9 silent
match:class = ^(.virt-manager-wrapped)$
}
windowrule {
name = windowrule-74
workspace = 9 silent
match:tag = multimedia*
}
# FLOAT
windowrule {
name = windowrule-75
float = on
opacity = 0.9 0.7
size = (monitor_w*0.7) (monitor_h*0.7)
match:tag = wallpaper*
}
windowrule {
name = windowrule-76
float = on
opacity = 0.8 0.7
size = (monitor_w*0.7) (monitor_h*0.7)
match:tag = settings*
}
windowrule {
name = windowrule-77
float = on
match:tag = viewer*
}
windowrule {
name = windowrule-78
float = on
match:class = ([Zz]oom|onedriver|onedriver-launcher)$
}
windowrule {
name = windowrule-79
float = on
match:class = (org.gnome.Calculator)
match:title = (Calculator)
}
windowrule {
name = windowrule-80
float = on
match:class = ^(mpv|com.github.rafostar.Clapper)$
}
windowrule {
name = windowrule-81
float = on
match:class = ^([Qq]alculate-gtk)$
}
#windowrule = float, class:^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$
#windowrule = float, title:^(Firefox)$
windowrule {
name = windowrule-82
float = on
match:class = ^(fdm|freedownloadmanager)$
}
windowrule {
name = windowrule-83
float = on
match:class = ^(proton-authenticator)$
}
windowrule {
name = windowrule-84
float = on
match:class = ^([Ww]indscribe)$
}
# windowrule - ######### float popups and dialogue #######
windowrule {
name = windowrule-85
float = on
center = on
match:title = ^(Authentication Required)$
}
windowrule {
name = windowrule-86
float = on
match:class = (codium|codium-url-handler|VSCodium)
match:title = negative:(.*codium.*|.*VSCodium.*)
}
windowrule {
name = windowrule-87
float = on
match:class = ^(com.heroicgameslauncher.hgl)$
match:title = negative:(Heroic Games Launcher)
}
windowrule {
name = windowrule-88
float = on
match:class = ^([Ss]team)$
match:title = negative:^([Ss]team)$
}
windowrule {
name = windowrule-89
float = on
size = (monitor_w*0.7) (monitor_h*0.6)
center = on
match:title = ^(Add Folder to Workspace)$
}
windowrule {
name = windowrule-90
float = on
size = (monitor_w*0.7) (monitor_h*0.6)
center = on
match:title = ^(Save As)$
}
windowrule {
name = windowrule-91
float = on
size = (monitor_w*0.7) (monitor_h*0.6)
match:initial_title = (Open Files)
}
windowrule {
name = windowrule-92
float = on
center = on
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
# windowrule = opacity 0.9 0.7, tag:browser*
# windowrule = opacity 0.9 0.8, tag:projects*
# windowrule = opacity 0.94 0.86, tag:im*
# windowrule = opacity 0.94 0.86, tag:multimedia*
# windowrule = opacity 0.9 0.8, tag:file-manager*
windowrule {
name = windowrule-93
opacity = 0.8 0.7
match:tag = terminal*
}
# windowrule = opacity 0.82 0.75, tag:viewer*
windowrule {
name = windowrule-94
opacity = 0.8 0.7
match:class = ^(gedit|org.gnome.TextEditor|mousepad)$
}
windowrule {
name = windowrule-95
opacity = 0.9 0.8
match:class = ^(deluge)$
}
windowrule {
name = windowrule-96
opacity = 0.9 0.8
match:class = ^(seahorse)$ # gnome-keyring gui
}
# windowrule = opacity 0.95 0.75, title:^(Picture-in-Picture)$
# SIZE
#windowrule = size 25% 25%, title:^(Firefox)$
# PINNING
#windowrule = pin,title:^(Firefox)$
# windowrule - extras
# BLUR & FULLSCREEN
#windowrule = bordercolor rgb(EE4B55) rgb(880808), fullscreen:1
#windowrule = bordercolor rgb(282737) rgb(1E1D2D), floating:1
#windowrule = opacity 0.8 0.8, pinned:1
# LAYER RULES
layerrule {
name = layerrule-1
blur = on
ignore_alpha = 0
match:namespace = rofi
}
layerrule {
name = layerrule-2
blur = on
ignore_alpha = 0
match:namespace = notifications
}
layerrule {
name = layerrule-3
blur = on
ignore_alpha = 0.5
match:namespace = quickshell:overview
}
#layerrule = ignorealpha 0.5, tag:notif*
#layerrule = ignorezero, class:^([Rr]ofi)$
#layerrule = blur, class:^([Rr]ofi)$
#layerrule = unset,class:^([Rr]ofi)$
#layerrule = ignorezero, <rofi>
#layerrule = ignorezero, overview
#layerrule = blur, overview

View File

@@ -1,666 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# For window rules and layerrules
# See https://wiki.hyprland.org/Configuring/Window-Rules/ for more
# NOTES: This is only for Hyprland > 0.48
# note for ja: This should NOT be implemented on Debian and Ubuntu
# windowrule - tags - add apps under appropriate tag to use the same settings
# browser tags
windowrule {
name = windowrule-1
tag = +browser
match:class = ^([Ff]irefox|org.mozilla.firefox|[Ff]irefox-esr|[Ff]irefox-bin)$
}
windowrule {
name = rustdesk
tag = +rustdesk
match:class = ^(rustdesk)$, xwayland
}
windowrule {
name = windowrule-2
tag = +browser
match:class = ^([Gg]oogle-chrome(-beta|-dev|-unstable)?)$
}
windowrule {
name = windowrule-3
tag = +browser
match:class = ^(chrome-.+-Default)$ # Chrome PWAs
}
# windowrule = tag +browser, class:^([Cc]hromium)$
windowrule {
name = windowrule-4
tag = +browser
match:class = ^([Mm]icrosoft-edge(-stable|-beta|-dev|-unstable))$
}
windowrule {
name = windowrule-5
tag = +browser
match:class = ^(Brave-browser(-beta|-dev|-unstable)?)$
}
windowrule {
name = windowrule-6
tag = +browser
match:class = ^([Tt]horium-browser|[Cc]achy-browser)$
}
windowrule {
name = windowrule-7
tag = +browser
match:class = ^(zen-alpha|zen)$
}
# notif tags
windowrule {
name = windowrule-8
tag = +notif
match:class = ^(swaync-control-center|swaync-notification-window|swaync-client|class)$
}
# HyprFlux settings tag
windowrule {
name = windowrule-9
tag = +HyprFlux_Cheat
match:title = ^(HyprFlux Quick Cheat Sheet)$
}
windowrule {
name = windowrule-10
tag = +HyprFlux_Settings
match:title = ^(HyprFlux Settings)$
}
windowrule {
name = windowrule-11
tag = +HyprFlux-Settings
match:class = ^(nwg-displays|nwg-look)$
}
# terminal tags
windowrule {
name = windowrule-12
tag = +terminal
match:class = ^(Alacritty|kitty|kitty-dropterm)$
}
# email tags
windowrule {
name = windowrule-13
tag = +email
match:class = ^([Tt]hunderbird|org.gnome.Evolution)$
}
windowrule {
name = windowrule-14
tag = +email
match:class = ^(eu.betterbird.Betterbird)$
}
# project tags
windowrule {
name = windowrule-15
tag = +projects
match:class = ^(codium|codium-url-handler|VSCodium)$
}
windowrule {
name = windowrule-16
tag = +projects
match:class = ^(VSCode|code-url-handler)$
}
windowrule {
name = windowrule-17
tag = +projects
match:class = ^(jetbrains-.+)$ # JetBrains IDEs
}
# screenshare tags
windowrule {
name = windowrule-18
tag = +screenshare
match:class = ^(com.obsproject.Studio)$
}
# IM tags
# windowrule = tag +im, class:^([Dd]iscord|[Ww]ebCord|[Vv]esktop)$
windowrule {
name = windowrule-19
tag = +im
center = on
float = on
size = (monitor_w*0.6) (monitor_h*0.7)
match:class = ^([Ff]erdium)$
}
windowrule {
name = windowrule-20
tag = +im
match:class = ^([Ww]hatsapp-for-linux)$
}
windowrule {
name = windowrule-21
tag = +im
match:class = ^(ZapZap|com.rtosta.zapzap)$
}
windowrule {
name = windowrule-22
tag = +im
match:class = ^(org.telegram.desktop|io.github.tdesktop_x64.TDesktop)$
}
windowrule {
name = windowrule-23
tag = +im
match:class = ^(teams-for-linux)$
}
windowrule {
name = windowrule-24
tag = +im
match:class = ^(im.riot.Riot|Element)$ # Element Matrix client
}
# game tags
windowrule {
name = windowrule-25
tag = +games
match:class = ^(gamescope)$
}
windowrule {
name = windowrule-26
tag = +games
match:class = ^(steam_app_\d+)$
}
# gamestore tags
windowrule {
name = windowrule-27
tag = +gamestore
match:class = ^([Ss]team)$
}
windowrule {
name = windowrule-28
tag = +gamestore
match:title = ^([Ll]utris)$
}
windowrule {
name = windowrule-29
tag = +gamestore
match:class = ^(com.heroicgameslauncher.hgl)$
}
# file-manager tags
windowrule {
name = windowrule-30
tag = +file-manager
match:class = ^([Tt]hunar|org.gnome.Nautilus|[Pp]cmanfm-qt)$
}
windowrule {
name = windowrule-31
tag = +file-manager
match:class = ^(app.drey.Warp)$
}
# wallpaper tags
windowrule {
name = windowrule-32
tag = +wallpaper
match:class = ^([Ww]aytrogen)$
}
# multimedia tags
windowrule {
name = windowrule-33
tag = +multimedia
match:class = ^([Aa]udacious)$
}
# multimedia-video tags
windowrule {
name = windowrule-34
tag = +multimedia_video
match:class = ^([Mm]pv|vlc)$
}
# settings tags
windowrule {
name = windowrule-35
tag = +settings
center = on
match:title = ^(ROG Control)$
}
windowrule {
name = windowrule-36
tag = +settings
match:class = ^(wihotspot(-gui)?)$ # wifi hotspot
}
windowrule {
name = windowrule-37
tag = +settings
match:class = ^([Bb]aobab|org.gnome.[Bb]aobab)$ # Disk usage analyzer
}
windowrule {
name = windowrule-38
tag = +settings
match:class = ^(gnome-disks|wihotspot(-gui)?)$
}
windowrule {
name = windowrule-39
tag = +settings
match:title = (Kvantum Manager)
}
windowrule {
name = windowrule-40
tag = +settings
match:class = ^(file-roller|org.gnome.FileRoller)$ # archive manager
}
windowrule {
name = windowrule-41
tag = +settings
match:class = ^(nm-applet|nm-connection-editor|blueman-manager)$
}
windowrule {
name = windowrule-42
tag = +settings
center = on
match:class = ^(pavucontrol|org.pulseaudio.pavucontrol|com.saivert.pwvucontrol)$
}
windowrule {
name = windowrule-43
tag = +settings
match:class = ^(qt5ct|qt6ct|[Yy]ad)$
}
windowrule {
name = windowrule-44
tag = +settings
match:class = (xdg-desktop-portal-gtk)
}
windowrule {
name = windowrule-45
tag = +settings
match:class = ^(org.kde.polkit-kde-authentication-agent-1)$
}
windowrule {
name = windowrule-46
tag = +settings
match:class = ^([Rr]ofi)$
}
# Custom rule
windowrule {
name = windowrule-47
tile = on
match:class = ^([Cc]hromium)$
}
windowrule {
name = windowrule-48
size = (monitor_w*0.25) (monitor_h*0.55)
float = on
match:class = ^(com.network.manager)$
}
windowrule {
name = windowrule-49
workspace = special:nyx
match:class = ^([Vv]esktop|[Dd]iscord|[Ss]potify)$
}
windowrule {
name = windowrule-255
size = (monitor_w*0.75) (monitor_h*0.80)
float = on
match:class = ^([Bb]itwarden)$
}
# viewer tags
windowrule {
name = windowrule-50
tag = +viewer
match:class = ^(gnome-system-monitor|org.gnome.SystemMonitor|io.missioncenter.MissionCenter)$ # system monitor
}
windowrule {
name = windowrule-51
tag = +viewer
match:class = ^(evince)$ # document viewer
}
windowrule {
name = windowrule-52
tag = +viewer
match:class = ^(eog|org.gnome.Loupe)$ # image viewer
}
# Some special override rules
windowrule {
name = windowrule-53
no_blur = on
opacity = 1.0
match:tag = multimedia_video*
}
# POSITION
# windowrule = center,floating:1 # warning, it cause even the menu to float and center.
windowrule {
name = windowrule-54
center = on
float = on
size = (monitor_w*0.65) (monitor_h*0.9)
match:tag = HyprFlux_Cheat*
}
windowrule {
name = windowrule-55
center = on
float = on
match:class = ([Tt]hunar)
match:title = negative:(.*[Tt]hunar.*)
}
windowrule {
name = windowrule-56
center = on
float = on
match:tag = HyprFlux-Settings*
}
windowrule {
name = windowrule-57
center = on
match:title = ^(Keybindings)$
}
windowrule {
name = windowrule-58
center = on
size = (monitor_w*0.6) (monitor_h*0.7)
match:class = ^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$
}
windowrule {
name = windowrule-59
move = ((monitor_w*0.61)) ((monitor_h*0.07))
float = on
size = (monitor_w*0.38) (monitor_h*0.38)
pin = on
keep_aspect_ratio = on
match:title = ^(Picture-in-Picture)$
}
#windowrule = move 72% 7%,title:^(Firefox)$
# windowrule to avoid idle for fullscreen apps
#windowrule = idleinhibit fullscreen, class:^(*)$
#windowrule = idleinhibit fullscreen, title:^(*)$
windowrule {
name = windowrule-60
idle_inhibit = fullscreen
match:fullscreen = 1
}
# Discord to workspace 3
windowrule = match:class ^([Vv]esktop|[Dd]iscord|[Ss]potify|[Pp]lexamp)$, workspace 3
windowrule = match:class ^([Pp]lexamp), size 20% 20%
# FLOAT
windowrule {
name = windowrule-75
float = on
opacity = 0.9 0.7
size = (monitor_w*0.7) (monitor_h*0.7)
match:tag = wallpaper*
}
windowrule {
name = windowrule-76
float = on
opacity = 0.8 0.7
size = (monitor_w*0.7) (monitor_h*0.7)
match:tag = settings*
}
windowrule {
name = windowrule-77
float = on
match:tag = viewer*
}
windowrule {
name = windowrule-78
float = on
match:class = ([Zz]oom|onedriver|onedriver-launcher)$
}
windowrule {
name = windowrule-79
float = on
match:class = (org.gnome.Calculator)
match:title = (Calculator)
}
windowrule {
name = windowrule-80
float = on
match:class = ^(mpv|com.github.rafostar.Clapper)$
}
windowrule {
name = windowrule-81
float = on
match:class = ^([Qq]alculate-gtk)$
}
#windowrule = float, class:^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$
#windowrule = float, title:^(Firefox)$
windowrule {
name = windowrule-82
float = on
match:class = ^(fdm|freedownloadmanager)$
}
windowrule {
name = windowrule-83
float = on
match:class = ^(proton-authenticator)$
}
windowrule {
name = windowrule-84
float = on
match:class = ^([Ww]indscribe)$
}
# windowrule - ######### float popups and dialogue #######
windowrule {
name = windowrule-85
float = on
center = on
match:title = ^(Authentication Required)$
}
windowrule {
name = windowrule-86
float = on
match:class = (codium|codium-url-handler|VSCodium)
match:title = negative:(.*codium.*|.*VSCodium.*)
}
windowrule {
name = windowrule-87
float = on
match:class = ^(com.heroicgameslauncher.hgl)$
match:title = negative:(Heroic Games Launcher)
}
windowrule {
name = windowrule-88
float = on
match:class = ^([Ss]team)$
match:title = negative:^([Ss]team)$
}
windowrule {
name = windowrule-89
float = on
size = (monitor_w*0.7) (monitor_h*0.6)
center = on
match:title = ^(Add Folder to Workspace)$
}
windowrule {
name = windowrule-90
float = on
size = (monitor_w*0.7) (monitor_h*0.6)
center = on
match:title = ^(Save As)$
}
windowrule {
name = windowrule-91
float = on
size = (monitor_w*0.7) (monitor_h*0.6)
match:initial_title = (Open Files)
}
windowrule {
name = windowrule-92
float = on
center = on
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
# windowrule = opacity 0.9 0.7, tag:browser*
# windowrule = opacity 0.9 0.8, tag:projects*
# windowrule = opacity 0.94 0.86, tag:im*
# windowrule = opacity 0.94 0.86, tag:multimedia*
# windowrule = opacity 0.9 0.8, tag:file-manager*
windowrule {
name = windowrule-93
opacity = 0.8 0.7
match:tag = terminal*
}
# windowrule = opacity 0.82 0.75, tag:viewer*
windowrule {
name = windowrule-94
opacity = 0.8 0.7
match:class = ^(gedit|org.gnome.TextEditor|mousepad)$
}
windowrule {
name = windowrule-95
opacity = 0.9 0.8
match:class = ^(deluge)$
}
windowrule {
name = windowrule-96
opacity = 0.9 0.8
match:class = ^(seahorse)$ # gnome-keyring gui
}
# windowrule = opacity 0.95 0.75, title:^(Picture-in-Picture)$
# SIZE
#windowrule = size 25% 25%, title:^(Firefox)$
# PINNING
#windowrule = pin,title:^(Firefox)$
# windowrule - extras
# BLUR & FULLSCREEN
#windowrule = bordercolor rgb(EE4B55) rgb(880808), fullscreen:1
#windowrule = bordercolor rgb(282737) rgb(1E1D2D), floating:1
#windowrule = opacity 0.8 0.8, pinned:1
# LAYER RULES
layerrule {
name = layerrule-1
blur = on
ignore_alpha = 0
match:namespace = rofi
}
layerrule {
name = layerrule-2
blur = on
ignore_alpha = 0
match:namespace = notifications
}
layerrule {
name = layerrule-3
blur = on
ignore_alpha = 0.5
match:namespace = quickshell:overview
}
#layerrule = ignorealpha 0.5, tag:notif*
#layerrule = ignorezero, class:^([Rr]ofi)$
#layerrule = blur, class:^([Rr]ofi)$
#layerrule = unset,class:^([Rr]ofi)$
#layerrule = ignorezero, <rofi>
#layerrule = ignorezero, overview
#layerrule = blur, overview

View File

@@ -1,12 +0,0 @@
# 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 tui to edit, leave this at the end of your file (the last monitor config applies)
# and leave the markers.
# <<<<< TUI AUTO START
monitor=desc:LG Electronics LG ULTRAWIDE 0x0002E68A,2560x1080@60.00000,1920x-740,1.00000000,transform,3,vrr,0
monitor=desc:HP Inc. HP 524pf VNG447017D,1920x1080@100.00000,-1920x0,1.00000000,transform,0,vrr,0
monitor=desc:Acer Technologies VG240Y S 0x126191DE,1920x1080@165.00301,0x0,1.00000000,transform,0,vrr,1,bitdepth,10,cm,wide
# <<<<< TUI AUTO END

View File

@@ -1,30 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# NOTE: This will be overwritten by NWG-Displays
# once you use and click apply.
#
# *********************************************************** #
# 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.hyprland.org/Configuring/Workspace-Rules/
# Assigning workspace to a certain monitor. Below are just examples
workspace = 1, monitor:DP-2, default=true
workspace = 2, monitor:DP-1, default=true
workspace = 3, monitor:HDMI-A-1, default=true
# workspace = 4, monitor:eDP-1
# workspace = 5, monitor:DP-2
# workspace = 6, monitor:DP-2
# workspace = 7, monitor:DP-2
# workspace = 8, monitor:DP-2
# example rules (from wiki)
# workspace = 3, rounding:false, decorate:false
# workspace = name:coding, rounding:false, decorate:false, gapsin:0, gapsout:0, border:false, decorate:false, monitor:DP-1
# workspace = 8,bordersize:8
# workspace = name:Hello, monitor:DP-1, default:true
# workspace = name:gaming, monitor:desc:Chimei Innolux Corporation 0x150C, default:true
# workspace = 5, on-created-empty:[float] firefox
# workspace = special:scratchpad, on-created-empty:foot

View File

@@ -1,3 +1,3 @@
# Place your new scripts here.
# If you need to edit a script from main script (~/.config/hypr/scripts), copy it on this directory, and edit.
# Make sure to update as well the keybinds in ~/.config/hypr/UserConfigs directory if any script is linked to it
# Make sure to update as well the keybinds in ~/.config/hypr/input/keybinds.lua if any script is linked to it

View File

@@ -1,25 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
animations {
enabled = yes
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
bezier = overshot, 0.05, 0.9, 0.1, 1.05
bezier = smoothOut, 0.5, 0, 0.99, 0.99
bezier = smoothIn, 0.5, -0.5, 0.68, 1.5
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 5, winIn, slide
animation = windowsOut, 1, 3, smoothOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 180, liner, loop #used by rainbow borders and rotating colors
animation = fade, 1, 3, smoothOut
animation = workspaces, 1, 5, overshot
# animations for -git or version >0.42.0
animation = workspacesIn, 1, 5, winIn, slide
animation = workspacesOut, 1, 5, winOut, slide
}

View File

@@ -1,26 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# old animations
animations {
enabled = yes
bezier = myBezier, 0.05, 0.9, 0.1, 1.05
bezier = linear, 0.0, 0.0, 1.0, 1.0
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = slow, 0, 0.85, 0.3, 1
bezier = overshot, 0.7, 0.6, 0.1, 1.1
bezier = bounce, 1.1, 1.6, 0.1, 0.85
bezier = sligshot, 1, -1, 0.15, 1.25
bezier = nice, 0, 6.9, 0.5, -4.20
animation = windowsIn, 1, 5, slow, popin
animation = windowsOut, 1, 5, winOut, popin
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 10, linear
animation = borderangle, 1, 180, linear, loop #used by rainbow borders and rotating colors
animation = fade, 1, 5, overshot
animation = workspaces, 1, 5, wind
animation = windows, 1, 5, bounce, popin
}

View File

@@ -1,4 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
animations {
enabled = no
}

View File

@@ -1,39 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "End-4"
# credit https://github.com/end-4/dots-hyprland
animations {
enabled = true
# Animation curves
bezier = linear, 0, 0, 1, 1
bezier = md3_standard, 0.2, 0, 0, 1
bezier = md3_decel, 0.05, 0.7, 0.1, 1
bezier = md3_accel, 0.3, 0, 0.8, 0.15
bezier = overshot, 0.05, 0.9, 0.1, 1.1
bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
bezier = menu_decel, 0.1, 1, 0, 1
bezier = menu_accel, 0.38, 0.04, 1, 0.07
bezier = easeInOutCirc, 0.85, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutExpo, 0.16, 1, 0.3, 1
bezier = softAcDecel, 0.26, 0.26, 0.15, 1
bezier = md2, 0.4, 0, 0.2, 1 # use with .2s duration
# Animation configs
animation = windows, 1, 3, md3_decel, popin 60%
animation = windowsIn, 1, 3, md3_decel, popin 60%
animation = windowsOut, 1, 3, md3_accel, popin 60%
animation = border, 1, 10, default
animation = fade, 1, 3, md3_decel
# animation = layers, 1, 2, md3_decel, slide
animation = layersIn, 1, 3, menu_decel, slide
animation = layersOut, 1, 1.6, menu_accel
animation = fadeLayersIn, 1, 2, menu_decel
animation = fadeLayersOut, 1, 4.5, menu_accel
animation = workspaces, 1, 7, menu_decel, slide
# animation = workspaces, 1, 2.5, softAcDecel, slide
# animation = workspaces, 1, 7, menu_decel, slidefade 15%
# animation = specialWorkspace, 1, 3, md3_decel, slidefadevert 15%
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}

View File

@@ -1,33 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Vertical"
# credit https://github.com/prasanthrangan/hyprdots
animations {
enabled = yes
bezier = fluent_decel, 0, 0.2, 0.4, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutCubic, 0.33, 1, 0.68, 1
bezier = easeinoutsine, 0.37, 0, 0.63, 1
# Windows
animation = windowsIn, 1, 1.5, easeinoutsine, popin 60% # window open
animation = windowsOut, 1, 1.5, easeOutCubic, popin 60% # window close.
animation = windowsMove, 1, 1.5, easeinoutsine, slide # everything in between, moving, dragging, resizing.
# Fading
animation = fade, 1, 2.5, fluent_decel
animation = fadeLayersIn, 0
animation = border, 0
# Layers
animation = layers, 1, 1.5, easeinoutsine, popin
# Workspaces
#animation = workspaces, 1, 3, fluent_decel, slidefade 30% # styles: slide, slidevert, fade, slidefade, slidefadevert
animation = workspaces, 1, 3, fluent_decel, slidefadevert 30% # styles: slide, slidevert, fade, slidefade, slidefadevert
animation = specialWorkspace, 1, 2, fluent_decel, slidefade 10%
}

View File

@@ -1,20 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Default"
# credit https://github.com/prasanthrangan/hyprdots
animations {
enabled = yes
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, once
animation = fade, 1, 10, default
animation = workspaces, 1, 5, wind
animation = specialWorkspace, 1, 5, wind, slidevert
}

View File

@@ -1,26 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Minimal-1"
# credit https://github.com/prasanthrangan/hyprdots-
animations {
enabled = true
# █▄▄ █▀▀ ▀█ █ █▀▀ █▀█   █▀▀ █░█ █▀█ █░█ █▀▀
# █▄█ ██▄ █▄ █ ██▄ █▀▄   █▄▄ █▄█ █▀▄ ▀▄▀ ██▄
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
#▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
#█▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, once
animation = fade, 1, 10, default
animation = workspaces, 1, 5, wind
}

View File

@@ -1,15 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Minimal-2"
# credit https://github.com/prasanthrangan/hyprdots
animations {
enabled = yes
bezier = quart, 0.25, 1, 0.5, 1
animation = windows, 1, 6, quart, slide
animation = border, 1, 6, quart
animation = borderangle, 1, 6, quart
animation = fade, 1, 6, quart
animation = workspaces, 1, 6, quart
}

View File

@@ -1,40 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Optimized"
# credit https://github.com/prasanthrangan/hyprdots
animations {
enabled = true
bezier = wind, 0.05, 0.85, 0.03, 0.97
bezier = winIn, 0.07, 0.88, 0.04, 0.99
bezier = winOut, 0.20, -0.15, 0, 1
bezier = liner, 1, 1, 1, 1
bezier = md3_standard, 0.12, 0, 0, 1
bezier = md3_decel, 0.05, 0.80, 0.10, 0.97
bezier = md3_accel, 0.20, 0, 0.80, 0.08
bezier = overshot, 0.05, 0.85, 0.07, 1.04
bezier = crazyshot, 0.1, 1.22, 0.68, 0.98
bezier = hyprnostretch, 0.05, 0.82, 0.03, 0.94
bezier = menu_decel, 0.05, 0.82, 0, 1
bezier = menu_accel, 0.20, 0, 0.82, 0.10
bezier = easeInOutCirc, 0.75, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.48, 0.38, 1
bezier = easeOutExpo, 0.10, 0.94, 0.23, 0.98
bezier = softAcDecel, 0.20, 0.20, 0.15, 1
bezier = md2, 0.30, 0, 0.15, 1
bezier = OutBack, 0.28, 1.40, 0.58, 1
bezier = easeInOutCirc, 0.78, 0, 0.15, 1
animation = border, 1, 1.6, liner
animation = borderangle, 1, 82, liner, once
animation = windowsIn, 1, 3.2, winIn, slide
animation = windowsOut, 1, 2.8, easeOutCirc
animation = windowsMove, 1, 3.0, wind, slide
animation = fade, 1, 1.8, md3_decel
animation = layersIn, 1, 1.8, menu_decel, slide
animation = layersOut, 1, 1.5, menu_accel
animation = fadeLayersIn, 1, 1.6, menu_decel
animation = fadeLayersOut, 1, 1.8, menu_accel
animation = workspaces, 1, 4.0, menu_decel, slide
animation = specialWorkspace, 1, 2.3, md3_decel, slidefadevert 15%
}

View File

@@ -1,14 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Classic"
# credit https://github.com/mylinuxforwork/dotfiles
animations {
enabled = true
bezier = myBezier, 0.05, 0.9, 0.1, 1.05
animation = windows, 1, 7, myBezier
animation = windowsOut, 1, 7, default, popin 80%
animation = border, 1, 10, default
animation = borderangle, 1, 8, default
animation = fade, 1, 7, default
animation = workspaces, 1, 6, default
}

View File

@@ -1,19 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Dynamic"
# credit https://github.com/mylinuxforwork/dotfiles
animations {
enabled = true
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, loop
animation = fade, 1, 10, default
animation = workspaces, 1, 5, wind
}

View File

@@ -1,23 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Fast"
# credit https://github.com/mylinuxforwork/dotfiles
animations {
enabled = true
bezier = linear, 0, 0, 1, 1
bezier = md3_standard, 0.2, 0, 0, 1
bezier = md3_decel, 0.05, 0.7, 0.1, 1
bezier = md3_accel, 0.3, 0, 0.8, 0.15
bezier = overshot, 0.05, 0.9, 0.1, 1.1
bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
bezier = fluent_decel, 0.1, 1, 0, 1
bezier = easeInOutCirc, 0.85, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutExpo, 0.16, 1, 0.3, 1
animation = windows, 1, 3, md3_decel, popin 60%
animation = border, 1, 10, default
animation = fade, 1, 2.5, md3_decel
animation = workspaces, 1, 3.5, easeOutExpo, slide
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}

View File

@@ -1,19 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "High"
# credit https://github.com/mylinuxforwork/dotfiles
animations {
enabled = true
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, once
animation = fade, 1, 10, default
animation = workspaces, 1, 5, wind
}

View File

@@ -1,19 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Moving"
# credit https://github.com/mylinuxforwork/dotfiles
animations {
enabled = true
bezier = overshot, 0.05, 0.9, 0.1, 1.05
bezier = smoothOut, 0.5, 0, 0.99, 0.99
bezier = smoothIn, 0.5, -0.5, 0.68, 1.5
animation = windows, 1, 5, overshot, slide
animation = windowsOut, 1, 3, smoothOut
animation = windowsIn, 1, 3, smoothOut
animation = windowsMove, 1, 4, smoothIn, slide
animation = border, 1, 5, default
animation = fade, 1, 5, smoothIn
animation = fadeDim, 1, 5, smoothIn
animation = workspaces, 1, 6, default
}

View File

@@ -1,14 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Standard"
# credit https://github.com/mylinuxforwork/dotfiles
animations {
enabled = true
bezier = myBezier, 0.05, 0.9, 0.1, 1.05
animation = windows, 1, 7, myBezier
animation = windowsOut, 1, 7, default, popin 80%
animation = border, 1, 10, default
animation = borderangle, 1, 8, default
animation = fade, 1, 7, default
animation = workspaces, 1, 6, default
}

View File

@@ -1,54 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Me-1"
# credit https://github.com/mahaveergurjar
animations {
enabled = true
# Animation curves
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
# bezier = linear, 0, 0, 1, 1
bezier = md3_standard, 0.2, 0, 0, 1
bezier = md3_decel, 0.05, 0.7, 0.1, 1
bezier = md3_accel, 0.3, 0, 0.8, 0.15
bezier = overshot, 0.05, 0.9, 0.1, 1.1
bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
bezier = menu_decel, 0.1, 1, 0, 1
bezier = menu_accel, 0.38, 0.04, 1, 0.07
bezier = easeInOutCirc, 0.85, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutExpo, 0.16, 1, 0.3, 1
bezier = softAcDecel, 0.26, 0.26, 0.15, 1
bezier = md2, 0.4, 0, 0.2, 1 # use with .2s duration
# Animation configs
# animation = windows, 1, 3, md3_decel, popin 60%
# animation = windowsIn, 1, 3, md3_decel, popin 60%
# animation = windowsOut, 1, 3, md3_accel, popin 60%
# animation = windows, 1, 6, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, once
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
# animation = border, 1, 10, default
animation = fade, 1, 3, md3_decel
# animation = layers, 1, 2, md3_decel, slide
animation = layersIn, 1, 3, menu_decel, slide
animation = layersOut, 1, 1.6, menu_accel
animation = fadeLayersIn, 1, 2, menu_decel
animation = fadeLayersOut, 1, 4.5, menu_accel
animation = workspaces, 1, 7, menu_decel, slide
animation = workspaces, 1, 5, wind
# animation = workspaces, 1, 2.5, softAcDecel, slide
# animation = workspaces, 1, 7, menu_decel, slidefade 15%
animation = specialWorkspace, 1, 3, md3_decel, slidefadevert 15%
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}

View File

@@ -1,29 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# name "Me-2"
# credit https://github.com/mahaveergurjar
animations {
enabled = yes
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
bezier = overshot, 0.05, 0.9, 0.1, 1.05
bezier = smoothOut, 0.5, 0, 0.99, 0.99
bezier = smoothIn, 0.5, -0.5, 0.68, 1.5
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 5, winIn, slide
animation = windowsOut, 1, 3, smoothOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 180, liner, loop #used by rainbow borders and rotating colors
animation = fade, 1, 3, smoothOut
animation = workspaces, 1, 5, overshot
# animations for -git or version >0.42.0
animation = workspacesIn, 1, 5, winIn, slide
animation = workspacesOut, 1, 5, winOut, slide
}

View File

@@ -1,34 +1,6 @@
-- 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").
-- Active animation preset.
-- The presets live in ../presets/anim-*.lua. Super+Shift+A (scripts/Animations.sh)
-- picks one and rewrites the require() line below; edit it by hand to switch too,
-- e.g. require("presets.anim-ml4w-classic"). Keep it to this single line.
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" })
require("presets.anim-hyprflux")

View File

@@ -24,7 +24,7 @@ hl.config({
rounding = 10,
active_opacity = 1.0,
inactive_opacity = 0.9,
inactive_opacity = 1.0,
fullscreen_opacity = 1.0,
dim_inactive = true,

View File

@@ -1,21 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# HyprFlux static color palette
$background = rgb(010102)
$foreground = rgb(FDF8FE)
$color0 = rgb(313131)
$color1 = rgb(09050C)
$color2 = rgb(221647)
$color3 = rgb(2C1A40)
$color4 = rgb(5E3887)
$color5 = rgb(7344A6)
$color6 = rgb(BAB0BD)
$color7 = rgb(F3ECF5)
$color8 = rgb(AAA5AC)
$color9 = rgb(0B0711)
$color10 = rgb(2D1D5F)
$color11 = rgb(3B2355)
$color12 = rgb(7D4AB4)
$color13 = rgb(9A5BDD)
$color14 = rgb(F8EAFC)
$color15 = rgb(F3ECF5)

View File

@@ -19,11 +19,7 @@ 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")
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct") -- qt6ct also styles Qt5 apps that read it; qt5ct.conf is kept in sync
-- xwayland apps scale fix (useful if you use monitor scaling).
-- Set same value if you use scaling in monitors.lua
@@ -32,11 +28,17 @@ hl.env("QT_QUICK_CONTROLS_STYLE", "org.hyprland.style")
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.
-- Cursor: Bibata-Modern-Classic (AUR bibata-cursor-theme-bin, XCursor format).
-- Same theme in gtk-*/settings.ini, the gsettings call in hyprland.lua and initial-boot.sh.
-- There's no hyprcursor build installed, so Hyprland falls back to XCURSOR_THEME.
-- https://wiki.hyprland.org/Hypr-Ecosystem/hyprcursor/
hl.env("HYPRCURSOR_THEME", "Bibata-Modern-Classic")
hl.env("HYPRCURSOR_SIZE", "24")
hl.env("XCURSOR_THEME", "Bibata-Modern-Classic")
hl.env("XCURSOR_SIZE", "24")
-- Default terminal for anything that asks $TERMINAL (rofi-sensible-terminal, scripts)
hl.env("TERMINAL", "ghostty")
-- firefox
hl.env("MOZ_ENABLE_WAYLAND", "1")
@@ -46,13 +48,17 @@ hl.env("MOZ_ENABLE_WAYLAND", "1")
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")
-- Only when the NVIDIA kernel driver is loaded, so the same config works on the
-- laptop (forcing LIBVA_DRIVER_NAME=nvidia there breaks VA-API video decoding).
local nvidia = io.open("/proc/driver/nvidia/version")
if nvidia then
nvidia:close()
hl.env("LIBVA_DRIVER_NAME", "nvidia")
hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia")
hl.env("NVD_BACKEND", "direct")
hl.env("GSK_RENDERER", "ngl")
end
-- additional ENV's for nvidia. Caution, activate with care
-- hl.env("GBM_BACKEND", "nvidia-drm")
@@ -85,3 +91,6 @@ hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") -- auto selects Wayland if possib
-- hl.env("HYPRLAND_NO_RT", "1") -- Disables realtime priority setting by Hyprland.
-- hl.env("HYPRLAND_NO_SD_NOTIFY", "1") -- If systemd, disables the 'sd_notify' calls.
-- hl.env("HYPRLAND_NO_SD_VARS", "1") -- Disables management of variables in systemd and dbus activation environments.
-- Bitwarden SSH agent (so GUI apps like VS Code see it, not just shells)
hl.env("SSH_AUTH_SOCK", os.getenv("HOME") .. "/.bitwarden-ssh-agent.sock")

View File

@@ -5,6 +5,11 @@
-- whatever it last wrote. If you reinstall HyprMod, prefer letting its own "Migrate to Lua…"
-- wizard take over managing this file instead of hand-editing it going forward.
-- vrr forced off globally: the per-monitor vrr = 0 flag below does NOT override this when
-- misc.vrr is 1 (tested live on Hyprland 0.56.2 — DP-2 stayed vrr:true until this global
-- was changed). DP-2 (Acer VG240Y S) flickers under VRR when idle in fullscreen (mouse
-- stationary -> no repaint damage -> frame delivery stalls near the panel's VRR floor).
-- The other two monitors don't negotiate VRR at all, so this has no effect on them.
hl.config({
general = {
allow_tearing = true,
@@ -13,17 +18,19 @@ hl.config({
},
},
misc = {
vrr = 1,
vrr = 0,
},
})
-- Monitors
hl.monitor({ output = "DP-2", mode = "1920x1080@165.00Hz", position = "0x0", scale = 1, bitdepth = 10, cm = "wide" })
hl.monitor({ output = "DP-2", mode = "1920x1080@165.00Hz", position = "0x0", scale = 1, bitdepth = 10, cm = "wide", vrr = 0 })
-- Keybinds
-- (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 + Print", hl.dsp.exec_cmd([[sh -c 'if command -v hyprshot >/dev/null; then hyprshot -o "$HOME/Pictures/Screenshots-Linux" -m region; else grim -g "$(slurp)" "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"; fi']])) -- drag an area (hyprshot if installed, else grim/slurp)
hl.bind("SUPER + SHIFT + S", hl.dsp.exec_cmd([[sh -c 'grim -g "$(slurp)" "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"']])) -- region; Surface keyboard has no Print key
hl.bind("SUPER + F6", hl.dsp.exec_cmd([[sh -c 'grim "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"']])) -- full screen
hl.bind("SUPER + up", hl.dsp.focus({ direction = "u" }))
hl.bind("SUPER + SHIFT + Print", hl.dsp.exec_cmd("hyprshot -o " .. os.getenv("HOME") .. "/Pictures/Screenshots-Linux -m window"))
hl.bind("SUPER + SHIFT + Print", hl.dsp.exec_cmd([[sh -c 'if command -v hyprshot >/dev/null; then hyprshot -o "$HOME/Pictures/Screenshots-Linux" -m window; else grim "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"; fi']])) -- window (hyprshot) or full screen (fallback)

View File

@@ -13,6 +13,11 @@ hl.on("hyprland.start", function()
-- Startup
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
-- restart portals once the env is imported, otherwise screen sharing breaks
hl.exec_cmd("sleep 1 && systemctl --user restart xdg-desktop-portal-hyprland && systemctl --user restart xdg-desktop-portal")
-- Sunshine (virtual monitor only on desktop; skipped on laptops, detected by a system battery)
hl.exec_cmd("sleep 1 && ! ls -d /sys/class/power_supply/BAT* >/dev/null 2>&1 && hyprctl output create headless")
-- Polkit (Polkit Gnome / KDE)
hl.exec_cmd(scriptsDir .. "/Polkit.sh")
@@ -23,7 +28,7 @@ hl.on("hyprland.start", function()
-- hl.exec_cmd("ags")
-- hl.exec_cmd("blueman-applet")
-- hl.exec_cmd("rog-control-center")
hl.exec_cmd("waybar")
-- hl.exec_cmd("waybar")
hl.exec_cmd("qs") -- quickshell AGS Desktop Overview alternative
-- clipboard manager
@@ -31,15 +36,22 @@ hl.on("hyprland.start", function()
hl.exec_cmd("wl-paste --type image --watch cliphist store")
-- Starting hypridle to start hyprlock
-- Sunshine streams hold an idle inhibitor (scripts/SunshineIdle.sh) so this never locks mid-stream
hl.exec_cmd("hypridle")
-- Sunshine for streaming media
hl.exec_cmd("sunshine")
-- Float + center the Bitwarden extension pop-out (window rules can't catch it — see the script)
hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh")
hl.dispatch(hl.dsp.focus({ workspace = 3 }))
-- NOTE: the trailing "&&" here is a stray leftover from the original .conf (dangling, does nothing) — ported as-is
hl.exec_cmd("/usr/bin/Plexamp.AppImage &&")
hl.exec_cmd("plexamp") -- plexamp-bin (AUR)
hl.exec_cmd("sleep 3 && vesktop")
hl.dispatch(hl.dsp.focus({ workspace = 1 }))
hl.exec_cmd("bitwarden-desktop --ozone-platform-hint=auto --silent")
hl.exec_cmd("seafile-applet")
-- Here are a list of features available but disabled by default
-- hl.exec_cmd("awww-daemon --format xrgb && awww img " .. os.getenv("HOME") .. "/Pictures/wallpapers/mecha-nostalgia.png") -- persistent wallpaper

View File

@@ -8,7 +8,7 @@ general {
lock_cmd = pidof hyprlock || hyprlock # runs hyprlock if it is not already running (this is always run when "loginctl lock-session" is called)
# unlock_cmd = killall hyprlock # kills hyprlock when unlocking (this is always run when "loginctl unlock-session" is called)
before_sleep_cmd = loginctl lock-session # ensures that the session is locked before going to sleep
after_sleep_cmd = hyprctl dispatch dpms on # turn of screen after sleep (not strictly necessary, but just in case)
after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({action = "on"})' # turn of screen after sleep (not strictly necessary, but just in case)
ignore_dbus_inhibit = false # whether to ignore dbus-sent idle-inhibit requests (used by e.g. firefox or steam)
}
@@ -16,8 +16,8 @@ general {
# (disabled by default)
# listener {
# timeout = 30 # 30 seconds
# on-timeout = pidof hyprlock && hyprctl dispatch dpms off # turns off the screen if hyprlock is active
# on-resume = pidof hyprlock && hyprctl dispatch dpms on # command to run when activity is detected after timeout has fired.
# on-timeout = pidof hyprlock && hyprctl dispatch 'hl.dsp.dpms({action = "off"})' # turns off the screen if hyprlock is active
# on-resume = pidof hyprlock && hyprctl dispatch 'hl.dsp.dpms({action = "on"})' # command to run when activity is detected after timeout has fired.
# }
# Warn
@@ -38,8 +38,8 @@ listener {
# (disabled by default)
# listener {
# timeout = 630 # 10.5 min
# on-timeout = hyprctl dispatch dpms off # command to run when timeout has passed
# on-resume = hyprctl dispatch dpms on # command to run when activity is detected after timeout has fired.
# on-timeout = hyprctl dispatch 'hl.dsp.dpms({action = "off"})' # command to run when timeout has passed
# on-resume = hyprctl dispatch 'hl.dsp.dpms({action = "on"})' # command to run when activity is detected after timeout has fired.
# }
# Suspend # disabled by default

View File

@@ -1,15 +0,0 @@
# Generated by HyprMod
general:allow_tearing = 1
general:snap:enabled = 0
misc:vrr = 1
# Monitors
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-Linux -m region
bind = SUPER, up, movefocus, u
bind = SUPER SHIFT, Print, exec, hyprshot -o $HOME/Pictures/Screenshots-Linux -m window

View File

@@ -1,34 +0,0 @@
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# always refer to Hyprland wiki
# https://wiki.hyprland.org/
# Initial boot script enable to apply initial wallpapers, theming, new settings etc.
# suggest not to change this or delete this including deleting referrence file in ~/.config/hypr/.initial_startup_done
# as long as the referrence file is present, this initial-boot.sh will not execute
exec-once = $HOME/.config/hypr/initial-boot.sh
# ## This is where you want to start tinkering
$UserConfigs = $HOME/.config/hypr/UserConfigs # User Configs directory path
source= $UserConfigs/Startup_Apps.conf # put your start-up packages on this file
source= $UserConfigs/ENVariables.conf # Environment variables to load
source= $UserConfigs/WindowRules.conf # all about Hyprland Window Rules and Layer Rules
source= $UserConfigs/UserDecorations.conf # Decorations config file
source= $UserConfigs/UserAnimations.conf # Animation config file
source= $UserConfigs/Keybinds.conf # Put your own keybinds here
source= $UserConfigs/UserSettings.conf # Main Hyprland Settings.
# nwg-displays
source= $UserConfigs/monitors.conf
source= $UserConfigs/workspaces.conf
exec = gsettings set org.gnome.desktop.interface cursor-theme Future-black Cursors
# HyprMod managed settings
source = $HOME/.config/hypr/hyprland-gui.conf

View File

@@ -35,7 +35,7 @@ require("layout.workspaces")
-- 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")
hl.exec_cmd("gsettings set org.gnome.desktop.interface cursor-theme Bibata-Modern-Classic && gsettings set org.gnome.desktop.interface cursor-size 24")
end)
-- HyprMod managed settings

View File

@@ -113,7 +113,7 @@ label {
text = cmd[update:1000] echo "$(~/.config/hypr/hyprlock/Scripts/songdetail.sh)"
color = rgba(255, 255, 255, 0.6)
font_size = 18
font_family = JetBrains Mono Nerd, SF Pro Display Bold
font_family = JetBrainsMono Nerd Font, SF Pro Display Bold
position = 0, 90
halign = center
valign = bottom
@@ -221,7 +221,7 @@ label {
text = cmd[update:1000] echo "$(~/.config/hypr/hyprlock/Scripts/songdetail.sh)"
color = rgba(255, 255, 255, 0.6)
font_size = 28
font_family = JetBrains Mono Nerd, SF Pro Display Bold
font_family = JetBrainsMono Nerd Font, SF Pro Display Bold
position = 0, 90
halign = center
valign = bottom

View File

@@ -13,7 +13,7 @@ kvantum_theme="catppuccin-mocha-blue"
color_scheme="prefer-dark"
gtk_theme="Material-DeepOcean-BL"
icon_theme="Papirus-Dark"
cursor_theme="Moga-Cursor"
cursor_theme="Bibata-Modern-Classic"
# Check if a marker file exists.
if [ ! -f "$HOME/.config/hypr/.initial_startup_done" ]; then

View File

@@ -10,7 +10,7 @@ local userScripts = os.getenv("HOME") .. "/.config/hypr/UserScripts"
local edit = os.getenv("EDITOR") or "nvim"
-- These two are for UserKeybinds.conf & Waybar Modules
local term = "foot" -- Terminal
local term = "ghostty" -- Terminal (foot is still installed as a fallback)
local files = "thunar" -- File Manager
-- Default Search Engine for ROFI Search (SUPER S)
@@ -31,7 +31,7 @@ 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 .. " + S", hl.dsp.exec_cmd("hyprmod")) -- Hyprmod - A Hyprland settings manager (AUR: hyprmod)
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
@@ -40,8 +40,9 @@ hl.bind(mainMod .. " + SHIFT + F", hl.dsp.window.fullscreen({ mode = "fullscreen
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}')"]]))
-- (`hyprctl keyword` is rejected by the Lua config — "keyword can't work with non-legacy parsers" — so set it via eval)
hl.bind(mainMod .. " + ALT + mouse_down", hl.dsp.exec_cmd([[hyprctl eval "hl.config({ 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 eval "hl.config({ 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
@@ -116,23 +117,22 @@ hl.bind(mainMod .. " + SHIFT + tab", hl.dsp.focus({ workspace = "m-1" }))
hl.bind(mainMod .. " + U", hl.dsp.workspace.toggle_special("nyx"))
hl.bind(mainMod .. " + 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
-- Number-row keys (the Lua bind parser does not accept the code:NN keycode syntax)
-- 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
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }))
end
-- Move active window and follow to workspace mainMod + SHIFT + [0-9]
for i, 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 }))
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i, follow = true }))
end
hl.bind(mainMod .. " + SHIFT + bracketleft", hl.dsp.window.move({ workspace = -1, follow = true })) -- brackets [
hl.bind(mainMod .. " + SHIFT + bracketright", hl.dsp.window.move({ workspace = 1, follow = true })) -- brackets ]
-- Move active window to a workspace silently mainMod + CTRL + [0-9]
for i, 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 }))
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = i, follow = false }))
end
hl.bind(mainMod .. " + CTRL + bracketleft", hl.dsp.window.move({ workspace = -1, follow = false })) -- brackets [
hl.bind(mainMod .. " + CTRL + bracketright", hl.dsp.window.move({ workspace = 1, follow = false })) -- brackets ]
@@ -151,9 +151,6 @@ hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) --
-- 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
@@ -161,7 +158,7 @@ hl.bind("XF86Launch3", hl.dsp.exec_cmd("asusctl led-mode -n")) -- FN+F4 Switch k
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
hl.bind("XF86TouchpadToggle", hl.dsp.exec_cmd(scriptsDir .. "/TouchPad.sh")) -- toggle touchpad (finds the device itself)
-- Screenshot keybindings using F6 (no PrintSrc button)
-- hl.bind(mainMod .. " + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --now")) -- screenshot
@@ -169,8 +166,3 @@ hl.bind("XF86TouchpadToggle", hl.dsp.exec_cmd(scriptsDir .. "/TouchPad.sh")) --
-- 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

@@ -10,5 +10,6 @@
-- <<<<< 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" })
hl.monitor({ output = "desc:Acer Technologies VG240Y S 0x126191DE", mode = "1920x1080@165.00301", position = "0x0", scale = 1.00000000, transform = 0, vrr = 0, bitdepth = 10, cm = "wide" })
hl.monitor({ output = "HEADLESS-1", mode = "1728x1080@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })
-- <<<<< TUI AUTO END

View File

@@ -57,6 +57,11 @@ hl.window_rule({ name = "windowrule-24", tag = "+im", match = { class = "^(im.ri
-- 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+)$" } })
-- While Sunshine is streaming, open Steam games fullscreen on the headless stream output
-- (workspace 9 -> HEADLESS-1). Old games like KOTOR fix their render size/scaling at launch
-- and never adapt to a later resize, so they must start there. Off by default (local play
-- unaffected); toggled by ~/.config/sunshine/scripts/sunshine-games.sh via the global.
SunshineGamesRule = hl.window_rule({ name = "sunshine-games", enabled = false, workspace = "9", fullscreen = true, match = { class = "^(steam_app_\\d+)$" } })
-- gamestore tags
hl.window_rule({ name = "windowrule-27", tag = "+gamestore", match = { class = "^([Ss]team)$" } })
@@ -93,8 +98,13 @@ hl.window_rule({ name = "windowrule-46", tag = "+settings", match = { class = "^
-- 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)$" } })
-- System updater terminal (quickshell/scripts/update-system.sh) — float + center it instead of
-- tiling into the current workspace. Matches the title the script launches it with, so it works
-- for Ghostty and the foot fallback alike; initial_title so a later rename can't dodge it.
-- Fixed pixel size rather than monitor_w/h fractions: HDMI-A-1 is rotated portrait (1080 wide),
-- where a fraction of the width made it too narrow for package output; 1000x650 fits every screen.
hl.window_rule({ name = "system-update-terminal", float = true, center = true, size = "1000 650", match = { initial_title = "^(update)$" } })
-- viewer tags
hl.window_rule({ name = "windowrule-50", tag = "+viewer", match = { class = "^(gnome-system-monitor|org.gnome.SystemMonitor|io.missioncenter.MissionCenter)$" } }) -- system monitor
@@ -128,17 +138,14 @@ hl.window_rule({
-- 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.
-- Discord / Spotify / Plexamp to workspace 3 (the portrait LG). An older rule also sent
-- Vesktop/Discord/Spotify to special:nyx; this one always won, so that rule was removed.
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-75", float = true, opacity = "0.9 0.9", size = "monitor_w*0.7 monitor_h*0.7", match = { tag = "wallpaper*" } })
hl.window_rule({ name = "windowrule-76", float = true, opacity = "0.8 0.8", size = "monitor_w*0.7 monitor_h*0.7", match = { tag = "settings*" } })
hl.window_rule({ name = "windowrule-77", float = true, match = { tag = "viewer*" } })
hl.window_rule({ name = "windowrule-78", float = true, match = { class = "([Zz]oom|onedriver|onedriver-launcher)$" } })
hl.window_rule({ name = "windowrule-79", float = true, match = { class = "(org.gnome.Calculator)", title = "(Calculator)" } })
@@ -162,17 +169,17 @@ hl.window_rule({ name = "windowrule-92", float = true, center = true, size = "mo
-- 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)$" } })
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "browser*" } })
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "projects*" } })
-- hl.window_rule({ opacity = "0.94 0.94", match = { tag = "im*" } })
-- hl.window_rule({ opacity = "0.94 0.94", match = { tag = "multimedia*" } })
-- hl.window_rule({ opacity = "0.9 0.9", match = { tag = "file-manager*" } })
hl.window_rule({ name = "windowrule-93", opacity = "0.8 0.8", match = { tag = "terminal*" } })
-- hl.window_rule({ opacity = "0.82 0.82", match = { tag = "viewer*" } })
hl.window_rule({ name = "windowrule-94", opacity = "0.8 0.8", match = { class = "^(gedit|org.gnome.TextEditor|mousepad)$" } })
hl.window_rule({ name = "windowrule-95", opacity = "0.9 0.9", match = { class = "^(deluge)$" } })
hl.window_rule({ name = "windowrule-96", opacity = "0.9 0.9", match = { class = "^(seahorse)$" } }) -- gnome-keyring gui
-- hl.window_rule({ opacity = "0.95 0.95", match = { title = "^(Picture-in-Picture)$" } })
-- SIZE
-- hl.window_rule({ size = "25% 25%", match = { title = "^(Firefox)$" } })

View File

@@ -13,6 +13,7 @@
hl.workspace_rule({ workspace = 1, monitor = "DP-2", default = true })
hl.workspace_rule({ workspace = 2, monitor = "DP-1", default = true })
hl.workspace_rule({ workspace = 3, monitor = "HDMI-A-1", default = true })
hl.workspace_rule({ workspace = 9, monitor = "HEADLESS-1", default = true })
-- hl.workspace_rule({ workspace = 4, monitor = "eDP-1" })
-- hl.workspace_rule({ workspace = 5, monitor = "DP-2" })
-- hl.workspace_rule({ workspace = 6, monitor = "DP-2" })

View File

@@ -0,0 +1,31 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "HyprFlux (default)"
-- The animation set that shipped as appearance/animations.lua (ported from
-- UserConfigs/UserAnimations.conf). Selected by default; Super+Shift+A switches.
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

@@ -1,31 +1,38 @@
#!/bin/bash
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# For applying Animations from different users
# Animation preset picker (Super+Shift+A).
# Lists ~/.config/hypr/presets/anim-*.lua by their original HyprFlux preset name
# (the "Ported from animations/<name>.conf" line, else `-- name "..."`), then
# points appearance/animations.lua's require() at the chosen one and reloads.
iDIR="$HOME/.config/swaync/images"
presets_dir="$HOME/.config/hypr/presets"
active_file="$HOME/.config/hypr/appearance/animations.lua"
rofi_theme="$HOME/.config/rofi/launchers/type-4/style-4.rasi"
# Check if rofi is already running
if pidof rofi >/dev/null; then
pkill rofi
fi
# Variables
iDIR="$HOME/.config/swaync/images"
SCRIPTSDIR="$HOME/.config/hypr/scripts"
animations_dir="$HOME/.config/hypr/animations"
UserConfigs="$HOME/.config/hypr/UserConfigs"
rofi_theme="$HOME/.config/rofi/config-Animations.rasi"
msg='❗NOTE:❗ This will copy animations into UserAnimations.conf'
# list of animation files, sorted alphabetically with numbers first
animations_list=$(find -L "$animations_dir" -maxdepth 1 -type f | sed 's/.*\///' | sed 's/\.conf$//' | sort -V)
# "Display name<TAB>module" per preset, sorted by name
list=$(for f in "$presets_dir"/anim-*.lua; do
module=$(basename "$f" .lua)
name=$(sed -n 's/^-- Ported from animations\/\(.*\)\.conf.*/\1/p' "$f" | head -n1)
[[ -z "$name" ]] && name=$(sed -n 's/^-- name "\(.*\)"$/\1/p' "$f" | head -n1)
printf '%s\t%s\n' "${name:-$module}" "$module"
done | sort -f)
# Rofi Menu
chosen_file=$(echo "$animations_list" | rofi -i -dmenu -config $rofi_theme -mesg "$msg")
current=$(sed -n 's/^require("presets\.\(.*\)")$/\1/p' "$active_file")
current_name=$(awk -F'\t' -v m="$current" '$2 == m { print $1 }' <<<"$list")
# Check if a file was selected
if [[ -n "$chosen_file" ]]; then
full_path="$animations_dir/$chosen_file.conf"
cp "$full_path" "$UserConfigs/UserAnimations.conf"
notify-send -u low -i "$iDIR/ja.png" "$chosen_file" "Hyprland Animation Loaded"
fi
chosen=$(cut -f1 <<<"$list" | rofi -i -dmenu -p "Animations" \
-theme "$rofi_theme" \
-theme-str 'mainbox { children: [ "inputbar", "message", "listview" ]; }' \
-mesg "Current: ${current_name:-$current}")
[[ -z "$chosen" ]] && exit 0
sleep 1
"$SCRIPTSDIR/RefreshNoWaybar.sh"
module=$(awk -F'\t' -v n="$chosen" '$1 == n { print $2; exit }' <<<"$list")
[[ -z "$module" ]] && exit 1
sed -i "s/^require(\"presets\.[^\"]*\")$/require(\"presets.$module\")/" "$active_file"
hyprctl reload >/dev/null
notify-send -u low -i "$iDIR/note.png" "$chosen" "Hyprland animation loaded"

View File

@@ -3,7 +3,7 @@
# Clipboard Manager. This script uses cliphist, rofi, and wl-copy.
# Variables
rofi_theme="$HOME/.config/rofi/config-clipboard.rasi"
rofi_theme="$HOME/.config/rofi/launchers/type-4/style-4.rasi"
msg='👀 **note** CTRL DEL = cliphist del (entry) or ALT DEL - cliphist wipe (all)'
# Actions:
# CTRL Del to delete an entry
@@ -19,7 +19,9 @@ while true; do
rofi -i -dmenu \
-kb-custom-1 "Control-Delete" \
-kb-custom-2 "Alt-Delete" \
-config $rofi_theme < <(cliphist list) \
-theme "$rofi_theme" \
-theme-str 'mainbox { children: [ "inputbar", "message", "listview" ]; }' \
< <(cliphist list) \
-mesg "$msg"
)

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

@@ -4,6 +4,11 @@
music_icon="$HOME/.config/swaync/icons/music.png"
# Use the app's own icon for known players
case "$(playerctl metadata --format '{{playerName}}' 2>/dev/null)" in
Plexamp) music_icon="com.plexamp.Plexamp" ;;
esac
# Play the next track
play_next() {
playerctl next

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# Scripts for refreshing waybar, rofi, swaync
# Scripts for refreshing quickshell, rofi, swaync (Waybar is retired — not restarted here)
SCRIPTSDIR=$HOME/.config/hypr/scripts
UserScripts=$HOME/.config/hypr/UserScripts
@@ -15,31 +15,24 @@ file_exists() {
}
# Kill already running processes
_ps=(waybar rofi swaync ags)
_ps=(rofi swaync ags)
for _prs in "${_ps[@]}"; do
if pidof "${_prs}" >/dev/null; then
pkill "${_prs}"
fi
done
# ensure waybar fully reloads
killall -SIGUSR2 waybar
# quit ags & relaunch ags
#ags -q && ags &
# quit quickshell & relaunch quickshell
pkill qs && qs &
pkill -x qs; qs &
# some process to kill
for pid in $(pidof waybar rofi swaync ags swaybg); do
for pid in $(pidof rofi swaync ags swaybg); do
kill -SIGUSR1 "$pid"
done
#Restart waybar
sleep 1
waybar &
# relaunch swaync
sleep 0.5
swaync > /dev/null 2>&1 &

View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Keeps hypridle from locking the screen while Sunshine is streaming.
# Hooked up as Sunshine's global_prep_cmd, so it runs for every app:
# start -> hold a systemd idle inhibitor (hypridle respects these) and
# unlock hyprlock if the session was already locked
# stop -> release the inhibitor so normal idle locking resumes
pidfile="${XDG_RUNTIME_DIR:-/tmp}/sunshine-idle-inhibit.pid"
case "$1" in
start)
if ! { [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; }; then
setsid systemd-inhibit --what=idle --who=Sunshine --why="Streaming" \
sleep infinity >/dev/null 2>&1 &
echo $! > "$pidfile"
fi
# SIGUSR1 tells hyprlock to unlock
pkill -USR1 -x hyprlock
;;
stop)
[ -f "$pidfile" ] && kill "$(cat "$pidfile")" 2>/dev/null
rm -f "$pidfile"
;;
*)
echo "Usage: $0 start|stop" >&2
exit 1
;;
esac
exit 0

View File

@@ -1,32 +1,30 @@
#!/bin/bash
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# For disabling touchpad.
# Edit the Touchpad_Device on ~/.config/hypr/UserConfigs/Laptops.conf according to your system
# use hyprctl devices to get your system touchpad device name
# source https://github.com/hyprwm/Hyprland/discussions/4283?sort=new#discussioncomment-8648109
# Toggles the laptop touchpad (XF86TouchpadToggle).
# Finds the touchpad by name in `hyprctl devices`, so it works on any laptop
# without hard-coding the device. The Lua config rejects `hyprctl keyword`,
# so the device is switched with hl.device() through `hyprctl eval`.
notif="$HOME/.config/swaync/images/ja.png"
notif="$HOME/.config/swaync/images/note.png"
STATUS_FILE="$XDG_RUNTIME_DIR/touchpad.status"
export STATUS_FILE="$XDG_RUNTIME_DIR/touchpad.status"
enable_touchpad() {
printf "true" >"$STATUS_FILE"
notify-send -u low -i $notif " Enabling" " touchpad"
hyprctl keyword '$TOUCHPAD_ENABLED' "true" -r
}
disable_touchpad() {
printf "false" >"$STATUS_FILE"
notify-send -u low -i $notif " Disabling" " touchpad"
hyprctl keyword '$TOUCHPAD_ENABLED' "false" -r
}
if ! [ -f "$STATUS_FILE" ]; then
enable_touchpad
else
if [ $(cat "$STATUS_FILE") = "true" ]; then
disable_touchpad
elif [ $(cat "$STATUS_FILE") = "false" ]; then
enable_touchpad
fi
device=$(hyprctl devices -j | jq -r '[.mice[].name | select(test("touchpad"; "i"))][0] // empty')
if [ -z "$device" ]; then
notify-send -u low -i "$notif" "Touchpad" "No touchpad found"
exit 1
fi
set_touchpad() {
printf '%s' "$1" >"$STATUS_FILE"
hyprctl eval "hl.device({ name = \"$device\", enabled = $1 })" >/dev/null
if [ "$1" = true ]; then
notify-send -u low -i "$notif" " Enabling" " touchpad"
else
notify-send -u low -i "$notif" " Disabling" " touchpad"
fi
}
if [ "$(cat "$STATUS_FILE" 2>/dev/null)" = "false" ]; then
set_touchpad true
else
set_touchpad false
fi

View File

@@ -1,143 +1,37 @@
#!/bin/bash
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# Scripts for volume controls for audio and mic
# Volume controls for audio and mic via wpctl (PipeWire).
# Quickshell's VolumeOsd watches PipeWire directly, so no notifications here.
iDIR="$HOME/.config/swaync/icons"
sDIR="$HOME/.config/hypr/scripts"
SINK="@DEFAULT_AUDIO_SINK@"
SOURCE="@DEFAULT_AUDIO_SOURCE@"
STEP="5%"
# Get Volume
# Print volume as an integer percent, or "Muted"
get_volume() {
volume=$(pamixer --get-volume)
if [[ "$volume" -eq "0" ]]; then
local out
out=$(wpctl get-volume "$1")
if [[ "$out" == *MUTED* ]]; then
echo "Muted"
else
echo "$volume %"
awk '{ printf "%d %%\n", $2 * 100 + 0.5 }' <<< "$out"
fi
}
# Get icons
get_icon() {
current=$(get_volume)
if [[ "$current" == "Muted" ]]; then
echo "$iDIR/volume-mute.png"
elif [[ "${current%\%}" -le 30 ]]; then
echo "$iDIR/volume-low.png"
elif [[ "${current%\%}" -le 60 ]]; then
echo "$iDIR/volume-mid.png"
else
echo "$iDIR/volume-high.png"
fi
is_muted() {
[[ "$(wpctl get-volume "$1")" == *MUTED* ]]
}
# Notify
notify_user() {
if [[ "$(get_volume)" == "Muted" ]]; then
notify-send -e -h string:x-canonical-private-synchronous:volume_notif -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$(get_icon)" " Volume:" " Muted"
else
notify-send -e -h int:value:"$(get_volume | sed 's/%//')" -h string:x-canonical-private-synchronous:volume_notif -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$(get_icon)" " Volume Level:" " $(get_volume)" &&
"$sDIR/Sounds.sh" --volume
fi
}
# Raising/lowering while muted unmutes first, matching the old behaviour
inc() { is_muted "$1" && wpctl set-mute "$1" 0 || wpctl set-volume -l 1.0 "$1" "$STEP+"; }
dec() { is_muted "$1" && wpctl set-mute "$1" 0 || wpctl set-volume "$1" "$STEP-"; }
# Increase Volume
inc_volume() {
if [ "$(pamixer --get-mute)" == "true" ]; then
toggle_mute
else
pamixer -i 5 --allow-boost --set-limit 150 && notify_user
fi
}
# Decrease Volume
dec_volume() {
if [ "$(pamixer --get-mute)" == "true" ]; then
toggle_mute
else
pamixer -d 5 && notify_user
fi
}
# Toggle Mute
toggle_mute() {
if [ "$(pamixer --get-mute)" == "false" ]; then
pamixer -m && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/volume-mute.png" " Mute"
elif [ "$(pamixer --get-mute)" == "true" ]; then
pamixer -u && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$(get_icon)" " Volume:" " Switched ON"
fi
}
# Toggle Mic
toggle_mic() {
if [ "$(pamixer --default-source --get-mute)" == "false" ]; then
pamixer --default-source -m && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/microphone-mute.png" " Microphone:" " Switched OFF"
elif [ "$(pamixer --default-source --get-mute)" == "true" ]; then
pamixer -u --default-source u && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/microphone.png" " Microphone:" " Switched ON"
fi
}
# Get Mic Icon
get_mic_icon() {
current=$(pamixer --default-source --get-volume)
if [[ "$current" -eq "0" ]]; then
echo "$iDIR/microphone-mute.png"
else
echo "$iDIR/microphone.png"
fi
}
# Get Microphone Volume
get_mic_volume() {
volume=$(pamixer --default-source --get-volume)
if [[ "$volume" -eq "0" ]]; then
echo "Muted"
else
echo "$volume %"
fi
}
# Notify for Microphone
notify_mic_user() {
volume=$(get_mic_volume)
icon=$(get_mic_icon)
notify-send -e -h int:value:"$volume" -h "string:x-canonical-private-synchronous:volume_notif" -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$icon" " Mic Level:" " $volume"
}
# Increase MIC Volume
inc_mic_volume() {
if [ "$(pamixer --default-source --get-mute)" == "true" ]; then
toggle_mic
else
pamixer --default-source -i 5 && notify_mic_user
fi
}
# Decrease MIC Volume
dec_mic_volume() {
if [ "$(pamixer --default-source --get-mute)" == "true" ]; then
toggle-mic
else
pamixer --default-source -d 5 && notify_mic_user
fi
}
# Execute accordingly
if [[ "$1" == "--get" ]]; then
get_volume
elif [[ "$1" == "--inc" ]]; then
inc_volume
elif [[ "$1" == "--dec" ]]; then
dec_volume
elif [[ "$1" == "--toggle" ]]; then
toggle_mute
elif [[ "$1" == "--toggle-mic" ]]; then
toggle_mic
elif [[ "$1" == "--get-icon" ]]; then
get_icon
elif [[ "$1" == "--get-mic-icon" ]]; then
get_mic_icon
elif [[ "$1" == "--mic-inc" ]]; then
inc_mic_volume
elif [[ "$1" == "--mic-dec" ]]; then
dec_mic_volume
else
get_volume
fi
case "$1" in
--inc) inc "$SINK" ;;
--dec) dec "$SINK" ;;
--toggle) wpctl set-mute "$SINK" toggle ;;
--toggle-mic) wpctl set-mute "$SOURCE" toggle ;;
--mic-inc) inc "$SOURCE" ;;
--mic-dec) dec "$SOURCE" ;;
--get-mic) get_volume "$SOURCE" ;;
*) get_volume "$SINK" ;;
esac

View File

@@ -1,5 +0,0 @@
accent_primary = "#007acc"
accent_secondary = "#06b6d4"
background = "#1e1e1e"
foreground = "#d4d4d8"
opacity = 0.91

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Sunshine_Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" width="256px" height="256px" viewBox="10 6 236 245" style="enable-background:new 0 0 256 256;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FDD107;}
.st1{fill:#F89A1C;}
.st2{fill:#EF3E23;}
.st3{fill:#F26222;}
</style>
<path class="st0" d="M118.7688675,20.7120476c0,0-63.8333359,26-74.3333359,83.8333282s37.1666718,91.5,86.3333282,75.3333282
s70.3333435-51,81.8333435-86.9999924c0,0-9.3333435,100.4999924-96.1666718,115.4999924s-118.1666641-50-82.1666641-119.8333282
C44.2688675,67.0453796,80.5188675,29.6287155,118.7688675,20.7120476z"/>
<path class="st1" d="M118.7688675,20.7120476c0,0-41.125,3.6666679-83.25,61.0416679s-28.125,139.125,34.25,149.375
s115.8749924-44.875,133.5-82.375s15.1666718-61.4583282,9.75-77.8749924c0,0,0.6666718,36.4166641-13.3333282,59.6666641
s-29.75,46.3333282-65.0833282,62.1666718s-74.1666718,13.75-95.4166718-19.25s-5.9166641-76.0833359-0.2916641-85.3333359
S72.3938675,33.7953796,118.7688675,20.7120476z"/>
<path class="st2" d="M73.0188675,39.6287155c0,0,38.125-28.125,76.8749924-28.125s63,28.25,68.5,52.25s6,54.125-11.5,87.6249924
s-37.375,56-79.1249924,76.125s-84.625,2.75-84.625,2.75s25.9769745,25.8750153,71.0509872,16.5
c45.0740051-9.375,82.2406769-40.875,98.4073486-69.5s28.7916565-57.3749924,27.6666565-92.2499924s-23.75-54.5-31.25-60.25
s-23.1875-17.8125-58.1875-16.5625S86.4563675,29.8162155,73.0188675,39.6287155z"/>
<path class="st3" d="M73.0188675,39.6287155c0,0,35-32.8125,82.4374924-32.8125s69.1875,24.8125,78.875,44.6875
s21.8125,70-12.1875,122.9999924s-74.625,67.375-93.625,71.625s-42.4311447,4.269165-59.1114044-1.2299957
c0,0,35.1947479,8.3966675,66.7780762-7.4366608s51.6666718-32.1666718,74.0833282-68.8333435
s25.9166718-72.7499924,22.1666718-93.9166565s-12.1666718-42.4166718-36.5-56.3333359s-56.7291718-10.531251-74.4791641-4.531251
S91.9876175,26.4099655,73.0188675,39.6287155z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -4,6 +4,15 @@ 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"
// Brand logos (YouTube, Spotify, Plex...), which Material Symbols doesn't
// have — used by MediaPlayer.qml's per-source icon. The "Propo" build
// draws Nerd Font icons at full size; the Mono one squeezes them into a
// single character cell, which renders them noticeably smaller.
readonly property string brandIconFontFamily: "CaskaydiaCove Nerd Font Propo"
// 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
@@ -22,4 +31,23 @@ QtObject {
readonly property int spacing: 10
readonly property int animationDuration: 150
// Max characters for the bar's free-text labels before they're cut off
// with "…" (ActiveWindow.qml and MediaPlayer.qml).
readonly property int activeWindowMaxChars: 50
readonly property int mediaMaxChars: 40
// Tighter limits for narrow screens (the laptop's eDP-1 is 1440 logical
// px wide), where the full-length labels nearly run into the centre
// clock/weather group. Bar.qml switches when it's narrower than
// compactBarWidth.
readonly property int compactBarWidth: 1600
readonly property int activeWindowMaxCharsCompact: 32
readonly property int mediaMaxCharsCompact: 28
// Counts code points rather than UTF-16 units so emoji / some CJK
// aren't split in half at the cut.
function truncate(str, maxChars) {
const chars = Array.from(str || "")
return chars.length > maxChars ? chars.slice(0, maxChars - 1).join("").replace(/\s+$/, "") + "…" : chars.join("")
}
}

View File

@@ -0,0 +1,72 @@
pragma Singleton
import QtQuick
import Quickshell.Io
// Shared state for SystemUpdate.qml, the bar's package-update indicator.
// Bar.qml is instantiated once per screen (see shell.qml's Variants), so
// without this being a singleton, plugging in extra monitors would mean
// running `checkupdates` that many times in parallel on every poll — which
// isn't just wasteful, it's actively unreliable: concurrent invocations
// race over checkupdates' own sync directory and can intermittently report
// 0 pending updates. One shared Process here means exactly one `checkupdates`
// run at a time, and every bar's icon reflects the same result.
QtObject {
id: root
// checkupdates syncs a throwaway copy of the pacman db, so it's not
// free to run often — matches the old waybar custom/updater module's
// ~21.7 minute poll interval (see .config/waybar/config).
readonly property int refreshIntervalMs: 1300 * 1000
property int updateCount: 0
property bool checking: false
property bool updating: false
function refresh() {
if (_checkProc.running) return
root.checking = true
_checkProc.running = true
}
// scriptPath: absolute path to update-system.sh (resolved by the caller
// via Quickshell.shellPath, which isn't available as a plain import
// here without pulling in the whole Quickshell singleton for one call).
function runUpdate(scriptPath) {
if (_updateProc.running) return
root.updating = true
_updateProc.command = [scriptPath]
_updateProc.running = true
}
// QtObject has no default property, so these are attached via property
// assignment rather than as plain child elements (same as Colors.qml's
// FileView).
readonly property Process _checkProc: Process {
command: ["sh", "-c", "checkupdates 2>/dev/null | wc -l"]
stdout: StdioCollector {
id: checkCollector
onStreamFinished: {
root.updateCount = parseInt(checkCollector.text.trim(), 10) || 0
root.checking = false
}
}
}
// Runs update-system.sh, which opens its own terminal and blocks until
// that terminal is closed — so this only exits once the user's actually
// done updating, which is exactly when a re-check matters.
readonly property Process _updateProc: Process {
onExited: {
root.updating = false
root.refresh()
}
}
readonly property Timer _timer: Timer {
interval: root.refreshIntervalMs
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refresh()
}
}

View File

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

View File

@@ -0,0 +1,242 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Live weather for the bar's Weather.qml icon. A singleton for the same
// reason as SystemUpdates.qml: Bar.qml is instantiated once per monitor
// (shell.qml's Variants), so without this being shared, every extra screen
// would mean another set of network requests running in parallel.
//
// Location is auto-detected from the public IP (via ipwho.is, no API key)
// and cached to disk (~/.cache/quickshell/weather-location.json) so it's
// only ever looked up once, not on every quickshell restart — IP-geolocation
// free tiers rate-limit fast, and a desktop's location barely ever changes
// anyway. Only the weather itself re-polls on a timer. Conditions come from
// Open-Meteo (api.open-meteo.com) — also free and keyless, and it does its
// own °C/km-h conversion server-side via query params.
//
// Loosely modeled on omarchy's weather panel
// (github.com/omacom/omarchy, quattro branch,
// shell/plugins/panels/weather/Model.js) for the WMO weather-code
// grouping, but the icons are this bar's own Material Symbols glyphs
// rather than pulling in a separate icon font just for this.
QtObject {
id: root
// Location barely ever changes for a desktop machine, so it's fetched
// once (and cached); the weather itself is worth re-checking
// periodically.
readonly property int refreshIntervalMs: 15 * 60 * 1000
property bool locating: true
property bool loading: false
property bool hasLocation: false
property string locationName: ""
property real latitude: 0
property real longitude: 0
function saveLocation() {
_locationCache.setText(JSON.stringify({
latitude: root.latitude,
longitude: root.longitude,
name: root.locationName
}))
}
// Called from Weather.qml's search box once you pick a result — always
// wins over whatever IP-based guess was cached before, and persists so
// it sticks across restarts.
function selectLocation(name, lat, lon) {
root.latitude = lat
root.longitude = lon
root.locationName = name
root.hasLocation = true
root.locating = false
root.saveLocation()
root.refresh()
}
property bool searching: false
property var searchResults: []
function search(query) {
const q = String(query || "").trim()
if (q.length < 2) {
root.searchResults = []
return
}
root.searching = true
_searchProc.command = ["curl", "-s", "--max-time", "6",
"https://geocoding-api.open-meteo.com/v1/search?count=5&language=en&format=json&name=" + encodeURIComponent(q)]
_searchProc.running = true
}
property real tempC: 0
property real feelsLikeC: 0
property int humidity: 0
property real windKmh: 0
property int weatherCode: 0
property bool isDay: true
// [{date, code, maxC, minC}] — tomorrow onward; today is already
// covered by the current-conditions fields above.
property var forecast: []
function locate() {
if (_locateProc.running) return
root.locating = true
_locateProc.running = true
}
function refresh() {
if (!root.hasLocation || _weatherProc.running) return
root.loading = true
_weatherProc.command = ["curl", "-s", "--max-time", "8",
"https://api.open-meteo.com/v1/forecast?latitude=" + root.latitude
+ "&longitude=" + root.longitude
+ "&current=temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,is_day,wind_speed_10m"
+ "&daily=weather_code,temperature_2m_max,temperature_2m_min"
+ "&temperature_unit=celsius&wind_speed_unit=kmh&timezone=auto&forecast_days=4"]
_weatherProc.running = true
}
// Material Symbols Rounded glyph for a WMO weather code (see
// https://open-meteo.com/en/docs#weathervariables), grouped the same
// way omarchy's Model.js groups them into its own icon set.
function iconFor(code, day) {
if (code === 0) return day ? "sunny" : "clear_night"
if (code === 1 || code === 2) return day ? "partly_cloudy_day" : "partly_cloudy_night"
if (code === 3) return "cloud"
if (code === 45 || code === 48) return "foggy"
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].indexOf(code) !== -1) return "rainy"
if ([71, 73, 75, 77, 85, 86].indexOf(code) !== -1) return "weather_snowy"
if (code === 95 || code === 96 || code === 99) return "thunderstorm"
return "cloud"
}
// QtObject has no default property, so these are attached via property
// assignment rather than as plain child elements (same as Colors.qml's
// FileView and SystemUpdates.qml's Process/Timer).
// Read once at startup; onLoaded/onLoadFailed (below) decide whether a
// network lookup is still needed.
readonly property FileView _locationCache: FileView {
path: Quickshell.env("HOME") + "/.cache/quickshell/weather-location.json"
onLoaded: {
try {
const data = JSON.parse(text())
const lat = parseFloat(data.latitude)
const lon = parseFloat(data.longitude)
if (!isNaN(lat) && !isNaN(lon)) {
root.latitude = lat
root.longitude = lon
root.locationName = data.name || ""
root.hasLocation = true
root.locating = false
root.refresh()
return
}
} catch (e) {
// fall through to a fresh lookup below
}
root.locate()
}
onLoadFailed: root.locate()
}
readonly property Process _locateProc: Process {
command: ["curl", "-s", "--max-time", "6", "https://ipwho.is/"]
stdout: StdioCollector {
id: locateCollector
onStreamFinished: {
try {
const data = JSON.parse(locateCollector.text)
const lat = parseFloat(data.latitude)
const lon = parseFloat(data.longitude)
root.hasLocation = data.success !== false && !isNaN(lat) && !isNaN(lon)
if (root.hasLocation) {
root.latitude = lat
root.longitude = lon
root.locationName = data.city || data.region || data.country || ""
root.saveLocation()
}
} catch (e) {
console.warn("Weather: locate parse failed:", e)
root.hasLocation = false
}
root.locating = false
if (root.hasLocation) root.refresh()
}
}
}
readonly property Process _weatherProc: Process {
stdout: StdioCollector {
id: weatherCollector
onStreamFinished: {
try {
const data = JSON.parse(weatherCollector.text)
const current = data.current || {}
root.tempC = current.temperature_2m
root.feelsLikeC = current.apparent_temperature
root.humidity = current.relative_humidity_2m
root.windKmh = current.wind_speed_10m
root.weatherCode = current.weather_code
root.isDay = current.is_day === 1
const daily = data.daily || {}
const times = daily.time || []
const days = []
// Start at 1 — index 0 is today, already shown as the
// current conditions above; this is "what's coming".
for (let i = 1; i < times.length && days.length < 3; i++) {
days.push({
date: times[i],
code: (daily.weather_code || [])[i],
maxC: (daily.temperature_2m_max || [])[i],
minC: (daily.temperature_2m_min || [])[i]
})
}
root.forecast = days
} catch (e) {
console.warn("Weather: failed to parse Open-Meteo response:", e)
}
root.loading = false
}
}
}
readonly property Process _searchProc: Process {
stdout: StdioCollector {
id: searchCollector
onStreamFinished: {
try {
const data = JSON.parse(searchCollector.text)
const results = data.results || []
root.searchResults = results.filter(function(r) {
return r && r.name && r.latitude !== undefined && r.longitude !== undefined
}).map(function(r) {
return {
name: r.name,
description: [r.admin1, r.country].filter(function(p) { return !!p }).join(", "),
latitude: r.latitude,
longitude: r.longitude
}
})
} catch (e) {
root.searchResults = []
}
root.searching = false
}
}
}
readonly property Timer _timer: Timer {
interval: root.refreshIntervalMs
running: true
repeat: true
onTriggered: root.refresh()
}
}

View File

@@ -2,3 +2,5 @@ module config
singleton Colors 1.0 Colors.qml
singleton Appearance 1.0 Appearance.qml
singleton VpnExclusivity 1.0 VpnExclusivity.qml
singleton SystemUpdates 1.0 SystemUpdates.qml
singleton WeatherData 1.0 WeatherData.qml

View File

@@ -4,10 +4,9 @@ import Quickshell.Hyprland
import "../../config"
Text {
Layout.maximumWidth: 320
elide: Text.ElideRight
property bool compact: false
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
text: Hyprland.activeToplevel ? Hyprland.activeToplevel.title : ""
text: Hyprland.activeToplevel ? Appearance.truncate(Hyprland.activeToplevel.title, compact ? Appearance.activeWindowMaxCharsCompact : Appearance.activeWindowMaxChars) : ""
}

View File

@@ -16,6 +16,10 @@ PanelWindow {
exclusiveZone: implicitHeight
color: "transparent"
// Narrow screen (laptop): shorten the left-side free-text labels so they
// don't crowd the centre group.
readonly property bool compact: width < Appearance.compactBarWidth
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "quickshell:bar"
@@ -32,11 +36,16 @@ PanelWindow {
}
spacing: Appearance.spacing
Workspaces { screen: bar.screen }
ActiveWindow {}
MediaPlayer { barWindow: bar; compact: bar.compact }
ActiveWindow { compact: bar.compact }
}
Clock {
RowLayout {
anchors.centerIn: parent
spacing: Appearance.spacing
Weather { barWindow: bar }
Clock {}
SystemUpdate { barWindow: bar }
}
RowLayout {
@@ -46,12 +55,13 @@ PanelWindow {
rightMargin: Appearance.spacing
}
spacing: Appearance.spacing
//Tray {}
PinnedTray {}
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

@@ -7,7 +7,15 @@ import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 4
// 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
@@ -71,6 +79,7 @@ Item {
bonded: !!d.bonded,
trusted: !!d.trusted,
pairing: !!d.pairing,
state: d.state,
batteryAvailable: !!d.batteryAvailable,
battery: d.battery || 0
}
@@ -97,19 +106,107 @@ Item {
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) d.connect()
else d.pair()
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) d.disconnect()
if (!d) return
root.pendingAction = { address: row.address, kind: "disconnect" }
pendingTimeoutTimer.restart()
d.disconnect()
}
function forgetDevice(row) {
@@ -117,6 +214,18 @@ Item {
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
@@ -128,8 +237,10 @@ Item {
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamily
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
@@ -163,6 +274,18 @@ Item {
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
@@ -173,14 +296,21 @@ Item {
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` hides the forget (x) button for
// discovered-but-unpaired devices, which have nothing to forget yet.
// 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
@@ -189,9 +319,47 @@ Item {
width: parent ? parent.width : 0
radius: 0
implicitHeight: rowContent.implicitHeight + 12
color: devRow.dev.connected
? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18)
: (rowMouse.containsMouse ? Colors.primary : "transparent")
// 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
@@ -207,7 +375,7 @@ Item {
Layout.maximumWidth: 20
horizontalAlignment: Text.AlignLeft
text: devRow.dev.icon
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
@@ -229,6 +397,17 @@ Item {
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"
@@ -242,17 +421,100 @@ Item {
}
}
// 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 {
id: forgetIcon
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
text: "close"
font.family: Appearance.iconFontFamily
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
@@ -261,22 +523,16 @@ Item {
height: rowContent.height + 12
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
// forgetIcon has no MouseArea of its own — nested inside
// rowContent, which sits underneath this full-row area in
// stacking order, so a separate MouseArea there would just be
// shadowed (see the identical fix in NetworkIndicator.qml's
// NetworkRow). Dispatch by click position against this single
// area instead.
onClicked: (mouse) => {
if (devRow.showForget && forgetIcon.visible) {
const forgetLeft = devRow.mapFromItem(forgetIcon, 0, 0).x
if (mouse.x >= forgetLeft - 8) {
root.forgetDevice(devRow.dev)
return
// 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()
}
}
devRow.activate()
}
}
}
@@ -286,7 +542,11 @@ Item {
id: panel
visible: false
color: "transparent"
readonly property int contentWidth: 244
// 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
@@ -335,9 +595,9 @@ Item {
Text {
text: label.text
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 30
font.pixelSize: 21
color: Colors.foreground
}
@@ -350,6 +610,29 @@ Item {
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
@@ -389,7 +672,10 @@ Item {
color: Colors.muted
}
Column {
width: parent.width
// 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
@@ -403,6 +689,42 @@ Item {
}
}
// 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
@@ -412,9 +734,9 @@ Item {
color: Colors.muted
}
// Paired/Available — capped height, scrolls once there are
// more devices than fit rather than growing the popup
// indefinitely.
// 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
@@ -427,35 +749,19 @@ Item {
Column {
id: deviceListColumn
width: deviceListFlick.width
// 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.knownRows.length > 0
Text {
text: "Paired Devices"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Column {
width: parent.width
spacing: 2
Repeater {
model: root.knownRows
DeviceRow {
required property var modelData
dev: modelData
showForget: true
onActivate: root.connectDevice(dev)
}
}
}
}
Column {
width: parent.width
spacing: 4

View File

@@ -0,0 +1,470 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Mpris
import "../../config"
// Now-playing label. A playing player always wins; otherwise the label
// sticks to whichever player was last playing, so pausing keeps the track
// in the bar (dimmed). Hidden once that player stops or quits.
//
// Left-click opens a player panel (art, seek bar, transport, shuffle/repeat);
// middle-click toggles play/pause; right-click skips to the next track.
Item {
id: root
implicitWidth: row.implicitWidth
implicitHeight: row.implicitHeight
// The bar's own PanelWindow, passed in from Bar.qml — the panel anchors
// to the bar so it sits flush under it rather than under this label's
// (shorter) text box.
required property var barWindow
// Set by Bar.qml on narrow screens — shortens the label.
property bool compact: false
// Last player seen playing. Becomes null by itself if that player's
// process exits and Quickshell destroys the object.
property var lastPlayer: null
readonly property var player: {
const players = Mpris.players.values
for (let i = 0; i < players.length; i++) {
if (players[i].isPlaying)
return players[i]
}
if (lastPlayer && lastPlayer.playbackState === MprisPlaybackState.Paused)
return lastPlayer
// Nothing has played since the shell started (e.g. right after a
// reload) — fall back to any paused player that has a track loaded.
for (let i = 0; i < players.length; i++) {
if (players[i].playbackState === MprisPlaybackState.Paused && players[i].trackTitle)
return players[i]
}
return null
}
readonly property bool paused: player !== null && !player.isPlaying
// --- Per-source bar icon ---
// First match wins. `app` is tested against the player's identity +
// desktop entry; `url` against the track's URL, which is how a browser
// tab gets told apart (YouTube vs Twitch...) since the player itself is
// just "Firefox"/"Zen" either way. `glyph` is a Nerd Font code point —
// look new ones up at https://www.nerdfonts.com/cheat-sheet.
readonly property var sources: [
{ name: "YouTube", url: /youtube\.com|youtu\.be/i, glyph: 0xf16a, color: "#ff0033" },
{ name: "Twitch", url: /twitch\.tv/i, glyph: 0xf0543, color: "#9146ff" },
{ name: "SoundCloud", url: /soundcloud\.com/i, glyph: 0xf1be, color: "#ff5500" },
{ name: "Spotify", app: /spotify/i, url: /open\.spotify\.com/i, glyph: 0xf1bc, color: "#1db954" },
{ name: "Plexamp", app: /plex/i, glyph: 0xf06ba, color: "#e5a00d" },
{ name: "VLC", app: /vlc/i, glyph: 0xf057c, color: "#ff8800" }
]
// Anything unmatched: a plain music note in the theme colour.
readonly property var defaultSource: ({ name: "Audio", glyph: 0xf075a, color: Colors.primary })
readonly property var source: {
if (!player)
return defaultSource
const app = [player.identity, player.desktopEntry].join(" ")
const url = String((player.metadata && player.metadata["xesam:url"]) || "")
return sources.find(s => (s.app && s.app.test(app)) || (s.url && s.url.test(url))) || defaultSource
}
onPlayerChanged: {
if (player && player.isPlaying)
lastPlayer = player
if (!player)
panel.visible = false
}
visible: player !== null
function formatTime(seconds) {
if (!(seconds > 0))
return "0:00"
const s = Math.floor(seconds)
const m = Math.floor(s / 60)
const h = Math.floor(m / 60)
const ss = String(s % 60).padStart(2, "0")
return h > 0 ? h + ":" + String(m % 60).padStart(2, "0") + ":" + ss : m + ":" + ss
}
RowLayout {
id: row
spacing: 6
// Source logo in its brand colour; greyed out while paused.
Text {
font.family: Appearance.brandIconFontFamily
font.pixelSize: 14
color: root.paused ? Colors.muted : root.source.color
text: String.fromCodePoint(root.source.glyph)
}
// CJK characters pull in a fallback font with a taller line box
// (16px vs 13px, baseline 2.5px lower), and centring that taller box
// dropped Japanese titles ~2px below ActiveWindow's text. So the label
// sits in a box sized from the primary font alone, positioned so its
// baseline lands where a plain-Latin label's would.
FontMetrics { id: labelMetrics; font: label.font }
Item {
implicitWidth: label.implicitWidth
implicitHeight: Math.ceil(labelMetrics.height)
Text {
id: label
y: labelMetrics.ascent - baselineOffset
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: root.paused ? Colors.muted : Colors.foreground
text: {
if (!root.player)
return ""
const title = root.player.trackTitle || root.player.identity
const artist = root.player.trackArtist
return Appearance.truncate(artist ? artist + " — " + title : title, root.compact ? Appearance.mediaMaxCharsCompact : Appearance.mediaMaxChars)
}
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => {
if (!root.player)
return
if (mouse.button === Qt.LeftButton) {
// Line the panel's left edge up with this label. Read at
// click time since the label's x shifts with the workspace
// count and isn't something the anchor can bind to.
panel.anchor.rect.x = root.mapToItem(null, 0, 0).x
panel.visible = !panel.visible
grab.active = panel.visible
} else if (mouse.button === Qt.MiddleButton) {
if (root.player.canTogglePlaying)
root.player.togglePlaying()
} else if (root.player.canGoNext) {
root.player.next()
}
}
}
// MPRIS doesn't push position updates while playing (only on seeks and
// state changes), so poll it — only while the panel is actually showing.
Timer {
running: panel.visible && root.player !== null && root.player.isPlaying
interval: 1000
repeat: true
onTriggered: root.player.positionChanged()
}
// --- Reusable pieces -----------------------------------------------
// Same look as Volume.qml's VolumeSlider. While dragging it shows the
// drag position rather than `value`, so a seek bar doesn't fight the
// player's own position updates mid-drag; `committed` fires on release.
component MediaSlider: Item {
id: sliderRoot
property real value: 0
property bool enabled: true
property real dragValue: -1
readonly property real shownValue: dragValue >= 0 ? dragValue : value
signal moved(real newValue)
signal committed(real newValue)
implicitHeight: 16
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 4
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.shownValue))
height: 4
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.shownValue - width / 2))
}
MouseArea {
anchors.fill: parent
enabled: sliderRoot.enabled
cursorShape: Qt.PointingHandCursor
function valueAt(mx) {
return Math.max(0, Math.min(1, mx / track.width))
}
onPressed: (mouse) => {
sliderRoot.dragValue = valueAt(mouse.x)
sliderRoot.moved(sliderRoot.dragValue)
}
onPositionChanged: (mouse) => {
if (!pressed)
return
sliderRoot.dragValue = valueAt(mouse.x)
sliderRoot.moved(sliderRoot.dragValue)
}
onReleased: {
sliderRoot.committed(sliderRoot.dragValue)
sliderRoot.dragValue = -1
}
}
}
component IconButton: Item {
id: btn
property string icon: ""
property int size: 22
property bool enabled: true
// Filled = the big primary-coloured play/pause button.
property bool filled: false
// Active = a toggle that's switched on (shuffle/repeat), tinted primary.
property bool active: false
signal clicked()
implicitWidth: size + 16
implicitHeight: size + 16
opacity: enabled ? 1 : 0.35
Rectangle {
anchors.fill: parent
radius: btn.filled ? width / 2 : Appearance.radiusSmall
color: btn.filled ? Colors.primary
: btnMouse.containsMouse ? Qt.rgba(Colors.foreground.r, Colors.foreground.g, Colors.foreground.b, 0.12)
: "transparent"
Behavior on color { ColorAnimation { duration: Appearance.animationDuration } }
}
Text {
anchors.centerIn: parent
text: btn.icon
font.family: Appearance.iconFontFamilyRounded
// Filled glyphs read better than outlines at transport-button size.
font.variableAxes: ({ "FILL": 1, "wght": 400, "GRAD": 0, "opsz": 24 })
font.pixelSize: btn.size
color: btn.filled ? Colors.primaryText : btn.active ? Colors.primary : Colors.foreground
}
MouseArea {
id: btnMouse
anchors.fill: parent
enabled: btn.enabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: btn.clicked()
}
}
// --- Panel -----------------------------------------------------------
PopupWindow {
id: panel
visible: false
color: "transparent"
readonly property int artSize: 160
readonly property int contentWidth: 470
readonly property int contentPadding: 16
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: Math.max(artSize, infoColumn.implicitHeight) + contentPadding * 2
anchor {
window: root.barWindow
// rect.x is set on click (see the MouseArea above).
rect.y: 0
rect.width: 1
rect.height: root.barWindow.height
edges: Edges.Bottom | Edges.Left
gravity: Edges.Bottom | Edges.Right
margins.bottom: -4
}
HyprlandFocusGrab {
id: grab
windows: [panel]
onCleared: panel.visible = false
}
Rectangle {
anchors.fill: parent
color: Colors.surface
border.color: Colors.muted
border.width: 3
RowLayout {
anchors.fill: parent
anchors.margins: panel.contentPadding
spacing: 16
// Album art, or a placeholder note when the player has none.
Rectangle {
Layout.preferredWidth: panel.artSize
Layout.preferredHeight: panel.artSize
Layout.alignment: Qt.AlignTop
color: Colors.background
Text {
anchors.centerIn: parent
visible: art.status !== Image.Ready
text: "music_note"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 56
color: Colors.muted
}
Image {
id: art
anchors.fill: parent
source: root.player ? root.player.trackArtUrl : ""
fillMode: Image.PreserveAspectCrop
sourceSize.width: panel.artSize * 2
sourceSize.height: panel.artSize * 2
asynchronous: true
smooth: true
}
}
ColumnLayout {
id: infoColumn
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 8
// Track info
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
visible: text !== ""
text: root.player ? root.player.trackArtist : ""
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
color: Colors.foreground
}
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
text: root.player ? (root.player.trackTitle || root.player.identity) : ""
font.family: Appearance.fontFamily
font.pixelSize: 15
font.bold: true
color: Colors.foreground
}
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
visible: text !== ""
text: root.player ? root.player.trackAlbum : ""
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
// Seek bar
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 4
spacing: 8
visible: root.player !== null && root.player.lengthSupported && root.player.length > 0
Text {
Layout.preferredWidth: 36
horizontalAlignment: Text.AlignRight
text: root.player ? root.formatTime(seekSlider.dragValue >= 0
? seekSlider.dragValue * root.player.length : root.player.position) : ""
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
MediaSlider {
id: seekSlider
Layout.fillWidth: true
enabled: root.player !== null && root.player.canSeek && root.player.positionSupported
value: root.player && root.player.length > 0 ? root.player.position / root.player.length : 0
onCommitted: (v) => { if (root.player) root.player.position = v * root.player.length }
}
Text {
Layout.preferredWidth: 36
text: root.player ? root.formatTime(root.player.length) : ""
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
// Transport
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 4
IconButton {
icon: "shuffle"
size: 18
visible: root.player !== null && root.player.shuffleSupported
active: root.player !== null && root.player.shuffle
onClicked: root.player.shuffle = !root.player.shuffle
}
IconButton {
icon: "replay_10"
size: 18
visible: root.player !== null && root.player.canSeek
onClicked: root.player.seek(-10)
}
IconButton {
icon: "skip_previous"
enabled: root.player !== null && root.player.canGoPrevious
onClicked: root.player.previous()
}
IconButton {
icon: root.paused ? "play_arrow" : "pause"
size: 26
filled: true
enabled: root.player !== null && root.player.canTogglePlaying
onClicked: root.player.togglePlaying()
}
IconButton {
icon: "skip_next"
enabled: root.player !== null && root.player.canGoNext
onClicked: root.player.next()
}
IconButton {
icon: "forward_10"
size: 18
visible: root.player !== null && root.player.canSeek
onClicked: root.player.seek(10)
}
// Cycles off -> repeat all -> repeat one -> off.
IconButton {
icon: root.player && root.player.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
size: 18
visible: root.player !== null && root.player.loopSupported
active: root.player !== null && root.player.loopState !== MprisLoopState.None
onClicked: root.player.loopState =
root.player.loopState === MprisLoopState.None ? MprisLoopState.Playlist
: root.player.loopState === MprisLoopState.Playlist ? MprisLoopState.Track
: MprisLoopState.None
}
}
}
}
}
}
}

View File

@@ -44,7 +44,13 @@ Item {
return null
}
// Only scan for networks while the panel is actually open.
// Only scan for networks while the panel is actually open. Deliberately
// NOT paused while a password prompt is up: scannerEnabled: false stops
// NetworkManager from refreshing its AP cache at all, so the existing
// list quietly ages out with nothing to replace it — the fix for typing
// being interrupted by scan-driven refreshes lives below instead (the
// password field itself is pulled out of the repeated row so refreshes
// can't touch it), not here.
Binding {
target: root.wifiDevice
property: "scannerEnabled"
@@ -84,6 +90,24 @@ Item {
? sortWifiRows(root.wifiDevice.networks.values.map(root.wifiRow).filter(function(r) { return r && r.ssid }))
: []
// Quickshell's WifiDevice has no "a scan is in flight" signal — only
// scannerEnabled, the on/off request, which stays true for as long as
// the panel is open rather than pulsing per scan. wifiRows re-deriving
// to a new array (as it does on every actual network-list change — see
// the comment on wifiRow() above) is the closest available proxy for
// "a scan just landed", so pulse briefly off that instead.
property bool wifiJustScanned: false
onWifiRowsChanged: {
root.wifiJustScanned = true
wifiScanPulseTimer.restart()
}
Timer {
id: wifiScanPulseTimer
interval: 1200
onTriggered: root.wifiJustScanned = false
}
// Split for the "Known Networks" / "Other Networks" sections. wifiRows
// is already sorted connected-first then by signal, so filtering
// preserves that order within each group.
@@ -135,8 +159,17 @@ Item {
}
}
// SSID of the row currently showing its inline password field, if any.
// SSID awaiting a password, if any. The input itself lives in a single
// persistent widget below the network list (see passwordEntryInput),
// not inside the per-row NetworkRow delegate — wifiRows is rebuilt from
// scratch on every scan result, and a Repeater over a plain array has no
// way to diff that, so it destroys and recreates every delegate each
// time. Keeping the field outside that churn is what lets typing survive
// a scan landing mid-keystroke.
property string passwordSsid: ""
// Cleared whenever the target SSID changes so stale text from a
// cancelled/previous prompt never carries over to the next one.
onPasswordSsidChanged: passwordEntryInput.text = ""
// Wired connections don't have a user-facing "name" on the device or
// Network object — those just expose the interface name (e.g. "eth0").
@@ -161,6 +194,14 @@ Item {
return (row && row.ssid) || "Network"
}
// Signal strength of the currently-connected WiFi network, if any — lets
// the bar icon show the same wifi-fan glyph (via wifiIcon()) as the
// connected row inside the popup, instead of a generic "wifi" icon.
readonly property int connectedWifiSignal: {
const row = root.wifiRows.find(function(r) { return r.connected })
return row ? row.signal : 0
}
function activateNetwork(net) {
if (net.connected) return
if (net.known || !root.requiresCredentials(net.security)) {
@@ -171,6 +212,92 @@ Item {
}
}
function submitPassword() {
const n = root.networkForSsid(root.passwordSsid)
if (n && passwordEntryInput.text.length > 0) n.connectWithPsk(passwordEntryInput.text)
root.passwordSsid = ""
}
function cancelPassword() {
root.passwordSsid = ""
}
// --- Captive portal ---------------------------------------------------
// NetworkManager tells "portal" (a walled-garden sign-in page sits
// between us and the real internet) apart from "full" connectivity only
// via an active probe — nmcli's cached CONNECTIVITY field just reflects
// NM's own internal check, which runs every few minutes and is far too
// slow to catch a portal right after joining a network. So this forces a
// fresh probe itself, same shell-out approach as the stats below, and
// runs it whenever we're connected — not gated to panel.visible like
// those — since the whole point is noticing this before the popup is
// ever opened.
//
// "limited" is treated the same as "portal", not just "full" vs.
// everything-else: NM only reports "portal" when the probe request
// actually comes back *different* (redirected/rewritten) from what it
// expected. Plenty of real captive portals instead black-hole every
// request that isn't to their own login domain, so the probe just times
// out — which is also why this is slow to resolve — and NM reports
// "limited" (it got a route/gateway, but the internet check itself
// never landed) rather than "portal". From the user's side both look
// identical: connected, but no real internet.
property string connectivityState: "unknown"
readonly property bool connectivityRestricted: root.connectivityState === "portal" || root.connectivityState === "limited"
// A plain, unencrypted HTTP page whose only job is to be interceptable
// by a captive portal's redirect. An https:// URL mostly just gets
// silently blocked instead of redirected, which looks identical to the
// portal not existing at all.
readonly property string captivePortalCheckUrl: "http://neverssl.com/"
// Auto-opened once per network join (mirrors TailscaleIndicator's
// openAuthUrl/_authUrlOpened) so flapping connectivity on the same
// network doesn't keep popping a new browser tab — the manual "Open"
// button in the panel banner below still works regardless.
property bool _portalAutoOpened: false
property bool _wasConnected: false
onConnectedDeviceChanged: {
const isConnected = root.connectedDevice !== null
if (isConnected && !root._wasConnected) {
root._portalAutoOpened = false
root.connectivityState = "unknown"
}
root._wasConnected = isConnected
}
onConnectivityRestrictedChanged: {
if (root.connectivityRestricted && !root._portalAutoOpened)
root.openCaptivePortal()
}
function openCaptivePortal() {
root._portalAutoOpened = true
Qt.openUrlExternally(root.captivePortalCheckUrl)
}
Process {
id: connectivityProc
command: ["nmcli", "networking", "connectivity", "check"]
stdout: StdioCollector {
id: connectivityCollector
onStreamFinished: root.connectivityState = connectivityCollector.text.trim()
}
}
Timer {
id: connectivityTimer
// Poll quickly while things are still unresolved (fresh join, or an
// active portal we're waiting to clear) and back off once
// connectivity is confirmed full, rather than shelling out every
// few seconds forever for no reason.
interval: root.connectivityState === "full" ? 20000 : 3000
repeat: true
triggeredOnStart: true
running: root.connectedDevice !== null
onTriggered: if (!connectivityProc.running) connectivityProc.running = true
}
// --- Live stats --------------------------------------------------
// None of this comes from Quickshell's Networking API — it only knows
// about devices/networks, not throughput, ping, or routing. So this
@@ -448,6 +575,40 @@ Item {
}
}
// Public (WAN-side) IP — nothing local knows this, the address on the
// interface is the LAN one behind NAT, so it has to come from an
// outside service. Polled much more slowly than the local stats since
// it's a real HTTP request and rarely changes.
property string publicIp: "—"
Process {
id: publicIpProc
command: ["curl", "-s", "--max-time", "5", "https://api.ipify.org"]
stdout: StdioCollector {
id: publicIpCollector
onStreamFinished: {
const ip = publicIpCollector.text.trim()
root.publicIp = /^[\d.]+$|^[0-9a-fA-F:]+$/.test(ip) ? ip : "—"
}
}
}
// Switching networks almost always changes the public IP, so don't
// leave the old one up until the next slow poll.
onConnectedNetworkNameChanged: {
root.publicIp = "—"
if (panel.visible && root.connectedDevice !== null && !publicIpProc.running)
publicIpProc.running = true
}
Timer {
interval: 60000
repeat: true
triggeredOnStart: true
running: panel.visible && root.connectedDevice !== null
onTriggered: if (!publicIpProc.running) publicIpProc.running = true
}
Process {
id: bytesProc
command: root.ifaceName
@@ -517,14 +678,58 @@ Item {
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: root.connectedDevice ? Colors.foreground : Colors.muted
// Only Wired renders its own distinct glyph; disconnected uses
// "signal_wifi_off" — the actual off companion to the
// network_wifi/signal_wifi fan family used below (not "wifi_off",
// which belongs to a visually different, newer glyph family and
// looked inconsistent next to the connected bars) — so it needs the
// same vertical-stretch correction as the rest of that family.
readonly property bool isWifiFan: root.connectedDevice === null || root.connectedDevice.type !== DeviceType.Wired
text: {
if (!root.connectedDevice)
return "wifi_off"
return root.connectedDevice.type === DeviceType.Wired ? "settings_ethernet" : "wifi"
return "signal_wifi_off"
if (root.connectedDevice.type === DeviceType.Wired)
return "settings_ethernet"
return root.wifiIcon(root.connectedWifiSignal)
}
// Same correction as the matching glyph inside the popup's
// NetworkRow — the wifi-fan family is inherently flat/wide.
transform: Scale {
origin.x: label.width / 2
origin.y: label.height / 2
yScale: label.isWifiFan ? 1.2 : 1.0
}
}
// Small warning badge over the icon's corner when the connection looks
// restricted (a captive portal, confirmed or just suspected — see
// connectivityRestricted above) — same treatment as TailscaleIndicator's
// login-needed badge, so "this needs your attention" reads the same way
// across indicators.
Rectangle {
visible: root.connectivityRestricted
width: Math.max(7, label.font.pixelSize * 0.42)
height: width
radius: width / 2
color: Colors.urgent
border.color: Colors.surface
border.width: 1
anchors.right: label.right
anchors.bottom: label.bottom
anchors.rightMargin: -1
anchors.bottomMargin: -1
Text {
anchors.centerIn: parent
text: "!"
color: Colors.surface
font.family: Appearance.fontFamily
font.pixelSize: Math.max(6, parent.height * 0.75)
font.bold: true
}
}
@@ -552,6 +757,18 @@ Item {
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. Networking.wifiEnabled), 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
@@ -562,7 +779,9 @@ Item {
Behavior on x { NumberAnimation { duration: Appearance.animationDuration } }
}
MouseArea {
id: toggleMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: toggleRoot.toggled()
}
@@ -572,18 +791,32 @@ Item {
id: netRow
required property var net
signal activate()
// Still tracked for the highlight below — the actual input lives
// outside this (repeatedly rebuilt) delegate, in the persistent
// password entry widget under the network list.
readonly property bool passwordOpen: root.passwordSsid === netRow.net.ssid
width: parent ? parent.width : 0
radius: 0
implicitHeight: rowContent.implicitHeight + 12 + (passwordOpen ? passwordRow.implicitHeight + 6 : 0)
color: netRow.net.connected
? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18)
: (rowMouse.containsMouse ? Colors.primary : "transparent")
implicitHeight: rowContent.implicitHeight + 12
// Hover checked first (same ordering as Volume.qml's DeviceRow):
// hovering always shows a solid Colors.primary fill, even for an
// already-active row, so the primaryText used below never has to
// sit on top of the dimmer translucent tint instead.
color: rowMouse.containsMouse ? Colors.primary
: ((netRow.net.connected || netRow.passwordOpen) ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
function submit() {
const n = root.networkForSsid(netRow.net.ssid)
if (n && passwordInput.text.length > 0) n.connectWithPsk(passwordInput.text)
root.passwordSsid = ""
// Hover-only outline, offset outside the row rather than drawn on
// its own edge — same treatment as ToggleSwitch's halo and the DNS
// pills. networkListColumn insets its rows by 3px on each side
// specifically to give this room without getting cropped by the
// Flickable's clip.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: rowMouse.containsMouse ? 1 : 0
}
Column {
@@ -600,11 +833,12 @@ Item {
Layout.preferredWidth: 20
Layout.minimumWidth: 20
Layout.maximumWidth: 20
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
text: root.wifiIcon(netRow.net.signal)
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
font.pixelSize: 16
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
// The wifi-fan glyph itself is flat/wide — stretch it
// vertically to read taller while keeping its width (and
@@ -639,7 +873,7 @@ Item {
Text {
visible: root.requiresCredentials(netRow.net.security)
text: "lock"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 13
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
@@ -648,59 +882,12 @@ Item {
id: forgetIcon
visible: netRow.net.known
text: "close"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
}
}
RowLayout {
id: passwordRow
visible: netRow.passwordOpen
width: parent.width
spacing: 6
Rectangle {
Layout.fillWidth: true
implicitHeight: 24
radius: 0
color: Colors.background
border.color: Colors.muted
border.width: 1
TextInput {
id: passwordInput
anchors.fill: parent
anchors.margins: 6
echoMode: TextInput.Password
color: Colors.foreground
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
clip: true
focus: netRow.passwordOpen
onAccepted: netRow.submit()
// Explicit fallback alongside onAccepted, which only
// fires when acceptableInput is true — covers both
// the main Return key and numpad Enter directly.
Keys.onReturnPressed: netRow.submit()
Keys.onEnterPressed: netRow.submit()
}
}
Text {
text: "check"
font.family: Appearance.iconFontFamily
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.primary
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: netRow.submit()
}
}
}
}
MouseArea {
@@ -786,16 +973,25 @@ Item {
spacing: 8
Text {
id: headerIcon
text: label.text
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 30
color: Colors.foreground
// Mirrors the bar icon's own correction — see label's
// isWifiFan/transform above — since this just
// displays the same text at a larger size.
transform: Scale {
origin.x: headerIcon.width / 2
origin.y: headerIcon.height / 2
yScale: label.isWifiFan ? 1.2 : 1.0
}
}
Text {
Layout.fillWidth: true
text: root.connectedNetworkName
text: "Network"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: 18
@@ -804,6 +1000,137 @@ Item {
}
}
// Row rather than RowLayout — RowLayout is built for
// constraint-based stretching (Layout.fillWidth etc.), and
// without either Text opting into that, its sizing pass can
// still leave more room between them than spacing:0 implies.
// Row has no such stretch logic: spacing is exactly the gap
// you get, so these two sit flush (plus one small gap) no
// matter what width is bound above them.
Row {
width: parent.width
spacing: 4
Text {
text: "Connected Interface:"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Text {
text: root.connectedNetworkName
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.foreground
font.bold: true
}
}
Row {
width: parent.width
spacing: 4
visible: root.connectedDevice !== null
Text {
text: "Public IP:"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Text {
text: root.publicIp
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.foreground
font.bold: true
}
}
// Captive portal banner — shown when the connectivity probe
// above finds either a confirmed sign-in page or a
// "limited" result standing between this network and the
// real internet (see connectivityRestricted). openCaptivePortal()
// already fired once automatically when this was first
// detected; this stays up as a manual retry in case that tab
// got closed.
Rectangle {
width: parent.width
visible: root.connectivityRestricted
radius: 0
color: Qt.rgba(Colors.urgent.r, Colors.urgent.g, Colors.urgent.b, 0.18)
border.color: Colors.urgent
border.width: 1
implicitHeight: portalBannerRow.implicitHeight + 12
RowLayout {
id: portalBannerRow
anchors.fill: parent
anchors.margins: 6
spacing: 8
Text {
text: "lock_open"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.urgent
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
text: root.connectivityState === "portal" ? "Sign-in required" : "No internet access"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
font.bold: true
color: Colors.foreground
}
Text {
Layout.fillWidth: true
text: root.connectivityState === "portal"
? "This network needs a web sign-in first"
: "This network may need a web sign-in first"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
Rectangle {
implicitWidth: openPortalText.implicitWidth + 16
implicitHeight: 24
radius: 0
color: openPortalMouse.containsMouse ? Colors.urgent : "transparent"
border.color: Colors.urgent
border.width: 1
Text {
id: openPortalText
anchors.centerIn: parent
text: "Open"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: openPortalMouse.containsMouse ? Colors.primaryText : Colors.urgent
}
MouseArea {
id: openPortalMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openCaptivePortal()
}
}
}
}
// Live stats — ping/loss/throughput/IP/gateway for whichever
// device is currently connected.
GridLayout {
@@ -863,6 +1190,18 @@ Item {
border.width: 1
opacity: root.dnsBusy ? 0.6 : 1
// Hover-only outline, offset outside the pill
// itself rather than drawn on its own border —
// same treatment as ToggleSwitch's halo.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: dnsMouse.containsMouse ? 1 : 0
}
Text {
anchors.centerIn: parent
text: dnsPill.modelData
@@ -936,7 +1275,7 @@ Item {
Text {
text: "check"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.primary
@@ -974,7 +1313,7 @@ Item {
spacing: 8
Text {
text: "settings_ethernet"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: Colors.foreground
@@ -1025,6 +1364,32 @@ Item {
font.pixelSize: Appearance.fontSize
color: Colors.foreground
}
// Spins briefly each time a scan actually lands —
// same "still searching" cue as
// BluetoothIndicator.qml's spinner, but pulsed via
// wifiJustScanned (see above) rather than staying on
// for the whole time the panel is open, since
// WifiDevice has no per-scan signal of its own to
// key off.
Text {
id: wifiScanSpinner
visible: root.wifiDevice !== null && root.wifiDevice.scannerEnabled && root.wifiJustScanned
text: "progress_activity"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.muted
RotationAnimation on rotation {
running: wifiScanSpinner.visible
loops: Animation.Infinite
from: 0
to: 360
duration: 900
}
}
ToggleSwitch {
checked: Networking.wifiEnabled
onToggled: Networking.wifiEnabled = !Networking.wifiEnabled
@@ -1049,6 +1414,97 @@ Item {
color: Colors.muted
}
// Password entry — a single persistent widget rather
// than living inside the NetworkRow delegate for
// root.passwordSsid, so a wifi scan landing mid-keystroke
// (which rebuilds the whole Known/Other Networks list
// below) can't destroy the focused TextInput and wipe
// out what's been typed. See passwordSsid's own comment.
Rectangle {
width: parent.width
visible: root.passwordSsid !== ""
radius: 0
color: Colors.background
border.color: Colors.muted
border.width: 1
implicitHeight: passwordEntryColumn.implicitHeight + 12
Column {
id: passwordEntryColumn
anchors.fill: parent
anchors.margins: 6
spacing: 6
Text {
width: parent.width
text: "Password for “" + root.passwordSsid + "”"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
RowLayout {
width: parent.width
spacing: 6
Rectangle {
Layout.fillWidth: true
implicitHeight: 24
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 1
TextInput {
id: passwordEntryInput
anchors.fill: parent
anchors.margins: 6
echoMode: TextInput.Password
color: Colors.foreground
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
clip: true
focus: root.passwordSsid !== ""
onAccepted: root.submitPassword()
// Explicit fallback alongside onAccepted, which only
// fires when acceptableInput is true — covers both
// the main Return key and numpad Enter directly.
Keys.onReturnPressed: root.submitPassword()
Keys.onEnterPressed: root.submitPassword()
Keys.onEscapePressed: root.cancelPassword()
}
}
Text {
text: "check"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.primary
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.submitPassword()
}
}
Text {
text: "close"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.muted
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.cancelPassword()
}
}
}
}
}
// Known/Other Networks — capped height, scrolls once
// there are more networks than fit rather than growing
// the popup indefinitely.
@@ -1064,7 +1520,19 @@ Item {
Column {
id: networkListColumn
width: networkListFlick.width
// Inset 3px on every side (rather than flush with
// the Flickable) so NetworkRow's hover halo below
// has room to sit outside the row without being
// cropped by the Flickable's own 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 the
// Flickable's contentHeight tracks it.
x: 3
width: networkListFlick.width - 6
topPadding: 3
bottomPadding: 3
spacing: 6
Column {

View File

@@ -0,0 +1,110 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config"
RowLayout {
id: root
spacing: Appearance.spacing * 1.
readonly property var pinned: [
{
name: "Bitwarden",
icon: Qt.resolvedUrl("../../assets/bitwarden.png"),
binary: "bitwarden",
launch: ["bitwarden"],
secondary: ["pkill", "bitwarden"]
},
{
name: "Seafile",
icon: "seafile",
binary: "seafile-applet",
launch: ["seafile-applet"],
secondary: ["pkill", "seafile-applet"]
},
{
name: "Sunshine",
icon: Qt.resolvedUrl("../../assets/sunshine.svg"),
binary: "sunshine",
launch: ["systemctl", "--user", "start", "app-dev.lizardbyte.app.Sunshine.service"],
secondary: ["xdg-open", "https://xol.twhomelab.tailnet:47990"],
},
]
function findTrayItem(name) {
const needle = name.toLowerCase()
for (const item of SystemTray.items.values) {
if (item.id.toLowerCase().includes(needle) || item.title.toLowerCase().includes(needle))
return item
}
return null
}
Repeater {
model: root.pinned
delegate: Item {
id: slot
required property var modelData
readonly property var trayItem: root.findTrayItem(modelData.name)
readonly property bool running: trayItem !== null
property bool installed: false // Application installation check
visible: slot.installed
implicitWidth: Appearance.iconSize
implicitHeight: Appearance.iconSize
Process {
running: true
command: ["which", slot.modelData.binary]
onExited: (exitCode) => slot.installed = exitCode === 0
}
IconImage {
anchors.fill: parent
// modelData.icon is a QUrl when it comes from
// Qt.resolvedUrl() (Bitwarden/Sunshine's local assets), not
// a QString — coerce to string before using String methods
// on it, or this throws instead of ever matching.
source: slot.running
? slot.trayItem.icon
: (String(slot.modelData.icon).includes("://")
? slot.modelData.icon
: Quickshell.iconPath(slot.modelData.icon))
opacity: slot.running ? 1.0 : 0.5
}
Process { id: launcher }
Process { id: secondaryProc }
MouseArea {
anchors.fill: parent
anchors.margins: -4
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
if (slot.modelData.secondary) {
if (!secondaryProc.running) {
secondaryProc.command = slot.modelData.secondary
secondaryProc.running = true
}
} else if (slot.running) {
slot.trayItem.secondaryActivate()
}
} else if (slot.running) {
slot.trayItem.activate()
} else if (!launcher.running) {
launcher.command = slot.modelData.launch
launcher.running = true
}
}
}
}
}
}

View File

@@ -7,7 +7,7 @@ import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 4
implicitWidth: label.implicitWidth + 2
implicitHeight: Appearance.barHeight
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
@@ -19,11 +19,11 @@ Item {
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.foreground
text: "power_settings_new"
text: "settings_power"
}
MouseArea {
@@ -41,10 +41,12 @@ Item {
id: menu
visible: false
color: "transparent"
implicitWidth: 210
// Same padding as Volume.qml's panel.
// 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
readonly property int labelOffset: -135
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
anchor {
@@ -91,9 +93,9 @@ Item {
Text {
text: label.text
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 30
font.pixelSize: 23
color: Colors.foreground
}
@@ -117,41 +119,60 @@ Item {
{ 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: "Log out", icon: "logout", cmd: ["hyprctl", "dispatch", "hl.dsp.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
height: 40
radius: Appearance.radiusSmall
radius: 0
implicitHeight: entryContent.implicitHeight + 12
color: (!entry.modelData.disabled && entryMouse.containsMouse) ? Colors.primary : "transparent"
RowLayout {
Rectangle {
anchors.fill: parent
anchors.leftMargin: 8
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: 25
Layout.minimumWidth: 25
Layout.maximumWidth: 25
Layout.preferredWidth: 20
Layout.minimumWidth: 20
Layout.maximumWidth: 20
horizontalAlignment: Text.AlignLeft
text: entry.modelData.icon
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: entry.modelData.iconSize || 15
font.pixelSize: 14
opacity: entry.modelData.disabled ? 0.4 : 1
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
Text {
Layout.leftMargin: menu.labelOffset + (entry.modelData.textOffset || 0)
Layout.fillWidth: true
text: entry.modelData.label
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: 16
font.pixelSize: Appearance.fontSize
opacity: entry.modelData.disabled ? 0.4 : 1
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
}

View File

@@ -0,0 +1,194 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import "../../config"
Item {
id: root
implicitWidth: row.implicitWidth + 6
implicitHeight: Appearance.barHeight
required property var barWindow
RowLayout {
id: row
anchors.centerIn: parent
spacing: 4
Text {
id: label
font.family: SystemUpdates.checking ? Appearance.iconFontFamilyRounded : Appearance.fontFamily
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: SystemUpdates.checking ? 15 : 22
color: Colors.foreground
text: SystemUpdates.checking ? "progress_activity" : "󰏖"
RotationAnimation on rotation {
running: SystemUpdates.checking
loops: Animation.Infinite
from: 0
to: 360
duration: 900
onRunningChanged: if (!running) label.rotation = 0
}
}
Text {
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: label.color
text: SystemUpdates.updateCount
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
SystemUpdates.refresh()
} else {
panel.visible = !panel.visible
grab.active = panel.visible
}
}
}
PopupWindow {
id: panel
visible: false
color: "transparent"
readonly property int contentWidth: 200
readonly property int contentPadding: 16
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
anchor {
window: root.barWindow
rect.x: 0
rect.y: 0
rect.width: root.barWindow.width
rect.height: root.barWindow.height
edges: Edges.Bottom
gravity: Edges.Bottom
margins.bottom: -4
}
HyprlandFocusGrab {
id: grab
windows: [panel]
onCleared: panel.visible = false
}
Rectangle {
anchors.fill: parent
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 3
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: panel.contentPadding
spacing: 10
RowLayout {
width: parent.width
spacing: 8
Text {
text: label.text
font.family: Appearance.fontFamily
font.pixelSize: 23
color: Colors.foreground
}
Text {
Layout.fillWidth: true
text: "System Update"
font.family: Appearance.fontFamily
font.pixelSize: 18
font.bold: true
color: Colors.foreground
}
}
Text {
width: parent.width
wrapMode: Text.WordWrap
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
color: Colors.foreground
text: {
if (SystemUpdates.updating) return "Update running in a terminal…"
if (SystemUpdates.checking) return "Checking for updates…"
if (SystemUpdates.updateCount === 0) return "System is up to date."
return SystemUpdates.updateCount + (SystemUpdates.updateCount === 1 ? " update available." : " updates available.")
}
}
Rectangle {
id: updateBtn
width: parent.width
radius: 0
implicitHeight: btnContent.implicitHeight + 12
enabled: !SystemUpdates.updating
opacity: SystemUpdates.updating ? 0.5 : 1
color: btnMouse.containsMouse ? Colors.primary : "transparent"
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: btnMouse.containsMouse ? 1 : 0
}
RowLayout {
id: btnContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 6
spacing: 8
Text {
Layout.preferredWidth: 20
horizontalAlignment: Text.AlignLeft
text: "download"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: btnMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
Text {
Layout.fillWidth: true
text: "Update Now"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
color: btnMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
}
MouseArea {
id: btnMouse
anchors.fill: parent
enabled: !SystemUpdates.updating
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SystemUpdates.runUpdate(Quickshell.shellPath("scripts/update-system.sh"))
panel.visible = false
grab.active = false
}
}
}
}
}
}
}

View File

@@ -7,7 +7,7 @@ import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 4
implicitWidth: label.implicitWidth
implicitHeight: Appearance.barHeight
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
@@ -54,7 +54,7 @@ Item {
// Set when the VPN-exclusivity guard (below) forces Tailscale down —
// as opposed to you turning it off yourself — so it knows to bring
// Tailscale back up automatically once Mullvad/ProtonVPN disconnect,
// Tailscale back up automatically once the other VPN disconnects,
// without overriding a deliberate manual "off".
property bool _forcedDownByVpn: false
@@ -204,7 +204,7 @@ Item {
return
}
if (VpnExclusivity.commercialVpnActive) {
root.actionStatus = "Disconnect Mullvad/ProtonVPN first"
root.actionStatus = "Disconnect the active VPN first"
actionStatusTimer.restart()
return
}
@@ -432,8 +432,8 @@ Item {
model: [0, 0, 0, 1, 1, 1, 0, 1, 0]
Rectangle {
required property int modelData
width: logo.dotSize
height: logo.dotSize
width: logo.dotSize - 1
height: logo.dotSize - 1
radius: width / 2
color: modelData ? '#c9f0f0f0' : logo.dimColor
}
@@ -505,6 +505,18 @@ Item {
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. root.active), 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
@@ -515,7 +527,9 @@ Item {
Behavior on x { NumberAnimation { duration: Appearance.animationDuration } }
}
MouseArea {
id: toggleMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: toggleRoot.toggled()
}
@@ -533,9 +547,25 @@ Item {
width: parent ? parent.width : 0
radius: 0
implicitHeight: rowContent.implicitHeight + 12
color: exitRow.active
? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18)
: (rowMouse.containsMouse ? Colors.primary : "transparent")
// Hover checked first (same ordering as Volume.qml's DeviceRow):
// hovering always shows a solid Colors.primary fill, even for an
// already-active row, so the primaryText used below never has to
// sit on top of the dimmer translucent tint instead.
color: rowMouse.containsMouse ? Colors.primary
: (exitRow.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 NetworkIndicator.qml's NetworkRow.
// listColumn below is inset by 3px to give this room without being
// cropped by listFlick'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
@@ -551,7 +581,7 @@ Item {
Layout.maximumWidth: 20
horizontalAlignment: Text.AlignLeft
text: exitRow.subIcon
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
@@ -568,7 +598,7 @@ Item {
Text {
visible: exitRow.active
text: "check_circle"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.primary
@@ -595,9 +625,25 @@ Item {
width: parent ? parent.width : 0
radius: 0
implicitHeight: accountContent.implicitHeight + 12
color: accountRow.selected
? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18)
: (accountMouse.containsMouse ? Colors.primary : "transparent")
// Hover checked first (same ordering as Volume.qml's DeviceRow):
// hovering always shows a solid Colors.primary fill, even for an
// already-selected row, so the primaryText the texts below already
// assume on hover is always correct (this row had the same
// active-first ordering bug as ExitNodeRow, just never reported).
color: accountMouse.containsMouse ? Colors.primary
: (accountRow.selected ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
// Hover-only outline, offset outside the row — same treatment as
// ExitNodeRow/NetworkRow. Not inside a clipped Flickable, so no
// inset needed on its container.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: accountMouse.containsMouse ? 1 : 0
}
RowLayout {
id: accountContent
@@ -613,7 +659,7 @@ Item {
Layout.maximumWidth: 20
horizontalAlignment: Text.AlignLeft
text: "person"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: accountMouse.containsMouse ? Colors.primaryText : Colors.foreground
@@ -638,7 +684,7 @@ Item {
Text {
visible: accountRow.selected
text: "check_circle"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: accountMouse.containsMouse ? Colors.primaryText : Colors.primary
@@ -680,6 +726,18 @@ Item {
: (peerRow.expanded ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
implicitHeight: peerContent.implicitHeight + 12
// Hover-only outline, offset outside the row — same treatment as
// ExitNodeRow/NetworkRow. listColumn is inset by 3px to give this
// room without being cropped by listFlick's clip.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: summaryMouse.containsMouse ? 1 : 0
}
RowLayout {
id: peerContent
anchors.left: parent.left
@@ -694,7 +752,7 @@ Item {
Layout.maximumWidth: 20
horizontalAlignment: Text.AlignLeft
text: root.osIcon(peerRow.peer.os)
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: summaryMouse.containsMouse ? Colors.primaryText : Colors.foreground
@@ -709,7 +767,7 @@ Item {
}
Text {
text: peerRow.expanded ? "expand_less" : "content_copy"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: summaryMouse.containsMouse ? Colors.primaryText : Colors.muted
@@ -739,6 +797,17 @@ Item {
color: optionMouse.containsMouse ? Colors.primary : "transparent"
implicitHeight: optionRow.implicitHeight + 8
// Hover-only outline, offset outside the row — same
// treatment as the rest of this file's selectable rows.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: optionMouse.containsMouse ? 1 : 0
}
RowLayout {
id: optionRow
anchors.left: parent.left
@@ -838,7 +907,7 @@ Item {
spacing: 8
TailscaleLogo {
size: 30
size: 25
color: label.color
crossed: label.crossed
warning: label.warning
@@ -915,7 +984,7 @@ Item {
spacing: 8
Text {
text: "open_in_new"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: signInMouse.containsMouse ? Colors.primaryText : Colors.foreground
@@ -986,7 +1055,7 @@ Item {
Text {
visible: root.status.selfIp !== ""
text: "content_copy"
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 12
color: copyMouse.containsMouse ? Colors.primary : Colors.muted
@@ -1026,7 +1095,18 @@ Item {
Column {
id: listColumn
width: listFlick.width
// Inset 3px on every side so ExitNodeRow/PeerRow's
// hover halo has room without being cropped by
// listFlick'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: listFlick.width - 6
topPadding: 3
bottomPadding: 3
spacing: 6
Column {

View File

@@ -5,7 +5,7 @@ import Quickshell.Widgets
import "../../config"
RowLayout {
spacing: Appearance.spacing * 0.6
spacing: Appearance.spacing
Repeater {
model: SystemTray.items

View File

@@ -7,7 +7,7 @@ import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 4
implicitWidth: label.implicitWidth
implicitHeight: Appearance.barHeight
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
@@ -40,6 +40,12 @@ Item {
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.
@@ -67,27 +73,52 @@ Item {
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)
return blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1
if (blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1
|| blob.indexOf("earbud") !== -1 || blob.indexOf("earphone") !== -1
|| blob.indexOf("airpod") !== -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("bluetooth") !== -1) return "bluetooth_audio"
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("headset") !== -1) return "headphones"
if (blob.indexOf("bluetooth") !== -1) return "bluetooth_audio"
if (blob.indexOf("webcam") !== -1 || blob.indexOf("camera") !== -1) return "videocam"
return "mic"
}
@@ -95,9 +126,9 @@ Item {
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
font.pixelSize: 18
color: Colors.foreground
text: (root.outputMuted || root.outputVolume <= 0) ? "volume_off"
: (root.outputVolume < 0.5 ? "volume_down" : "volume_up")
@@ -106,15 +137,25 @@ Item {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
// Scroll distance is accumulated rather than stepping per event:
// a mouse wheel sends one 120-unit event per notch, but a touchpad
// sends a stream of tiny events, which at a fixed step per event
// made touchpad scrolling far too fast. One notch's worth (120) of
// scrolling = one volume step, regardless of input device.
property real wheelAccum: 0
readonly property real volumeStep: 0.05
readonly property real unitsPerStep: 120
onClicked: {
panel.visible = !panel.visible
grab.active = panel.visible
}
onWheel: (wheel) => {
if (!root.sink || !root.sink.audio) return
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))
wheelAccum += wheel.angleDelta.y
const steps = Math.trunc(wheelAccum / unitsPerStep)
if (steps === 0) return
wheelAccum -= steps * unitsPerStep
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + steps * volumeStep))
}
}
@@ -234,6 +275,19 @@ Item {
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
@@ -245,7 +299,7 @@ Item {
Layout.maximumWidth: 18
horizontalAlignment: Text.AlignHCenter
text: deviceRow.icon
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
@@ -315,7 +369,7 @@ Item {
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: panel.contentPadding
anchors.margins: panel.contentPadding - 2
spacing: 10
// Header
@@ -325,9 +379,9 @@ Item {
Text {
text: label.text
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 40
font.pixelSize: 30
color: Colors.foreground
}
@@ -375,9 +429,14 @@ Item {
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: 2
spacing: 4
Repeater {
model: root.candidateSinks
DeviceRow {
@@ -425,9 +484,10 @@ Item {
level: inputPeak.peak
}
Column {
width: parent.width
spacing: 2
spacing: 4
Repeater {
model: root.candidateSources
DeviceRow {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,413 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import "../../config"
// Weather indicator — modeled on omarchy's weather bar pill
// (github.com/omacom/omarchy, quattro branch,
// shell/plugins/panels/weather) but rebuilt in this bar's own style: the
// actual location/polling logic lives in the WeatherData singleton (config/),
// shared across every monitor's bar; this is just the icon + panel UI,
// same split as SystemUpdate.qml.
Item {
id: root
implicitWidth: row.implicitWidth + 2
implicitHeight: Appearance.barHeight
// The bar's own PanelWindow — same barWindow-anchoring convention as
// every other popup-having module here (Volume.qml, SystemUpdate.qml,
// etc.), so the panel opens in a fixed spot rather than following this
// icon's exact position in its RowLayout.
required property var barWindow
readonly property bool busy: WeatherData.locating || WeatherData.loading
// Local to this popup instance — unlike WeatherData's own state, there's
// no need to share an in-progress search across monitors' bars.
property bool searchOpen: false
property string searchQuery: ""
RowLayout {
id: row
anchors.centerIn: parent
spacing: 4
Text {
id: label
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 18
color: Colors.foreground
// Same spinner treatment as SystemUpdate.qml/
// BluetoothIndicator.qml's scan spinner while the location or
// conditions are still loading.
text: root.busy ? "progress_activity" : (WeatherData.hasLocation ? WeatherData.iconFor(WeatherData.weatherCode, WeatherData.isDay) : "cloud_off")
RotationAnimation on rotation {
running: root.busy
loops: Animation.Infinite
from: 0
to: 360
duration: 900
onRunningChanged: if (!running) label.rotation = 0
}
}
Text {
visible: WeatherData.hasLocation && !WeatherData.locating
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.foreground
text: Math.round(WeatherData.tempC) + "°"
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
WeatherData.refresh()
} else {
panel.visible = !panel.visible
grab.active = panel.visible
}
}
}
PopupWindow {
id: panel
visible: false
color: "transparent"
// Same contentWidth/contentPadding split as the other panels
// (Volume.qml, SystemUpdate.qml, etc.) rather than a single magic
// implicitWidth.
readonly property int contentWidth: 220
readonly property int contentPadding: 16
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
anchor {
window: root.barWindow
// Unlike anchor.item, anchor.window doesn't default rect to the
// window's own bounds — set it explicitly so edges/gravity below
// resolve against the full bar. Bottom alone (no Left/Right)
// anchors to the bar's bottom-center, which lines up with this
// icon since it sits in the bar's centered RowLayout.
rect.x: 0
rect.y: 0
rect.width: root.barWindow.width
rect.height: root.barWindow.height
edges: Edges.Bottom
gravity: Edges.Bottom
margins.bottom: -4
}
HyprlandFocusGrab {
id: grab
windows: [panel]
onCleared: panel.visible = false
}
Rectangle {
anchors.fill: parent
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 3
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: panel.contentPadding
spacing: 10
RowLayout {
width: parent.width
spacing: 8
Text {
text: label.text
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 28
color: Colors.foreground
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
elide: Text.ElideRight
text: {
if (WeatherData.locating) return "Locating…"
if (!WeatherData.hasLocation) return "Location unavailable"
return WeatherData.locationName || "Weather"
}
font.family: Appearance.fontFamily
font.pixelSize: 18
font.bold: true
color: Colors.foreground
}
Text {
visible: WeatherData.hasLocation && !WeatherData.locating
text: WeatherData.loading ? "Updating…" : "Feels like " + Math.round(WeatherData.feelsLikeC) + "°"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
Text {
visible: WeatherData.hasLocation && !WeatherData.locating
text: Math.round(WeatherData.tempC) + "°"
font.family: Appearance.fontFamily
font.pixelSize: 28
font.bold: true
color: Colors.foreground
}
// Toggles the search box below — same tap target size
// and hover feedback as the picker-target rows in
// VpnIndicator.qml.
Text {
text: root.searchOpen ? "close" : "edit_location"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: editLocationMouse.containsMouse ? Colors.primary : Colors.muted
MouseArea {
id: editLocationMouse
anchors.fill: parent
anchors.margins: -4
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.searchOpen = !root.searchOpen
root.searchQuery = ""
WeatherData.searchResults = []
}
}
}
}
// City search — always available regardless of how the
// current location was resolved (IP guess or a previous
// search), so a wrong auto-detected city is one click away
// from being fixed. Same search-box + result-list shape as
// VpnIndicator.qml's Mullvad location picker.
Column {
visible: root.searchOpen
width: parent.width
spacing: 6
Rectangle {
width: parent.width
implicitHeight: 24
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 1
Text {
anchors.fill: parent
anchors.margins: 6
visible: citySearchInput.text.length === 0
text: "Search city…"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
TextInput {
id: citySearchInput
anchors.fill: parent
anchors.margins: 6
color: Colors.foreground
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
clip: true
focus: root.searchOpen
text: root.searchQuery
onTextChanged: {
root.searchQuery = text
searchDebounce.restart()
}
}
Timer {
id: searchDebounce
interval: 400
onTriggered: WeatherData.search(root.searchQuery)
}
}
Text {
visible: WeatherData.searching
text: "Searching…"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Text {
visible: !WeatherData.searching && citySearchInput.text.length >= 2 && WeatherData.searchResults.length === 0
text: "No matches"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Flickable {
width: parent.width
height: Math.min(resultsColumn.implicitHeight, 150)
contentWidth: width
contentHeight: resultsColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: resultsColumn
width: parent.width
spacing: 1
Repeater {
model: WeatherData.searchResults
delegate: Rectangle {
required property var modelData
width: parent.width
implicitHeight: resultText.implicitHeight + 8
radius: 0
color: resultMouse.containsMouse ? Colors.primary : "transparent"
Text {
id: resultText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 4
elide: Text.ElideRight
text: modelData.description ? (modelData.name + " — " + modelData.description) : modelData.name
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: resultMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
MouseArea {
id: resultMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
WeatherData.selectLocation(modelData.name, modelData.latitude, modelData.longitude)
root.searchOpen = false
root.searchQuery = ""
WeatherData.searchResults = []
}
}
}
}
}
}
}
// Humidity / wind — same two-stat layout, just laid out as
// a row of icon+value pairs rather than full sentences.
RowLayout {
visible: WeatherData.hasLocation && !WeatherData.locating
width: parent.width
spacing: 16
RowLayout {
spacing: 4
Text {
text: "water_drop"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: Colors.muted
}
Text {
text: WeatherData.humidity + "%"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.foreground
}
}
RowLayout {
spacing: 4
Text {
text: "air"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: Colors.muted
}
Text {
text: Math.round(WeatherData.windKmh) + " km/h"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.foreground
}
}
}
Rectangle {
visible: WeatherData.forecast.length > 0
width: parent.width
height: 1
color: Colors.muted
}
RowLayout {
visible: WeatherData.forecast.length > 0
width: parent.width
spacing: 4
Repeater {
model: WeatherData.forecast
delegate: ColumnLayout {
required property var modelData
Layout.fillWidth: true
spacing: 2
Text {
Layout.alignment: Qt.AlignHCenter
text: Qt.formatDateTime(new Date(modelData.date + "T12:00:00"), "ddd")
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Text {
Layout.alignment: Qt.AlignHCenter
text: WeatherData.iconFor(modelData.code, true)
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 18
color: Colors.foreground
}
Text {
Layout.alignment: Qt.AlignHCenter
text: Math.round(modelData.maxC) + "°/" + Math.round(modelData.minC) + "°"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.foreground
}
}
}
}
}
}
}
}

View File

@@ -37,7 +37,8 @@ RowLayout {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Hyprland.dispatch("workspace " + modelData.name)
// Hyprland's Lua config expects Lua syntax from dispatch
onClicked: Hyprland.dispatch("hl.dsp.focus({ workspace = " + JSON.stringify(modelData.id > 0 ? modelData.id : modelData.name) + " })")
}
}
}

View File

@@ -11,16 +11,13 @@ PanelWindow {
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
// Bottom-center: anchoring only the bottom edge lets the layer shell
// center it horizontally.
anchors { bottom: true }
margins.bottom: Appearance.spacing * 4
implicitWidth: 272
implicitHeight: 54
implicitWidth: 300
implicitHeight: 60
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "quickshell:osd"
@@ -49,13 +46,13 @@ PanelWindow {
RowLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 12
anchors.margins: 15
spacing: 13
Text {
font.family: Appearance.iconFontFamily
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 18
font.pixelSize: 21
color: Colors.foreground
text: {
const audio = osd.sink ? osd.sink.audio : null
@@ -67,7 +64,7 @@ PanelWindow {
Item {
id: slider
Layout.fillWidth: true
implicitHeight: 16
implicitHeight: 18
readonly property real value: osd.sink && osd.sink.audio ? osd.sink.audio.volume : 0
readonly property bool enabled: !!osd.sink
@@ -76,7 +73,7 @@ PanelWindow {
id: track
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 4
height: 5
radius: 0
color: Colors.muted
opacity: 0.35
@@ -85,15 +82,15 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: track.width * Math.max(0, Math.min(1, slider.value))
height: 4
height: 5
radius: 0
color: Colors.primary
opacity: slider.enabled ? 1 : 0.4
}
Rectangle {
width: 12
height: 12
radius: 6
width: 14
height: 14
radius: 7
color: Colors.primary
opacity: slider.enabled ? 1 : 0.4
anchors.verticalCenter: parent.verticalCenter
@@ -113,10 +110,10 @@ PanelWindow {
}
Text {
Layout.preferredWidth: 32
Layout.preferredWidth: 38
horizontalAlignment: Text.AlignRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
font.pixelSize: 13
color: Colors.muted
text: Math.round(slider.value * 100) + "%"
}

View File

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

View File

@@ -0,0 +1,84 @@
#!/bin/bash
# Imports a UniFi WireGuard VPN Server client config into NetworkManager
# under the profile name VpnIndicator.qml expects (homeWgConnection,
# "unifi-home" by default), so the bar's VPN panel can toggle it.
#
# Get the .conf from UniFi Network: Settings → VPN → VPN Server → your
# WireGuard server → Add Client → Download.
#
# Usage: ./ImportUnifiWireguard.sh <client.conf> [profile-name] [--replace]
# ./ImportUnifiWireguard.sh --pick [profile-name] [--replace]
#
# --pick opens a file chooser (PickFile.py) instead of taking a path — this
# is what the VPN panel's "Add config" button runs. Exits 3 if cancelled.
#
# NetworkManager names both the profile and the interface after the file's
# basename, so the config is copied to <profile-name>.conf first (interface
# names max out at 15 chars). The private key ends up in a root-only file
# under /etc/NetworkManager/system-connections — delete the downloaded
# .conf afterwards.
set -euo pipefail
replace=0
pick=0
args=()
for a in "$@"; do
case "$a" in
--replace) replace=1 ;;
--pick) pick=1 ;;
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) args+=("$a") ;;
esac
done
if [ $pick -eq 1 ]; then
conf=""
name="${args[0]:-unifi-home}"
rc=0
conf=$("$(dirname "$0")/PickFile.py" "Select UniFi WireGuard config" "*.conf") || rc=$?
if [ $rc -eq 1 ]; then echo "Cancelled" >&2; exit 3; fi
if [ $rc -ne 0 ]; then echo "Couldn't open a file chooser" >&2; exit 1; fi
else
conf="${args[0]:-}"
name="${args[1]:-unifi-home}"
fi
if [ -z "$conf" ] || [ ! -f "$conf" ]; then
echo "Usage: $0 <client.conf> [profile-name] [--replace]" >&2
exit 1
fi
if ! [[ "$name" =~ ^[A-Za-z0-9_-]{1,15}$ ]]; then
echo "Profile name must be 1-15 chars of [A-Za-z0-9_-] (it doubles as the interface name)" >&2
exit 1
fi
if nmcli -g connection.id connection show "$name" >/dev/null 2>&1; then
if [ $replace -eq 1 ]; then
nmcli connection delete "$name" >/dev/null
else
echo "A NetworkManager profile named '$name' already exists — rerun with --replace to overwrite it" >&2
exit 1
fi
fi
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
install -m 600 "$conf" "$tmpdir/$name.conf"
if ! grep -qi '^\[Interface\]' "$tmpdir/$name.conf" || ! grep -qi '^\[Peer\]' "$tmpdir/$name.conf"; then
echo "$(basename "$conf") doesn't look like a WireGuard config" >&2
exit 1
fi
nmcli connection import type wireguard file "$tmpdir/$name.conf"
# Only ever brought up deliberately from the bar — never on boot or on
# joining a network, where it'd silently fight Tailscale/Mullvad/ProtonVPN.
nmcli connection modify "$name" connection.autoconnect no
# Import itself defaults to autoconnect=yes, so NetworkManager may already
# have started bringing it up in the moment before the modify above.
nmcli connection down "$name" >/dev/null 2>&1 || true
echo
echo "Imported as '$name'. Toggle it from the VPN panel, or: nmcli connection up $name"
echo "You can now delete $conf"

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Opens the xdg-desktop-portal file chooser and prints the picked file's
# path. Quickshell has no usable file dialog of its own (QtQuick.Dialogs'
# fallback renders inside the calling window, i.e. the tiny bar popup), and
# no zenity/kdialog is installed — the portal goes through
# xdg-desktop-portal-gtk, which is.
#
# Usage: PickFile.py [title] [glob ...]
# Exits 0 and prints the path on a pick, 1 on cancel, 2 on error.
import os
import sys
from urllib.parse import unquote, urlparse
from gi.repository import Gio, GLib
title = sys.argv[1] if len(sys.argv) > 1 else "Open File"
globs = sys.argv[2:]
try:
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
except GLib.Error as e:
print(f"No session bus: {e.message}", file=sys.stderr)
sys.exit(2)
# The portal answers on a Request object whose path is derived from our
# unique bus name and handle_token, so subscribe to it before calling —
# otherwise a fast response could arrive before we're listening.
token = f"quickshell_{os.getpid()}"
sender = bus.get_unique_name().lstrip(":").replace(".", "_")
handle = f"/org/freedesktop/portal/desktop/request/{sender}/{token}"
loop = GLib.MainLoop()
result = {"code": 2, "uris": []}
def on_response(_conn, _sender, _path, _iface, _signal, params):
code, results = params.unpack()
result["code"] = code
result["uris"] = results.get("uris", [])
loop.quit()
bus.signal_subscribe("org.freedesktop.portal.Desktop", "org.freedesktop.portal.Request",
"Response", handle, None, Gio.DBusSignalFlags.NONE, on_response)
options = {
"handle_token": GLib.Variant("s", token),
"modal": GLib.Variant("b", True),
}
filters = []
if globs:
filters.append((", ".join(globs), [(0, g) for g in globs]))
filters.append(("All files", [(0, "*")]))
options["filters"] = GLib.Variant("a(sa(us))", filters)
downloads = os.path.expanduser("~/Downloads")
if os.path.isdir(downloads):
options["current_folder"] = GLib.Variant("ay", downloads.encode() + b"\0")
try:
bus.call_sync("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
"org.freedesktop.portal.FileChooser", "OpenFile",
GLib.Variant("(ssa{sv})", ("", title, options)),
GLib.VariantType("(o)"), Gio.DBusCallFlags.NONE, -1, None)
except GLib.Error as e:
print(f"File chooser portal unavailable: {e.message}", file=sys.stderr)
sys.exit(2)
loop.run()
# Response codes: 0 = picked, 1 = cancelled, 2 = other failure.
if result["code"] != 0 or not result["uris"]:
sys.exit(1 if result["code"] == 1 else 2)
print(unquote(urlparse(result["uris"][0]).path))

View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Run by update-system.sh inside the update terminal, before the system
# update — pulls the dotfiles repo and re-stows it so both machines pick up
# config changes on every update.
#
# Never fails the update: a pull that can't fast-forward (local commits or
# uncommitted changes in the way) or a stow conflict just prints a warning
# and leaves things as they were, then the system update carries on.
DOTFILES="$HOME/Projects/dotfiles"
warn() { printf '\033[1;33m%s\033[0m\n' "$*"; }
info() { printf '\033[1;36m%s\033[0m\n' "$*"; }
if [ ! -d "$DOTFILES/.git" ]; then
warn "Dotfiles repo not found at $DOTFILES — skipping."
exit 0
fi
info "Pulling dotfiles..."
# Fetch and merge separately so the warning says which one went wrong — the
# remote is SSH via Bitwarden's agent, so a failed fetch is almost always
# Bitwarden being closed or locked.
if ! git -C "$DOTFILES" fetch; then
warn "Couldn't reach the dotfiles remote — is Bitwarden open and unlocked? Skipping stow."
echo
exit 0
fi
# --ff-only so a diverged branch never produces a surprise merge commit;
# sort that out by hand instead.
if ! git -C "$DOTFILES" merge --ff-only; then
warn "Couldn't fast-forward dotfiles (local changes or diverged branch) — skipping stow."
echo
exit 0
fi
if ! command -v stow &> /dev/null; then
warn "stow not installed — skipping."
exit 0
fi
info "Applying dotfiles with stow..."
# Real dirs first, or stow links the whole folder into the repo (see bootstrap.sh)
mkdir -p "$HOME/.local/share/fonts"
# -R restows: drops links for files removed from the repo and adds new ones.
if stow -R -d "$DOTFILES" -t "$HOME" dotfiles; then
info "Dotfiles applied."
fc-cache "$HOME/.local/share/fonts" >/dev/null 2>&1
else
warn "stow hit a conflict — nothing was changed. Fix it and re-run:"
warn " stow -R -d $DOTFILES -t ~ dotfiles"
fi
echo

View File

@@ -0,0 +1,105 @@
#!/bin/bash
# Launched by SystemUpdate.qml (top bar, next to the clock) — opens a
# terminal running whatever update command fits this distro, then notifies
# once it's done. Ported from the old waybar custom/updater module
# (.config/waybar/scripts/update-distro.sh); kept multi-distro even though
# this machine is Arch, since these dotfiles aren't.
#
# The window it opens is floated + centered by a matching Hyprland rule
# (system-update-terminal, in .config/hypr/layout/windowrules.lua),
# same treatment as Bitwarden's window, rather than tiling into whatever
# workspace you happened to be on.
iDIR="$HOME/.config/swaync/images"
# Printed inside the terminal before the actual update command runs, so it
# reads as "updating" from the first frame rather than looking like an
# empty/stuck window while pacman/apt/etc. take a moment to start printing.
#
# The terminal runs this in a fresh `bash -c`, which doesn't inherit shell
# functions from this script's own environment (export -f only reaches bash
# processes forked from *this* one, not whatever the terminal execs) — so
# instead, the already-colored text is built once here with real escape
# bytes ($'...'), then %q-quoted into a self-contained `echo '...'` that
# reproduces those same bytes verbatim when the child bash parses it.
msg=" Updating System — please wait "
line=$(printf '=%.0s' $(seq 1 ${#msg}))
banner_text=$'\033[1;36m'"$line"$'\033[0m\n\033[1;36m'"$msg"$'\033[0m\n\033[1;36m'"$line"$'\033[0m\n\n'
BANNER="echo $(printf '%q' "$banner_text")"
# GHOSTTY_INSTALL is how to install the terminal on this distro, if it's
# packaged there at all — only Arch ships it in the official repos.
if command -v paru &> /dev/null; then
CMD="paru -Syu"
LABEL="Arch-based system"
GHOSTTY_INSTALL="pacman -S --needed --noconfirm ghostty"
elif command -v yay &> /dev/null; then
CMD="yay -Syu"
LABEL="Arch-based system"
GHOSTTY_INSTALL="pacman -S --needed --noconfirm ghostty"
elif command -v dnf &> /dev/null; then
CMD="sudo dnf update --refresh -y"
LABEL="Fedora system"
elif command -v apt &> /dev/null; then
CMD="sudo apt update && sudo apt upgrade -y"
LABEL="Debian/Ubuntu system"
elif command -v zypper &> /dev/null; then
CMD="sudo zypper dup -y"
LABEL="openSUSE system"
else
notify-send -i "$iDIR/error.png" -u critical "Unsupported system" "This script does not support your distribution."
exit 1
fi
# --- Terminal: Ghostty, offering to install it if missing ---
# This runs from the bar with no terminal of its own, so there's nowhere to
# type a sudo password — the install goes through pkexec instead, which
# pops up the polkit agent's graphical password prompt. Declining (or a
# failed install) falls back to foot for this run, so updating never gets
# blocked on the terminal choice.
if ! command -v ghostty &> /dev/null; then
if [ -n "$GHOSTTY_INSTALL" ]; then
# -A waits for a click and prints the chosen action's name (nothing if
# the notification is dismissed instead).
choice=$(notify-send -i "$iDIR/error.png" -u critical \
-A install="Install Ghostty" -A foot="Use foot this time" \
"Ghostty not installed" "The system updater runs in Ghostty. Install it now?")
if [ "$choice" = "install" ]; then
if pkexec $GHOSTTY_INSTALL; then
notify-send -i "$iDIR/profile-pic.jpg" -u low "Ghostty installed" "Starting the update..."
else
notify-send -i "$iDIR/error.png" "Ghostty install failed" "Using foot for this update."
fi
fi
else
notify-send -i "$iDIR/error.png" "Ghostty not installed" \
"Install Ghostty for the system updater — using foot for now."
fi
fi
if command -v ghostty &> /dev/null; then
# --gtk-single-instance=false: otherwise, with a Ghostty window already
# open, this would just hand the window off to that running instance and
# return immediately — SystemUpdates.qml relies on this script blocking
# until the update window is closed (that's when it re-checks the count),
# and the "has been updated" notification below would fire up front.
TERMINAL=(ghostty --gtk-single-instance=false --title=update -e)
elif command -v foot &> /dev/null; then
TERMINAL=(foot -T update)
else
notify-send -i "$iDIR/error.png" "Need a terminal:" "Neither Ghostty nor foot is installed."
exit 1
fi
# Keeps the terminal open (whether the update succeeded or failed — read
# always runs regardless, since it's ; not &&) until you press Enter,
# instead of the terminal closing the instant the update command exits and
# taking any output/errors with it.
WAIT_PROMPT="echo; read -p 'Update finished — press Enter to close...'"
# Pull + re-stow the dotfiles first, so config changes from the other machine
# land alongside the package update.
SYNC="$(printf '%q' "$(dirname "$0")/sync-dotfiles.sh")"
"${TERMINAL[@]}" bash -c "$BANNER; $SYNC; $CMD; $WAIT_PROMPT"
notify-send -i "$iDIR/profile-pic.jpg" -u low "$LABEL" 'has been updated.'

View File

@@ -4,7 +4,11 @@ import "./modules/osd"
ShellRoot {
Variants {
model: Quickshell.screens
// Exclude the Sunshine headless virtual monitor — nobody ever sees a
// bar there, but PanelWindow's exclusiveZone still reserves space for
// it, which left a black strip cut out of Steam Big Picture's
// fullscreen area on that output.
model: Quickshell.screens.filter(s => !s.name.startsWith("HEADLESS"))
Bar {
required property var modelData

5
dotfiles/.config/sunshine/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Machine-local Sunshine state lives next to the stowed config; never commit it
credentials/
*.log
*.log.*
sunshine_state.json

View File

@@ -0,0 +1,47 @@
{
"env": {
"PATH": "$(PATH):$(HOME)/.local/bin"
},
"apps": [
{
"name": "Desktop",
"image-path": "desktop.png"
},
{
"name": "Low Res Desktop",
"image-path": "desktop.png",
"prep-cmd": [
{
"do": "xrandr --output HDMI-1 --mode 1920x1080",
"undo": "xrandr --output HDMI-1 --mode 1920x1200"
}
]
},
{
"name": "Steam Big Picture",
"detached": [
"setsid steam steam://open/bigpicture"
],
"prep-cmd": [
{
"do": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-prep.sh",
"undo": "setsid steam steam://close/bigpicture"
}
],
"image-path": "steam.png"
},
{
"name": "Steam Big Picture (TV)",
"detached": [
"setsid steam steam://open/bigpicture"
],
"prep-cmd": [
{
"do": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh",
"undo": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh"
}
],
"image-path": "steam.png"
}
]
}

View File

@@ -0,0 +1,17 @@
#!/bin/sh
# Run before Sunshine launches Steam Big Picture: focus the headless virtual
# monitor and park the cursor on it, so Big Picture (which spawns on whatever
# monitor currently has focus/the mouse) opens there instead of wherever the
# mouse happened to be.
set -e
hyprctl dispatch 'hl.dsp.focus({monitor = "HEADLESS-1"})'
hyprctl dispatch 'hl.dsp.cursor.move({x = 5344, y = 540})'
# Sunshine only flips the system default audio sink to sink-sunshine-stereo
# after the streaming session/client handshake completes (~500ms after this
# script runs and Steam is spawned) — see sunshine.log timestamps. If Steam's
# own audio (SDL) initializes in that window, it binds to whatever was
# default at the time (the real HDMI output) and never re-homes even after
# Sunshine's swap. Set it here first so it's already correct before Steam
# ever launches; Sunshine's own (redundant) swap right after is harmless.
pactl set-default-sink sink-sunshine-stereo || true

View File

@@ -0,0 +1,18 @@
#!/bin/sh
# Run before Sunshine launches Steam Big Picture for the 4K TV target.
# Switches the headless virtual monitor from its default 1728x1080@60
# (used for the Steam Deck target) up to 3840x2160@60, then focuses it
# and parks the cursor there so Big Picture opens on it. Reverted by
# steam-bigpicture-tv-undo.sh once the stream ends.
set -e
# NOTE: this Hyprland build uses the Lua config layer, which rejects the
# usual `hyprctl keyword monitor ...` syntax ("keyword can't work with
# non-legacy parsers. Use eval.") — hl.monitor() via `hyprctl eval` is the
# runtime equivalent of the hl.monitor() call in layout/monitors.lua.
hyprctl eval 'hl.monitor({ output = "HEADLESS-1", mode = "3840x2160@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })'
hyprctl dispatch 'hl.dsp.focus({monitor = "HEADLESS-1"})'
hyprctl dispatch 'hl.dsp.cursor.move({x = 6400, y = 1080})'
# See steam-bigpicture-prep.sh for why this runs here instead of relying on
# Sunshine's own post-handshake audio-sink swap.
pactl set-default-sink sink-sunshine-stereo || true

Some files were not shown because too many files have changed in this diff Show More