137 Commits

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

Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-08-21 04:30:06 +01:00
2935fc7cfe Update README.md 2026-08-20 07:37:58 +01:00
6f9651ccb3 added new linux screenshot location 2026-08-19 09:45:59 +01:00
c88a96406a added alias for ssh-ing 2026-08-19 09:45:26 +01:00
13c4f95e08 interval poll for updates and fixed css 2026-08-19 09:44:59 +01:00
c4f220d95a docker & pnpm context added
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-16 04:34:50 +01:00
f6d69332db Update bookmarks
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-16 04:34:28 +01:00
1559da170e Update zen.toml
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-16 04:13:55 +01:00
ca18fe26c1 Update zen.toml
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-16 04:00:45 +01:00
cfd6cf2700 ohmyposh toml file added
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-16 03:31:58 +01:00
5909ed14cb autocomplete & ohmyposh fix
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-16 03:31:39 +01:00
3487732f46 added oh-my-posh 2026-05-14 16:12:20 +01:00
6aff35bd5b update to 0.55
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-12 20:04:37 +01:00
176902616b add wallpapers
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-04 19:20:21 +01:00
70cda8d08e Delete wallpapers
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-04 19:19:55 +01:00
28c6a0c3d7 changed update terminal to foot
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-04 19:15:09 +01:00
fa2ae6f4e2 adding wallpapers 2026-05-03 21:20:50 +01:00
911182628d added themes folder 2026-05-03 14:50:27 +01:00
308e8440bb ssh agent adapted for multiple machines 2026-05-03 14:45:07 +01:00
fad66cf071 added qt5 and qt6 config files 2026-05-03 14:43:11 +01:00
d7cf9e3da3 configured waybar notification icons
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-03 14:33:08 +01:00
03d58f73fb configured notification to waybar
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-03 14:32:17 +01:00
a35b3b75a3 changed notification location placement
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-03 14:24:26 +01:00
040cf83811 Update config.jsonc
Signed-off-by: thomas <thomas.williams29899@gmail.com>
2026-05-03 13:45:24 +01:00
923 changed files with 114473 additions and 316 deletions

View File

