quickshell fixes

This commit is contained in:
nvrl committed 2026-09-24 13:35:20 +02:00
1 parent d2975650fa
commit 32b43bdbb3
68 files changed
+1454 -1040

No files matched your search

+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"
+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)'
-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"
+1
View File
@@ -8,6 +8,7 @@ end
shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" }) shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
shell("power", { key = mod .. " + P", description = "power menu" }) 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("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" }) shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" }) shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
+7
View File
@@ -29,3 +29,10 @@ hl.env("EDITOR", "nvim")
-- hl.env("INTEL_DEBUG", "noccs") -- hl.env("INTEL_DEBUG", "noccs")
-- hl.env("WLR_DRM_NO_ATOMIC", "1") -- hl.env("WLR_DRM_NO_ATOMIC", "1")
-- hl.env("debug:full_cm_proto", "true") -- hl.env("debug:full_cm_proto", "true")
-- headroom
hl.env("HEADROOM_ROLLOUT_CHANNEL", "beta")
hl.env("HEADROOM_OUTPUT_SHAPER", "1")
+15 -8
View File
@@ -1,4 +1,5 @@
-- Takemi — Persona 5 flavoured theme, matched to ~/.config/quickshell. -- 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 -- Everything static about how the desktop is painted. Motion lives next door
-- in lua/animations.lua. -- in lua/animations.lua.
@@ -15,7 +16,10 @@ hl.config({
col = { col = {
active_border = c.activeBorder, active_border = c.activeBorder,
inactive_border = c.outline, -- 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, resize_on_border = false,
@@ -35,13 +39,16 @@ hl.config({
active_opacity = 1.0, active_opacity = 1.0,
inactive_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 = { shadow = {
enabled = true, enabled = true,
range = 18, sharp = true,
render_power = 3, range = 1,
offset = { 0, 4 }, offset = { 4, 4 },
color = "rgba(000000ee)", color = c.ink,
color_inactive = "rgba(00000088)", color_inactive = c.ink,
}, },
-- Off. Blur is the signature of the glass-and-frost look, which is the -- Off. Blur is the signature of the glass-and-frost look, which is the
@@ -66,7 +73,7 @@ hl.config({
group = { group = {
col = { col = {
border_active = c.activeBorder, border_active = c.activeBorder,
border_inactive = c.outline, border_inactive = c.surfaceAlt,
}, },
groupbar = { groupbar = {
+1
View File
@@ -22,6 +22,7 @@ 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;
+119 -63
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
@@ -41,8 +42,8 @@ cost 4.4% of the height on top of what the bar already takes.
Each permanent surface is a single continuous `ChromeSurface` with modules Each permanent surface is a single continuous `ChromeSurface` with modules
divided by sheared hairlines. `BarPill` is chromeless by default — a module is divided by sheared hairlines. `BarPill` is chromeless by default — a module is
*content inside* a surface, not a surface of its own — and marks hover with a *content inside* a surface, not a surface of its own — and marks hover with an
crimson underline. Pass `standalone: true` for the rare thing that really is its accent underline. Pass `standalone: true` for the rare thing that really is its
own panel. own panel.
The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is
@@ -55,7 +56,7 @@ 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
@@ -106,12 +107,13 @@ 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:
@@ -120,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
@@ -165,69 +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
is white and turns crimson on a non-zero exit — the shell's one "wrong"
signal, and so the one place that earns the accent.
- **The launcher** — no external theming to keep in step, because it is part of - **The launcher** — no external theming to keep in step, because it is part of
this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for
`fuzzel --dmenu` where a script needs a picker. `fuzzel --dmenu` where a script needs a picker.
- **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
}
}
}
+2 -2
View File
@@ -56,7 +56,7 @@ Item {
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
// Crimson duplicate: visible only during the landing transition. The // Accent duplicate: visible only during the landing transition. The
// small opposing offset gives the title Persona's rough print snap // small opposing offset gives the title Persona's rough print snap
// without leaving permanent motion in the bar. // without leaving permanent motion in the bar.
Text { Text {
@@ -119,7 +119,7 @@ Item {
landedToplevel = toplevel; landedToplevel = toplevel;
} }
// A quick crimson wipe under the title on focus change. // A quick accent wipe under the title on focus change.
Rectangle { Rectangle {
id: wipe id: wipe
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
+1 -1
View File
@@ -126,7 +126,7 @@ PanelWindow {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.ruleHeight * Theme.skew + parent.ruleWeight width: parent.ruleHeight * Theme.skew + parent.ruleWeight
height: parent.ruleHeight height: parent.ruleHeight
color: Theme.alpha(Theme.outline, 0.9) color: Theme.alpha(Theme.outline, 0.5)
} }
} }
} }
+3 -3
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,7 +34,7 @@ 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, // The band reserved the full rule plus its inset — 7px off a 40px module,
@@ -102,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
+2 -2
View File
@@ -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
} }
@@ -98,7 +99,6 @@ BarPill {
text: Qt.formatDateTime(clock.date, "HH:mm") text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: Theme.fsTitle pixelSize: Theme.fsTitle
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
split: 1.4
} }
} }
-72
View File
@@ -1,72 +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.
Skew {
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
// The badge count stays upright. It used to inherit the shear from the
// transform on the block below, which at 8px left the digits smeared.
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
}
}
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 }
}
}
}
+6 -6
View File
@@ -94,7 +94,7 @@ PanelWindow {
statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName
hint: "Scroll to adjust" hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive suppressTooltip: audioPopout.detailActive
alert: Audio.muted dim: Audio.muted
onClicked: audioPopout.togglePinned() onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05) onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
} }
@@ -106,7 +106,7 @@ PanelWindow {
statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName
hint: "Scroll to adjust" hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive suppressTooltip: audioPopout.detailActive
alert: Audio.micMuted dim: Audio.micMuted
onClicked: audioPopout.togglePinned() 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))
} }
@@ -132,7 +132,7 @@ PanelWindow {
label: "Battery" label: "Battery"
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
suppressTooltip: battPopout.detailActive suppressTooltip: battPopout.detailActive
alert: Battery.critical alarm: Battery.critical
pulsing: Battery.critical pulsing: Battery.critical
onClicked: battPopout.togglePinned() onClicked: battPopout.togglePinned()
} }
@@ -165,7 +165,7 @@ PanelWindow {
: "" : ""
reserveReadout: true reserveReadout: true
dim: Notifs.dnd dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd live: Notifs.hasUnread && !Notifs.dnd
suppressTooltip: notifPopout.detailActive suppressTooltip: notifPopout.detailActive
onClicked: { onClicked: {
notifPopout.togglePinned(); notifPopout.togglePinned();
@@ -178,7 +178,7 @@ PanelWindow {
icon: "󰐥" icon: "󰐥"
label: "Power" label: "Power"
statusText: "Session actions" statusText: "Session actions"
alert: Actions.powerMenuOpen live: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu() onClicked: Actions.togglePowerMenu()
} }
} }
@@ -201,7 +201,7 @@ PanelWindow {
width: parent.ruleWidth width: parent.ruleWidth
height: parent.ruleWidth * Theme.skew + parent.ruleWeight height: parent.ruleWidth * Theme.skew + parent.ruleWeight
vertical: true vertical: true
color: Theme.alpha(Theme.outline, 0.9) color: Theme.alpha(Theme.outline, 0.5)
} }
} }
+27 -13
View File
@@ -3,10 +3,10 @@ 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
@@ -17,7 +17,15 @@ Item {
property string hint: "" property string hint: ""
property bool suppressTooltip: false 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
@@ -63,7 +71,7 @@ Item {
id: glyphMetrics id: glyphMetrics
text: root.icon text: root.icon
font.family: Theme.fontIcon font.family: Theme.fontIcon
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsRailIcon
} }
Item { Item {
@@ -76,9 +84,10 @@ Item {
y: (root.glyphBand - height) / 2 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;
@@ -96,7 +105,11 @@ Item {
anchors.topMargin: root.glyphBand + 1 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
@@ -116,9 +129,9 @@ 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
@@ -137,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
+2 -2
View File
@@ -63,7 +63,7 @@ BarPill {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.height * Theme.skew + 1 width: parent.height * Theme.skew + 1
height: parent.height height: parent.height
color: Theme.alpha(Theme.outline, 0.8) color: Theme.alpha(Theme.outline, 0.5)
} }
} }
@@ -106,7 +106,7 @@ BarPill {
id: labelText id: labelText
anchors.baseline: gauge.baseline anchors.baseline: gauge.baseline
text: metric.label text: metric.label
color: Theme.muted color: Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
+1 -1
View File
@@ -22,7 +22,7 @@ BarPill {
// 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)
}
}
+17 -16
View File
@@ -1,5 +1,4 @@
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
@@ -15,10 +14,19 @@ 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. nm-applet and blueman keep
// running — they are the Wi-Fi secret agent and the pairing agent — but
// their icons were a second network and a second bluetooth glyph a few
// slots below the rail's own.
readonly property var hiddenIds: ["nm-applet", "blueman"]
readonly property var shownItems: SystemTray.items.values
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
Repeater { Repeater {
model: SystemTray.items model: root.shownItems
Item { Item {
id: entry id: entry
@@ -31,8 +39,8 @@ 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
@@ -41,22 +49,15 @@ Column {
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
+27 -14
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 {
@@ -80,23 +84,32 @@ Item {
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;
} }
borderColor: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
borderWidth: 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.
@@ -107,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
-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()
}
}
-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
} }
} }
+11 -10
View File
@@ -34,13 +34,16 @@ Item {
chop: root.chop 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 {
@@ -66,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
} }
@@ -126,18 +129,16 @@ Item {
Skew { Skew {
anchors.fill: parent anchors.fill: parent
color: actionHover.hovered color: actionHover.hovered ? Theme.accent : Theme.surfaceAlt
? Theme.alpha(root.tone, 0.3)
: Theme.alpha(root.tone, 0.1)
borderWidth: 1 borderWidth: 1
borderColor: Theme.alpha(root.tone, 0.6) borderColor: actionHover.hovered ? Theme.accent : Theme.overlay
} }
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
+10 -3
View File
@@ -21,6 +21,12 @@ FocusScope {
readonly property bool focusedOrSelected: root.activeFocus || root.selected readonly property bool focusedOrSelected: root.activeFocus || root.selected
readonly property bool highlighted: root.hovered || root.focusedOrSelected 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 // Pointer activation must not turn a transient action into the black/white
// keyboard-focus treatment. Tab still reaches it, and programmatic focus // keyboard-focus treatment. Tab still reaches it, and programmatic focus
// (power-menu roving selection / pinned popouts) still works. // (power-menu roving selection / pinned popouts) still works.
@@ -49,6 +55,7 @@ FocusScope {
if (root.destructive) if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveFill return root.highlighted ? Theme.treatmentDestructiveActiveFill
: Theme.treatmentDestructiveFill; : Theme.treatmentDestructiveFill;
if (root.inverted) return Theme.treatmentFocusFill;
if (root.highlighted) return Theme.treatmentHoverFill; if (root.highlighted) return Theme.treatmentHoverFill;
return Theme.treatmentRestFill; return Theme.treatmentRestFill;
} }
@@ -58,15 +65,13 @@ FocusScope {
if (root.destructive) if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveMark return root.highlighted ? Theme.treatmentDestructiveActiveMark
: Theme.treatmentDestructiveMark; : Theme.treatmentDestructiveMark;
if (root.inverted) return Theme.treatmentFocusMark;
if (root.highlighted) return Theme.treatmentHoverMark; if (root.highlighted) return Theme.treatmentHoverMark;
return Theme.alpha(root.accent, 0.55); return Theme.alpha(root.accent, 0.55);
} }
borderWidth: Theme.stroke borderWidth: Theme.stroke
slash: !root.highlighted slash: !root.highlighted
slashColor: root.destructive ? Theme.alarm : root.accent slashColor: root.destructive ? Theme.alarm : root.accent
halftone: root.enabled && root.highlighted
halftoneColor: Theme.ink
halftoneOpacity: 0.14
Behavior on fillOpacity { Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast } NumberAnimation { duration: Theme.durFast }
@@ -88,6 +93,7 @@ FocusScope {
if (root.destructive) if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText; : Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText; if (root.highlighted) return Theme.treatmentHoverText;
return root.accent; return root.accent;
} }
@@ -101,6 +107,7 @@ FocusScope {
if (root.destructive) if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText; : Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText; if (root.highlighted) return Theme.treatmentHoverText;
return Theme.treatmentRestText; return Theme.treatmentRestText;
} }
+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 {
+5 -2
View File
@@ -31,10 +31,13 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: root.activeFocus ? 10 : 6 height: root.activeFocus ? 10 : 6
// 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 color: root.activeFocus
? Theme.treatmentFocusFill ? Theme.surfaceAlt
: Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7) : Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7)
borderColor: root.activeFocus ? Theme.treatmentFocusMark : "transparent" borderColor: root.activeFocus ? Theme.accent : "transparent"
borderWidth: root.activeFocus ? Theme.stroke : 0 borderWidth: root.activeFocus ? Theme.stroke : 0
Behavior on height { Behavior on height {
+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)
}
}
+55 -44
View File
@@ -1,8 +1,14 @@
import QtQuick import QtQuick
import "root:/config" import "root:/config"
// Display text with the wallpaper's RGB split baked in: a crimson and a cyan // Display text, screen-printed: the body sits on a hard crust copy knocked a
// ghost sitting a pixel off the white body. Drives most headings in the shell. // pixel and a half down-right — the same offset `P5Panel` puts behind every
// slab. Drives most headings in the shell.
//
// This used to carry a permanent RGB split, a blue and a mauve ghost either
// side of the body. On crimson-and-black that read as misregistered print; on
// Mocha's pastels it read as blur, and it turned every heading in the shell
// soft. The two ghosts still exist, but only for the moment `glitch()` runs.
Item { Item {
id: root id: root
@@ -12,23 +18,34 @@ Item {
property string family: Theme.fontDisplay property string family: Theme.fontDisplay
property bool italic: true property bool italic: true
property color color: Theme.text property color color: Theme.text
property real split: 1.6
property real splitOpacity: 0.85
property int elide: Text.ElideNone property int elide: Text.ElideNone
property int horizontalAlignment: Text.AlignLeft property int horizontalAlignment: Text.AlignLeft
property real letterSpacing: 0.5 property real letterSpacing: 0.5
// Print shadow. Scales gently with size so a 34px heading does not sit on
// the same hairline offset as an 11px label.
property real shadowOffset: Math.max(1, Math.round(root.pixelSize / 10))
property color shadowColor: Theme.ink
// Ghost offset and resting strength. Resting strength is zero, so the
// ghosts are invisible until `glitch()` kicks them out. A caller that sets
// `splitOpacity` is asking for the old permanent split and still gets it.
property real split: 1.6
property real splitOpacity: 0
// 0..1, driven only by `glitch()`. Kept apart from `split` and
// `splitOpacity` so the animation never has to remember a caller's values.
property real kick: 0
readonly property real ghostOffset: root.split + root.kick * 4
readonly property real ghostOpacity: Math.max(root.splitOpacity, root.kick * 0.85)
implicitWidth: body.implicitWidth implicitWidth: body.implicitWidth
implicitHeight: body.implicitHeight implicitHeight: body.implicitHeight
Text { component Layer: Text {
id: ghostRed
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: -root.split
anchors.topMargin: root.split * 0.4
text: root.text text: root.text
color: Theme.splitRed
opacity: root.splitOpacity
elide: root.elide elide: root.elide
horizontalAlignment: root.horizontalAlignment horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -40,39 +57,33 @@ Item {
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Text { Layer {
id: ghostCyan anchors.leftMargin: root.shadowOffset
anchors.fill: parent anchors.rightMargin: -root.shadowOffset
anchors.leftMargin: root.split anchors.topMargin: root.shadowOffset
anchors.topMargin: -root.split * 0.4 anchors.bottomMargin: -root.shadowOffset
text: root.text color: root.shadowColor
color: Theme.splitCyan
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
} }
Text { Layer {
visible: root.ghostOpacity > 0.01
anchors.leftMargin: -root.ghostOffset
anchors.topMargin: root.ghostOffset * 0.4
color: Theme.accent
opacity: root.ghostOpacity
}
Layer {
visible: root.ghostOpacity > 0.01
anchors.leftMargin: root.ghostOffset
anchors.topMargin: -root.ghostOffset * 0.4
color: Theme.alarm
opacity: root.ghostOpacity
}
Layer {
id: body id: body
anchors.fill: parent
text: root.text
color: root.color color: root.color
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
} }
// Kick the ghosts outward briefly — used on track changes, mode flips, etc. // Kick the ghosts outward briefly — used on track changes, mode flips, etc.
@@ -83,14 +94,14 @@ Item {
SequentialAnimation { SequentialAnimation {
id: glitchAnim id: glitchAnim
NumberAnimation { NumberAnimation {
target: root; property: "split" target: root; property: "kick"
to: 5.5; duration: Theme.durFast to: 1; duration: Theme.durFast
easing.type: Easing.OutExpo easing.type: Easing.OutExpo
} }
NumberAnimation { NumberAnimation {
target: root; property: "split" target: root; property: "kick"
to: 1.6; duration: Theme.durSlow to: 0; duration: Theme.durSlow
easing.type: Easing.OutBack easing.type: Easing.OutCubic
} }
} }
} }
+8 -2
View File
@@ -1,11 +1,17 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
// Layer effect that recolours whatever it is applied to — used to tint the // Layer effect that recolours whatever it is applied to into a flat silhouette
// white texture tiles without shipping a copy per colour. // of `tintColor` — used for the white texture tiles and for tray icons.
//
// Colorization alone keeps the source's lightness, so a dark icon came out as a
// dark shade of the tint and all but vanished against the rail. Brightness runs
// first and lifts every pixel to white, which leaves colorization nothing to
// preserve but the alpha.
MultiEffect { MultiEffect {
property color tintColor: "white" property color tintColor: "white"
brightness: 1.0
colorization: 1.0 colorization: 1.0
colorizationColor: tintColor colorizationColor: tintColor
} }
+25 -18
View File
@@ -3,7 +3,7 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
// Mocha palette: Catppuccin Mocha ground, blue as the accent, pink as the // Mocha palette: Catppuccin Mocha ground, blue as the accent, mauve as the
// alarm. Persona geometry, softer colour. // alarm. Persona geometry, softer colour.
// //
// The Takemi branch ran monochrome-plus-crimson, where one hue carried both // The Takemi branch ran monochrome-plus-crimson, where one hue carried both
@@ -40,8 +40,10 @@ Singleton {
// ------------------------------------------------------------ THE KNOBS // ------------------------------------------------------------ THE KNOBS
// Two colours, two meanings, and nothing else in the shell carries hue. // Two colours, two meanings, and nothing else in the shell carries hue.
// //
// `accent` is blue: anything live, focused or selected. `alarm` is pink: // `accent` is blue: anything live, focused or selected. `alarm` is mauve:
// anything destructive or failed. Swap either line and everything derived // anything destructive, failed or critical. Things that are merely off —
// muted, disabled, do-not-disturb — carry no hue at all; see the `Off`
// treatment below. Swap either line and everything derived
// from it recolours. // from it recolours.
// //
// Keep them in step with `c.accent` / `c.alarm` in // Keep them in step with `c.accent` / `c.alarm` in
@@ -66,8 +68,8 @@ Singleton {
// ------------------------------------------------ semantic treatments // ------------------------------------------------ semantic treatments
// These tokens describe interaction meaning, not new palette entries. Each // These tokens describe interaction meaning, not new palette entries. Each
// state is composed only from Takemi's black, white, crimson, and neutral // state is composed only from crust, the neutral ramp, blue and mauve, so
// surfaces so callers do not invent one-off state colors. // callers do not invent one-off state colours.
readonly property color treatmentRestFill: root.surface readonly property color treatmentRestFill: root.surface
readonly property color treatmentRestMark: root.outline readonly property color treatmentRestMark: root.outline
readonly property color treatmentRestText: root.text readonly property color treatmentRestText: root.text
@@ -76,7 +78,11 @@ Singleton {
readonly property color treatmentHoverMark: root.accent readonly property color treatmentHoverMark: root.accent
readonly property color treatmentHoverText: root.text readonly property color treatmentHoverText: root.text
readonly property color treatmentFocusFill: root.text // Focus and selection are an inverted slab: solid blue with crust punched
// out of it (9:1). This used to be a white fill, which was right against
// Takemi's black but glares as a lavender-white block on Mocha — and it
// made "selected" a third look that was neither hover nor accent.
readonly property color treatmentFocusFill: root.accent
readonly property color treatmentFocusMark: root.ink readonly property color treatmentFocusMark: root.ink
readonly property color treatmentFocusText: root.ink readonly property color treatmentFocusText: root.ink
readonly property color treatmentSelectedFill: root.treatmentFocusFill readonly property color treatmentSelectedFill: root.treatmentFocusFill
@@ -112,16 +118,13 @@ Singleton {
readonly property color treatmentDisabledMark: root.outline readonly property color treatmentDisabledMark: root.outline
readonly property color treatmentDisabledText: root.muted readonly property color treatmentDisabledText: root.muted
// Semantic aliases. Still only two hues: a thing is fine (text), live // Off is a state the user chose — muted, do-not-disturb, radio switched
// (accent) or wrong (alarm). Nothing here introduces a third colour. // off. It is neither live nor wrong, so it gets no hue: painting a muted
readonly property color blue: root.accent // speaker blue said "active", and painting it mauve would say "broken".
readonly property color ok: root.text readonly property color treatmentOffFill: root.surface
readonly property color warn: root.alarm readonly property color treatmentOffMark: root.overlay
readonly property color treatmentOffText: root.muted
// Display text carries a blue and a mauve offset rather than an RGB split.
// Same trick, this palette's two colours.
readonly property color splitRed: root.accent
readonly property color splitCyan: root.alarm
function alpha(c: color, a: real): color { function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a); return Qt.rgba(c.r, c.g, c.b, a);
@@ -154,7 +157,9 @@ Singleton {
readonly property int weightDisplay: Font.Black readonly property int weightDisplay: Font.Black
readonly property int weightBody: Font.Medium readonly property int weightBody: Font.Medium
readonly property int fsMicro: 9 // Nothing in the shell renders below this. 9px mono was legible on
// crimson-on-black; on Mocha's lower-contrast ramp it was not.
readonly property int fsMicro: 10
readonly property int fsSmall: 11 readonly property int fsSmall: 11
readonly property int fsBody: 12 readonly property int fsBody: 12
readonly property int fsLarge: 15 readonly property int fsLarge: 15
@@ -162,6 +167,8 @@ Singleton {
readonly property int fsHuge: 34 readonly property int fsHuge: 34
readonly property int fsMega: 92 readonly property int fsMega: 92
readonly property int fsRailIcon: 17
// -------------------------------------------------------------- geometry // -------------------------------------------------------------- geometry
readonly property int barHeight: 40 readonly property int barHeight: 40
readonly property int barMargin: 8 readonly property int barMargin: 8
@@ -180,7 +187,7 @@ Singleton {
readonly property int cut: 10 readonly property int cut: 10
readonly property int stroke: 2 readonly property int stroke: 2
// Pure-black keyline and offset copy behind panels. This is the one piece of // Crust keyline and offset copy behind panels. This is the one piece of
// layering the style needs; it is not a soft shadow and never blurs. // layering the style needs; it is not a soft shadow and never blurs.
readonly property real dropOffset: 3 readonly property real dropOffset: 3
readonly property real keylineWidth: 2 readonly property real keylineWidth: 2
@@ -213,5 +220,5 @@ Singleton {
// which asks hyprpaper what is actually on screen — a path written down // which asks hyprpaper what is actually on screen — a path written down
// here goes stale the moment the wallpaper is switched, which is exactly // here goes stale the moment the wallpaper is switched, which is exactly
// what happened to the lock screen. // what happened to the lock screen.
readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/takemi_1.jpg" readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/arch.png"
} }
+1 -1
View File
@@ -36,7 +36,7 @@ Item {
} }
} }
// Hover is a crimson tick in the gutter rather than a second slab, so // Hover is an accent tick in the gutter rather than a second slab, so
// pointing at a row cannot be mistaken for selecting it. // pointing at a row cannot be mistaken for selecting it.
Skew { Skew {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
+1 -1
View File
@@ -68,7 +68,7 @@ Item {
// The same wedge-and-stripes ground the power menu stands on. The wedge // The same wedge-and-stripes ground the power menu stands on. The wedge
// sits under the clock, which is where the power menu puts it under the // sits under the clock, which is where the power menu puts it under the
// title — so the two screens land the crimson mass in the same place. // title — so the two screens land the accent mass in the same place.
P5Backdrop { P5Backdrop {
id: backdrop id: backdrop
anchors.fill: parent anchors.fill: parent
+7 -11
View File
@@ -66,10 +66,12 @@ PanelWindow {
readonly property color tone: { readonly property color tone: {
switch (root.kind) { switch (root.kind) {
case "mic": return Audio.micMuted ? Theme.accent : Theme.blue; // One tone for every level: the accent while the channel is live, the
// Turning the screen up is not a warning. White, like volume. // off grey while it is muted. Muted used to be the accent too, so a
case "brightness": return Theme.text; // silenced speaker flashed the same colour as a live one.
default: return Audio.muted ? Theme.accent : Theme.primary; case "mic": return Audio.micMuted ? Theme.treatmentOffText : Theme.accent;
case "brightness": return Theme.accent;
default: return Audio.muted ? Theme.treatmentOffText : Theme.accent;
} }
} }
@@ -104,15 +106,11 @@ PanelWindow {
anchors.margins: 4 anchors.margins: 4
fill: Theme.mantle fill: Theme.mantle
fillOpacity: 0.97 fillOpacity: 0.97
border: Theme.alpha(root.tone, 0.85) border: Theme.surfaceAlt
borderWidth: Theme.stroke borderWidth: Theme.stroke
slash: true slash: true
slashColor: root.tone slashColor: root.tone
slashWidth: 5 slashWidth: 5
halftone: true
halftoneColor: root.tone
halftoneOpacity: 0.08
sheen: true
lean: 0.09 lean: 0.09
chop: 14 chop: 14
padding: Theme.padM padding: Theme.padM
@@ -180,8 +178,6 @@ PanelWindow {
// widening readout only moves its own left edge. // widening readout only moves its own left edge.
text: Math.round(root.level * 100) + "%" text: Math.round(root.level * 100) + "%"
pixelSize: Theme.fsBody pixelSize: Theme.fsBody
split: 1
splitOpacity: 0.5
} }
} }
+2 -2
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components" import "root:/components"
import "root:/services" import "root:/services"
// Full-screen power menu. Crimson stripes sweep in behind a stack of leaning // Full-screen power menu. An accent wedge and stripes sweep in behind a stack of leaning
// buttons; Escape or a click on the backdrop dismisses it. // buttons; Escape or a click on the backdrop dismisses it.
PanelWindow { PanelWindow {
id: root id: root
@@ -67,7 +67,7 @@ PanelWindow {
Rectangle { Rectangle {
id: backdrop id: backdrop
anchors.fill: parent anchors.fill: parent
color: Theme.alpha(Theme.base, 0.82) color: Theme.alpha(Theme.base, 0.9)
opacity: 0 opacity: 0
TapHandler { TapHandler {
+5 -6
View File
@@ -13,9 +13,9 @@ PanelWindow {
required property var modelData required property var modelData
screen: modelData screen: modelData
// Only the primary bar screen shows toasts, to avoid duplicates. // A toast window exists on every screen, but each notification lands only
readonly property bool primaryScreen: Quickshell.screens.length === 0 // on the monitor that had focus when it arrived. Latched per toast rather
|| Quickshell.screens[0] === modelData // than live, so a card does not hop screens while it is being read.
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-toasts" WlrLayershell.namespace: "takemi-toasts"
@@ -31,7 +31,7 @@ PanelWindow {
implicitHeight: Math.max(1, stack.implicitHeight) implicitHeight: Math.max(1, stack.implicitHeight)
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
visible: primaryScreen && stack.children.length > 0 visible: root.live.length > 0
// Toasts currently on screen, newest first. // Toasts currently on screen, newest first.
property list<Notification> live: [] property list<Notification> live: []
@@ -39,9 +39,8 @@ PanelWindow {
Connections { Connections {
target: Notifs target: Notifs
enabled: root.primaryScreen
function onPopupRequested(notif) { function onPopupRequested(notif) {
if (Actions.focusedScreenName() !== root.modelData.name) return;
root.live = [notif, ...root.live].slice(0, root.maxLive); root.live = [notif, ...root.live].slice(0, root.maxLive);
} }
} }
+2 -2
View File
@@ -19,7 +19,7 @@ Item {
Icon { Icon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Audio.micIcon text: Audio.micIcon
color: Audio.micMuted ? Theme.accent : Theme.text color: Audio.micMuted ? Theme.treatmentOffText : Theme.text
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsLarge
} }
@@ -42,7 +42,7 @@ Item {
Text { Text {
text: Audio.micMuted ? "MUTED" : Audio.micPercent + "%" text: Audio.micMuted ? "MUTED" : Audio.micPercent + "%"
color: Audio.micMuted ? Theme.accent : Theme.subtext color: Audio.micMuted ? Theme.treatmentOffText : Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
+1 -1
View File
@@ -51,7 +51,7 @@ Item {
Text { Text {
text: Audio.muted ? "MUTED" : "LIVE" text: Audio.muted ? "MUTED" : "LIVE"
color: Audio.muted ? Theme.accent : Theme.text color: Audio.muted ? Theme.treatmentOffText : Theme.accent
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
+4 -94
View File
@@ -85,12 +85,12 @@ Popout {
Repeater { Repeater {
model: Audio.sinks.length > 1 ? Audio.sinks : [] model: Audio.sinks.length > 1 ? Audio.sinks : []
PickRow { ListRow {
required property var modelData required property var modelData
width: column.width width: column.width
label: modelData.nickname || modelData.description || modelData.name label: modelData.nickname || modelData.description || modelData.name
icon: "󰓃" icon: "󰓃"
selected: Audio.sink === modelData current: Audio.sink === modelData
onClicked: Audio.setSink(modelData) onClicked: Audio.setSink(modelData)
} }
} }
@@ -147,7 +147,7 @@ Popout {
id: levelIcon id: levelIcon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: levelRow.icon text: levelRow.icon
color: levelRow.muted ? Theme.accent : levelRow.accent color: levelRow.muted ? Theme.treatmentOffText : levelRow.accent
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsLarge
TapHandler { onTapped: levelRow.toggled() } TapHandler { onTapped: levelRow.toggled() }
@@ -171,7 +171,7 @@ Popout {
id: readoutText id: readoutText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: levelRow.readout text: levelRow.readout
color: levelRow.muted ? Theme.accent : Theme.subtext color: levelRow.muted ? Theme.treatmentOffText : Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
@@ -189,94 +189,4 @@ Popout {
onMoved: v => levelRow.moved(v) onMoved: v => levelRow.moved(v)
} }
} }
component SectionRule: Item {
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: sectionLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: parent.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: sectionLabel.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.6)
}
}
component PickRow: Item {
id: pickRow
property string label: ""
property string icon: ""
property bool selected: false
signal clicked()
implicitHeight: 26
Rectangle {
anchors.fill: parent
color: pickHover.hovered
? Theme.alpha(Theme.primary, 0.14)
: (pickRow.selected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: pickRow.selected ? 18 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.icon
color: pickRow.selected ? Theme.primary : Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.label
color: pickRow.selected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
renderType: Text.NativeRendering
}
}
HoverHandler {
id: pickHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: pickRow.clicked()
}
}
} }
+1 -5
View File
@@ -52,8 +52,6 @@ Popout {
text: Math.round(Battery.percent) + "%" text: Math.round(Battery.percent) + "%"
pixelSize: Theme.fsHuge pixelSize: Theme.fsHuge
color: Battery.tint color: Battery.tint
split: 2.5
splitOpacity: 0.7
} }
} }
@@ -68,8 +66,6 @@ Popout {
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
text: Battery.timeLabel text: Battery.timeLabel
pixelSize: Theme.fsBody pixelSize: Theme.fsBody
split: 1.1
splitOpacity: 0.55
} }
Text { Text {
@@ -89,7 +85,7 @@ Popout {
text: "Health " + Math.round(Battery.health) + "%" text: "Health " + Math.round(Battery.health) + "%"
anchors.right: parent.right anchors.right: parent.right
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
color: Battery.health < 70 ? Theme.warn : Theme.muted color: Battery.health < 70 ? Theme.alarm : Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering renderType: Text.NativeRendering
+20 -96
View File
@@ -20,7 +20,7 @@ Popout {
PopoutSurface { PopoutSurface {
anchors.fill: parent anchors.fill: parent
tone: Bt.enabled ? Theme.blue : Theme.muted tone: Bt.enabled ? Theme.accent : Theme.muted
PopoutHeader { PopoutHeader {
id: header id: header
@@ -29,7 +29,7 @@ Popout {
anchors.right: parent.right anchors.right: parent.right
title: "Bluetooth" title: "Bluetooth"
icon: Bt.icon icon: Bt.icon
accent: Theme.blue accent: Theme.accent
subtitle: Bt.adapter ? Bt.adapter.name : "NO ADAPTER" subtitle: Bt.adapter ? Bt.adapter.name : "NO ADAPTER"
} }
@@ -43,7 +43,7 @@ Popout {
P5Button { P5Button {
text: Bt.enabled ? "On" : "Off" text: Bt.enabled ? "On" : "Off"
icon: Bt.enabled ? "󰂯" : "󰂲" icon: Bt.enabled ? "󰂯" : "󰂲"
accent: Bt.enabled ? Theme.blue : Theme.muted accent: Bt.enabled ? Theme.accent : Theme.muted
onClicked: Bt.toggle() onClicked: Bt.toggle()
} }
@@ -55,17 +55,13 @@ Popout {
} }
} }
Text { SectionRule {
id: listLabel id: listLabel
anchors.top: controls.bottom anchors.top: controls.bottom
anchors.topMargin: Theme.padM anchors.topMargin: Theme.padM
anchors.left: parent.left anchors.left: parent.left
text: Bt.devices.length > 0 ? "DEVICES" : "NO DEVICES" anchors.right: parent.right
color: Theme.muted text: Bt.devices.length > 0 ? "Devices" : "No devices"
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
} }
ListView { ListView {
@@ -78,92 +74,36 @@ Popout {
spacing: 2 spacing: 2
model: Bt.devices model: Bt.devices
delegate: Item { delegate: ListRow {
id: devRow id: devRow
required property BluetoothDevice modelData required property BluetoothDevice modelData
width: ListView.view.width width: ListView.view.width
height: 34 icon: Bt.deviceIcon(modelData)
label: Bt.deviceLabel(modelData)
Rectangle { detail: {
anchors.fill: parent if (modelData.pairing) return "pairing…";
color: devHover.hovered if (modelData.connected) return "connected";
? Theme.alpha(Theme.blue, 0.16) if (modelData.paired || modelData.bonded) return "paired";
: (devRow.modelData.connected ? Theme.alpha(Theme.blue, 0.08) : "transparent") return modelData.address;
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: devRow.modelData.connected ? 22 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Bt.deviceIcon(devRow.modelData)
color: devRow.modelData.connected ? Theme.blue : Theme.subtext
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -2
Text {
text: Bt.deviceLabel(devRow.modelData)
color: devRow.modelData.connected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: devRow.modelData.connected ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Text {
text: {
if (devRow.modelData.pairing) return "pairing…";
if (devRow.modelData.connected) return "connected";
if (devRow.modelData.paired || devRow.modelData.bonded) return "paired";
return devRow.modelData.address;
}
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
renderType: Text.NativeRendering
}
}
} }
current: modelData.connected
onClicked: Bt.toggleDevice(modelData)
onRightClicked: if (modelData.paired) modelData.forget()
// Device battery, where BlueZ exposes it. // Device battery, where BlueZ exposes it.
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: 3
visible: devRow.modelData.batteryAvailable
Icon { Icon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: devRow.modelData.batteryAvailable
text: "󰥉" text: "󰥉"
color: devRow.modelData.battery > 0.25 ? Theme.ok : Theme.warn color: devRow.modelData.battery > 0.25 ? Theme.subtext : Theme.alarm
font.pixelSize: Theme.fsSmall font.pixelSize: Theme.fsSmall
} }
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: devRow.modelData.batteryAvailable
text: Math.round(devRow.modelData.battery * 100) + "%" text: Math.round(devRow.modelData.battery * 100) + "%"
color: Theme.subtext color: Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
@@ -171,22 +111,6 @@ Popout {
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
} }
HoverHandler {
id: devHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: Bt.toggleDevice(devRow.modelData)
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: if (devRow.modelData.paired) devRow.modelData.forget()
}
}
} }
} }
} }
+9 -9
View File
@@ -4,7 +4,7 @@ import "root:/config"
import "root:/components" import "root:/components"
import "root:/services" import "root:/services"
// Month grid with the current day slashed in crimson, plus uptime and a couple // Month grid with the current day slashed in the accent, plus uptime and a couple
// of world clocks. // of world clocks.
Popout { Popout {
id: root id: root
@@ -99,9 +99,9 @@ Popout {
width: dayNames.width / 7 width: dayNames.width / 7
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: modelData text: modelData
color: index >= 5 ? Theme.alpha(Theme.accent, 0.8) : Theme.muted color: index >= 5 ? Theme.subtext : Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1 font.letterSpacing: 1
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -126,7 +126,7 @@ Popout {
width: grid.width / 7 width: grid.width / 7
height: 30 height: 30
// Today gets a leaning crimson slab. // Today gets a leaning accent slab, inked like every selected slab.
Skew { Skew {
anchors.centerIn: parent anchors.centerIn: parent
width: 26 width: 26
@@ -139,7 +139,7 @@ Popout {
anchors.centerIn: parent anchors.centerIn: parent
text: cell.modelData.day text: cell.modelData.day
color: { color: {
if (cell.modelData.today) return Theme.text; if (cell.modelData.today) return Theme.treatmentSelectedText;
if (!cell.modelData.inMonth) return Theme.alpha(Theme.muted, 0.45); if (!cell.modelData.inMonth) return Theme.alpha(Theme.muted, 0.45);
return Theme.subtext; return Theme.subtext;
} }
@@ -167,7 +167,7 @@ Popout {
text: "UPTIME" text: "UPTIME"
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5 font.letterSpacing: 1.5
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -191,7 +191,7 @@ Popout {
text: "TOKYO" text: "TOKYO"
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5 font.letterSpacing: 1.5
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -226,9 +226,9 @@ Popout {
Skew { Skew {
anchors.fill: parent anchors.fill: parent
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent" color: stepHover.hovered ? Theme.treatmentHoverFill : "transparent"
borderWidth: 1 borderWidth: 1
borderColor: Theme.alpha(Theme.outline, 0.7) borderColor: stepHover.hovered ? Theme.treatmentHoverMark : Theme.alpha(Theme.outline, 0.7)
} }
Icon { Icon {
+15 -11
View File
@@ -80,7 +80,7 @@ Popout {
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
fillColor: Theme.surface fillColor: Theme.surface
strokeColor: Theme.alpha(Theme.primary, 0.5) strokeColor: Theme.surfaceAlt
strokeWidth: 2 strokeWidth: 2
PathPolyline { PathPolyline {
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3]) path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
@@ -93,7 +93,7 @@ Popout {
visible: art.status !== Image.Ready visible: art.status !== Image.Ready
text: Media.sourceIcon text: Media.sourceIcon
font.pixelSize: 42 font.pixelSize: 42
color: Theme.alpha(Theme.primary, 0.7) color: Theme.muted
} }
// Spinning rule around the art while playing. // Spinning rule around the art while playing.
@@ -265,14 +265,14 @@ Popout {
: (chip.activeFocus : (chip.activeFocus
? Theme.treatmentFocusFill ? Theme.treatmentFocusFill
: (chip.current : (chip.current
? Theme.alpha(Theme.primary, 0.22) ? Theme.treatmentActiveFill
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent"))) : (chipHover.hovered ? Theme.treatmentHoverFill : "transparent")))
borderWidth: chip.activeFocus ? Theme.stroke : 1 borderWidth: chip.activeFocus ? Theme.stroke : 1
borderColor: !chip.enabled borderColor: !chip.enabled
? Theme.treatmentDisabledMark ? Theme.treatmentDisabledMark
: (chip.activeFocus : (chip.activeFocus
? Theme.treatmentFocusMark ? Theme.treatmentFocusMark
: (chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7))) : (chip.current ? Theme.treatmentActiveMark : Theme.alpha(Theme.outline, 0.7)))
} }
Text { Text {
@@ -283,7 +283,7 @@ Popout {
? Theme.treatmentDisabledText ? Theme.treatmentDisabledText
: (chip.activeFocus : (chip.activeFocus
? Theme.treatmentFocusText ? Theme.treatmentFocusText
: (chip.current ? Theme.text : Theme.muted)) : (chip.current || chipHover.hovered ? Theme.text : Theme.subtext))
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering renderType: Text.NativeRendering
@@ -334,14 +334,17 @@ Popout {
color: { color: {
if (!tb.enabled) return "transparent"; if (!tb.enabled) return "transparent";
if (tb.activeFocus) return Theme.treatmentFocusFill; if (tb.activeFocus) return Theme.treatmentFocusFill;
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22); // The play button is the one solid control on the panel: a
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent"; // surface slab at rest, the accent slab under the pointer. The
// others stay bare until hovered.
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.treatmentHoverFill;
return tb.primary ? Theme.surface : "transparent";
} }
borderWidth: tb.activeFocus ? Theme.stroke * 2 : (tb.primary ? 2 : 1) borderWidth: tb.activeFocus ? Theme.stroke * 2 : (tb.primary ? 2 : 1)
borderColor: tb.enabled borderColor: tb.enabled
? (tb.activeFocus ? (tb.activeFocus
? Theme.treatmentFocusMark ? Theme.treatmentFocusMark
: (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))) : (tbHover.hovered ? Theme.treatmentHoverMark : (tb.primary ? Theme.overlay : Theme.surfaceAlt)))
: Theme.alpha(Theme.outline, 0.4) : Theme.alpha(Theme.outline, 0.4)
Behavior on color { Behavior on color {
@@ -356,8 +359,9 @@ Popout {
color: { color: {
if (!tb.enabled) return Theme.alpha(Theme.muted, 0.5); if (!tb.enabled) return Theme.alpha(Theme.muted, 0.5);
if (tb.activeFocus) return Theme.treatmentFocusText; if (tb.activeFocus) return Theme.treatmentFocusText;
if (tbHover.hovered && tb.primary) return Theme.text; // Ink on the accent slab. White on light blue was barely 2:1.
return Theme.primary; if (tbHover.hovered && tb.primary) return Theme.ink;
return Theme.text;
} }
} }
+27 -81
View File
@@ -76,17 +76,13 @@ Popout {
} }
} }
Text { SectionRule {
id: apsLabel id: apsLabel
anchors.top: controls.bottom anchors.top: controls.bottom
anchors.topMargin: Theme.padM anchors.topMargin: Theme.padM
anchors.left: parent.left anchors.left: parent.left
text: Net.scanning ? "SCANNING…" : "NETWORKS" anchors.right: parent.right
color: Theme.muted text: Net.scanning ? "Scanning…" : "Networks"
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
} }
ListView { ListView {
@@ -99,85 +95,17 @@ Popout {
spacing: 2 spacing: 2
model: Net.wifiNetworks model: Net.wifiNetworks
delegate: Item { delegate: ListRow {
id: apRow id: apRow
required property var modelData required property var modelData
width: ListView.view.width width: ListView.view.width
height: 30 icon: Net.signalIcon(modelData.signalStrength ?? 0)
label: modelData.name
current: modelData.connected
Rectangle { onClicked: {
anchors.fill: parent
color: apHover.hovered
? Theme.alpha(Theme.primary, 0.14)
: (apRow.modelData.connected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: apRow.modelData.connected ? 20 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Net.signalIcon(apRow.modelData.signalStrength ?? 0)
color: apRow.modelData.connected ? Theme.primary : Theme.subtext
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.name
color: apRow.modelData.connected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: apRow.modelData.connected ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: Net.secured(apRow.modelData)
text: "󰌾"
color: Theme.muted
font.pixelSize: Theme.fsMicro
}
}
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.known ? "SAVED" : ""
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1
renderType: Text.NativeRendering
}
HoverHandler {
id: apHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
if (apRow.modelData.connected) { if (apRow.modelData.connected) {
apRow.modelData.disconnect(); apRow.modelData.disconnect();
} else { } else {
@@ -187,6 +115,24 @@ Popout {
apRow.modelData.connect(); apRow.modelData.connect();
} }
} }
Text {
anchors.verticalCenter: parent.verticalCenter
visible: apRow.modelData.known
text: "SAVED"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: Net.secured(apRow.modelData)
text: "󰌾"
color: Theme.muted
font.pixelSize: Theme.fsSmall
} }
} }
} }
@@ -228,7 +174,7 @@ Popout {
text: rateBlock.label.toUpperCase() text: rateBlock.label.toUpperCase()
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5 font.letterSpacing: 1.5
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
+1 -1
View File
@@ -37,7 +37,7 @@ Popout {
P5Button { P5Button {
text: Notifs.dnd ? "Silenced" : "Silence" text: Notifs.dnd ? "Silenced" : "Silence"
icon: Notifs.dnd ? "󰂛" : "󰂚" icon: Notifs.dnd ? "󰂛" : "󰂚"
accent: Notifs.dnd ? Theme.accent : Theme.primary accent: Notifs.dnd ? Theme.treatmentOffText : Theme.primary
onClicked: Notifs.toggleDnd() onClicked: Notifs.toggleDnd()
} }
+3 -5
View File
@@ -2,8 +2,8 @@ import QtQuick
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// Title bar for a popout: glyph, split-ink heading, right-aligned status text, // Title bar for a popout: glyph, print-shadowed heading, right-aligned status
// and a crimson rule with a chopped tail. // text, and a rule with a chopped accent tail.
Item { Item {
id: root id: root
@@ -32,8 +32,6 @@ Item {
text: root.title.toUpperCase() text: root.title.toUpperCase()
pixelSize: Theme.fsLarge pixelSize: Theme.fsLarge
letterSpacing: 1.5 letterSpacing: 1.5
split: 1.3
splitOpacity: 0.7
} }
} }
@@ -43,7 +41,7 @@ Item {
anchors.topMargin: 6 anchors.topMargin: 6
visible: root.subtitle !== "" visible: root.subtitle !== ""
text: root.subtitle text: root.subtitle
color: Theme.muted color: Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.letterSpacing: 1 font.letterSpacing: 1
+5 -1
View File
@@ -10,7 +10,11 @@ P5Panel {
fill: Theme.mantle fill: Theme.mantle
fillOpacity: 0.97 fillOpacity: 0.97
border: Theme.alpha(tone, 0.75) // A neutral keyline. `tone` used to colour the whole border at 75%, and
// since most panels pass the resting text colour that put a pale 2px frame
// around every popout — the loudest thing on screen for no reason. The
// tone lives in the slash now; the frame is structure.
border: Theme.surfaceAlt
borderWidth: Theme.stroke borderWidth: Theme.stroke
halftone: textured halftone: textured
halftoneColor: tone halftoneColor: tone
+1 -1
View File
@@ -107,7 +107,7 @@ Column {
height: row.entry.isSeparator ? 0 : 26 height: row.entry.isSeparator ? 0 : 26
visible: !row.entry.isSeparator visible: !row.entry.isSeparator
// Hover is a solid crimson block, the same statement the // Hover is a solid accent block, the same statement the
// workspace ticks and the rail icons make. // workspace ticks and the rail icons make.
Skew { Skew {
anchors.fill: parent anchors.fill: parent
+26
View File
@@ -17,6 +17,7 @@ Singleton {
property bool lockRequested: false property bool lockRequested: false
property bool sidebarOpen: false property bool sidebarOpen: false
property bool launcherOpen: false property bool launcherOpen: false
property bool askOpen: false
// Seed text for the next launcher open. Lets a keybind or a script hand the // Seed text for the next launcher open. Lets a keybind or a script hand the
// launcher a query it did not have to type — `qs ipc call shell prompt // launcher a query it did not have to type — `qs ipc call shell prompt
@@ -32,6 +33,13 @@ Singleton {
// Same story for the power menu: a window per screen, one of them wanted. // Same story for the power menu: a window per screen, one of them wanted.
property string powerMenuScreen: "" property string powerMenuScreen: ""
// …and for the ask panel, which also takes the keyboard.
property string askScreen: ""
// Seed text for the next ask open. A seeded open asks straight away, so
// `qs ipc call shell askAbout "$(wl-paste)"` is a one-shot question.
property string askSeed: ""
signal osdRequested(string kind) signal osdRequested(string kind)
function togglePowerMenu() { function togglePowerMenu() {
@@ -57,6 +65,22 @@ Singleton {
} }
function closeLauncher() { root.launcherOpen = false; } function closeLauncher() { root.launcherOpen = false; }
function toggleAsk() {
if (!root.askOpen) {
root.askSeed = "";
root.askScreen = root.focusedScreenName();
}
root.askOpen = !root.askOpen;
}
function closeAsk() { root.askOpen = false; }
function askAbout(seed: string) {
root.askSeed = seed;
root.askScreen = root.focusedScreenName();
root.askOpen = false;
root.askOpen = true;
}
// Stand in for `fuzzel --dmenu`. See Apps.beginDmenu for the transport. // Stand in for `fuzzel --dmenu`. See Apps.beginDmenu for the transport.
function dmenuLauncher(inPath: string, outPath: string, prompt: string) { function dmenuLauncher(inPath: string, outPath: string, prompt: string) {
Apps.beginDmenu(inPath, outPath, prompt); Apps.beginDmenu(inPath, outPath, prompt);
@@ -106,6 +130,8 @@ Singleton {
function lock(): void { root.lock(); } function lock(): void { root.lock(); }
function power(): void { root.togglePowerMenu(); } function power(): void { root.togglePowerMenu(); }
function launcher(): void { root.toggleLauncher(); } function launcher(): void { root.toggleLauncher(); }
function ask(): void { root.toggleAsk(); }
function askAbout(question: string): void { root.askAbout(question); }
function prompt(seed: string): void { root.promptLauncher(seed); } function prompt(seed: string): void { root.promptLauncher(seed); }
function dmenu(inPath: string, outPath: string, prompt: string): void { function dmenu(inPath: string, outPath: string, prompt: string): void {
root.dmenuLauncher(inPath, outPath, prompt); root.dmenuLauncher(inPath, outPath, prompt);
+248
View File
@@ -0,0 +1,248 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Ask — the question panel's brain.
//
// One `claude -p` per turn, streamed. The CLI is the transport rather than the
// API because the subscription lives in the CLI's credentials: nothing here
// ever sees a key, and nothing here has to hold one.
//
// The defaults deliberately strip Claude Code down to a chat: no settings
// sources, no MCP servers, no project files. That is not only about tokens —
// a session started from the shell would otherwise inherit whatever hooks and
// CLAUDE.md the last project left lying around, and answer in its voice.
// Everything below is overridable from ~/.config/quickshell/ask.json.
Singleton {
id: root
// ---------------------------------------------------------------- config
readonly property string configPath:
Quickshell.env("HOME") + "/.config/quickshell/ask.json"
property var config: ({})
readonly property var defaults: ({
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,
// "" = none. "user,project,local" is the full Claude Code behaviour.
settingSources: "",
allowedTools: ["WebSearch", "WebFetch"],
disallowedTools: ["Bash", "Edit", "Write", "NotebookEdit", "Agent", "Task"],
workingDirectory: Quickshell.env("HOME"),
maxTurns: 6
})
function setting(key: string): var {
const v = root.config[key];
return v === undefined || v === null ? root.defaults[key] : v;
}
readonly property string model: root.setting("model")
// The model ring. Ctrl+M walks it; whatever ask.json asks for is row zero,
// so a custom model stays reachable even when it is not one of the three.
readonly property var models: {
const ring = ["sonnet", "opus", "haiku"];
const m = root.model;
return ring.indexOf(m) >= 0 ? ring : [m].concat(ring);
}
function cycleModel(): void {
const ring = root.models;
const next = ring[(ring.indexOf(root.model) + 1) % ring.length];
const c = JSON.parse(JSON.stringify(root.config));
c.model = next;
root.config = c;
store.setText(JSON.stringify(c, null, 2) + "\n");
}
FileView {
id: store
path: root.configPath
preload: true
watchChanges: true
printErrors: false
atomicWrites: true
onFileChanged: store.reload()
onLoaded: {
try {
root.config = JSON.parse(store.text()) || ({});
} catch (e) {
root.config = ({});
root.error = "ask.json is not valid JSON";
}
}
// No file yet: write the defaults out so there is something to edit.
onLoadFailed: error => {
root.config = ({});
store.setText(JSON.stringify(root.defaults, null, 2) + "\n");
}
}
// ----------------------------------------------------------------- state
// `turns` is the transcript the panel draws: { role, text } in order. The
// answer being streamed is the last entry and is rewritten in place.
property var turns: []
property bool busy: false
property string error: ""
property string sessionId: ""
property real lastCost: 0
property int exitCode: 0
property string stderrText: ""
signal answered()
function clear(): void {
root.stop();
root.turns = [];
root.sessionId = "";
root.error = "";
root.lastCost = 0;
}
function stop(): void {
if (proc.running) proc.running = false;
root.busy = false;
}
function ask(question: string): void {
const q = question.trim();
if (q === "" || root.busy) return;
root.error = "";
root.turns = root.turns.concat([
{ role: "user", text: q },
{ role: "assistant", text: "" }
]);
root.busy = true;
root.exitCode = 0;
root.stderrText = "";
proc.running = false;
// stdin is closed explicitly. Inherited from the shell it never ends,
// and the CLI waits three seconds for input that is never coming
// before it starts on the question.
proc.command = ["sh", "-c", 'exec claude "$@" </dev/null', "claude"]
.concat(root.argv(q).slice(1));
proc.running = true;
}
// Arguments, not a shell line: the question is a single argv entry, so a
// question containing quotes or `$(…)` is data and stays data.
function argv(question: string): var {
const args = ["claude", "-p", question,
"--model", root.setting("model"),
"--output-format", "stream-json",
"--include-partial-messages",
"--verbose",
"--setting-sources", root.setting("settingSources"),
"--strict-mcp-config",
"--mcp-config", '{"mcpServers":{}}',
"--max-turns", String(root.setting("maxTurns"))];
const system = root.setting("systemPrompt");
if (system !== "") args.push("--system-prompt", system);
const fallback = root.setting("fallbackModel");
if (fallback !== "") args.push("--fallback-model", fallback);
const allowed = root.setting("allowedTools");
if (allowed.length > 0) args.push("--allowed-tools", allowed.join(","));
const disallowed = root.setting("disallowedTools");
if (disallowed.length > 0) args.push("--disallowed-tools", disallowed.join(","));
// Continuity is a resume, not a re-send: the CLI keeps the transcript,
// so a follow-up costs one question rather than the whole thread.
if (root.setting("keepContext") && root.sessionId !== "")
args.push("--resume", root.sessionId);
return args;
}
// The streamed answer lives in the last turn. Rewriting an array element in
// place does not notify, so the array is rebuilt — the transcript is a
// handful of entries, and this keeps the panel's binding honest.
function _append(text: string): void {
const t = root.turns.slice();
const last = t.length - 1;
if (last < 0 || t[last].role !== "assistant") return;
t[last] = { role: "assistant", text: t[last].text + text };
root.turns = t;
}
function _ingest(line: string): void {
if (line === "") return;
let ev;
try {
ev = JSON.parse(line);
} catch (e) {
return; // partial line, or a stray log
}
switch (ev.type) {
case "system":
if (ev.subtype === "init" && ev.session_id) root.sessionId = ev.session_id;
return;
case "stream_event": {
const inner = ev.event;
if (inner?.type === "content_block_delta" && inner.delta?.type === "text_delta")
root._append(inner.delta.text);
return;
}
case "result":
root.busy = false;
if (ev.session_id) root.sessionId = ev.session_id;
if (typeof ev.total_cost_usd === "number") root.lastCost = ev.total_cost_usd;
if (ev.is_error) root.error = ev.result || "the request failed";
// A turn that streamed nothing (a refusal, a rate limit) still has
// an answer in the result field. Use it rather than showing a gap.
else if (root.turns.length > 0
&& root.turns[root.turns.length - 1].text === ""
&& ev.result)
root._append(ev.result);
root.answered();
return;
}
}
Process {
id: proc
// Somewhere harmless and stable. The CLI still reads the directory it
// is started in for project context; the shell's own is whatever
// Hyprland handed it at login.
workingDirectory: root.setting("workingDirectory")
stdout: SplitParser {
onRead: data => root._ingest(data)
}
// Only shown when the run actually failed. The CLI writes advisory
// warnings here on a good run too, and a warning in red under the
// answer reads as the answer being wrong.
stderr: StdioCollector {
onStreamFinished: root.stderrText = this.text.trim();
}
onExited: (code, status) => {
root.busy = false;
root.exitCode = code;
if (code === 0 || root.error !== "") return;
const lines = root.stderrText.split("\n").filter(l => l.trim() !== "");
root.error = lines.length > 0
? lines[lines.length - 1]
: "claude exited with " + code;
}
}
}
+2 -2
View File
@@ -58,9 +58,9 @@ Singleton {
} }
readonly property color tint: { readonly property color tint: {
if (root.charging || root.full) return Theme.ok; if (root.charging || root.full) return Theme.text;
if (root.critical) return Theme.alarm; if (root.critical) return Theme.alarm;
if (root.low) return Theme.warn; if (root.low) return Theme.alarm;
return Theme.primary; return Theme.primary;
} }
+8
View File
@@ -4,6 +4,7 @@ import "root:/config"
import "root:/services" import "root:/services"
import "root:/bar" import "root:/bar"
import "root:/launcher" import "root:/launcher"
import "root:/ask"
import "root:/overlays" import "root:/overlays"
import "root:/lock" import "root:/lock"
@@ -12,6 +13,7 @@ import "root:/lock"
// bar/ the top bar and its modules // bar/ the top bar and its modules
// popouts/ hover panels hanging off bar modules // popouts/ hover panels hanging off bar modules
// launcher/ the application launcher // launcher/ the application launcher
// ask/ the question panel, dropped from the top edge
// overlays/ power menu, notification toasts, OSD // overlays/ power menu, notification toasts, OSD
// lock/ session lock // lock/ session lock
// services/ data sources (fluxo, pipewire, bluez, upower, mpris, …) // services/ data sources (fluxo, pipewire, bluez, upower, mpris, …)
@@ -51,6 +53,12 @@ ShellRoot {
Launcher {} Launcher {}
} }
Variants {
model: Quickshell.screens
AskPanel {}
}
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens
@@ -0,0 +1,12 @@
# Written by `claude-account router install`. Do not edit by hand.
[Unit]
Description=Router for <name>.headroom.localhost
After=network-online.target
[Service]
ExecStart=/home/narl/.local/bin/claude-account router serve --port 80
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
+13
View File
@@ -0,0 +1,13 @@
# Written by `claude-account headroom install`. Do not edit by hand.
[Unit]
Description=Headroom proxy for the %i Claude account
After=network-online.target
[Service]
EnvironmentFile=%h/.config/claude-account/headroom-%i.env
ExecStart=/home/narl/.local/share/headroom-venv/bin/headroom proxy --host 127.0.0.1 --port ${CLAUDE_ACCOUNT_PORT} --mode cache --no-telemetry
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
@@ -0,0 +1 @@
/home/narl/.config/systemd/user/claude-account-router.service
@@ -0,0 +1 @@
/home/narl/.config/systemd/user/claude-headroom@.service
@@ -0,0 +1 @@
/home/narl/.config/systemd/user/claude-headroom@.service
@@ -1 +0,0 @@
/home/narl/.config/systemd/user/enqs-headroom.service
@@ -1 +0,0 @@
/home/narl/.config/systemd/user/headroom-private.service
@@ -1 +0,0 @@
/home/narl/.config/systemd/user/headroom-work.service
+1
View File
@@ -0,0 +1 @@
/usr/lib/systemd/user/ydotool.service
-13
View File
@@ -1,13 +0,0 @@
[Unit]
Description=Headroom proxy (private Claude profile)
After=network-online.target
[Service]
Environment=HEADROOM_WORKSPACE_DIR=%h/.headroom
Environment=HEADROOM_TELEMETRY=off
ExecStart=%h/.local/share/headroom-venv/bin/headroom proxy --host 127.0.0.1 --port 8787 --mode cache --no-telemetry
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
-13
View File
@@ -1,13 +0,0 @@
[Unit]
Description=Headroom proxy (work Claude profile)
After=network-online.target
[Service]
Environment=HEADROOM_WORKSPACE_DIR=%h/.headroom-work
Environment=HEADROOM_TELEMETRY=off
ExecStart=%h/.local/share/headroom-venv/bin/headroom proxy --host 127.0.0.1 --port 8788 --mode cache --no-telemetry
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target