17 Commits
Author SHA1 Message Date
nvrl 1f7f5aef8a fixed some quickshell stuff + hypr 2026-09-28 23:27:00 +02:00
nvrl 32b43bdbb3 quickshell fixes 2026-09-24 13:35:20 +02:00
nvrlandClaude Opus 5 d2975650fa theme: move the alarm colour from pink to mauve
Mocha's pink is a pastel. Filled at slab size against the near-black
Persona surfaces it went chalky rather than sharp, which is what made
hovered destructive buttons read as a washed-out red. Mauve is saturated
enough to bite against that ground and still cannot be confused with the
blue accent.

Applies across quickshell, Hyprland, dunst, fish and nvim. The GTK
overrides are dropped entirely — stock Catppuccin Mocha Blue already
paints those surfaces, and a hand-written destructive colour there was
one more thing to keep in step for no gain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 02:12:53 +02:00
nvrlandClaude Opus 5 e221445a51 theme: Catppuccin Mocha blue/pink variant of the Persona rice
Keeps the Takemi geometry — the lean, the chamfer, the slabs — and swaps
the palette wholesale. Mocha carries the ground; blue is the accent for
anything live (focus, selection, borders, hover) and pink is the alarm
for anything destructive or failed, so one hue no longer has to mean
both 'active' and 'wrong'.

Destructive controls rest as a pink keyline and fill solid only when
hovered or selected: Mocha's pink is a pastel, and a resting slab of it
reads as washed-out red rather than as a warning. The load ramp climbs
text -> blue -> pink for the same reason, instead of fading straight
through a muddy mid-tone.

Surfaces covered: quickshell, Hyprland borders, alacritty, fish, dunst,
and nvim (via the catppuccin plugin, submodule branch catppuccin-mocha).
GTK 3/4, Qt and xsettingsd move to catppuccin-mocha-blue and are now
tracked, since nothing else makes non-shell apps match the rice; the
previous Takemi copies are in theme-repair-backups/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 02:11:02 +02:00
nvrlandClaude Opus 5 3fb89c4370 fix(qs): show power menu only on the focused screen
A PowerMenu window exists per screen and every one of them lit up on
open. Latch the focused monitor in Actions at open time — same shape as
launcherScreen — and gate each window's open state on it, so exactly one
copy draws and takes keyboard focus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 01:53:56 +02:00
nvrlandClaude Opus 5 1cf5a4b68d chore: add enqs-headroom user service, set nvim as text/plain handler
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 01:52:50 +02:00
nvrl 5f3a44f82d fixed launcher nav + headroom completion 2026-09-17 12:21:09 +02:00
nvrl 51dc5e80c4 feat(menu) added launcher, bt menu, clipboard 2026-09-16 15:53:19 +02:00
nvrl 35048ae283 feat(quickshell): replace fuzzel with an in-process launcher
fuzzel could only answer "which app", so launching Brave at a URL meant
picking the app, waiting for it, then typing the address somewhere else.
The launcher now takes a verb and an object in one line: type
`gitlab.enqs.de` and every app that can open it sorts to the top, default
browser first, ready for Enter — or arrow/hjkl down to Brave's New
Incognito Window, which is offered as a peer of Brave itself rather than
hidden behind a submenu.

services/Apps.qml is the brain and holds no UI: it flattens desktop
entries and their actions into one index, classifies the typed argument
(url / mail / path), ranks by fuzzy match plus handler affinity plus
frecency, and launches. Launching through `uwsm app` keeps each app in its
own scope; entries whose Exec declares no field code (Brave's
`--incognito` action, among others) are launched by tokenising Exec per
the Desktop Entry spec and appending the argument, never via `sh -c`.

Four prefixes reuse the same window, so there is one picker and one set of
keys: `>` runs a command, `=` evaluates arithmetic, `:` is clipboard
history (cliphist, which had been a second fuzzel invocation), and a
dmenu mode is driven over IPC.

quickshell/scripts/takemi-dmenu is the bridge for that last one — a
`fuzzel --dmenu` drop-in for scripts that want a picker, symlinked into
~/.local/bin. fluxo's bt menu now goes through it.

Living in the Quickshell process means the window is on screen on the
frame the bind fires, with no process to spawn.