@@ -26,6 +26,39 @@ My dotfiles, a constantly evolving set of configurations which I arguably spend
too much time tweaking, but they make the command line feel like home, so here too much time tweaking, but they make the command line feel like home, so here
we are. we are.
## 🖥️ Installation
In order to apply these dotfiles, you must have the SSH Key stored in your password manager,
then connect the SSH-Agent to your shell. Once that is done, you will need the application
**Stow** in order to keep it synced.
After this, you will need to git clone the repo in the Projects directory
```console
mkdir Projects
git clone ssh://git@gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles.git
```
On a fresh install, run the bootstrap script. It installs drivers (auto-detected),
every package in `system/packages.txt`, yay, flatpaks, then links the dotfiles and
system config below:
```console
~/Projects/dotfiles/bootstrap.sh # asks about the optional SVT-AV1 + ffmpeg rebuild
~/Projects/dotfiles/bootstrap.sh --svt-av1 # or --no-svt-av1 to skip the question
```
To only link the dotfiles, run this command:
```console
stow -d ~/Projects/dotfiles/ -t ~/ dotfiles
```
This will sync wallpapers/config files to where they are usually located
System-level config (e.g. pacman options) lives in `system/` and is installed with sudo:
```console
~/Projects/dotfiles/system/install.sh
```
## 📦 Programs ## 📦 Programs
List of programs and tools I use. List of programs and tools I use.
@@ -39,7 +72,7 @@ List of programs and tools I use.
|📁 File Manager | [thunar](https://wiki.archlinux.org/title/Thunar) | |📁 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) | |📝 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) | |🌐 Browser | [zen](https://github.com/zen-browser/desktop) / [firefox](https://github.com/topics/firefox-browser) / [startpage](https://blog-bucket-personal.s3.eu-west-2.amazonaws.com/Screenshot_02-May_08-44-23_6614.png) |
|📊 Bar | [waybar](https://github.com/Alexays/Waybar) | |📊 Bar | [waybar](https://github.com/Alexays/Waybar) / [quickshell](https://quickshell.org/) |
|🚀 Launcher | [rofi](https://github.com/davatorium/rofi) | |🚀 Launcher | [rofi](https://github.com/davatorium/rofi) |
|🔒 Lockscreen | [hyprlock](https://github.com/hyprwm/hyprlock) | |🔒 Lockscreen | [hyprlock](https://github.com/hyprwm/hyprlock) |
|🚪 Login Menu | [sddm](https://github.com/sddm/sddm) | |🚪 Login Menu | [sddm](https://github.com/sddm/sddm) |

168
bootstrap.sh Executable file
View File

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

View File

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

View File

@@ -1,15 +1,14 @@
{ {
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
"logo": { "logo": {
"padding": { "padding": {
"top": 1 "top": 1
} }
}, },
"display": { "display": {
"separator": " 󰑃 " "separator": " 󰑃 "
}, },
"modules": [ "modules": [
"break", "break",
{ {
"type": "os", "type": "os",
@@ -49,12 +48,12 @@
{ {
"type": "cursor", "type": "cursor",
"key": "│ ├", "key": "│ ├",
"keyColor": "blue", "keyColor": "blue"
}, },
{ {
"type": "terminalfont", "type": "terminalfont",
"key": "│ ├", "key": "│ ├",
"keyColor": "blue", "keyColor": "blue"
}, },
{ {
"type": "terminal", "type": "terminal",
@@ -123,6 +122,6 @@
"type": "custom", "type": "custom",
"format": "\u001b[90m \u001b[31m \u001b[32m \u001b[33m \u001b[34m \u001b[35m \u001b[36m \u001b[37m \u001b[38m \u001b[39m \u001b[39m  \u001b[38m \u001b[37m \u001b[36m \u001b[35m \u001b[34m \u001b[33m \u001b[32m \u001b[31m \u001b[90m " "format": "\u001b[90m \u001b[31m \u001b[32m \u001b[33m \u001b[34m \u001b[35m \u001b[36m \u001b[37m \u001b[38m \u001b[39m \u001b[39m  \u001b[38m \u001b[37m \u001b[36m \u001b[35m \u001b[34m \u001b[33m \u001b[32m \u001b[31m \u001b[90m "
}, },
"break", "break"
] ]
} }

View File

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

View File

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

View File

@@ -51,10 +51,10 @@ env = ELECTRON_OZONE_PLATFORM_HINT,auto # auto selects Wayland if possible, X11
# This is from Hyprland Wiki. Below will be activated nvidia gpu detected # This is from Hyprland Wiki. Below will be activated nvidia gpu detected
# See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables # See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
#env = LIBVA_DRIVER_NAME,nvidia env = LIBVA_DRIVER_NAME,nvidia
#env = __GLX_VENDOR_LIBRARY_NAME,nvidia env = __GLX_VENDOR_LIBRARY_NAME,nvidia
#env = NVD_BACKEND,direct env = NVD_BACKEND,direct
#env = GSK_RENDERER,ngl env = GSK_RENDERER,ngl
# additional ENV's for nvidia. Caution, activate with care # additional ENV's for nvidia. Caution, activate with care
#env = GBM_BACKEND,nvidia-drm #env = GBM_BACKEND,nvidia-drm
@@ -87,3 +87,6 @@ env = ELECTRON_OZONE_PLATFORM_HINT,auto # auto selects Wayland if possible, X11
# env = HYPRLAND_NO_RT,1 # Disables realtime priority setting by Hyprland. # env = HYPRLAND_NO_RT,1 # Disables realtime priority setting by Hyprland.
# env = HYPRLAND_NO_SD_NOTIFY,1 # If systemd, disables the 'sd_notify' calls. # env = HYPRLAND_NO_SD_NOTIFY,1 # If systemd, disables the 'sd_notify' calls.
# env = HYPRLAND_NO_SD_VARS,1 # Disables management of variables in systemd and dbus activation environments. # env = HYPRLAND_NO_SD_VARS,1 # Disables management of variables in systemd and dbus activation environments.
# Bitwarden SSH agent (so GUI apps like VS Code see it, not just shells)
env = SSH_AUTH_SOCK,/home/thomas/.bitwarden-ssh-agent.sock

View File

@@ -74,7 +74,6 @@ bind = $mainMod, K, layoutmsg, cycleprev
bind = $mainMod CTRL, Return, layoutmsg, swapwithmaster bind = $mainMod CTRL, Return, layoutmsg, swapwithmaster
# Dwindle Layout # Dwindle Layout
bind = $mainMod SHIFT, I, togglesplit # only works on dwindle layout
bind = $mainMod, P, pseudo, # dwindle bind = $mainMod, P, pseudo, # dwindle
# Special Keys / Hot Keys # Special Keys / Hot Keys

View File

@@ -21,7 +21,7 @@ decoration {
rounding = 10 rounding = 10
active_opacity = 1.0 active_opacity = 1.0
inactive_opacity = 0.9 inactive_opacity = 1.0
fullscreen_opacity = 1.0 fullscreen_opacity = 1.0
dim_inactive = true dim_inactive = true

View File

@@ -8,7 +8,6 @@
# NOTE: some settings are in ~/.config/hypr/UserConfigs/UserDecorAnimations.conf # NOTE: some settings are in ~/.config/hypr/UserConfigs/UserDecorAnimations.conf
dwindle { dwindle {
pseudotile = true
preserve_split = true preserve_split = true
#smart_split = true #smart_split = true
special_scale_factor = 1 special_scale_factor = 1
@@ -77,7 +76,6 @@ gestures {
misc { misc {
disable_hyprland_logo = true disable_hyprland_logo = true
disable_splash_rendering = true disable_splash_rendering = true
vfr = true
vrr = 2 vrr = 2
mouse_move_enables_dpms = true mouse_move_enables_dpms = true
enable_swallow = off enable_swallow = off

View File

@@ -8,5 +8,5 @@
# <<<<< TUI AUTO START # <<<<< TUI AUTO START
monitor=desc:LG Electronics LG ULTRAWIDE 0x0002E68A,2560x1080@60.00000,1920x-740,1.00000000,transform,3,vrr,0 monitor=desc:LG Electronics LG ULTRAWIDE 0x0002E68A,2560x1080@60.00000,1920x-740,1.00000000,transform,3,vrr,0
monitor=desc:HP Inc. HP 524pf VNG447017D,1920x1080@100.00000,-1920x0,1.00000000,transform,0,vrr,0 monitor=desc:HP Inc. HP 524pf VNG447017D,1920x1080@100.00000,-1920x0,1.00000000,transform,0,vrr,0
monitor=desc:Acer Technologies VG240Y S 0x126191DE,1920x1080@165.00301,0x0,1.00000000,transform,0,vrr,1,bitdepth,10,cm,wide monitor=desc:Acer Technologies VG240Y S 0x126191DE,1920x1080@165.00301,0x0,1.00000000,transform,0,vrr,0,bitdepth,10,cm,wide
# <<<<< TUI AUTO END # <<<<< TUI AUTO END

View File

@@ -0,0 +1,34 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- Ported from UserConfigs/UserAnimations.conf (the active preset)
--
-- The 14 alternative presets from animations/*.conf were also ported, flat, as
-- anim-*.lua (see the plan) — none of them are wired in. They now live in ../presets/.
-- To switch styles, replace the body of this file with `require("presets.anim-<preset>")`,
-- e.g. require("presets.anim-ml4w-classic").
hl.config({
animations = {
enabled = true,
},
})
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("smoothOut", { type = "bezier", points = { { 0.5, 0 }, { 0.99, 0.99 } } })
hl.curve("smoothIn", { type = "bezier", points = { { 0.5, -0.5 }, { 0.68, 1.5 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "smoothOut", style = "slide" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 100, bezier = "liner", style = "loop" }) -- used by rainbow borders and rotating colors; speed clamped from 180 -> Lua hl.animation caps speed at 100
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "smoothOut" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "overshot" })
-- animations for -git or version >0.42.0
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })

View File

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

View File

@@ -0,0 +1,65 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- Decoration Settings
-- Ported from UserConfigs/UserDecorations.conf
--
-- Hyprland Wiki Links
-- Animation - https://wiki.hyprland.org/Configuring/Animations/
-- Decoration - https://wiki.hyprland.org/Configuring/Variables/#decoration
local colors = require("appearance.colors")
hl.config({
general = {
border_size = 2,
gaps_in = 2,
gaps_out = 4,
col = {
active_border = colors.color12,
inactive_border = colors.color10,
},
},
decoration = {
rounding = 10,
active_opacity = 1.0,
inactive_opacity = 1.0,
fullscreen_opacity = 1.0,
dim_inactive = true,
dim_strength = 0.1,
dim_special = 0.8,
shadow = {
enabled = false,
range = 3,
render_power = 1,
color = colors.color12,
color_inactive = colors.color10,
},
blur = {
enabled = true,
size = 6,
passes = 2,
ignore_opacity = true,
new_optimizations = true,
special = true,
popups = true,
},
},
group = {
col = {
border_active = colors.color15,
},
groupbar = {
col = {
active = colors.color0,
},
},
},
})

View File

@@ -0,0 +1,90 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- Environment variables. See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Environment-variables/
-- Ported from UserConfigs/ENVariables.conf
-- Toolkit Backend Variables
hl.env("GDK_BACKEND", "wayland,x11,*")
hl.env("QT_QPA_PLATFORM", "wayland;xcb")
hl.env("CLUTTER_BACKEND", "wayland")
-- Run SDL2 applications on Wayland.
-- Remove or set to x11 if games that provide older versions of SDL cause compatibility issues
-- hl.env("SDL_VIDEODRIVER", "wayland")
-- xdg Specifications
hl.env("XDG_CURRENT_DESKTOP", "Hyprland")
hl.env("XDG_SESSION_DESKTOP", "Hyprland")
hl.env("XDG_SESSION_TYPE", "wayland")
-- QT Variables
hl.env("QT_AUTO_SCREEN_SCALE_FACTOR", "1")
hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1")
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct")
-- hyprland-qt-support
hl.env("QT_QUICK_CONTROLS_STYLE", "org.hyprland.style")
-- xwayland apps scale fix (useful if you use monitor scaling).
-- Set same value if you use scaling in monitors.lua
-- 1 is 100% 1.5 is 150%
-- see https://wiki.hyprland.org/Configuring/XWayland/
hl.env("GDK_SCALE", "1")
hl.env("QT_SCALE_FACTOR", "1")
-- Bibata-Modern-Ice-Cursor
-- NOTE! You must have the hyprcursor version to activate this.
-- https://wiki.hyprland.org/Hypr-Ecosystem/hyprcursor/
hl.env("HYPRCURSOR_THEME", "Bibata-Modern-Classic")
hl.env("HYPRCURSOR_SIZE", "24")
-- firefox
hl.env("MOZ_ENABLE_WAYLAND", "1")
-- electron >28 apps (may help) ##
-- https://www.electronjs.org/docs/latest/api/environment-variables
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") -- auto selects Wayland if possible, X11 otherwise
-- NVIDIA
-- This is from the Hyprland wiki. Uncomment below if an nvidia GPU is detected.
-- See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
hl.env("LIBVA_DRIVER_NAME", "nvidia")
hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia")
hl.env("NVD_BACKEND", "direct")
hl.env("GSK_RENDERER", "ngl")
-- additional ENV's for nvidia. Caution, activate with care
-- hl.env("GBM_BACKEND", "nvidia-drm")
-- hl.env("__GL_GSYNC_ALLOWED", "1") -- adaptive Vsync
-- hl.env("__NV_PRIME_RENDER_OFFLOAD", "1")
-- hl.env("__VK_LAYER_NV_optimus", "NVIDIA_only")
-- hl.env("WLR_DRM_NO_ATOMIC", "1")
-- FOR VM and POSSIBLY NVIDIA
-- LIBGL_ALWAYS_SOFTWARE software mesa rendering
-- hl.env("LIBGL_ALWAYS_SOFTWARE", "1") -- Warning. May cause hyprland to crash
-- hl.env("WLR_RENDERER_ALLOW_SOFTWARE", "1")
-- nvidia firefox (for hardware acceleration on FF)?
-- check this post https://github.com/elFarto/nvidia-vaapi-driver#configuration
-- hl.env("MOZ_DISABLE_RDD_SANDBOX", "1")
-- hl.env("EGL_PLATFORM", "wayland")
---- Aquamarine Environment Variables ---- ( Hyprland > 0.45 )
-- https://wiki.hyprland.org/Configuring/Environment-variables/#aquamarine-environment-variables
-- hl.env("AQ_TRACE", "1") -- Enables more verbose logging.
-- hl.env("AQ_DRM_DEVICES", "/dev/dri/card1:/dev/dri/card0") -- explicit list of DRM devices (GPUs) to use.
-- hl.env("AQ_MGPU_NO_EXPLICIT", "1") -- Disables explicit syncing on mgpu buffers
-- hl.env("AQ_NO_MODIFIERS", "1") -- Disables modifiers for DRM buffers
---- Hyprland Environment Variables ----
-- https://wiki.hyprland.org/Configuring/Environment-variables/#hyprland-environment-variables
-- hl.env("HYPRLAND_TRACE", "1") -- Enables more verbose logging.
-- hl.env("HYPRLAND_NO_RT", "1") -- Disables realtime priority setting by Hyprland.
-- hl.env("HYPRLAND_NO_SD_NOTIFY", "1") -- If systemd, disables the 'sd_notify' calls.
-- hl.env("HYPRLAND_NO_SD_VARS", "1") -- Disables management of variables in systemd and dbus activation environments.
-- Bitwarden SSH agent (so GUI apps like VS Code see it, not just shells)
hl.env("SSH_AUTH_SOCK", os.getenv("HOME") .. "/.bitwarden-ssh-agent.sock")

View File

@@ -0,0 +1,36 @@
-- Generated by HyprMod
-- Ported from hyprland-gui.conf
--
-- HyprMod isn't currently installed on this machine, so this is a straight syntax port of
-- whatever it last wrote. If you reinstall HyprMod, prefer letting its own "Migrate to Lua…"
-- wizard take over managing this file instead of hand-editing it going forward.
-- vrr forced off globally: the per-monitor vrr = 0 flag below does NOT override this when
-- misc.vrr is 1 (tested live on Hyprland 0.56.2 — DP-2 stayed vrr:true until this global
-- was changed). DP-2 (Acer VG240Y S) flickers under VRR when idle in fullscreen (mouse
-- stationary -> no repaint damage -> frame delivery stalls near the panel's VRR floor).
-- The other two monitors don't negotiate VRR at all, so this has no effect on them.
hl.config({
general = {
allow_tearing = true,
snap = {
enabled = false,
},
},
misc = {
vrr = 0,
},
})
-- Monitors
hl.monitor({ output = "DP-2", mode = "1920x1080@165.00Hz", position = "0x0", scale = 1, bitdepth = 10, cm = "wide", vrr = 0 })
-- Keybinds
-- (unbind = SUPER SHIFT, Print / unbind = SUPER, up from the source .conf have no Lua equivalent needed —
-- we just don't define a conflicting bind elsewhere; SUPER + up -> movefocus u below is otherwise identical
-- to the one already in keybinds.lua, kept here too for parity with the original file)
hl.bind("SUPER + Print", hl.dsp.exec_cmd([[sh -c 'if command -v hyprshot >/dev/null; then hyprshot -o "$HOME/Pictures/Screenshots-Linux" -m region; else grim -g "$(slurp)" "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"; fi']])) -- drag an area (hyprshot if installed, else grim/slurp)
hl.bind("SUPER + SHIFT + S", hl.dsp.exec_cmd([[sh -c 'grim -g "$(slurp)" "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"']])) -- region; Surface keyboard has no Print key
hl.bind("SUPER + F6", hl.dsp.exec_cmd([[sh -c 'grim "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"']])) -- full screen
hl.bind("SUPER + up", hl.dsp.focus({ direction = "u" }))
hl.bind("SUPER + SHIFT + Print", hl.dsp.exec_cmd([[sh -c 'if command -v hyprshot >/dev/null; then hyprshot -o "$HOME/Pictures/Screenshots-Linux" -m window; else grim "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"; fi']])) -- window (hyprshot) or full screen (fallback)

View File

@@ -0,0 +1,120 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- User Settings
-- Ported from UserConfigs/UserSettings.conf
--
-- refer to Hyprland wiki for more info https://wiki.hyprland.org/Configuring/Variables/
-- NOTE: some settings are in animations.lua / decorations.lua
hl.config({
dwindle = {
preserve_split = true,
-- smart_split = true,
special_scale_factor = 1,
},
master = {
new_status = "master",
new_on_top = true,
mfact = 0.5,
},
general = {
resize_on_border = true,
layout = "dwindle",
},
input = {
kb_layout = "gb",
kb_variant = "",
kb_model = "",
kb_options = "",
kb_rules = "",
repeat_rate = 50,
repeat_delay = 300,
sensitivity = 0.6, -- mouse sensitivity
-- accel_profile = "", -- flat or adaptive or blank or EMPTY means libinput's default mode
numlock_by_default = true,
left_handed = false,
follow_mouse = 1,
float_switch_override_focus = false,
touchpad = {
disable_while_typing = true,
natural_scroll = true,
clickfinger_behavior = false,
middle_button_emulation = false,
tap_to_click = true,
drag_lock = false,
},
-- below for devices with touchdevice ie. touchscreen
touchdevice = {
enabled = true,
},
-- below is for tablet, see link above for proper variables
tablet = {
transform = 0,
left_handed = false,
},
},
-- "gestures" here only holds the tuning knobs. workspace_swipe/workspace_swipe_fingers
-- were removed from this category upstream in favor of hl.gesture() (see below) — and
-- were already commented out in the source .conf, so the swipe gesture itself stays
-- disabled here too, ported as-is.
gestures = {
workspace_swipe_distance = 500,
workspace_swipe_invert = true,
workspace_swipe_min_speed_to_force = 30,
workspace_swipe_cancel_ratio = 0.5,
workspace_swipe_create_new = true,
workspace_swipe_forever = true,
-- workspace_swipe_use_r = true, -- uncomment if wanted a forever create a new workspace with swipe right
},
misc = {
disable_hyprland_logo = true,
disable_splash_rendering = true,
vrr = 2,
mouse_move_enables_dpms = true,
enable_swallow = false,
swallow_regex = "^(kitty)$",
focus_on_activate = false,
initial_workspace_tracking = 0,
middle_click_paste = false,
enable_anr_dialog = true, -- Application not Responding (ANR)
anr_missed_pings = 15, -- ANR Threshold default 1 is too low
},
-- opengl = {
-- nvidia_anti_flicker = true,
-- },
binds = {
workspace_back_and_forth = true,
allow_workspace_cycles = true,
pass_mouse_when_bound = false,
},
-- Could help when scaling and not pixelating
xwayland = {
enabled = true,
force_zero_scaling = true,
},
render = {
direct_scanout = false,
},
cursor = {
sync_gsettings_theme = true,
no_hardware_cursors = 2, -- change to 1 if you want to disable
enable_hyprcursor = true,
warp_on_change_workspace = 2,
no_warps = true,
},
})
-- Uncomment to actually enable the touchpad workspace-swipe gesture (see the note above):
-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })

View File

@@ -0,0 +1,62 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- Commands and Apps to be executed at launch
-- Ported from UserConfigs/Startup_Apps.conf
-- See https://wiki.hypr.land/Configuring/Basics/Autostart/
local scriptsDir = os.getenv("HOME") .. "/.config/hypr/scripts"
hl.on("hyprland.start", function()
-- wallpaper stuff
hl.exec_cmd("awww-daemon --format xrgb")
-- hl.exec_cmd("mpvpaper '*' -o \"load-scripts=no no-audio --loop\" $livewallpaper")
-- Startup
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
-- restart portals once the env is imported, otherwise screen sharing breaks
hl.exec_cmd("sleep 1 && systemctl --user restart xdg-desktop-portal-hyprland && systemctl --user restart xdg-desktop-portal")
-- Sunshine (virtual monitor only on desktop; skipped on laptops, detected by a system battery)
hl.exec_cmd("sleep 1 && ! ls -d /sys/class/power_supply/BAT* >/dev/null 2>&1 && hyprctl output create headless")
-- Polkit (Polkit Gnome / KDE)
hl.exec_cmd(scriptsDir .. "/Polkit.sh")
-- startup apps
hl.exec_cmd("nm-applet --indicator")
hl.exec_cmd("swaync")
-- hl.exec_cmd("ags")
-- hl.exec_cmd("blueman-applet")
-- hl.exec_cmd("rog-control-center")
-- hl.exec_cmd("waybar")
hl.exec_cmd("qs") -- quickshell AGS Desktop Overview alternative
-- clipboard manager
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
-- Starting hypridle to start hyprlock
-- Sunshine streams hold an idle inhibitor (scripts/SunshineIdle.sh) so this never locks mid-stream
hl.exec_cmd("hypridle")
-- Sunshine for streaming media
hl.exec_cmd("sunshine")
-- Float + center the Bitwarden extension pop-out (window rules can't catch it — see the script)
hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh")
hl.dispatch(hl.dsp.focus({ workspace = 3 }))
-- NOTE: the trailing "&&" here is a stray leftover from the original .conf (dangling, does nothing) — ported as-is
hl.exec_cmd("/usr/bin/Plexamp.AppImage &&")
hl.exec_cmd("sleep 3 && vesktop")
hl.dispatch(hl.dsp.focus({ workspace = 1 }))
hl.exec_cmd("bitwarden-desktop --ozone-platform-hint=auto --silent")
hl.exec_cmd("seafile-applet")
-- Here are a list of features available but disabled by default
-- hl.exec_cmd("awww-daemon --format xrgb && awww img " .. os.getenv("HOME") .. "/Pictures/wallpapers/mecha-nostalgia.png") -- persistent wallpaper
-- xdg-desktop-portal-hyprland (should be auto starting. However, you can force to start)
-- hl.exec_cmd(scriptsDir .. "/PortalHyprland.sh")
end)

View File

@@ -2,14 +2,19 @@
general:allow_tearing = 1 general:allow_tearing = 1
general:snap:enabled = 0 general:snap:enabled = 0
misc:vrr = 1 # vrr forced off globally: the per-monitor vrr,0 flag below does NOT override this when
# misc:vrr is 1 (tested live on Hyprland 0.56.2 — DP-2 stayed vrr:true until this global
# was changed). DP-2 (Acer VG240Y S) flickers under VRR when idle in fullscreen (mouse
# stationary -> no repaint damage -> frame delivery stalls near the panel's VRR floor).
# The other two monitors don't negotiate VRR at all, so this has no effect on them.
misc:vrr = 0
# Monitors # Monitors
monitor = DP-2, 1920x1080@165.00Hz, 0x0, 1, bitdepth, 10, cm, wide monitor = DP-2, 1920x1080@165.00Hz, 0x0, 1, bitdepth, 10, cm, wide, vrr, 0
# Keybinds # Keybinds
unbind = SUPER SHIFT, Print unbind = SUPER SHIFT, Print
unbind = SUPER, up unbind = SUPER, up
bind = SUPER, Print, exec, hyprshot -o $HOME/Pictures/Screenshots -m region bind = SUPER, Print, exec, hyprshot -o $HOME/Pictures/Screenshots-Linux -m region
bind = SUPER, up, movefocus, u bind = SUPER, up, movefocus, u
bind = SUPER SHIFT, Print, exec, hyprshot -o $HOME/Pictures/Screenshots -m window bind = SUPER SHIFT, Print, exec, hyprshot -o $HOME/Pictures/Screenshots-Linux -m window

View File

@@ -0,0 +1,42 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- Ported from hyprland.conf — see https://wiki.hyprland.org/
--
-- This mirrors the old source= chain 1:1, just as require()'d Lua modules instead
-- of hyprlang includes. See each file for its origin. The presets/ preset library
-- (anim-*.lua) is also ported but intentionally not required from here — only the
-- currently-active style (appearance/animations.lua) is wired in, exactly like before.
--
-- Modules live in subfolders sorted by concern (core/, appearance/, layout/, input/,
-- presets/) — require() paths below use dotted module names, which Lua resolves to
-- the matching subfolder (e.g. require("core.env") -> core/env.lua).
-- Initial boot script: applies initial wallpapers, theming, new settings etc.
-- suggest not changing/deleting this, including the reference file at ~/.config/hypr/.initial_startup_done
-- as long as that reference file is present, initial-boot.sh will not execute
hl.on("hyprland.start", function()
hl.exec_cmd(os.getenv("HOME") .. "/.config/hypr/initial-boot.sh")
end)
-- ## This is where you want to start tinkering
require("core.startup") -- put your start-up packages in core/startup.lua
require("core.env") -- Environment variables to load
require("layout.windowrules") -- all about Hyprland Window Rules and Layer Rules
require("appearance.decorations") -- Decorations config
require("appearance.animations") -- Animation config
require("input.keybinds") -- Put your own keybinds here
require("core.settings") -- Main Hyprland Settings
-- nwg-displays / hyprdynamicmonitors
require("layout.monitors")
require("layout.workspaces")
-- NOTE: the original hyprland.conf used a plain `exec` (not `exec-once`) here, so it
-- re-ran on every `hyprctl reload`, not just at startup. hl.on("hyprland.start", ...)
-- only fires once at actual compositor start — close enough for a one-shot gsettings
-- call, but worth knowing if you relied on the reload-time re-apply.
hl.on("hyprland.start", function()
hl.exec_cmd("gsettings set org.gnome.desktop.interface cursor-theme Future-black Cursors")
end)
-- HyprMod managed settings
require("core.hyprmod")

View File

@@ -0,0 +1,175 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- Ported from UserConfigs/Keybinds.conf
-- See https://wiki.hypr.land/Configuring/Basics/Binds/ and .../Basics/Dispatchers/
local mainMod = "SUPER"
local scriptsDir = os.getenv("HOME") .. "/.config/hypr/scripts"
local userScripts = os.getenv("HOME") .. "/.config/hypr/UserScripts"
-- settings for user default apps
local edit = os.getenv("EDITOR") or "nvim"
-- These two are for UserKeybinds.conf & Waybar Modules
local term = "foot" -- Terminal
local files = "thunar" -- File Manager
-- Default Search Engine for ROFI Search (SUPER S)
local searchEngine = "https://www.google.com/search?q={}"
-- common shortcuts
hl.bind(mainMod .. " + SPACE", hl.dsp.exec_cmd(".config/rofi/launchers/type-4/launcher.sh")) -- desktop overview (if installed)
hl.bind(mainMod .. " + Q", hl.dsp.window.close()) -- close active (not kill)
hl.bind(mainMod .. " + SHIFT + Q", hl.dsp.exec_cmd(scriptsDir .. "/KillActiveProcess.sh")) -- Kill active process
hl.bind(mainMod .. " + Return", hl.dsp.exec_cmd(term)) -- terminal
hl.bind(mainMod .. " + F", hl.dsp.exec_cmd(files)) -- file manager
hl.bind(mainMod .. " + B", hl.dsp.exec_cmd("zen-browser")) -- Launch Zen Browser New Everytime
hl.bind(mainMod .. " + V", hl.dsp.exec_cmd(scriptsDir .. "/ClipManager.sh")) -- Clipboard Manager
hl.bind(mainMod .. " + C", hl.dsp.exec_cmd("code --ozone-platform=x11")) -- Visual Studio Code
hl.bind(mainMod .. " + O", hl.dsp.exec_cmd("obsidian --ozone-platform=x11")) -- Obsidian
hl.bind(mainMod .. " + X", hl.dsp.exec_cmd("vesktop")) -- Vesktop(Discord)
hl.bind(mainMod .. " + M", hl.dsp.exec_cmd("fdm")) -- Free Download Manager
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd("waypaper")) -- Wayland Wallpaper Setter
hl.bind(mainMod .. " + L", hl.dsp.exec_cmd("hyprlock")) -- Screen Locker
hl.bind(mainMod .. " + DELETE", hl.dsp.exec_cmd("~/.config/rofi/powermenu/type-2/powermenu.sh")) -- Power Menu
hl.bind(mainMod .. " + S", hl.dsp.exec_cmd("cd /home/thomas/hyprmod && uv run hyprmod")) -- Hyprmod - A Hyprland settings manager (if installed)
hl.bind(mainMod .. " + SHIFT + R", hl.dsp.exec_cmd(scriptsDir .. "/Refresh.sh")) -- Refresh waybar, swaync, rofirrr
hl.bind(mainMod .. " + N", hl.dsp.exec_cmd("swaync-client -t -sw")) -- swayNC notification panel
-- FEATURES / EXTRAS
hl.bind(mainMod .. " + SHIFT + F", hl.dsp.window.fullscreen({ mode = "fullscreen" })) -- whole full screen
hl.bind(mainMod .. " + SHIFT + SPACE", hl.dsp.window.float({ action = "toggle" })) -- Float Mode
-- Desktop zooming or magnifier
hl.bind(mainMod .. " + ALT + mouse_down", hl.dsp.exec_cmd([[hyprctl keyword cursor:zoom_factor "$(hyprctl getoption cursor:zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor * 2.0}')"]]))
hl.bind(mainMod .. " + ALT + mouse_up", hl.dsp.exec_cmd([[hyprctl keyword cursor:zoom_factor "$(hyprctl getoption cursor:zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor / 2.0}')"]]))
-- Waybar / Bar related
hl.bind(mainMod .. " + CTRL + ALT + B", hl.dsp.exec_cmd("pkill -SIGUSR1 waybar")) -- Toggle hide/show waybar
-- FEATURES / EXTRAS (UserScripts)
hl.bind(mainMod .. " + SHIFT + A", hl.dsp.exec_cmd(scriptsDir .. "/Animations.sh")) -- hyprland animations menu
hl.bind(mainMod .. " + SHIFT + Z", hl.dsp.exec_cmd(userScripts .. "/ZshChangeTheme.sh")) -- Change oh-my-zsh theme
--------------------------------------------------------------------------------------
-- Default Keybinds
--------------------------------------------------------------------------------------
hl.bind("CTRL + ALT + Delete", hl.dsp.exit()) -- exit Hyprland
-- Master Layout
hl.bind(mainMod .. " + CTRL + D", hl.dsp.layout("removemaster"))
hl.bind(mainMod .. " + I", hl.dsp.layout("addmaster"))
hl.bind(mainMod .. " + J", hl.dsp.layout("cyclenext"))
hl.bind(mainMod .. " + K", hl.dsp.layout("cycleprev"))
hl.bind(mainMod .. " + CTRL + Return", hl.dsp.layout("swapwithmaster"))
-- Dwindle Layout
hl.bind(mainMod .. " + P", hl.dsp.window.pseudo()) -- dwindle
-- Special Keys / Hot Keys
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd(scriptsDir .. "/Volume.sh --inc"), { repeating = true, locked = true }) -- volume up
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd(scriptsDir .. "/Volume.sh --dec"), { repeating = true, locked = true }) -- volume down
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd(scriptsDir .. "/Volume.sh --toggle-mic"), { locked = true }) -- mic mute
hl.bind("XF86AudioMute", hl.dsp.exec_cmd(scriptsDir .. "/Volume.sh --toggle"), { locked = true }) -- mute
hl.bind("XF86Sleep", hl.dsp.exec_cmd("systemctl suspend"), { locked = true }) -- sleep button
hl.bind("XF86RFKill", hl.dsp.exec_cmd(scriptsDir .. "/AirplaneMode.sh"), { locked = true }) -- Airplane mode
-- media controls using keyboards
hl.bind("XF86MediaPlayPause", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --pause"), { locked = true })
hl.bind("XF86AudioPause", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --pause"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --pause"), { locked = true })
hl.bind("XF86AudioNext", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --nxt"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --prv"), { locked = true })
hl.bind("XF86AudioStop", hl.dsp.exec_cmd(scriptsDir .. "/MediaCtrl.sh --stop"), { locked = true })
-- Resize windows
hl.bind(mainMod .. " + SHIFT + left", hl.dsp.window.resize({ x = -50, y = 0, relative = true }), { repeating = true })
hl.bind(mainMod .. " + SHIFT + right", hl.dsp.window.resize({ x = 50, y = 0, relative = true }), { repeating = true })
hl.bind(mainMod .. " + SHIFT + up", hl.dsp.window.resize({ x = 0, y = -50, relative = true }), { repeating = true })
hl.bind(mainMod .. " + SHIFT + down", hl.dsp.window.resize({ x = 0, y = 50, relative = true }), { repeating = true })
-- Move windows
hl.bind(mainMod .. " + CTRL + left", hl.dsp.window.move({ direction = "l" }))
hl.bind(mainMod .. " + CTRL + right", hl.dsp.window.move({ direction = "r" }))
hl.bind(mainMod .. " + CTRL + up", hl.dsp.window.move({ direction = "u" }))
hl.bind(mainMod .. " + CTRL + down", hl.dsp.window.move({ direction = "d" }))
-- Swap windows
hl.bind(mainMod .. " + ALT + left", hl.dsp.window.swap({ direction = "l" }))
hl.bind(mainMod .. " + ALT + right", hl.dsp.window.swap({ direction = "r" }))
hl.bind(mainMod .. " + ALT + up", hl.dsp.window.swap({ direction = "u" }))
hl.bind(mainMod .. " + ALT + down", hl.dsp.window.swap({ direction = "d" }))
-- Move focus with mainMod + arrow keys
hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "l" }))
hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "r" }))
hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "u" }))
hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "d" }))
-- Workspaces related
hl.bind(mainMod .. " + tab", hl.dsp.focus({ workspace = "m+1" }))
hl.bind(mainMod .. " + SHIFT + tab", hl.dsp.focus({ workspace = "m-1" }))
-- Special workspace
-- hl.bind(mainMod .. " + SHIFT + U", hl.dsp.window.move({ workspace = "special" }))
-- hl.bind(mainMod .. " + U", hl.dsp.workspace.toggle_special())
hl.bind(mainMod .. " + U", hl.dsp.workspace.toggle_special("nyx"))
hl.bind(mainMod .. " + SHIFT + U", hl.dsp.window.move({ workspace = "special:nyx" }))
-- Number-row keys (the Lua bind parser does not accept the code:NN keycode syntax)
-- Switch workspaces with mainMod + [0-9]
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }))
end
-- Move active window and follow to workspace mainMod + SHIFT + [0-9]
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i, follow = true }))
end
hl.bind(mainMod .. " + SHIFT + bracketleft", hl.dsp.window.move({ workspace = -1, follow = true })) -- brackets [
hl.bind(mainMod .. " + SHIFT + bracketright", hl.dsp.window.move({ workspace = 1, follow = true })) -- brackets ]
-- Move active window to a workspace silently mainMod + CTRL + [0-9]
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = i, follow = false }))
end
hl.bind(mainMod .. " + CTRL + bracketleft", hl.dsp.window.move({ workspace = -1, follow = false })) -- brackets [
hl.bind(mainMod .. " + CTRL + bracketright", hl.dsp.window.move({ workspace = 1, follow = false })) -- brackets ]
-- Scroll through existing workspaces with mainMod + scroll
hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
hl.bind(mainMod .. " + period", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mainMod .. " + comma", hl.dsp.focus({ workspace = "e-1" }))
-- Move/resize windows with mainMod + LMB/RMB and dragging
hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) -- mouse:272 = left click
hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) -- mouse:273 = right click
--------------------------------------------------------------------------------------
-- Laptop specific keybinds
--------------------------------------------------------------------------------------
-- for disabling Touchpad. hyprctl devices to get device name.
local touchpadDevice = "asue1209:00-04f3:319f-touchpad"
hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd(scriptsDir .. "/BrightnessKbd.sh --dec"), { repeating = true }) -- decrease keyboard brightness
hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd(scriptsDir .. "/BrightnessKbd.sh --inc"), { repeating = true }) -- increase keyboard brightness
hl.bind("XF86Launch1", hl.dsp.exec_cmd("rog-control-center")) -- ASUS Armory crate button
hl.bind("XF86Launch3", hl.dsp.exec_cmd("asusctl led-mode -n")) -- FN+F4 Switch keyboard RGB profile
hl.bind("XF86Launch4", hl.dsp.exec_cmd("asusctl profile -n")) -- FN+F5 change of fan profiles (Quiet, Balance, Performance)
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd(scriptsDir .. "/Brightness.sh --dec"), { repeating = true }) -- decrease monitor brightness
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(scriptsDir .. "/Brightness.sh --inc"), { repeating = true }) -- increase monitor brightness
hl.bind("XF86TouchpadToggle", hl.dsp.exec_cmd(scriptsDir .. "/TouchPad.sh")) -- disable touchpad
-- Screenshot keybindings using F6 (no PrintSrc button)
-- hl.bind(mainMod .. " + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --now")) -- screenshot
-- hl.bind(mainMod .. " + SHIFT + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --area")) -- screenshot (area)
-- hl.bind(mainMod .. " + CTRL + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --in5")) -- screenshot (5 secs delay)
-- hl.bind(mainMod .. " + ALT + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --in10")) -- screenshot (10 secs delay)
-- hl.bind("ALT + F6", hl.dsp.exec_cmd(scriptsDir .. "/ScreenShot.sh --active")) -- screenshot (active window only)
hl.device({
name = touchpadDevice,
enabled = true,
})

View File

@@ -0,0 +1,15 @@
-- Generated with hyprdynamicmonitors freeze.
-- This is a template that you can edit, it is just a starter that pulled your current monitor setup,
-- adjust as needed.
-- You can use templates here etc, see https://github.com/fiffeek/hyprdynamicmonitors/blob/main/examples/basic/hyprconfigs/template.go.tmpl.
-- Monitors are given arbitrary tags (the "monitor" prefix and the ID coming from hyprland).
-- If you are using the TUI to edit, leave this at the end of the file (the last monitor config applies)
-- and leave the markers.
-- Ported from UserConfigs/monitors.conf — hyprdynamicmonitors already supports writing this file
-- directly as monitors.lua once hyprland.lua is active, so re-running it should stay compatible.
-- <<<<< TUI AUTO START
hl.monitor({ output = "desc:LG Electronics LG ULTRAWIDE 0x0002E68A", mode = "2560x1080@60.00000", position = "1920x-740", scale = 1.00000000, transform = 3, vrr = 0 })
hl.monitor({ output = "desc:HP Inc. HP 524pf VNG447017D", mode = "1920x1080@100.00000", position = "-1920x0", scale = 1.00000000, transform = 0, vrr = 0 })
hl.monitor({ output = "desc:Acer Technologies VG240Y S 0x126191DE", mode = "1920x1080@165.00301", position = "0x0", scale = 1.00000000, transform = 0, vrr = 0, bitdepth = 10, cm = "wide" })
hl.monitor({ output = "HEADLESS-1", mode = "1728x1080@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })
-- <<<<< TUI AUTO END

View File

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

View File

@@ -0,0 +1,30 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- NOTE: This will be overwritten by NWG-Displays
-- once you use it and click apply.
-- (nwg-displays already supports writing this file directly as workspaces.lua once
-- hyprland.lua is active, so re-running it should stay compatible.)
--
-- Ported from UserConfigs/workspaces.conf
-- You can set workspace rules to achieve workspace-specific behaviors.
-- For instance, you can define a workspace where all windows are drawn without borders or gaps.
-- https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/
-- Assigning workspace to a certain monitor. Below are just examples
hl.workspace_rule({ workspace = 1, monitor = "DP-2", default = true })
hl.workspace_rule({ workspace = 2, monitor = "DP-1", default = true })
hl.workspace_rule({ workspace = 3, monitor = "HDMI-A-1", default = true })
hl.workspace_rule({ workspace = 9, monitor = "HEADLESS-1", default = true })
-- hl.workspace_rule({ workspace = 4, monitor = "eDP-1" })
-- hl.workspace_rule({ workspace = 5, monitor = "DP-2" })
-- hl.workspace_rule({ workspace = 6, monitor = "DP-2" })
-- hl.workspace_rule({ workspace = 7, monitor = "DP-2" })
-- hl.workspace_rule({ workspace = 8, monitor = "DP-2" })
-- example rules (from wiki)
-- hl.workspace_rule({ workspace = 3, rounding = false, decorate = false })
-- hl.workspace_rule({ workspace = "name:coding", rounding = false, decorate = false, gaps_in = 0, gaps_out = 0, border = false, monitor = "DP-1" })
-- hl.workspace_rule({ workspace = 8, border_size = 8 })
-- hl.workspace_rule({ workspace = "name:Hello", monitor = "DP-1", default = true })
-- hl.workspace_rule({ workspace = "name:gaming", monitor = "desc:Chimei Innolux Corporation 0x150C", default = true })
-- hl.workspace_rule({ workspace = 5, on_created_empty = "[float] firefox" })
-- hl.workspace_rule({ workspace = "special:scratchpad", on_created_empty = "foot" })

View File

@@ -0,0 +1,22 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- Ported from animations/00-default.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "smoothOut", style = "slide" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 100, bezier = "liner", style = "loop" }) -- used by rainbow borders and rotating colors; speed clamped from 180 -> Lua hl.animation caps speed at 100
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "smoothOut" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "overshot" })
-- animations for -git or version >0.42.0
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })

View File

@@ -0,0 +1,25 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- old animations
-- Ported from animations/01-default - v2.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("myBezier", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("linear", { type = "bezier", points = { { 0.0, 0.0 }, { 1.0, 1.0 } } })
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("slow", { type = "bezier", points = { { 0, 0.85 }, { 0.3, 1 } } })
hl.curve("overshot", { type = "bezier", points = { { 0.7, 0.6 }, { 0.1, 1.1 } } })
hl.curve("bounce", { type = "bezier", points = { { 1.1, 1.6 }, { 0.1, 0.85 } } })
hl.curve("sligshot", { type = "bezier", points = { { 1, -1 }, { 0.15, 1.25 } } })
hl.curve("nice", { type = "bezier", points = { { 0, 6.9 }, { 0.5, -4.20 } } })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "slow", style = "popin" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "popin" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "linear" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 100, bezier = "linear", style = "loop" }) -- used by rainbow borders and rotating colors; speed clamped from 180 -> Lua hl.animation caps speed at 100
hl.animation({ leaf = "fade", enabled = true, speed = 5, bezier = "overshot" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
hl.animation({ leaf = "windows", enabled = true, speed = 5, bezier = "bounce", style = "popin" })

View File

@@ -0,0 +1,4 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- Ported from animations/03- Disable Animation.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = false } })

View File

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

View File

@@ -0,0 +1,21 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Default"
-- credit https://github.com/prasanthrangan/hyprdots
-- Ported from animations/HYDE - default.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "once" })
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 5, bezier = "wind", style = "slidevert" })

View File

@@ -0,0 +1,20 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Minimal-1"
-- credit https://github.com/prasanthrangan/hyprdots
-- Ported from animations/HYDE - minimal-1.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "once" })
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })

View File

@@ -0,0 +1,14 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Minimal-2"
-- credit https://github.com/prasanthrangan/hyprdots
-- Ported from animations/HYDE - minimal-2.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("quart", { type = "bezier", points = { { 0.25, 1 }, { 0.5, 1 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "quart", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 6, bezier = "quart" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 6, bezier = "quart" })
hl.animation({ leaf = "fade", enabled = true, speed = 6, bezier = "quart" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "quart" })

View File

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

View File

@@ -0,0 +1,32 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Vertical"
-- credit https://github.com/prasanthrangan/hyprdots
-- Ported from animations/HYDE - Vertical.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("fluent_decel", { type = "bezier", points = { { 0, 0.2 }, { 0.4, 1 } } })
hl.curve("easeOutCirc", { type = "bezier", points = { { 0, 0.55 }, { 0.45, 1 } } })
hl.curve("easeOutCubic", { type = "bezier", points = { { 0.33, 1 }, { 0.68, 1 } } })
hl.curve("easeinoutsine", { type = "bezier", points = { { 0.37, 0 }, { 0.63, 1 } } })
-- Windows
hl.animation({ leaf = "windowsIn", enabled = true, speed = 1.5, bezier = "easeinoutsine", style = "popin 60%" }) -- window open
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.5, bezier = "easeOutCubic", style = "popin 60%" }) -- window close.
hl.animation({ leaf = "windowsMove", enabled = true, speed = 1.5, bezier = "easeinoutsine", style = "slide" }) -- everything in between, moving, dragging, resizing.
-- Fading
hl.animation({ leaf = "fade", enabled = true, speed = 2.5, bezier = "fluent_decel" })
hl.animation({ leaf = "fadeLayersIn", enabled = false })
hl.animation({ leaf = "border", enabled = false })
-- Layers
hl.animation({ leaf = "layers", enabled = true, speed = 1.5, bezier = "easeinoutsine", style = "popin" })
-- Workspaces
-- styles: slide, slidevert, fade, slidefade, slidefadevert
-- hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "fluent_decel", style = "slidefade 30%" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "fluent_decel", style = "slidefadevert 30%" })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 2, bezier = "fluent_decel", style = "slidefade 10%" })

View File

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

View File

@@ -0,0 +1,27 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Me-2"
-- credit https://github.com/mahaveergurjar
-- Ported from animations/Mahaveer - me-2.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("smoothOut", { type = "bezier", points = { { 0.5, 0 }, { 0.99, 0.99 } } })
hl.curve("smoothIn", { type = "bezier", points = { { 0.5, -0.5 }, { 0.68, 1.5 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "smoothOut", style = "slide" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 100, bezier = "liner", style = "loop" }) -- used by rainbow borders and rotating colors; speed clamped from 180 -> Lua hl.animation caps speed at 100
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "smoothOut" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "overshot" })
-- animations for -git or version >0.42.0
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 5, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })

View File

@@ -0,0 +1,15 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Classic"
-- credit https://github.com/mylinuxforwork/dotfiles
-- Ported from animations/ML4W - classic.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("myBezier", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 7, bezier = "myBezier" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 7, bezier = "default", style = "popin 80%" })
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 8, bezier = "default" })
hl.animation({ leaf = "fade", enabled = true, speed = 7, bezier = "default" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "default" })

View File

@@ -0,0 +1,20 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Dynamic"
-- credit https://github.com/mylinuxforwork/dotfiles
-- Ported from animations/ML4W - dynamic.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "loop" })
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })

View File

@@ -0,0 +1,24 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Fast"
-- credit https://github.com/mylinuxforwork/dotfiles
-- Ported from animations/ML4W - fast.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
hl.curve("md3_standard", { type = "bezier", points = { { 0.2, 0 }, { 0, 1 } } })
hl.curve("md3_decel", { type = "bezier", points = { { 0.05, 0.7 }, { 0.1, 1 } } })
hl.curve("md3_accel", { type = "bezier", points = { { 0.3, 0 }, { 0.8, 0.15 } } })
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.1 } } })
hl.curve("crazyshot", { type = "bezier", points = { { 0.1, 1.5 }, { 0.76, 0.92 } } })
hl.curve("hyprnostretch", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.0 } } })
hl.curve("fluent_decel", { type = "bezier", points = { { 0.1, 1 }, { 0, 1 } } })
hl.curve("easeInOutCirc", { type = "bezier", points = { { 0.85, 0 }, { 0.15, 1 } } })
hl.curve("easeOutCirc", { type = "bezier", points = { { 0, 0.55 }, { 0.45, 1 } } })
hl.curve("easeOutExpo", { type = "bezier", points = { { 0.16, 1 }, { 0.3, 1 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 3, bezier = "md3_decel", style = "popin 60%" })
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "fade", enabled = true, speed = 2.5, bezier = "md3_decel" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 3.5, bezier = "easeOutExpo", style = "slide" })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "md3_decel", style = "slidevert" })

View File

@@ -0,0 +1,20 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "High"
-- credit https://github.com/mylinuxforwork/dotfiles
-- Ported from animations/ML4W - high.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "once" })
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })

View File

@@ -0,0 +1,19 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Moving"
-- credit https://github.com/mylinuxforwork/dotfiles
-- Ported from animations/ML4W - moving.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("overshot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("smoothOut", { type = "bezier", points = { { 0.5, 0 }, { 0.99, 0.99 } } })
hl.curve("smoothIn", { type = "bezier", points = { { 0.5, -0.5 }, { 0.68, 1.5 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 5, bezier = "overshot", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "smoothOut" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 3, bezier = "smoothOut" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 4, bezier = "smoothIn", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 5, bezier = "default" })
hl.animation({ leaf = "fade", enabled = true, speed = 5, bezier = "smoothIn" })
hl.animation({ leaf = "fadeDim", enabled = true, speed = 5, bezier = "smoothIn" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "default" })

View File

@@ -0,0 +1,15 @@
-- HyprFlux — https://github.com/ahmad9059/HyprFlux
-- name "Standard"
-- credit https://github.com/mylinuxforwork/dotfiles
-- Ported from animations/ML4W - standard.conf (unused preset — not required by anything)
hl.config({ animations = { enabled = true } })
hl.curve("myBezier", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 7, bezier = "myBezier" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 7, bezier = "default", style = "popin 80%" })
hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 8, bezier = "default" })
hl.animation({ leaf = "fade", enabled = true, speed = 7, bezier = "default" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "default" })

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Float + resize (to 781x702) + center the Bitwarden browser-extension pop-out window.
#
# Why this exists instead of a window rule: Hyprland's `float`/`center`/`size`
# rules are "static" — evaluated exactly once, at the instant a window is
# created — and this popup opens with the generic title "Zen Browser" (same
# initialTitle/initialClass as the main browser window), only updating to
# "Extension: (Bitwarden Password Manager) - Bitwarden..." a moment after
# it's already mapped. By then the static rule has already missed its one
# chance and there's no field available at creation time to tell the popup
# apart from a normal browser window. See layout/windowrules.lua and
# https://github.com/hyprwm/Hyprland/issues/7402 for the same problem class.
#
# No socat/ncat is installed, so instead of an event-driven IPC listener this
# just polls `hyprctl clients` at a short interval with jq (already
# installed).
#
# Dispatch syntax note: Hyprland 0.55+'s Lua config changed `hyprctl dispatch`
# to take a literal Lua expression calling hl.dsp.* (e.g.
# `hyprctl dispatch 'hl.dsp.window.center()'`), not the old
# "centerwindow"-style dispatcher strings. Verified live against the real
# API in /usr/share/hypr/stubs/hl.meta.lua before wiring this in.
#
# Why it re-asserts for a few seconds instead of firing once: right after the
# popup leaves the tiled layout, Hyprland briefly re-tiles the sibling window
# to fill the freed space, and during that reflow this popup was observed
# reporting `floating: false` again for one poll tick — snapping it back to
# its old tiled geometry. Re-asserting float+center for a short settle window
# after first detection rides that out. After the settle window it's left
# alone, so a manual re-tile/move by you later isn't fought.
TITLE_PATTERN='Extension: \(Bitwarden Password Manager\)'
POLL_INTERVAL=0.3
SETTLE_TICKS=6 # ~2s of re-assertion after first detection, at POLL_INTERVAL
TARGET_WIDTH=781
TARGET_HEIGHT=702
declare -A ticks_left # address -> remaining re-assert ticks
while true; do
sleep "$POLL_INTERVAL"
# Forget any tracked window that's no longer open.
open_addresses=$(hyprctl clients -j | jq -r '.[].address')
for addr in "${!ticks_left[@]}"; do
grep -qF "$addr" <<<"$open_addresses" || unset 'ticks_left[$addr]'
done
# Process substitution (not a pipe) so the while loop runs in this shell
# and its writes to ticks_left actually persist across iterations.
while IFS= read -r win; do
title=$(jq -r '.title' <<<"$win")
[[ "$title" =~ $TITLE_PATTERN ]] || continue
address=$(jq -r '.address' <<<"$win")
floating=$(jq -r '.floating' <<<"$win")
# First time we've seen this window: start its settle window.
[[ -v ticks_left[$address] ]] || ticks_left[$address]=$SETTLE_TICKS
# Already floating and past its settle window: leave it alone.
[[ "$floating" == "true" && ${ticks_left[$address]} -le 0 ]] && continue
hyprctl dispatch "hl.dsp.focus({ window = \"address:$address\" })" >/dev/null
hyprctl dispatch 'hl.dsp.window.float({ action = "enable" })' >/dev/null
# resize({x=W,y=H}) sets the absolute size (not a delta) — verified live.
# Must happen before center(), since resizing shifts what "centered" means.
hyprctl dispatch "hl.dsp.window.resize({ x = $TARGET_WIDTH, y = $TARGET_HEIGHT })" >/dev/null
hyprctl dispatch 'hl.dsp.window.center()' >/dev/null
ticks_left[$address]=$((ticks_left[$address] - 1))
done < <(hyprctl clients -j | jq -c '.[] | {address, title, floating}')
done

View File

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

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

View File

@@ -0,0 +1,93 @@
#:schema https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json
version = 2
final_space = true
console_title_template = '{{ .Shell }} in {{ .Folder }}'
[[blocks]]
type = 'prompt'
alignment = 'left'
newline = true
[[blocks.segments]]
type = 'path'
style = 'plain'
background = 'transparent'
foreground = 'blue'
template = '{{ .Path }}'
[blocks.segments.properties]
style = 'full'
[[blocks.segments]]
type = 'git'
style = 'plain'
foreground = 'p:grey'
background = 'transparent'
template = ' {{ .HEAD }}{{ if or (.Working.Changed) (.Staging.Changed) }}*{{ end }} <cyan>{{ if gt .Behind 0 }}⇣{{ end }}{{ if gt .Ahead 0 }}⇡{{ end }}</>'
[blocks.segments.properties]
branch_icon = ''
commit_icon = '@'
fetch_status = true
[[blocks.segments]]
type = "pnpm"
style = "powerline"
powerline_symbol = "❯"
foreground = "blue"
background = "transparent"
template = " {{ .Full }} "
[[blocks.segments]]
type = 'docker'
style = 'powerline'
foreground = 'blue'
background = 'transparent'
template = ' {{ .Context }} '
[blocks.segments.options]
display_mode = 'files'
[[blocks]]
type = 'rprompt'
overflow = 'hidden'
[[blocks.segments]]
type = 'executiontime'
style = 'plain'
foreground = 'yellow'
background = 'transparent'
template = '{{ .FormattedMs }}'
[blocks.segments.properties]
threshold = 5000
[[blocks]]
type = 'prompt'
alignment = 'left'
newline = true
[[blocks.segments]]
type = 'text'
style = 'plain'
foreground_templates = [
"{{if gt .Code 0}}red{{end}}",
"{{if eq .Code 0}}magenta{{end}}",
]
background = 'transparent'
template = '❯'
[transient_prompt]
foreground_templates = [
"{{if gt .Code 0}}red{{end}}",
"{{if eq .Code 0}}magenta{{end}}",
]
background = 'transparent'
template = '❯ '
[secondary_prompt]
foreground = 'magenta'
background = 'transparent'
template = '❯❯ '

View File

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

View File

@@ -0,0 +1,4 @@
[ColorScheme]
active_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff1e66f5, #ffeff1f5, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
disabled_colors=#ff6c6f85, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff6c6f85, #ff6c6f85, #ff6c6f85, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff9ca0b0, #ff5c5f77, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
inactive_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ffccd0da, #ff6c6f85, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1

View File

@@ -0,0 +1,4 @@
[ColorScheme]
active_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ff1e1e2e, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
disabled_colors=#ffa6adc8, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffa6adc8, #ffa6adc8, #ffa6adc8, #ff1e1e2e, #ff11111b, #ff7f849c, #ff89b4fa, #ff45475a, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
inactive_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ffa6adc8, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c

View File

@@ -0,0 +1,32 @@
[Appearance]
color_scheme_path=
custom_palette=true
icon_theme=Papirus-Dark
standard_dialogs=default
style=Fusion
[Fonts]
fixed="Adwaita Sans,10,-1,5,50,0,0,0,0,0,Regular"
general="Adwaita Sans,12,-1,5,50,0,0,0,0,0,Regular"
[Interface]
activate_item_on_single_click=1
buttonbox_layout=0
cursor_flash_time=1000
dialog_buttons_have_icons=1
double_click_interval=400
gui_effects=General, AnimateMenu, AnimateCombo, AnimateTooltip, AnimateToolBox
keyboard_scheme=2
menus_have_icons=true
show_shortcuts_in_context_menus=true
stylesheets=@Invalid()
toolbutton_style=4
underline_shortcut=1
wheel_scroll_lines=3
[SettingsWindow]
geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3\0\0\0\0\x2\0\0\0\a\x80\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3)
[Troubleshooting]
force_raster_widgets=1
ignored_applications=@Invalid()

View File

@@ -0,0 +1,4 @@
[ColorScheme]
active_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff1e66f5, #ffeff1f5, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
disabled_colors=#ff6c6f85, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff6c6f85, #ff6c6f85, #ff6c6f85, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ff9ca0b0, #ff5c5f77, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1
inactive_colors=#ff4c4f69, #ffe6e9ef, #ff6c6f85, #ff7c7f93, #ffbcc0cc, #ff9ca0b0, #ff4c4f69, #ff4c4f69, #ff4c4f69, #ffeff1f5, #ffe6e9ef, #ff8c8fa1, #ffccd0da, #ff6c6f85, #ff7287fd, #ffe64553, #ffeff1f5, #ff4c4f69, #ffdce0e8, #ff4c4f69, #808c8fa1

View File

@@ -0,0 +1,4 @@
[ColorScheme]
active_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ff1e1e2e, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
disabled_colors=#ffa6adc8, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffa6adc8, #ffa6adc8, #ffa6adc8, #ff1e1e2e, #ff11111b, #ff7f849c, #ff89b4fa, #ff45475a, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c
inactive_colors=#ffcdd6f4, #ff1e1e2e, #ffa6adc8, #ff9399b2, #ff45475a, #ff6c7086, #ffcdd6f4, #ffcdd6f4, #ffcdd6f4, #ff1e1e2e, #ff181825, #ff7f849c, #ff89b4fa, #ffa6adc8, #ff89b4fa, #fff38ba8, #ff1e1e2e, #ffcdd6f4, #ff11111b, #ffcdd6f4, #807f849c

View File

@@ -0,0 +1,32 @@
[Appearance]
color_scheme_path=
custom_palette=true
icon_theme=Papirus-Dark
standard_dialogs=default
style=Fusion
[Fonts]
fixed="Adwaita Sans,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1,Regular"
general="Adwaita Sans,12,-1,5,400,0,0,0,0,0,0,0,0,0,0,1,Regular"
[Interface]
activate_item_on_single_click=1
buttonbox_layout=0
cursor_flash_time=1000
dialog_buttons_have_icons=1
double_click_interval=400
gui_effects=General, AnimateMenu, AnimateCombo, AnimateTooltip, AnimateToolBox
keyboard_scheme=2
menus_have_icons=true
show_shortcuts_in_context_menus=true
stylesheets=@Invalid()
toolbutton_style=4
underline_shortcut=1
wheel_scroll_lines=3
[SettingsWindow]
geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3\0\0\0\0\x2\0\0\0\a\x80\0\0\0\0\0\0\0\0\0\0\x5?\0\0\x2\xf3)
[Troubleshooting]
force_raster_widgets=1
ignored_applications=@Invalid()

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

@@ -0,0 +1,29 @@
pragma Singleton
import QtQuick
QtObject {
readonly property string fontFamily: "CaskaydiaCove Nerd Font Mono"
readonly property string iconFontFamily: "Material Symbols Outlined"
// Same glyph set, softer/rounded terminals — used where a module wants
// a visually distinct icon style from the rest of the bar (currently
// just BluetoothIndicator.qml) without switching every icon over.
readonly property string iconFontFamilyRounded: "Material Symbols Rounded"
// Material Symbols is a variable font. Left unset, glyphs fall back to the
// font's default optical size (24px), which is tuned for larger rendering
// and makes some icons (e.g. "lock") look heavier/bigger than others when
// drawn small — pin "opsz" near the actual render size to keep icons
// visually consistent. See Appearance.iconSize for the matching pixelSize.
readonly property var iconFontAxes: ({ "FILL": 0, "wght": 400, "GRAD": 0, "opsz": 20 })
readonly property int iconSize: 16
readonly property int fontSize: 13
readonly property int fontSizeSmall: 11
readonly property int radius: 14
readonly property int radiusSmall: 8
readonly property int barHeight: 34
readonly property int spacing: 10
readonly property int animationDuration: 150
}

View File

@@ -0,0 +1,62 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Wallpaper-driven palette, sourced from pywal's ~/.cache/wal/colors.json.
// Run `wal -i /path/to/wallpaper.png` to (re)generate it — this file watches
// the cache and updates live, no shell restart needed. Until that's been run
// once, everything falls back to a hand-picked dark palette below.
QtObject {
id: root
readonly property bool active: _parsed !== null
property var _parsed: null
// --- Fallback palette (used until pywal has run) ---
readonly property color fallbackBg: "#1a1b26"
readonly property color fallbackFg: "#c0caf5"
readonly property var fallbackPalette: [
"#15161e", "#f7768e", "#9ece6a", "#e0af68",
"#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6",
"#414868", "#f7768e", "#9ece6a", "#e0af68",
"#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"
]
function paletteColor(i) {
if (_parsed && _parsed.colors)
return _parsed.colors["color" + i] || fallbackPalette[i]
return fallbackPalette[i]
}
readonly property color background: _parsed && _parsed.special ? _parsed.special.background : fallbackBg
readonly property color foreground: _parsed && _parsed.special ? _parsed.special.foreground : fallbackFg
readonly property color primary: paletteColor(4)
// Named primaryText (not "onPrimary") because QML treats any "onXxx"
// identifier as a signal handler slot, not a regular property.
readonly property color primaryText: background
readonly property color urgent: paletteColor(1)
// paletteColor(8) alone renders quite dark/low-contrast for text (it's
// meant as a muted *background* tone in most wal palettes) — lighten it
// so muted labels stay legible against the bar's dark background.
readonly property color muted: Qt.lighter(paletteColor(8), 2.2)
readonly property color surface: Qt.lighter(background, 1.35)
// QtObject has no default property, so the FileView is attached via a
// property assignment rather than as a plain child element.
readonly property FileView _walFile: FileView {
path: Quickshell.env("HOME") + "/.cache/wal/colors.json"
watchChanges: true
onLoaded: {
try {
root._parsed = JSON.parse(text())
} catch (e) {
console.warn("Colors: failed to parse pywal cache:", e)
root._parsed = null
}
}
onLoadFailed: (error) => {
root._parsed = null
}
}
}

View File

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

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

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

@@ -0,0 +1,6 @@
module config
singleton Colors 1.0 Colors.qml
singleton Appearance 1.0 Appearance.qml
singleton VpnExclusivity 1.0 VpnExclusivity.qml
singleton SystemUpdates 1.0 SystemUpdates.qml
singleton WeatherData 1.0 WeatherData.qml

View File

@@ -0,0 +1,13 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import "../../config"
Text {
Layout.maximumWidth: 320
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
text: Hyprland.activeToplevel ? Hyprland.activeToplevel.title : ""
}

View File

@@ -0,0 +1,62 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
import "../../config"
PanelWindow {
id: bar
anchors {
top: true
left: true
right: true
}
implicitHeight: Appearance.barHeight
exclusiveZone: implicitHeight
color: "transparent"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "quickshell:bar"
Rectangle {
anchors.fill: parent
color: Colors.background
}
RowLayout {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: Appearance.spacing
}
spacing: Appearance.spacing
Workspaces { screen: bar.screen }
ActiveWindow {}
}
RowLayout {
anchors.centerIn: parent
spacing: Appearance.spacing
Weather { barWindow: bar }
Clock {}
SystemUpdate { barWindow: bar }
}
RowLayout {
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: Appearance.spacing
}
spacing: Appearance.spacing
PinnedTray {}
TailscaleIndicator { barWindow: bar }
VpnIndicator { barWindow: bar }
NetworkIndicator { barWindow: bar }
BluetoothIndicator { barWindow: bar }
Volume { barWindow: bar }
BatteryIndicator { barWindow: bar }
PowerButton { barWindow: bar }
}
}

View File

@@ -0,0 +1,605 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.UPower
import "../../config"
// Battery + power-profile panel, ported from omarchy's power panel
// (github.com/basecamp/omarchy shell/plugins/panels/power/{Model.js,Panel.qml})
// onto this project's own conventions: Quickshell's native UPower/PowerProfiles
// bindings instead of omarchy's `omarchy-battery-status` /
// `omarchy-powerprofiles-list` / `omarchy-powerprofiles-set` helper scripts
// (which don't exist outside omarchy), and Material Symbols glyphs instead of
// Nerd Font ones. The charge-threshold ("Holding"/limited-charge) concept from
// the original is hardware-specific (vendor charge-limit sysfs knobs) and has
// no equivalent in Quickshell's UPower binding, so it's dropped rather than
// guessed at; "Battery health" (computed from sysfs energy_full/design) is shown in its
// place, using data UPower actually reports.
Item {
id: root
readonly property var device: UPower.displayDevice
// isLaptopBattery (type === Battery && powerSupply === true) is the
// Quickshell-recommended check for "is this actually a battery" — more
// precise than isPresent, which the docs note is only meaningful once
// you already know the device is of Battery type.
readonly property bool batteryPresent: !!(root.device && root.device.ready && root.device.isLaptopBattery)
// Hidden entirely on a desktop with no battery — RowLayout skips
// invisible children for both sizing and spacing, so this just vanishes
// from the bar rather than leaving a gap.
visible: root.batteryPresent
implicitWidth: root.batteryPresent ? label.implicitWidth : 0
implicitHeight: Appearance.barHeight
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
// anchor to the bar itself rather than to this icon, keeping it a fixed
// distance from the bar and the screen edge regardless of where this
// icon sits in the row.
required property var barWindow
readonly property bool onBattery: UPower.onBattery
// Named deviceState, not "state" — Item already has a built-in `state`
// property (the property-based state-machine one) that this would
// otherwise collide with.
readonly property int deviceState: root.batteryPresent ? root.device.state : UPowerDeviceState.Unknown
readonly property bool fullyCharged: root.deviceState === UPowerDeviceState.FullyCharged
readonly property bool charging: root.batteryPresent && !root.onBattery && !root.fullyCharged
// device.percentage is a 0..1 fraction despite the name (same convention
// Volume.qml relies on for Pipewire's audio.volume).
readonly property real fraction: root.batteryPresent ? Math.max(0, Math.min(1, root.device.percentage)) : 0
readonly property bool low: root.batteryPresent && root.fraction <= 0.15 && !root.charging
// Quickshell's UPower binding has no charge-cycle property (see the
// file-header comment on why the old charge-until threshold was
// dropped) — it comes straight from sysfs instead.
//
// device.nativePath turned out to be empty on real hardware (tested
// live), so rather than build a path from it, a shell glob finds
// whichever BAT* directory actually exists — same shell-out-to-a-CLI
// convention TailscaleIndicator.qml uses for its own live stats.
// This barely changes during a session, so one read at startup is
// enough; no polling timer.
property int cycleCount: -1
Process {
id: cycleCountProc
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/cycle_count\" 2>/dev/null"]
running: root.batteryPresent
stdout: StdioCollector {
id: cycleCountCollector
onStreamFinished: {
const n = parseInt(cycleCountCollector.text.trim(), 10)
root.cycleCount = (isNaN(n) || n <= 0) ? -1 : n
}
}
}
// device.healthPercentage/healthSupported come from upowerd over D-Bus,
// and upowerd has been observed reporting energy-full as a flat 0 (a
// live daemon glitch, `upower -i` shows it too) while sysfs still has
// the real numbers underneath — same class of unreliability that put
// cycle_count on sysfs above instead of trusting upowerd's own
// charge-cycles field. Health is computed the same way UPower computes
// it (current full-charge capacity over the battery's original design
// capacity), just read directly so a daemon hiccup doesn't blank it.
property real energyFullWh: -1
property real energyFullDesignWh: -1
readonly property real healthPercent: (root.energyFullWh > 0 && root.energyFullDesignWh > 0)
? Math.min(100, (root.energyFullWh / root.energyFullDesignWh) * 100) : -1
Process {
id: energyFullProc
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_full\" 2>/dev/null"]
running: root.batteryPresent
stdout: StdioCollector {
id: energyFullCollector
onStreamFinished: {
const n = parseFloat(energyFullCollector.text.trim())
root.energyFullWh = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
}
}
}
Process {
id: energyFullDesignProc
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_full_design\" 2>/dev/null"]
running: root.batteryPresent
stdout: StdioCollector {
id: energyFullDesignCollector
onStreamFinished: {
const n = parseFloat(energyFullDesignCollector.text.trim())
root.energyFullDesignWh = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
}
}
}
// device.timeToFull/timeToEmpty are computed by Quickshell from
// energy/energyCapacity/changeRate — when upowerd's EnergyFull glitches
// to 0 (the same daemon flake HEALTH routes around above), that formula
// divides by a bogus capacity and comes out negative rather than a
// real estimate (confirmed live: timeToFull sat at -3751 while `upower
// -i` itself still reported a normal 26.7 minutes). This recomputes
// the same estimate straight from sysfs as a fallback, only used when
// Quickshell's own value isn't positive. Unlike cycle_count/design
// capacity above, energy_now and power_now change continuously while
// (dis)charging, so this polls on a timer instead of reading once.
property real sysfsEnergyNowWh: -1
property real sysfsPowerNowW: -1
readonly property real fallbackTimeToFullSeconds: (root.sysfsPowerNowW > 0 && root.energyFullWh > 0 && root.sysfsEnergyNowWh >= 0)
? Math.max(0, (root.energyFullWh - root.sysfsEnergyNowWh) / root.sysfsPowerNowW * 3600) : -1
readonly property real fallbackTimeToEmptySeconds: (root.sysfsPowerNowW > 0 && root.sysfsEnergyNowWh > 0)
? (root.sysfsEnergyNowWh / root.sysfsPowerNowW * 3600) : -1
// Prefers upowerd's own live estimate when it's sane; only falls back
// to the sysfs-computed one when upowerd's has gone zero/negative.
readonly property real timeRemainingSeconds: {
const primary = root.onBattery ? root.device.timeToEmpty : root.device.timeToFull
if (primary > 0) return primary
return root.onBattery ? root.fallbackTimeToEmptySeconds : root.fallbackTimeToFullSeconds
}
Timer {
id: sysfsRateTimer
interval: 15000
repeat: true
running: root.batteryPresent
triggeredOnStart: true
onTriggered: {
if (!sysfsEnergyNowProc.running) sysfsEnergyNowProc.running = true
if (!sysfsPowerNowProc.running) sysfsPowerNowProc.running = true
}
}
Process {
id: sysfsEnergyNowProc
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/energy_now\" 2>/dev/null"]
stdout: StdioCollector {
id: sysfsEnergyNowCollector
onStreamFinished: {
const n = parseFloat(sysfsEnergyNowCollector.text.trim())
root.sysfsEnergyNowWh = (isNaN(n) || n < 0) ? -1 : (n / 1000000)
}
}
}
Process {
id: sysfsPowerNowProc
command: ["sh", "-c", "cat \"$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n1)/power_now\" 2>/dev/null"]
stdout: StdioCollector {
id: sysfsPowerNowCollector
onStreamFinished: {
const n = parseFloat(sysfsPowerNowCollector.text.trim())
root.sysfsPowerNowW = (isNaN(n) || n <= 0) ? -1 : (n / 1000000)
}
}
}
// Raw UPower state string (Charging/Discharging/PendingCharge/…) — more
// granular than modeLabel below, which collapses those down to the
// three phrases the hero text and STATE row show.
readonly property string rawStateLabel: root.batteryPresent
? UPowerDeviceState.toString(root.deviceState) : "—"
// Material Symbols' charging glyphs aren't an evenly-spaced "_bar"
// scale like the non-charging ones below — they're fixed at these
// seven breakpoints (confirmed against the installed font's ligature
// table), so this picks the highest one at or below the current
// percentage rather than rounding to an index.
function chargingIcon(fraction) {
const percent = fraction * 100
const thresholds = [20, 30, 50, 60, 80, 90, 100]
const icons = [
"battery_charging_20", "battery_charging_30", "battery_charging_50", "battery_charging_60",
"battery_charging_80", "battery_charging_90", "battery_charging_full"
]
for (let i = thresholds.length - 1; i >= 0; i--) {
if (percent >= thresholds[i]) return icons[i]
}
// Below 20% there's no lower charging glyph — it's still the
// truthful floor of what's available while current is flowing in.
return icons[0]
}
function batteryIcon() {
if (!root.batteryPresent) return "battery_unknown"
if (root.fullyCharged) return "battery_full"
if (root.charging) return root.chargingIcon(root.fraction)
if (root.fraction <= 0.1) return "battery_alert"
const bars = ["battery_0_bar", "battery_1_bar", "battery_2_bar", "battery_3_bar", "battery_4_bar", "battery_5_bar", "battery_full"]
return bars[Math.max(0, Math.min(bars.length - 1, Math.round(root.fraction * (bars.length - 1))))]
}
function formatDuration(seconds) {
const s = Math.max(0, Math.round(seconds || 0))
if (s <= 0) return "—"
const h = Math.floor(s / 3600)
const m = Math.round((s % 3600) / 60)
return h > 0 ? (h + "h " + m + "m") : (m + "m")
}
readonly property string modeLabel: {
if (!root.batteryPresent) return ""
if (root.fullyCharged) return "Fully charged"
return root.onBattery ? "On battery" : "Charging"
}
// Info grid rows — same shape as NetworkIndicator.qml's statRows/
// statCells: a flat label/value list, flattened again into individual
// grid cells (label, value, label, value, …) so a 4-column GridLayout
// lays them out as two label/value pairs per row.
readonly property var statRows: [
{
label: "HEALTH",
value: root.healthPercent > 0 ? Math.round(root.healthPercent) + "%" : "—"
},
{
label: root.onBattery ? "TIME LEFT" : "TIME TO FULL",
value: root.batteryPresent ? root.formatDuration(root.timeRemainingSeconds) : "—"
},
{ label: "STATUS", value: root.rawStateLabel },
{ label: "CYCLES", value: root.cycleCount > 0 ? String(root.cycleCount) : "—" }
]
readonly property var statCells: {
const cells = []
for (let i = 0; i < root.statRows.length; i++) {
cells.push({ text: root.statRows[i].label, isLabel: true })
cells.push({ text: root.statRows[i].value, isLabel: false })
}
return cells
}
// Cute rotating status phrases while current is actually flowing —
// ported as-is from omarchy's power panel, which pairs the same phrase
// sets with the same charging/on-battery states.
readonly property var chargingPhrases: [
"Pumping power", "Injecting electrons", "Pouring juice", "Amassing watts",
"Hoarding joules", "Sucking volts", "Topping reserves", "Soaking amps", "Inhaling kilowatts"
]
readonly property var onBatteryPhrases: [
"Slurping power", "Spending joules", "Draining watts", "Burning electrons",
"Sipping juice", "Spending coulombs", "Bleeding amps", "Guzzling volts", "Munching reserves"
]
readonly property var activePhrases: root.fullyCharged ? []
: (root.charging ? root.chargingPhrases : (root.onBattery ? root.onBatteryPhrases : []))
readonly property bool rotatingPhrases: root.activePhrases.length > 0
property int phraseIndex: 0
readonly property string heroStatusText: root.rotatingPhrases
? root.activePhrases[root.phraseIndex % root.activePhrases.length]
: root.modeLabel
// Only rotates while the panel is open and actually charging/discharging.
Timer {
id: phraseTimer
interval: 2800
running: panel.visible && root.rotatingPhrases
repeat: true
triggeredOnStart: false
onTriggered: phraseSwap.restart()
}
SequentialAnimation {
id: phraseSwap
PropertyAnimation { target: heroStatus; property: "opacity"; to: 0.0; duration: 180; easing.type: Easing.OutQuad }
ScriptAction {
script: {
const n = root.activePhrases.length
if (n > 0) root.phraseIndex = (root.phraseIndex + 1) % n
}
}
PropertyAnimation { target: heroStatus; property: "opacity"; to: 1.0; duration: 260; easing.type: Easing.InQuad }
}
// If we leave a rotating state mid-swap (e.g. unplugged mid-fade),
// snap back to full opacity so the new state reads immediately.
Connections {
target: root
function onRotatingPhrasesChanged() {
if (!root.rotatingPhrases) {
phraseSwap.stop()
heroStatus.opacity = 1.0
}
}
}
// --- Power profile list ------------------------------------------------
// power-profiles-daemon only ever exposes these three fixed profiles;
// Performance is conditional on hasPerformanceProfile (e.g. absent on
// battery-only/thermally-limited hardware) — same gating omarchy's own
// Model.js applies via its profile-list script.
readonly property var profileEntries: {
const entries = [
{ value: PowerProfile.PowerSaver, label: "Saver", icon: "eco" },
{ value: PowerProfile.Balanced, label: "Balanced", icon: "balance" }
]
if (PowerProfiles.hasPerformanceProfile)
entries.push({ value: PowerProfile.Performance, label: "Performance", icon: "bolt" })
return entries
}
// --- Automatic power profile --------------------------------------
// Battery-state-driven, on top of the manual picker above: below 20%
// → Power Saver (protect what's left), plugged in → Performance,
// unplugged and healthy → Balanced. Low battery wins even if a weak
// charger is still plugged in — running low is the more urgent signal.
// Performance only gets requested when the daemon actually reports it
// as available, falling back to Balanced instead of erroring on
// hardware without a performance profile (e.g. the ERROR this exact
// widget hit before power-profiles-daemon was installed).
//
// This only ever writes when the *target* changes (crossing the 20%
// line, plugging/unplugging) rather than on every poll, so a manual
// pick from the buttons above still sticks until the next real
// transition instead of being fought immediately.
readonly property int desiredProfile: {
if (root.fraction <= 0.20) return PowerProfile.PowerSaver
if (!root.onBattery) return PowerProfiles.hasPerformanceProfile ? PowerProfile.Performance : PowerProfile.Balanced
return PowerProfile.Balanced
}
function applyDesiredProfile() {
if (!root.batteryPresent) return
if (PowerProfiles.profile !== root.desiredProfile) PowerProfiles.profile = root.desiredProfile
}
onDesiredProfileChanged: root.applyDesiredProfile()
Component.onCompleted: Qt.callLater(root.applyDesiredProfile)
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: root.low ? Colors.urgent : Colors.foreground
text: root.batteryIcon()
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
panel.visible = !panel.visible
grab.active = panel.visible
}
}
// --- Reusable pieces -----------------------------------------------
component ProfileButton: Rectangle {
id: profBtn
required property string label
required property string icon
required property bool active
signal activate()
width: parent ? parent.width : 0
radius: 0
implicitHeight: 44
color: mouse.containsMouse ? Colors.primary
: (profBtn.active ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
// Hover-only outline, offset outside the button rather than drawn on
// its own edge — same treatment as this project's other selectable
// rows (e.g. Volume.qml's DeviceRow).
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: mouse.containsMouse ? 1 : 0
}
Column {
anchors.centerIn: parent
spacing: 2
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: profBtn.icon
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 18
color: mouse.containsMouse ? Colors.primaryText : Colors.foreground
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: profBtn.label
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
font.bold: profBtn.active
color: mouse.containsMouse ? Colors.primaryText : Colors.foreground
}
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: profBtn.activate()
}
}
// --- Panel -----------------------------------------------------------
PopupWindow {
id: panel
visible: false
color: "transparent"
// Wider than before (244) to give the 2-column info grid's
// label/value pairs enough room per half without eliding —
// same reasoning as NetworkIndicator.qml's own wider panel.
readonly property int contentWidth: 280
readonly property int contentPadding: 16
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
anchor {
window: root.barWindow
// Unlike anchor.item, anchor.window doesn't default rect to the
// window's own bounds — set it explicitly so edges/gravity below
// resolve against the full bar (and thus the true screen edges)
// instead of a near-zero rect at the window's origin.
rect.x: 0
rect.y: 0
rect.width: root.barWindow.width
rect.height: root.barWindow.height
edges: Edges.Bottom | Edges.Right
gravity: Edges.Bottom | Edges.Left
margins.bottom: -4
margins.right: 0
}
HyprlandFocusGrab {
id: grab
windows: [panel]
onCleared: panel.visible = false
}
Rectangle {
anchors.fill: parent
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 3
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: panel.contentPadding
spacing: 10
// Hero: icon · title/status · percentage
RowLayout {
width: parent.width
spacing: 10
Text {
text: root.batteryIcon()
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 30
color: root.low ? Colors.urgent : Colors.foreground
}
Column {
Layout.fillWidth: true
spacing: 2
Text {
width: parent.width
text: "Battery"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: 18
font.bold: true
color: Colors.foreground
}
Text {
id: heroStatus
width: parent.width
text: root.heroStatusText
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
Text {
text: Math.round(root.fraction * 100) + "%"
font.family: Appearance.fontFamily
font.pixelSize: 20
font.bold: true
color: Colors.foreground
}
}
// Charge level bar
Item {
width: parent.width
height: 4
Rectangle {
anchors.fill: parent
radius: 0
color: Colors.muted
opacity: 0.35
}
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
radius: 0
color: root.low ? Colors.urgent : Colors.primary
width: Math.max(height, parent.width * root.fraction)
Behavior on width { NumberAnimation { duration: 320; easing.type: Easing.OutCubic } }
}
}
// Stats — a 2-column, 2-row info grid, same layout
// NetworkIndicator.qml uses for its own live stats: a
// 4-column GridLayout fed a flattened label/value/label/value
// list, so each row holds two label/value pairs side by side
// with the values right-aligned against their own half.
GridLayout {
width: parent.width
columns: 4
columnSpacing: 12
rowSpacing: 5
Repeater {
model: root.statCells
Text {
required property var modelData
Layout.fillWidth: !modelData.isLabel
horizontalAlignment: modelData.isLabel ? Text.AlignLeft : Text.AlignRight
text: modelData.text
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: modelData.isLabel ? Colors.muted : Colors.foreground
}
}
}
// Power profile picker
Column {
width: parent.width
spacing: 6
Text {
text: "POWER PROFILE"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Row {
id: profileRow
width: parent.width
spacing: 6
readonly property real cellWidth: root.profileEntries.length > 0
? (width - spacing * (root.profileEntries.length - 1)) / root.profileEntries.length
: 0
Repeater {
model: root.profileEntries
ProfileButton {
required property var modelData
width: profileRow.cellWidth
label: modelData.label
icon: modelData.icon
active: PowerProfiles.profile === modelData.value
onActivate: PowerProfiles.profile = modelData.value
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,795 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Bluetooth
import "../../config"
Item {
id: root
// Independent left/right breathing room around the glyph. Material
// Symbols icons aren't monospaced — each one's font-reported advance
// width (label.implicitWidth) bakes in its own side bearings, so the
// same "+4, centered" math that every other bar icon uses can still
// look tighter/looser icon-to-icon. Tune these two by eye instead of
// the icon's own bounding box to even it out against its neighbours.
property real iconLeftPadding: 0
property real iconRightPadding: 0
implicitWidth: root.iconLeftPadding + label.implicitWidth + root.iconRightPadding
implicitHeight: Appearance.barHeight
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
// anchor to the bar itself rather than to this icon, keeping it a fixed
// distance from the bar and the screen edge regardless of where this
// icon sits in the row.
required property var barWindow
readonly property var adapter: Bluetooth.defaultAdapter
// Scan only while the panel is open — same convention as the WiFi
// scannerEnabled binding in NetworkIndicator.qml.
Binding {
target: root.adapter
property: "discovering"
value: panel.visible && root.adapter.enabled
when: root.adapter !== null
}
// --- Device row helpers ----------------------------------------------
// Rows are plain objects rather than the live BluetoothDevice QObject —
// BlueZ churn (pairing timeouts, a device dropping out of range mid-scan)
// can destroy the object while a delegate still holds it, the same
// crash risk noted for WiFi networks in NetworkIndicator.qml. The actual
// object is re-resolved by address only when an action needs it, via
// deviceForAddress().
function deviceLabel(d) {
return (d.deviceName || d.name || d.address || "").trim()
}
// BlueZ's Icon property is a freedesktop icon-theme name (verified
// against a real paired device on this machine: "audio-headset") — map
// the common categories to a Material Symbols glyph, falling back to a
// generic "devices_other" for anything unrecognized.
function deviceIcon(icon) {
const i = icon || ""
if (i.indexOf("headset") !== -1 || i.indexOf("headphone") !== -1) return "headphones"
if (i.indexOf("mouse") !== -1) return "mouse"
if (i.indexOf("keyboard") !== -1) return "keyboard"
if (i.indexOf("gaming") !== -1 || i.indexOf("joystick") !== -1) return "sports_esports"
if (i.indexOf("tablet") !== -1) return "tablet"
if (i.indexOf("camera") !== -1) return "videocam"
if (i.indexOf("printer") !== -1) return "print"
if (i.indexOf("watch") !== -1) return "watch"
if (i.indexOf("tv") !== -1) return "tv"
if (i.indexOf("computer") !== -1) return "computer"
if (i.indexOf("card") !== -1) return "speaker"
return "devices_other"
}
function deviceRow(d) {
if (!d) return null
const label = root.deviceLabel(d)
if (!label) return null
return {
address: d.address || "",
label: label,
icon: root.deviceIcon(d.icon),
connected: !!d.connected,
paired: !!d.paired,
bonded: !!d.bonded,
trusted: !!d.trusted,
pairing: !!d.pairing,
state: d.state,
batteryAvailable: !!d.batteryAvailable,
battery: d.battery || 0
}
}
readonly property var deviceRows: root.adapter
? root.adapter.devices.values.map(root.deviceRow).filter(function(r) { return r !== null })
: []
readonly property var connectedRows: root.deviceRows.filter(function(r) { return r.connected })
readonly property var knownRows: root.deviceRows.filter(function(r) {
return !r.connected && (r.paired || r.bonded || r.trusted)
})
readonly property var discoveredRows: root.deviceRows.filter(function(r) {
return !r.connected && !r.paired && !r.bonded && !r.trusted
})
function deviceForAddress(address) {
if (!root.adapter) return null
const devs = root.adapter.devices.values
for (let i = 0; i < devs.length; i++) {
if (devs[i].address === address) return devs[i]
}
return null
}
// --- Action feedback ---------------------------------------------------
// BlueZ operations here are fire-and-forget from QML's side — connect()/
// disconnect()/pair()/forget() report failure only via a C++-side log
// line (see quickshell's bluetooth/device.cpp), nothing reaches QML. So
// "did it work" has to be inferred from the device's own properties
// instead of a success/failure callback. Shown as a line right under the
// specific row it's about (DeviceRow.statusText below) rather than a
// panel-wide banner — actionStatusAddress is how a row knows a message
// is meant for it; since it's address-keyed rather than tied to a
// particular DeviceRow instance, the message still finds its row even
// when the device jumps sections (Paired → Connected on a successful
// connect, say), because that's a new DeviceRow for the same address.
// - busyStatus is purely reactive — it just reads pairing/state off
// whichever row is currently mid-operation, however that operation
// was triggered. It's not shown directly (each row already derives
// the same "Connecting…"/"Disconnecting…" off its own dev.state);
// it only exists here to tell resolvePendingAction when to fire.
// - pendingAction+the busyStatus watcher below is how a *specific*
// click gets its own outcome message: it snapshots what was asked
// for, waits for busyStatus to clear, then checks whether the
// expected end state (connected/paired) actually landed.
readonly property string busyStatus: {
for (const r of root.deviceRows) {
if (r.pairing) return "pairing:" + r.address
if (r.state === BluetoothDeviceState.Connecting) return "connecting:" + r.address
if (r.state === BluetoothDeviceState.Disconnecting) return "disconnecting:" + r.address
}
return ""
}
property var pendingAction: null
property string actionStatus: ""
property string actionStatusAddress: ""
// Checks whether pendingAction's expected end state actually landed and
// reports it, then clears pendingAction. Called both when busyStatus
// clears normally and from pendingTimeoutTimer below — an operation
// that resolves inside a single binding re-evaluation (a device already
// in range reconnecting near-instantly, say) can flip busyStatus from ""
// straight back to "" without ever visibly changing, which would
// otherwise leave pendingAction stuck forever and misattribute whatever
// busyStatus transition happens to come along next.
function resolvePendingAction() {
if (!root.pendingAction) return
const p = root.pendingAction
root.pendingAction = null
const d = root.deviceForAddress(p.address)
root.actionStatusAddress = p.address
if (p.kind === "disconnect") {
root.actionStatus = (d && !d.connected) ? "Disconnected" : "Couldn't disconnect"
} else if (p.kind === "pair") {
root.actionStatus = (d && d.paired) ? "Paired" : "Couldn't pair"
} else {
root.actionStatus = (d && d.connected) ? "Connected" : "Couldn't connect"
}
actionStatusTimer.restart()
}
onBusyStatusChanged: if (root.busyStatus === "") root.resolvePendingAction()
Timer {
id: actionStatusTimer
interval: 3500
repeat: false
onTriggered: root.actionStatus = ""
}
// Backstop for the "resolved before busyStatus ever visibly changed"
// case above, and a genuine timeout for an operation that's just stuck
// (device out of range, etc.) — either way, restarted whenever
// pendingAction is (re)set, so it always reflects the latest click.
Timer {
id: pendingTimeoutTimer
interval: 8000
repeat: false
onTriggered: root.resolvePendingAction()
}
function connectDevice(row) {
const d = root.deviceForAddress(row.address)
if (!d) return
// Already-paired devices just reconnect; a fresh discovery result
// has to pair first (BlueZ's own agent handles the SSP/passkey
// handshake — there's no PIN-entry surface exposed by this API).
if (row.paired || row.bonded || row.trusted) {
root.pendingAction = { address: row.address, kind: "connect" }
pendingTimeoutTimer.restart()
d.connect()
} else {
root.pendingAction = { address: row.address, kind: "pair" }
pendingTimeoutTimer.restart()
d.pair()
}
}
function disconnectDevice(row) {
const d = root.deviceForAddress(row.address)
if (!d) return
root.pendingAction = { address: row.address, kind: "disconnect" }
pendingTimeoutTimer.restart()
d.disconnect()
}
function forgetDevice(row) {
const d = root.deviceForAddress(row.address)
if (!d) return
if (d.connected) d.disconnect()
d.forget()
// No feedback line here — forgetting removes this row from every
// list immediately, so there's nothing left to show a message
// under. The row vanishing is the confirmation.
}
function setTrusted(row, trusted) {
const d = root.deviceForAddress(row.address)
if (!d) return
d.trusted = trusted
root.actionStatusAddress = row.address
root.actionStatus = trusted ? "Trusting" : "No longer trusting"
actionStatusTimer.restart()
}
readonly property bool anyConnected: root.connectedRows.length > 0
// Caps how tall the Paired/Available list gets before it scrolls
// instead of continuing to grow the popup — same convention as
// NetworkIndicator.qml's Known/Other Networks list.
readonly property int maxDeviceListHeight: 260
Text {
id: label
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.iconLeftPadding
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: (root.adapter && root.adapter.enabled) ? Colors.foreground : Colors.muted
text: {
if (!root.adapter || !root.adapter.enabled) return "bluetooth_disabled"
return root.anyConnected ? "bluetooth_connected" : "bluetooth"
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
panel.visible = !panel.visible
grab.active = panel.visible
}
}
// --- Reusable pieces ---------------------------------------------------
component ToggleSwitch: Item {
id: toggleRoot
property bool checked: false
signal toggled()
implicitWidth: 34
implicitHeight: 18
Rectangle {
anchors.fill: parent
radius: 0
color: toggleRoot.checked ? Colors.primary : Colors.muted
opacity: toggleRoot.checked ? 1 : 0.35
}
// Hover-only outline, offset outside the switch itself rather than
// drawn on its edge — the switch lags behind a click until the
// backend confirms it (e.g. the adapter's enabled state), so this
// is just a "you can click this" cue, not click feedback.
Rectangle {
anchors.fill: parent
anchors.margins: -4
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: toggleMouse.containsMouse ? 1 : 0
}
Rectangle {
width: parent.height - 4
height: parent.height - 4
radius: 0
color: Colors.background
anchors.verticalCenter: parent.verticalCenter
x: toggleRoot.checked ? parent.width - width - 2 : 2
Behavior on x { NumberAnimation { duration: Appearance.animationDuration } }
}
MouseArea {
id: toggleMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: toggleRoot.toggled()
}
}
// One device row. `showForget` gates the whole trailing action cluster —
// trust toggle, disconnect, forget — for discovered-but-unpaired devices,
// which have none of those to act on yet (a plain click there pairs
// instead, via activate()). For a paired/connected row, the row body
// itself still connects/disconnects (activate()) same as before; the
// actions below are the explicit, discoverable versions of that plus
// the two this row previously had no way to reach at all.
component DeviceRow: Rectangle {
id: devRow
required property var dev
property bool showForget: true
signal activate()
width: parent ? parent.width : 0
radius: 0
implicitHeight: rowContent.implicitHeight + 12
// Which action icon (if any) the pointer is currently over — drives
// both that icon's own hover color below and rowMouse's click
// dispatch. Derived from rowMouse.mouseX/containsMouse rather than a
// MouseArea on each icon: those icons live inside rowContent, which
// sits underneath rowMouse in stacking order, so a nested MouseArea
// there would just be shadowed by it (same reason
// NetworkIndicator.qml's NetworkRow dispatches by position too).
// Checked right-to-left since that's the order the icons are laid
// out in, so an earlier icon's "from here to the row's right edge"
// zone doesn't swallow the ones after it.
readonly property string hoveredAction: {
if (!rowMouse.containsMouse) return ""
if (devRow.showForget && forgetIcon.visible
&& rowMouse.mouseX >= devRow.mapFromItem(forgetIcon, 0, 0).x - 8) return "forget"
if (devRow.showForget && trustIcon.visible
&& rowMouse.mouseX >= devRow.mapFromItem(trustIcon, 0, 0).x - 8) return "trust"
if (disconnectIcon.visible
&& rowMouse.mouseX >= devRow.mapFromItem(disconnectIcon, 0, 0).x - 8) return "disconnect"
return ""
}
// Hover checked first (same ordering as Volume.qml's DeviceRow):
// hovering always shows a solid Colors.primary fill, even for an
// already-connected row, so the primaryText used below never has to
// sit on top of the dimmer translucent tint instead.
color: rowMouse.containsMouse ? Colors.primary
: (devRow.dev.connected ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
// Hover-only outline, offset outside the row rather than drawn on
// its own edge — same treatment as NetworkIndicator.qml's NetworkRow.
// The lists this feeds are inset by 3px (see deviceListColumn and
// the Connected section below) so this has room without being
// cropped by deviceListFlick's clip.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: rowMouse.containsMouse ? 1 : 0
}
RowLayout {
id: rowContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 6
spacing: 8
Text {
Layout.preferredWidth: 20
Layout.minimumWidth: 20
Layout.maximumWidth: 20
horizontalAlignment: Text.AlignLeft
text: devRow.dev.icon
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
text: devRow.dev.label
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
font.bold: devRow.dev.connected
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
Text {
readonly property string statusText: {
if (devRow.dev.pairing) return "Pairing…"
if (devRow.dev.state === BluetoothDeviceState.Connecting) return "Connecting…"
if (devRow.dev.state === BluetoothDeviceState.Disconnecting) return "Disconnecting…"
// The outcome of the last action taken on this
// specific device (connect/disconnect/pair/trust) —
// see the "Action feedback" comment above
// root.busyStatus. Matched by address rather than by
// this being "the same row" that was clicked, since
// a successful connect/disconnect moves the device
// to a different section (a new DeviceRow entirely).
if (root.actionStatusAddress === devRow.dev.address && root.actionStatus !== "")
return root.actionStatus
if (devRow.dev.connected && devRow.dev.batteryAvailable)
return Math.round(devRow.dev.battery * 100) + "%"
if (devRow.dev.connected) return "Connected"
return ""
}
visible: statusText !== ""
text: statusText
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
}
}
// Each action below is a fixed-size slot with its own
// hover-halo outline — offset outside the slot rather than drawn
// on its own edge, same treatment as devRow's own outline above
// and every other hoverable box in this file (ToggleSwitch,
// DeviceRow itself, etc.) — instead of just the color swap the
// row's own icons get.
// Disconnect without forgetting — same effect as clicking the
// row body on a Connected row (activate() below calls
// disconnectDevice too), just given its own explicit, always-
// visible icon instead of relying on that being guessed.
Item {
id: disconnectIcon
visible: devRow.showForget && devRow.dev.connected
Layout.preferredWidth: 20
Layout.preferredHeight: 20
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: devRow.hoveredAction === "disconnect" ? 1 : 0
}
Text {
anchors.centerIn: parent
text: "link_off"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
// Stays primaryText (dark) rather than switching to
// Colors.primary while targeted — devRow's own fill is
// already Colors.primary on hover, so that would've put
// a primary-colored glyph on a primary-colored
// background. The outline Rectangle above is what shows
// which slot is targeted.
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
}
}
// Trust toggle — full brightness when trusted, dimmed when not,
// same "state via opacity" convention PowerButton.qml uses for
// its disabled entries.
Item {
id: trustIcon
visible: devRow.showForget
Layout.preferredWidth: 20
Layout.preferredHeight: 20
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: devRow.hoveredAction === "trust" ? 1 : 0
}
Text {
anchors.centerIn: parent
text: "verified_user"
opacity: devRow.dev.trusted ? 1 : 0.4
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
}
}
Item {
id: forgetIcon
visible: devRow.showForget
Layout.preferredWidth: 20
Layout.preferredHeight: 20
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: devRow.hoveredAction === "forget" ? 1 : 0
}
Text {
anchors.centerIn: parent
text: "close"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.muted
}
}
}
MouseArea {
id: rowMouse
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: rowContent.height + 12
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
// Dispatches on devRow.hoveredAction rather than its own
// MouseArea per icon — see that property's comment above.
onClicked: {
switch (devRow.hoveredAction) {
case "forget": root.forgetDevice(devRow.dev); break
case "trust": root.setTrusted(devRow.dev, !devRow.dev.trusted); break
case "disconnect": root.disconnectDevice(devRow.dev); break
default: devRow.activate()
}
}
}
}
// --- Panel -------------------------------------------------------------
PopupWindow {
id: panel
visible: false
color: "transparent"
// Wider than the other panels' usual 244 — each device row now ends
// in a row of three 20px action slots (disconnect/trust/forget)
// instead of a single close icon, and the name column needs room to
// not immediately start eliding against that.
readonly property int contentWidth: 300
readonly property int contentPadding: 16
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
// Connected/paired/discovered devices come and go while the panel
// is open — animate the resize instead of snapping, same as the
// network panel's WiFi list.
Behavior on implicitHeight {
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
}
anchor {
window: root.barWindow
rect.x: 0
rect.y: 0
rect.width: root.barWindow.width
rect.height: root.barWindow.height
edges: Edges.Bottom | Edges.Right
gravity: Edges.Bottom | Edges.Left
margins.bottom: -4
margins.right: 0
}
HyprlandFocusGrab {
id: grab
windows: [panel]
onCleared: panel.visible = false
}
Rectangle {
anchors.fill: parent
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 3
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: panel.contentPadding
spacing: 10
// Header
RowLayout {
width: parent.width
spacing: 8
Text {
text: label.text
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 21
color: Colors.foreground
}
Text {
Layout.fillWidth: true
text: "Bluetooth"
font.family: Appearance.fontFamily
font.pixelSize: 18
font.bold: true
color: Colors.foreground
}
// Spins continuously while root.adapter.discovering is
// true — a persistent "still searching" cue independent
// of the list itself, unlike the "Scanning for devices…"
// text below (which only shows while the device list is
// still empty).
Text {
id: scanSpinner
visible: root.adapter !== null && root.adapter.discovering
text: "progress_activity"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.muted
RotationAnimation on rotation {
running: scanSpinner.visible
loops: Animation.Infinite
from: 0
to: 360
duration: 900
}
}
ToggleSwitch {
visible: root.adapter !== null
checked: root.adapter !== null && root.adapter.enabled
onToggled: root.adapter.enabled = !root.adapter.enabled
}
}
Text {
width: parent.width
visible: root.adapter === null
text: "No Bluetooth adapter"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Text {
width: parent.width
visible: root.adapter !== null && !root.adapter.enabled
text: "Turn Bluetooth on to see devices"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
// Connected — always shown in full above the scrollable
// list, mirroring the network panel's layout.
Column {
width: parent.width
spacing: 4
visible: root.connectedRows.length > 0
Text {
text: "Connected"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Column {
// Inset 3px on each side to match deviceListColumn
// below, so DeviceRow's hover halo has room here too.
x: 3
width: parent.width - 6
spacing: 2
Repeater {
model: root.connectedRows
DeviceRow {
required property var modelData
dev: modelData
showForget: true
onActivate: root.disconnectDevice(dev)
}
}
}
}
// Paired — always shown in full, same as Connected above and
// for the same reason: these are one click away from
// reconnecting (root.connectDevice reconnects rather than
// re-pairing whenever a row is already paired/bonded/trusted),
// so they shouldn't be buried in the capped-height scroll area
// below or hidden behind an active scan turning up other
// nearby devices.
Column {
width: parent.width
spacing: 4
visible: root.knownRows.length > 0
Text {
text: "Paired Devices"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Column {
// Inset 3px on each side to match deviceListColumn
// below, so DeviceRow's hover halo has room here too.
x: 3
width: parent.width - 6
spacing: 2
Repeater {
model: root.knownRows
DeviceRow {
required property var modelData
dev: modelData
showForget: true
onActivate: root.connectDevice(dev)
}
}
}
}
Text {
width: parent.width
visible: root.adapter !== null && root.adapter.enabled && root.deviceRows.length === 0
text: (root.adapter && root.adapter.discovering) ? "Scanning for devices…" : "No devices found"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
// Available — freshly discovered, unpaired devices only.
// Capped height, scrolls once there are more than fit rather
// than growing the popup indefinitely.
Flickable {
id: deviceListFlick
width: parent.width
height: Math.min(deviceListColumn.implicitHeight, root.maxDeviceListHeight)
contentWidth: width
contentHeight: deviceListColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
visible: deviceListColumn.implicitHeight > 0
Column {
id: deviceListColumn
// Inset 3px on every side so DeviceRow's hover halo
// has room without being cropped by deviceListFlick's
// clip. Left/right needs the manual x/width shift
// (children bind width: parent.width, which ignores
// Column's own padding); top/bottom just needs
// padding, since that's along Column's own layout
// axis and Flickable's contentHeight tracks it.
x: 3
width: deviceListFlick.width - 6
topPadding: 3
bottomPadding: 3
spacing: 6
Column {
width: parent.width
spacing: 4
visible: root.adapter !== null && root.adapter.discovering && root.discoveredRows.length > 0
Text {
text: "Available Devices"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Column {
width: parent.width
spacing: 2
Repeater {
model: root.discoveredRows
DeviceRow {
required property var modelData
dev: modelData
showForget: false
onActivate: root.connectDevice(dev)
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
import QtQuick
import "../../config"
Text {
id: root
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
font.bold: true
color: Colors.foreground
text: Qt.formatDateTime(new Date(), "ddd d MMM hh:mm")
Timer {
interval: 1000
running: true
repeat: true
onTriggered: root.text = Qt.formatDateTime(new Date(), "ddd d MMM hh:mm")
}
}

File diff suppressed because it is too large Load Diff

View File

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

@@ -0,0 +1,200 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth + 2
implicitHeight: Appearance.barHeight
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
// anchor to the bar itself rather than to this icon, keeping it a fixed
// distance from the bar and the screen edge regardless of where this
// icon sits in the row.
required property var barWindow
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.foreground
text: "settings_power"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
menu.visible = !menu.visible
grab.active = menu.visible
}
}
Process { id: proc }
PopupWindow {
id: menu
visible: false
color: "transparent"
// Same contentWidth/contentPadding split as the other panels
// (Volume.qml, NetworkIndicator.qml, etc.) rather than a single
// magic implicitWidth.
readonly property int contentWidth: 178
readonly property int contentPadding: 16
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
anchor {
window: root.barWindow
// Unlike anchor.item, anchor.window doesn't default rect to the
// window's own bounds — set it explicitly so edges/gravity below
// resolve against the full bar (and thus the true screen edges)
// instead of a near-zero rect at the window's origin.
rect.x: 0
rect.y: 0
rect.width: root.barWindow.width
rect.height: root.barWindow.height
edges: Edges.Bottom | Edges.Right
gravity: Edges.Bottom | Edges.Left
margins.bottom: -4
margins.right: 0
}
HyprlandFocusGrab {
id: grab
windows: [menu]
onCleared: menu.visible = false
}
Rectangle {
anchors.fill: parent
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 3
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: menu.contentPadding
spacing: 10
// Header — same title layout as Volume.qml's panel: the bar
// icon large, plus a bold title.
RowLayout {
id: header
width: parent.width
spacing: 8
Text {
text: label.text
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 23
color: Colors.foreground
}
Text {
Layout.fillWidth: true
text: "Power"
font.family: Appearance.fontFamily
font.pixelSize: 18
font.bold: true
color: Colors.foreground
}
}
Column {
width: parent.width
spacing: 2
Repeater {
id: list
model: [
{ label: "Lock", icon: "lock", cmd: ["hyprlock"] },
{ label: "Suspend", icon: "bedtime", cmd: ["systemctl", "suspend"] },
{ label: "Hibernate", icon: "ac_unit", cmd: ["systemctl", "hibernate"] },
{ label: "Log out", icon: "logout", cmd: ["hyprctl", "dispatch", "exit"] },
{ label: "Reboot", icon: "restart_alt", cmd: ["systemctl", "reboot"] },
{ label: "Shut down", icon: "power_settings_new", cmd: ["systemctl", "poweroff"] }
]
// Same shape as this project's other selectable rows
// (e.g. VpnIndicator.qml's ProviderRow,
// TailscaleIndicator.qml's ExitNodeRow): radius 0, a
// fixed-width icon column followed by a fillWidth
// label, and an offset hover halo rather than one
// drawn on the row's own edge.
delegate: Rectangle {
id: entry
required property var modelData
width: parent ? parent.width : 0
radius: 0
implicitHeight: entryContent.implicitHeight + 12
color: (!entry.modelData.disabled && entryMouse.containsMouse) ? Colors.primary : "transparent"
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: entryMouse.containsMouse ? 1 : 0
}
RowLayout {
id: entryContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 6
spacing: 8
Text {
Layout.preferredWidth: 20
Layout.minimumWidth: 20
Layout.maximumWidth: 20
horizontalAlignment: Text.AlignLeft
text: entry.modelData.icon
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
opacity: entry.modelData.disabled ? 0.4 : 1
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
Text {
Layout.fillWidth: true
text: entry.modelData.label
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
opacity: entry.modelData.disabled ? 0.4 : 1
color: entryMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
}
MouseArea {
id: entryMouse
anchors.fill: parent
enabled: !entry.modelData.disabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
proc.command = entry.modelData.cmd
proc.running = true
menu.visible = false
grab.active = false
}
}
}
}
}
}
}
}
}

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config"
RowLayout {
spacing: Appearance.spacing
Repeater {
model: SystemTray.items
delegate: IconImage {
id: trayIcon
required property var modelData
implicitWidth: 16
implicitHeight: 16
source: modelData.icon
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton)
trayIcon.modelData.activate()
else
trayIcon.modelData.secondaryActivate()
}
}
}
}
}

View File

@@ -0,0 +1,506 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Pipewire
import "../../config"
Item {
id: root
implicitWidth: label.implicitWidth
implicitHeight: Appearance.barHeight
// The bar's own PanelWindow — passed in from Bar.qml so this popup can
// anchor to the bar itself rather than to this icon, keeping it a fixed
// distance from the bar and the screen edge regardless of where this
// icon sits in the row.
required property var barWindow
readonly property var sink: Pipewire.defaultAudioSink
readonly property var source: Pipewire.defaultAudioSource
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
readonly property real outputVolume: (root.sink && root.sink.audio) ? root.sink.audio.volume : 0
readonly property bool outputMuted: (root.sink && root.sink.audio) ? root.sink.audio.muted : false
readonly property real inputVolume: (root.source && root.source.audio) ? root.source.audio.volume : 0
readonly property bool inputMuted: (root.source && root.source.audio) ? root.source.audio.muted : false
readonly property var candidateSinks: root.nodes.filter(function(n) { return n && n.isSink && !n.isStream })
readonly property var candidateSources: root.nodes.filter(function(n) { return n && !n.isSink && !n.isStream && n.audio })
PwObjectTracker { objects: root.candidateSinks }
PwObjectTracker { objects: root.candidateSources }
// Live mic level for the input gauge below — separate from inputVolume
// (the configured gain), this tracks the actual incoming signal.
// Only runs while the panel is open, to skip metering when unused.
PwNodePeakMonitor {
id: inputPeak
node: root.source
enabled: panel.visible
}
PwNodePeakMonitor {
id: outputPeak
node: root.sink
enabled: panel.visible
}
// --- Device label/icon heuristics, ported from omarchy's audio panel
// Model.js (github.com/basecamp/omarchy shell/plugins/panels/audio/Model.js),
// adapted to this project's Material Symbols icon set instead of Nerd Font.
function friendlyDeviceLabel(text) {
var out = String(text || "").trim()
out = out.replace(/^sof-soundwire\s+/i, "")
out = out.replace(/^built-?in audio\s+/i, "")
out = out.replace(/\s+Output$/i, "")
out = out.replace(/\s+Input$/i, "")
out = out.replace(/Microphones/gi, "Microphone")
return out
}
function deviceLabel(node) {
if (!node) return "Unknown"
var nick = friendlyDeviceLabel(node.nickname || "")
if (nick) return nick
return friendlyDeviceLabel(node.description || node.name || "Unknown")
}
function deviceBlob(node) {
if (!node) return ""
var p = node.properties || {}
return [node.name, node.description, node.nickname,
p["device.icon-name"] || "", p["device.product.name"] || ""].join(" ").toLowerCase()
}
// Quickshell's Pipewire API only exposes a node's own properties, not
// its parent Device — so device.api (unlike device.form-factor or the
// richer device.icon-name, which only live on the Device object) is the
// one reliable Bluetooth signal actually present here: it's set on
// every node PipeWire hands off to a bluez5-backed sink/source. Checking
// it directly beats sniffing the word "bluetooth" out of node names,
// which usually just say "bluez_output..." and never match.
function isBluetoothNode(node) {
var p = (node && node.properties) || {}
return p["device.api"] === "bluez5"
}
// PipeWire's own device.form-factor (which would call ACCENTUM a
// "headset" outright) isn't reachable from here for the same reason —
// it lives on the Device object, not the node. And some devices (a
// generic USB DAC like an EKSA headset) have no form-factor anywhere in
// PipeWire at all, headset or not, so no property-based check can catch
// them. This override list is the deliberate manual escape hatch for
// exactly that case — add a distinctive substring from the product name
// (device.product.name/description, matched case-insensitively via
// deviceBlob) whenever a new no-metadata device shows up misclassified.
readonly property var headphoneNameOverrides: ["e900plus"]
function isHeadphoneNode(node) {
var blob = deviceBlob(node)
if (blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1
|| blob.indexOf("earbud") !== -1 || blob.indexOf("earphone") !== -1
|| blob.indexOf("airpod") !== -1)
return true
return root.headphoneNameOverrides.some(function(needle) { return blob.indexOf(needle) !== -1 })
}
function sinkIcon(node) {
if (!node) return "speaker"
if (isHeadphoneNode(node)) return "headphones"
if (isBluetoothNode(node)) return "bluetooth_audio"
var blob = deviceBlob(node)
if (blob.indexOf("hdmi") !== -1 || blob.indexOf("display") !== -1) return "tv"
return "speaker"
}
function sourceIcon(node) {
if (!node) return "mic"
if (isHeadphoneNode(node)) return "headphones"
if (isBluetoothNode(node)) return "bluetooth_audio"
var blob = deviceBlob(node)
if (blob.indexOf("webcam") !== -1 || blob.indexOf("camera") !== -1) return "videocam"
return "mic"
}
Text {
id: label
anchors.centerIn: parent
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 18
color: Colors.foreground
text: (root.outputMuted || root.outputVolume <= 0) ? "volume_off"
: (root.outputVolume < 0.5 ? "volume_down" : "volume_up")
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
// Scroll distance is accumulated rather than stepping per event:
// a mouse wheel sends one 120-unit event per notch, but a touchpad
// sends a stream of tiny events, which at a fixed step per event
// made touchpad scrolling far too fast. One notch's worth (120) of
// scrolling = one volume step, regardless of input device.
property real wheelAccum: 0
readonly property real volumeStep: 0.05
readonly property real unitsPerStep: 120
onClicked: {
panel.visible = !panel.visible
grab.active = panel.visible
}
onWheel: (wheel) => {
if (!root.sink || !root.sink.audio) return
wheelAccum += wheel.angleDelta.y
const steps = Math.trunc(wheelAccum / unitsPerStep)
if (steps === 0) return
wheelAccum -= steps * unitsPerStep
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + steps * volumeStep))
}
}
// --- Reusable pieces -----------------------------------------------
component VolumeSlider: Item {
id: sliderRoot
property real value: 0
property bool enabled: true
signal moved(real newValue)
implicitHeight: 16
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 4
radius: 0
color: Colors.muted
opacity: 0.35
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: track.width * Math.max(0, Math.min(1, sliderRoot.value))
height: 4
radius: 0
color: Colors.primary
opacity: sliderRoot.enabled ? 1 : 0.4
}
Rectangle {
width: 12
height: 12
radius: 6
color: Colors.primary
opacity: sliderRoot.enabled ? 1 : 0.4
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(track.width - width, track.width * sliderRoot.value - width / 2))
}
MouseArea {
anchors.fill: parent
enabled: sliderRoot.enabled
cursorShape: Qt.PointingHandCursor
function updateFromX(mx) {
sliderRoot.moved(Math.max(0, Math.min(1, mx / track.width)))
}
onPressed: (mouse) => updateFromX(mouse.x)
onPositionChanged: (mouse) => { if (pressed) updateFromX(mouse.x) }
}
}
// Read-only live level gauge — same track styling as VolumeSlider but no
// handle/interaction, filled from an actual signal level instead of a
// configured value.
component LevelMeter: Item {
id: meterRoot
property real level: 0
implicitHeight: 4
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 4
radius: 0
color: Colors.muted
opacity: 0.25
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
height: 4
radius: 0
color: Colors.paletteColor(2)
width: parent.width * Math.max(0, Math.min(1, meterRoot.level))
Behavior on width { NumberAnimation { duration: 80 } }
}
}
component ToggleSwitch: Item {
id: toggleRoot
property bool checked: false
signal toggled()
implicitWidth: 34
implicitHeight: 18
Rectangle {
anchors.fill: parent
radius: 0
color: toggleRoot.checked ? Colors.primary : Colors.muted
opacity: toggleRoot.checked ? 1 : 0.35
}
Rectangle {
width: parent.height - 4
height: parent.height - 4
radius: 0
color: Colors.background
anchors.verticalCenter: parent.verticalCenter
x: toggleRoot.checked ? parent.width - width - 2 : 2
Behavior on x { NumberAnimation { duration: Appearance.animationDuration } }
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: toggleRoot.toggled()
}
}
component DeviceRow: Rectangle {
id: deviceRow
required property var node
required property string icon
required property bool active
signal activate()
width: parent ? parent.width : 0
height: 30
radius: 0
color: rowMouse.containsMouse ? Colors.primary
: (deviceRow.active ? Qt.rgba(Colors.primary.r, Colors.primary.g, Colors.primary.b, 0.18) : "transparent")
// Hover-only outline, offset outside the row rather than drawn on
// its own edge — same treatment as the other bar widgets' selectable
// rows (e.g. NetworkIndicator.qml's NetworkRow). Not inside a
// clipped Flickable here, so no inset needed on its container.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: rowMouse.containsMouse ? 1 : 0
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Text {
Layout.preferredWidth: 18
Layout.minimumWidth: 18
Layout.maximumWidth: 18
horizontalAlignment: Text.AlignHCenter
text: deviceRow.icon
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
Text {
Layout.fillWidth: true
text: root.deviceLabel(deviceRow.node)
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
font.bold: deviceRow.active
color: rowMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
}
MouseArea {
id: rowMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: deviceRow.activate()
}
}
// --- Panel -----------------------------------------------------------
PopupWindow {
id: panel
visible: false
color: "transparent"
// Padding between the panel's border and its content. implicitWidth
// is contentWidth + padding on both sides, so the content area
// itself stays a fixed size regardless of how much padding surrounds it.
readonly property int contentWidth: 244
readonly property int contentPadding: 16
implicitWidth: contentWidth + contentPadding * 2
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
anchor {
window: root.barWindow
// Unlike anchor.item, anchor.window doesn't default rect to the
// window's own bounds — set it explicitly so edges/gravity below
// resolve against the full bar (and thus the true screen edges)
// instead of a near-zero rect at the window's origin.
rect.x: 0
rect.y: 0
rect.width: root.barWindow.width
rect.height: root.barWindow.height
edges: Edges.Bottom | Edges.Right
gravity: Edges.Bottom | Edges.Left
margins.bottom: -4
margins.right: 0
}
HyprlandFocusGrab {
id: grab
windows: [panel]
onCleared: panel.visible = false
}
Rectangle {
anchors.fill: parent
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 3
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: panel.contentPadding - 2
spacing: 10
// Header
RowLayout {
width: parent.width
spacing: 8
Text {
text: label.text
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 30
color: Colors.foreground
}
Text {
Layout.fillWidth: true
text: "Audio"
font.family: Appearance.fontFamily
font.pixelSize: 18
font.bold: true
color: Colors.foreground
}
ToggleSwitch {
checked: !root.outputMuted
onToggled: if (root.sink && root.sink.audio) root.sink.audio.muted = !root.sink.audio.muted
}
}
// Output
Column {
width: parent.width
spacing: 6
RowLayout {
width: parent.width
Text {
Layout.fillWidth: true
text: "OUTPUT"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Text {
text: Math.round(root.outputVolume * 100) + "%"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
VolumeSlider {
width: parent.width
value: root.outputVolume
enabled: !!root.sink
onMoved: (v) => { if (root.sink && root.sink.audio) root.sink.audio.volume = v }
}
LevelMeter {
width: parent.width
level: outputPeak.peak
}
Column {
width: parent.width
spacing: 4
Repeater {
model: root.candidateSinks
DeviceRow {
required property var modelData
node: modelData
icon: root.sinkIcon(modelData)
active: !!(root.sink && modelData && root.sink.id === modelData.id)
onActivate: Pipewire.preferredDefaultAudioSink = modelData
}
}
}
}
// Input
Column {
width: parent.width
spacing: 6
RowLayout {
width: parent.width
Text {
Layout.fillWidth: true
text: "INPUT"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Text {
text: Math.round(root.inputVolume * 100) + "%"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
VolumeSlider {
width: parent.width
value: root.inputVolume
enabled: !!root.source
onMoved: (v) => { if (root.source && root.source.audio) root.source.audio.volume = v }
}
LevelMeter {
width: parent.width
level: inputPeak.peak
}
Column {
width: parent.width
spacing: 4
Repeater {
model: root.candidateSources
DeviceRow {
required property var modelData
node: modelData
icon: root.sourceIcon(modelData)
active: !!(root.source && modelData && root.source.id === modelData.id)
onActivate: Pipewire.preferredDefaultAudioSource = modelData
}
}
}
}
}
}
}
}

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

@@ -0,0 +1,45 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import "../../config"
RowLayout {
id: root
property var screen
spacing: 4
Repeater {
model: Hyprland.workspaces
delegate: Rectangle {
id: pill
required property var modelData
visible: modelData.monitor && root.screen && modelData.monitor.name === root.screen.name
implicitWidth: 26
implicitHeight: 22
radius: Appearance.radiusSmall
color: modelData.urgent ? Colors.urgent
: modelData.active ? Colors.primary
: "transparent"
Behavior on color { ColorAnimation { duration: Appearance.animationDuration } }
Text {
anchors.centerIn: parent
text: modelData.name
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: modelData.active ? Colors.primaryText : Colors.foreground
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
// Hyprland's Lua config expects Lua syntax from dispatch
onClicked: Hyprland.dispatch("hl.dsp.focus({ workspace = " + JSON.stringify(modelData.id > 0 ? modelData.id : modelData.name) + " })")
}
}
}
}

View File

@@ -0,0 +1,122 @@
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import "../../config"
PanelWindow {
id: osd
visible: false
color: "transparent"
exclusiveZone: 0
// Bottom-center: anchoring only the bottom edge lets the layer shell
// center it horizontally.
anchors { bottom: true }
margins.bottom: Appearance.spacing * 4
implicitWidth: 300
implicitHeight: 60
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "quickshell:osd"
readonly property var sink: Pipewire.defaultAudioSink
PwObjectTracker { objects: osd.sink ? [osd.sink] : [] }
Timer {
id: hideTimer
interval: 1400
onTriggered: osd.visible = false
}
Connections {
target: osd.sink ? osd.sink.audio : null
function onVolumeChanged() { osd.visible = true; hideTimer.restart() }
function onMutedChanged() { osd.visible = true; hideTimer.restart() }
}
Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: Colors.surface
border.color: Colors.muted
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 15
spacing: 13
Text {
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 21
color: Colors.foreground
text: {
const audio = osd.sink ? osd.sink.audio : null
if (!audio || audio.muted || audio.volume <= 0) return "volume_off"
return audio.volume < 0.5 ? "volume_down" : "volume_up"
}
}
Item {
id: slider
Layout.fillWidth: true
implicitHeight: 18
readonly property real value: osd.sink && osd.sink.audio ? osd.sink.audio.volume : 0
readonly property bool enabled: !!osd.sink
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 5
radius: 0
color: Colors.muted
opacity: 0.35
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: track.width * Math.max(0, Math.min(1, slider.value))
height: 5
radius: 0
color: Colors.primary
opacity: slider.enabled ? 1 : 0.4
}
Rectangle {
width: 14
height: 14
radius: 7
color: Colors.primary
opacity: slider.enabled ? 1 : 0.4
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(track.width - width, track.width * slider.value - width / 2))
}
MouseArea {
anchors.fill: parent
enabled: slider.enabled
cursorShape: Qt.PointingHandCursor
function updateFromX(mx) {
if (!osd.sink || !osd.sink.audio) return
osd.sink.audio.volume = Math.max(0, Math.min(1, mx / track.width))
}
onPressed: (mouse) => updateFromX(mouse.x)
onPositionChanged: (mouse) => { if (pressed) updateFromX(mouse.x) }
}
}
Text {
Layout.preferredWidth: 38
horizontalAlignment: Text.AlignRight
font.family: Appearance.fontFamily
font.pixelSize: 13
color: Colors.muted
text: Math.round(slider.value * 100) + "%"
}
}
}
}

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

View File

@@ -0,0 +1,66 @@
#!/bin/bash
# Launched by SystemUpdate.qml (top bar, next to the clock) — opens a
# terminal running whatever update command fits this distro, then notifies
# once it's done. Ported from the old waybar custom/updater module
# (.config/waybar/scripts/update-distro.sh); kept multi-distro even though
# this machine is Arch, since these dotfiles aren't.
#
# The window it opens is floated + centered by a matching Hyprland rule
# (system-update-terminal, in .config/hypr/UserConfigs/WindowRules.conf),
# same treatment as Bitwarden's window, rather than tiling into whatever
# workspace you happened to be on.
iDIR="$HOME/.config/swaync/images"
if ! command -v foot &> /dev/null; then
notify-send -i "$iDIR/error.png" "Need foot:" "foot terminal not found. Please install foot terminal."
exit 1
fi
# Printed inside the terminal before the actual update command runs, so it
# reads as "updating" from the first frame rather than looking like an
# empty/stuck window while pacman/apt/etc. take a moment to start printing.
#
# foot runs this in a fresh `bash -c`, which doesn't inherit shell functions
# from this script's own environment (export -f only reaches bash processes
# forked from *this* one, not whatever foot execs) — so instead, the
# already-colored text is built once here with real escape bytes ($'...'),
# then %q-quoted into a self-contained `echo '...'` that reproduces those
# same bytes verbatim when the child bash parses it.
msg=" Updating System — please wait "
line=$(printf '=%.0s' $(seq 1 ${#msg}))
banner_text=$'\033[1;36m'"$line"$'\033[0m\n\033[1;36m'"$msg"$'\033[0m\n\033[1;36m'"$line"$'\033[0m\n\n'
BANNER="echo $(printf '%q' "$banner_text")"
if command -v paru &> /dev/null; then
CMD="paru -Syu"
LABEL="Arch-based system"
elif command -v yay &> /dev/null; then
CMD="yay -Syu"
LABEL="Arch-based system"
elif command -v dnf &> /dev/null; then
CMD="sudo dnf update --refresh -y"
LABEL="Fedora system"
elif command -v apt &> /dev/null; then
CMD="sudo apt update && sudo apt upgrade -y"
LABEL="Debian/Ubuntu system"
elif command -v zypper &> /dev/null; then
CMD="sudo zypper dup -y"
LABEL="openSUSE system"
else
notify-send -i "$iDIR/error.png" -u critical "Unsupported system" "This script does not support your distribution."
exit 1
fi
# Keeps the terminal open (whether the update succeeded or failed — read
# always runs regardless, since it's ; not &&) until you press Enter,
# instead of foot closing the instant the update command exits and taking
# any output/errors with it.
WAIT_PROMPT="echo; read -p 'Update finished — press Enter to close...'"
# Pull + re-stow the dotfiles first, so config changes from the other machine
# land alongside the package update.
SYNC="$(printf '%q' "$(dirname "$0")/sync-dotfiles.sh")"
foot -T update bash -c "$BANNER; $SYNC; $CMD; $WAIT_PROMPT"
notify-send -i "$iDIR/profile-pic.jpg" -u low "$LABEL" 'has been updated.'

View File

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

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

View File

@@ -0,0 +1,6 @@
#!/bin/sh
# Undo for steam-bigpicture-tv-prep.sh: close Big Picture and restore
# HEADLESS-1 to its default mode (the one the Steam Deck target expects).
set -e
setsid steam steam://close/bigpicture
hyprctl eval 'hl.monitor({ output = "HEADLESS-1", mode = "1728x1080@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })'

View File

@@ -0,0 +1,5 @@
audio_sink = sink-sunshine-stereo
csrf_allowed_origins = https://xol.twhomelab.local:47990
nvenc_preset = 4
output_name = HEADLESS-1
global_prep_cmd = [{"do":"/home/thomas/.config/hypr/scripts/SunshineIdle.sh start","undo":"/home/thomas/.config/hypr/scripts/SunshineIdle.sh stop"}]

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "/etc/xdg/swaync/configSchema.json", "$schema": "/etc/xdg/swaync/configSchema.json",
"positionX": "right", "positionX": "left",
"positionY": "top", "positionY": "top",
"layer": "overlay", "layer": "overlay",
"control-center-layer": "top", "control-center-layer": "top",

View File

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

View File

@@ -5,9 +5,9 @@
# Local Paths # Local Paths
iDIR="$HOME/.config/swaync/images" iDIR="$HOME/.config/swaync/images"
# Check for required tools (kitty) # Check for required tools (foot)
if ! command -v kitty &> /dev/null; then if ! command -v foot &> /dev/null; then
notify-send -i "$iDIR/error.png" "Need Kitty:" "Kitty terminal not found. Please install Kitty terminal." notify-send -i "$iDIR/error.png" "Need foot:" "foot terminal not found. Please install foot terminal."
exit 1 exit 1
fi fi
@@ -15,23 +15,23 @@ fi
if command -v paru &> /dev/null || command -v yay &> /dev/null; then if command -v paru &> /dev/null || command -v yay &> /dev/null; then
# Arch-based # Arch-based
if command -v paru &> /dev/null; then if command -v paru &> /dev/null; then
kitty -T update paru -Syu foot -T update paru -Syu
notify-send -i "$iDIR/profile-pic.jpg" -u low 'Arch-based system' 'has been updated.' notify-send -i "$iDIR/profile-pic.jpg" -u low 'Arch-based system' 'has been updated.'
else else
kitty -T update yay -Syu foot -T update yay -Syu
notify-send -i "$iDIR/profile-pic.jpg" -u low 'Arch-based system' 'has been updated.' notify-send -i "$iDIR/profile-pic.jpg" -u low 'Arch-based system' 'has been updated.'
fi fi
elif command -v dnf &> /dev/null; then elif command -v dnf &> /dev/null; then
# Fedora-based # Fedora-based
kitty -T update sudo dnf update --refresh -y foot -T update sudo dnf update --refresh -y
notify-send -i "$iDIR/profile-pic.jpg" -u low 'Fedora system' 'has been updated.' notify-send -i "$iDIR/profile-pic.jpg" -u low 'Fedora system' 'has been updated.'
elif command -v apt &> /dev/null; then elif command -v apt &> /dev/null; then
# Debian-based (Debian, Ubuntu, etc.) # Debian-based (Debian, Ubuntu, etc.)
kitty -T update sudo apt update && sudo apt upgrade -y foot -T update sudo apt update && sudo apt upgrade -y
notify-send -i "$iDIR/profile-pic.jpg" -u low 'Debian/Ubuntu system' 'has been updated.' notify-send -i "$iDIR/profile-pic.jpg" -u low 'Debian/Ubuntu system' 'has been updated.'
elif command -v zypper &> /dev/null; then elif command -v zypper &> /dev/null; then
# openSUSE-based # openSUSE-based
kitty -T update sudo zypper dup -y foot -T update sudo zypper dup -y
notify-send -i "$iDIR/profile-pic.jpg" -u low 'openSUSE system' 'has been updated.' notify-send -i "$iDIR/profile-pic.jpg" -u low 'openSUSE system' 'has been updated.'
else else
# Unsupported distro # Unsupported distro

View File

@@ -1,6 +1,13 @@
* { * {
border: none; border: none;
font-family: Font Awesome, Roboto, Arial, sans-serif; font-family:
Font Awesome,
Roboto,
"NotoSansM Nerd Font",
"NotoSansMono Nerd Font",
"Font Awesome 6 Free",
Arial,
sans-serif;
font-size: 13px; font-size: 13px;
color: #ffffff; color: #ffffff;
border-radius: 20px; border-radius: 20px;
@@ -12,11 +19,21 @@ window#waybar {
/*-----module groups----*/ /*-----module groups----*/
.modules-right { .modules-right {
margin: 2px 10px 0 0; margin: 2px 10px 0 0;
padding-top: 1px;
}
#custom-notification {
font-family: "NotoSansM Nerd Font", "NotoSansMono Nerd Font";
font-size: 13px;
background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px;
padding: 5px 2px 5px 8px;
margin: 5px 3px;
} }
#custom-updater { #custom-updater {
font-size: 13px; font-size: 13px;
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px; border-radius: 20px;
padding: 5px 8px; padding: 5px 8px;
margin: 5px 3px; margin: 5px 3px;
@@ -24,6 +41,7 @@ window#waybar {
.modules-left { .modules-left {
margin: 2px 0 0 5px; margin: 2px 0 0 5px;
padding-top: 1px;
} }
/*-----modules indv----*/ /*-----modules indv----*/
#workspaces button { #workspaces button {
@@ -32,16 +50,16 @@ window#waybar {
} }
#workspaces button:hover { #workspaces button:hover {
box-shadow: inherit; box-shadow: inherit;
background-color: rgba(0,153,153,1); background-color: rgba(0, 153, 153, 1);
} }
#workspaces button.focused { #workspaces button.focused {
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
} }
#clock { #clock {
padding: 5px 15px; padding: 5px 15px;
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px; border-radius: 20px;
margin: 5px 3px; margin: 5px 3px;
} }
@@ -51,28 +69,29 @@ window#waybar {
#temperature, #temperature,
#network { #network {
padding: 5px 15px; padding: 5px 15px;
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px; border-radius: 20px;
margin: 5px 3px; margin: 5px 3px;
} }
#pulseaudio { #wireplumber,
#wireplumber-microphone {
padding: 5px 10px 5px 15px; padding: 5px 10px 5px 15px;
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px; border-radius: 20px;
margin: 5px 3px; margin: 5px 3px;
} }
#bluetooth { #bluetooth {
padding: 5px 15px 5px 15px; padding: 5px 15px 5px 15px;
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px; border-radius: 20px;
margin: 5px 3px; margin: 5px 3px;
} }
#custom-media, #custom-media,
#custom-launcher{ #custom-launcher {
font-size: 16px; font-size: 16px;
background-image: url('./bitwarden.svg'); background-image: url("./bitwarden.svg");
background-position: center; background-position: center;
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 18px 18px; background-size: 18px 18px;
padding: 5px 8px; padding: 5px 8px;
@@ -86,7 +105,7 @@ window#waybar {
#custom-menu, #custom-menu,
#idle_inhibitor { #idle_inhibitor {
padding: 5px 15px; padding: 5px 15px;
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px; border-radius: 20px;
margin: 5px 3px; margin: 5px 3px;
} }
@@ -105,7 +124,8 @@ window#waybar {
#idle_inhibitor.activated { #idle_inhibitor.activated {
color: #2dcc36; color: #2dcc36;
} }
#pulseaudio.muted { #wireplumber.muted,
#wireplumber-microphone.muted {
color: #cc3436; color: #cc3436;
} }
#battery.charging { #battery.charging {
@@ -122,7 +142,7 @@ window#waybar {
} }
#workspaces { #workspaces {
padding: 5px 13px 5px 10px; padding: 5px 13px 5px 10px;
background-color: rgba(0,43,51,0.85); background-color: rgba(0, 43, 51, 0.85);
border-radius: 20px; border-radius: 20px;
margin: 5px 3px; margin: 5px 3px;
} }

