Build the Panama Hyprland desktop
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# Panama desktop parity
|
||||
|
||||
This is the living inventory for the GNOME-to-Hyprland transition. It records
|
||||
real behavior, not a one-for-one count of extensions: an enabled extension with
|
||||
no configured action is not treated as missing functionality.
|
||||
|
||||
Last live audit: 2026-08-17, Fedora 44, Hyprland 0.56.2, Quickshell 0.3.0.
|
||||
|
||||
## Daily desktop surfaces
|
||||
|
||||
| Capability | Panama implementation | Status |
|
||||
|---|---|---|
|
||||
| App launcher and run dialog | Vicinae; Wofi emergency fallback | Live |
|
||||
| App grid | Vicinae applications provider | Live |
|
||||
| Hidden applications | GNOME App Hider list in Vicinae provider config | Live |
|
||||
| Tiling and dynamic workspaces | Hyprland Lua config, Forge keymap preserved | Live |
|
||||
| Overview | Continuum filmstrip, large workspace preview, cross-workspace search | Live |
|
||||
| Minimise | `SUPER+SHIFT+X` sends to scratchpad; `SUPER+X` restores | Live |
|
||||
| Dock | Quickshell dock with GNOME favourites and intellihide | Live |
|
||||
| Panel | Workspaces, CPU/RAM/GPU, clock, weather, media, clipboard, tray, status | Live |
|
||||
| Clipboard history | Vicinae owns capture/storage; Quickshell provides the panel | Live |
|
||||
| Notifications | Quickshell server, toasts, grouped history, DND, clear actions | Live |
|
||||
| Date menu | Notifications, month calendar, weather, conditional media card | Live |
|
||||
| Quick settings | Wi-Fi, Bluetooth, audio input/output, caffeine, night light, focus | Live |
|
||||
| Focus sessions | Persistent workspace-bound timer, DND/caffeine coordination, Signal Glass controls | Live |
|
||||
| Brightness | Appears only when a backlight device exists | Live, not applicable on this monitor |
|
||||
| AppIndicator / tray | Native StatusNotifierItem host and themed menus | Live |
|
||||
| Screenshot and recording | Quickshell picker, grim/slurp/satty/wf-recorder | Live |
|
||||
| Screen text and code recognition | Local Tesseract/ZBar Read mode, selectable Prism result sheet | Live after package install |
|
||||
| Lock, idle, and power | hyprlock, hypridle, login1, Quickshell power menu | Live |
|
||||
| Wallpaper | hyprpaper | Live |
|
||||
| Keyring and authorization | gnome-keyring and hyprpolkitagent | Live |
|
||||
| Portals and screen sharing | xdg-desktop-portal-hyprland plus GTK portal | Live |
|
||||
| Removable media | udiskie plus udisks notifications | Live |
|
||||
| Autostart apps | Nextcloud, Bitwarden, and RustDesk system service/tray | Live |
|
||||
| System settings | Panama Settings for display policy, appearance, desktop, sound, focus, shortcuts, and services; labelled GNOME hardware/account handoffs | Live |
|
||||
|
||||
## GNOME extension migration
|
||||
|
||||
| GNOME extension | Result |
|
||||
|---|---|
|
||||
| Forge | Replaced by Hyprland tiling and the original keymap |
|
||||
| Dash to Dock | Replaced by the Quickshell dock |
|
||||
| AppIndicator | Replaced by the Quickshell system tray |
|
||||
| Clipboard Indicator | Replaced by the bar clipboard panel and Vicinae history |
|
||||
| Emoji Copy | Replaced by Vicinae emoji search on `SUPER+.` |
|
||||
| Vitals | Replaced by the CPU, memory, and GPU bar readouts |
|
||||
| Weather O'Clock | Replaced by bar weather and the date-menu weather card |
|
||||
| Caffeine | Replaced by a real logind inhibitor in quick settings |
|
||||
| Blur My Shell / Openbar / User Theme | Replaced by the Prism shell and compositor blur |
|
||||
| Bluetooth Quick Connect | Replaced by the full Bluetooth picker |
|
||||
| Wi-Fi QR | Package and UI support still to be completed |
|
||||
| GSConnect | No paired phone profile was present; KDE Connect support is staged |
|
||||
| Home Assistant | Useful integration, but needs a purpose-built panel and secure credential migration |
|
||||
| Custom Hot Corners Extended | No action was configured, so there is no behavior to port |
|
||||
| App Hider | Hidden app list is now applied in Vicinae |
|
||||
| Allow Locked Remote Desktop | Mutter-specific patch; RustDesk is independent, but lock-screen access needs a real remote test |
|
||||
|
||||
## System UI still worth adding
|
||||
|
||||
These are not blockers for normal use, but they are the remaining places where
|
||||
a polished general-purpose desktop can go beyond the current shell.
|
||||
|
||||
1. Calendar events, not just the month grid. The right integration point is
|
||||
Evolution Data Server so existing GNOME/Google calendars remain the source
|
||||
of truth.
|
||||
2. Home Assistant favourites in a dedicated, secure quick panel. The token must
|
||||
stay in the keyring or a private state file, never in this repository.
|
||||
4. Phone integration through KDE Connect, including battery/share status in the
|
||||
tray. The package is staged but needs installation and a new pairing.
|
||||
5. Connected-network QR sharing after `qrencode` is installed. Password access
|
||||
must be on demand and must never enter shell logs.
|
||||
6. A printer administration GUI. CUPS is already active; the staged
|
||||
`system-config-printer` package supplies the missing UI.
|
||||
7. A real external RustDesk test while hyprlock is active. Local process state
|
||||
cannot prove what a remote client sees or whether input is accepted.
|
||||
|
||||
Packages staged in `setup/packages/hyprland-packages` but not installed in the
|
||||
current session because privilege escalation requires an interactive password:
|
||||
|
||||
```sh
|
||||
sudo dnf install kde-connect qrencode system-config-printer tesseract zbar
|
||||
```
|
||||
|
||||
## Global menu decision
|
||||
|
||||
A macOS-style global application menu is not being shipped. Quickshell can
|
||||
render a `DBusMenu` that an application already exports, but it does not provide
|
||||
an application-menu registrar or a reliable focused-window-to-menu association.
|
||||
Hyprland has no cross-toolkit global-menu protocol, and the daily applications
|
||||
audited here do not consistently export one: GTK 4/Nautilus and Electron apps
|
||||
are the important failures.
|
||||
|
||||
That would produce a premium-looking empty strip for most windows and a working
|
||||
menu for a minority, which is worse than omitting it. Relevant references:
|
||||
|
||||
- <https://quickshell.org/docs/v0.3.0/types/Quickshell/DBusMenu/>
|
||||
- <https://github.com/hyprwm/Hyprland/discussions/1358>
|
||||
|
||||
A future focused-window menu could expose compositor actions such as float,
|
||||
move, fullscreen, and close. It should not be labelled as an application menu.
|
||||
@@ -0,0 +1,250 @@
|
||||
# Panama :: Hyprland
|
||||
|
||||
A Hyprland desktop built to reproduce the GNOME + Forge setup it replaces, so
|
||||
that muscle memory transfers unchanged.
|
||||
|
||||
## The one thing to know first
|
||||
|
||||
**Hyprland 0.55 replaced hyprlang with Lua.** `hyprland.conf` still loads on
|
||||
0.56 as a legacy fallback and is removed in 0.57. Everything here is Lua.
|
||||
|
||||
The authoritative option reference for the installed build is on disk:
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `/usr/share/hypr/stubs/hl.meta.lua` | Generated type stub — the exact option list for this build |
|
||||
| `/usr/share/hypr/hyprland.lua` | Upstream example config |
|
||||
|
||||
Wire the stub into your editor with `.luarc.json` in this directory for
|
||||
autocomplete.
|
||||
|
||||
**The Hypr ecosystem tools did NOT move to Lua.** `hyprlock.conf`,
|
||||
`hypridle.conf`, `hyprpaper.conf` and `hyprtoolkit.conf` are still hyprlang.
|
||||
Don't "fix" them.
|
||||
|
||||
## Layout
|
||||
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `hyprland.lua` | Entry point. Each `require()` is its own error scope |
|
||||
| `env.lua` | Environment. Note the uwsm caveat below |
|
||||
| `monitors.lua` | DP-2 geometry, scaling, and the HDR decision |
|
||||
| `looks.lua` | Colours, blur, glow, shadows, animations, VRR, scanout |
|
||||
| `input.lua` | Keyboard/mouse. Click-to-focus, like GNOME |
|
||||
| `rules.lua` | Window rules, gaming rules, layer rules for the shell |
|
||||
| `keybinds.lua` | The full keymap |
|
||||
| `autostart.lua` | Session startup |
|
||||
| `overrides.lua` | Per-machine escape hatch, loaded last |
|
||||
| `hyprlock.conf` / `hypridle.conf` / `hyprpaper.conf` / `hyprtoolkit.conf` | Ecosystem tools (hyprlang) |
|
||||
|
||||
Validate any change without leaving your session:
|
||||
|
||||
```sh
|
||||
Hyprland --verify-config
|
||||
```
|
||||
|
||||
## The look
|
||||
|
||||
Tokyo Night Moon, with two accents that come from the tmux theme:
|
||||
|
||||
| Token | Value | Role |
|
||||
|---|---|---|
|
||||
| `accent` | `#82aaff` | Carries every state meaning — focused, active, on |
|
||||
| `accentSecondary` | `#b172b0` | Never used alone; only the far end of a gradient |
|
||||
|
||||
The signature is **the prism**: the two accents meeting. It appears in exactly
|
||||
four places, and nowhere else —
|
||||
|
||||
1. A one-pixel hairline along the top edge of every glass surface (the bar,
|
||||
dock, popovers), running blue on the left to orchid on the right and fading
|
||||
out before the corners. See `quickshell/widgets/PrismEdge.qml`.
|
||||
2. The focused window's border — `general.col.active_border`, blue→orchid at
|
||||
115°. Unfocused windows get no colour at all, because the gradient only
|
||||
means something if one window on screen is wearing it.
|
||||
3. The active workspace pill in the bar.
|
||||
4. Slider fills.
|
||||
|
||||
The restraint is the point. The pink stops being special the moment it's used
|
||||
as a flat fill, so it never is.
|
||||
|
||||
### Typography
|
||||
|
||||
**Adwaita Sans for everything the user reads as text — no monospace in the UI.**
|
||||
A monospaced clock or percentage reads as terminal output pasted into a panel,
|
||||
which is the opposite of the intent.
|
||||
|
||||
The Nerd Font is still used, but only to draw **icon glyphs** — it is the
|
||||
pragmatic alternative to freedesktop symbolic icons, which ship with a
|
||||
hardcoded `#2e3436` fill that Qt (unlike GTK) will not recolour. Where a
|
||||
themed freedesktop icon is wanted instead, `quickshell/widgets/ThemedIcon.qml`
|
||||
paints a palette colour through the icon's alpha.
|
||||
|
||||
Anything whose digits change in place — the clock, the vitals percentages, the
|
||||
recording timer, the selection readout — sets `font.features:
|
||||
Theme.tabularFigures`. Tabular figures share one advance width, so the text
|
||||
stops twitching as numbers tick without reaching for a monospaced face.
|
||||
|
||||
Window rounding is 18, matching the shell's popover radius, so a window and a
|
||||
panel next to each other read as the same family of object.
|
||||
|
||||
**On animated gradient borders:** Hyprland can rotate the border gradient
|
||||
continuously with `borderangle` + `style = "loop"`. Don't. The wiki is explicit
|
||||
that it forces a full-refresh-rate repaint forever, even when no border is
|
||||
visible — at 4500×3000 that is pure idle GPU burn. This config uses
|
||||
`style = "once"`, so the gradient sweeps into place when a window takes focus
|
||||
and then costs nothing.
|
||||
|
||||
## Session: use the uwsm one
|
||||
|
||||
Log in as **"Hyprland (uwsm-managed)"**, not plain "Hyprland".
|
||||
|
||||
`xdg-desktop-portal-hyprland`'s systemd unit requires `graphical-session.target`,
|
||||
and Fedora ships no `hyprland-session.target`. Without uwsm that target never
|
||||
activates and **screen sharing in OBS, Sunshine and Zoom silently fails.**
|
||||
|
||||
### The uwsm environment gotcha
|
||||
|
||||
`hl.env()` in `env.lua` exports into the *compositor's* environment, **not** the
|
||||
systemd user manager. Anything started as a user unit — `vicinae.service`,
|
||||
`hyprpaper.service`, `hyprpolkitagent.service`, `hypridle.service` — and every
|
||||
app those launch will not see it.
|
||||
|
||||
That is why `~/.config/uwsm/env` and `~/.config/uwsm/env-hyprland` exist and
|
||||
duplicate the important variables. Keep them in sync with `env.lua`; `env.lua`
|
||||
remains the fallback for the plain session.
|
||||
|
||||
## HDR — read this before turning it on
|
||||
|
||||
GNOME ran this panel in bt2100/HDR. This config deliberately does not.
|
||||
|
||||
On Hyprland 0.56.x, `cm = "hdr"` currently breaks **screencopy**: `grim`
|
||||
screenshots come back empty, and OBS/Sunshine capture and hyprlock's blurred
|
||||
background go with them. Root cause is still open upstream.
|
||||
|
||||
So the desktop runs SDR at 10-bit (`cm = "auto"`) and HDR is handed to
|
||||
fullscreen games only, via `render.cm_auto_hdr = 1` in `looks.lua`. Games get
|
||||
HDR; screenshots keep working.
|
||||
|
||||
To try full-time HDR anyway, `overrides.lua` has the block and the ordered list
|
||||
of workarounds to reach for when things break.
|
||||
|
||||
## Keymap
|
||||
|
||||
The mental model is unchanged from Forge:
|
||||
|
||||
- **SUPER** acts on windows
|
||||
- **ALT** acts on workspaces
|
||||
- **SUPER + CTRL** changes layout structure
|
||||
|
||||
### Windows
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `SUPER + H/J/K/L` (or arrows) | Focus |
|
||||
| `SUPER + SHIFT + H/J/K/L` | Move window |
|
||||
| `SUPER + CTRL + H/J/K/L` | Swap window |
|
||||
| `SUPER + SHIFT + Y/O` · `B/M` | Wider · narrower |
|
||||
| `SUPER + SHIFT + I/U` · `P/N` | Taller · shorter |
|
||||
| `SUPER + [` / `]` / `=` | Shrink / expand / reset split |
|
||||
| `SUPER + Q` | Close |
|
||||
| `SUPER + U` | Fullscreen |
|
||||
| `SUPER + CTRL + C` | Toggle float |
|
||||
| `SUPER + CTRL + SHIFT + C` | Pin (nearest thing to Forge's "always float") |
|
||||
| `SUPER + CTRL + G` / `Z` / `V` | Toggle split / preselect right / preselect down |
|
||||
| `SUPER + Tab` / `SHIFT + Tab` | Cycle windows |
|
||||
| `SUPER + SHIFT + grave` | Last window |
|
||||
| `SUPER + X` / `SUPER + SHIFT + X` | Restore scratchpad / minimise to scratchpad |
|
||||
|
||||
### Workspaces (dynamic, like GNOME)
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `ALT + H` / `ALT + L` | Previous / next workspace |
|
||||
| `ALT + SHIFT + H/L` | Move window to previous / next |
|
||||
| `ALT + 1..9`, `ALT + 0` | Jump to workspace |
|
||||
| `ALT + SHIFT + 1..9` | Send window to workspace |
|
||||
| `SUPER + scroll` | Change workspace |
|
||||
|
||||
### Launcher and shell
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `SUPER + A` / `R` / `Space` | Launcher (vicinae) — all three, pick your favourite |
|
||||
| `SUPER + SHIFT + R` | Fallback launcher (wofi) if the shell is broken |
|
||||
| `SUPER + V` | Clipboard history |
|
||||
| `SUPER + .` | Emoji picker |
|
||||
| `SUPER + S` | Quick settings |
|
||||
| `SUPER + I` | Panama Settings |
|
||||
| `SUPER + SHIFT + F` | Start or reveal focus session |
|
||||
| `SUPER + B` | Notification centre |
|
||||
| `SUPER + grave` | Workspace overview |
|
||||
| `Print` | Screenshot / record picker |
|
||||
| `SHIFT` / `ALT + Print` | Screenshot screen / window immediately |
|
||||
| `SUPER + SHIFT + S` | Screen Intelligence — read text and codes from a selection |
|
||||
| `SUPER + SHIFT + P` | Colour picker |
|
||||
| `CTRL + ALT + L` | Lock (SUPER+L is "focus right") |
|
||||
| `CTRL + ALT + Delete` | Power menu |
|
||||
|
||||
### Apps
|
||||
`SUPER + T` terminal · `N` neovim · `W` browser · `F` files · `C` calculator ·
|
||||
`E` mail · `I` Panama Settings · `CTRL + SHIFT + Esc` system monitor. GNOME
|
||||
Settings remains searchable in Vicinae for hardware and account panels.
|
||||
|
||||
## Screen Intelligence
|
||||
|
||||
`SUPER + SHIFT + S` opens the capture picker directly in Selection + Read
|
||||
mode. Read is also available beside Screenshot and Record under `Print`, and
|
||||
works on a whole display, a window, or a region. Tesseract recognizes English
|
||||
text locally; ZBar recognizes QR codes and barcodes. The result sheet can copy
|
||||
text, search it, translate it, or open a detected web address. No pixels leave
|
||||
the workstation unless one of those explicit network actions is selected.
|
||||
|
||||
## Deliberate deviations from GNOME
|
||||
|
||||
These are the places a 1:1 port was impossible, and what was done instead:
|
||||
|
||||
- **Per-edge resize.** Forge resized one named edge; Hyprland resizes along an
|
||||
axis and lets the layout pick the edge. The eight Forge keys collapse to four
|
||||
behaviours, keeping the horizontal/vertical and grow/shrink pairing.
|
||||
- **Overview on `SUPER + grave`, not a bare SUPER tap.** Tap-detection on a
|
||||
modifier misfires when you're quick with SUPER combos.
|
||||
- **Lock on `CTRL + ALT + L`.** `SUPER + L` is "focus right" in this keymap.
|
||||
- **`SUPER + grave` was Forge's "cycle windows of same app"**, which Hyprland
|
||||
has no equivalent for. "Last window" moved to `SUPER + SHIFT + grave`.
|
||||
- **`gnome-control-center` is launched with `XDG_CURRENT_DESKTOP=GNOME`**,
|
||||
because it hard-refuses to start otherwise. Panels backed by system services
|
||||
work; panels backed by GNOME Shell (Displays, Keyboard Shortcuts,
|
||||
Multitasking, Appearance) are inert — Hyprland owns those now.
|
||||
|
||||
## Gaming
|
||||
|
||||
`content = "game"` on the window rules in `rules.lua` is the keystone — it is
|
||||
what `misc.vrr = 3`, `render.direct_scanout = 2` and `cursor.no_break_fs_vrr = 2`
|
||||
key off. Games additionally get blur, animation, shadow and dim disabled, plus
|
||||
`immediate` for tearing.
|
||||
|
||||
Tearing only takes effect when the game is fullscreen and the only thing on
|
||||
screen — no bar, no notifications.
|
||||
|
||||
## Do not install a notification daemon
|
||||
|
||||
`mako`, `dunst` and `SwayNotificationCenter` all register
|
||||
`Name=org.freedesktop.Notifications` for D-Bus activation. Installing any of
|
||||
them creates a startup race against Quickshell's notification server, and
|
||||
whoever wins keeps the name. If one gets installed as a dependency,
|
||||
`systemctl --user mask <name>.service`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Nothing autostarts** — check `systemctl --user status hyprland-session.target`
|
||||
and `systemctl --user show-environment | grep WAYLAND_DISPLAY`. An empty
|
||||
environment means uwsm didn't export it and units with
|
||||
`ConditionEnvironment=WAYLAND_DISPLAY` silently skip.
|
||||
|
||||
**Screen sharing missing** — `busctl --user list | grep impl.portal` should show
|
||||
`xdg-desktop-portal-hyprland`.
|
||||
|
||||
**Qt apps look wrong** — `QT_QPA_PLATFORMTHEME` must be `gtk3`. It is a single
|
||||
value, not a list: Qt splits on `:` and uses only the first token.
|
||||
|
||||
**Wrong GPU / won't start** — `AQ_DRM_DEVICES` points at
|
||||
`/dev/dri/amd-dgpu`, created by `config/copy/etc/udev/rules.d/99-panama-gpu.rules`.
|
||||
Both `env.lua` and `uwsm/env-hyprland` guard on the symlink existing, so a
|
||||
missing rule degrades to "let aquamarine choose" rather than failing to start.
|
||||
@@ -0,0 +1,60 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Autostart
|
||||
--
|
||||
-- `exec-once` no longer exists in 0.56; startup is an event handler.
|
||||
--
|
||||
-- Session: Fedora installs two sessions, "Hyprland" and "Hyprland
|
||||
-- (uwsm-managed)". Panama targets the uwsm one, because
|
||||
-- xdg-desktop-portal-hyprland's systemd unit requires graphical-session.target
|
||||
-- and Fedora ships no hyprland-session.target -- without uwsm, screen sharing
|
||||
-- in OBS/Sunshine/Zoom silently fails. The target is started explicitly below
|
||||
-- so the plain session works too.
|
||||
--
|
||||
-- Anything that ships a systemd user unit is started as a unit rather than as
|
||||
-- a compositor child. That is not cosmetic: under uwsm, units get the correct
|
||||
-- activation environment (see uwsm/env), they restart on failure, and they
|
||||
-- shut down in order. Panama's `change-settings` script enables them once;
|
||||
-- the explicit starts here make a fresh clone work before that has run.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
hl.on("hyprland.start", function()
|
||||
-- Publish the session environment to systemd and D-Bus so user units and
|
||||
-- the portals can see WAYLAND_DISPLAY. Cheap, and essential without uwsm.
|
||||
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=Hyprland")
|
||||
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
||||
|
||||
-- Units: polkit prompts, wallpaper, launcher daemon, idle/lock.
|
||||
hl.exec_cmd("systemctl --user start hyprpolkitagent.service hyprpaper.service vicinae.service hypridle.service")
|
||||
|
||||
-- The shell: bar, dock, overview, quick settings, notifications, capture.
|
||||
-- No systemd unit ships with quickshell, so it runs as a compositor child.
|
||||
hl.exec_cmd("quickshell --daemonize")
|
||||
|
||||
-- Removable-media automounting. GNOME did this invisibly via gvfs+udisks;
|
||||
-- outside GNOME something has to ask udisks to mount. No tray icon: the
|
||||
-- Quickshell bar already has a tray, and udiskie's own icon would be
|
||||
-- redundant. -f opens Nautilus when you click the mount notification.
|
||||
hl.exec_cmd("udiskie --automount --notify --no-tray --file-manager nautilus")
|
||||
|
||||
-- Keyring unlock, for Nextcloud and Bitwarden credential storage.
|
||||
hl.exec_cmd("/usr/bin/gnome-keyring-daemon --start --components=secrets,ssh,pkcs11")
|
||||
|
||||
-- Tray applications carried over from ~/.config/autostart. These need the
|
||||
-- Quickshell tray (an SNI host) to be up, hence starting after it.
|
||||
--
|
||||
-- Commands are copied verbatim from the GNOME .desktop files rather than
|
||||
-- guessed: Bitwarden is a flatpak with no `bitwarden` binary on PATH, and
|
||||
-- its autostart entry launches a specific script with --autostart.
|
||||
--
|
||||
-- RustDesk is deliberately absent: it ships an enabled *system* service
|
||||
-- (`rustdesk --service`) that spawns --server and --tray for the session on
|
||||
-- its own. Starting it here as well would give you two trays.
|
||||
hl.exec_cmd("nextcloud --background")
|
||||
hl.exec_cmd("flatpak run --command=bitwarden.sh com.bitwarden.desktop --autostart")
|
||||
end)
|
||||
|
||||
hl.on("hyprland.shutdown", function()
|
||||
hl.exec_cmd("systemctl --user stop hyprland-session.target")
|
||||
end)
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,84 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Environment
|
||||
--
|
||||
-- hl.env() sets variables before the display server initialises, which is what
|
||||
-- makes it valid for AQ_* and cursor variables.
|
||||
--
|
||||
-- NOTE for uwsm sessions: if you log in via "Hyprland (uwsm-managed)", uwsm
|
||||
-- prefers these live in ~/.config/uwsm/env and ~/.config/uwsm/env-hyprland
|
||||
-- instead. They still work here; see the session notes in autostart.lua.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- ── GPU selection ───────────────────────────────────────────────────────────
|
||||
-- This box has a discrete RX 7800 XT (0000:03:00.0) and a Granite Ridge iGPU
|
||||
-- (0000:12:00.0). The monitor hangs off the dGPU, so the dGPU must render.
|
||||
--
|
||||
-- /dev/dri/cardN numbering is not stable across boots, and the by-path names
|
||||
-- contain colons -- which are AQ_DRM_DEVICES' own separator, so they cannot be
|
||||
-- used directly. Panama installs a udev rule creating colon-free stable
|
||||
-- symlinks (/dev/dri/amd-dgpu, /dev/dri/amd-igpu); see
|
||||
-- config/copy/etc/udev/rules.d/99-panama-gpu.rules
|
||||
--
|
||||
-- First entry is the primary renderer. The iGPU stays in the list so anything
|
||||
-- plugged into its outputs still lights up.
|
||||
--
|
||||
-- Guarded on the symlink actually existing: this config is shared across
|
||||
-- machines, and pointing AQ_DRM_DEVICES at a device that isn't there stops
|
||||
-- Hyprland from starting at all. If the udev rule hasn't been installed (or
|
||||
-- this is a different box), we say nothing and let aquamarine choose.
|
||||
local function exists(path)
|
||||
local f = io.open(path, "r")
|
||||
if f then f:close() return true end
|
||||
return false
|
||||
end
|
||||
|
||||
if exists("/dev/dri/amd-dgpu") then
|
||||
local devices = "/dev/dri/amd-dgpu"
|
||||
if exists("/dev/dri/amd-igpu") then
|
||||
devices = devices .. ":/dev/dri/amd-igpu"
|
||||
end
|
||||
hl.env("AQ_DRM_DEVICES", devices)
|
||||
end
|
||||
|
||||
-- ── Cursor ──────────────────────────────────────────────────────────────────
|
||||
-- Theme carried over from GNOME.
|
||||
--
|
||||
-- Deliberately no HYPRCURSOR_THEME: oreo_blue_cursors is a plain XCursor theme
|
||||
-- (it has cursors/ + index.theme and no hyprcursors/ directory or manifest.hl),
|
||||
-- so setting it would point hyprcursor at nothing. Hyprland falls back to the
|
||||
-- XCursor path, which is what we want.
|
||||
hl.env("XCURSOR_THEME", "oreo_blue_cursors")
|
||||
hl.env("XCURSOR_SIZE", "24")
|
||||
|
||||
-- ── Toolkits ────────────────────────────────────────────────────────────────
|
||||
-- Wayland first, X11 only as a fallback for apps that genuinely need it.
|
||||
hl.env("QT_QPA_PLATFORM", "wayland;xcb")
|
||||
|
||||
-- "gtk3" makes Qt derive its palette from the live GTK3 theme via the qgtk3
|
||||
-- platform plugin, so Qt apps match adw-gtk3-dark exactly rather than
|
||||
-- approximating it -- and it gives them GTK file/font/colour dialogs. It also
|
||||
-- covers Qt5 and Qt6 with one value.
|
||||
--
|
||||
-- Note this is a single value, NOT a fallback list: Qt splits on ":" and uses
|
||||
-- only the first token, so the commonly-copied "qt5ct:qt6ct" silently means
|
||||
-- just "qt5ct".
|
||||
hl.env("QT_QPA_PLATFORMTHEME", "gtk3")
|
||||
hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1")
|
||||
hl.env("QT_AUTO_SCREEN_SCALE_FACTOR", "1")
|
||||
hl.env("GDK_BACKEND", "wayland,x11")
|
||||
hl.env("SDL_VIDEODRIVER", "wayland")
|
||||
hl.env("CLUTTER_BACKEND", "wayland")
|
||||
hl.env("MOZ_ENABLE_WAYLAND", "1")
|
||||
|
||||
-- Electron apps (Slack, Obsidian, Claude, Codex, BlueBubbles, Podman Desktop)
|
||||
-- pick Wayland from this rather than needing per-app flags.
|
||||
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")
|
||||
|
||||
-- ── Desktop identity ────────────────────────────────────────────────────────
|
||||
-- Portals and some GNOME apps key off these. uwsm sets them itself, so these
|
||||
-- are only load-bearing for the plain (non-uwsm) session.
|
||||
hl.env("XDG_CURRENT_DESKTOP", "Hyprland")
|
||||
hl.env("XDG_SESSION_TYPE", "wayland")
|
||||
hl.env("XDG_SESSION_DESKTOP", "Hyprland")
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,39 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# hypridle — idle behaviour
|
||||
#
|
||||
# Mirrors the GNOME power profile this replaces, exactly:
|
||||
# org.gnome.desktop.session idle-delay = 300 (screen off at 5min)
|
||||
# org.gnome.desktop.screensaver lock-delay = 300 (lock 5min after that)
|
||||
# org.gnome.settings-daemon.plugins.power
|
||||
# sleep-inactive-ac-type = nothing (never auto-suspend)
|
||||
# power-button-action = nothing
|
||||
#
|
||||
# So: screen off at 5 minutes, lock at 10, never suspend. This is a desktop.
|
||||
#
|
||||
# hyprlang syntax, not Lua — hypridle is a separate project from Hyprland.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
general {
|
||||
# `pidof` guard prevents stacking lockers if this fires twice.
|
||||
lock_cmd = pidof hyprlock || hyprlock
|
||||
before_sleep_cmd = loginctl lock-session
|
||||
after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
||||
|
||||
# 2 = auto. Holds off sleep until the locker is actually up.
|
||||
inhibit_sleep = 2
|
||||
}
|
||||
|
||||
# 5 minutes -> screen off.
|
||||
listener {
|
||||
timeout = 300
|
||||
on-timeout = hyprctl dispatch 'hl.dsp.dpms({ action = "off" })'
|
||||
on-resume = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
||||
}
|
||||
|
||||
# 10 minutes -> lock. Matches GNOME's lock-delay of 300s after blanking.
|
||||
listener {
|
||||
timeout = 600
|
||||
on-timeout = loginctl lock-session
|
||||
}
|
||||
|
||||
# Deliberately no suspend listener: sleep-inactive-ac-type was 'nothing'.
|
||||
@@ -0,0 +1,26 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Panama :: Hyprland
|
||||
--
|
||||
-- Hyprland 0.55 replaced hyprlang (hyprland.conf) with Lua. The .conf format
|
||||
-- still loads as a legacy fallback in 0.56 but is removed in 0.57, so this
|
||||
-- config is Lua-native. Reference for every option below:
|
||||
-- /usr/share/hypr/stubs/hl.meta.lua <- authoritative for THIS build
|
||||
-- /usr/share/hypr/hyprland.lua <- upstream example
|
||||
--
|
||||
-- Entry point only. Each require() is its own error scope, so a mistake in one
|
||||
-- module doesn't take the whole config down with it.
|
||||
--
|
||||
-- Reload: hyprctl reload
|
||||
-- Check: Hyprland --verify-config
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
require("env")
|
||||
require("monitors")
|
||||
require("looks")
|
||||
require("input")
|
||||
require("rules")
|
||||
require("keybinds")
|
||||
require("autostart")
|
||||
|
||||
-- Per-machine overrides, loaded last so it wins. Ships effectively empty.
|
||||
require("overrides")
|
||||
@@ -0,0 +1,114 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# hyprlock — lock screen
|
||||
#
|
||||
# Tokyo Night Moon, matching quickshell/config/Theme.qml:
|
||||
# accent #82aaff fg #c8d3f5 dim #828bb8 bg #222436 red #ff757f
|
||||
#
|
||||
# hyprlang syntax, not Lua — hyprlock is a separate project from Hyprland.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
general {
|
||||
hide_cursor = true
|
||||
|
||||
# 2 = auto. Matters at the 1.5x fractional scale this display runs.
|
||||
fractional_scaling = 2
|
||||
|
||||
# 0 = GPU-accelerated screencopy. 1 is a CPU fallback and is very slow at
|
||||
# 4500x3000.
|
||||
screencopy_mode = 0
|
||||
|
||||
fail_timeout = 2000
|
||||
}
|
||||
|
||||
auth {
|
||||
pam:enabled = true
|
||||
pam:module = hyprlock
|
||||
}
|
||||
|
||||
# ── Background ──────────────────────────────────────────────────────────────
|
||||
# `path = screenshot` grabs the desktop at lock time and blurs it in place,
|
||||
# which is what GNOME does.
|
||||
#
|
||||
# Caveat: this uses screencopy. If the desktop is ever switched to full-time
|
||||
# HDR (see hypr/overrides.lua), this background may come back black — that is
|
||||
# the same upstream screencopy bug, not a hyprlock misconfiguration.
|
||||
background {
|
||||
monitor =
|
||||
path = screenshot
|
||||
|
||||
blur_passes = 3
|
||||
blur_size = 8
|
||||
noise = 0.0117
|
||||
contrast = 0.9
|
||||
brightness = 0.8
|
||||
vibrancy = 0.17
|
||||
vibrancy_darkness = 0.05
|
||||
|
||||
# Shown if the screenshot is unavailable.
|
||||
color = rgba(34, 36, 54, 1.0)
|
||||
|
||||
zindex = -1
|
||||
}
|
||||
|
||||
# ── Clock ───────────────────────────────────────────────────────────────────
|
||||
label {
|
||||
monitor =
|
||||
text = cmd[update:1000] date +"%-I:%M"
|
||||
color = rgba(200, 211, 245, 1.0)
|
||||
font_size = 120
|
||||
font_family = Adwaita Sans Light
|
||||
position = 0, 260
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
|
||||
label {
|
||||
monitor =
|
||||
text = cmd[update:60000] date +"%A, %B %-d"
|
||||
color = rgba(130, 139, 184, 1.0)
|
||||
font_size = 24
|
||||
font_family = Adwaita Sans
|
||||
position = 0, 160
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
|
||||
# ── Password field ──────────────────────────────────────────────────────────
|
||||
input-field {
|
||||
monitor =
|
||||
size = 340, 52
|
||||
position = 0, -40
|
||||
halign = center
|
||||
valign = center
|
||||
|
||||
outline_thickness = 2
|
||||
rounding = 26
|
||||
|
||||
outer_color = rgba(130, 170, 255, 0.9)
|
||||
inner_color = rgba(46, 47, 61, 0.85)
|
||||
font_color = rgba(200, 211, 245, 1.0)
|
||||
check_color = rgba(130, 170, 255, 1.0)
|
||||
fail_color = rgba(255, 117, 127, 1.0)
|
||||
|
||||
dots_size = 0.25
|
||||
dots_spacing = 0.3
|
||||
dots_center = true
|
||||
|
||||
placeholder_text = <span foreground="##828bb8"><i>Password</i></span>
|
||||
fail_text = <span foreground="##ff757f"><i>$FAIL ($ATTEMPTS)</i></span>
|
||||
|
||||
fade_on_empty = false
|
||||
hide_input = false
|
||||
}
|
||||
|
||||
# ── User ────────────────────────────────────────────────────────────────────
|
||||
label {
|
||||
monitor =
|
||||
text = $USER
|
||||
color = rgba(200, 211, 245, 0.9)
|
||||
font_size = 16
|
||||
font_family = Adwaita Sans
|
||||
position = 0, -110
|
||||
halign = center
|
||||
valign = center
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# hyprpaper — static wallpaper
|
||||
#
|
||||
# NOTE: hyprpaper, hypridle, hyprlock and hyprtoolkit are separate projects and
|
||||
# still use hyprlang. Only Hyprland itself moved to Lua. Don't "fix" these.
|
||||
#
|
||||
# SYNTAX NOTE: hyprpaper 0.8 was rewritten on hyprtoolkit/hyprwire and replaced
|
||||
# the old flat syntax with blocks. The pre-0.8 form
|
||||
#
|
||||
# preload = /path/to.jpg
|
||||
# wallpaper = DP-2,/path/to.jpg
|
||||
#
|
||||
# is silently ignored — it parses, then logs "Monitor DP-2 has no target: no wp
|
||||
# will be created" and shows nothing. Almost every guide online still uses it.
|
||||
#
|
||||
# Static only, by choice: animated wallpapers repaint continuously and cost
|
||||
# frame time for nothing.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
wallpaper {
|
||||
monitor = DP-2
|
||||
path = ~/Pictures/Wallpapers/faroe_islands.jpg
|
||||
# cover = fill the output, cropping overflow. Matches the "zoom" picture
|
||||
# option this setup used under GNOME.
|
||||
fit_mode = cover
|
||||
}
|
||||
|
||||
# Fallback for any output without its own block above.
|
||||
wallpaper {
|
||||
monitor =
|
||||
path = ~/Pictures/Wallpapers/faroe_islands.jpg
|
||||
fit_mode = cover
|
||||
}
|
||||
|
||||
ipc = on
|
||||
splash = false
|
||||
@@ -0,0 +1,34 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# hyprtoolkit — shared theme for the first-party Hypr GUI apps
|
||||
#
|
||||
# One file themes all of these, which is why they're preferred over the usual
|
||||
# grab-bag of GTK/Qt utilities:
|
||||
# hyprshutdown session exit dialog (bound to the power menu)
|
||||
# hyprpwcenter PipeWire / audio routing GUI (replaces a pavucontrol/TUI)
|
||||
# hyprsysteminfo system info panel
|
||||
# hyprland-guiutils hyprland-dialog / hyprland-run
|
||||
# hyprpolkitagent authentication prompts (via hyprland-qt-support)
|
||||
#
|
||||
# Colours are Tokyo Night Moon, matching quickshell/config/Theme.qml and
|
||||
# hypr/looks.lua. Format is hyprlang; path is fixed at
|
||||
# $XDG_CONFIG_HOME/hypr/hyprtoolkit.conf (confirmed in libhyprtoolkit).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
background = 0xFF1E2030 # bg_dark — window background
|
||||
base = 0xFF222436 # bg — raised surfaces
|
||||
alternate_base = 0xFF2F334D # bg_highlight — rows, hover
|
||||
text = 0xFFC8D3F5 # fg
|
||||
bright_text = 0xFFFFFFFF
|
||||
link_text = 0xFF82AAFF # blue
|
||||
|
||||
accent = 0xFF82AAFF # blue — same accent as everything else
|
||||
accent_secondary = 0xFFC099FF # magenta
|
||||
|
||||
font_family = Adwaita Sans
|
||||
font_family_monospace = VictorMono Nerd Font
|
||||
|
||||
icon_theme = Adwaita
|
||||
|
||||
# Match the rounding used by the Quickshell popovers and the Hyprland windows.
|
||||
rounding_large = 18
|
||||
rounding_small = 8
|
||||
@@ -0,0 +1,40 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Input
|
||||
--
|
||||
-- Deliberately GNOME-like: click-to-focus, no focus-follows-mouse, no mouse
|
||||
-- acceleration changes. The goal is that muscle memory transfers untouched.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
hl.config({
|
||||
input = {
|
||||
kb_layout = "us",
|
||||
kb_variant = "",
|
||||
kb_model = "",
|
||||
kb_options = "",
|
||||
kb_rules = "",
|
||||
|
||||
numlock_by_default = true,
|
||||
|
||||
-- GNOME's defaults are 500ms delay / 33Hz repeat.
|
||||
repeat_delay = 500,
|
||||
repeat_rate = 33,
|
||||
|
||||
-- 1 = click to focus. GNOME's behaviour; NOT sloppy focus.
|
||||
follow_mouse = 1,
|
||||
|
||||
-- Don't refocus on mouse move alone -- only on click.
|
||||
mouse_refocus = false,
|
||||
|
||||
-- Flat pointer response, no acceleration. Matters for gaming.
|
||||
sensitivity = 0,
|
||||
accel_profile = "flat",
|
||||
|
||||
-- Clicking a floating window raises and focuses it.
|
||||
float_switch_override_focus = 2,
|
||||
},
|
||||
})
|
||||
|
||||
-- Desktop machine: no touchpad, no gestures worth wiring. If a laptop ever
|
||||
-- runs this config, add touchpad settings in overrides.lua.
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,243 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Keybindings
|
||||
--
|
||||
-- Ported 1:1 from the GNOME + Forge setup this replaces. Where Hyprland has no
|
||||
-- equivalent, the deviation is called out in a comment rather than silently
|
||||
-- dropped.
|
||||
--
|
||||
-- The mental model, unchanged from Forge:
|
||||
-- SUPER -> act on windows (focus / move / swap / resize)
|
||||
-- ALT -> act on workspaces
|
||||
-- SUPER + CTRL -> change layout structure (split, float, swap)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local mod = "SUPER"
|
||||
|
||||
-- Programs, matched to the GNOME custom keybindings and media-key settings.
|
||||
local terminal = "kitty"
|
||||
local editor = "kitty nvim ."
|
||||
local browser = "helium-browser-bin"
|
||||
local files = "nautilus --new-window"
|
||||
local calculator = "gnome-calculator"
|
||||
local mail = "flatpak run org.mozilla.thunderbird_esr"
|
||||
-- Panama owns the Hyprland and shell controls. GNOME Settings remains installed
|
||||
-- and searchable in Vicinae for its hardware/account panels.
|
||||
local settings = "qs ipc call settings toggle"
|
||||
local sysmonitor = "flatpak run io.missioncenter.MissionCenter"
|
||||
|
||||
-- Vicinae is the Raycast-style launcher. `vicinae toggle` shows/hides the
|
||||
-- window against the already-running server (started in autostart.lua).
|
||||
local launcher = "vicinae toggle"
|
||||
|
||||
-- Quickshell IPC targets. See quickshell/shell.qml for the handlers.
|
||||
local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end
|
||||
|
||||
-- ── Applications ────────────────────────────────────────────────────────────
|
||||
hl.bind(mod .. " + T", hl.dsp.exec_cmd(terminal), { description = "Terminal" })
|
||||
hl.bind(mod .. " + N", hl.dsp.exec_cmd(editor), { description = "Neovim" })
|
||||
hl.bind(mod .. " + W", hl.dsp.exec_cmd(browser), { description = "Browser" })
|
||||
hl.bind(mod .. " + F", hl.dsp.exec_cmd(files), { description = "Files" })
|
||||
hl.bind(mod .. " + C", hl.dsp.exec_cmd(calculator), { description = "Calculator" })
|
||||
hl.bind(mod .. " + E", hl.dsp.exec_cmd(mail), { description = "Mail" })
|
||||
hl.bind(mod .. " + I", hl.dsp.exec_cmd(settings), { description = "Panama Settings" })
|
||||
hl.bind("CTRL + SHIFT + Escape", hl.dsp.exec_cmd(sysmonitor), { description = "System monitor" })
|
||||
|
||||
-- ── Launcher ────────────────────────────────────────────────────────────────
|
||||
-- All three keys open the same launcher, on purpose: SUPER+A and SUPER+R were
|
||||
-- the GNOME app-grid and run-dialog shortcuts, and SUPER+SPACE is here as a
|
||||
-- third option to settle on. Vicinae covers apps, calculator, files, clipboard,
|
||||
-- emoji and window switching, so a separate run dialog and app grid are gone.
|
||||
hl.bind(mod .. " + A", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||
hl.bind(mod .. " + R", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||
hl.bind(mod .. " + Space", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||
|
||||
-- Emergency fallback launcher. Vicinae runs as a systemd user service and the
|
||||
-- bar is Quickshell; if either fails to come up, this is how you start an
|
||||
-- application without dropping to a TTY. Depends on nothing but wofi itself.
|
||||
hl.bind(mod .. " + SHIFT + R", hl.dsp.exec_cmd("wofi"), { description = "Fallback launcher" })
|
||||
|
||||
-- Clipboard history and emoji, straight into the relevant launcher view.
|
||||
-- Deeplink form is the one from vicinae's own Hyprland quickstart.
|
||||
hl.bind(mod .. " + V", hl.dsp.exec_cmd("vicinae vicinae://launch/clipboard/history"),
|
||||
{ description = "Clipboard history" })
|
||||
hl.bind(mod .. " + Period", hl.dsp.exec_cmd("vicinae vicinae://launch/emoji/search"),
|
||||
{ description = "Emoji picker" })
|
||||
|
||||
-- ── Shell surfaces (Quickshell) ─────────────────────────────────────────────
|
||||
-- SUPER+S was GNOME's quick settings; kept.
|
||||
hl.bind(mod .. " + S", hl.dsp.exec_cmd(qs("quicksettings", "toggle")), { description = "Quick settings" })
|
||||
|
||||
-- Start a 45-minute focus session on the current workspace, or reveal its
|
||||
-- Signal Glass controls if one is already running.
|
||||
hl.bind(mod .. " + SHIFT + F", hl.dsp.exec_cmd(qs("focus", "reveal")), { description = "Focus session" })
|
||||
|
||||
-- Workspace overview. GNOME put this on a bare SUPER tap; tap-detection on a
|
||||
-- modifier misfires when you're fast with SUPER+key combos, so it lives on a
|
||||
-- real chord instead. SUPER+grave was Forge's "cycle windows of same app",
|
||||
-- which Hyprland has no equivalent for.
|
||||
hl.bind(mod .. " + grave", hl.dsp.exec_cmd(qs("overview", "toggle")), { description = "Overview" })
|
||||
|
||||
-- Notification centre.
|
||||
hl.bind(mod .. " + B", hl.dsp.exec_cmd(qs("notifications", "toggle")), { description = "Notifications" })
|
||||
|
||||
-- Screenshot / screen record. One key, then pick screen / window / region and
|
||||
-- whether to capture or record -- reproducing GNOME's Print-screen UI.
|
||||
hl.bind("Print", hl.dsp.exec_cmd(qs("capture", "open")), { description = "Screenshot / record" })
|
||||
-- The GNOME direct-capture variants, kept as shortcuts past the picker.
|
||||
hl.bind("SHIFT + Print", hl.dsp.exec_cmd(qs("capture", "screenNow")), { description = "Screenshot: whole screen" })
|
||||
hl.bind("ALT + Print", hl.dsp.exec_cmd(qs("capture", "windowNow")), { description = "Screenshot: window" })
|
||||
|
||||
-- Local OCR and QR/barcode recognition through the same region picker. This
|
||||
-- opens directly in Selection + Read mode; Print still exposes every mode.
|
||||
hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd(qs("screen-intelligence", "open")),
|
||||
{ description = "Screen Intelligence" })
|
||||
|
||||
-- Colour picker: copies the hex under the cursor to the clipboard.
|
||||
hl.bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd("hyprpicker -a -f hex"), { description = "Colour picker" })
|
||||
|
||||
-- ── Window management ───────────────────────────────────────────────────────
|
||||
hl.bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||
hl.bind(mod .. " + U", hl.dsp.window.fullscreen({ mode = "fullscreen" }), { description = "Fullscreen" })
|
||||
|
||||
-- Forge: window-toggle-float / window-toggle-always-float.
|
||||
-- "Always float" has no Hyprland equivalent (it wrote a persistent rule); pin
|
||||
-- is the nearest useful thing -- the window floats above every workspace.
|
||||
hl.bind(mod .. " + CTRL + C", hl.dsp.window.float({ action = "toggle" }), { description = "Toggle float" })
|
||||
hl.bind(mod .. " + CTRL + SHIFT + C", hl.dsp.window.pin({ action = "toggle" }), { description = "Pin window" })
|
||||
|
||||
-- Forge: con-split-layout-toggle / con-split-horizontal / con-split-vertical.
|
||||
hl.bind(mod .. " + CTRL + G", hl.dsp.layout("togglesplit"), { description = "Toggle split direction" })
|
||||
hl.bind(mod .. " + CTRL + Z", hl.dsp.layout("preselect r"), { description = "Next window splits right" })
|
||||
hl.bind(mod .. " + CTRL + V", hl.dsp.layout("preselect d"), { description = "Next window splits down" })
|
||||
|
||||
-- Forge: window-shrink / window-expand / window-reset-sizes.
|
||||
hl.bind(mod .. " + bracketleft", hl.dsp.layout("splitratio -0.05"), { repeating = true, description = "Shrink" })
|
||||
hl.bind(mod .. " + bracketright", hl.dsp.layout("splitratio +0.05"), { repeating = true, description = "Expand" })
|
||||
hl.bind(mod .. " + equal", hl.dsp.layout("splitratio exact 0.5"), { description = "Reset split" })
|
||||
|
||||
-- Focus (Forge: window-focus-*). Both vim keys and arrows, as in Forge.
|
||||
hl.bind(mod .. " + H", hl.dsp.focus({ direction = "l" }), { description = "Focus left" })
|
||||
hl.bind(mod .. " + J", hl.dsp.focus({ direction = "d" }), { description = "Focus down" })
|
||||
hl.bind(mod .. " + K", hl.dsp.focus({ direction = "u" }), { description = "Focus up" })
|
||||
hl.bind(mod .. " + L", hl.dsp.focus({ direction = "r" }), { description = "Focus right" })
|
||||
hl.bind(mod .. " + left", hl.dsp.focus({ direction = "l" }))
|
||||
hl.bind(mod .. " + down", hl.dsp.focus({ direction = "d" }))
|
||||
hl.bind(mod .. " + up", hl.dsp.focus({ direction = "u" }))
|
||||
hl.bind(mod .. " + right", hl.dsp.focus({ direction = "r" }))
|
||||
|
||||
-- Move (Forge: window-move-*).
|
||||
hl.bind(mod .. " + SHIFT + H", hl.dsp.window.move({ direction = "l" }), { description = "Move window left" })
|
||||
hl.bind(mod .. " + SHIFT + J", hl.dsp.window.move({ direction = "d" }), { description = "Move window down" })
|
||||
hl.bind(mod .. " + SHIFT + K", hl.dsp.window.move({ direction = "u" }), { description = "Move window up" })
|
||||
hl.bind(mod .. " + SHIFT + L", hl.dsp.window.move({ direction = "r" }), { description = "Move window right" })
|
||||
|
||||
-- Swap (Forge: window-swap-*).
|
||||
hl.bind(mod .. " + CTRL + H", hl.dsp.window.swap({ direction = "l" }), { description = "Swap left" })
|
||||
hl.bind(mod .. " + CTRL + J", hl.dsp.window.swap({ direction = "d" }), { description = "Swap down" })
|
||||
hl.bind(mod .. " + CTRL + K", hl.dsp.window.swap({ direction = "u" }), { description = "Swap up" })
|
||||
hl.bind(mod .. " + CTRL + L", hl.dsp.window.swap({ direction = "r" }), { description = "Swap right" })
|
||||
|
||||
-- Resize (Forge: window-resize-<edge>-<increase|decrease>).
|
||||
--
|
||||
-- Forge resized one named edge at a time. Hyprland resizes the active window
|
||||
-- along an axis and lets the layout decide which edge actually moves, so the
|
||||
-- eight Forge keys collapse onto four behaviours. The pairing is kept
|
||||
-- consistent with the original: Y/B/O/M are horizontal, I/P/U/N are vertical,
|
||||
-- and "increase" always grows while "decrease" always shrinks.
|
||||
local step = 60
|
||||
hl.bind(mod .. " + SHIFT + Y", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" })
|
||||
hl.bind(mod .. " + SHIFT + O", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" })
|
||||
hl.bind(mod .. " + SHIFT + B", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
||||
hl.bind(mod .. " + SHIFT + M", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
||||
hl.bind(mod .. " + SHIFT + I", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
||||
hl.bind(mod .. " + SHIFT + U", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
||||
hl.bind(mod .. " + SHIFT + P", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
||||
hl.bind(mod .. " + SHIFT + N", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
||||
|
||||
-- Window cycling (GNOME: cycle-windows on SUPER+Tab).
|
||||
hl.bind(mod .. " + Tab", hl.dsp.window.cycle_next({ next = true }), { description = "Next window" })
|
||||
hl.bind(mod .. " + SHIFT + Tab", hl.dsp.window.cycle_next({ next = false }), { description = "Previous window" })
|
||||
-- Jump back to the previously focused window.
|
||||
hl.bind(mod .. " + SHIFT + grave", hl.dsp.focus({ last = true }), { description = "Last window" })
|
||||
|
||||
-- Mouse: drag to move, right-drag to resize.
|
||||
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
||||
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
||||
|
||||
-- ── Workspaces ──────────────────────────────────────────────────────────────
|
||||
-- ALT is the workspace modifier, matching the GNOME setup.
|
||||
--
|
||||
-- Plain relative selectors ("+1" / "-1") reproduce GNOME's dynamic workspaces:
|
||||
-- moving right past the last workspace creates a new one, and moving left from
|
||||
-- the first clamps instead of wrapping.
|
||||
hl.bind("ALT + H", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
||||
hl.bind("ALT + L", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
||||
hl.bind("ALT + SHIFT + H", hl.dsp.window.move({ workspace = "-1" }), { description = "Move window to workspace left" })
|
||||
hl.bind("ALT + SHIFT + L", hl.dsp.window.move({ workspace = "+1" }), { description = "Move window to workspace right" })
|
||||
|
||||
-- GNOME also had these on CTRL+ALT+Up/Down.
|
||||
hl.bind("CTRL + ALT + up", hl.dsp.focus({ workspace = "-1" }))
|
||||
hl.bind("CTRL + ALT + down", hl.dsp.focus({ workspace = "+1" }))
|
||||
|
||||
-- Direct jump. ALT+0 is workspace 10.
|
||||
for i = 1, 10 do
|
||||
local key = i % 10
|
||||
hl.bind("ALT + " .. key, hl.dsp.focus({ workspace = i }), { description = "Workspace " .. i })
|
||||
hl.bind("ALT + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }), { description = "Move window to workspace " .. i })
|
||||
end
|
||||
|
||||
-- Scroll the mouse wheel over the desktop with SUPER held to change workspace.
|
||||
-- (Scrolling the workspace indicator in the bar does the same; that's handled
|
||||
-- in quickshell/modules/bar/Workspaces.qml.)
|
||||
hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "+1" }))
|
||||
hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "-1" }))
|
||||
|
||||
-- Minimise, as far as Hyprland has one.
|
||||
--
|
||||
-- Hyprland has no minimise: it receives the request (the binary has
|
||||
-- setSetMinimized handlers for xdg, XWayland and foreign-toplevel) but exposes
|
||||
-- no dispatcher, no config option and not even an event to hook, so titlebar
|
||||
-- minimise buttons are inert and cannot be made to work. A tiling WM has no
|
||||
-- iconified state and no taskbar to restore from.
|
||||
--
|
||||
-- The scratchpad is the honest equivalent: the window goes away, and the same
|
||||
-- key brings it back. Bound to X to match the muscle memory it replaces.
|
||||
hl.bind(mod .. " + X", hl.dsp.workspace.toggle_special("scratch"), { description = "Toggle scratchpad (restore minimised)" })
|
||||
hl.bind(mod .. " + SHIFT + X", hl.dsp.window.move({ workspace = "special:scratch" }), { description = "Minimise to scratchpad" })
|
||||
|
||||
-- ── Session ─────────────────────────────────────────────────────────────────
|
||||
-- GNOME's lock was SUPER+L, which is "focus right" here, so lock moves to
|
||||
-- CTRL+ALT+L -- the other binding most people already have in muscle memory.
|
||||
hl.bind("CTRL + ALT + L", hl.dsp.exec_cmd("loginctl lock-session"), { description = "Lock" })
|
||||
hl.bind("CTRL + ALT + Delete", hl.dsp.exec_cmd(qs("powermenu", "toggle")), { description = "Power menu" })
|
||||
|
||||
-- ── Media and volume ────────────────────────────────────────────────────────
|
||||
-- locked = true keeps these working on the lock screen, as they do in GNOME.
|
||||
-- 6% steps match the GNOME volume-step setting.
|
||||
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 6%+"), { locked = true, repeating = true })
|
||||
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 6%-"), { locked = true, repeating = true })
|
||||
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true })
|
||||
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true })
|
||||
|
||||
-- Fine-grained steps, matching GNOME's shift/alt volume modifiers.
|
||||
hl.bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 1%+"), { locked = true, repeating = true })
|
||||
hl.bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-"), { locked = true, repeating = true })
|
||||
|
||||
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
|
||||
hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
|
||||
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
|
||||
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
|
||||
hl.bind("XF86AudioStop", hl.dsp.exec_cmd("playerctl stop"), { locked = true })
|
||||
|
||||
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true })
|
||||
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true })
|
||||
|
||||
-- Hardware keys GNOME mapped that have obvious equivalents.
|
||||
hl.bind("XF86Tools", hl.dsp.exec_cmd(settings))
|
||||
hl.bind("XF86Calculator", hl.dsp.exec_cmd(calculator))
|
||||
hl.bind("XF86Explorer", hl.dsp.exec_cmd(files))
|
||||
hl.bind("XF86WWW", hl.dsp.exec_cmd(browser))
|
||||
hl.bind("XF86Mail", hl.dsp.exec_cmd(mail))
|
||||
hl.bind("XF86Search", hl.dsp.exec_cmd(launcher))
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,218 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Look and feel -- Tokyo Night Moon
|
||||
--
|
||||
-- Colours here must stay in sync with quickshell/config/Theme.qml.
|
||||
-- accent #82aaff borders / focus
|
||||
-- bg #222436 base
|
||||
--
|
||||
-- Performance note: every animation below is event-driven. Nothing uses the
|
||||
-- "loop" style on borderangle/shadowangle/glowangle -- that style forces
|
||||
-- Hyprland to render at full refresh rate forever, even when nothing is
|
||||
-- visible, which is exactly the kind of idle GPU burn to avoid on a 4500x3000
|
||||
-- display.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
hl.config({
|
||||
general = {
|
||||
gaps_in = 5,
|
||||
gaps_out = 10,
|
||||
|
||||
border_size = 2,
|
||||
|
||||
col = {
|
||||
-- The prism: blue leads, orchid follows, on a diagonal so the pair
|
||||
-- is visible on both a tall and a wide window. Same two colours as
|
||||
-- the shell's hairline (quickshell/widgets/PrismEdge.qml) and the
|
||||
-- tmux theme this palette came from.
|
||||
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
||||
-- Unfocused windows get no colour at all. The gradient only means
|
||||
-- something if exactly one window on screen is wearing it.
|
||||
inactive_border = "rgba(3b426199)",
|
||||
},
|
||||
|
||||
resize_on_border = true,
|
||||
|
||||
-- Enables the per-window "immediate" rule used for games in rules.lua.
|
||||
-- Harmless on its own; tearing only happens where a rule opts in.
|
||||
allow_tearing = true,
|
||||
|
||||
layout = "dwindle",
|
||||
|
||||
snap = { enabled = true },
|
||||
},
|
||||
|
||||
decoration = {
|
||||
-- 18 to match the shell's popover radius, so a window and a panel sitting
|
||||
-- next to each other read as the same object family.
|
||||
rounding = 18,
|
||||
rounding_power = 2,
|
||||
|
||||
active_opacity = 1.0,
|
||||
inactive_opacity = 1.0,
|
||||
|
||||
blur = {
|
||||
enabled = true,
|
||||
size = 8,
|
||||
passes = 3,
|
||||
|
||||
-- Required for blur to be affordable. Never turn this off.
|
||||
new_optimizations = true,
|
||||
-- Floating windows skip blurring what's behind tiled ones. Big win.
|
||||
xray = true,
|
||||
|
||||
ignore_opacity = true,
|
||||
noise = 0.0117,
|
||||
contrast = 0.9,
|
||||
brightness = 0.85,
|
||||
vibrancy = 0.17,
|
||||
vibrancy_darkness = 0.05,
|
||||
|
||||
popups = true,
|
||||
popups_ignorealpha = 0.2,
|
||||
|
||||
-- Blurring the special workspace is expensive and rarely seen.
|
||||
special = false,
|
||||
},
|
||||
|
||||
shadow = {
|
||||
enabled = true,
|
||||
range = 20,
|
||||
render_power = 3,
|
||||
sharp = false,
|
||||
color = "rgba(15161eee)",
|
||||
offset = { 0, 4 },
|
||||
scale = 1.0,
|
||||
},
|
||||
|
||||
-- New in 0.56. Kept deliberately faint: in this direction the gradient
|
||||
-- border is the signature, and a strong halo would compete with it.
|
||||
-- This is just enough to lift the focused window off the wallpaper.
|
||||
glow = {
|
||||
enabled = true,
|
||||
range = 8,
|
||||
render_power = 2,
|
||||
color = "rgba(82aaff33)",
|
||||
color_inactive = "rgba(00000000)",
|
||||
},
|
||||
|
||||
-- Off: costs real frame time and reads as smeary on a 60Hz panel.
|
||||
motion_blur = { enabled = false },
|
||||
},
|
||||
|
||||
animations = { enabled = true },
|
||||
|
||||
dwindle = {
|
||||
-- Keep the split orientation a window was created with. Closest match
|
||||
-- to how the Forge extension behaved on GNOME.
|
||||
preserve_split = true,
|
||||
smart_resizing = true,
|
||||
},
|
||||
|
||||
misc = {
|
||||
force_default_wallpaper = 0,
|
||||
disable_hyprland_logo = true,
|
||||
disable_splash_rendering = true,
|
||||
|
||||
font_family = "Adwaita Sans",
|
||||
|
||||
-- Variable refresh rate. 3 = enable only for fullscreen windows whose
|
||||
-- content type is "video" or "game" -- the tag rules.lua applies to
|
||||
-- games. Keeps VRR off the desktop, where it causes visible flicker.
|
||||
vrr = 3,
|
||||
|
||||
-- Blur behind the lock screen.
|
||||
session_lock_blur = true,
|
||||
|
||||
-- Let a crashed lock screen be recovered rather than stranding you.
|
||||
allow_session_lock_restore = true,
|
||||
|
||||
-- Don't let apps steal focus by shouting; matches GNOME's behaviour.
|
||||
focus_on_activate = false,
|
||||
},
|
||||
|
||||
render = {
|
||||
-- Colour management is stable and on by default in 0.56.
|
||||
cm_enabled = true,
|
||||
|
||||
-- 1 = automatically flip the monitor into HDR for fullscreen content
|
||||
-- that asks for it, and back out afterwards. This is how games get HDR
|
||||
-- without the desktop paying the screencopy cost. See monitors.lua.
|
||||
cm_auto_hdr = 1,
|
||||
|
||||
-- 2 = direct scanout only for windows tagged content = "game"
|
||||
-- (set by the rules in rules.lua). Bypasses compositing for real
|
||||
-- fullscreen games.
|
||||
direct_scanout = 2,
|
||||
},
|
||||
|
||||
cursor = {
|
||||
-- 2 = auto. Disables the hardware cursor when tearing, which is what
|
||||
-- keeps the cursor from ghosting in games.
|
||||
no_hardware_cursors = 2,
|
||||
|
||||
-- Don't let the cursor break VRR on content tagged as a game.
|
||||
no_break_fs_vrr = 2,
|
||||
min_refresh_rate = 48,
|
||||
|
||||
-- Pull the cursor theme from gsettings so GTK apps and Hyprland agree.
|
||||
sync_gsettings_theme = true,
|
||||
|
||||
-- Fade the cursor out after 4s of no movement, like GNOME does.
|
||||
inactive_timeout = 4,
|
||||
},
|
||||
|
||||
ecosystem = {
|
||||
no_update_news = true,
|
||||
no_donation_nag = true,
|
||||
},
|
||||
|
||||
xwayland = {
|
||||
-- Render X11 apps at 1:1 device pixels so they're sharp rather than
|
||||
-- bilinearly upscaled. See the note in README.md about the unavoidable
|
||||
-- sizing tradeoff at 1.5x.
|
||||
force_zero_scaling = true,
|
||||
},
|
||||
|
||||
quirks = {
|
||||
-- 2 = report HDR as preferred to gamescope only. Stops gamescope from
|
||||
-- white-screening when it expects HDR to already be live.
|
||||
prefer_hdr = 2,
|
||||
},
|
||||
})
|
||||
|
||||
-- ── Motion ──────────────────────────────────────────────────────────────────
|
||||
-- speed is in deciseconds: speed = 4 means 400ms. Higher = slower.
|
||||
|
||||
hl.curve("easeOutQuint", { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } })
|
||||
hl.curve("almostLinear", { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } })
|
||||
hl.curve("quick", { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } })
|
||||
hl.curve("easy", { type = "spring", mass = 1, stiffness = 238.1191, dampening = 24.21279 })
|
||||
-- A touch bouncier than "easy"; used for windows appearing.
|
||||
hl.curve("snappy", { type = "spring", mass = 1, stiffness = 320, dampening = 22 })
|
||||
|
||||
hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" })
|
||||
|
||||
-- The focus sweep: when a window takes focus, its gradient border rotates into
|
||||
-- place once. Style "once" is load-bearing -- the alternative, "loop", makes
|
||||
-- Hyprland render at full refresh rate forever, even when no border is visible.
|
||||
-- That is exactly the idle GPU cost this config exists to avoid, so: never loop.
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 6, bezier = "easeOutQuint", style = "once" })
|
||||
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "snappy", style = "popin 90%" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "almostLinear", style = "popin 90%" })
|
||||
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" })
|
||||
|
||||
-- Layer surfaces = the Quickshell bar, dock, popovers and overlays.
|
||||
hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" })
|
||||
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "easeOutQuint", style = "fade" })
|
||||
hl.animation({ leaf = "layersOut", enabled = true, speed = 2, bezier = "almostLinear", style = "fade" })
|
||||
|
||||
-- Workspace switching: horizontal slide, matching Alt+H / Alt+L direction.
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 2.5, bezier = "easeOutQuint", style = "slidefade 15%" })
|
||||
|
||||
hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" })
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,43 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Monitors
|
||||
--
|
||||
-- Kuycon P20 on DP-2: 4500x3000 @ 60Hz, 1.5x fractional scale.
|
||||
-- 4500/1.5 = 3000 and 3000/1.5 = 2000, both integers, so this is a "clean"
|
||||
-- fractional scale and Hyprland will not complain.
|
||||
--
|
||||
-- On HDR --------------------------------------------------------------------
|
||||
-- GNOME runs this panel in bt2100/HDR. Hyprland CAN do that (cm = "hdr"), but
|
||||
-- on 0.56.x it currently breaks screencopy: grim screenshots come back empty,
|
||||
-- and OBS/Sunshine capture and hyprlock's blurred background go with them.
|
||||
-- Root cause is still open upstream. Since screen capture matters more here
|
||||
-- than an HDR desktop, the desktop runs SDR at 10-bit and HDR is handed to
|
||||
-- fullscreen games only, via render.cm_auto_hdr in looks.lua.
|
||||
--
|
||||
-- To try full-time HDR anyway, set cm = "hdr" below (or override it in
|
||||
-- overrides.lua) and read the notes in that file first.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
hl.monitor({
|
||||
output = "DP-2",
|
||||
mode = "4500x3000@60",
|
||||
position = "0x0",
|
||||
scale = 1.5,
|
||||
|
||||
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of
|
||||
-- DP 1.4 HBR3, so this relies on DSC. If the display fails to light up or
|
||||
-- falls back to a lower mode, drop this line first.
|
||||
bitdepth = 10,
|
||||
|
||||
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
||||
cm = "auto",
|
||||
})
|
||||
|
||||
-- Any monitor not named above: sane defaults rather than nothing.
|
||||
hl.monitor({
|
||||
output = "",
|
||||
mode = "preferred",
|
||||
position = "auto",
|
||||
scale = "auto",
|
||||
})
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,33 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Per-machine overrides
|
||||
--
|
||||
-- Loaded last, so anything here beats the modules above. Keep machine-specific
|
||||
-- settings in this file rather than editing the others -- it keeps the rest of
|
||||
-- the config portable between boxes.
|
||||
--
|
||||
-- Full-time HDR ---------------------------------------------------------------
|
||||
-- The desktop ships SDR at 10-bit with HDR handed to fullscreen games (see
|
||||
-- monitors.lua for why). To run the whole desktop in HDR, uncomment this:
|
||||
--
|
||||
-- hl.monitor({
|
||||
-- output = "DP-2",
|
||||
-- mode = "4500x3000@60",
|
||||
-- position = "0x0",
|
||||
-- scale = 1.5,
|
||||
-- bitdepth = 10,
|
||||
-- cm = "hdr",
|
||||
-- sdrbrightness = 1.2, -- how bright SDR content appears, 1.0 - 2.0
|
||||
-- sdrsaturation = 0.98,
|
||||
-- })
|
||||
--
|
||||
-- Then expect to also need some of these, in this order:
|
||||
-- hl.config({ render = { keep_unmodified_copy = 1 } }) -- if captures go transparent
|
||||
-- hl.config({ render = { use_shader_blur_blend = true } }) -- if that kills blur
|
||||
-- hl.config({ render = { direct_scanout = 0 } }) -- if screenshots come back empty
|
||||
-- hl.config({ cursor = { no_hardware_cursors = 1 } }) -- if the cursor looks oversaturated
|
||||
--
|
||||
-- If screenshots, OBS or Sunshine break and stay broken, that is the known
|
||||
-- upstream issue -- go back to cm = "auto".
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,207 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Window, workspace and layer rules
|
||||
--
|
||||
-- Rules evaluate top to bottom, last match wins -- but every NAMED rule is
|
||||
-- evaluated before every anonymous one, so a named rule can never override an
|
||||
-- anonymous rule further down. Names are used sparingly here for that reason.
|
||||
--
|
||||
-- Matching is Google RE2 (no backtracking). Prefix a pattern with "negative:"
|
||||
-- to invert it.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- ── Upstream sanity rules ───────────────────────────────────────────────────
|
||||
hl.window_rule({
|
||||
name = "suppress-maximize-events",
|
||||
match = { class = ".*" },
|
||||
suppress_event = "maximize",
|
||||
})
|
||||
|
||||
hl.window_rule({
|
||||
name = "fix-xwayland-drags",
|
||||
match = {
|
||||
class = "^$",
|
||||
title = "^$",
|
||||
xwayland = true,
|
||||
float = true,
|
||||
fullscreen = false,
|
||||
pin = false,
|
||||
},
|
||||
no_focus = true,
|
||||
})
|
||||
|
||||
-- ── Floating apps ───────────────────────────────────────────────────────────
|
||||
-- Carried over from the Forge windows.json override list, minus entries for
|
||||
-- apps that are no longer installed.
|
||||
hl.window_rule({
|
||||
match = {
|
||||
class = "^(jetbrains-toolbox|zoom|Cider|Hidamari|com\\.mattjakeman\\.ExtensionManager)$",
|
||||
},
|
||||
float = true,
|
||||
})
|
||||
|
||||
hl.window_rule({
|
||||
match = { class = "^([Bb]itwarden|com\\.bitwarden\\.desktop)$" },
|
||||
float = true,
|
||||
})
|
||||
|
||||
hl.window_rule({
|
||||
match = { class = "^(com\\.nextcloud\\.desktopclient\\.nextcloud|Nextcloud)$" },
|
||||
float = true,
|
||||
})
|
||||
|
||||
hl.window_rule({
|
||||
match = { class = "^(org\\.gnome\\.Calculator|gnome-calculator)$" },
|
||||
float = true,
|
||||
size = { 400, 600 },
|
||||
center = true,
|
||||
})
|
||||
|
||||
hl.window_rule({
|
||||
match = { class = "^(mpv|io\\.mpv\\.Mpv)$" },
|
||||
float = true,
|
||||
-- Video should never be blurred or dimmed behind another window.
|
||||
no_blur = true,
|
||||
no_dim = true,
|
||||
})
|
||||
|
||||
hl.window_rule({
|
||||
match = { class = "^(com\\.spotify\\.Client|Spotify)$" },
|
||||
float = true,
|
||||
})
|
||||
|
||||
-- Settings-style utility windows and pickers: float and centre, like GNOME did.
|
||||
hl.window_rule({
|
||||
match = { class = "^(pavucontrol|org\\.pulseaudio\\.pavucontrol|nm-connection-editor|blueman-manager|org\\.gnome\\.Settings)$" },
|
||||
float = true,
|
||||
size = { "monitor_w * 0.45", "monitor_h * 0.6" },
|
||||
center = true,
|
||||
})
|
||||
|
||||
-- Portal dialogs (file chooser, screen share picker) should always float.
|
||||
hl.window_rule({
|
||||
match = { class = "^(xdg-desktop-portal-gtk|org\\.freedesktop\\.impl\\.portal\\.desktop\\.gtk|hyprland-share-picker)$" },
|
||||
float = true,
|
||||
center = true,
|
||||
})
|
||||
|
||||
hl.window_rule({
|
||||
match = { title = "^(Open File|Save File|Save As|Open Folder|Select a File|Choose Files)$" },
|
||||
float = true,
|
||||
center = true,
|
||||
})
|
||||
|
||||
-- Polkit prompt.
|
||||
hl.window_rule({
|
||||
match = { class = "^(hyprpolkitagent|polkit-gnome-authentication-agent-1)$" },
|
||||
float = true,
|
||||
center = true,
|
||||
})
|
||||
|
||||
-- ── Gaming ──────────────────────────────────────────────────────────────────
|
||||
-- content = "game" is the keystone: misc.vrr = 3, render.direct_scanout = 2 and
|
||||
-- cursor.no_break_fs_vrr = 2 all key off it. Blur, animation, shadow and dim are
|
||||
-- all disabled so the compositor gets out of the way entirely.
|
||||
hl.window_rule({
|
||||
match = { class = "^(steam_app_\\d+|gamescope|lutris|net\\.lutris\\.Lutris|com\\.heroicgameslauncher\\.hgl|Minecraft.*|moonlight|com\\.moonlight_stream\\.Moonlight)$" },
|
||||
content = "game",
|
||||
immediate = true,
|
||||
no_blur = true,
|
||||
no_anim = true,
|
||||
no_shadow = true,
|
||||
no_dim = true,
|
||||
})
|
||||
|
||||
-- Steam itself is a normal window, but its transient popups are a mess.
|
||||
hl.window_rule({
|
||||
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
||||
float = true,
|
||||
})
|
||||
|
||||
-- Fullscreen video in a browser also benefits from no blur / no dim.
|
||||
hl.window_rule({
|
||||
match = { fullscreen = true },
|
||||
no_blur = true,
|
||||
no_dim = true,
|
||||
})
|
||||
|
||||
-- ── Workspace rules ─────────────────────────────────────────────────────────
|
||||
-- Deliberately NO "smart gaps".
|
||||
--
|
||||
-- The usual trick is to strip gaps, border and rounding when a workspace holds
|
||||
-- a single tiled window (`workspace = "w[tv1]"` + matching window rules). It
|
||||
-- was here and it was wrong for this setup: the common case is one window on a
|
||||
-- workspace, so the desktop spent most of its time square-cornered and
|
||||
-- edge-to-edge -- the opposite of the intended look. Gaps and rounding are
|
||||
-- constant now, however many windows are open.
|
||||
|
||||
-- ── Layer rules (Quickshell surfaces) ───────────────────────────────────────
|
||||
-- Namespaces are set in QML via WlrLayershell.namespace; keep these regexes and
|
||||
-- those strings in sync. `hyprctl layers` lists what is actually live.
|
||||
--
|
||||
-- The bar intentionally has no rule: it is a transparent edge-to-edge input
|
||||
-- surface, not glass. Its individual controls paint their own hover feedback.
|
||||
-- Keeping a full-width blur rule here would spend compositor work on pixels
|
||||
-- the bar does not draw.
|
||||
|
||||
hl.layer_rule({
|
||||
name = "qs-dock",
|
||||
match = { namespace = "^qs-dock$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.3,
|
||||
})
|
||||
|
||||
-- Popovers: calendar, quick settings, notification centre, tray menus.
|
||||
hl.layer_rule({
|
||||
name = "qs-popover",
|
||||
match = { namespace = "^qs-popover" },
|
||||
blur = true,
|
||||
blur_popups = true,
|
||||
ignore_alpha = 0.2,
|
||||
})
|
||||
|
||||
-- Overview, capture and local screen-reading UI dim the desktop behind them.
|
||||
hl.layer_rule({
|
||||
name = "qs-overlay",
|
||||
match = { namespace = "^qs-(overview|capture|screen-intelligence)$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.4,
|
||||
dim_around = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- Notification toasts. Blurred like every other shell surface -- without this
|
||||
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
||||
-- read as washed out rather than as glass.
|
||||
--
|
||||
-- no_anim because the toasts animate themselves in QML, and no_screen_share so
|
||||
-- they don't leak into OBS or Sunshine streams.
|
||||
hl.layer_rule({
|
||||
name = "qs-notifications",
|
||||
match = { namespace = "^qs-notifications$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.15,
|
||||
no_anim = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- Signal Glass provides its own event-driven motion, so the compositor only
|
||||
-- supplies the material blur and privacy behavior.
|
||||
hl.layer_rule({
|
||||
name = "qs-signal-glass",
|
||||
match = { namespace = "^qs-signal-glass$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.15,
|
||||
no_anim = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- The launcher blurs and dims whatever is behind it.
|
||||
hl.layer_rule({
|
||||
name = "launcher",
|
||||
match = { namespace = "^(vicinae|wofi)$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.5,
|
||||
dim_around = true,
|
||||
})
|
||||
|
||||
return true
|
||||
Reference in New Issue
Block a user