Removes fuzzel entirely: its config, its theme submodule, the package,
and the last consumers (the bar sigil, fluxo's menu_command). Also drops
hypr/scripts/powermenu.sh, which was fuzzel-based and bound to nothing.
2026-09-16 15:46:51 +02:00
nvrl 9e73c1338f fixed some bugs 2026-08-15 02:11:03 +02:00
nvrl 9cf510e93b fix(qs,fluxo) performance fix for repaints 2026-08-12 20:18:56 +02:00
nvrl 86e90b0213 chore(hypr) migrated to lua confs 2026-08-12 18:05:43 +02:00
nvrl a868f50ab7 improvements 2026-08-11 17:35:26 +02:00
nvrl 39f925cb58 updated design 2026-08-11 16:34:09 +02:00
nvrl 3e02c3f36a fixed tray + media popouts 2026-08-11 11:29:04 +02:00
nvrl ccb54f01d9 added tray menu + fixed antialising 2026-08-11 11:08:44 +02:00
nvrl 58e0ba2f99 fixed tray 2026-08-11 09:40:55 +02:00
140 changed files with 6655 additions and 2267 deletions

No files matched your search

+18 -1
View File
@@ -12,12 +12,13 @@
!waybar/ !waybar/
!zathura/ !zathura/
!mimeapps.list !mimeapps.list
!fuzzel/
!fish/ !fish/
!packages !packages
!brave-flags.conf !brave-flags.conf
!chromium-flags.conf !chromium-flags.conf
!README.md !README.md
!plans/
!DESIGN-IS-2026-08-11/
!rice.png !rice.png
!rice-2.png !rice-2.png
!rice-3.png !rice-3.png
@@ -27,3 +28,19 @@
fish/fish_variables fish/fish_variables
waybar/fluxo-rs waybar/fluxo-rs
!fluxo/ !fluxo/
# Desktop theming. These live outside the per-app configs above, but they are
# what makes an app that is neither GTK-aware nor shell-aware match the rice,
# so the branch carries them.
!gtk-3.0/
gtk-3.0/*
!gtk-3.0/settings.ini
!gtk-3.0/gtk.css
!gtk-4.0/
gtk-4.0/*
!gtk-4.0/settings.ini
!gtk-4.0/gtk.css
!xsettingsd/
!qt6ct/
qt6ct/*
!qt6ct/qt6ct.conf
-3
View File
@@ -19,9 +19,6 @@
[submodule "btop/catppuccin-btop"] [submodule "btop/catppuccin-btop"]
path = btop/catppuccin-btop path = btop/catppuccin-btop
url = https://github.com/catppuccin/btop.git url = https://github.com/catppuccin/btop.git
[submodule "fuzzel/catppuccin-fuzzel"]
path = fuzzel/catppuccin-fuzzel
url = https://github.com/catppuccin/fuzzel.git
[submodule "nvim"] [submodule "nvim"]
path = nvim path = nvim
url = https://git.narl.io/nvrl/nvim-config.git url = https://git.narl.io/nvrl/nvim-config.git
+36
View File
@@ -0,0 +1,36 @@
# Takemi shell design audit — scope
## Audited artifact
- Running Persona 5-inspired desktop shell shown in the two supplied 1920×1080 screenshots.
- Quickshell implementation under `quickshell/`, including the top bar, right rail,
pop-outs, tray menus, power overlay, notifications/OSD, and lock screen.
- Shared design tokens and primitives in `quickshell/config/` and
`quickshell/components/`.
## User and task
- Primary user: the workstation owner using the shell throughout the day.
- Primary task: identify system state and reach common desktop controls quickly,
without the theme obscuring application content.
## Constraints
- Preserve the Persona 5 / Tae Takemi identity.
- Preserve the crimson, black, and white palette and screen-print character.
- Stack remains Quickshell, Hyprland, and Fluxo.
- The result must work at desktop scale, with keyboard and pointer input, and
remain legible during long sessions.
## References
- Persona 5 interface language: asymmetric editorial composition, hard ink,
decisive silhouettes, halftone texture, and rapid card-like motion.
- Current implementation and supplied screenshots are the concrete baseline;
fidelity to the game is subordinate to daily desktop usefulness.
## Out of scope
- Application-level themes outside the shell.
- Replacing the wallpaper or character artwork.
- Changing the desktop stack.
+114
View File
@@ -0,0 +1,114 @@
# Evidence
## 1. Innovative
- The shell applies one custom QML geometry and print system across panels,
backdrops, motion, and textures (`quickshell/config/Theme.qml:118-153`,
`quickshell/components/P5Panel.qml:7-56`).
- This refreshes standard desktop bars/popovers, but the interaction model is
still conventional bar + rail + popup + modal.
## 2. Useful
- The permanent bar exposes workspaces, active window, media, resources, and
time in one slab (`quickshell/bar/Bar.qml:67-122`).
- Power and lock are keyboard-operable; bar, rail, tray, and most pop-out
actions remain pointer-only (`quickshell/overlays/PowerMenu.qml:242-261`,
`quickshell/lock/LockFace.qml:290-311`,
`quickshell/bar/RailIcon.qml:104-124`).
- The primary surfaces contain at least 41 interactive targets; the live total
grows with workspaces, devices, streams, notifications, and tray entries.
## 3. Aesthetic
- A clear system exists: spacing `[6,12,20,32]`, type
`[9,11,12,15,20,34,92]`, 12 literal palette colors, one nominal 14° angle,
and shared 10px cuts (`quickshell/config/Theme.qml:19-49,92-148`).
- Runtime type exceptions include 8, 30, 42, and 156.4px; local geometry also
introduces several non-token cuts and leans.
- White on crimson is 3.68:1 and fails normal-text AA where selected destructive
actions or unread badges use that pairing
(`quickshell/components/P5Button.qml:32-73`,
`quickshell/bar/NotifButton.qml:38-58`).
- Screenshot #1: permanent chrome is visually continuous and disciplined, but
competes with an already high-contrast Persona wallpaper/frame.
- Screenshot #2: the left headline/right action-stack composition is strong;
the repeated stripe field and still-legible desktop behind it dilute focus.
## 4. Understandable
- The power screen labels all actions and prints its keyboard map
(`quickshell/overlays/PowerMenu.qml:31-38,216-261`).
- Rail icons expose different left/right/middle/scroll behaviors without visible
instruction (`quickshell/bar/Rail.qml:76-132`).
- Several state labels behave as inverse actions: “Wi-Fi On,” Bluetooth “On,”
“Scanning,” and “Silenced” toggle those states off.
- “WHO GOES THERE,” “WRONG,” “HELD,” “PROCS,” “VRAM,” and unlabeled load values
favor voice or jargon over immediate comprehension.
## 5. Unobtrusive
- Normal chrome consumes only a 40px top bar and 48px right rail
(`quickshell/config/Theme.qml:109-116`).
- The full-screen modes add stripes, a wedge, oversized split text, attribution,
six slabs, and a hint simultaneously
(`quickshell/overlays/PowerMenu.qml:50-228`).
- Twelve action identities are repeated between permanent chrome and detail
panels; repetition improves reach but adds competing affordances.
## 6. Honest
- No marketing inflation or dark patterns were found. Power labels map directly
to their system action (`quickshell/overlays/PowerMenu.qml:31-38,230-239`).
- “n HELD” counts all history rather than suppressed items
(`quickshell/popouts/NotifPopout.qml:27`,
`quickshell/services/Notifs.qml:36-44`).
- Muted OSDs display 0% even when stored volume remains nonzero
(`quickshell/overlays/Osd.qml:51-64,172-185`).
- A visually disabled slider can still emit changes
(`quickshell/components/P5Slider.qml:9-12,48-84`).
## 7. Long-lasting
- The system is based on reusable primitives rather than per-screen decoration.
- Literal `P5` branding, the quote attribution, and the esports-like continuous
frame tie the result closely to one entertainment reference and visual era
(`quickshell/bar/Sigil.qml:31`,
`quickshell/overlays/PowerMenu.qml:88-118`).
## 8. Thorough
- Empty, loading, failure, focus, and disabled states exist.
- A rendered success state is absent; generic buttons lack a disabled state;
focus treatment is concentrated in power and lock; explicit accessible names
and roles total zero (`quickshell/components/P5Button.qml:6-107`).
- The lock’s disabled state is clear, but `P5Slider` can look disabled while
remaining interactive.
## 9. Environmentally friendly
- The shell has 0 JavaScript bytes, about 259KB of QML, 1.4KB of local texture
assets, no declared HTTP request, and a measured load proxy below two seconds.
- Seven Fluxo reads recur in polling batches; playing media creates 3–6 idle
animation loops (`quickshell/services/Sys.qml:78-102`,
`quickshell/bar/MediaPill.qml:125-153`).
- No reduced-motion branch exists (`quickshell/config/Theme.qml:139-148`).
## 10. As little design as possible
- The one-slab bar avoids a row of redundant cards
(`quickshell/bar/Bar.qml:8-14,40-63`).
- The audited tree reaches seven declarative levels, contains twelve repeated
action identities, and includes removable atmospheric elements in modal and
lock compositions.
- The permanent surface, transient surface, and cinematic surface currently
share too much of the same visual intensity instead of forming a hierarchy.
## Known gaps
- Dynamic application strings, device counts, and menu depth cannot be fixed
from static source.
- Exact compositor frame time, GPU cost, and live accessibility-tree output
were not available.
- Screenshot evidence covers the normal desktop and power overlay; lock-screen
judgments also use its implementation source.
+43
View File
@@ -0,0 +1,43 @@
# Rams scorecard
1. **Good design is innovative — 2/3**
Evidence: one custom print/geometry system refreshes conventional shell surfaces (`01-evidence.md#1-innovative`).
Justification: the expression is distinctive, but it does not introduce a new desktop interaction pattern.
2. **Good design makes a product useful — 2/3**
Evidence: core state and actions are direct, while most shell controls require a pointer (`01-evidence.md#2-useful`).
Justification: the primary tasks work quickly, but keyboard and discoverability gaps add friction.
3. **Good design is aesthetic — 2/3**
Evidence: the token system is strong, with several type/geometry exceptions and a 3.68:1 text pairing (`01-evidence.md#3-aesthetic`).
Justification: the visual system is unmistakable, but contrast and intensity inconsistencies prevent a fully resolved score.
4. **Good design makes a product understandable — 1/3**
Evidence: multiple rail gestures are undisclosed and several state labels read like actions (`01-evidence.md#4-understandable`).
Justification: more than three controls require prior knowledge or experimentation.
5. **Good design is unobtrusive — 1/3**
Evidence: compact permanent chrome expands into visually competing full-screen decoration (`01-evidence.md#5-unobtrusive`).
Justification: the theme sometimes becomes the figure when the user’s task should remain the figure.
6. **Good design is honest — 2/3**
Evidence: no dark patterns exist, but held-count, muted-volume, and disabled-slider presentation diverge from behavior (`01-evidence.md#6-honest`).
Justification: the mismatches are minor rather than deceptive, but prevent a one-to-one mapping everywhere.
7. **Good design is long-lasting — 2/3**
Evidence: reusable primitives coexist with literal franchise and gaming-HUD markers (`01-evidence.md#7-long-lasting`).
Justification: the craft will last, while the most literal references may date the surface.
8. **Good design is thorough — 1/3**
Evidence: success, generic disabled, broad focus, and accessibility metadata are missing or rough (`01-evidence.md#8-thorough`).
Justification: three or more important state families remain incomplete.
9. **Good design is environmentally friendly — 1/3**
Evidence: source weight is small, but recurring polling and idle motion have no reduced-motion gate (`01-evidence.md#9-environmentally-friendly`).
Justification: the runtime is lean yet motion is always on when its conditions are met.
10. **Good design is as little design as possible — 1/3**
Evidence: restrained single slabs coexist with repeated affordances and multi-layer modal atmosphere (`01-evidence.md#10-as-little-design-as-possible`).
Justification: at least three decorative or duplicated elements could be removed without breaking a task.
**Total: 15/30**
+30
View File
@@ -0,0 +1,30 @@
# Verdict — REDESIGN
The strict Rams verdict is **REDESIGN (15/30)**: preserve the excellent brand
tokens and working shell primitives, but redesign the hierarchy so permanent,
transient, and cinematic surfaces no longer compete at the same intensity.
## Highest-leverage moves
1. **Principles #4 and #8 — make behavior legible:** give every icon one primary
click action, add concise Persona-styled hover labels, expose secondary actions
inside its pop-out, and add real focus/disabled/accessibility states. Evidence:
`01-evidence.md#4-understandable` and `#8-thorough`.
2. **Principles #5 and #10 — introduce three intensity tiers:** quiet permanent
chrome, expressive transient panels, theatrical full-screen scenes; reserve
stripes, large split type, and strong halftone for the latter two. Evidence:
`01-evidence.md#5-unobtrusive` and `#10-as-little-design-as-possible`.
3. **Principles #3 and #6 — assign semantics by treatment, not red alone:** use
white fill for selection, a red rule/slash for active state, solid red only
for destructive/error, and pulse only for urgency; fix white-on-red small text.
Evidence: `01-evidence.md#3-aesthetic` and `#6-honest`.
4. **Principles #3 and #7 — move the chaos to macro-composition:** keep clean
module geometry, but vary panel silhouette, title placement, and scale by
context instead of repeating the same framed HUD grammar everywhere. Evidence:
`01-evidence.md#3-aesthetic` and `#7-long-lasting`.
5. **Principles #2 and #9 — add a calm mode:** stop idle equalizer/pulse/marquee
motion when reduced motion is requested and reduce polling when a detail panel
is closed. Evidence: `01-evidence.md#2-useful` and `#9-environmentally-friendly`.
This is a hierarchy redesign, not an instruction to discard the palette,
textures, panel engine, services, or current functionality.
+41
View File
@@ -0,0 +1,41 @@
# Planning handoff
```text
/make-plan Redesign the Takemi Persona 5 desktop shell. Current design failed audit at 15/30 with critical gaps in principles #4 understandable, #5 unobtrusive, #8 thorough, #9 environmentally friendly, and #10 as little design as possible.
Verdict paragraph:
> The strict Rams verdict is REDESIGN (15/30): preserve the excellent brand tokens and working shell primitives, but redesign the hierarchy so permanent, transient, and cinematic surfaces no longer compete at the same intensity.
Why redesign and not refine: the total is below the 20-point refine threshold because the same high-intensity visual and interaction grammar spans permanent chrome, transient controls, and full-screen scenes.
Preserve from current design:
- Crimson/black/white palette, type scale foundation, and motion timings in quickshell/config/Theme.qml:19-148.
- Single-slab bar/rail architecture in quickshell/bar/Bar.qml:8-63 and quickshell/bar/Rail.qml:9-64.
- Reusable P5Panel geometry/texture engine in quickshell/components/P5Panel.qml:7-248.
- Existing services and feature coverage; this is an information/design hierarchy change, not a functionality cut.
Discard:
- One-intensity-fits-all use of stripes, halftone, split type, slashes, and repeated framed geometry. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#5-unobtrusive. Caused failure on principles #5 and #10.
- Undisclosed multi-button/multi-gesture behavior on icon-only permanent controls. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#4-understandable. Caused failure on principles #4 and #8.
Top moves:
1. Principles #4 and #8 — make behavior legible: give every icon one primary click action, add concise Persona-styled hover labels, expose secondary actions inside its pop-out, and add real focus/disabled/accessibility states. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#4-understandable and #8-thorough.
2. Principles #5 and #10 — introduce three intensity tiers: quiet permanent chrome, expressive transient panels, theatrical full-screen scenes; reserve stripes, large split type, and strong halftone for the latter two. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#5-unobtrusive and #10-as-little-design-as-possible.
3. Principles #3 and #6 — assign semantics by treatment, not red alone: use white fill for selection, a red rule/slash for active state, solid red only for destructive/error, and pulse only for urgency; fix white-on-red small text. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#3-aesthetic and #6-honest.
4. Principles #3 and #7 — move the chaos to macro-composition: keep clean module geometry, but vary panel silhouette, title placement, and scale by context instead of repeating the same framed HUD grammar everywhere. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#3-aesthetic and #7-long-lasting.
5. Principles #2 and #9 — add a calm mode: stop idle equalizer/pulse/marquee motion when reduced motion is requested and reduce polling when a detail panel is closed. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#2-useful and #9-environmentally-friendly.
Redesign priorities:
1. Principle #4 — every visible control communicates its primary action without memorized mouse-button lore.
2. Principles #5/#10 — visual intensity increases deliberately from chrome to pop-out to modal.
3. Principles #3/#6 — color, fill, outline, pattern, and motion each have one stable semantic job.
Deliverables:
- New information architecture, not derived mechanically from the old layout.
- New labeled primary flow compared side-by-side with the current shell.
- Empty/loading/error/success/focus/disabled state matrix.
- Migration path that preserves existing service behavior and keybinds.
- Cutover criteria based on discoverability, contrast, reduced motion, and visual hierarchy.
Guard against porting the old structure under new styling, running both hierarchies indefinitely, following generic gaming-HUD trends, or discarding the preserved brand tokens and primitives.
```
+1 -1
View File
@@ -107,7 +107,7 @@ ipc_socket = true
## GENERAL -------------------------------------------------------- ## GENERAL --------------------------------------------------------
## Import additional configuration files. ## Import additional configuration files.
import = ["~/.config/alacritty/takemi.toml", "~/.config/alacritty/fonts.toml"] import = ["~/.config/alacritty/catppuccin-mocha.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]] [[keyboard.bindings]]
key = "Return" key = "Return"
+4 -2
View File
@@ -4,9 +4,11 @@ foreground = "#cdd6f4"
dim_foreground = "#7f849c" dim_foreground = "#7f849c"
bright_foreground = "#cdd6f4" bright_foreground = "#cdd6f4"
# Cursor in the shell accent (Mocha blue) rather than stock rosewater, so the
# terminal caret matches the focus colour everywhere else on the desktop.
[colors.cursor] [colors.cursor]
text = "#1e1e2e" text = "#11111b"
cursor = "#f5e0dc" cursor = "#89b4fa"
[colors.vi_mode_cursor] [colors.vi_mode_cursor]
text = "#1e1e2e" text = "#1e1e2e"
+1 -1
View File
@@ -68,7 +68,7 @@ update_ms = 1000
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct", #* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly. #* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
proc_sorting = "memory" proc_sorting = "cpu direct"
#* Reverse sorting order, True or False. #* Reverse sorting order, True or False.
proc_reversed = false proc_reversed = false
+2 -2
View File
@@ -1,10 +1,10 @@
[global] [global]
corner_radius = 4 corner_radius = 4
transparency = 90 transparency = 90
frame_color = "#f38ba8" frame_color = "#89b4fa"
frame_width = 1 frame_width = 1
separator_color= frame separator_color= frame
highlight = "#89dceb" highlight = "#cba6f7"
[urgency_low] [urgency_low]
background = "#1e1e2e" background = "#1e1e2e"
+100
View File
@@ -0,0 +1,100 @@
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_claude_account_global_optspecs
string join \n h/help V/version
end
function __fish_claude_account_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_claude_account_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end
function __fish_claude_account_using_subcommand
set -l cmd (__fish_claude_account_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end
complete -c claude-account -n "__fish_claude_account_needs_command" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_needs_command" -s V -l version -d 'Print version'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "status" -d 'Show every account, every proxy, every name and everything that is wrong'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "list" -d 'List the accounts, one per line'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "switch" -d 'Make an account the one `claude` runs as'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "add" -d 'Register another account, with a profile and a proxy of its own'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "remove" -d 'Forget an account. Its profile directory is left where it is'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "current" -d 'Print the name of the active account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "dir" -d 'Print the profile directory of an account, for `CLAUDE_CONFIG_DIR`'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "env" -d 'Print the shell lines that put this shell into an account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "url" -d 'Print the address a name resolves to'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "setup" -d 'Install everything at once: the wrapper, a proxy per account, the router'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "adopt" -d 'Take over the proxy services that were written by hand'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "doctor" -d 'Report every problem and exit non-zero when there is one'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "shim" -d 'The `claude` wrapper in ~/.local/bin'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "headroom" -d 'The proxy of one account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "router" -d 'The router that resolves <name>.headroom.localhost'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "completions" -d 'Tab completion, with the account names in it'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s v -l verbose -d 'Add the last lines each proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -l names -d 'Print the names alone, for scripts and for tab completion'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand switch" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l dir -d 'A profile directory elsewhere' -r -F
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l port -d 'A port for its proxy; the next free one by default' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l switch -d 'Make it the account `claude` runs as straight away'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand remove" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand current" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand dir" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand env" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand url" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand setup" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand adopt" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand doctor" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "install" -d 'Write ~/.local/bin/claude'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "uninstall" -d 'Remove it again'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -l force -d 'Overwrite a `claude` that this tool did not write'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "install" -d 'Write the service for an account and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "start" -d 'Start the proxy of an account'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "stop" -d 'Stop it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "restart" -d 'Stop and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "port" -d 'Give an account another port, and write it everywhere it is named'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "logs" -d 'Print what one proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from start" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from stop" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from restart" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s n -l lines -d 'How many lines' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "serve" -d 'Serve the names until stopped'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "install" -d 'Install the router as a user service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "port" -d 'Move the router to another port'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "list" -d 'List the names that resolve'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -l port -r
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -l install -d 'Write the file where the shell reads it, instead of printing it'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -s h -l help -d 'Print help'
# The accounts as they are right now, asked for when Tab is pressed.
complete -c claude-account -n '__fish_seen_subcommand_from switch dir env url remove start stop restart logs install uninstall port' -f -a '(claude-account list --names)'
+1
View File
@@ -0,0 +1 @@
complete --keep-order --exclusive --command enqs-guard --arguments "(COMPLETE=fish enqs-guard -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
+1
View File
@@ -0,0 +1 @@
complete --keep-order --exclusive --command enqs-headroom --arguments "(COMPLETE=fish enqs-headroom -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
+1
View File
@@ -0,0 +1 @@
complete --keep-order --exclusive --command enqs-jail --arguments "(COMPLETE=fish enqs-jail -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
+43 -41
View File
@@ -1,50 +1,52 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml, # Mocha — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua. # ~/.config/alacritty/catppuccin-mocha.toml and ~/.config/nvim/lua/takemi.lua.
# #
# Replaces the catppuccin fish_frozen_theme.fish that fish 4.3 generated on # Catppuccin Mocha's own fish theme (themes/Catppuccin Mocha.theme), with the
# upgrade. Same content as themes/Takemi.theme — that copy exists only so # selection and pager rows carried over from the Takemi layout: a solid accent
# `fish_config` can preview it; this one is what actually loads. # block with crust punched out of it, rather than a tinted surface. The shell
# selects with a blue slab and marks with mauve; the prompt should not
# disagree.
# #
# Note that `fish_config` writing a theme from the web UI will recreate # Note that `fish_config` writing a theme from the web UI will recreate
# fish_frozen_theme.fish, which sorts before this file and so would lose. # fish_frozen_theme.fish, which sorts before this file and so would lose.
set --global fish_color_normal ffffff set --global fish_color_normal cdd6f4
set --global fish_color_command ffffff --bold set --global fish_color_command 89b4fa
set --global fish_color_keyword ff2d40 set --global fish_color_keyword f38ba8
set --global fish_color_param c8c8ce set --global fish_color_param f2cdcd
set --global fish_color_option 8d9bb8 set --global fish_color_option a6e3a1
set --global fish_color_quote 7f9b6a set --global fish_color_quote a6e3a1
set --global fish_color_escape 9db8bd set --global fish_color_escape eba0ac
set --global fish_color_operator c8c8ce set --global fish_color_operator f5c2e7
set --global fish_color_redirection ff6b78 set --global fish_color_redirection f5c2e7
set --global fish_color_end 6b6b70 set --global fish_color_end fab387
set --global fish_color_comment 6b6b70 --italics set --global fish_color_comment 7f849c --italics
set --global fish_color_error ff2d40 --bold set --global fish_color_error f38ba8 --bold
set --global fish_color_cancel ff2d40 set --global fish_color_cancel f38ba8
set --global fish_color_gray 6b6b70 set --global fish_color_gray 6c7086
set --global fish_color_autosuggestion 4a4a52 set --global fish_color_autosuggestion 6c7086
set --global fish_color_selection 000000 --background=ff2d40 set --global fish_color_selection 11111b --background=89b4fa
set --global fish_color_search_match 000000 --background=c8c8ce set --global fish_color_search_match 11111b --background=cba6f7
set --global fish_color_match 000000 --background=ff2d40 set --global fish_color_match 11111b --background=89b4fa
set --global fish_color_history_current --bold set --global fish_color_history_current --bold
set --global fish_color_valid_path --underline set --global fish_color_valid_path --underline
set --global fish_color_cwd ffffff --bold set --global fish_color_cwd f9e2af --bold
set --global fish_color_cwd_root ff2d40 --bold set --global fish_color_cwd_root f38ba8 --bold
set --global fish_color_user c8c8ce set --global fish_color_user 94e2d5
set --global fish_color_host ffffff --bold set --global fish_color_host 89b4fa --bold
set --global fish_color_host_remote ff6b78 set --global fish_color_host_remote a6e3a1
set --global fish_color_status ff2d40 set --global fish_color_status f38ba8
set --global fish_pager_color_progress 6b6b70 --background=141418 set --global fish_pager_color_progress 6c7086 --background=313244
set --global fish_pager_color_background set --global fish_pager_color_background
set --global fish_pager_color_prefix ffffff --bold set --global fish_pager_color_prefix cba6f7 --bold
set --global fish_pager_color_completion c8c8ce set --global fish_pager_color_completion cdd6f4
set --global fish_pager_color_description 6b6b70 set --global fish_pager_color_description 6c7086
set --global fish_pager_color_secondary_background --background=0d0d10 set --global fish_pager_color_secondary_background --background=181825
set --global fish_pager_color_secondary_completion c8c8ce set --global fish_pager_color_secondary_completion cdd6f4
set --global fish_pager_color_secondary_description 6b6b70 set --global fish_pager_color_secondary_description 6c7086
set --global fish_pager_color_secondary_prefix ffffff --bold set --global fish_pager_color_secondary_prefix cba6f7 --bold
set --global fish_pager_color_selected_background --background=ff2d40 set --global fish_pager_color_selected_background --background=89b4fa
set --global fish_pager_color_selected_completion 000000 set --global fish_pager_color_selected_completion 11111b
set --global fish_pager_color_selected_description 000000 set --global fish_pager_color_selected_description 11111b
set --global fish_pager_color_selected_prefix 000000 --bold set --global fish_pager_color_selected_prefix 11111b --bold
-3
View File
@@ -38,6 +38,3 @@ if status is-login && test -z "$DISPLAY" -a -z "$WAYLAND_DISPLAY"
end end
end end
# Claude environments
alias claude="CLAUDE_CONFIG_DIR=~/.claude command claude"
alias claude-work="CLAUDE_CONFIG_DIR=~/.claude-work command claude"
+25 -3
View File
@@ -5,9 +5,16 @@
# strings. The Quickshell config (~/.config/quickshell/services/Sys.qml) parses # strings. The Quickshell config (~/.config/quickshell/services/Sys.qml) parses
# them and does its own formatting. The previous human-readable Waybar formats # them and does its own formatting. The previous human-readable Waybar formats
# are preserved in config.toml.waybar-bak. # are preserved in config.toml.waybar-bak.
#
# Quickshell only consumes cpu, mem, gpu, disk, net, sys and power from fluxo.
# Everything else it gets natively — Quickshell.Services.Mpris, .Pipewire,
# .UPower, .Notifications, Quickshell.Bluetooth — and Brightness.qml reads
# sysfs directly. Those fluxo modules are therefore switched off below: left on,
# they held a libpulse context, a BlueZ D-Bus poll every 2 s and an MPRIS
# marquee ticker firing twice a second, all feeding a bar that does not exist.
[general] [general]
menu_command = "fuzzel --dmenu --prompt \"$FLUXO_PROMPT\"" menu_command = "takemi-dmenu --prompt \"$FLUXO_PROMPT\""
[signals] [signals]
network = 1 network = 1
@@ -44,12 +51,18 @@ format = "{mount}|{used:.1}|{total:.1}"
format = "{percentage}" format = "{percentage}"
[audio] [audio]
# Quickshell.Services.Pipewire drives Audio.qml; leaving this on kept a
# libpulse context and its callbacks alive for nothing.
enabled = false
format_sink_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>" format_sink_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>"
format_sink_muted = "{name} <span size='large'> {icon}</span>" format_sink_muted = "{name} <span size='large'> {icon}</span>"
format_source_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>" format_source_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>"
format_source_muted = "{name} <span size='large'>{icon}</span>" format_source_muted = "{name} <span size='large'>{icon}</span>"
[bt] [bt]
# Quickshell.Bluetooth drives Bt.qml. This module polled BlueZ over D-Bus every
# 2 seconds regardless.
enabled = false
format_plugin = "{alias} [{left}|{right}] {anc} <span size='large'>󰂰</span>" format_plugin = "{alias} [{left}|{right}] {anc} <span size='large'>󰂰</span>"
format_connected = "{alias} <span size='large'>󰂰</span>" format_connected = "{alias} <span size='large'>󰂰</span>"
format_disconnected = "Disconnected <span size='large'>󰂯</span>" format_disconnected = "Disconnected <span size='large'>󰂯</span>"
@@ -60,6 +73,11 @@ format_active = "<span size='large'>󰊖</span>"
format_inactive = "<span size='large'></span>" format_inactive = "<span size='large'></span>"
[mpris] [mpris]
# Quickshell.Services.Mpris drives Media.qml, and MediaPill/MediaPopout do their
# own eliding and scrolling. This module was the single most expensive thing in
# the daemon: `scroll` woke a ticker every `scroll_speed` ms to advance a marquee
# offset for a Waybar that is not running, and each tick drove the signaler.
enabled = false
format = "{artist} - {title}" format = "{artist} - {title}"
max_length = 20 max_length = 20
scroll = true scroll = true
@@ -81,10 +99,14 @@ enabled = false
format = "{layout}" format = "{layout}"
[backlight] [backlight]
enable = true # Was `enable = true` — not a key fluxo reads, so this section was silently
# running on its default of enabled. Brightness.qml reads
# /sys/class/backlight directly, so the D-Bus watcher here is redundant.
enabled = false
format = "{percentage}" format = "{percentage}"
[dnd] [dnd]
enabled = true # Notifs.qml owns do-not-disturb via Quickshell.Services.Notifications.
enabled = false
format_dnd = "<span size='large'>󰂛</span>" format_dnd = "<span size='large'>󰂛</span>"
format_normal = "<span size='large'>󰂚</span>" format_normal = "<span size='large'>󰂚</span>"
-19
View File
@@ -1,19 +0,0 @@
include=~/.config/fuzzel/takemi.ini
[main]
# JetBrainsMono to match Theme.fontMono, so the launcher and the bar are
# visibly the same typeface.
font=JetBrainsMono Nerd Font:size=14
terminal=alacritty
dpi-aware=no
width=45
lines=8
horizontal-pad=20
vertical-pad=20
inner-pad=10
prompt=" "
show-actions=no
layer=overlay
-37
View File
@@ -1,37 +0,0 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua.
#
# Near-black ground, white text, crimson as the only saturated colour. The
# selection is a solid crimson block with the text punched out of it in black,
# which is the same "active" signal the bar and the terminal use.
[colors]
# Flat and fully opaque. The Persona surfaces are ink on ground — translucency
# and blur read as a different design language, so the alpha stays at ff.
background=0a0a0cff
text=c8c8ceff
input=ffffffff
placeholder=6b6b70ff
prompt=ff2d40ff
# Fuzzy-match characters in an unselected row: crimson, since they are the
# reason the row is on screen at all.
match=ff2d40ff
selection=ff2d40ff
selection-text=000000ff
# White rather than black, so the matched characters still read as marked once
# the row is already crimson.
selection-match=ffffffff
# Deliberately the same as the background — hides the match counter.
counter=0a0a0cff
border=ff2d40ff
[border]
# Square. Every corner in this style is either a right angle or a hard chamfer;
# nothing in the shell is rounded.
radius=0
# Theme.stroke.
width=2
+4
View File
@@ -0,0 +1,4 @@
/* Catppuccin Mocha Blue carries the whole GTK3 palette itself, so there is
* nothing to override here — the Takemi branch only needed this file because
* it was painting Fluent's greys black. Kept as a hook for the next time the
* rice disagrees with stock Catppuccin. */
+7
View File
@@ -0,0 +1,7 @@
[Settings]
gtk-theme-name=catppuccin-mocha-blue-standard+default
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
+4
View File
@@ -0,0 +1,4 @@
/* Catppuccin Mocha Blue, straight from the installed theme. The Takemi branch
* overrode Fluent's greys with black here; Mocha needs no override — its own
* surfaces are the palette the rest of the rice is built on. */
@import url("/usr/share/themes/catppuccin-mocha-blue-standard+default/gtk-4.0/gtk.css");
+7
View File
@@ -0,0 +1,7 @@
[Settings]
gtk-theme-name=catppuccin-mocha-blue-standard+default
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime.version": "Lua 5.4",
"workspace.library": [
"/usr/share/hypr/stubs"
],
"diagnostics.globals": [
"hl"
]
}
-18
View File
@@ -1,18 +0,0 @@
# narls hyprland dotfiles
source = ~/.config/hypr/modules/monitors.conf
source = ~/.config/hypr/modules/programs.conf
source = ~/.config/hypr/modules/autostart.conf
source = ~/.config/hypr/modules/env.conf
source = ~/.config/hypr/modules/theme.conf
source = ~/.config/hypr/modules/input.conf
source = ~/.config/hypr/modules/keybinds.conf
source = ~/.config/hypr/modules/window_rules.conf
+36
View File
@@ -0,0 +1,36 @@
-- narls hyprland dotfiles
--
-- Hyprland 0.55+ reads this file instead of hyprland.conf. Every part lives in
-- lua/, and each require() is its own scope: an error in one file does not stop
-- the others from loading.
--
-- Order matters only in two places: palette must exist before anything paints
-- with it, and monitors must exist before the lid handler binds to them. Both
-- are handled by the requires below, top to bottom.
-- what the desktop looks like
require("lua/palette")
require("lua/look")
require("lua/animations")
-- what it runs on
require("lua/env")
require("lua/monitors")
require("lua/lid")
require("lua/input")
-- what it runs
require("lua/apps")
require("lua/autostart")
-- how windows behave
require("lua/rules/windows")
require("lua/rules/layers")
-- how it is driven
require("lua/binds/apps")
require("lua/binds/windows")
require("lua/binds/workspaces")
require("lua/binds/shell")
require("lua/binds/media")
require("lua/binds/capture")
+1 -1
View File
@@ -1,6 +1,6 @@
wallpaper { wallpaper {
monitor = monitor =
path = ~/.wallpapers/takemi_1.jpg path = ~/.wallpapers/arch.png
fit_mode = cover fit_mode = cover
} }
-1
View File
@@ -1 +0,0 @@
+47
View File
@@ -0,0 +1,47 @@
-- https://wiki.hypr.land/Configuring/Advanced-and-Cool/Animations/
--
-- Quick and clean. Motion should get out of the way, not perform.
--
-- An earlier pass overdid it: a 1.56 overshoot on every window, an 88% popin
-- scale, and a borderangle looping forever so the focused border was in
-- constant motion. The bounce is dialled back to a hint, the popin barely
-- scales, and the border angle does not animate at all.
hl.config({
animations = {
enabled = true,
},
})
-- ------------------------------------------------------------------- curves
hl.curve("snap", { type = "bezier", points = { { 0.16, 1.00 }, { 0.30, 1.00 } } })
hl.curve("overshoot", { type = "bezier", points = { { 0.34, 1.12 }, { 0.64, 1.00 } } })
hl.curve("hard", { type = "bezier", points = { { 0.55, 0.00 }, { 0.90, 0.35 } } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
-- --------------------------------------------------------------------- tree
-- speed is in ds (1ds = 100ms). Unset leaves inherit their parent.
hl.animation({ leaf = "global", enabled = true, speed = 4, bezier = "snap" })
hl.animation({ leaf = "border", enabled = true, speed = 5, bezier = "snap" })
hl.animation({ leaf = "windows", enabled = true, speed = 4, bezier = "snap" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4, bezier = "overshoot", style = "popin 96%" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 2.6, bezier = "hard", style = "popin 97%" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 3.6, bezier = "snap" })
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "fadeIn", enabled = true, speed = 2.4, bezier = "snap" })
hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.8, bezier = "hard" })
hl.animation({ leaf = "fadeSwitch", enabled = true, speed = 2, bezier = "snap" })
hl.animation({ leaf = "fadeShadow", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "fadeDim", enabled = true, speed = 2.4, bezier = "snap" })
hl.animation({ leaf = "layers", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "snap", style = "slide" })
hl.animation({ leaf = "layersOut", enabled = true, speed = 2, bezier = "hard", style = "slide" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 2.4, bezier = "hard", style = "slidevert" })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
+24
View File
@@ -0,0 +1,24 @@
-- The programs the binds reach for. Exported so lua/binds/* name them once.
return {
terminal = "alacritty",
fileManager = "nautilus",
-- The launcher lives in the Quickshell process (quickshell/launcher/), so
-- the bind toggles it over IPC instead of spawning anything. Nothing to
-- start up means it is on screen on the same frame as the keypress.
menu = "qs ipc call shell launcher",
-- Clipboard history. Opens the launcher already in its ":" mode, which
-- reads cliphist itself and copies the chosen entry back — so the picker is
-- the same window, with the same keys, as everything else.
clipboard = "qs ipc call shell prompt :",
-- Proton Drive search. --fzf re-queries the daemon on every keystroke, so
-- results appear as you type; a dmenu could only filter a list it was
-- handed once, which made searching a two-step. It spawns its own terminal
-- (class pdfs-prompt, floated in lua/rules/windows.lua).
pdfs = "pdfs-prompt --gtk",
-- bluetooth menu
bluetooth = "fluxo bt menu",
}
+30
View File
@@ -0,0 +1,30 @@
-- https://wiki.hypr.land/Configuring/Basics/Autostart/
--
-- Everything that used to be exec-once now runs off the start event.
hl.on("hyprland.start", function()
-- Takemi shell — bar, popouts, notifications, OSD, power menu and lock
-- screen. Config lives in ~/.config/quickshell. It owns
-- org.freedesktop.Notifications, so dunst must stay masked
-- (systemctl --user mask dunst.service).
hl.exec_cmd("uwsm app -- qs")
hl.exec_cmd("uwsm app -- nm-applet --indicator")
hl.exec_cmd("uwsm app -- hyprpaper")
-- clipboard history, text and images
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
-- pdfs-prompt has no hidden/resident mode, so the first search of the
-- session is cold. Run one throwaway query once the Proton Drive daemon
-- is up to pull the binary into the page cache and warm the daemon's index.
hl.exec_cmd("sh -c 'sleep 15; pdfs-prompt --feed a >/dev/null 2>&1'")
-- hl.exec_cmd("uwsm app -- nextcloud --background")
-- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive")
-- hl.exec_cmd("uwsm app -- protonvpn-app")
-- hl.exec_cmd("uwsm app -- /usr/bin/discord --enable-features=UseOzonePlatform --ozone-platform=wayland --start-minimized")
-- hl.exec_cmd("uwsm app -- /usr/lib/xdg-desktop-portal-hyprland")
-- hl.exec_cmd("sleep 5 && ~/.config/hypr/scripts/replay-ctrl.sh start")
end)
+14
View File
@@ -0,0 +1,14 @@
-- Launching things.
-- https://wiki.hypr.land/Configuring/Basics/Binds/
local apps = require("lua/apps")
local mod = "SUPER" -- Sets "Windows" key as main modifier
hl.bind(mod .. " + RETURN", hl.dsp.exec_cmd(apps.terminal), { description = "terminal" })
hl.bind(mod .. " + E", hl.dsp.exec_cmd(apps.fileManager), { description = "file manager" })
hl.bind(mod .. " + SPACE", hl.dsp.exec_cmd(apps.menu), { description = "app launcher" })
hl.bind(mod .. " + M", hl.dsp.exec_cmd(apps.pdfs), { description = "pdf picker" })
hl.bind(mod .. " + B", hl.dsp.exec_cmd(apps.bluetooth), { description = "bluetooth menu" })
-- Clipboard history. On release so the picker does not inherit the held ALT.
hl.bind("ALT + m", hl.dsp.exec_cmd(apps.clipboard), { release = true, description = "clipboard history" })
+19
View File
@@ -0,0 +1,19 @@
-- Screenshots, replay buffer, webcam. Everything that records something.
local scripts = os.getenv("HOME") .. "/.config/hypr/scripts/"
-- ------------------------------------------------------------- screenshots
-- PRINT selects a region, CTRL grabs the focused window, SHIFT saves to disk
-- instead of the clipboard.
hl.bind("PRINT", hl.dsp.exec_cmd('grim -g "$(slurp)" - | wl-copy'))
hl.bind("SHIFT + PRINT", hl.dsp.exec_cmd('grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +\'%Y-%m-%d_%H-%M-%S\').png'))
hl.bind("CTRL + PRINT", hl.dsp.exec_cmd(scripts .. "screenshot_window.sh copy"))
hl.bind("CTRL + SHIFT + PRINT", hl.dsp.exec_cmd(scripts .. "screenshot_window.sh save"))
-- ------------------------------------------------------- replay and camera
-- On release, so the shortcut is not caught mid-chord.
local on_release = { release = true }
hl.bind("ALT + z", hl.dsp.exec_cmd(scripts .. "replay-ctrl.sh toggle"), on_release)
hl.bind("ALT + SHIFT + z", hl.dsp.exec_cmd(scripts .. "replay-ctrl.sh save"), on_release)
hl.bind("ALT + SHIFT + c", hl.dsp.exec_cmd(scripts .. "droidcam-ctrl.sh toggle"), on_release)
+20
View File
@@ -0,0 +1,20 @@
-- Laptop multimedia keys: volume, mic, brightness, transport.
-- `locked` keeps them working over the lock screen.
-- Volume and brightness repeat when held.
local held = { locked = true, repeating = true }
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("fluxo vol up 5"), held)
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("fluxo vol down 5"), held)
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("fluxo vol mute"), held)
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("fluxo mic mute"), held)
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl s +10%"), held)
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl s 10%-"), held)
-- Transport. Requires playerctl.
local locked = { locked = true }
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), locked)
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), locked)
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), locked)
hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), locked)
+18
View File
@@ -0,0 +1,18 @@
-- Takemi shell (quickshell) IPC. Config lives in ~/.config/quickshell.
local mod = "SUPER"
local function shell(call, opts)
hl.bind(opts.key, hl.dsp.exec_cmd("qs ipc call shell " .. call), { description = opts.description })
end
shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
shell("power", { key = mod .. " + P", description = "power menu" })
shell("ask", { key = mod .. " + A", description = "ask claude" })
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
-- Not a shell call, but it lives with the other session toggles.
local mirror = require("lua/mirror")
hl.bind(mod .. " + SHIFT + P", mirror.toggle, { description = "mirror laptop to all screens" })
+30
View File
@@ -0,0 +1,30 @@
-- Focus, layout and groups — everything that acts on the window under you.
local mod = "SUPER"
-- ------------------------------------------------------------------- window
hl.bind(mod .. " + SHIFT + Q", hl.dsp.window.close())
hl.bind(mod .. " + V", hl.dsp.window.float())
hl.bind(mod .. " + F", hl.dsp.window.fullscreen())
-- hl.bind(mod .. " + t", hl.dsp.layout("togglesplit")) -- dwindle
-- -------------------------------------------------------------------- focus
local directions = { h = "l", l = "r", k = "u", j = "d" }
for key, direction in pairs(directions) do
hl.bind(mod .. " + " .. key, hl.dsp.focus({ direction = direction }))
hl.bind(mod .. " + SHIFT + " .. key:upper(), hl.dsp.window.move({ direction = direction, group_aware = true }))
end
-- ------------------------------------------------------------------- groups
hl.bind(mod .. " + n", hl.dsp.group.toggle())
hl.bind("ALT + Tab", hl.dsp.group.next())
-- hl.bind("ALT + Tab", hl.dsp.group.prev())
-- -------------------------------------------------------------------- mouse
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
-- ----------------------------------------------------------------- keyboard
-- cycle keyboard layout (dvorak-intl <-> us-intl), see lua/input.lua
hl.bind(mod .. " + ALT + SPACE", hl.dsp.exec_cmd("hyprctl switchxkblayout all next"))
+22
View File
@@ -0,0 +1,22 @@
-- Workspaces, including the two scratchpads.
local mod = "SUPER"
-- Switch with mod + [0-9], move the active window with mod + SHIFT + [0-9].
for i = 1, 10 do
local key = i % 10 -- 10 maps to key 0
hl.bind(mod .. " + " .. key, hl.dsp.focus({ workspace = i }))
hl.bind(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }))
end
-- Scroll through existing workspaces with mod + scroll
hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
-- Scratchpads: S holds anything virtual, D holds chat.
local special = { S = "virtual", D = "discord" }
for key, name in pairs(special) do
hl.bind(mod .. " + " .. key, hl.dsp.workspace.toggle_special(name))
hl.bind(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = "special:" .. name }))
end
+38
View File
@@ -0,0 +1,38 @@
-- https://wiki.hypr.land/Configuring/Advanced-and-Cool/Environment-variables/
--
-- Note: this session starts through uwsm, which would rather have these in
-- ~/.config/uwsm/env (toolkit, cursor, GPU) and ~/.config/uwsm/env-hyprland
-- (HYPR*, AQ_*). They are kept here for now so the config stays self-contained.
-- cursor
hl.env("HYPRCURSOR_THEME", "Bibata-Modern-Classic")
hl.env("HYPRCURSOR_SIZE", "24")
hl.env("XCURSOR_THEME", "Bibata-Modern-Classic")
hl.env("XCURSOR_SIZE", "24")
-- session
hl.env("XDG_CURRENT_DESKTOP", "Hyprland")
-- toolkits
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct")
hl.env("MOZ_ENABLE_WAYLAND", "1")
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "wayland")
-- AMD graphics / video acceleration
hl.env("LIBVA_DRIVER_NAME", "radeonsi")
hl.env("VDPAU_DRIVER", "radeonsi")
hl.env("AMD_VULKAN_ICD", "RADV")
-- misc
hl.env("EDITOR", "nvim")
-- hl.env("INTEL_DEBUG", "noccs")
-- hl.env("WLR_DRM_NO_ATOMIC", "1")
-- hl.env("debug:full_cm_proto", "true")
-- headroom
hl.env("HEADROOM_ROLLOUT_CHANNEL", "beta")
hl.env("HEADROOM_OUTPUT_SHAPER", "1")
+32
View File
@@ -0,0 +1,32 @@
-- https://wiki.hypr.land/Configuring/Basics/Variables/#input
hl.config({
input = {
-- index 0 = dvorak-intl (default), index 1 = us-intl
-- switch with $mainMod ALT, SPACE (see lua/binds/windows.lua)
kb_layout = "us,us",
kb_variant = "dvorak-intl,intl",
kb_model = "",
kb_rules = "",
kb_options = "caps:backspace",
follow_mouse = 0,
accel_profile = "flat",
sensitivity = 0, -- -1.0 - 1.0, 0 means no modification.
touchpad = {
natural_scroll = false,
},
},
})
-- Layouts are inherited from the global input block so that layout switching
-- applies to these too. Only bind resolution differs here.
-- https://wiki.hypr.land/Configuring/Advanced-and-Cool/Devices/
for _, name in ipairs({
"at-translated-set-2-keyboard",
"topre-corporation-hhkb-professional",
}) do
hl.device({ name = name, resolve_binds_by_sym = true })
end
+129
View File
@@ -0,0 +1,129 @@
-- Laptop lid handling.
--
-- One rule, applied from several triggers:
--
-- panel off <=> lid is shut AND something else is plugged in
--
-- Both halves are re-checked every time, so the session can never end up with
-- zero outputs. Undocking with the lid shut brings the panel back; booting
-- alone after a docked shutdown just works, because nothing about the previous
-- session is remembered.
--
-- That last part is the fix for the old design: scripts/lid_handler.sh wrote a
-- lid_state.conf that got sourced on the next start, so a laptop that was shut
-- while docked came up believing it still had an external display and blanked
-- its own panel. Lid state now comes from the kernel instead, and the external
-- check is a live one.
local monitors = require("lua/monitors")
-- ------------------------------------------------------------- reading state
-- The kernel's own view of the hinge. Present on this machine as
-- /proc/acpi/button/lid/LID/state; the device name differs per vendor, hence
-- the glob. Returns nil if the file is missing or unparseable.
local function acpi_lid_closed()
local pipe = io.popen("cat /proc/acpi/button/lid/*/state 2>/dev/null")
if not pipe then
return nil
end
local out = pipe:read("a") or ""
pipe:close()
if out:find("closed", 1, true) then
return true
elseif out:find("open", 1, true) then
return false
end
return nil
end
-- Whatever the last switch event said, used only when ACPI has nothing for us.
local last_switch_closed = nil
local function lid_closed()
local acpi = acpi_lid_closed()
if acpi ~= nil then
return acpi
end
return last_switch_closed == true
end
-- ---------------------------------------------------------------- reconciling
-- nil until the first decision, so the first pass always applies.
local panel_off = nil
-- `ignore` is the output being unplugged: during monitor.removed it can still
-- be listed, and counting it would keep the panel switched off.
local function reconcile(ignore)
-- Nothing enumerated yet means we are still parsing the config on a cold
-- start. Leave the defaults from lua/monitors.lua alone and wait for the
-- start event.
local all = hl.get_monitors()
if #all == 0 then
return
end
local externals = 0
for _, mon in ipairs(monitors.externals_present()) do
if mon.name ~= ignore then
externals = externals + 1
end
end
local want_off = lid_closed() and externals > 0
if want_off == panel_off then
return
end
-- First decision of the session, and it agrees with what lua/monitors.lua
-- already applied: nothing actually moved, so no daemon needs poking.
local settled = panel_off == nil and not want_off
panel_off = want_off
if want_off then
monitors.disable_builtin()
else
monitors.enable_builtin()
end
monitors.apply_external()
-- Quickshell follows monitor hotplug by itself; only the wallpaper daemon
-- needs a kick.
if not settled then
hl.exec_cmd("systemctl restart --user hyprpaper")
end
end
-- ------------------------------------------------------------------ triggers
hl.bind("switch:on:Lid Switch", function()
last_switch_closed = true
reconcile()
end, { locked = true })
hl.bind("switch:off:Lid Switch", function()
last_switch_closed = false
reconcile()
end, { locked = true })
-- Docking and undocking. Removal passes the output that is going away.
hl.on("monitor.added", function()
reconcile()
end)
hl.on("monitor.removed", function(mon)
reconcile(mon and mon.name)
end)
-- Cold start: outputs are not up yet at this point, so this settles once they
-- are. On `hyprctl reload` the start event does not fire again, but the
-- monitor list is already populated, so run it inline too.
hl.on("hyprland.start", function()
reconcile()
end)
reconcile()
+109
View File
@@ -0,0 +1,109 @@
-- Persona × Mocha — Persona 5 shapes on the Catppuccin Mocha palette, matched
-- to ~/.config/quickshell.
--
-- Everything static about how the desktop is painted. Motion lives next door
-- in lua/animations.lua.
local c = require("lua/palette")
hl.config({
-- https://wiki.hypr.land/Configuring/Basics/Variables/
general = {
gaps_in = 5,
gaps_out = { top = 6, right = 8, bottom = 8, left = 8 },
border_size = 2,
col = {
active_border = c.activeBorder,
-- Surface-level, not outline: an idle window's frame is structure,
-- and the outline grey was bright enough to compete with the
-- focused border.
inactive_border = c.surfaceAlt,
},
resize_on_border = false,
-- Please see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Tearing/
-- before you turn this on
allow_tearing = false,
layout = "dwindle",
},
decoration = {
-- Persona 5 has no rounded corners. Neither do we.
rounding = 0,
-- Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0,
inactive_opacity = 1.0,
-- A hard crust offset, the same printed drop the shell's panels
-- carry, rather than a soft blur. Sharp with a 1px range leaves a
-- clean band down the right and bottom edges only.
shadow = {
enabled = true,
sharp = true,
range = 1,
offset = { 4, 4 },
color = c.ink,
color_inactive = c.ink,
},
-- Off. Blur is the signature of the glass-and-frost look, which is the
-- opposite of what this theme is doing — every surface here is opaque,
-- flat and hard-edged. With nothing translucent left to blur it also
-- costs three render passes for no visible result.
blur = {
enabled = false,
},
},
misc = {
force_default_wallpaper = 0,
disable_hyprland_logo = true,
-- 0 = off, 1 = always, 2 = fullscreen only (G7 is a 240Hz VRR panel)
vrr = 0,
font_family = "FiraCode Nerd Font",
background_color = c.base,
focus_on_activate = true,
},
group = {
col = {
border_active = c.activeBorder,
border_inactive = c.surfaceAlt,
},
groupbar = {
font_family = "JetBrainsMono Nerd Font",
font_size = 11,
height = 24,
gradients = false,
-- --- Active tab ---
col = {
active = c.accent,
inactive = c.surface,
},
text_color = c.ink,
-- --- Inactive tab ---
text_color_inactive = c.muted,
indicator_height = 3,
},
},
-- https://wiki.hypr.land/Configuring/Layouts/Dwindle-Layout/
dwindle = {
preserve_split = true,
},
-- https://wiki.hypr.land/Configuring/Layouts/Master-Layout/
master = {
new_status = "master",
},
})
+107
View File
@@ -0,0 +1,107 @@
-- Presentation mode: every external output shows the laptop panel.
--
-- Toggled from lua/binds/shell.lua. Mirroring is just a `mirror` field on the
-- monitor rule, so switching it off clears that field and re-applies the specs
-- from lua/monitors.lua to put the Samsung back where it belongs.
--
-- Anything plugged in while mirroring is on gets mirrored too, which is the
-- usual order at a talk: press the key, then find the HDMI cable.
local monitors = require("lua/monitors")
local M = {}
local function notify(text)
hl.notification.create({ text = text, timeout = 3000 })
end
-- The panel is off while docked with the lid shut, see lua/lid.lua. Mirroring
-- a disabled output would leave every screen black.
local function builtin()
for _, mon in ipairs(hl.get_monitors()) do
if mon.name == monitors.laptop then
return mon
end
end
return nil
end
-- A mirroring output drops out of hl.get_monitors(), so the only place left
-- to find it is the panel's own `mirrors` list. That list is also the state:
-- a Lua flag would reset on every config reload while the outputs stay
-- mirrored.
local function mirroring(panel)
local names = {}
for _, mon in ipairs(panel and panel.mirrors or {}) do
table.insert(names, mon.name)
end
return names
end
-- Outputs keep their own mode while mirroring, and the panel image gets
-- rescaled to fit it every frame. The Dells at 1920x1200 flicker with that,
-- so match the panel's resolution; Hyprland falls back to the preferred mode
-- if the output cannot do it.
local function mirror_all(panel)
local mode = panel.width .. "x" .. panel.height
for _, mon in ipairs(monitors.externals_present()) do
hl.monitor({ output = mon.name, mirror = monitors.laptop, mode = mode, position = "auto" })
end
end
-- The bar and popouts stay on the outputs they were created for, so
-- respawn the shell the same way lua/autostart.lua starts it.
local function restart_clients()
hl.exec_cmd("systemctl restart --user hyprpaper")
hl.exec_cmd("sh -c 'qs kill; exec uwsm app -- qs'")
end
-- hl.monitor() only takes effect after the calling handler returns, so
-- anything that has to see the new layout goes on a timer.
local function later(fn)
hl.timer(fn, { timeout = 500, type = "oneshot" })
end
-- Clearing `mirror` alone brings the image back, but Hyprland never
-- re-announces the output to clients: hyprpaper and Quickshell keep seeing
-- only the panel, however often they restart. Cycling the output through
-- disabled makes it a fresh wl_output. Undeclared outputs get their auto
-- defaults back, apply_external() then overrides what lua/monitors.lua knows
-- better.
local function unmirror_all(names)
for _, name in ipairs(names) do
hl.monitor({ output = name, mirror = "", disabled = true })
end
later(function()
for _, name in ipairs(names) do
hl.monitor({ output = name, disabled = false, mode = "preferred", position = "auto" })
end
monitors.apply_external()
later(restart_clients)
end)
end
function M.toggle()
local panel = builtin()
local mirrored = mirroring(panel)
if #mirrored > 0 then
unmirror_all(mirrored)
notify("Mirroring off")
elseif panel then
mirror_all(panel)
later(restart_clients)
notify("Mirroring " .. monitors.laptop .. " to all screens")
else
notify("Laptop panel is off, open the lid first")
end
end
hl.on("monitor.added", function()
local panel = builtin()
if #mirroring(panel) > 0 then
mirror_all(panel)
end
end)
return M
+62
View File
@@ -0,0 +1,62 @@
-- https://wiki.hypr.land/Configuring/Basics/Monitors/
--
-- Declares every display once and exports the table, so lua/lid.lua can turn
-- the laptop panel on and off without duplicating any of these numbers.
--
-- Load-time behaviour is deliberately dumb: the built-in panel is always
-- enabled here. Deciding to switch it off needs to know whether anything else
-- is actually plugged in, and during a cold start the outputs are not
-- enumerated yet — so that decision lives in lua/lid.lua, which re-runs it on
-- every lid and hotplug event. Worst case the panel flickers on for a moment
-- while docked; the alternative failure is a black screen.
local M = {}
M.laptop = "eDP-1"
M.external = {
-- samsung home monitor. Other modes it has run at, if you ever need them:
-- "2560x1440@144", "1920x1080@144", "1280x720@240"
{ output = "desc:Samsung Electric Company LC27G7xT H4ZRA00734", mode = "2560x1440@240", position = "-2560x0", scale = 1 },
}
-- `disabled = false` is load-bearing, not decoration. hl.monitor() merges into
-- whatever rule the output already has, so a spec that simply omits the flag
-- leaves an earlier `disabled = true` in place: the panel stays dark and the
-- mode/position are applied to a monitor nobody can see. Opening the lid while
-- docked hit exactly that.
M.builtin = { output = M.laptop, mode = "1920x1080@60", position = "0x0", scale = 1, disabled = false }
-- Anything not declared above falls back to Hyprland's auto placement, so a
-- borrowed projector or a monitor at the office still lights up.
function M.apply_external()
for _, mon in ipairs(M.external) do
hl.monitor(mon)
end
end
function M.enable_builtin()
hl.monitor(M.builtin)
end
function M.disable_builtin()
hl.monitor({ output = M.laptop, disabled = true })
end
-- Every connected output except the laptop panel. Empty during early startup,
-- which callers have to treat as "do not know yet", never as "nothing here".
function M.externals_present()
local found = {}
for _, mon in ipairs(hl.get_monitors()) do
if mon.name ~= M.laptop then
table.insert(found, mon)
end
end
return found
end
M.enable_builtin()
M.apply_external()
return M
+52
View File
@@ -0,0 +1,52 @@
-- Mocha palette — kept in step with ~/.config/quickshell/config/Theme.qml.
--
-- Catppuccin Mocha ground, blue accent, mauve alarm. Nothing else carries hue.
--
-- Returned as a table, so every module that paints something does
-- local c = require("lua/palette")
-- and there is exactly one place the colours are written.
local c = {}
-- ---------------------------------------------------------------- THE KNOBS
-- Two colours in this setup: blue for anything live (window borders, group
-- indicators, focus), mauve for anything wrong. Change these lines and every
-- painted edge recolours with them.
--
-- Keep them in step with `accent` / `alarm` in
-- ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
-- Those are the only places the colours are written.
c.accent = "rgb(89b4fa)"
c.alarm = "rgb(cba6f7)"
c.accentDim = "rgb(5a7dc0)"
c.accentSoft = "rgb(b4befe)"
-- ------------------------------------------------------------------ neutrals
c.ink = "rgb(11111b)"
c.base = "rgb(1e1e2e)"
c.mantle = "rgb(181825)"
c.surface = "rgb(313244)"
c.surfaceAlt = "rgb(45475a)"
c.overlay = "rgb(585b70)"
c.outline = "rgb(6c7086)"
c.text = "rgb(cdd6f4)"
c.subtext = "rgb(a6adc8)"
c.muted = "rgb(7f849c)"
-- Semantic aliases, so existing references resolve without inventing hues.
c.primary = c.text
c.primaryDim = "rgb(9399b2)"
c.glow = c.accent
c.blue = c.accent
c.deep = c.surfaceAlt
c.warn = c.alarm
c.ok = c.text
-- Blue into lavender, on the diagonal. Both ends are the accent's own family,
-- so the border never reads as two colours arguing.
c.activeBorder = { colors = { c.accent, c.accentSoft }, angle = 135 }
return c
+13
View File
@@ -0,0 +1,13 @@
-- https://wiki.hypr.land/Configuring/Basics/Window-Rules/#layer-rules
--
-- Quickshell surfaces: the bar, popouts, the OSD and the launcher. Note
-- decoration.blur is off in lua/look.lua, so these only take effect if blur is
-- switched back on.
for _, namespace in ipairs({ "bottom", "top", "takemi-launcher" }) do
hl.layer_rule({
match = { namespace = namespace },
blur = true,
ignore_alpha = 0.5,
})
end
+78
View File
@@ -0,0 +1,78 @@
-- https://wiki.hypr.land/Configuring/Basics/Window-Rules/
--
-- Rules are evaluated top to bottom and the last match wins, so order matters.
-- Named rules are all evaluated before anonymous ones.
-- ==========================================
-- MATCHERS
-- ==========================================
-- Dialogs and utility apps that should float in the center
local DIALOG_TITLES = "^(Open Form|Open File|Select a File|Choose a file|Open Workspace|Choose Directory.*|Save As.*|Save File.*|branchdialog|pinentry-gtk-2|Confirm to replace files|File Operation Progress|Open Files.*|Anmelden.*|File Upload.*|TRuDI-Export laden)$"
-- pdfs-prompt is the Proton Drive search: io.narl.* is its GTK window, plain
-- pdfs-prompt the terminal `pdfs-prompt --fzf` spawns for itself. Every
-- terminal it knows how to start sets that same class, so this one rule holds
-- whichever is installed.
local DIALOG_CLASSES = "^(pavucontrol|blueman-manager|nm-connection-editor|org.pulseaudio.pavucontrol|io.narl.proton-drive-linux-prompt|pdfs-prompt)$"
local STEAM = "^(steam)$"
local GAMESCOPE = "^(gamescope)$"
-- ==========================================
-- GENERAL & FIXES
-- ==========================================
-- Ignore maximize requests from apps. You'll probably like this.
-- hl.window_rule({ match = { class = ".*" }, suppress_event = "maximize" })
-- Fix some dragging issues with XWayland
-- hl.window_rule({
-- match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false },
-- no_focus = true,
-- })
hl.window_rule({ match = { class = "^$", title = "^$" }, no_blur = true })
-- ==========================================
-- FLOATING & CENTERED DIALOGS
-- ==========================================
-- Float, size, center, no blur — one rule per matcher instead of four.
for _, match in ipairs({ { title = DIALOG_TITLES }, { class = DIALOG_CLASSES } }) do
hl.window_rule({
match = match,
float = true,
size = { 800, 600 },
center = true,
no_blur = true,
})
end
-- ==========================================
-- WORKSPACE ASSIGNMENTS
-- ==========================================
-- hl.window_rule({ match = { class = "^(firefox)$" }, workspace = "1" })
-- hl.window_rule({ match = { class = "^(kitty)$" }, workspace = "2" })
-- hl.window_rule({ match = { class = "^(Code)$" }, workspace = "3" })
hl.window_rule({ match = { class = GAMESCOPE }, workspace = "1" })
-- Special workspaces
hl.window_rule({ match = { class = "^(Spotify|spotify)$" }, workspace = "special:virtual" })
hl.window_rule({ match = { class = "^(discord|vesktop)$" }, workspace = "special:discord" })
-- ==========================================
-- STEAM & GAMING
-- ==========================================
-- hl.window_rule({ match = { class = STEAM, title = "^(Steam)$" }, workspace = "3 silent" })
-- Fixed sizes for specific Steam windows
hl.window_rule({ match = { class = STEAM, title = "^(Friends List)$" }, size = { 400, 800 } })
hl.window_rule({ match = { class = STEAM, title = "^(Steam Settings)$" }, size = { 1000, 800 } })
hl.window_rule({ match = { class = STEAM, title = "^(Add Non-Steam Game)$" }, size = { 1000, 800 } })
-- Float and disable blur for Steam windows that aren't the main window
hl.window_rule({
match = { class = STEAM, title = "negative:^(Steam)$" },
float = true,
no_blur = true,
-- center = true,
})
-- Allow tearing for games started with Gamescope
hl.window_rule({ match = { class = GAMESCOPE }, immediate = true })
-21
View File
@@ -1,21 +0,0 @@
# Autostart necessary processes (like notifications daemons, status bars, etc.)
# Or execute your favorite apps at launch like this:
# Takemi shell — bar, popouts, notifications, OSD, power menu and lock screen.
# Config lives in ~/.config/quickshell. It owns org.freedesktop.Notifications,
# so dunst must stay masked (systemctl --user mask dunst.service).
exec-once = uwsm app -- qs
# exec-once = uwsm app -- waybar
# exec-once = uwsm app -- hyprpanel
# exec-once = uwsm app -- nextcloud --background
exec-once = uwsm app -- nm-applet --indicator
# exec-once = uwsm app -- rclone mount google_drive: ~/gdrive
# exec-once = uwsm app -- protonvpn-app
exec-once = uwsm app -- hyprpaper
# exec-once = uwsm app -- /usr/bin/discord --enable-features=UseOzonePlatform --ozone-platform=wayland --start-minimized &> /dev/null
# exec-once = sleep 5 && ~/.config/hypr/scripts/replay-ctrl.sh start
# exec-once = uwsm app -- /usr/lib/xdg-desktop-portal-hyprland
exec-once = wl-paste --type text --watch cliphist store # Stores only text data
exec-once = wl-paste --type image --watch cliphist store # Stores only image data
-18
View File
@@ -1,18 +0,0 @@
# See https://wiki.hyprland.org/Configuring/Environment-variables/
# env = INTEL_DEBUG,noccs
# env = WLR_DRM_NO_ATOMIC,1
env = HYPRCURSOR_THEME,Bibata-Modern-Classic
env = HYPRCURSOR_SIZE,24
env = XCURSOR_SIZE,24
env = XCURSOR_THEME,Bibata-Modern-Classic
env = XDG_CURRENT_DESKTOP,Hyprland
# env = debug:full_cm_proto,true
env = QT_QPA_PLATFORMTHEME,qt6ct
env = LIBVA_DRIVER_NAME,radeonsi
env = VDPAU_DRIVER,radeonsi
env = AMD_VULKAN_ICD,RADV
env = MOZ_ENABLE_WAYLAND,1
env = ELECTRON_OZONE_PLATFORM_HINT,wayland
env = EDITOR,nvim
-32
View File
@@ -1,32 +0,0 @@
# https://wiki.hyprland.org/Configuring/Variables/#input
input {
# index 0 = dvorak-intl (default), index 1 = us-intl
# switch with $mainMod ALT, SPACE (see keybinds.conf)
kb_layout = us,us
kb_variant = dvorak-intl,intl
kb_model =
kb_rules =
kb_options = caps:backspace
follow_mouse = 0
accel_profile = flat
sensitivity = 0 # -1.0 - 1.0, 0 means no modification.
touchpad {
natural_scroll = false
}
}
# Layouts are inherited from the global input block so that layout
# switching applies to these too. Only bind resolution differs here.
device {
name = at-translated-set-2-keyboard
resolve_binds_by_sym = 1
}
device {
name = topre-corporation-hhkb-professional
resolve_binds_by_sym = 1
}
-103
View File
@@ -1,103 +0,0 @@
$mainMod = SUPER # Sets "Windows" key as main modifier
bindl=,switch:on:Lid Switch,exec,~/.config/hypr/scripts/lid_handler.sh close
bindl=,switch:off:Lid Switch,exec,~/.config/hypr/scripts/lid_handler.sh open
bindr = ALT, m, exec, cliphist list | fuzzel --dmenu | cliphist decode | wl-copy
bind = , PRINT, exec, grim -g "$(slurp)" - | wl-copy
bind = SHIFT, PRINT, exec, grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +'%Y-%m-%d_%H-%M-%S').png
bind = CTRL, PRINT, exec, ~/.config/hypr/scripts/screenshot_window.sh copy
bind = CTRL SHIFT, PRINT, exec, ~/.config/hypr/scripts/screenshot_window.sh save
bindr = ALT SHIFT, z, exec, ~/.config/hypr/scripts/replay-ctrl.sh save
bindr = ALT, z, exec, ~/.config/hypr/scripts/replay-ctrl.sh toggle
bindr = ALT SHIFT, c, exec, ~/.config/hypr/scripts/droidcam-ctrl.sh toggle
# general binds
bind = $mainMod, M, exec, pdfs-prompt
bind = $mainMod, B, exec, fluxo bt menu
bind = $mainMod, RETURN, exec, $terminal
bind = $mainMod SHIFT, Q, killactive,
bind = $mainMod CTRL, L, exec, qs ipc call shell lock
bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating,
bind = $mainMod, F, fullscreen,
bind = $mainMod, SPACE, exec, $menu
bind = $mainMod, P, exec, qs ipc call shell power
# Takemi shell
bind = $mainMod SHIFT, N, exec, qs ipc call shell toggleDnd
bind = $mainMod CTRL SHIFT, N, exec, qs ipc call shell clearNotifications
bind = $mainMod SHIFT, R, exec, qs ipc call shell reloadConfig
# cycle keyboard layout (dvorak-intl <-> us-intl)
bind = $mainMod ALT, SPACE, exec, hyprctl switchxkblayout all next
# bind = $mainMod, t, togglesplit, # dwindle
bind = $mainMod, n, togglegroup
# Move focus with mainMod + arrow keys
bind = $mainMod, h, movefocus, l
bind = $mainMod, l, movefocus, r
bind = $mainMod, k, movefocus, u
bind = $mainMod, j, movefocus, d
bind = $mainMod SHIFT, H, movewindoworgroup, l
bind = $mainMod SHIFT, L, movewindoworgroup, r
bind = $mainMod SHIFT, K, movewindoworgroup, u
bind = $mainMod SHIFT, J, movewindoworgroup, d
# Switch workspaces with mainMod + [0-9]
bind = $mainMod, 1, workspace, 1
bind = $mainMod, 2, workspace, 2
bind = $mainMod, 3, workspace, 3
bind = $mainMod, 4, workspace, 4
bind = $mainMod, 5, workspace, 5
bind = $mainMod, 6, workspace, 6
bind = $mainMod, 7, workspace, 7
bind = $mainMod, 8, workspace, 8
bind = $mainMod, 9, workspace, 9
bind = $mainMod, 0, workspace, 10
# Move active window to a workspace with mainMod + SHIFT + [0-9]
bind = $mainMod SHIFT, 1, movetoworkspace, 1
bind = $mainMod SHIFT, 2, movetoworkspace, 2
bind = $mainMod SHIFT, 3, movetoworkspace, 3
bind = $mainMod SHIFT, 4, movetoworkspace, 4
bind = $mainMod SHIFT, 5, movetoworkspace, 5
bind = $mainMod SHIFT, 6, movetoworkspace, 6
bind = $mainMod SHIFT, 7, movetoworkspace, 7
bind = $mainMod SHIFT, 8, movetoworkspace, 8
bind = $mainMod SHIFT, 9, movetoworkspace, 9
bind = $mainMod SHIFT, 0, movetoworkspace, 10
# groups
# bind = ALT, Tab, changegroupactive, prev
bind = ALT, Tab, changegroupactive, next
# Example special workspace (scratchpad)
bind = $mainMod, S, togglespecialworkspace, virtual
bind = $mainMod SHIFT, S, movetoworkspace, special:virtual
bind = $mainMod, D, togglespecialworkspace, discord
bind = $mainMod SHIFT, D, movetoworkspace, special:discord
# Scroll through existing workspaces with mainMod + scroll
bind = $mainMod, mouse_down, workspace, e+1
bind = $mainMod, mouse_up, workspace, e-1
# Move/resize windows with mainMod + LMB/RMB and dragging
bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:273, resizewindow
# Laptop multimedia keys for volume and LCD brightness
bindel = ,XF86AudioRaiseVolume, exec, fluxo vol up 5
bindel = ,XF86AudioLowerVolume, exec, fluxo vol down 5
bindel = ,XF86AudioMute, exec, fluxo vol mute
bindel = ,XF86AudioMicMute, exec, fluxo mic mute
bindel = ,XF86MonBrightnessUp, exec, brightnessctl s +10%
bindel = ,XF86MonBrightnessDown, exec, brightnessctl s 10%-
# Requires playerctl
bindl = , XF86AudioNext, exec, playerctl next
bindl = , XF86AudioPause, exec, playerctl play-pause
bindl = , XF86AudioPlay, exec, playerctl play-pause
bindl = , XF86AudioPrev, exec, playerctl previous
-13
View File
@@ -1,13 +0,0 @@
# laptop screen
monitor = eDP-1, 1920x1080@60, 0x0, 1
# samsung home monitor
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 1280x720@240, 0x0, 1
monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@240, -2560x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@144, 0x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 1920x1080@144, 0x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@60, 0x0, 1
# lid_state fallback
source = ~/.config/hypr/lid_state.conf
-7
View File
@@ -1,7 +0,0 @@
# See https://wiki.hyprland.org/Configuring/Keywords/
# Set programs that you use
$terminal = alacritty
$fileManager = nautilus
$menu = fuzzel
-39
View File
@@ -1,39 +0,0 @@
# Takemi palette — kept in step with ~/.config/quickshell/config/Theme.qml.
#
# Crimson, black, white. Nothing else carries hue.
# ----------------------------------------------------------------- THE KNOB
# The one colour in this setup. Change this line and the window borders,
# group indicators and focus states all recolour with it.
#
# Keep it in step with `readonly property color accent` in
# ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
# Those are the only two places the colour is written.
$accent = rgb(ff2d40)
$accentDim = rgb(8f1826)
$accentSoft = rgb(ff6b78)
# ------------------------------------------------------------------ neutrals
$ink = rgb(000000)
$base = rgb(0a0a0c)
$mantle = rgb(0d0d10)
$surface = rgb(141418)
$surfaceAlt = rgb(1c1c22)
$overlay = rgb(26262e)
$outline = rgb(3a3a44)
$text = rgb(ffffff)
$subtext = rgb(c8c8ce)
$muted = rgb(6b6b70)
# Retired hues, kept as aliases so existing references resolve. The shell has
# no teal any more — the wallpaper is the colour in the composition.
$primary = $text
$primaryDim = rgb(9a9aa2)
$glow = $accent
$blue = $primaryDim
$deep = $surfaceAlt
$warn = $accentSoft
$ok = $text
-137
View File
@@ -1,137 +0,0 @@
# Takemi — Persona 5 flavoured theme, matched to ~/.config/quickshell.
source = ~/.config/hypr/modules/takemi.conf
# Refer to https://wiki.hyprland.org/Configuring/Variables/
# https://wiki.hyprland.org/Configuring/Variables/#general
general {
gaps_in = 5
gaps_out = 6, 8, 8, 8
border_size = 2
# Crimson into its own shadow, on the diagonal. The accent is the only hue
# here — a gradient into a second colour is what put blue in the border.
col.active_border = $accent $accentDim 135deg
col.inactive_border = $outline
resize_on_border = false
# Please see https://wiki.hyprland.org/Configuring/Tearing/ before you turn this on
allow_tearing = false
layout = dwindle
}
# https://wiki.hyprland.org/Configuring/Variables/#decoration
decoration {
# Persona 5 has no rounded corners. Neither do we.
rounding = 0
# Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0
inactive_opacity = 1.0
shadow {
enabled = true
range = 18
render_power = 3
offset = 0 4
color = rgba(000000ee)
color_inactive = rgba(00000088)
}
# https://wiki.hyprland.org/Configuring/Variables/#blur
#
# Off. Blur is the signature of the glass-and-frost look, which is the
# opposite of what this theme is doing — every surface here is opaque, flat
# and hard-edged. With nothing translucent left to blur it also costs three
# render passes for no visible result.
blur {
enabled = false
}
}
# https://wiki.hyprland.org/Configuring/Animations/
# Quick and clean. Motion should get out of the way, not perform.
#
# The previous pass overdid it: a 1.56 overshoot on every window, an 88% popin
# scale, and a borderangle looping forever so the focused border was in constant
# motion. The bounce is dialled back to a hint, the popin barely scales, and the
# border angle no longer animates at all.
animations {
enabled = yes, please :)
bezier = snap, 0.16, 1.00, 0.30, 1.00
bezier = overshoot, 0.34, 1.12, 0.64, 1.00
bezier = hard, 0.55, 0.00, 0.90, 0.35
bezier = linear, 0, 0, 1, 1
animation = global, 1, 4, snap
animation = border, 1, 5, snap
animation = windows, 1, 4, snap
animation = windowsIn, 1, 4, overshoot, popin 96%
animation = windowsOut, 1, 2.6, hard, popin 97%
animation = windowsMove, 1, 3.6, snap
animation = fade, 1, 3, snap
animation = fadeIn, 1, 2.4, snap
animation = fadeOut, 1, 1.8, hard
animation = fadeSwitch, 1, 2, snap
animation = fadeShadow, 1, 3, snap
animation = fadeDim, 1, 2.4, snap
animation = layers, 1, 3, snap
animation = layersIn, 1, 3, snap, slide
animation = layersOut, 1, 2, hard, slide
animation = workspaces, 1, 3, snap, slidevert
animation = workspacesIn, 1, 3, snap, slidevert
animation = workspacesOut, 1, 2.4, hard, slidevert
animation = specialWorkspace, 1, 3, snap, slidevert
}
# See https://wiki.hyprland.org/Configuring/Master-Layout/ for more
master {
new_status = master
}
dwindle {
preserve_split = true
}
# https://wiki.hyprland.org/Configuring/Variables/#misc
misc {
force_default_wallpaper = 0
disable_hyprland_logo = true
# 0 = off, 1 = always, 2 = fullscreen only (G7 is a 240Hz VRR panel)
vrr = 2
font_family = FiraCode Nerd Font
background_color = $base
focus_on_activate = true
}
group {
col.border_active = $accent $accentDim 135deg
col.border_inactive = $outline
groupbar {
font_family = JetBrainsMono Nerd Font
font_size = 11
height = 24
gradients = false
# --- Active Tab ---
col.active = $accent
text_color = $ink
# --- Inactive Tab ---
col.inactive = $surface
text_color_inactive = $muted
indicator_height = 3
}
}
-89
View File
@@ -1,89 +0,0 @@
# See https://wiki.hyprland.org/Configuring/Window-Rules/ for more
# See https://wiki.hyprland.org/Configuring/Workspace-Rules/ for workspace rules
# ==========================================
# VARIABLES
# ==========================================
# Dialogs and utility apps that should float in the center
$dialog_titles = ^(Open Form|Open File|Select a File|Choose a file|Open Workspace|Choose Directory.*|Save As.*|Save File.*|branchdialog|pinentry-gtk-2|Confirm to replace files|File Operation Progress|Open Files.*|Anmelden.*|File Upload.*|TRuDI-Export laden)$
$dialog_classes = ^(pavucontrol|blueman-manager|nm-connection-editor|org.pulseaudio.pavucontrol|io.narl.proton-drive-linux-prompt)$
# App classes
$steam = ^(steam)$
$gamescope = ^(gamescope)$
# ==========================================
# GENERAL & FIXES
# ==========================================
# Ignore maximize requests from apps. You'll probably like this.
# windowrule = suppress_event maximize, match:class .*
# Fix some dragging issues with XWayland
# windowrule = no_focus 1, match:class ^$, match:title ^$, match:xwayland 1, match:floating 1, match:fullscreen 0, match:pinned 0
windowrule = no_blur 1, match:class ^$, match:title ^$
# ==========================================
# FLOATING & CENTERED DIALOGS
# ==========================================
# Make them float
windowrule = float 1, match:title $dialog_titles
windowrule = float 1, match:class $dialog_classes
# Set to 800x600
windowrule = size 800 600, match:title $dialog_titles
windowrule = size 800 600, match:class $dialog_classes
# Center them on the screen
windowrule = center 1, match:title $dialog_titles
windowrule = center 1, match:class $dialog_classes
# Disable blur
windowrule = no_blur 1, match:title $dialog_titles
windowrule = no_blur 1, match:class $dialog_classes
# ==========================================
# WORKSPACE ASSIGNMENTS
# ==========================================
# Normal Workspaces
# windowrule = workspace 1, match:class ^(firefox)$
# windowrule = workspace 2, match:class ^(kitty)$
# windowrule = workspace 3, match:class ^(Code)$
windowrule = workspace 1, match:class $gamescope
# Special Workspaces
windowrule = workspace special:virtual, match:class ^(Spotify|spotify)$
windowrule = workspace special:discord, match:class ^(discord|vesktop)$
# ==========================================
# APP-SPECIFIC RULES: STEAM & GAMING
# ==========================================
# windowrule = workspace 3 silent, match:class $steam, match:title ^(Steam)$
# Fixed sizes for specific Steam windows
windowrule = size 400 800, match:title ^(Friends List)$, match:class $steam
windowrule = size 1000 800, match:title ^(Steam Settings)$, match:class $steam
windowrule = size 1000 800, match:title ^(Add Non-Steam Game)$, match:class $steam
# Float and disable blur for Steam windows that aren't the main window
windowrule = float 1, match:class $steam, match:title negative:^(Steam)$
windowrule = no_blur 1, match:class $steam, match:title negative:^(Steam)$
# windowrule = center 1, match:class $steam, match:title negative:^(Steam)$
# Allow tearing for games started with Gamescope
windowrule = immediate 1, match:class $gamescope
# Layer rules
# waybar blur
layerrule = blur on, match:namespace bottom
layerrule = blur on, match:namespace top
layerrule = ignore_alpha 0.5, match:namespace bottom
layerrule = ignore_alpha 0.5, match:namespace top
# launcher blur
layerrule = blur on, match:namespace launcher
layerrule = ignore_alpha 0.5, match:namespace launcher
-54
View File
@@ -1,54 +0,0 @@
#!/bin/bash
# ~/.config/hypr/scripts/lid_handler.sh
# The file that tells Hyprland to keep the lid off during reloads
LID_STATE_FILE="$HOME/.config/hypr/lid_state.conf"
restart_ui() {
# The Quickshell bar follows monitor hotplug on its own, so only the
# wallpaper daemon needs a kick.
systemctl restart --user hyprpaper
}
if [[ "$1" == "close" ]]; then
# Check if ANY external monitor is connected and active
if hyprctl monitors all | grep -qE "Monitor (DP|HDMI|Type-C)-"; then
# Prevent laptop screen from turning on during manual config reloads
echo "monitor=eDP-1, disable" > "$LID_STATE_FILE"
# Extract the CURRENT live settings of all external monitors using jq
# This grabs the active resolution, refresh rate, position, and scale.
LIVE_MONITORS=$(hyprctl -j monitors | jq -c '.[] | select(.name != "eDP-1")')
# 3. Disable the laptop screen
hyprctl keyword monitor "eDP-1, disable"
# 4. Re-apply the live settings to external monitors so they don't reset
echo "$LIVE_MONITORS" | while read -r mon_json; do
NAME=$(echo "$mon_json" | jq -r '.name')
WIDTH=$(echo "$mon_json" | jq -r '.width')
HEIGHT=$(echo "$mon_json" | jq -r '.height')
REFRESH=$(echo "$mon_json" | jq -r '.refreshRate')
X=$(echo "$mon_json" | jq -r '.x')
Y=$(echo "$mon_json" | jq -r '.y')
SCALE=$(echo "$mon_json" | jq -r '.scale')
# Formats it exactly as Hyprland expects: DP-1, 1920x1080@240, 0x0, 1
hyprctl keyword monitor "$NAME, ${WIDTH}x${HEIGHT}@${REFRESH}, ${X}x${Y}, $SCALE"
done
# restart ui
restart_ui
fi
elif [[ "$1" == "open" ]]; then
# Clear the override file so the laptop screen is allowed to turn on again
echo "" > "$LID_STATE_FILE"
# Let Hyprland reload itself.
# This automatically re-enables eDP-1 based on your hardcoded hyprland.conf!
hyprctl reload
restart_ui
fi
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
options="Shutdown\nReboot\nLock\nLogout"
selected=$(echo -e $options | fuzzel --dmenu --prompt='Power:')
case $selected in
"Shutdown")
systemctl poweroff
;;
"Reboot")
systemctl reboot
;;
"Lock")
hyprlock
;;
"Logout")
hyprctl dispatch exit
;;
esac
+3 -1
View File
@@ -21,7 +21,8 @@ x-scheme-handler/claude-cli=claude-code-url-handler.desktop
x-scheme-handler/msteams=teams-for-linux.desktop x-scheme-handler/msteams=teams-for-linux.desktop
x-scheme-handler/slack=slack.desktop x-scheme-handler/slack=slack.desktop
audio/flac=mpv.desktop audio/flac=mpv.desktop
x-scheme-handler/codex=ChatGPT.desktop x-scheme-handler/codex=chatgpt.desktop
x-scheme-handler/proton-inbox=proton-mail.desktop
[Added Associations] [Added Associations]
application/pdf=org.pwmt.zathura-pdf-poppler.desktop; application/pdf=org.pwmt.zathura-pdf-poppler.desktop;
@@ -40,3 +41,4 @@ application/xhtml+xml=zen.desktop;
application/x-extension-xhtml=zen.desktop; application/x-extension-xhtml=zen.desktop;
application/x-extension-xht=zen.desktop; application/x-extension-xht=zen.desktop;
audio/flac=mpv.desktop; audio/flac=mpv.desktop;
text/plain=org.neovim.nvim.desktop;
+1 -1
Submodule nvim updated: 8d657d0f8d...4736228253.
-1
View File
@@ -178,7 +178,6 @@ fribidi
fuse-common fuse-common
fuse2 fuse2
fuse3 fuse3
fuzzel
fzf fzf
gawk gawk
gc gc
+542
View File
@@ -0,0 +1,542 @@
# Takemi design hierarchy redesign
## Outcome
Turn the existing Persona 5 shell into a three-level visual system:
1. **Chrome** — quiet, glanceable, and subordinate to applications.
2. **Transient** — expressive pop-outs with distinct compositions and obvious controls.
3. **Cinematic** — theatrical power and lock scenes with focused hierarchy.
Preserve the crimson/black/white palette, services, keybinds, one-slab bar/rail,
`P5Panel`, session-lock security, and current functional coverage. This plan changes
visual hierarchy and interaction clarity, not the desktop stack.
## Phase 1 execution override — 2026-08-11
For the current Phase 1 implementation, the user explicitly removed reduced-motion
work and screen-reader metadata from scope. This override supersedes Phase 1.1, the
`Accessible.*` parts of Phase 1.4, and their related definition-of-done and
verification requirements for this execution only.
- Do not create durable motion preferences or change animation policy.
- Do not add or alter `Accessible.*` metadata or accessible action handlers.
- Keep keyboard focus, visible focus treatment, keyboard navigation, root-disabled
input behavior, semantic color/treatment tokens, and the surface hierarchy in
scope.
- Defer the remaining overridden requirements to a separately approved milestone.
## Implementation progress — 2026-08-11
- **Phase 1 foundation: complete for the approved scope.** Added explicit surface
presets and semantic treatment tokens; made `P5Button` and `P5Slider`
keyboard-capable with visible focus and root-gated disabled behavior. The user
explicitly excluded durable reduced-motion work and `Accessible.*` metadata, so
those items remain deferred rather than incomplete regressions.
- **Phase 2 calm chrome: complete.** Bar and rail use the quiet `ChromeSurface`
tier without permanent texture; the clock omits seconds; CPU/MEM/GPU are visibly
labelled; MediaPill uses elided static text and a static play/pause mark; rail
clicks consistently pin detail panels and undisclosed secondary state actions
were removed; delayed Persona tooltips show state and scroll hints without
taking focus or overlapping an opening detail panel.
- **Audio vertical slice: complete.** Output percentage is dominant, output/input
are distinct bands, application streams are subordinate, and mute plus mixer
actions are explicit. Pinned popouts use `PopupWindow.grabFocus` as their sole
focus/dismiss owner, remap when promoted from hover, focus the first Audio
control, support Tab and Escape, and synchronously release coordinator ownership.
- **Verification status.** Changed QML passes individual Qt 6 `qmllint` and the
repository diff passes whitespace validation. Tooltip placement/suppression and
hover open/close were exercised in a live Quickshell session and passed. Pinned
click/remap, outside-click dismissal, Escape, and compositor-close synchronization
remain untested and require interaction QA before the milestone is considered
runtime-verified.
## Non-goals
- Do not replace Quickshell, Hyprland, Fluxo, PAM, or the service layer.
- Do not add new accent hues or generic glass/blur cards.
- Do not reproduce Persona 5 screens literally; keep this a usable desktop shell.
- Do not build a second “control center” that duplicates every existing pop-out.
- Do not rewrite tray-menu recursion or the popup coordinator unless verification
exposes a concrete defect.
## Definition of done
- Permanent chrome remains readable but no longer competes with application content.
- Each primary rail icon has one obvious click action and a concise hover label.
- Selected, active, destructive, error, and urgent states are distinguishable by
treatment as well as color.
- All small text meets WCAG AA; black-on-crimson replaces white-on-crimson where needed.
- Pinned pop-outs and modal controls are keyboard reachable and visibly focused.
- Power actions that end the session require a deliberate confirmation step.
- Lock has idle, editing, busy, failure, and visible success compositions.
- Reduced motion survives process restarts and stops all nonessential loops.
- `qmllint`, live reload, interaction checks, and 1920×1080 plus 1366×768 visual
captures pass without new warnings.
---
## Phase 0 — API and pattern lock
### Allowed APIs
- **Quickshell 0.3.0 `PopupWindow`:** `visible`, `anchor`, `grabFocus`, `screen`.
Configure placement through `anchor.item/window`, `edges`, `gravity`, `margins`,
`adjustment`; call `anchor.updateAnchor()` when a visible trigger moves.
[Official PopupWindow docs](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupWindow/)
and [PopupAnchor docs](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupAnchor/).
- **Popup bounds:** combine `PopupAdjustment.Flip | Slide | Resize` where content
can genuinely resize. Use `QsWindow.mask: Region { item: ... }` for transparent
overshoot bleed only after testing transformed entry animation.
[PopupAdjustment](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupAdjustment/),
[Region](https://quickshell.org/docs/v0.3.0/types/Quickshell/Region/).
- **Pinned dismissal:** current `HyprlandFocusGrab.active/windows/onCleared` is valid
only after a visible window exists (`components/Popout.qml:83-97`). Do not combine
it casually with `PopupWindow.grabFocus`; choose one owner per opening mode.
[HyprlandFocusGrab](https://quickshell.org/docs/v0.3.0/types/Quickshell.Hyprland/HyprlandFocusGrab/).
- **Durable preferences:** `FileView` + `JsonAdapter`, using
`Quickshell.statePath("design-preferences.json")`, `watchChanges`, `reload()`, and
`writeAdapter()`. `PersistentProperties` survives reload only, not restart.
[FileView](https://quickshell.org/docs/v0.3.0/types/Quickshell.Io/FileView/),
[JsonAdapter](https://quickshell.org/docs/v0.3.0/types/Quickshell.Io/JsonAdapter/).
- **Accessibility:** `Accessible.role/name/description/focusable/focused/pressed`,
`Accessible.onPressAction`, `onIncreaseAction`, and `onDecreaseAction`; decorative
duplicates use `Accessible.ignored: true`.
[Qt Accessible](https://doc.qt.io/qt-6/qml-qtquick-accessible.html).
- **Focus:** `FocusScope`, `focusPolicy: Qt.StrongFocus`, `activeFocusOnTab`,
`KeyNavigation`, `Keys`, and `forceActiveFocus(Qt.PopupFocusReason)`.
[Qt focus model](https://doc.qt.io/qt-6/qtquick-input-focus.html).
- **Disabled controls:** disable the root `Item`; this suppresses pointer and keyboard
input and propagates to children. Explicitly bind `HoverHandler.enabled` when a
disabled control must not react visually.
[Qt Item](https://doc.qt.io/qt-6/qml-qtquick-item.html).
- **System contrast:**
`Application.styleHints.accessibility.contrastPreference === Qt.HighContrast`.
Qt 6.11 has no documented reduced-motion preference, so motion policy is owned by
`Preferences`.
- **Secure lock:** retain `WlSessionLock`/`WlSessionLockSurface` in
`lock/Lock.qml:12-28`; `Overlay + Exclusive` is not a secure replacement.
[WlSessionLock](https://quickshell.org/docs/v0.3.0/types/Quickshell.Wayland/WlSessionLock/).
### Repository patterns to copy
- Copy singleton structure from `config/Theme.qml:1-16` and `services/Actions.qml:1-10`.
- Copy popup show/hide ownership from `components/Popout.qml:58-116,150-190`.
- Copy keyboard roving selection from `overlays/PowerMenu.qml:242-261`.
- Copy development-safe lock preview boundary from `lock/Lock.qml:7-27` and
`lockpreview.qml`; never style by repeatedly locking the real session.
- Copy one-slab chrome composition from `bar/Bar.qml:40-63` and `bar/Rail.qml:43-64`.
### Verification
- Record `qs --version` and Qt version in the implementation PR: expected Quickshell
0.3.0 and Qt 6.11.1.
- Run `qmllint` on each copied API in a minimal changed component before broad rollout.
- Use `qs ipc show` to confirm existing IPC surface before adding typed functions.
### Guards
- No deprecated `PopupWindow.parentWindow`, `relativeX`, or `relativeY`.
- No direct `QsWindow.width/height`; use implicit sizes.
- No `WlrKeyboardFocus.Exclusive` as a lock substitute.
- No stock `QsMenuAnchor` or default Qt tooltip visuals inside the custom theme.
- No claimed system reduced-motion detection; it does not exist in the documented API.
---
## Phase 1 — Establish the semantic design foundation
### 1.1 Durable preferences
Create `quickshell/config/Preferences.qml` using the Phase 0 `FileView`/`JsonAdapter`
pattern. Persist:
- `reducedMotion: false`
- `tooltips: true`
- `highIntensityChrome: false` only as a migration escape hatch; default remains calm
Expose typed setters through the existing `IpcHandler` in
`services/Actions.qml:60-68`, such as `setReducedMotion(enabled: bool): void`. Do not
build a settings UI in this phase.
### 1.2 Three explicit surface presets
- Add `components/ChromeSurface.qml`: a thin `P5Panel` preset with opaque flat fill,
one keyline, no halftone, no drop, and an active slash only when requested.
- Refine `popouts/PopoutSurface.qml:7-23` as the transient preset: halftone becomes
opt-in by composition rather than present on every panel; retain the hard offset
and stronger border.
- Keep `components/P5Backdrop.qml` cinematic-only. Bar, rail, tooltips, cards, and OSD
must not instantiate it.
Do not add a generic numeric “intensity” prop to `P5Panel`; explicit presets make
the hierarchy visible in code and prevent arbitrary intermediate tiers.
### 1.3 Semantic treatments
Document and encode in `config/Theme.qml`:
| State | Fill | Mark | Text | Motion |
|---|---|---|---|---|
| Rest | surface | neutral keyline | white | none |
| Hover | surface | short red wipe | white | one short lunge |
| Focus/selected | white | black keyline | black | snap once |
| Active | surface | persistent red slash | white | none |
| Destructive/error | solid red | black keyline | black | shake only on error |
| Urgent | surface | red pulse mark | white | bounded pulse |
| Disabled | surface | muted outline | muted | none |
Replace small white-on-crimson labels with black-on-crimson; measured contrast improves
from 3.68:1 to 5.70:1. Do not add a new status color.
### 1.4 Base-control contract
- Convert `components/P5Button.qml` to a `FocusScope` or focusable root with one
activation path shared by `TapHandler`, Enter, Space, and
`Accessible.onPressAction`. Add `description`, visible focus state, and a real
disabled state.
- Replace `P5Slider.enabledControl` with root `enabled` and update its call sites.
Gate tap, drag, hover, accessible increase/decrease, and left/right keys through
the same enabled root.
- Add semantic names and roles to `RailIcon`, `StatusIcon`, notification actions,
media transport, calendar navigation, and tray-menu rows. Mark split-text ghosts,
masks, texture layers, and duplicate glyphs ignored.
### Verification
- Focus every base control with Tab and confirm a visible non-color-only state.
- Trigger each through pointer, Enter/Space, and its accessible action; assert one
signal per activation.
- Disable each control and assert no `clicked`/`moved` signal and no hover animation.
- Recompute all foreground/background contrast pairs; normal text must be ≥4.5:1.
- Restart Quickshell and confirm preferences persist.
### Guards
- Do not treat `Accessible` metadata as keyboard support; implement both.
- Do not attach semantics to every decorative child.
- Do not use `MouseArea.enabled` as the component-wide disabled gate.
- Do not spread raw state colors through callers; state treatment belongs in base controls.
---
## Phase 2 — Quiet and clarify permanent chrome
### 2.1 Bar
Update `bar/Bar.qml` and `bar/BarPill.qml` to use `ChromeSurface` and remove all
always-on texture. Keep the single slab and hairline dividers.
- `bar/Clock.qml`: remove permanent seconds; reveal them only inside Calendar.
- `bar/Resources.qml`: render tiny `CPU`, `MEM`, and `GPU` labels beside values so
`2.6G` is not contextless; remove the currently dead `Metric.label` path only after
its visible replacement exists.
- `bar/MediaPill.qml`: keep title/artist but replace the three continuously animated
equalizer bars with one static play-state mark in normal mode. Allow the loop only
when high-intensity chrome is explicitly enabled and reduced motion is off.
- Preserve active-window elision and current collision calculation in
`bar/Bar.qml:90-96`.
### 2.2 Rail interaction grammar
Make left click mean **open/pin details** for network, Bluetooth, audio, battery,
notifications, and power. Remove undisclosed right/middle-click state changes from
`bar/Rail.qml:76-132,151-171`; move those actions into their corresponding pop-outs.
Retain scrolling only for volume, microphone, and brightness because it directly
maps to a continuous readout. Mention that gesture in the tooltip.
### 2.3 Persona tooltip
Add `components/P5Tooltip.qml` as a small anchored `PopupWindow`, not a stock Qt
tooltip. Copy anchor/delay/hide behavior from `components/Popout.qml`, but use:
- calm `ChromeSurface`
- 350–450ms delay
- one-line name + current state
- optional second micro-line such as `SCROLL TO ADJUST`
- no focus grab, no halftone, no scale loop
- `PopupAdjustment.Flip | Slide`
Add `label` and `hint` to `RailIcon`; tooltips honor `Preferences.tooltips` and never
become the only source of an accessible name.
### 2.4 Pinned keyboard opening
Add explicit `openHover()` and `openPinned()` paths to `components/Popout.qml`.
`openPinned()` must set its focus mode before mapping; if a hover popup is already
visible, close and remap rather than assuming a live `grabFocus` change works. Start
with Audio as the vertical slice. Choose either `PopupWindow.grabFocus` or the current
`HyprlandFocusGrab` for pinned dismissal after runtime comparison; never leave both
owning the same close event.
### Verification
- Screenshot clean desktop against both a quiet and high-contrast wallpaper.
- First-time test: identify every rail icon and predict left click from tooltip.
- Verify no secondary pointer action remains unless it is printed in the tooltip.
- Open Audio by hover and click: hover must remain passive; pinned mode must accept
keyboard focus, Esc-close, and outside-click close exactly once.
- Move/rescale the rail while a popup is open and call `anchor.updateAnchor()` from
the actual geometry-change path if it drifts.
### Guards
- Do not reintroduce separate floating cards into bar or rail.
- Do not increase bar/rail footprint.
- Do not put stripes, halftone, or large display text in permanent chrome.
- Do not make a tooltip interactive.
---
## Phase 3 — Give transient surfaces distinct macro-compositions
Retain shared popup lifecycle, width discipline, coordinator, header baseline, and
body controls. Redesign one family at a time; capture and approve each family before
moving on so a bad motif is not copied eight times.
### 3.1 Control pop-outs
- **Audio (`AudioPopout.qml`)** — make current volume the headline number; output and
microphone form two clear bands; applications become a restrained mixer list.
Put Mute and `Open audio mixer` inside the panel.
- **Network (`NetPopout.qml`)** — dossier composition: current connection/IP at top,
download/upload fully named, network list as clipped paper rows. Use action labels
`Turn Wi-Fi on/off` and `Network settings`.
- **Bluetooth (`BtPopout.qml`)** — share list-row mechanics with Network but not its
exact silhouette. Use action copy `Turn on/off`, `Start/stop scanning`; pair/connect
status must not rely on color alone.
- **Battery (`BatteryPopout.qml`)** — oversized percentage crossing one edge, with
`Plugged in`/`On battery`, remaining time, draw, and health ordered underneath.
Keep Suspend as the one explicit action.
### 3.2 Information pop-outs
- **Media (`MediaPopout.qml`)** — let album art define one side of the silhouette;
keep metadata in a solid legibility zone; label transport actions accessibly.
- **Calendar (`CalendarPopout.qml`)** — large selected day/month as the composition,
grid subordinate; move seconds here from the permanent bar; label previous/next.
- **Notifications (`NotifPopout.qml`, `NotifCard.qml`)** — overlapping-paper rhythm
only at the list level, not on every inner action. Rename `n HELD` to `n IN HISTORY`;
make DND copy an action (`Silence notifications` / `Resume notifications`).
- **System (`SysPopout.qml`)** — treat it as a diagnostic poster: four primary metrics
first, hardware detail second. Expand `PROCS`, `MEM`, `VRAM`, `Root`, and label load
averages `1 / 5 / 15 min`.
### 3.3 Shared transient rules
- `PopoutHeader` supplies alignment and focus traversal, not the full silhouette.
- Only one dominant numeral/image/headline per panel.
- Maximum one halftone region and one red slash per panel.
- Lists use consistent 32–40px targets and explicit selected/focused/disabled states.
- Empty and loading layouts preserve the panel silhouette instead of collapsing.
- Fix muted OSD honesty: show stored percentage beside `MUTED`, or omit percentage;
never display a fabricated 0%.
### Verification
- Capture all eight pop-outs at rest plus empty/loading/error/disabled states where applicable.
- Test with longest realistic SSID, device, track, app, and notification strings.
- Verify list focus order, Enter/Space activation, Esc close, and disabled-row skipping.
- Ensure only one transient window stays open through a rapid rail sweep.
- Test popup bounds at every screen edge and both target resolutions.
### Guards
- Do not create eight copies of the same card grid.
- Do not vary edge angle randomly inside one surface.
- Do not use display italic for body copy or dense device lists.
- Do not put action wording in state form (`Wi-Fi On`) when clicking performs the inverse.
---
## Phase 4 — Focus the cinematic scenes
### 4.1 Power menu
In `overlays/PowerMenu.qml`:
- Keep the left headline/right action-stack asymmetry.
- Increase backdrop opacity enough that application text no longer competes.
- Use one stripe band or wedge, not a full-screen stripe field plus every other motif.
- Separate Lock/Suspend/Hibernate from Log Out/Reboot/Shut Down with a labeled break
and greater spacing.
- Keep white fill/black text for normal selection. Use solid crimson/black text for
destructive selection.
- Add `pendingAction`: the first activation of Log Out/Reboot/Shut Down replaces the
action region with `CONFIRM LOG OUT`, `CONFIRM REBOOT`, or `CONFIRM SHUT DOWN`
plus Cancel; second activation executes.
Escape clears confirmation first and closes the menu only when no action is pending.
- Keep roving Up/Down/J/K navigation and update focus order for confirmation.
### 4.2 Lock screen
Refactor `lock/LockFace.qml` into explicit visual states while retaining the single
PAM context and `WlSessionLock` shell:
- **Idle:** clock is dominant; status is quiet; auth field is present but subordinate.
- **Editing:** first user edit shifts emphasis to auth and dims/moves clock/status.
- **Busy:** `CHECKING…`, disabled input, bounded sweep.
- **Failure:** `PASSWORD INCORRECT` or PAM message, solid error treatment, one shake.
- **Success:** paint a short `ACCESS GRANTED` state before unlocking; skip delay under
reduced motion.
Replace `WHO GOES THERE`, `WRONG`, and `PAM ERROR` with explicit copy. Resolve the
username safely from `USER` then `LOGNAME`; if both are empty, do not call PAM with a
literal placeholder—show `AUTHENTICATION USER UNAVAILABLE`.
Keep responsive scaling, per-monitor wallpaper, and `lockpreview.qml`. Ensure the
password input exposes `Accessible.EditableText`, name `Password`, `editable: true`,
and `passwordEdit: true`.
### Verification
- Power: pointer and keyboard through safe action, destructive confirmation, cancel,
outside click, and repeated open/close.
- Lock preview: capture all five states at 1920×1080 and 1366×768.
- Real lock test only after preview and PAM-path review; test wrong password, correct
password, empty username environment simulation, and Quickshell reload behavior.
- Confirm `WlSessionLock.secure` path remains unchanged.
### Guards
- Never execute destructive action on the first activation.
- Never fake authentication success in preview or bypass PAM.
- Never replace session lock with a focus-exclusive overlay.
- Do not let theatrical copy obscure what the user must do.
---
## Phase 5 — Motion, contrast, and runtime calm
### 5.1 Motion policy
In `Theme.qml`, derive `motionScale` from `Preferences.reducedMotion`. Apply it to:
- every `Behavior`
- popup enter/exit sequences
- media equalizer and marquee loops
- icon pulses
- toast countdown/entry
- OSD, power, and lock scene transitions
When reduced motion is on, set final state directly or disable `Behavior`; do not merely
shorten loops to 1ms. Essential progress indicators may remain static with clear text.
### 5.2 High contrast
Read `Application.styleHints.accessibility.contrastPreference`. In high contrast:
- increase keyline/border separation
- disable low-opacity texture behind text
- retain shape/icon differences among selection, active, danger, and urgency
- do not change the three-color identity
### 5.3 Polling cleanup
After visual behavior is stable, move `sysReader` from the 2-second batch to the
20-second batch in `services/Sys.qml:78-102`; uptime/load/process count do not drive
permanent animation. Keep CPU/MEM/GPU at 2 seconds because the bar renders them.
Measure before making further demand-driven polling changes; do not introduce a
consumer-count abstraction without evidence that it materially reduces work.
### Verification
- Restart with reduced motion and verify zero infinite animations via source grep and
live observation; all final states must still appear.
- Toggle high contrast and re-run contrast checks.
- Compare Fluxo process-launch frequency before/after for 60 seconds.
- Confirm notification timing, popup dismissal, and lock success still complete when
animation is disabled.
### Guards
- Do not claim to follow an OS reduced-motion setting.
- Do not gate functional timers such as notification lifetime or polling behind motion.
- Do not add blur, gradients, or extra texture to compensate for removed motion.
---
## Phase 6 — Integrated verification and cutover
### Automated checks
1. `qmllint -I quickshell` on every changed QML file individually; the current tool can
exit 255 without diagnostics on some recursive/singleton batches, so record files
and results rather than treating one giant invocation as authoritative.
2. `git diff --check`.
3. Grep for:
- white small text on solid accent
- `enabledControl`
- pointer handlers without an accessible name on shared controls
- infinite animations not gated by reduced motion
- deprecated popup properties
- old copy: `WHO GOES THERE|PAM error|HELD|PROCS|VRAM`
4. Reload through `qs ipc call shell reloadConfig` and inspect `qs log`.
### Interaction matrix
Test each bar/rail/pop-out action with pointer, keyboard where focusable, outside-click,
Esc, disabled state, and service unavailable. Specifically test tray menus with nested
submenus, Wi-Fi off, no Bluetooth adapter, no battery, no player, no notifications,
and missing album art.
### Visual matrix
Capture:
- 1920×1080 and 1366×768
- calm and high-contrast wallpaper
- default, reduced-motion, and high-contrast preferences
- desktop, every pop-out, tray menu, OSD, toast, power normal/confirm, and all lock states
Compare against these invariants:
- Chrome has no texture and never dominates content.
- Transient panels each have one dominant composition.
- Cinematic scenes retain the strongest typography and atmosphere.
- Nothing important is encoded by color alone.
- No text clips, crosses a cut edge, or falls below contrast requirements.
### Cutover sequence
Implement in six reviewable commits matching Phases 1–6. Do not delete the old visual
path until its phase passes screenshots and interaction tests. Remove migration aliases
(`enabledControl`, high-intensity escape hatch) only in the final cleanup commit after
all call sites and preferences have migrated.
## Expected file map
### New
- `quickshell/config/Preferences.qml`
- `quickshell/components/ChromeSurface.qml`
- `quickshell/components/P5Tooltip.qml`
### Foundation edits
- `quickshell/config/Theme.qml`
- `quickshell/components/P5Panel.qml`
- `quickshell/components/P5Button.qml`
- `quickshell/components/P5Slider.qml`
- `quickshell/components/Popout.qml`
- `quickshell/components/Icon.qml`
- `quickshell/components/Marquee.qml`
- `quickshell/services/Actions.qml`
### Surface edits
- `quickshell/bar/{Bar,BarPill,Rail,RailIcon,StatusIcon,Clock,Resources,MediaPill,Tray}.qml`
- `quickshell/popouts/{PopoutSurface,PopoutHeader,AudioPopout,NetPopout,BtPopout,BatteryPopout,MediaPopout,CalendarPopout,NotifPopout,SysPopout,TrayMenuList}.qml`
- `quickshell/components/NotifCard.qml`
- `quickshell/overlays/{Osd,Toasts,PowerMenu}.qml`
- `quickshell/lock/LockFace.qml`
- `quickshell/services/Sys.qml`
## Recommended execution order
Execute Phase 1 first and review its state matrix before any visual restyling. Then use
Audio as the full vertical slice through Phases 2–3. Once its tooltip, pinned focus,
semantic states, reduced motion, and transient composition pass, repeat that proven
pattern across the remaining surfaces. Leave real session-lock testing until the lock
preview and power confirmation work are complete.
+32
View File
@@ -0,0 +1,32 @@
[Appearance]
color_scheme_path=/usr/share/qt6ct/colors/catppuccin-mocha-blue.conf
custom_palette=true
icon_theme=Papirus-Dark
standard_dialogs=default
style=Fusion
[Fonts]
fixed="Sans Serif,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
general="Sans Serif,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
[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=@Invalid()
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\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l\0\0\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l\0\0\0\x1\x2\0\0\0\a\x80\0\0\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l)
[Troubleshooting]
force_raster_widgets=1
ignored_applications=@Invalid()
+153 -74
View File
@@ -1,20 +1,21 @@
# Takemi # Takemi — Persona × Mocha
A Persona 5 flavoured Quickshell desktop for Hyprland. Crimson, black and white, A Persona 5 flavoured Quickshell desktop for Hyprland, painted in Catppuccin
with no gradients anywhere. Everything leans by the same 14°, corners are Mocha. Blue for anything live, mauve for anything wrong, the Mocha greys for
chopped rather than rounded, and every surface is one continuous slab divided by everything else, and no gradients anywhere. Everything leans by the same 14°,
hairlines rather than a cluster of floating cards. corners are chopped rather than rounded, and every surface is one continuous
slab divided by hairlines rather than a cluster of floating cards.
Replaces Waybar, dunst and hyprlock. Replaces Waybar, dunst and hyprlock.
## Running ## Running
```sh ```sh
qs # starts the shell (Hyprland does this via autostart.conf) qs # starts the shell (Hyprland does this via autostart.lua)
qs -p lockpreview.qml # the lock screen in an ordinary window, for styling qs -p lockpreview.qml # the lock screen in an ordinary window, for styling
``` ```
Hyprland starts it from `~/.config/hypr/modules/autostart.conf`. Hyprland starts it from `~/.config/hypr/lua/autostart.lua`.
## Layout ## Layout
@@ -39,15 +40,23 @@ cost 4.4% of the height on top of what the bar already takes.
- **rail** — one slab: network, bluetooth, audio, mic, backlight, battery, tray, - **rail** — one slab: network, bluetooth, audio, mic, backlight, battery, tray,
notifications, power. Popouts open leftward, into the screen. notifications, power. Popouts open leftward, into the screen.
Each surface is a single continuous `P5Panel` with modules divided by sheared Each permanent surface is a single continuous `ChromeSurface` with modules
hairlines. `BarPill` is chromeless by default — a module is *content inside* a divided by sheared hairlines. `BarPill` is chromeless by default — a module is
surface, not a surface of its own — and marks hover with a crimson underline. *content inside* a surface, not a surface of its own — and marks hover with an
Pass `standalone: true` for the rare thing that really is its own panel. accent underline. Pass `standalone: true` for the rare thing that really is its
own panel.
The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is
the calm, texture-free bar, rail, and tooltip layer. **Transient**
(`PopoutSurface`) is reserved for expressive menus and detail panels, with one
clear composition and stronger separation from the desktop. **Cinematic**
(`P5Backdrop`) belongs only to scene-scale experiences such as power and lock;
it is never used as decoration inside ordinary chrome or popouts.
``` ```
shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves config/ palette, type scale, geometry, motion curves
components/ the P5 primitives: panels, gauges, sliders, split-ink text components/ the P5 primitives: panels, sliders, list rows, print-shadowed text
services/ data sources, all singletons services/ data sources, all singletons
bar/ the two surfaces and their modules bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar modules popouts/ hover panels hanging off rail and bar modules
@@ -67,29 +76,44 @@ contentPad * 2`), which keeps the shape maths out of binding loops.
| Sigil | bar | app menu | reload shell | | | | | Sigil | bar | app menu | reload shell | | | |
| Workspaces | bar | switch | | | | | | Workspaces | bar | switch | | | | |
| Clock | bar | pin calendar | | | | calendar | | Clock | bar | pin calendar | | | | calendar |
| Network | rail | pin panel | toggle Wi-Fi | nm-connection-editor | | networks | | Network | rail | pin details | | | | state + networks |
| Bluetooth | rail | pin panel | toggle adapter | | | devices | | Bluetooth | rail | pin details | | | | state + devices |
| Volume | rail | mute | pin panel | pavucontrol | volume | audio mixer | | Volume | rail | pin audio | | | volume | state + scroll hint + audio |
| Mic | rail | mute | pin panel | | mic volume | audio mixer | | Mic | rail | pin audio | | | mic volume | state + scroll hint + audio |
| Brightness | rail | | | | brightness | | | Brightness | rail | | | | brightness | state + scroll hint |
| Battery | rail | pin panel | | | | power detail | | Battery | rail | pin panel | | | | power detail |
| Tray | rail | activate | app menu | secondary | | | | Tray | rail | activate | app menu | secondary | | |
| Bell | rail | history + mark read | do not disturb | | | history | | Bell | rail | pin history + mark read | | | | state + history |
| Power | rail | power menu | | | | | | Power | rail | power menu | | | | |
| Media | bar | play/pause | next | raise player | next/prev | full player | | Media | bar | play/pause | next | raise player | next/prev | full player |
| CPU/MEM/GPU | bar | btop | | | | resource dashboard | | CPU/MEM/GPU | bar | btop | | | | resource dashboard |
Popouts open on a short dwell and stay while the pointer is over the trigger or Popouts open on a short dwell and stay while the pointer is over the trigger or
the panel. Clicking pins one open. the panel. Hover openings remain passive. Clicking remaps a detail panel as a
keyboard-focused popup: Tab moves through its controls, Escape dismisses it,
and an outside click closes it. A shell-wide coordinator keeps only one
transient surface mapped at a time, including tray context menus, so sweeping
across the rail cannot stack several animated panels over one another.
Audio is the reference vertical-slice popout: current output percentage leads,
output and input have distinct bands, and application streams form a subordinate
mixer list. Output/input mute actions and **Open audio mixer** are explicit
buttons inside the panel; scrolling the rail icons remains the fast level
adjustment.
The power menu is fully keyboard-driven as well as clickable: `Up`/`Down` or
`J`/`K` moves the active slab, `Enter`/`Space` confirms it, and `Escape`
dismisses the screen.
## Keybinds ## Keybinds
Set in `~/.config/hypr/modules/keybinds.conf`: Set in `~/.config/hypr/lua/binds/shell.lua`:
- `Super+Ctrl+L` — lock - `Super+Ctrl+L` — lock
- `Super+P` — power menu - `Super+P` — power menu
- `Super+Shift+N` — toggle do not disturb - `Super+Shift+N` — toggle do not disturb
- `Super+Ctrl+Shift+N` — clear notifications - `Super+Ctrl+Shift+N` — clear notifications
- `Super+A` — ask panel
- `Super+Shift+R` — reload the shell - `Super+Shift+R` — reload the shell
All of these go through Quickshell's IPC, so they work from any script: All of these go through Quickshell's IPC, so they work from any script:
@@ -98,10 +122,52 @@ All of these go through Quickshell's IPC, so they work from any script:
qs ipc call shell lock qs ipc call shell lock
qs ipc call shell power qs ipc call shell power
qs ipc call shell toggleDnd qs ipc call shell toggleDnd
qs ipc call shell ask
qs ipc call shell askAbout "why is the sky blue"
qs ipc call shell clearNotifications qs ipc call shell clearNotifications
qs ipc call shell reloadConfig qs ipc call shell reloadConfig
``` ```
## Ask
`Super+A` drops a panel off the top edge and hands whatever you type to
`claude -p`, streamed token by token. It is the subscription that answers —
the CLI already holds the credentials, so nothing here has an API key and
nothing here has to be given one.
The panel is a chat, not a coding session. Settings sources, MCP servers and
project files are switched off by default, which keeps a question asked from
the desktop from arriving with the last project's hooks, `CLAUDE.md` and tone
attached. The thread survives closing the panel; `Ctrl+L` starts a new one.
| Key | Does |
|---|---|
| `Enter` | ask |
| `Shift+Enter` | newline |
| `Ctrl+M` | next model, written back to `ask.json` |
| `Ctrl+Y` | copy the last answer |
| `Ctrl+L` | new conversation |
| `Ctrl+S` | stop the answer |
| `Escape` | hide, keeping the thread |
Configured in `ask.json`, watched and reloaded live:
```json
{
"model": "sonnet",
"systemPrompt": "…",
"keepContext": true,
"settingSources": "",
"allowedTools": ["WebSearch", "WebFetch"],
"disallowedTools": ["Bash", "Edit", "Write"],
"workingDirectory": "/home/narl",
"maxTurns": 6
}
```
`settingSources` is the escape hatch: set it to `"user,project,local"` and the
panel becomes your ordinary Claude Code, hooks and all.
## Data sources ## Data sources
Hardware telemetry comes from `fluxo`, whose hardware modules are configured to Hardware telemetry comes from `fluxo`, whose hardware modules are configured to
@@ -143,68 +209,81 @@ The display face is Archivo Black, set italic almost everywhere. Change
## Palette discipline ## Palette discipline
Crimson, black, white. That is the whole palette. Catppuccin Mocha, spent on two hues. **Hue is meaning**; nothing gets a colour
for decoration.
**Changing the accent.** The colour is written in exactly three places, one per - **Blue (`accent`, Mocha blue)** means live: the focused workspace, the
program: selected row, the hovered control, a channel that is on, an unread badge.
- **Mauve (`alarm`, Mocha mauve)** means wrong or destructive: critical battery,
an urgent workspace, a critical notification, reboot and shut down.
- **Grey** is everything else, and that includes *off*. A muted speaker, a
muted mic and Do Not Disturb drop to `muted` grey with their off glyph. They
used to be painted in the accent, which made a silenced channel look the most
alive thing on the rail.
- **Crust (`ink`)** is structure: the hard keyline, the offset under display
type and panels, the text on an accent slab.
| File | Line | Covers | **Selection is an inverted slab.** Focus and selection fill with the accent and
|---|---|---| print their text in crust (about 9:1). Hover stays on the surface tone with an
| `quickshell/config/Theme.qml` | `readonly property color accent: "#ff2d40"` | the shell | accent mark. Destructive-active is the same slab in mauve. The `treatment*`
| `hypr/modules/takemi.conf` | `$accent = rgb(ff2d40)` | window borders, groups | tokens in `Theme.qml` encode all of this; use them rather than picking colours
| `alacritty/takemi.toml` | `red`, cursor, selection | the terminal | per widget.
Everything else derives. `accentDim` and `accentSoft` come off `accent` via **Print, not blur.** Display text (`SplitText`) carries one hard crust offset.
`Qt.darker`/`Qt.lighter`, `glow` (the hover highlight) *is* `accent`, and every The blue and mauve ghosts appear only while `glitch()` runs. Drawn permanently
Hyprland border, group indicator and focus state references `$accent`. Change 1.6px off the body, they read as a focus problem on Mocha's pastels rather than
those lines and the whole setup recolours — swap in Persona 4's yellow or as misregistered print. The lock screen and the power menu are the exceptions;
Persona 3's blue and nothing else needs touching. they are the cinematic surfaces and keep their split.
**Texture is tiered.** Chrome (bar, rail, tooltips) and transient panels
(popouts, OSD, toasts) are flat. Stripes and halftone live only in
`P5Backdrop`, behind the lock screen and the power menu.
**Type has a floor.** Nothing renders below `Theme.fsMicro` (10px).
**Changing the palette.** The colours are written once per program:
| File | Covers |
|---|---|
| `quickshell/config/Theme.qml` | the shell: `accent`, `alarm` and the neutral ramp |
| `hypr/lua/palette.lua` | window borders, groups, shadows |
| `alacritty/catppuccin-mocha.toml` | the terminal (stock Mocha, cursor in the accent) |
Everything else derives. `accentDim` and `accentSoft` come off `accent`, `glow`
*is* `accent`, and `primary` is plain `text`. `Theme.heat()` is a continuous
text → blue → mauve ramp, so a CPU figure that has gone mauve means the same
thing as a battery that has gone mauve. Tray icons are flattened to one colour
(subtext, the accent on hover) for the same reason: app brand colours wreck the
palette faster than anything else in the shell. nm-applet and blueman are
hidden from the tray, since the rail already shows network and bluetooth, but
keep running as the secret and pairing agents.
## The rest of the desktop ## The rest of the desktop
- **Alacritty** — `alacritty/takemi.toml`, selected by the `import` in - **Hyprland** — `hypr/lua/look.lua`. No rounding, no blur, both opacities at
`alacritty.toml`. The Catppuccin files are left in place, just unreferenced. `1.0`. The active border runs blue into lavender and the inactive border is
Near-black ground, white text, crimson cursor and selection. The sixteen ANSI `surfaceAlt`. The window shadow is a sharp 4px crust offset, the same printed
slots keep their hues because syntax highlighters need to tell them apart, but drop the shell's panels carry, not a soft glow.
every one is desaturated so nothing competes with the accent. - **Alacritty** — `alacritty/catppuccin-mocha.toml`, selected by the `import`
- **Neovim** — `nvim/lua/takemi.lua`, loaded through `nvim/colors/takemi.lua` in `alacritty.toml`. Stock Mocha except for the cursor, which is the accent.
so `:colorscheme takemi` and `require("takemi").lualine()` both work. Same `takemi.toml` is left in place, unreferenced.
palette, same rule: crimson is spent only on keywords, types and errors, - **Neovim** — the `catppuccin` plugin, flavour mocha, configured in
functions are plain white-bold, and `terminal_color_0..15` mirror `nvim/lua/plugins/ui.lua`. Blue is the accent there too. `colors/takemi.lua`
`takemi.toml` slot for slot so `:terminal` matches the shell around it. is left in place, unused.
- **Fish** — `fish/conf.d/takemi_theme.fish`, with a preview copy in - **Fish** — `fish/conf.d/takemi_theme.fish` is Catppuccin Mocha's fish theme
`fish/themes/Takemi.theme` for `fish_config`. Replaced the Catppuccin with the Takemi selection rows: a solid accent block with crust punched out.
`fish_frozen_theme.fish` that fish 4.3 generated on upgrade. The prompt arrow - **The launcher** — no external theming to keep in step, because it is part of
is white and turns crimson on a non-zero exit — the shell's one "wrong" this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for
signal, and so the one place that earns the accent. `fuzzel --dmenu` where a script needs a picker.
- **Fuzzel** — `fuzzel/takemi.ini`, selected by the `include` in `fuzzel.ini`.
Square corners rather than the stock radius, and a fully opaque ground.
- **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and - **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and
Hyprland's `decoration:blur` is disabled with both opacities at `1.0`. Blur is Hyprland's `decoration:blur` is disabled. Blur is the signature of the
the signature of the frosted-glass look, which is the opposite of flat ink on frosted-glass look, which is the opposite of flat ink on flat ground — and
flat ground — and with nothing translucent left it was costing three render with nothing translucent left it was costing three render passes for no
passes for no visible result. visible result.
White is the resting voice — text, icons, idle indicators. Crimson is the only There are **no gradients in this shell**; every fill is flat. `Theme.sheen`
accent and always means one thing: active, or wrong. Black is structure, used as still exists as a no-op property because a teal-to-transparent wash on every
a hard keyline rather than as a soft shadow. **There are no gradients in this panel was once the biggest source of murk in the design.
shell**; every fill is flat. `Theme.sheen` still exists as a no-op property
because a teal-to-transparent wash on every panel was the single biggest source
of the murky blue cast the design used to have.
`primary`, `glow`, `blue`, `ok` and `warn` survive in `Theme.qml` only as
aliases onto white, crimson and grey, so the existing call sites keep working —
don't introduce new uses. `warn` used to alias `accentSoft`, which put a
washed-out pink-red wherever something was merely notable; there is no
half-crimson in this palette, so it aliases the real `accent` now and anything
merely informational (brightness, most readouts) is white instead. `Theme.heat()` is a continuous white→crimson ramp, so
a CPU figure that has gone red means the same thing as a battery that has gone
red. Tray icons are force-flattened to a single colour for the same reason: app
brand colours wreck the palette faster than anything else in the shell.
The wallpaper is teal. The UI used to be teal too, and the two fought each other
into a blue-grey mush. Going monochrome-plus-red is what lets the wallpaper be
the colour in the composition.
## Geometry discipline ## Geometry discipline
+11
View File
@@ -0,0 +1,11 @@
{
"model": "sonnet",
"fallbackModel": "",
"systemPrompt": "You are a desktop answer panel. Answer the question directly and compactly, in markdown, with no preamble. Prefer a few sentences over a list, and a list over an essay. Say plainly when you do not know.",
"keepContext": true,
"settingSources": "",
"allowedTools": ["WebSearch", "WebFetch"],
"disallowedTools": ["Bash", "Edit", "Write", "NotebookEdit", "Agent", "Task"],
"workingDirectory": "/home/narl",
"maxTurns": 6
}
+416
View File
@@ -0,0 +1,416 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/popouts"
import "root:/services"
// The ask panel. It comes down off the top edge, takes the keyboard, and hands
// a question to Claude.
//
// Deliberately not the launcher: the launcher is a list you pick from in one
// keystroke, this is a conversation you read. The shape says so — full width of
// the question, answer below it, and the thing drops from the top rather than
// rising into the middle so the two are never mistaken for each other mid-type.
PanelWindow {
id: root
required property var modelData
screen: modelData
// One screen at a time, latched at open — same reason as the launcher: the
// window takes focus, and a prompt should not hop monitors mid-question.
readonly property bool open: Actions.askOpen
&& Actions.askScreen === modelData.name
visible: root.open || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-ask"
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property int panelWidth: Math.min(860, root.width - Theme.padXL * 2)
readonly property int maxTranscript: Math.round(root.height * 0.55)
onOpenChanged: {
if (root.open) {
PopupCoordinator.dismissAll();
input.text = Actions.askSeed;
openAnim.restart();
input.forceActiveFocus();
input.cursorPosition = input.text.length;
if (Actions.askSeed !== "") root.send();
} else {
closeAnim.restart();
}
}
function send(): void {
const q = input.text.trim();
if (q === "" || Ask.busy) return;
Ask.ask(q);
input.text = "";
}
function copyLast(): void {
for (let i = Ask.turns.length - 1; i >= 0; i--) {
if (Ask.turns[i].role === "assistant" && Ask.turns[i].text !== "") {
Actions.copyText(Ask.turns[i].text);
return;
}
}
}
// -------------------------------------------------------------- backdrop
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.6)
opacity: 0
TapHandler { onTapped: Actions.closeAsk() }
}
// ----------------------------------------------------------------- panel
Item {
id: shell
width: root.panelWidth
height: header.height + divider.height + body.height + hint.height
anchors.horizontalCenter: parent.horizontalCenter
y: 0
opacity: 0
// The drop. The offset is animated, never bound: binding it to the
// panel's own height means every height change — every streamed line —
// snaps the panel back off the top of the screen.
transform: Translate { id: drop; y: 0 }
PopoutSurface {
anchors.fill: parent
tone: Ask.busy ? Theme.accent : Theme.primary
padding: 0
slashWidth: Ask.busy ? 8 : 5
// -------------------------------------------------------- header
Item {
id: header
width: parent.width
height: 62
anchors.top: parent.top
Row {
anchors.fill: parent
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
spacing: Theme.padM
// Pulses rather than spins. Native text rendering is
// subpixel-antialiased, so a rotated glyph smears into
// colour fringes; scale keeps it on the pixel grid.
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰭹"
color: Ask.busy ? Theme.accent : Theme.primary
font.pixelSize: Theme.fsTitle
pulsing: Ask.busy
pulseScale: 1.12
}
TextInput {
id: input
anchors.verticalCenter: parent.verticalCenter
width: parent.width - x - modelChip.width - Theme.padM
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsLarge
selectionColor: Theme.accent
selectedTextColor: Theme.ink
clip: true
focus: true
renderType: Text.NativeRendering
Keys.priority: Keys.BeforeItem
Keys.onPressed: event => root.handleKey(event)
Text {
anchors.verticalCenter: parent.verticalCenter
visible: input.text === ""
text: Ask.busy ? "thinking…"
: Ask.turns.length > 0 ? "follow up…" : "ask anything"
color: Theme.muted
font: input.font
renderType: Text.NativeRendering
}
}
// The model, which is also the control for it: Ctrl+M walks
// the ring and writes the choice back to ask.json, so the
// panel and the file never disagree.
Item {
id: modelChip
anchors.verticalCenter: parent.verticalCenter
width: modelText.width + Theme.padM * 2
height: 24
Skew {
anchors.fill: parent
color: Theme.surface
}
Text {
id: modelText
anchors.centerIn: parent
text: Ask.model.toUpperCase()
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Ask.cycleModel()
}
}
}
}
Skew {
id: divider
anchors.top: header.bottom
width: parent.width
height: Theme.stroke
color: Theme.alpha(Theme.outline, 0.8)
lean: 0
}
// --------------------------------------------------- transcript
Item {
id: body
anchors.top: divider.bottom
width: parent.width
height: Math.min(transcript.contentHeight + Theme.padM * 2,
root.maxTranscript)
visible: Ask.turns.length > 0 || Ask.error !== ""
clip: true
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
ListView {
id: transcript
anchors.fill: parent
anchors.margins: Theme.padM
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
model: Ask.turns
spacing: Theme.padM
boundsBehavior: Flickable.StopAtBounds
// Stay at the bottom while an answer streams in, but leave
// the view alone the moment it is scrolled by hand.
property bool pinned: true
onMovementEnded: pinned = atYEnd
onContentHeightChanged: if (pinned) positionViewAtEnd()
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: Column {
required property var modelData
width: transcript.width
spacing: Theme.padS
Text {
visible: modelData.role === "user"
text: "YOU"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
}
Text {
width: parent.width
text: modelData.text
textFormat: modelData.role === "assistant"
? Text.MarkdownText : Text.PlainText
color: modelData.role === "user" ? Theme.subtext : Theme.text
linkColor: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsBody
wrapMode: Text.Wrap
renderType: Text.NativeRendering
onLinkActivated: link => Actions.run(["xdg-open", link])
}
}
footer: Text {
width: transcript.width
visible: Ask.error !== ""
topPadding: visible ? Theme.padM : 0
text: Ask.error
color: Theme.alarm
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
}
// -------------------------------------------------------- hints
Item {
id: hint
anchors.top: body.visible ? body.bottom : divider.bottom
width: parent.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
text: Ask.busy
? "⏎ QUEUE CTRL+S STOP ESC HIDE"
: "⏎ ASK CTRL+M MODEL CTRL+Y COPY CTRL+L NEW ESC HIDE"
}
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
visible: Ask.turns.length > 0
text: Math.floor(Ask.turns.length / 2) + " TURN"
+ (Ask.turns.length > 2 ? "S" : "")
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
}
}
// ------------------------------------------------------------------ keys
function handleKey(event: var): void {
if (!root.open) return;
const ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
if (event.key === Qt.Key_Escape || (ctrl && event.key === Qt.Key_C)) {
// Hide, do not clear. The thread outlives the panel so that closing
// it to read something and reopening to ask about that is one
// conversation, not two.
Actions.closeAsk();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
// Shift+Enter is a newline; a multi-line question is still one
// question. Plain Enter sends.
if ((event.modifiers & Qt.ShiftModifier) === 0) {
root.send();
event.accepted = true;
}
return;
}
if (ctrl && event.key === Qt.Key_L) {
Ask.clear();
input.text = "";
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_S) {
Ask.stop();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_M) {
Ask.cycleModel();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_Y) {
root.copyLast();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_U) {
input.text = "";
event.accepted = true;
return;
}
// The transcript scrolls on the arrows, because the input is one line
// and has nowhere for them to go.
if (event.key === Qt.Key_PageDown || (ctrl && event.key === Qt.Key_J)) {
transcript.flick(0, -900);
event.accepted = true;
return;
}
if (event.key === Qt.Key_PageUp || (ctrl && event.key === Qt.Key_K)) {
transcript.flick(0, 900);
event.accepted = true;
return;
}
}
// ------------------------------------------------------------- animation
ParallelAnimation {
id: openAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
}
NumberAnimation {
target: shell; property: "opacity"
to: 1; duration: Theme.durFast
}
NumberAnimation {
target: drop; property: "y"
from: -shell.height; to: 0
duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: shell; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: drop; property: "y"
to: -shell.height; duration: Theme.durFast; easing.type: Easing.InQuad
}
}
}
+107 -14
View File
@@ -4,8 +4,8 @@ import Quickshell.Hyprland
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// Focused window title, glitching on every change the way a Persona 5 caption // Focused window title. Rapid application title churn is settled before it is
// snaps into place. // painted, then the stable caption lands like a misregistered print layer.
Item { Item {
id: root id: root
@@ -19,7 +19,13 @@ Item {
&& (root.monitor === null || toplevel.monitor === null && (root.monitor === null || toplevel.monitor === null
|| toplevel.monitor.id === root.monitor.id) || toplevel.monitor.id === root.monitor.id)
readonly property string title: mine && toplevel.title ? toplevel.title : "—" readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—"
property string displayedTitle: "—"
// Which toplevel the landing transition last played for. The landing is a
// focus-change stamp, so it has to key off window identity rather than off
// the caption text — see commitTitle().
property var landedToplevel: null
// The bar shrinks this when the three clusters would otherwise collide. // The bar shrinks this when the three clusters would otherwise collide.
property real maxWidth: 240 property real maxWidth: 240
@@ -31,20 +37,89 @@ Item {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo } NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo }
} }
Marquee { Item {
id: label id: titleStage
anchors.fill: parent anchors.fill: parent
text: root.title clip: true
color: root.mine ? Theme.subtext : Theme.muted
pixelSize: Theme.fsSmall Text {
family: Theme.fontDisplay id: label
italic: true anchors.verticalCenter: parent.verticalCenter
weight: Font.DemiBold width: parent.width
text: root.displayedTitle
color: root.mine ? Theme.subtext : Theme.muted
elide: Text.ElideRight
font.pixelSize: Theme.fsSmall
font.family: Theme.fontDisplay
font.italic: true
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
// Accent duplicate: visible only during the landing transition. The
// small opposing offset gives the title Persona's rough print snap
// without leaving permanent motion in the bar.
Text {
id: ghost
anchors.verticalCenter: parent.verticalCenter
width: parent.width
text: root.displayedTitle
color: Theme.accent
opacity: 0
elide: Text.ElideRight
font: label.font
renderType: Text.NativeRendering
}
} }
onTitleChanged: flash.restart() // Adopt the settled caption, and stamp it only when focus actually moved.
//
// The landing used to restart on any title change, which is not the same
// thing: plenty of windows rewrite their own title on a timer — a terminal
// running a task with a spinner or a percentage, a browser tab with a live
// clock — and each rewrite restarted seven overlapping animations totalling
// over half a second. At a title churning twice a second on a 240 Hz screen
// the bar never stopped animating, and repainting it that hard measured at
// roughly 18% of a CPU core with nothing else happening. Keying the stamp to
// window identity restores what the effect was described as doing.
function commitTitle(): void {
const focusMoved = root.toplevel !== root.landedToplevel;
// A quick crimson wipe under the title on focus change. if (root.displayedTitle !== root.incomingTitle)
root.displayedTitle = root.incomingTitle;
else if (!focusMoved)
return;
if (focusMoved) {
root.landedToplevel = root.toplevel;
titleLanding.restart();
}
}
// Browser tabs, terminals and editors can update titles several times in a
// frame. Keep the old caption steady until the stream settles instead of
// restarting an animation for every intermediate value.
Timer {
id: titleSettle
interval: 120
repeat: false
onTriggered: root.commitTitle()
}
onIncomingTitleChanged: titleSettle.restart()
// Focus can move to a window whose caption happens to match the outgoing
// one, which changes no title and so would otherwise never land.
onToplevelChanged: titleSettle.restart()
Component.onCompleted: {
displayedTitle = incomingTitle;
// Adopt the current window silently, so a config reload does not play a
// focus-change stamp for a focus that did not change.
landedToplevel = toplevel;
}
// A quick accent wipe under the title on focus change.
Rectangle { Rectangle {
id: wipe id: wipe
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -55,7 +130,7 @@ Item {
} }
SequentialAnimation { SequentialAnimation {
id: flash id: titleLanding
ParallelAnimation { ParallelAnimation {
NumberAnimation { NumberAnimation {
target: wipe; property: "width" target: wipe; property: "width"
@@ -66,6 +141,24 @@ Item {
target: wipe; property: "opacity" target: wipe; property: "opacity"
from: 0; to: 1; duration: Theme.durFast from: 0; to: 1; duration: Theme.durFast
} }
NumberAnimation {
target: label; property: "x"
from: -10; to: 0
duration: Theme.durBase; easing.type: Easing.OutBack
}
NumberAnimation {
target: label; property: "opacity"
from: 0.35; to: 1; duration: Theme.durFast
}
NumberAnimation {
target: ghost; property: "x"
from: 9; to: -3
duration: Theme.durBase; easing.type: Easing.OutExpo
}
NumberAnimation {
target: ghost; property: "opacity"
from: 0.8; to: 0; duration: Theme.durBase
}
} }
NumberAnimation { NumberAnimation {
target: wipe; property: "opacity" target: wipe; property: "opacity"
+14 -25
View File
@@ -37,29 +37,16 @@ PanelWindow {
exclusiveZone: Theme.barHeight + Theme.barMargin exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent" color: "transparent"
// The slab. One panel, full width, flat fill, black keyline. // Calm permanent chrome: one opaque slab with no texture or offset copy.
P5Panel { ChromeSurface {
id: slab id: slab
anchors.fill: parent anchors.fill: parent
lean: Theme.skew lean: Theme.skew
chop: Theme.cut chop: Theme.cut
chopCorners: [1, 3] chopCorners: [1, 3]
padding: 0 padding: 0
fill: Theme.surface active: true
fillOpacity: 1.0
border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5 slashWidth: 5
// No offset copy on the big slabs. The 3px black silhouette behind the
// fill is a print-misregistration trick, and at this scale it stops
// reading as one: on a 1900px bar you only ever see it at the chopped
// corners, where it turns the single clean cut into a staggered double
// edge that looks like a rendering fault. The black keyline still
// carries the heavy outer contour, which is what the corner needs.
drop: false
} }
readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM
@@ -128,16 +115,18 @@ PanelWindow {
implicitWidth: Theme.padM * 2 implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight implicitHeight: Theme.barHeight
Rectangle { // A leaning hairline needs a box wide enough to hold the lean: at 20px
anchors.centerIn: parent // tall the top edge sits 20 * skew ≈ 5px right of the bottom one, so a
width: 1 // 1px-wide Skew folded into a bowtie instead. Width is the lean plus the
height: 20 // stroke the rule should actually read as.
color: Theme.alpha(Theme.outline, 0.9) readonly property real ruleWeight: 1.5
readonly property real ruleHeight: 20
transform: Matrix4x4 { Skew {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew, anchors.centerIn: parent
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) width: parent.ruleHeight * Theme.skew + parent.ruleWeight
} height: parent.ruleHeight
color: Theme.alpha(Theme.outline, 0.5)
} }
} }
} }
+37 -11
View File
@@ -5,7 +5,7 @@ import "root:/components"
// Shared chrome for every bar module. // Shared chrome for every bar module.
// //
// The bar is one continuous slab, so by default a module draws no panel of its // The bar is one continuous slab, so by default a module draws no panel of its
// own — it is content inside the bar's surface, and marks hover with a crimson // own — it is content inside the bar's surface, and marks hover with an accent
// underline rather than by lighting up a card. That is what stops the bar from // underline rather than by lighting up a card. That is what stops the bar from
// reading as a row of floating islands. // reading as a row of floating islands.
// //
@@ -34,12 +34,25 @@ Item {
// //
// The rule used to be drawn into the same box as the content, so anything // The rule used to be drawn into the same box as the content, so anything
// taller than about 32px — the media pill's art, the metric meters — had a // taller than about 32px — the media pill's art, the metric meters — had a
// crimson line struck through its bottom edge on hover. Reserving the band // accent line struck through its bottom edge on hover. Reserving the band
// and centring content in what is left is what keeps the rule under the // and centring content in what is left is what keeps the rule under the
// module instead of across it. // module instead of across it.
// The band reserved the full rule plus its inset — 7px off a 40px module,
// all of it taken from the bottom — so every module's content was centred
// 3.5px above the slab's own centre and the whole bar read as top-heavy.
// The rule is pushed closer to the edge now and the band keeps only enough
// clearance to stop the tallest content (the media pill's 24px art) from
// touching it, so content sits within a pixel of the slab's centre line.
readonly property real ruleHeight: 2 readonly property real ruleHeight: 2
readonly property real ruleInset: 5 readonly property real ruleInset: 3
readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset readonly property real ruleBand: root.standalone ? 0 : 2
// Modules with a notion of progress (the player's track position) draw it in
// the same band, so it can never be struck through the content. A module
// cannot do this for itself: anything it declares becomes a child of the
// content holder, whose bottom edge is above the band, and the rule lands
// across the text instead of under it. Negative means "no progress".
property real progress: -1
// Total horizontal inset a module must add on top of its content width. // Total horizontal inset a module must add on top of its content width.
readonly property real contentPad: standalone readonly property real contentPad: standalone
@@ -89,7 +102,7 @@ Item {
anchors.bottomMargin: root.ruleBand anchors.bottomMargin: root.ruleBand
} }
// Hover mark for in-bar modules: a crimson rule under the content, wiping // Hover mark for in-bar modules: an accent rule under the content, wiping
// out from the leading edge. Sheared to match the one angle everything uses. // out from the leading edge. Sheared to match the one angle everything uses.
Item { Item {
visible: !root.standalone && root.interactive visible: !root.standalone && root.interactive
@@ -100,16 +113,29 @@ Item {
height: root.ruleHeight height: root.ruleHeight
clip: true clip: true
Rectangle { // Track progress, shown until the pointer arrives and the hover rule
// takes the band over.
//
// Deliberately untweened. MPRIS position is polled once a second, and a
// 480 ms ease on a 1 Hz input meant this rule was mid-animation roughly
// half of every second — about 115 animated frames per second on a
// 240 Hz screen, each one rebuilding this Skew's Shape geometry through
// the curve renderer and re-rendering the pill's layer textures with it.
// Stepping once per second costs 1 frame instead of 115, matches the rate
// the underlying data actually arrives at, and makes a seek land exactly
// where it was dropped instead of gliding there.
Skew {
height: parent.height
width: parent.width * Math.max(0, Math.min(1, root.progress))
color: Theme.accent
visible: root.progress >= 0 && !root.hovered && !root.active
}
Skew {
height: parent.height height: parent.height
width: root.hovered || root.active ? parent.width : 0 width: root.hovered || root.active ? parent.width : 0
color: root.accent color: root.accent
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 2 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on width { Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo } NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
} }
+41 -19
View File
@@ -17,7 +17,7 @@ BarPill {
SystemClock { SystemClock {
id: clock id: clock
precision: SystemClock.Seconds precision: SystemClock.Minutes
} }
onClicked: calendar.pinned = !calendar.pinned onClicked: calendar.pinned = !calendar.pinned
@@ -36,7 +36,8 @@ BarPill {
color: Theme.accent color: Theme.accent
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.letterSpacing: 2.5 font.weight: Font.Bold
font.letterSpacing: 3
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -50,35 +51,56 @@ BarPill {
} }
} }
Rectangle { // Declared with room for its own lean, so the slash sits inside the box
// the Row gave it instead of hanging several px into the date column.
Skew {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 2 width: 22 * Theme.skew + 3
height: 22 height: 22
color: Theme.alpha(Theme.accent, 0.6) color: Theme.alpha(Theme.accent, 0.6)
transform: Matrix4x4 { }
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) // Archivo Black's digits are not all the same width, so a clock sized to
} // its own text changed width as the minutes rolled over — 11:11 is
// several px narrower than 08:48 — and shoved the rest of the bar
// around. Reserve the widest possible reading and centre inside it.
TextMetrics {
id: timeMetrics
text: "88:88"
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsTitle
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
}
// Archivo Black reserves descender room the time never uses — "12:04"
// is all cap height — so centring the line box left the digits sitting
// above everything beside them. Centre the ink instead.
// `tightBoundingRect` is measured from the baseline, so the ink's top
// inside the line box is `ascent + ink.y`.
readonly property real timeInkOffset: {
const ink = timeMetrics.tightBoundingRect;
if (ink.height <= 0) return 0;
const inkCentre = timeFont.ascent + ink.y + ink.height / 2;
return Math.round(timeMetrics.boundingRect.height / 2 - inkCentre);
}
FontMetrics {
id: timeFont
font: timeMetrics.font
} }
SplitText { SplitText {
id: timeText id: timeText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: layout.timeInkOffset
width: timeMetrics.advanceWidth
text: Qt.formatDateTime(clock.date, "HH:mm") text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: Theme.fsTitle pixelSize: Theme.fsTitle
split: 1.4 horizontalAlignment: Text.AlignHCenter
} }
// Seconds, kept quiet so they read as a ticking detail rather than noise.
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 4
text: Qt.formatDateTime(clock.date, "ss")
color: Theme.alpha(Theme.primary, 0.8)
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
} }
// Glitch the time on the hour, because it should feel like something happened. // Glitch the time on the hour, because it should feel like something happened.
+89 -45
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import QtQuick.Window
import Quickshell import Quickshell
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
@@ -16,6 +17,9 @@ BarPill {
padding: Theme.padS padding: Theme.padS
visible: Media.hasPlayer || width > 1 visible: Media.hasPlayer || width > 1
// Track position, drawn by the pill in its own rule band.
progress: Media.hasTimeline ? Media.progress : -1
implicitWidth: Media.hasPlayer implicitWidth: Media.hasPlayer
? layout.implicitWidth + contentPad * 2 ? layout.implicitWidth + contentPad * 2
: 0 : 0
@@ -49,6 +53,7 @@ BarPill {
anchors.fill: parent anchors.fill: parent
visible: false visible: false
layer.enabled: true layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
fillColor: "white" fillColor: "white"
@@ -68,6 +73,7 @@ BarPill {
asynchronous: true asynchronous: true
visible: status === Image.Ready visible: status === Image.Ready
layer.enabled: true layer.enabled: true
layer.smooth: true
layer.effect: MaskedArt { maskItem: artMask } layer.effect: MaskedArt { maskItem: artMask }
} }
@@ -78,66 +84,120 @@ BarPill {
text: Media.sourceIcon text: Media.sourceIcon
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsLarge
color: Theme.primary color: Theme.primary
pulsing: Media.playing
pulseScale: 1.12
} }
} }
// Title and artist size to the text, up to a cap.
//
// These used to be a flat 130px wide each, which meant a short title
// still reserved 130px and left a hand-sized hole between the artist and
// the equaliser — the pill looked broken rather than compact. They take
// what they need now and only start scrolling past the cap.
Column { Column {
id: meta
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 0 spacing: 0
Marquee { readonly property real cap: 150
width: 130
Text {
width: Math.min(meta.cap, implicitWidth)
height: 14 height: 14
text: Media.title || Media.identity text: Media.title || Media.identity
color: Theme.text color: Theme.text
pixelSize: Theme.fsSmall elide: Text.ElideRight
family: Theme.fontDisplay font.pixelSize: Theme.fsSmall
weight: Font.DemiBold font.family: Theme.fontDisplay
italic: true font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
} }
Marquee { Text {
width: 130 width: Math.min(meta.cap, implicitWidth)
height: 12 height: 12
text: Media.artist || Media.album || Media.identity text: Media.artist || Media.album || Media.identity
color: Theme.muted color: Theme.muted
pixelSize: Theme.fsMicro elide: Text.ElideRight
family: Theme.fontMono font.pixelSize: Theme.fsMicro
font.family: Theme.fontMono
renderType: Text.NativeRendering
} }
} }
// Play state, as a two-bar equaliser that freezes when paused. // Compact play-state equaliser. It moves only while audio is playing
// and freezes into three quiet bars when paused.
//
// Sampled from a timer rather than tweened by an infinite
// NumberAnimation. DP-1 runs at 240 Hz, and a frame-synced animation
// here repainted the entire bar 240 times a second: this pill sits
// inside ancestors that use layer.enabled together with
// Shape.CurveRenderer, and the album art carries a MultiEffect pass, so
// every one of those frames re-rendered several layer textures. Measured
// on its own, this one animation cost about two thirds of a CPU core
// whenever anything was playing.
//
// The timer samples a sine, it does not step through arbitrary levels: a
// low tick rate only looks like the original if consecutive ticks stay
// near each other. Each bar keeps the eased breathing motion and the
// period it had before — the curve is just read 25 times a second
// instead of 240.
Row { Row {
id: eq
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 2 spacing: 2
// Sampling interval — the one knob trading smoothness against cost.
// Frames are the whole expense: a full-bar repaint runs about 0.3% of
// a core (the bar spans 2544x40 on a 240 Hz output), so cost scales
// linearly with this rate. 16 ms holds 60 Hz, which is visually
// indistinguishable from the original frame-synced tween at a quarter
// of its ~75%.
readonly property int tickMs: 16
property int phase: 0
// Reproduces the original per-bar tween exactly, sampled instead of
// frame-synced. Each bar eased between two heights with its own pair
// of durations, so the three differed in both swing and period — the
// middle bar moved barely a pixel while the outer two ran in
// opposition. Giving all three the full swing, as a single symmetric
// sine would, is three times the motion and reads far busier than
// this meter is supposed to.
function level(i: int): real {
const lo = 4 + (i * 5) % 13; // old first NumberAnimation `to`
const hi = 14 - (i * 4) % 9; // old second NumberAnimation `to`
const d1 = 320 + i * 90; // ...and their durations
const d2 = 280 + i * 70;
const t = (eq.phase * eq.tickMs) % (d1 + d2);
// Easing.InOutSine, which is what both halves used.
const ease = x => (1 - Math.cos(Math.PI * x)) / 2;
return t < d1
? hi + (lo - hi) * ease(t / d1)
: lo + (hi - lo) * ease((t - d1) / d2);
}
Timer {
interval: eq.tickMs
running: Media.playing && eq.visible && (eq.Window.window?.visible ?? true)
repeat: true
onTriggered: eq.phase++
// Restart the cycle from its trough, so playback always begins
// from a settled meter rather than mid-swing.
onRunningChanged: if (!running) eq.phase = 0
}
Repeater { Repeater {
model: 3 model: 3
Rectangle { Rectangle {
required property int index required property int index
width: 3 width: 3
height: 6 height: Media.playing ? eq.level(index) : 6
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: Media.playing ? Theme.primary : Theme.muted color: Media.playing ? Theme.primary : Theme.muted
SequentialAnimation on height {
running: Media.playing
loops: Animation.Infinite
NumberAnimation {
to: 4 + (index * 5) % 13
duration: 320 + index * 90
easing.type: Easing.InOutSine
}
NumberAnimation {
to: 14 - (index * 4) % 9
duration: 280 + index * 70
easing.type: Easing.InOutSine
}
}
Behavior on color { Behavior on color {
ColorAnimation { duration: Theme.durBase } ColorAnimation { duration: Theme.durBase }
} }
@@ -146,22 +206,6 @@ BarPill {
} }
} }
// Track progress, hairline along the bottom of the pill.
Rectangle {
anchors.bottom: parent.bottom
anchors.bottomMargin: 3
anchors.left: parent.left
anchors.leftMargin: root.leanInset
width: Math.max(0, (root.width - root.leanInset * 2) * Media.progress)
height: 2
color: Theme.accent
visible: Media.canSeek
Behavior on width {
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
}
}
onVisibleChanged: if (visible) titleGlitch.restart() onVisibleChanged: if (visible) titleGlitch.restart()
Connections { Connections {
-75
View File
@@ -1,75 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Notification bell with an unread badge. Click opens the history, right click
// toggles do-not-disturb.
BarPill {
id: root
required property var screenRef
accent: Notifs.dnd ? Theme.muted : Theme.primary
padding: Theme.padS
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: {
popout.pinned = !popout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
Icon {
id: bell
anchors.centerIn: parent
text: Notifs.icon
font.pixelSize: Theme.fsLarge
color: {
if (Notifs.dnd) return Theme.muted;
if (Notifs.hasUnread) return Theme.accent;
return root.hovered ? Theme.glow : Theme.primary;
}
}
// Unread badge, chopped like everything else.
Rectangle {
visible: Notifs.unread > 0 && !Notifs.dnd
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: parent.top
anchors.topMargin: 4
width: Math.max(12, badge.implicitWidth + 5)
height: 12
color: Theme.accent
Text {
id: badge
anchors.centerIn: parent
text: Notifs.unread > 9 ? "9+" : Notifs.unread
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: 8
font.weight: Font.Bold
renderType: Text.NativeRendering
}
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 12 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Connections {
target: Notifs
function onPopupRequested() { bell.bump(); }
}
NotifPopout {
id: popout
anchorItem: root
triggerHovered: root.hovered
}
}
-42
View File
@@ -1,42 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Far right. Opens the full-screen power menu.
BarPill {
id: root
required property var screenRef
accent: Theme.accent
slash: true
padding: Theme.padS
active: Actions.powerMenuOpen
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: Actions.togglePowerMenu()
Icon {
id: glyph
anchors.centerIn: parent
text: "󰐥"
font.pixelSize: Theme.fsLarge
color: root.hovered || Actions.powerMenuOpen ? Theme.accent : Theme.subtext
}
// A crimson halo that swells under the pointer.
Rectangle {
anchors.centerIn: glyph
z: -1
width: root.hovered ? 26 : 0
height: width
radius: width / 2
color: Theme.alpha(Theme.accent, 0.18)
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
}
+55 -41
View File
@@ -40,8 +40,8 @@ PanelWindow {
exclusiveZone: Theme.railWidth + Theme.barMargin exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent" color: "transparent"
// The slab. // The same calm permanent-chrome preset used by the top bar.
P5Panel { ChromeSurface {
id: slab id: slab
anchors.fill: parent anchors.fill: parent
vertical: true vertical: true
@@ -49,18 +49,8 @@ PanelWindow {
chop: Theme.cut chop: Theme.cut
chopCorners: [1, 3] chopCorners: [1, 3]
padding: 0 padding: 0
fill: Theme.surface active: true
fillOpacity: 1.0
border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5 slashWidth: 5
// Same as the bar — see the note there. The doubled corner is worse on
// the rail, because its chopped corners sit against the wallpaper's
// high-contrast stripes rather than against other chrome.
drop: false
} }
readonly property real edgeInset: Theme.railWidth * Theme.skew + Theme.padM readonly property real edgeInset: Theme.railWidth * Theme.skew + Theme.padM
@@ -76,18 +66,21 @@ PanelWindow {
RailIcon { RailIcon {
id: netIcon id: netIcon
icon: Net.icon icon: Net.icon
label: "Network"
statusText: Net.label
suppressTooltip: netPopout.detailActive
dim: !Sys.netUp dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned onClicked: netPopout.togglePinned()
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
} }
RailIcon { RailIcon {
id: btIcon id: btIcon
icon: Bt.icon icon: Bt.icon
label: "Bluetooth"
statusText: Bt.label
suppressTooltip: btPopout.detailActive
dim: !Bt.enabled dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned onClicked: btPopout.togglePinned()
onRightClicked: Bt.toggle()
} }
Divider {} Divider {}
@@ -96,19 +89,25 @@ PanelWindow {
id: volIcon id: volIcon
icon: Audio.icon icon: Audio.icon
readout: Audio.muted ? "" : Audio.volumePercent + "%" readout: Audio.muted ? "" : Audio.volumePercent + "%"
alert: Audio.muted reserveReadout: true
onClicked: Audio.toggleMute() label: "Volume"
onRightClicked: audioPopout.pinned = !audioPopout.pinned statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName
onMiddleClicked: Actions.openAudioSettings() hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
dim: Audio.muted
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05) onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
} }
RailIcon { RailIcon {
id: micIcon id: micIcon
icon: Audio.micIcon icon: Audio.micIcon
alert: Audio.micMuted label: "Microphone"
onClicked: Audio.toggleMicMute() statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName
onRightClicked: audioPopout.pinned = !audioPopout.pinned hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
dim: Audio.micMuted
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05)) onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
} }
@@ -117,6 +116,9 @@ PanelWindow {
visible: Brightness.available visible: Brightness.available
icon: Brightness.icon icon: Brightness.icon
readout: Math.round(Brightness.percent) + "%" readout: Math.round(Brightness.percent) + "%"
label: "Brightness"
statusText: Math.round(Brightness.percent) + "%"
hint: "Scroll to adjust"
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5) onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
} }
@@ -127,9 +129,12 @@ PanelWindow {
visible: Battery.available visible: Battery.available
icon: Battery.icon icon: Battery.icon
readout: Math.round(Battery.percent) + "%" readout: Math.round(Battery.percent) + "%"
alert: Battery.critical label: "Battery"
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
suppressTooltip: battPopout.detailActive
alarm: Battery.critical
pulsing: Battery.critical pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned onClicked: battPopout.togglePinned()
} }
} }
@@ -151,22 +156,29 @@ PanelWindow {
RailIcon { RailIcon {
id: bellIcon id: bellIcon
icon: Notifs.icon icon: Notifs.icon
label: "Notifications"
statusText: Notifs.dnd
? "Do not disturb"
: (Notifs.unread > 0 ? Notifs.unread + " unread" : "All caught up")
readout: Notifs.unread > 0 readout: Notifs.unread > 0
? (Notifs.unread > 9 ? "9+" : String(Notifs.unread)) ? (Notifs.unread > 9 ? "9+" : String(Notifs.unread))
: "" : ""
reserveReadout: true
dim: Notifs.dnd dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd live: Notifs.hasUnread && !Notifs.dnd
suppressTooltip: notifPopout.detailActive
onClicked: { onClicked: {
notifPopout.pinned = !notifPopout.pinned; notifPopout.togglePinned();
Notifs.markRead(); Notifs.markRead();
} }
onRightClicked: Notifs.toggleDnd()
} }
RailIcon { RailIcon {
id: powerIcon id: powerIcon
icon: "󰐥" icon: "󰐥"
alert: Actions.powerMenuOpen label: "Power"
statusText: "Session actions"
live: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu() onClicked: Actions.togglePowerMenu()
} }
} }
@@ -177,17 +189,19 @@ PanelWindow {
implicitWidth: Theme.railWidth implicitWidth: Theme.railWidth
implicitHeight: Theme.padM + 4 implicitHeight: Theme.padM + 4
Rectangle { // Same correction as the bar's divider, on the other axis: a 22px rule
anchors.centerIn: parent // rises 22 * skew ≈ 5px across its run, so the box has to be that tall
width: 22 // plus the weight of the stroke. At height 1 the polygon inverted and
height: 1 // the rule rendered as a stubby wedge sitting left of centre.
color: Theme.alpha(Theme.outline, 0.9) readonly property real ruleWeight: 1.5
readonly property real ruleWidth: 22
transform: Matrix4x4 { Skew {
matrix: Qt.matrix4x4(1, 0, 0, 0, anchors.centerIn: parent
-Theme.skew, 1, 0, 22 * Theme.skew / 2, width: parent.ruleWidth
0, 0, 1, 0, 0, 0, 0, 1) height: parent.ruleWidth * Theme.skew + parent.ruleWeight
} vertical: true
color: Theme.alpha(Theme.outline, 0.5)
} }
} }
+81 -28
View File
@@ -3,20 +3,39 @@ import "root:/config"
import "root:/components" import "root:/components"
// One slot on the right-hand rail: a glyph with an optional micro readout // One slot on the right-hand rail: a glyph with an optional micro readout
// stacked beneath it, and a crimson bar that wipes down the leading edge on // stacked beneath it, and an accent bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits // hover. The rail is only 48px wide, so the readout sits under the glyph
// under the glyph rather than beside it, and anything longer than a percentage // rather than beside it, and anything longer than a percentage belongs in the
// belongs in the popout instead. // popout instead.
Item { Item {
id: root id: root
property string icon: "" property string icon: ""
property string readout: "" property string readout: ""
property string label: ""
property string statusText: ""
property string hint: ""
property bool suppressTooltip: false
property color tint: Theme.primary property color tint: Theme.primary
property bool alert: false
// Three states, three meanings. `live` wants attention but nothing is wrong
// (unread notifications, an open power menu): accent. `alarm` is wrong or
// critical (a dying battery): alarm. `dim` is off by choice (muted, radio
// off, do-not-disturb): grey. These used to be one `alert` flag painted in
// the accent, which made a muted speaker look like the most active thing
// on the rail.
property bool live: false
property bool alarm: false
property bool dim: false property bool dim: false
property bool pulsing: false property bool pulsing: false
// Hold the readout line open even while there is nothing to put in it.
// Volume drops its percentage when muted and the bell only carries a count
// when something is unread, so without this the slot collapsed from 42px to
// 32px and every icon below it jumped a third of its own height.
property bool reserveReadout: false
readonly property bool hasReadout: root.readout !== "" || root.reserveReadout
readonly property bool hovered: hover.hovered readonly property bool hovered: hover.hovered
// Clearance between the wipe and the rail's left edge, chosen so the bar // Clearance between the wipe and the rail's left edge, chosen so the bar
@@ -24,26 +43,51 @@ Item {
readonly property real wipeInset: 5 readonly property real wipeInset: 5
signal clicked() signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta) signal scrolled(real delta)
implicitWidth: Theme.railWidth implicitWidth: Theme.railWidth
implicitHeight: root.readout !== "" ? 42 : 32 implicitHeight: root.hasReadout ? root.glyphBand + readoutLine.height + 1 : root.glyphBand
Column { // The glyph always sits centred in a fixed band at the top of the slot and
// the readout hangs below it, rather than the pair being centred together.
// Centring the pair meant the icon slid up and down by a few px whenever its
// number appeared or vanished — the icons stopped lining up with each other
// exactly when something was changing and you were looking at them.
readonly property real glyphBand: 32
// Nerd Font's status glyphs are not all centred inside their own advance —
// the muted-speaker and muted-mic marks carry their cross out to the right,
// so centring the advance box left them visibly off-axis next to the wifi
// and bluetooth glyphs. Centre the ink instead, capped so a glyph with an
// odd bounding box cannot slide far off the rail's axis.
readonly property real opticalShift: {
const ink = glyphMetrics.tightBoundingRect;
if (ink.width <= 0) return 0;
const off = glyphMetrics.advanceWidth / 2 - (ink.x + ink.width / 2);
return Math.max(-4, Math.min(4, off));
}
TextMetrics {
id: glyphMetrics
text: root.icon
font.family: Theme.fontIcon
font.pixelSize: Theme.fsRailIcon
}
Item {
id: layout id: layout
anchors.centerIn: parent anchors.fill: parent
spacing: 1
Icon { Icon {
id: glyph id: glyph
anchors.horizontalCenter: parent.horizontalCenter x: (root.width - width) / 2 + root.opticalShift
y: (root.glyphBand - height) / 2
text: root.icon text: root.icon
pulsing: root.pulsing pulsing: root.pulsing
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsRailIcon
color: { color: {
if (root.alert) return Theme.accent; if (root.alarm) return Theme.alarm;
if (root.live) return Theme.accent;
if (root.dim) return Theme.muted; if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow; if (root.hovered) return Theme.glow;
return root.tint; return root.tint;
@@ -55,10 +99,17 @@ Item {
} }
Text { Text {
id: readoutLine
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: root.glyphBand + 1
visible: root.readout !== "" visible: root.readout !== ""
text: root.readout text: root.readout
color: root.dim ? Theme.muted : Theme.subtext color: {
if (root.alarm) return Theme.alarm;
if (root.dim) return Theme.muted;
return Theme.subtext;
}
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold font.weight: Font.DemiBold
@@ -78,15 +129,15 @@ Item {
// scale then pushed it out past the silhouette onto the desktop. The inset // scale then pushed it out past the silhouette onto the desktop. The inset
// has to survive that scale, which is why it is not simply 1 or 2. // has to survive that scale, which is why it is not simply 1 or 2.
// //
// It is crimson rather than `tint` because it only ever appears on hover, // It is the accent rather than `tint` because it only ever appears on
// and hover is crimson everywhere else in the shell (Theme.glow). Painting // hover, and hover is the accent everywhere else in the shell (Theme.glow).
// it white put a white bar on the rail's edge for no stated reason. // Painting it white put a white bar on the rail's edge for no stated reason.
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: root.wipeInset anchors.leftMargin: root.wipeInset
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 2 width: 2
height: root.hovered ? layout.implicitHeight : 0 height: root.hovered ? root.height - 4 : 0
color: Theme.glow color: Theme.glow
Behavior on height { Behavior on height {
@@ -99,7 +150,8 @@ Item {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack } NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
} }
onAlertChanged: if (alert) glyph.bump() onAlarmChanged: if (alarm) glyph.bump()
onLiveChanged: if (live) glyph.bump()
HoverHandler { HoverHandler {
id: hover id: hover
@@ -110,16 +162,17 @@ Item {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
onTapped: root.clicked() onTapped: root.clicked()
} }
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler { WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y) onWheel: event => root.scrolled(event.angleDelta.y)
} }
P5Tooltip {
anchorItem: root
triggerHovered: root.hovered
title: root.label
status: root.statusText
hint: root.hint
suppressed: root.suppressTooltip
}
} }
+68 -23
View File
@@ -29,6 +29,7 @@ BarPill {
label: "CPU" label: "CPU"
value: Sys.cpuUsage value: Sys.cpuUsage
readout: Math.round(Sys.cpuUsage) + "%" readout: Math.round(Sys.cpuUsage) + "%"
widest: "100%"
} }
Divider {} Divider {}
@@ -37,6 +38,7 @@ BarPill {
label: "MEM" label: "MEM"
value: Sys.memPercent value: Sys.memPercent
readout: Sys.memUsed.toFixed(1) + "G" readout: Sys.memUsed.toFixed(1) + "G"
widest: "88.8G"
} }
Divider { visible: Sys.gpuAvailable } Divider { visible: Sys.gpuAvailable }
@@ -46,24 +48,27 @@ BarPill {
label: "GPU" label: "GPU"
value: Sys.gpuUsage value: Sys.gpuUsage
readout: Math.round(Sys.gpuUsage) + "%" readout: Math.round(Sys.gpuUsage) + "%"
widest: "100%"
} }
} }
component Divider: Rectangle { component Divider: Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 1 implicitWidth: rule.width
height: 18 implicitHeight: 18
color: Theme.alpha(Theme.outline, 0.8)
transform: Matrix4x4 { // Sized to hold its own lean — see the note on the bar's divider.
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 18 * Theme.skew, Skew {
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) id: rule
anchors.centerIn: parent
width: parent.height * Theme.skew + 1
height: parent.height
color: Theme.alpha(Theme.outline, 0.5)
} }
} }
// The CPU/MEM/GPU labels are gone: three characters of chrome per metric, // A tiny fixed label makes every number self-describing without turning the
// repeated three times, in the most cramped part of the shell. The number // compact meter cluster back into three separate cards.
// over its meter is legible on its own, and dropping them buys the room to
// set the figure in display italic instead of squeezing it into mono.
component Metric: Column { component Metric: Column {
id: metric id: metric
@@ -71,30 +76,70 @@ BarPill {
property real value: 0 property real value: 0
property string readout: "" property string readout: ""
// The widest reading this metric will ever show. The number column is
// sized from this rather than from the live text, because sizing to the
// live text is what made the whole right-hand group breathe in and out:
// "9%" and "100%" are ~14px apart in Archivo Black, so every tick of the
// CPU sampler nudged the metrics, the media pill and the clock sideways.
// Reserving the maximum costs a few px of blank and buys a bar that
// never moves.
property string widest: "100%"
readonly property real numberWidth: Math.max(gauge.width, metrics.advanceWidth)
readonly property real headWidth: labelText.implicitWidth + head.spacing + numberWidth
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 2 spacing: 2
width: Math.max(headWidth, 44)
Text { TextMetrics {
text: metric.readout id: metrics
color: Theme.heat(metric.value) text: metric.widest
font.family: Theme.fontDisplay font: gauge.font
font.pixelSize: Theme.fsBody }
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
renderType: Text.NativeRendering
Behavior on color { Row {
ColorAnimation { duration: Theme.durBase } id: head
spacing: 4
Text {
id: labelText
anchors.baseline: gauge.baseline
text: metric.label
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 0.5
renderType: Text.NativeRendering
}
Text {
id: gauge
text: metric.readout
color: Theme.heat(metric.value)
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
} }
} }
// Ends flush with the reading above it instead of stopping short at a
// fixed 43px, so each metric reads as one block.
MeterBar { MeterBar {
value: metric.value value: metric.value
segments: 9 segments: 9
segmentWidth: 3
spacing: 2 spacing: 2
height: 5 height: 5
width: metric.width
segmentWidth: (width - 8 * spacing) / 9
} }
} }
+2 -2
View File
@@ -17,12 +17,12 @@ BarPill {
lean: Theme.skew lean: Theme.skew
implicitWidth: Theme.barHeight + contentPad implicitWidth: Theme.barHeight + contentPad
onClicked: Actions.launch("fuzzel") onClicked: Actions.toggleLauncher()
onRightClicked: Actions.reloadShell() onRightClicked: Actions.reloadShell()
// The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a // The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a
// pictogram in a shell that otherwise says everything in heavy italic // pictogram in a shell that otherwise says everything in heavy italic
// type. SplitText puts it in the display face with the crimson-and-black // type. SplitText puts it in the display face with the ink print
// offset every other heading carries, so the house mark is built out of // offset every other heading carries, so the house mark is built out of
// the same parts as the rest of the bar. // the same parts as the rest of the bar.
SplitText { SplitText {
-117
View File
@@ -1,117 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Network, bluetooth, audio, mic, backlight and battery, in one slab. Each
// glyph carries its own popout.
BarPill {
id: root
required property var screenRef
accent: Theme.primary
padding: Theme.padS
interactive: false
implicitWidth: layout.implicitWidth + contentPad * 2
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
StatusIcon {
id: netIcon
icon: Net.icon
label: Sys.netUp ? Sys.rate(Sys.netRx + Sys.netTx) : ""
labelWidth: 62
tint: Theme.primary
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
StatusIcon {
id: btIcon
icon: Bt.icon
tint: Bt.connected.length > 0 ? Theme.blue : Theme.primary
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
StatusIcon {
id: volIcon
icon: Audio.icon
label: Audio.muted ? "" : Audio.volumePercent + "%"
labelWidth: Audio.muted ? 0 : 34
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
StatusIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
StatusIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
label: Math.round(Brightness.percent) + "%"
labelWidth: 34
// Brightness is never an alarm — it stays white and lets the
// battery be the only thing in the cluster that can go crimson.
tint: Theme.text
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
StatusIcon {
id: battIcon
visible: Battery.available
icon: Battery.icon
label: Math.round(Battery.percent) + "%"
labelWidth: 34
tint: Battery.tint
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
visible: Battery.available && shown
}
}
-108
View File
@@ -1,108 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// One indicator inside the status cluster: a glyph, an optional readout, and a
// crimson underline that wipes in on hover.
Item {
id: root
property string icon: ""
property string label: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
// Fixed label width. Readouts that change width constantly (throughput,
// percentages) would otherwise shove the whole cluster around.
property real labelWidth: 0
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: layout.implicitWidth
implicitHeight: Theme.barHeight - Theme.padS * 2
Row {
id: layout
anchors.centerIn: parent
spacing: 5
Icon {
id: glyph
anchors.verticalCenter: parent.verticalCenter
text: root.icon
pulsing: root.pulsing
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.label !== ""
text: root.label
width: root.labelWidth > 0 ? root.labelWidth : implicitWidth
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: 2
width: root.hovered ? layout.implicitWidth : 0
color: root.alert ? Theme.accent : root.tint
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.06 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+44 -26
View File
@@ -1,10 +1,10 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
import "root:/popouts"
// StatusNotifier tray, as a vertical column for the right-hand rail. Left click // StatusNotifier tray, as a vertical column for the right-hand rail. Left click
// activates, right click opens the app's own menu, middle click is the // activates, right click opens the app's own menu, middle click is the
@@ -14,10 +14,36 @@ Column {
id: root id: root
spacing: Theme.padS + 2 spacing: Theme.padS + 2
visible: SystemTray.items.values.length > 0 visible: root.shownItems.length > 0
// Applets whose whole job the rail already does. blueman keeps running as
// the pairing agent, but its icon was a second bluetooth glyph a few slots
// below the rail's own.
readonly property var hiddenIds: ["blueman"]
readonly property var shownItems: SystemTray.items.values
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
// Electron and Chromium apps send only a full-colour pixmap, and flattening
// that loses everything inside the outline: Brave's lion became a plain
// shield. The icon theme often ships a panel glyph drawn for exactly this
// job, so look one up by the app's name and use the pixmap only when none
// exists. The id is no help here ("chrome_status_icon_1" for Brave and
// Proton Pass alike), so the tooltip title is the name we go by.
function themedIcon(item: SystemTrayItem): string {
if (item.icon.startsWith("image://icon/")) return item.icon;
const name = (item.tooltipTitle || item.title || item.id)
.toLowerCase().replace(/[^a-z0-9]+/g, "-");
for (const suffix of ["-tray", "-panel", "-symbolic"]) {
const path = Quickshell.iconPath(name + suffix, true);
if (path !== "") return path;
}
return item.icon;
}
Repeater { Repeater {
model: SystemTray.items model: root.shownItems
Item { Item {
id: entry id: entry
@@ -30,32 +56,25 @@ Column {
// App icons arrive in their own brand colours, which wrecks the // App icons arrive in their own brand colours, which wrecks the
// two-hue palette faster than anything else in the shell. Flatten // two-hue palette faster than anything else in the shell. Flatten
// every one of them to a single-colour silhouette: white at rest, // every one of them to a single-colour silhouette: subtext at rest,
// crimson under the pointer. Detail inside the glyph is lost on // a notch under the rail's own glyphs, and accent under the pointer. Detail inside the glyph is lost on
// purpose — that flattening *is* the screen-printed look. // purpose — that flattening *is* the screen-printed look.
IconImage { IconImage {
id: img id: img
anchors.fill: parent anchors.fill: parent
source: entry.modelData.icon source: root.themedIcon(entry.modelData)
asynchronous: true asynchronous: true
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: TintEffect {
colorization: 1.0 tintColor: itemHover.hovered
colorizationColor: itemHover.hovered
? Theme.accent ? Theme.accent
: Theme.text : Theme.subtext
Behavior on colorizationColor { Behavior on tintColor {
ColorAnimation { duration: Theme.durFast } ColorAnimation { duration: Theme.durFast }
} }
} }
opacity: itemHover.hovered ? 1.0 : 0.82
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
} }
scale: itemHover.hovered ? 1.18 : 1.0 scale: itemHover.hovered ? 1.18 : 1.0
@@ -72,7 +91,7 @@ Column {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
onTapped: { onTapped: {
if (entry.modelData.onlyMenu) { if (entry.modelData.onlyMenu) {
menuAnchor.open(); menu.toggle();
} else { } else {
entry.modelData.activate(); entry.modelData.activate();
} }
@@ -81,7 +100,9 @@ Column {
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onTapped: menuAnchor.open() onTapped: {
if (entry.modelData.hasMenu) menu.toggle();
}
} }
TapHandler { TapHandler {
@@ -90,13 +111,10 @@ Column {
} }
// Menus open to the left, into the screen rather than off its edge. // Menus open to the left, into the screen rather than off its edge.
QsMenuAnchor { TrayMenu {
id: menuAnchor id: menu
menu: entry.modelData.menu anchorItem: entry
anchor.item: entry menuHandle: entry.modelData.menu
anchor.edges: Edges.Left
anchor.gravity: Edges.Left
anchor.margins.right: Theme.popoutGap
} }
} }
} }
+34 -24
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components" import "root:/components"
// Workspace indicator: leaning ticks that stretch open when focused. Occupied // Workspace indicator: leaning ticks that stretch open when focused. Occupied
// workspaces glow teal, urgent ones snap to crimson. // workspaces fill in, urgent ones snap to the alarm colour.
Item { Item {
id: root id: root
@@ -65,8 +65,12 @@ Item {
// Every tick is the same height and the same shear. Only the // Every tick is the same height and the same shear. Only the
// width changes, and only to say which workspace is focused. // width changes, and only to say which workspace is focused.
width: focused ? 32 : (occupied ? 16 : 9) //
height: 18 // Kept well short of the bar's 20px divider rules. At 18px an
// empty outline tick was the same height, lean and weight as a
// divider, and the workspace strip read as a row of rules.
width: focused ? 30 : (occupied ? 16 : 8)
height: 14
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
@@ -76,35 +80,36 @@ Item {
} }
} }
Rectangle { Skew {
id: tick id: tick
anchors.fill: parent anchors.fill: parent
// Focused is a solid crimson block. Occupied is a white // Focused is a solid accent block. Urgent is a solid alarm
// block at half weight. Empty is an outline. Three states, // block — it used to be the accent too, which made an
// two colours, no ambiguity. // urgent workspace indistinguishable from the focused one.
// Occupied is a subtext block. Empty is a flat recessed
// slot, filled rather than outlined so it cannot pass for a
// divider.
color: { color: {
if (pip.focused || pip.urgent) return Theme.accent; if (pip.focused) return Theme.accent;
if (pip.urgent) return Theme.alarm;
if (pip.hovered) return Theme.text; if (pip.hovered) return Theme.text;
if (pip.occupied) return Theme.alpha(Theme.text, 0.5); if (pip.occupied) return Theme.subtext;
return "transparent"; return Theme.surfaceAlt;
}
border.color: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
border.width: 1
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
1, -Theme.skew, 0, pip.height * Theme.skew,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
} }
Behavior on color { Behavior on color {
ColorAnimation { duration: Theme.durBase } ColorAnimation { duration: Theme.durBase }
} }
// Urgent is the one pip that has to be seen peripherally.
SequentialAnimation on opacity {
running: pip.urgent && !pip.focused
loops: Animation.Infinite
alwaysRunToEnd: true
NumberAnimation { to: 0.4; duration: 520; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 520; easing.type: Easing.InOutQuad }
}
} }
// The focused workspace wears its number. // The focused workspace wears its number.
@@ -115,7 +120,7 @@ Item {
text: pip.wsId text: pip.wsId
color: Theme.ink color: Theme.ink
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.italic: true font.italic: true
renderType: Text.NativeRendering renderType: Text.NativeRendering
@@ -140,8 +145,13 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
// Hyprland evaluates IPC `dispatch` payloads as Lua now that the
// config is Lua (hyprland.lua rather than hyprland.conf), so the
// old `dispatch workspace 3` string is a Lua syntax error and the
// click silently does nothing. Dispatchers have to be called the
// same way lua/binds/workspaces.lua calls them.
TapHandler { TapHandler {
onTapped: Hyprland.dispatch("workspace " + pip.wsId) onTapped: Hyprland.dispatch("hl.dsp.focus({workspace = " + pip.wsId + "})")
} }
} }
} }
+27
View File
@@ -0,0 +1,27 @@
import QtQuick
import "root:/config"
import "root:/components"
// Calm, permanent-chrome preset. It is deliberately flat and opaque: one
// neutral keyline, no texture, no offset copy, and no accent unless active.
P5Panel {
property bool active: false
property color tone: Theme.treatmentActiveMark
fill: Theme.treatmentRestFill
fillOpacity: 1.0
border: Theme.treatmentRestMark
borderWidth: 1
// Keep the hard ink contour. Removing both texture and keyline made the
// permanent slabs read as generic broadcast HUD chrome.
keyline: true
drop: false
halftone: false
sheen: false
slash: active
slashColor: tone
slashWidth: 4
lean: 0.06
chop: Theme.cut
padding: Theme.padS
}
-120
View File
@@ -1,120 +0,0 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
// Radial gauge used across the resource popout. Sweeps a 270° arc with a
// tick-marked rail behind it and the reading set in the middle.
Item {
id: root
property real value: 0 // 0..100
property string label: ""
property string readout: Math.round(value) + "%"
property string sub: ""
property color arcColor: Theme.heat(value)
property real thickness: 7
property real startAngle: 135
property real sweep: 270
property int ticks: 24
implicitWidth: 96
implicitHeight: 96
readonly property real _r: Math.min(width, height) / 2 - thickness / 2 - 4
readonly property real _cx: width / 2
readonly property real _cy: height / 2
// Rail
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Theme.alpha(Theme.outline, 0.5)
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep
}
}
}
// Tick marks around the rail.
Repeater {
model: root.ticks
Rectangle {
required property int index
readonly property real a: (root.startAngle + root.sweep * (index / (root.ticks - 1))) * Math.PI / 180
readonly property real rr: root._r + root.thickness / 2 + 3
width: 2
height: index % 4 === 0 ? 6 : 3
color: Theme.alpha(Theme.primary, index % 4 === 0 ? 0.55 : 0.25)
x: root._cx + Math.cos(a) * rr - width / 2
y: root._cy + Math.sin(a) * rr - height / 2
rotation: (root.startAngle + root.sweep * (index / (root.ticks - 1))) + 90
}
}
// Value arc
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: root.arcColor
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
id: valueArc
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep * Math.max(0, Math.min(100, root.value)) / 100
Behavior on sweepAngle {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
}
}
}
Column {
anchors.centerIn: parent
spacing: -2
SplitText {
anchors.horizontalCenter: parent.horizontalCenter
text: root.readout
pixelSize: Theme.fsLarge
color: Theme.text
split: 1.2
splitOpacity: 0.6
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.sub !== ""
text: root.sub
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+118
View File
@@ -0,0 +1,118 @@
import QtQuick
import "root:/config"
// One row of a popout list — a Wi-Fi network, a bluetooth device, an audio
// output. The pickers used to draw their own rows, each with its own height and
// its own translucent white wash for hover, which on Mocha's blue-grey ground
// came out as a faint milky film. This is the one row they share now.
//
// Rest is bare. Hover lifts the row onto the surface tone. The current item —
// connected, in use — carries an accent tick down its leading edge and an
// accent glyph, and its label goes to full weight.
Item {
id: root
property string icon: ""
property string label: ""
// Optional mono line under the label: a device state, an address.
property string detail: ""
property bool current: false
// Right-aligned trailing content — a lock, a battery level, a tag.
default property alias trailing: trailingRow.data
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
implicitHeight: root.detail !== "" ? 36 : 30
Rectangle {
anchors.fill: parent
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.current ? root.height - 10 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Icon {
id: glyph
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
width: 18
visible: root.icon !== ""
text: root.icon
color: root.current ? Theme.accent : (root.hovered ? Theme.text : Theme.subtext)
font.pixelSize: Theme.fsLarge
}
Column {
anchors.left: glyph.visible ? glyph.right : parent.left
anchors.leftMargin: glyph.visible ? Theme.padS : Theme.padM
anchors.right: trailingRow.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: -1
Text {
width: parent.width
text: root.label
elide: Text.ElideRight
color: root.current || root.hovered ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: root.current ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Text {
width: parent.width
visible: text !== ""
text: root.detail
elide: Text.ElideRight
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
Row {
id: trailingRow
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: 4
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
}
+9 -1
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Window
import "root:/config" import "root:/config"
// Horizontally scrolling text that only moves when it actually overflows, and // Horizontally scrolling text that only moves when it actually overflows, and
@@ -18,6 +19,13 @@ Item {
readonly property bool overflowing: label.implicitWidth > width + 1 readonly property bool overflowing: label.implicitWidth > width + 1
// An Item inside an unmapped PopupWindow still reports itself visible, so a
// marquee in a closed popout would keep its infinite animation alive. That
// animation writes QML properties on every frame — at 240 Hz, three closed
// popout marquees are enough to keep the whole animation driver spinning and
// burn a fifth of a core with nothing on screen. Follow the window instead.
readonly property bool onScreen: Window.window?.visible ?? true
clip: true clip: true
implicitHeight: label.implicitHeight implicitHeight: label.implicitHeight
implicitWidth: label.implicitWidth implicitWidth: label.implicitWidth
@@ -38,7 +46,7 @@ Item {
SequentialAnimation { SequentialAnimation {
id: scroll id: scroll
running: root.running && root.overflowing && root.visible running: root.running && root.overflowing && root.visible && root.onScreen
loops: Animation.Infinite loops: Animation.Infinite
PauseAnimation { duration: root.pause } PauseAnimation { duration: root.pause }
+9 -2
View File
@@ -8,7 +8,14 @@ MultiEffect {
maskEnabled: maskItem !== null maskEnabled: maskItem !== null
maskSource: maskItem maskSource: maskItem
// Hard edge — the chopped corners should read as cuts, not fades.
// The chopped corners should read as cuts, not fades — but a spread of 0
// is a hard `alpha < 0.5 ? 0 : 1` step, which throws away the one thing
// that makes the cut look drawn rather than pixelated: the mask Shape's own
// coverage antialiasing. Every partially covered pixel along a diagonal got
// rounded to all-or-nothing and the art came out with a visible staircase
// down both leaning edges. A narrow spread lets that single-pixel ramp
// through and nothing wider — still a cut, just not a jagged one.
maskThresholdMin: 0.5 maskThresholdMin: 0.5
maskSpreadAtMin: 0.0 maskSpreadAtMin: 0.3
} }
+3 -9
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import "root:/config" import "root:/config"
import "root:/components"
// A segmented load meter — slanted ticks that light up left to right. Reads as // A segmented load meter — slanted ticks that light up left to right. Reads as
// a gauge at a glance and keeps the panel language consistent. // a gauge at a glance and keeps the panel language consistent.
@@ -31,9 +32,10 @@ Item {
width: root.segmentWidth width: root.segmentWidth
height: root.height height: root.height
Rectangle { Skew {
width: root.segmentWidth width: root.segmentWidth
height: root.height height: root.height
lean: root.lean
color: parent.index < root.litCount ? root.activeColor : root.idleColor color: parent.index < root.litCount ? root.activeColor : root.idleColor
// Peak segments glow, so a pegged CPU is visible peripherally. // Peak segments glow, so a pegged CPU is visible peripherally.
@@ -41,14 +43,6 @@ Item {
? (parent.index >= root.segments - 2 ? 1.0 : 0.9) ? (parent.index >= root.segments - 2 ? 1.0 : 0.9)
: 1.0 : 1.0
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
1, -root.lean, 0, root.height * root.lean,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
}
Behavior on color { Behavior on color {
ColorAnimation { duration: Theme.durBase } ColorAnimation { duration: Theme.durBase }
} }
-2
View File
@@ -70,8 +70,6 @@ Item {
text: root.readout text: root.readout
pixelSize: Theme.fsBody pixelSize: Theme.fsBody
color: Theme.text color: Theme.text
split: 1
splitOpacity: 0.5
} }
} }
+22 -19
View File
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import "root:/config" import "root:/config"
import "root:/components"
import "root:/services" import "root:/services"
// One notification, used both in the history list and as a floating toast. // One notification, used both in the history list and as a floating toast.
@@ -17,23 +18,32 @@ Item {
signal dismissed() signal dismissed()
// The card's own geometry, published so anything drawn along its edges —
// the toast countdown rule — can stay inside the chopped corners instead of
// running out past them.
readonly property real lean: 0.05
readonly property real chop: 12
implicitHeight: panel.height implicitHeight: panel.height
P5Panel { P5Panel {
id: panel id: panel
width: parent.width width: parent.width
height: layout.implicitHeight + Theme.padM * 2 height: layout.implicitHeight + Theme.padM * 2
lean: 0.05 lean: root.lean
chop: 12 chop: root.chop
fill: Theme.surface fill: Theme.surface
fillOpacity: 0.97 fillOpacity: 0.97
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5) // Urgency lives in the slash, and in the frame only when critical. A
// tone-tinted frame on every card drew a pale outline round ordinary
// notifications, and the critical halftone was painted in the accent,
// so the most urgent card wore the colour that means "live".
border: root.notif?.urgency === NotificationUrgency.Critical
? root.tone
: (hover.hovered ? Theme.treatmentHoverMark : Theme.surfaceAlt)
slash: true slash: true
slashColor: root.tone slashColor: root.tone
slashWidth: 4 slashWidth: 4
halftone: root.notif?.urgency === NotificationUrgency.Critical
halftoneColor: Theme.accent
halftoneOpacity: 0.12
padding: Theme.padM padding: Theme.padM
Column { Column {
@@ -59,7 +69,7 @@ Item {
text: (root.notif?.appName || "System").toUpperCase() text: (root.notif?.appName || "System").toUpperCase()
color: root.tone color: root.tone
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 2 font.letterSpacing: 2
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -117,25 +127,18 @@ Item {
implicitWidth: actionLabel.implicitWidth + Theme.padM implicitWidth: actionLabel.implicitWidth + Theme.padM
implicitHeight: 22 implicitHeight: 22
Rectangle { Skew {
anchors.fill: parent anchors.fill: parent
color: actionHover.hovered color: actionHover.hovered ? Theme.accent : Theme.surfaceAlt
? Theme.alpha(root.tone, 0.3) borderWidth: 1
: Theme.alpha(root.tone, 0.1) borderColor: actionHover.hovered ? Theme.accent : Theme.overlay
border.width: 1
border.color: Theme.alpha(root.tone, 0.6)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
} }
Text { Text {
id: actionLabel id: actionLabel
anchors.centerIn: parent anchors.centerIn: parent
text: actionChip.modelData.text text: actionChip.modelData.text
color: Theme.text color: actionHover.hovered ? Theme.ink : Theme.text
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.italic: true font.italic: true
+4 -2
View File
@@ -3,7 +3,7 @@ import QtQuick.Shapes
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// The shared ground for the two full-screen surfaces: a crimson wedge leaning // The shared ground for the two full-screen surfaces: an accent wedge leaning
// at the house angle, a tiled stripe field that sweeps in from the left, and a // at the house angle, a tiled stripe field that sweeps in from the left, and a
// halftone dot wash over the top. // halftone dot wash over the top.
// //
@@ -33,7 +33,9 @@ Item {
property real wedgeSpan: 0.34 property real wedgeSpan: 0.34
property bool stripes: true property bool stripes: true
property real stripeOpacity: 0.10 // Tuned down from 0.10 for Mocha. Crust stripes on the blue-grey base read
// far heavier than they did on black, and buzzed against the text.
property real stripeOpacity: 0.07
property bool halftone: true property bool halftone: true
property real halftoneOpacity: 0.05 property real halftoneOpacity: 0.05
+75 -21
View File
@@ -1,24 +1,46 @@
import QtQuick import QtQuick
import "root:/config" import "root:/config"
// Chopped, leaning button. On hover it lunges toward the pointer and flips to // Chopped, leaning button with one activation path for pointer and keyboard.
// crimson-on-white; on press it snaps flat. FocusScope {
Item {
id: root id: root
property string text: "" property string text: ""
property string icon: "" property string icon: ""
property color accent: Theme.primary property color accent: Theme.primary
property color hoverFill: Theme.accent
property bool destructive: false property bool destructive: false
property bool selected: false
property real lean: Theme.skew property real lean: Theme.skew
property real lunge: 6 property real lunge: 6
property bool wide: false property bool wide: false
signal clicked() signal clicked()
readonly property bool hovered: hover.hovered readonly property bool hovered: root.enabled && hover.hovered
readonly property bool pressed: tap.pressed readonly property bool pressed: tap.pressed
readonly property bool focusedOrSelected: root.activeFocus || root.selected
readonly property bool highlighted: root.hovered || root.focusedOrSelected
// Keyboard focus and roving selection invert the slab to solid accent.
// Pointer hover stays a keyline change, so the two never look alike and a
// mouse user never sees the "selected" treatment by accident. Destructive
// buttons keep their own mauve inversion for both.
readonly property bool inverted: root.enabled && !root.destructive && root.focusedOrSelected
// Pointer activation must not turn a transient action into the black/white
// keyboard-focus treatment. Tab still reaches it, and programmatic focus
// (power-menu roving selection / pinned popouts) still works.
focusPolicy: root.enabled ? Qt.TabFocus : Qt.NoFocus
function activate(): void {
if (root.enabled)
root.clicked();
}
function activateFromKey(event): void {
if (!event.isAutoRepeat)
root.activate();
}
implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2) implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2)
implicitHeight: 44 implicitHeight: 44
@@ -28,14 +50,28 @@ Item {
anchors.fill: parent anchors.fill: parent
lean: root.lean lean: root.lean
padding: Theme.padS padding: Theme.padS
fill: root.hovered ? (root.destructive ? root.hoverFill : Theme.text) : Theme.surface fill: {
fillOpacity: root.hovered ? 1.0 : 0.9 if (!root.enabled) return Theme.treatmentDisabledFill;
border: root.hovered ? "transparent" : Theme.alpha(root.accent, 0.55) if (root.destructive)
slash: !root.hovered return root.highlighted ? Theme.treatmentDestructiveActiveFill
slashColor: root.destructive ? Theme.accent : root.accent : Theme.treatmentDestructiveFill;
halftone: root.hovered if (root.inverted) return Theme.treatmentFocusFill;
halftoneColor: root.destructive ? Theme.text : Theme.base if (root.highlighted) return Theme.treatmentHoverFill;
halftoneOpacity: 0.14 return Theme.treatmentRestFill;
}
fillOpacity: root.enabled ? 1.0 : 0.72
border: {
if (!root.enabled) return Theme.treatmentDisabledMark;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveMark
: Theme.treatmentDestructiveMark;
if (root.inverted) return Theme.treatmentFocusMark;
if (root.highlighted) return Theme.treatmentHoverMark;
return Theme.alpha(root.accent, 0.55);
}
borderWidth: Theme.stroke
slash: !root.highlighted
slashColor: root.destructive ? Theme.alarm : root.accent
Behavior on fillOpacity { Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast } NumberAnimation { duration: Theme.durFast }
@@ -52,17 +88,29 @@ Item {
visible: root.icon !== "" visible: root.icon !== ""
text: root.icon text: root.icon
font.pixelSize: Theme.fsTitle font.pixelSize: Theme.fsTitle
color: root.hovered color: {
? (root.destructive ? Theme.text : Theme.base) if (!root.enabled) return Theme.treatmentDisabledText;
: root.accent if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText;
return root.accent;
}
} }
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.text text: root.text
color: root.hovered color: {
? (root.destructive ? Theme.text : Theme.base) if (!root.enabled) return Theme.treatmentDisabledText;
: Theme.text if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText;
return Theme.treatmentRestText;
}
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
@@ -79,7 +127,7 @@ Item {
} }
transform: Translate { transform: Translate {
x: root.pressed ? 0 : (root.hovered ? root.lunge : 0) x: root.pressed ? 0 : (root.highlighted ? root.lunge : 0)
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
@@ -96,11 +144,17 @@ Item {
HoverHandler { HoverHandler {
id: hover id: hover
enabled: root.enabled
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
TapHandler { TapHandler {
id: tap id: tap
onTapped: root.clicked() enabled: root.enabled
onTapped: root.activate()
} }
Keys.onReturnPressed: event => root.activateFromKey(event)
Keys.onEnterPressed: event => root.activateFromKey(event)
Keys.onSpacePressed: event => root.activateFromKey(event)
} }
+3 -3
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components" import "root:/components"
// The base surface for everything in this shell: a leaning, corner-chopped slab // The base surface for everything in this shell: a leaning, corner-chopped slab
// with an optional halftone wash and a crimson slash along its leading edge. // with an optional halftone wash and a accent slash along its leading edge.
Item { Item {
id: root id: root
@@ -29,7 +29,7 @@ Item {
property real chop: Theme.cut property real chop: Theme.cut
property var chopCorners: [1, 3] property var chopCorners: [1, 3]
// Crimson slash down the leading edge — the Persona 5 tell. // Tone slash down the leading edge — the Persona 5 tell.
property bool slash: false property bool slash: false
property color slashColor: Theme.accent property color slashColor: Theme.accent
property real slashWidth: 4 property real slashWidth: 4
@@ -183,7 +183,7 @@ Item {
// //
// Its polygon is built from the panel's raw corners, so on a panel with a // Its polygon is built from the panel's raw corners, so on a panel with a
// chopped corner the slash used to run straight through the cut and out the // chopped corner the slash used to run straight through the cut and out the
// far side — a crimson wedge sitting a few pixels outside the silhouette, // far side — an accent wedge sitting a few pixels outside the silhouette,
// at an angle that matched nothing. It is masked to the outline now, so the // at an angle that matched nothing. It is masked to the outline now, so the
// slash ends exactly where the chamfer does. // slash ends exactly where the chamfer does.
Loader { Loader {
+53 -24
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import "root:/config" import "root:/config"
import "root:/components"
// Leaning slider with a chopped handle. Drag or click anywhere on the rail. // Leaning slider with a chopped handle. Drag or click anywhere on the rail.
Item { Item {
@@ -7,42 +8,50 @@ Item {
property real value: 0 // 0..1 property real value: 0 // 0..1
property color accent: Theme.primary property color accent: Theme.primary
property bool enabledControl: true property real stepSize: 0.05
signal moved(real value) signal moved(real value)
implicitHeight: 18 implicitHeight: 18
implicitWidth: 200 implicitWidth: 200
focusPolicy: root.enabled ? Qt.StrongFocus : Qt.NoFocus
readonly property real _lean: Theme.skew * height readonly property real _lean: Theme.skew * height
function commit(nextValue: real): void {
if (!root.enabled)
return;
root.moved(Math.max(0, Math.min(1, nextValue)));
}
// Rail // Rail
Rectangle { Skew {
id: rail id: rail
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: 6 height: root.activeFocus ? 10 : 6
color: Theme.alpha(Theme.outline, 0.7) // Focus widens the rail and rings it in the accent rather than filling
// it: the level fill is often the accent itself, and a solid accent
// rail swallowed it.
color: root.activeFocus
? Theme.surfaceAlt
: Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7)
borderColor: root.activeFocus ? Theme.accent : "transparent"
borderWidth: root.activeFocus ? Theme.stroke : 0
transform: Matrix4x4 { Behavior on height {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 6 * Theme.skew, NumberAnimation { duration: Theme.durFast }
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
} }
} }
// Fill // Fill
Rectangle { Skew {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
width: Math.max(0, Math.min(1, root.value)) * root.width width: Math.max(0, Math.min(1, root.value)) * root.width
height: 6 height: 6
color: root.enabledControl ? root.accent : Theme.muted color: root.enabled ? root.accent : Theme.treatmentDisabledText
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 6 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on width { Behavior on width {
enabled: !drag.active enabled: !drag.active
@@ -55,19 +64,14 @@ Item {
} }
// Handle // Handle
Rectangle { Skew {
id: handle id: handle
width: 6 width: 6
height: hover.hovered || drag.active ? 18 : 14 height: root.activeFocus || hover.hovered || drag.active ? 18 : 14
color: root.enabledControl ? Theme.text : Theme.muted color: root.enabled ? Theme.text : Theme.treatmentDisabledText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2 x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, height * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on height { Behavior on height {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack } NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
} }
@@ -79,21 +83,46 @@ Item {
HoverHandler { HoverHandler {
id: hover id: hover
enabled: root.enabled
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
TapHandler { TapHandler {
onTapped: event => root.moved(Math.max(0, Math.min(1, event.position.x / root.width))) enabled: root.enabled
onPressedChanged: if (pressed) root.forceActiveFocus(Qt.MouseFocusReason)
onTapped: event => root.commit(event.position.x / root.width)
} }
DragHandler { DragHandler {
id: drag id: drag
enabled: root.enabled
target: null target: null
xAxis.enabled: true xAxis.enabled: true
yAxis.enabled: false yAxis.enabled: false
onCentroidChanged: { onCentroidChanged: {
if (!active) return; if (!active) return;
root.moved(Math.max(0, Math.min(1, centroid.position.x / root.width))); root.forceActiveFocus(Qt.MouseFocusReason);
root.commit(centroid.position.x / root.width);
} }
} }
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Left:
root.commit(root.value - root.stepSize);
break;
case Qt.Key_Right:
root.commit(root.value + root.stepSize);
break;
case Qt.Key_Home:
root.commit(0);
break;
case Qt.Key_End:
root.commit(1);
break;
default:
return;
}
event.accepted = true;
}
} }
+114
View File
@@ -0,0 +1,114 @@
import QtQuick
import Quickshell
import "root:/config"
// Calm, informational hover label for permanent chrome. It never takes focus
// or participates in popup coordination; a delayed label should not dismiss or
// displace the detail panel attached to the same rail control.
PopupWindow {
id: root
required property Item anchorItem
property bool triggerHovered: false
property string title: ""
property string status: ""
property string hint: ""
property int delay: 400
property bool suppressed: false
readonly property int pad: Theme.padS
readonly property real textWidth: Math.max(titleText.implicitWidth,
statusText.implicitWidth, hintText.implicitWidth)
readonly property real contentWidth: Math.max(112, textWidth + pad * 4)
readonly property real contentHeight: textColumn.implicitHeight + pad * 2
anchor.item: anchorItem
anchor.edges: Edges.Left
anchor.gravity: Edges.Left
anchor.adjustment: PopupAdjustment.Flip | PopupAdjustment.SlideY
anchor.margins.left: Theme.popoutGap
implicitWidth: contentWidth
implicitHeight: contentHeight
color: "transparent"
visible: shown && title !== "" && !suppressed
property bool shown: false
onTriggerHoveredChanged: {
if (triggerHovered && title !== "" && !suppressed) {
hideTimer.stop();
showTimer.restart();
} else {
showTimer.stop();
hideTimer.restart();
}
}
onSuppressedChanged: {
if (suppressed) {
showTimer.stop();
shown = false;
} else if (triggerHovered) {
showTimer.restart();
}
}
Timer {
id: showTimer
interval: root.delay
onTriggered: root.shown = root.triggerHovered && !root.suppressed
}
Timer {
id: hideTimer
interval: 70
onTriggered: root.shown = false
}
ChromeSurface {
anchors.fill: parent
padding: root.pad
lean: 0.06
Column {
id: textColumn
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
id: titleText
text: root.title.toUpperCase()
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
}
Text {
id: statusText
visible: text !== ""
text: root.status
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
Text {
id: hintText
visible: text !== ""
text: root.hint.toUpperCase()
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 0.8
renderType: Text.NativeRendering
}
}
}
}
+107 -2
View File
@@ -16,6 +16,8 @@ PopupWindow {
property int fromEdge: Edges.Bottom property int fromEdge: Edges.Bottom
property real contentWidth: Theme.popoutWidth property real contentWidth: Theme.popoutWidth
property real contentHeight: 200 property real contentHeight: 200
property Item initialFocusItem: null
property bool remappingPinned: false
// Extra surface around the content so the entry overshoot and glow are not // Extra surface around the content so the entry overshoot and glow are not
// clipped by the window edge. // clipped by the window edge.
@@ -24,6 +26,8 @@ PopupWindow {
default property alias body: contentHolder.data default property alias body: contentHolder.data
readonly property bool wantsOpen: triggerHovered || panelHover.hovered || pinned readonly property bool wantsOpen: triggerHovered || panelHover.hovered || pinned
readonly property bool pending: showTimer.running || remapTimer.running || remappingPinned
readonly property bool detailActive: shown || pending
signal dismissedFully() signal dismissedFully()
@@ -51,13 +55,15 @@ PopupWindow {
implicitHeight: contentHeight + bleed * 2 implicitHeight: contentHeight + bleed * 2
color: "transparent" color: "transparent"
visible: shown visible: shown
grabFocus: pinned && shown
property bool shown: false property bool shown: false
onWantsOpenChanged: { onWantsOpenChanged: {
if (wantsOpen) { if (wantsOpen) {
hideTimer.stop(); hideTimer.stop();
showTimer.restart(); if (!pinned && !remappingPinned)
showTimer.restart();
} else { } else {
showTimer.stop(); showTimer.stop();
hideTimer.restart(); hideTimer.restart();
@@ -70,6 +76,19 @@ PopupWindow {
onTriggered: root.open() onTriggered: root.open()
} }
onVisibleChanged: {
if (!visible && shown)
dismissImmediately();
}
onClosed: {
if (remappingPinned) {
remappingPinned = false;
remapTimer.restart();
} else if (shown || pinned) {
dismissImmediately();
}
}
Timer { Timer {
id: hideTimer id: hideTimer
interval: root.hideDelay interval: root.hideDelay
@@ -77,19 +96,91 @@ PopupWindow {
} }
function open() { function open() {
PopupCoordinator.claim(root);
exitAnim.stop(); exitAnim.stop();
shown = true; shown = true;
enterAnim.restart(); enterAnim.restart();
} }
// Focus policy is fixed at map time. A hover-visible passive surface must
// therefore unmap before returning as a click-owned keyboard popup.
function openPinned() {
showTimer.stop();
hideTimer.stop();
PopupCoordinator.claim(root);
if (shown) {
remappingPinned = true;
shown = false;
enterAnim.stop();
exitAnim.stop();
pinned = true;
} else {
pinned = true;
open();
focusTimer.restart();
}
}
function togglePinned() {
if (pinned) {
pinned = false;
dismiss();
} else {
openPinned();
}
}
function dismiss() { function dismiss() {
if (!shown) return; if (!shown) {
if (pinned || pending)
dismissImmediately();
return;
}
pinned = false;
PopupCoordinator.release(root);
enterAnim.stop(); enterAnim.stop();
exitAnim.restart(); exitAnim.restart();
} }
function dismissImmediately() {
showTimer.stop();
hideTimer.stop();
remapTimer.stop();
focusTimer.stop();
enterAnim.stop();
exitAnim.stop();
remappingPinned = false;
pinned = false;
shown = false;
contentHolder.opacity = 0;
contentHolder.scale = 0.94;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed;
PopupCoordinator.release(root);
root.dismissedFully();
}
Timer {
id: remapTimer
interval: 0
onTriggered: {
root.open();
focusTimer.restart();
}
}
Timer {
id: focusTimer
interval: 0
onTriggered: {
const target = root.initialFocusItem || contentHolder;
target.forceActiveFocus(Qt.TabFocusReason);
}
}
Item { Item {
id: contentHolder id: contentHolder
focus: root.pinned
x: root.bleed x: root.bleed
y: root.bleed y: root.bleed
width: root.contentWidth width: root.contentWidth
@@ -109,8 +200,21 @@ PopupWindow {
// The handler has to live on the content item, not the window: a // The handler has to live on the content item, not the window: a
// HoverHandler parented to the PopupWindow itself latches on and the // HoverHandler parented to the PopupWindow itself latches on and the
// panel never learns the pointer left. // panel never learns the pointer left.
//
// Disabled while the popout is down, which clears `hovered`. An unmapped
// surface gets no leave event, so a handler that was hovered when the
// window went away stayed hovered — `wantsOpen` never went false again
// and the panel came back up and refused to close.
HoverHandler { HoverHandler {
id: panelHover id: panelHover
enabled: root.shown
}
Keys.onEscapePressed: event => {
if (root.pinned) {
event.accepted = true;
root.dismissImmediately();
}
} }
} }
@@ -151,6 +255,7 @@ PopupWindow {
root.pinned = false; root.pinned = false;
contentHolder.x = root.bleed; contentHolder.x = root.bleed;
contentHolder.y = root.bleed; contentHolder.y = root.bleed;
PopupCoordinator.release(root);
root.dismissedFully(); root.dismissedFully();
} }
} }
+35
View File
@@ -0,0 +1,35 @@
import QtQuick
import "root:/config"
// Section caption for popouts: a spaced mono label running into a hairline.
// Every panel used to label its lists differently — a bare caption here, a
// caption and rule there — which is part of why no two popouts looked related.
Item {
id: root
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: label
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: label.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.5)
}
}
+69
View File
@@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
import "root:/components"
// A leaning block — the small-scale counterpart to P5Panel.
//
// Every tick, rule, meter segment and slider rail in this shell leans at
// Theme.skew. Shearing a plain Rectangle with a Matrix4x4 does that, but Qt
// rasterises a Rectangle's geometry with no antialiasing at all, so the slanted
// edges come out as a hard pixel staircase — at 18px tall that staircase is
// most of what you see, and it reads as broken rendering rather than as a
// deliberate angle. Drawing the parallelogram as a Shape with the curve
// renderer gets analytic coverage AA on the diagonals instead.
//
// Drop-in for a sheared Rectangle: set width/height as before and use `color`,
// `borderColor` and `borderWidth` the same way.
Shape {
id: root
property color color: "transparent"
property color borderColor: "transparent"
property real borderWidth: 0
// Lean of the vertical edges as a fraction of height, matching P5Panel.
property real lean: Theme.skew
property bool leanLeft: false
// Wide, short pieces — the rail's hairline rules — slant their horizontal
// edges by a fraction of width instead, exactly as P5Panel does for the
// rail slab.
property bool vertical: false
// NOTE ON SIZING. The lean shifts one edge by `lean * height` (or by
// `lean * width` when vertical), and the piece that actually gets painted is
// what is left: a Skew declared 1px wide and 20px tall does not draw a 1px
// rule at an angle, it draws a rule |1 - 20 * 0.249| = 4px thick that hangs
// 4px to the *left* of the box it was given. That is why the bar's and the
// rail's hairlines came out fat and off-centre. A thin leaning rule has to be
// declared with room for its own lean — `height * Theme.skew + weight` — and
// the call sites that draw one now do exactly that.
//
// Zero-sized pieces still have a lean offset, so a collapsed hover wipe or a
// meter at 0 drew a sub-pixel wedge of pure accent — the stray red dot under
// the metrics. Nothing with no extent should paint at all.
visible: root.width > 0 && root.height > 0
preferredRendererType: Shape.CurveRenderer
asynchronous: false
ShapePath {
fillColor: root.color
strokeColor: root.borderWidth > 0 ? root.borderColor : "transparent"
strokeWidth: root.borderWidth
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: {
const w = root.width, h = root.height, l = root.lean;
if (root.vertical)
return Geom.closed(root.leanLeft ? Geom.parallelogramVL(w, h, l)
: Geom.parallelogramV(w, h, l));
return Geom.closed(root.leanLeft ? Geom.parallelogramL(w, h, l)
: Geom.parallelogram(w, h, l));
}
}
}
}
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more