18
dotfiles/.local/bin/Plex Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
# Wrapper for the plex-desktop package's /usr/bin/Plex (-> /opt/plex-desktop/Plex.sh).
#
# QT_QUICK_CONTROLS_STYLE=org.hyprland.style is set globally in
# hypr/core/env.lua for other Qt/QML apps' theming (hyprland-qt-support), but
# Plex Desktop bundles its own private Qt/QML runtime that doesn't ship that
# style plugin — inheriting the var makes it fail to load its UI and exit
# immediately with "module \"org.hyprland.style\" is not installed" (see
# ~/.local/share/plex/Logs/Plex.log). This dir is first on $PATH, so it
# shadows /usr/bin/Plex for both a terminal launch and the .desktop entry
# (Exec=Plex is PATH-resolved) without touching the global env var.
# QT_QPA_PLATFORM: hypr env sets "wayland;xcb"; under Wayland the bundled
# QtWebEngine never runs the web client and the window stays blank.
unset QT_QUICK_CONTROLS_STYLE
export QT_QPA_PLATFORM=xcb
# Unset in the session; without it the X11 cursor in Plex is huge.
export XCURSOR_SIZE=24
exec /opt/plex-desktop/Plex.sh "$@"

View File

@@ -0,0 +1,31 @@
# User override of /usr/share/applications/tv.plex.PlexDesktop.desktop
# (~/.local/share/applications takes XDG precedence over /usr/share for the
# same filename, so this is picked up by app launchers instead automatically
# — no association/mimeapps changes needed).
#
# QT_QPA_PLATFORM=xcb is kept from upstream: hypr env sets "wayland;xcb", and
# under Wayland the bundled QtWebEngine never runs the web client (blank
# window, no [Web] lines in Plex.log). XCURSOR_SIZE=24 is also kept from
# upstream: it is unset in the session, so the X11 cursor comes out huge.
#
# Change from upstream: Exec unsets QT_QUICK_CONTROLS_STYLE and calls
# the package's Plex.sh directly, bypassing $PATH entirely. Plex Desktop
# bundles its own private Qt/QML runtime that doesn't ship the
# "org.hyprland.style" QQC2 style hypr/core/env.lua sets globally for other
# apps' theming — inheriting it makes Plex fail to load its UI and exit
# immediately with "module \"org.hyprland.style\" is not installed" (see
# ~/.local/share/plex/Logs/Plex.log). Going straight to the fixed install
# path (rather than relying on the .local/bin/Plex wrapper shadowing
# /usr/bin/Plex on $PATH) means this keeps working even if $PATH order ever
# changes.
[Desktop Entry]
Type=Application
Name=Plex
Comment=Plex client for desktop computers
Exec=env -u QT_QUICK_CONTROLS_STYLE QT_QPA_PLATFORM=xcb XCURSOR_SIZE=24 /opt/plex-desktop/Plex.sh
Icon=tv.plex.PlexDesktop
Terminal=false
Categories=AudioVideo;Player;X-Media;
X-Flatpak=tv.plex.PlexDesktop
StartupWMClass=Plex
Name[en_US]=Plex

18
dotfiles/.ssh/config Normal file
View File

@@ -0,0 +1,18 @@
Host gavin.twhomelab.local
HostName gavin.twhomelab.local
User thoams
Host 192.168.1.87
HostName 192.168.1.87
User thoams
ForwardAgent yes
Host 192.168.1.83
HostName 192.168.1.83
User thoams
ForwardAgent yes
Host 192.168.1.86
HostName 192.168.1.86
User thoams
ForwardAgent yes

View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -0,0 +1,12 @@
<svg width="35" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="filter4154" x="-.132" y="-.020842" width="1.264" height="1.0417" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="1.65"/>
</filter>
</defs>
<g transform="translate(0 -852.36)" shape-rendering="auto">
<path d="m7.2 858.36h28v190h-28c-1.108 0-2-0.892-2-2v-186c0-1.108 0.892-2 2-2z" fill="#000000" fill-opacity=".45" filter="url(#filter4154)" opacity=".9"/>
<path d="m7 857.36h28v190h-28c-1.108 0-2-0.892-2-2v-186c0-1.108 0.892-2 2-2z" fill="#4DB6AC" />
<path transform="translate(0 852.36)" d="m18 94c-0.554 0-1 0.446-1 1v4h-4c-0.554 0-1 0.446-1 1s0.446 1 1 1h4v4c0 0.554 0.446 1 1 1s1-0.446 1-1v-4h4c0.554 0 1-0.446 1-1s-0.446-1-1-1h-4v-4c0-0.554-0.446-1-1-1zm8 6a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8z" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="35"
height="200"
id="svg2"
version="1.1"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="add-workspace-hover.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="-81.038405"
inkscape:cy="108.58132"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1366"
inkscape:window-height="709"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
showguides="false"
inkscape:guide-bbox="true"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:bbox-nodes="true">
<inkscape:grid
type="xygrid"
id="grid3040"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" />
<sodipodi:guide
orientation="0,1"
position="20,100"
id="guide3893"
inkscape:locked="false" />
<sodipodi:guide
orientation="1,0"
position="18,190"
id="guide3895"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-852.36218)">
<path
id="rect4694"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.5;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 7,857.36218 28,0 0,190.00002 -28,0 c -1.108,0 -2,-0.892 -2,-2 L 5,859.36218 c 0,-1.108 0.892,-2 2,-2 z"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sccssss" />
<g
transform="translate(0,-0.9999969)"
id="g3917-7"
style="fill:#eceff1;fill-opacity:1;opacity:0.5;stroke:none;stroke-opacity:1" />
<path
id="rect4804"
transform="translate(0,852.36218)"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 18,94 c -0.554,0 -1,0.446 -1,1 l 0,4 -4,0 c -0.554,0 -1,0.446 -1,1 0,0.554 0.446,1 1,1 l 4,0 0,4 c 0,0.554 0.446,1 1,1 0.554,0 1,-0.446 1,-1 l 0,-4 4,0 c 0.554,0 1,-0.446 1,-1 0,-0.554 -0.446,-1 -1,-1 l -4,0 0,-4 c 0,-0.554 -0.446,-1 -1,-1 z m 8,6 a 8,8 0 0 1 -8,8 8,8 0 0 1 -8,-8 8,8 0 0 1 8,-8 8,8 0 0 1 8,8 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="35"
height="200"
id="svg2"
version="1.1"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="add-workspace.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="-28.526103"
inkscape:cy="75.41215"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1366"
inkscape:window-height="709"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
showguides="false"
inkscape:guide-bbox="true"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:bbox-nodes="true"
inkscape:document-rotation="0">
<inkscape:grid
type="xygrid"
id="grid3040"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" />
<sodipodi:guide
orientation="0,1"
position="20,100"
id="guide3893"
inkscape:locked="false" />
<sodipodi:guide
orientation="1,0"
position="18,190"
id="guide3895"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-852.36218)">
<path
id="rect4694"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.35;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 7,857.36218 28,0 0,190.00002 -28,0 c -1.108,0 -2,-0.892 -2,-2 L 5,859.36218 c 0,-1.108 0.892,-2 2,-2 z"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sccssss" />
<g
transform="translate(0,-0.9999969)"
id="g3917-7"
style="fill:#eceff1;fill-opacity:1;opacity:0.5;stroke:none;stroke-opacity:1" />
<path
id="rect4804"
transform="translate(0,852.36218)"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.87;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 18,94 c -0.554,0 -1,0.446 -1,1 l 0,4 -4,0 c -0.554,0 -1,0.446 -1,1 0,0.554 0.446,1 1,1 l 4,0 0,4 c 0,0.554 0.446,1 1,1 0.554,0 1,-0.446 1,-1 l 0,-4 4,0 c 0.554,0 1,-0.446 1,-1 0,-0.554 -0.446,-1 -1,-1 l -4,0 0,-4 c 0,-0.554 -0.446,-1 -1,-1 z m 8,6 a 8,8 0 0 1 -8,8 8,8 0 0 1 -8,-8 8,8 0 0 1 8,-8 8,8 0 0 1 8,8 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

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