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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 01:53:56 +02:00
nvrlandClaude Opus 5 1cf5a4b68d chore: add enqs-headroom user service, set nvim as text/plain handler
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 01:52:50 +02:00
86 changed files with 1877 additions and 1189 deletions

No files matched your search

+16
View File
@@ -28,3 +28,19 @@
fish/fish_variables
waybar/fluxo-rs
!fluxo/
# Desktop theming. These live outside the per-app configs above, but they are
# what makes an app that is neither GTK-aware nor shell-aware match the rice,
# so the branch carries them.
!gtk-3.0/
gtk-3.0/*
!gtk-3.0/settings.ini
!gtk-3.0/gtk.css
!gtk-4.0/
gtk-4.0/*
!gtk-4.0/settings.ini
!gtk-4.0/gtk.css
!xsettingsd/
!qt6ct/
qt6ct/*
!qt6ct/qt6ct.conf
+1 -1
View File
@@ -107,7 +107,7 @@ ipc_socket = true
## GENERAL --------------------------------------------------------
## Import additional configuration files.
import = ["~/.config/alacritty/takemi.toml", "~/.config/alacritty/fonts.toml"]
import = ["~/.config/alacritty/catppuccin-mocha.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]]
key = "Return"
+4 -2
View File
@@ -4,9 +4,11 @@ foreground = "#cdd6f4"
dim_foreground = "#7f849c"
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]
text = "#1e1e2e"
cursor = "#f5e0dc"
text = "#11111b"
cursor = "#89b4fa"
[colors.vi_mode_cursor]
text = "#1e1e2e"
+2 -2
View File
@@ -1,10 +1,10 @@
[global]
corner_radius = 4
transparency = 90
frame_color = "#f38ba8"
frame_color = "#89b4fa"
frame_width = 1
separator_color= frame
highlight = "#89dceb"
highlight = "#cba6f7"
[urgency_low]
background = "#1e1e2e"
+100
View File
@@ -0,0 +1,100 @@
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_claude_account_global_optspecs
string join \n h/help V/version
end
function __fish_claude_account_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_claude_account_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end
function __fish_claude_account_using_subcommand
set -l cmd (__fish_claude_account_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end
complete -c claude-account -n "__fish_claude_account_needs_command" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_needs_command" -s V -l version -d 'Print version'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "status" -d 'Show every account, every proxy, every name and everything that is wrong'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "list" -d 'List the accounts, one per line'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "switch" -d 'Make an account the one `claude` runs as'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "add" -d 'Register another account, with a profile and a proxy of its own'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "remove" -d 'Forget an account. Its profile directory is left where it is'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "current" -d 'Print the name of the active account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "dir" -d 'Print the profile directory of an account, for `CLAUDE_CONFIG_DIR`'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "env" -d 'Print the shell lines that put this shell into an account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "url" -d 'Print the address a name resolves to'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "setup" -d 'Install everything at once: the wrapper, a proxy per account, the router'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "adopt" -d 'Take over the proxy services that were written by hand'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "doctor" -d 'Report every problem and exit non-zero when there is one'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "shim" -d 'The `claude` wrapper in ~/.local/bin'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "headroom" -d 'The proxy of one account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "router" -d 'The router that resolves <name>.headroom.localhost'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "completions" -d 'Tab completion, with the account names in it'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s v -l verbose -d 'Add the last lines each proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -l names -d 'Print the names alone, for scripts and for tab completion'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand switch" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l dir -d 'A profile directory elsewhere' -r -F
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l port -d 'A port for its proxy; the next free one by default' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l switch -d 'Make it the account `claude` runs as straight away'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand remove" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand current" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand dir" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand env" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand url" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand setup" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand adopt" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand doctor" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "install" -d 'Write ~/.local/bin/claude'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "uninstall" -d 'Remove it again'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -l force -d 'Overwrite a `claude` that this tool did not write'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "install" -d 'Write the service for an account and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "start" -d 'Start the proxy of an account'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "stop" -d 'Stop it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "restart" -d 'Stop and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "port" -d 'Give an account another port, and write it everywhere it is named'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "logs" -d 'Print what one proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from start" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from stop" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from restart" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s n -l lines -d 'How many lines' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "serve" -d 'Serve the names until stopped'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "install" -d 'Install the router as a user service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "port" -d 'Move the router to another port'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "list" -d 'List the names that resolve'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -l port -r
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -l install -d 'Write the file where the shell reads it, instead of printing it'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -s h -l help -d 'Print help'
# The accounts as they are right now, asked for when Tab is pressed.
complete -c claude-account -n '__fish_seen_subcommand_from switch dir env url remove start stop restart logs install uninstall port' -f -a '(claude-account list --names)'
+43 -41
View File
@@ -1,50 +1,52 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua.
# Mocha — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/catppuccin-mocha.toml and ~/.config/nvim/lua/takemi.lua.
#
# Replaces the catppuccin fish_frozen_theme.fish that fish 4.3 generated on
# upgrade. Same content as themes/Takemi.theme — that copy exists only so
# `fish_config` can preview it; this one is what actually loads.
# Catppuccin Mocha's own fish theme (themes/Catppuccin Mocha.theme), with the
# selection and pager rows carried over from the Takemi layout: a solid accent
# block with crust punched out of it, rather than a tinted surface. The shell
# selects with a blue slab and marks with mauve; the prompt should not
# disagree.
#
# Note that `fish_config` writing a theme from the web UI will recreate
# fish_frozen_theme.fish, which sorts before this file and so would lose.
set --global fish_color_normal ffffff
set --global fish_color_command ffffff --bold
set --global fish_color_keyword ff2d40
set --global fish_color_param c8c8ce
set --global fish_color_option 8d9bb8
set --global fish_color_quote 7f9b6a
set --global fish_color_escape 9db8bd
set --global fish_color_operator c8c8ce
set --global fish_color_redirection ff6b78
set --global fish_color_end 6b6b70
set --global fish_color_comment 6b6b70 --italics
set --global fish_color_error ff2d40 --bold
set --global fish_color_cancel ff2d40
set --global fish_color_gray 6b6b70
set --global fish_color_autosuggestion 4a4a52
set --global fish_color_selection 000000 --background=ff2d40
set --global fish_color_search_match 000000 --background=c8c8ce
set --global fish_color_match 000000 --background=ff2d40
set --global fish_color_normal cdd6f4
set --global fish_color_command 89b4fa
set --global fish_color_keyword f38ba8
set --global fish_color_param f2cdcd
set --global fish_color_option a6e3a1
set --global fish_color_quote a6e3a1
set --global fish_color_escape eba0ac
set --global fish_color_operator f5c2e7
set --global fish_color_redirection f5c2e7
set --global fish_color_end fab387
set --global fish_color_comment 7f849c --italics
set --global fish_color_error f38ba8 --bold
set --global fish_color_cancel f38ba8
set --global fish_color_gray 6c7086
set --global fish_color_autosuggestion 6c7086
set --global fish_color_selection 11111b --background=89b4fa
set --global fish_color_search_match 11111b --background=cba6f7
set --global fish_color_match 11111b --background=89b4fa
set --global fish_color_history_current --bold
set --global fish_color_valid_path --underline
set --global fish_color_cwd ffffff --bold
set --global fish_color_cwd_root ff2d40 --bold
set --global fish_color_user c8c8ce
set --global fish_color_host ffffff --bold
set --global fish_color_host_remote ff6b78
set --global fish_color_status ff2d40
set --global fish_color_cwd f9e2af --bold
set --global fish_color_cwd_root f38ba8 --bold
set --global fish_color_user 94e2d5
set --global fish_color_host 89b4fa --bold
set --global fish_color_host_remote a6e3a1
set --global fish_color_status f38ba8
set --global fish_pager_color_progress 6b6b70 --background=141418
set --global fish_pager_color_progress 6c7086 --background=313244
set --global fish_pager_color_background
set --global fish_pager_color_prefix ffffff --bold
set --global fish_pager_color_completion c8c8ce
set --global fish_pager_color_description 6b6b70
set --global fish_pager_color_secondary_background --background=0d0d10
set --global fish_pager_color_secondary_completion c8c8ce
set --global fish_pager_color_secondary_description 6b6b70
set --global fish_pager_color_secondary_prefix ffffff --bold
set --global fish_pager_color_selected_background --background=ff2d40
set --global fish_pager_color_selected_completion 000000
set --global fish_pager_color_selected_description 000000
set --global fish_pager_color_selected_prefix 000000 --bold
set --global fish_pager_color_prefix cba6f7 --bold
set --global fish_pager_color_completion cdd6f4
set --global fish_pager_color_description 6c7086
set --global fish_pager_color_secondary_background --background=181825
set --global fish_pager_color_secondary_completion cdd6f4
set --global fish_pager_color_secondary_description 6c7086
set --global fish_pager_color_secondary_prefix cba6f7 --bold
set --global fish_pager_color_selected_background --background=89b4fa
set --global fish_pager_color_selected_completion 11111b
set --global fish_pager_color_selected_description 11111b
set --global fish_pager_color_selected_prefix 11111b --bold
-3
View File
@@ -38,6 +38,3 @@ if status is-login && test -z "$DISPLAY" -a -z "$WAYLAND_DISPLAY"
end
end
# Claude environments
alias claude="CLAUDE_CONFIG_DIR=~/.claude command claude"
alias claude-work="CLAUDE_CONFIG_DIR=~/.claude-work command claude"
+4
View File
@@ -0,0 +1,4 @@
/* Catppuccin Mocha Blue carries the whole GTK3 palette itself, so there is
* nothing to override here — the Takemi branch only needed this file because
* it was painting Fluent's greys black. Kept as a hook for the next time the
* rice disagrees with stock Catppuccin. */
+7
View File
@@ -0,0 +1,7 @@
[Settings]
gtk-theme-name=catppuccin-mocha-blue-standard+default
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
+4
View File
@@ -0,0 +1,4 @@
/* Catppuccin Mocha Blue, straight from the installed theme. The Takemi branch
* overrode Fluent's greys with black here; Mocha needs no override — its own
* surfaces are the palette the rest of the rice is built on. */
@import url("/usr/share/themes/catppuccin-mocha-blue-standard+default/gtk-4.0/gtk.css");
+7
View File
@@ -0,0 +1,7 @@
[Settings]
gtk-theme-name=catppuccin-mocha-blue-standard+default
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
+1 -1
View File
@@ -1,6 +1,6 @@
wallpaper {
monitor =
path = ~/.wallpapers/takemi_3.jpg
path = ~/.wallpapers/arch.png
fit_mode = cover
}
+1 -1
View File
@@ -17,7 +17,7 @@ return {
-- results appear as you type; a dmenu could only filter a list it was
-- handed once, which made searching a two-step. It spawns its own terminal
-- (class pdfs-prompt, floated in lua/rules/windows.lua).
pdfs = "pdfs-prompt --fzf",
pdfs = "pdfs-prompt --gtk",
-- bluetooth menu
bluetooth = "fluxo bt menu",
+5
View File
@@ -16,6 +16,11 @@ hl.on("hyprland.start", function()
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
-- pdfs-prompt has no hidden/resident mode, so the first search of the
-- session is cold. Run one throwaway query once the Proton Drive daemon
-- is up to pull the binary into the page cache and warm the daemon's index.
hl.exec_cmd("sh -c 'sleep 15; pdfs-prompt --feed a >/dev/null 2>&1'")
-- hl.exec_cmd("uwsm app -- nextcloud --background")
-- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive")
-- hl.exec_cmd("uwsm app -- protonvpn-app")
+5
View File
@@ -8,6 +8,11 @@ end
shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
shell("power", { key = mod .. " + P", description = "power menu" })
shell("ask", { key = mod .. " + A", description = "ask claude" })
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
-- Not a shell call, but it lives with the other session toggles.
local mirror = require("lua/mirror")
hl.bind(mod .. " + SHIFT + P", mirror.toggle, { description = "mirror laptop to all screens" })
+7
View File
@@ -29,3 +29,10 @@ hl.env("EDITOR", "nvim")
-- hl.env("INTEL_DEBUG", "noccs")
-- hl.env("WLR_DRM_NO_ATOMIC", "1")
-- hl.env("debug:full_cm_proto", "true")
-- headroom
hl.env("HEADROOM_ROLLOUT_CHANNEL", "beta")
hl.env("HEADROOM_OUTPUT_SHAPER", "1")
+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
-- in lua/animations.lua.
@@ -15,7 +16,10 @@ hl.config({
col = {
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,
@@ -35,13 +39,16 @@ hl.config({
active_opacity = 1.0,
inactive_opacity = 1.0,
-- A hard crust offset, the same printed drop the shell's panels
-- carry, rather than a soft blur. Sharp with a 1px range leaves a
-- clean band down the right and bottom edges only.
shadow = {
enabled = true,
range = 18,
render_power = 3,
offset = { 0, 4 },
color = "rgba(000000ee)",
color_inactive = "rgba(00000088)",
sharp = true,
range = 1,
offset = { 4, 4 },
color = c.ink,
color_inactive = c.ink,
},
-- Off. Blur is the signature of the glass-and-frost look, which is the
@@ -66,7 +73,7 @@ hl.config({
group = {
col = {
border_active = c.activeBorder,
border_inactive = c.outline,
border_inactive = c.surfaceAlt,
},
groupbar = {
+107
View File
@@ -0,0 +1,107 @@
-- Presentation mode: every external output shows the laptop panel.
--
-- Toggled from lua/binds/shell.lua. Mirroring is just a `mirror` field on the
-- monitor rule, so switching it off clears that field and re-applies the specs
-- from lua/monitors.lua to put the Samsung back where it belongs.
--
-- Anything plugged in while mirroring is on gets mirrored too, which is the
-- usual order at a talk: press the key, then find the HDMI cable.
local monitors = require("lua/monitors")
local M = {}
local function notify(text)
hl.notification.create({ text = text, timeout = 3000 })
end
-- The panel is off while docked with the lid shut, see lua/lid.lua. Mirroring
-- a disabled output would leave every screen black.
local function builtin()
for _, mon in ipairs(hl.get_monitors()) do
if mon.name == monitors.laptop then
return mon
end
end
return nil
end
-- A mirroring output drops out of hl.get_monitors(), so the only place left
-- to find it is the panel's own `mirrors` list. That list is also the state:
-- a Lua flag would reset on every config reload while the outputs stay
-- mirrored.
local function mirroring(panel)
local names = {}
for _, mon in ipairs(panel and panel.mirrors or {}) do
table.insert(names, mon.name)
end
return names
end
-- Outputs keep their own mode while mirroring, and the panel image gets
-- rescaled to fit it every frame. The Dells at 1920x1200 flicker with that,
-- so match the panel's resolution; Hyprland falls back to the preferred mode
-- if the output cannot do it.
local function mirror_all(panel)
local mode = panel.width .. "x" .. panel.height
for _, mon in ipairs(monitors.externals_present()) do
hl.monitor({ output = mon.name, mirror = monitors.laptop, mode = mode, position = "auto" })
end
end
-- The bar and popouts stay on the outputs they were created for, so
-- respawn the shell the same way lua/autostart.lua starts it.
local function restart_clients()
hl.exec_cmd("systemctl restart --user hyprpaper")
hl.exec_cmd("sh -c 'qs kill; exec uwsm app -- qs'")
end
-- hl.monitor() only takes effect after the calling handler returns, so
-- anything that has to see the new layout goes on a timer.
local function later(fn)
hl.timer(fn, { timeout = 500, type = "oneshot" })
end
-- Clearing `mirror` alone brings the image back, but Hyprland never
-- re-announces the output to clients: hyprpaper and Quickshell keep seeing
-- only the panel, however often they restart. Cycling the output through
-- disabled makes it a fresh wl_output. Undeclared outputs get their auto
-- defaults back, apply_external() then overrides what lua/monitors.lua knows
-- better.
local function unmirror_all(names)
for _, name in ipairs(names) do
hl.monitor({ output = name, mirror = "", disabled = true })
end
later(function()
for _, name in ipairs(names) do
hl.monitor({ output = name, disabled = false, mode = "preferred", position = "auto" })
end
monitors.apply_external()
later(restart_clients)
end)
end
function M.toggle()
local panel = builtin()
local mirrored = mirroring(panel)
if #mirrored > 0 then
unmirror_all(mirrored)
notify("Mirroring off")
elseif panel then
mirror_all(panel)
later(restart_clients)
notify("Mirroring " .. monitors.laptop .. " to all screens")
else
notify("Laptop panel is off, open the lid first")
end
end
hl.on("monitor.added", function()
local panel = builtin()
if #mirroring(panel) > 0 then
mirror_all(panel)
end
end)
return M
+29 -28
View File
@@ -1,6 +1,6 @@
-- Takemi palette — kept in step with ~/.config/quickshell/config/Theme.qml.
-- Mocha palette — kept in step with ~/.config/quickshell/config/Theme.qml.
--
-- Crimson, black, white. Nothing else carries hue.
-- Catppuccin Mocha ground, blue accent, mauve alarm. Nothing else carries hue.
--
-- Returned as a table, so every module that paints something does
-- local c = require("lua/palette")
@@ -8,44 +8,45 @@
local c = {}
-- ----------------------------------------------------------------- THE KNOB
-- The one colour in this setup. Change this line and the window borders,
-- group indicators and focus states all recolour with it.
-- ---------------------------------------------------------------- THE KNOBS
-- Two colours in this setup: blue for anything live (window borders, group
-- indicators, focus), mauve for anything wrong. Change these lines and every
-- painted edge recolours with them.
--
-- Keep it in step with `readonly property color accent` in
-- Keep them in step with `accent` / `alarm` in
-- ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
-- Those are the only two places the colour is written.
c.accent = "rgb(ff2d40)"
-- Those are the only places the colours are written.
c.accent = "rgb(89b4fa)"
c.alarm = "rgb(cba6f7)"
c.accentDim = "rgb(8f1826)"
c.accentSoft = "rgb(ff6b78)"
c.accentDim = "rgb(5a7dc0)"
c.accentSoft = "rgb(b4befe)"
-- ------------------------------------------------------------------ neutrals
c.ink = "rgb(000000)"
c.ink = "rgb(11111b)"
c.base = "rgb(0a0a0c)"
c.mantle = "rgb(0d0d10)"
c.surface = "rgb(141418)"
c.surfaceAlt = "rgb(1c1c22)"
c.overlay = "rgb(26262e)"
c.outline = "rgb(3a3a44)"
c.base = "rgb(1e1e2e)"
c.mantle = "rgb(181825)"
c.surface = "rgb(313244)"
c.surfaceAlt = "rgb(45475a)"
c.overlay = "rgb(585b70)"
c.outline = "rgb(6c7086)"
c.text = "rgb(ffffff)"
c.subtext = "rgb(c8c8ce)"
c.muted = "rgb(6b6b70)"
c.text = "rgb(cdd6f4)"
c.subtext = "rgb(a6adc8)"
c.muted = "rgb(7f849c)"
-- Retired hues, kept as aliases so existing references resolve. The shell has
-- no teal any more — the wallpaper is the colour in the composition.
-- Semantic aliases, so existing references resolve without inventing hues.
c.primary = c.text
c.primaryDim = "rgb(9a9aa2)"
c.primaryDim = "rgb(9399b2)"
c.glow = c.accent
c.blue = c.primaryDim
c.blue = c.accent
c.deep = c.surfaceAlt
c.warn = c.accentSoft
c.warn = c.alarm
c.ok = c.text
-- Crimson into its own shadow, on the diagonal. The accent is the only hue
-- here — a gradient into a second colour is what put blue in the border.
c.activeBorder = { colors = { c.accent, c.accentDim }, angle = 135 }
-- Blue into lavender, on the diagonal. Both ends are the accent's own family,
-- so the border never reads as two colours arguing.
c.activeBorder = { colors = { c.accent, c.accentSoft }, angle = 135 }
return c
+2
View File
@@ -22,6 +22,7 @@ x-scheme-handler/msteams=teams-for-linux.desktop
x-scheme-handler/slack=slack.desktop
audio/flac=mpv.desktop
x-scheme-handler/codex=chatgpt.desktop
x-scheme-handler/proton-inbox=proton-mail.desktop
[Added Associations]
application/pdf=org.pwmt.zathura-pdf-poppler.desktop;
@@ -40,3 +41,4 @@ application/xhtml+xml=zen.desktop;
application/x-extension-xhtml=zen.desktop;
application/x-extension-xht=zen.desktop;
audio/flac=mpv.desktop;
text/plain=org.neovim.nvim.desktop;
+1 -1
Submodule nvim updated: 2cbfe6946a...4736228253.
+32
View File
@@ -0,0 +1,32 @@
[Appearance]
color_scheme_path=/usr/share/qt6ct/colors/catppuccin-mocha-blue.conf
custom_palette=true
icon_theme=Papirus-Dark
standard_dialogs=default
style=Fusion
[Fonts]
fixed="Sans Serif,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
general="Sans Serif,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
[Interface]
activate_item_on_single_click=1
buttonbox_layout=0
cursor_flash_time=1000
dialog_buttons_have_icons=1
double_click_interval=400
gui_effects=@Invalid()
keyboard_scheme=2
menus_have_icons=true
show_shortcuts_in_context_menus=true
stylesheets=@Invalid()
toolbutton_style=4
underline_shortcut=1
wheel_scroll_lines=3
[SettingsWindow]
geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l\0\0\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l\0\0\0\x1\x2\0\0\0\a\x80\0\0\a\x80\0\0\0\0\0\0\xe\xef\0\0\x4l)
[Troubleshooting]
force_raster_widgets=1
ignored_applications=@Invalid()
+119 -63
View File
@@ -1,20 +1,21 @@
# Takemi
# Takemi — Persona × Mocha
A Persona 5 flavoured Quickshell desktop for Hyprland. Crimson, black and white,
with no gradients anywhere. Everything leans by the same 14°, corners are
chopped rather than rounded, and every surface is one continuous slab divided by
hairlines rather than a cluster of floating cards.
A Persona 5 flavoured Quickshell desktop for Hyprland, painted in Catppuccin
Mocha. Blue for anything live, mauve for anything wrong, the Mocha greys for
everything else, and no gradients anywhere. Everything leans by the same 14°,
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.
## Running
```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
```
Hyprland starts it from `~/.config/hypr/modules/autostart.conf`.
Hyprland starts it from `~/.config/hypr/lua/autostart.lua`.
## 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
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
crimson underline. Pass `standalone: true` for the rare thing that really is its
*content inside* a surface, not a surface of its own — and marks hover with an
accent underline. Pass `standalone: true` for the rare thing that really is its
own panel.
The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is
@@ -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
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
bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar modules
@@ -106,12 +107,13 @@ dismisses the screen.
## Keybinds
Set in `~/.config/hypr/modules/keybinds.conf`:
Set in `~/.config/hypr/lua/binds/shell.lua`:
- `Super+Ctrl+L` — lock
- `Super+P` — power menu
- `Super+Shift+N` — toggle do not disturb
- `Super+Ctrl+Shift+N` — clear notifications
- `Super+A` — ask panel
- `Super+Shift+R` — reload the shell
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 power
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 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
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
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
program:
- **Blue (`accent`, Mocha blue)** means live: the focused workspace, the
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 |
|---|---|---|
| `quickshell/config/Theme.qml` | `readonly property color accent: "#ff2d40"` | the shell |
| `hypr/modules/takemi.conf` | `$accent = rgb(ff2d40)` | window borders, groups |
| `alacritty/takemi.toml` | `red`, cursor, selection | the terminal |
**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
accent mark. Destructive-active is the same slab in mauve. The `treatment*`
tokens in `Theme.qml` encode all of this; use them rather than picking colours
per widget.
Everything else derives. `accentDim` and `accentSoft` come off `accent` via
`Qt.darker`/`Qt.lighter`, `glow` (the hover highlight) *is* `accent`, and every
Hyprland border, group indicator and focus state references `$accent`. Change
those lines and the whole setup recolours — swap in Persona 4's yellow or
Persona 3's blue and nothing else needs touching.
**Print, not blur.** Display text (`SplitText`) carries one hard crust offset.
The blue and mauve ghosts appear only while `glitch()` runs. Drawn permanently
1.6px off the body, they read as a focus problem on Mocha's pastels rather than
as misregistered print. The lock screen and the power menu are the exceptions;
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
- **Alacritty** — `alacritty/takemi.toml`, selected by the `import` in
`alacritty.toml`. The Catppuccin files are left in place, just unreferenced.
Near-black ground, white text, crimson cursor and selection. The sixteen ANSI
slots keep their hues because syntax highlighters need to tell them apart, but
every one is desaturated so nothing competes with the accent.
- **Neovim** — `nvim/lua/takemi.lua`, loaded through `nvim/colors/takemi.lua`
so `:colorscheme takemi` and `require("takemi").lualine()` both work. Same
palette, same rule: crimson is spent only on keywords, types and errors,
functions are plain white-bold, and `terminal_color_0..15` mirror
`takemi.toml` slot for slot so `:terminal` matches the shell around it.
- **Fish** — `fish/conf.d/takemi_theme.fish`, with a preview copy in
`fish/themes/Takemi.theme` for `fish_config`. Replaced the Catppuccin
`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.
- **Hyprland** — `hypr/lua/look.lua`. No rounding, no blur, both opacities at
`1.0`. The active border runs blue into lavender and the inactive border is
`surfaceAlt`. The window shadow is a sharp 4px crust offset, the same printed
drop the shell's panels carry, not a soft glow.
- **Alacritty** — `alacritty/catppuccin-mocha.toml`, selected by the `import`
in `alacritty.toml`. Stock Mocha except for the cursor, which is the accent.
`takemi.toml` is left in place, unreferenced.
- **Neovim** — the `catppuccin` plugin, flavour mocha, configured in
`nvim/lua/plugins/ui.lua`. Blue is the accent there too. `colors/takemi.lua`
is left in place, unused.
- **Fish** — `fish/conf.d/takemi_theme.fish` is Catppuccin Mocha's fish theme
with the Takemi selection rows: a solid accent block with crust punched out.
- **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
`fuzzel --dmenu` where a script needs a picker.
- **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
the signature of the frosted-glass look, which is the opposite of flat ink on
flat ground — and with nothing translucent left it was costing three render
passes for no visible result.
Hyprland's `decoration:blur` is disabled. Blur is the signature of the
frosted-glass look, which is the opposite of flat ink on flat ground — and
with nothing translucent left it was costing three render passes for no
visible result.
White is the resting voice — text, icons, idle indicators. Crimson is the only
accent and always means one thing: active, or wrong. Black is structure, used as
a hard keyline rather than as a soft shadow. **There are no gradients in this
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.
There are **no gradients in this shell**; every fill is flat. `Theme.sheen`
still exists as a no-op property because a teal-to-transparent wash on every
panel was once the biggest source of murk in the design.
## 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
}
// 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
// without leaving permanent motion in the bar.
Text {
@@ -119,7 +119,7 @@ Item {
landedToplevel = toplevel;
}
// A quick crimson wipe under the title on focus change.
// A quick accent wipe under the title on focus change.
Rectangle {
id: wipe
anchors.bottom: parent.bottom
+1 -1
View File
@@ -126,7 +126,7 @@ PanelWindow {
anchors.centerIn: parent
width: parent.ruleHeight * Theme.skew + parent.ruleWeight
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.
//
// 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
// 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
// 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
// module instead of across it.
// The band reserved the full rule plus its inset — 7px off a 40px module,
@@ -102,7 +102,7 @@ Item {
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.
Item {
visible: !root.standalone && root.interactive
+2 -2
View File
@@ -36,7 +36,8 @@ BarPill {
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2.5
font.weight: Font.Bold
font.letterSpacing: 3
renderType: Text.NativeRendering
}
@@ -98,7 +99,6 @@ BarPill {
text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: Theme.fsTitle
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
hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
alert: Audio.muted
dim: Audio.muted
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
@@ -106,7 +106,7 @@ PanelWindow {
statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName
hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
alert: Audio.micMuted
dim: Audio.micMuted
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
@@ -132,7 +132,7 @@ PanelWindow {
label: "Battery"
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
suppressTooltip: battPopout.detailActive
alert: Battery.critical
alarm: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.togglePinned()
}
@@ -165,7 +165,7 @@ PanelWindow {
: ""
reserveReadout: true
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
live: Notifs.hasUnread && !Notifs.dnd
suppressTooltip: notifPopout.detailActive
onClicked: {
notifPopout.togglePinned();
@@ -178,7 +178,7 @@ PanelWindow {
icon: "󰐥"
label: "Power"
statusText: "Session actions"
alert: Actions.powerMenuOpen
live: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu()
}
}
@@ -201,7 +201,7 @@ PanelWindow {
width: parent.ruleWidth
height: parent.ruleWidth * Theme.skew + parent.ruleWeight
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"
// 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
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits
// under the glyph rather than beside it, and anything longer than a percentage
// belongs in the popout instead.
// stacked beneath it, and an accent bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so the readout sits under the glyph
// rather than beside it, and anything longer than a percentage belongs in the
// popout instead.
Item {
id: root
@@ -17,7 +17,15 @@ Item {
property string hint: ""
property bool suppressTooltip: false
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 pulsing: false
@@ -63,7 +71,7 @@ Item {
id: glyphMetrics
text: root.icon
font.family: Theme.fontIcon
font.pixelSize: Theme.fsLarge
font.pixelSize: Theme.fsRailIcon
}
Item {
@@ -76,9 +84,10 @@ Item {
y: (root.glyphBand - height) / 2
text: root.icon
pulsing: root.pulsing
font.pixelSize: Theme.fsLarge
font.pixelSize: Theme.fsRailIcon
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.hovered) return Theme.glow;
return root.tint;
@@ -96,7 +105,11 @@ Item {
anchors.topMargin: root.glyphBand + 1
visible: 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.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold
@@ -116,9 +129,9 @@ Item {
// 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.
//
// It is crimson rather than `tint` because it only ever appears on hover,
// and hover is crimson everywhere else in the shell (Theme.glow). Painting
// it white put a white bar on the rail's edge for no stated reason.
// It is the accent rather than `tint` because it only ever appears on
// hover, and hover is the accent everywhere else in the shell (Theme.glow).
// Painting it white put a white bar on the rail's edge for no stated reason.
Rectangle {
anchors.left: parent.left
anchors.leftMargin: root.wipeInset
@@ -137,7 +150,8 @@ Item {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
onAlarmChanged: if (alarm) glyph.bump()
onLiveChanged: if (live) glyph.bump()
HoverHandler {
id: hover
+2 -2
View File
@@ -63,7 +63,7 @@ BarPill {
anchors.centerIn: parent
width: parent.height * Theme.skew + 1
height: parent.height
color: Theme.alpha(Theme.outline, 0.8)
color: Theme.alpha(Theme.outline, 0.5)
}
}
@@ -106,7 +106,7 @@ BarPill {
id: labelText
anchors.baseline: gauge.baseline
text: metric.label
color: Theme.muted
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
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
// 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
// the same parts as the rest of the bar.
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)
}
}
+35 -17
View File
@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
@@ -15,10 +14,36 @@ Column {
id: root
spacing: Theme.padS + 2
visible: SystemTray.items.values.length > 0
visible: root.shownItems.length > 0
// Applets whose whole job the rail already does. blueman keeps running as
// the pairing agent, but its icon was a second bluetooth glyph a few slots
// below the rail's own.
readonly property var hiddenIds: ["blueman"]
readonly property var shownItems: SystemTray.items.values
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
// Electron and Chromium apps send only a full-colour pixmap, and flattening
// that loses everything inside the outline: Brave's lion became a plain
// shield. The icon theme often ships a panel glyph drawn for exactly this
// job, so look one up by the app's name and use the pixmap only when none
// exists. The id is no help here ("chrome_status_icon_1" for Brave and
// Proton Pass alike), so the tooltip title is the name we go by.
function themedIcon(item: SystemTrayItem): string {
if (item.icon.startsWith("image://icon/")) return item.icon;
const name = (item.tooltipTitle || item.title || item.id)
.toLowerCase().replace(/[^a-z0-9]+/g, "-");
for (const suffix of ["-tray", "-panel", "-symbolic"]) {
const path = Quickshell.iconPath(name + suffix, true);
if (path !== "") return path;
}
return item.icon;
}
Repeater {
model: SystemTray.items
model: root.shownItems
Item {
id: entry
@@ -31,32 +56,25 @@ Column {
// App icons arrive in their own brand colours, which wrecks the
// two-hue palette faster than anything else in the shell. Flatten
// every one of them to a single-colour silhouette: white at rest,
// crimson under the pointer. Detail inside the glyph is lost on
// every one of them to a single-colour silhouette: subtext at rest,
// 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.
IconImage {
id: img
anchors.fill: parent
source: entry.modelData.icon
source: root.themedIcon(entry.modelData)
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: itemHover.hovered
layer.effect: TintEffect {
tintColor: itemHover.hovered
? Theme.accent
: Theme.text
: Theme.subtext
Behavior on colorizationColor {
Behavior on tintColor {
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
+27 -14
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components"
// 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 {
id: root
@@ -65,8 +65,12 @@ Item {
// Every tick is the same height and the same shear. Only the
// 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 {
NumberAnimation {
@@ -80,23 +84,32 @@ Item {
id: tick
anchors.fill: parent
// Focused is a solid crimson block. Occupied is a white
// block at half weight. Empty is an outline. Three states,
// two colours, no ambiguity.
// Focused is a solid accent block. Urgent is a solid alarm
// block — it used to be the accent too, which made an
// 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: {
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.occupied) return Theme.alpha(Theme.text, 0.5);
return "transparent";
if (pip.occupied) return Theme.subtext;
return Theme.surfaceAlt;
}
borderColor: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
borderWidth: 1
Behavior on color {
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.
@@ -107,7 +120,7 @@ Item {
text: pip.wsId
color: Theme.ink
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
font.italic: true
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
pixelSize: Theme.fsBody
color: Theme.text
split: 1
splitOpacity: 0.5
}
}
+11 -10
View File
@@ -34,13 +34,16 @@ Item {
chop: root.chop
fill: Theme.surface
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
slashColor: root.tone
slashWidth: 4
halftone: root.notif?.urgency === NotificationUrgency.Critical
halftoneColor: Theme.accent
halftoneOpacity: 0.12
padding: Theme.padM
Column {
@@ -66,7 +69,7 @@ Item {
text: (root.notif?.appName || "System").toUpperCase()
color: root.tone
font.family: Theme.fontMono
font.pixelSize: 8
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
@@ -126,18 +129,16 @@ Item {
Skew {
anchors.fill: parent
color: actionHover.hovered
? Theme.alpha(root.tone, 0.3)
: Theme.alpha(root.tone, 0.1)
color: actionHover.hovered ? Theme.accent : Theme.surfaceAlt
borderWidth: 1
borderColor: Theme.alpha(root.tone, 0.6)
borderColor: actionHover.hovered ? Theme.accent : Theme.overlay
}
Text {
id: actionLabel
anchors.centerIn: parent
text: actionChip.modelData.text
color: Theme.text
color: actionHover.hovered ? Theme.ink : Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro
font.italic: true
+4 -2
View File
@@ -3,7 +3,7 @@ import QtQuick.Shapes
import "root:/config"
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
// halftone dot wash over the top.
//
@@ -33,7 +33,9 @@ Item {
property real wedgeSpan: 0.34
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 real halftoneOpacity: 0.05
+23 -8
View File
@@ -21,6 +21,12 @@ FocusScope {
readonly property bool focusedOrSelected: root.activeFocus || root.selected
readonly property bool highlighted: root.hovered || root.focusedOrSelected
// Keyboard focus and roving selection invert the slab to solid accent.
// Pointer hover stays a keyline change, so the two never look alike and a
// mouse user never sees the "selected" treatment by accident. Destructive
// buttons keep their own mauve inversion for both.
readonly property bool inverted: root.enabled && !root.destructive && root.focusedOrSelected
// Pointer activation must not turn a transient action into the black/white
// keyboard-focus treatment. Tab still reaches it, and programmatic focus
// (power-menu roving selection / pinned popouts) still works.
@@ -46,23 +52,26 @@ FocusScope {
padding: Theme.padS
fill: {
if (!root.enabled) return Theme.treatmentDisabledFill;
if (root.destructive) return Theme.treatmentDestructiveFill;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveFill
: Theme.treatmentDestructiveFill;
if (root.inverted) return Theme.treatmentFocusFill;
if (root.highlighted) return Theme.treatmentHoverFill;
return Theme.treatmentRestFill;
}
fillOpacity: root.enabled ? 1.0 : 0.72
border: {
if (!root.enabled) return Theme.treatmentDisabledMark;
if (root.destructive) return Theme.treatmentFocusMark;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveMark
: Theme.treatmentDestructiveMark;
if (root.inverted) return Theme.treatmentFocusMark;
if (root.highlighted) return Theme.treatmentHoverMark;
return Theme.alpha(root.accent, 0.55);
}
borderWidth: Theme.stroke
slash: !root.highlighted
slashColor: root.destructive ? Theme.treatmentDestructiveMark : root.accent
halftone: root.enabled && root.highlighted
halftoneColor: Theme.ink
halftoneOpacity: 0.14
slashColor: root.destructive ? Theme.alarm : root.accent
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
@@ -81,7 +90,10 @@ FocusScope {
font.pixelSize: Theme.fsTitle
color: {
if (!root.enabled) return Theme.treatmentDisabledText;
if (root.destructive) return Theme.treatmentDestructiveText;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText;
return root.accent;
}
@@ -92,7 +104,10 @@ FocusScope {
text: root.text
color: {
if (!root.enabled) return Theme.treatmentDisabledText;
if (root.destructive) return Theme.treatmentDestructiveText;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText;
return Theme.treatmentRestText;
}
+3 -3
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components"
// 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 {
id: root
@@ -29,7 +29,7 @@ Item {
property real chop: Theme.cut
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 color slashColor: Theme.accent
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
// 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
// slash ends exactly where the chamfer does.
Loader {
+5 -2
View File
@@ -31,10 +31,13 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
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
? Theme.treatmentFocusFill
? Theme.surfaceAlt
: 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
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 "root:/config"
// Display text with the wallpaper's RGB split baked in: a crimson and a cyan
// ghost sitting a pixel off the white body. Drives most headings in the shell.
// Display text, screen-printed: the body sits on a hard crust copy knocked a
// 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 {
id: root
@@ -12,23 +18,34 @@ Item {
property string family: Theme.fontDisplay
property bool italic: true
property color color: Theme.text
property real split: 1.6
property real splitOpacity: 0.85
property int elide: Text.ElideNone
property int horizontalAlignment: Text.AlignLeft
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
implicitHeight: body.implicitHeight
Text {
id: ghostRed
component Layer: Text {
anchors.fill: parent
anchors.leftMargin: -root.split
anchors.topMargin: root.split * 0.4
text: root.text
color: Theme.splitRed
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
@@ -40,39 +57,33 @@ Item {
renderType: Text.NativeRendering
}
Text {
id: ghostCyan
anchors.fill: parent
anchors.leftMargin: root.split
anchors.topMargin: -root.split * 0.4
text: root.text
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
Layer {
anchors.leftMargin: root.shadowOffset
anchors.rightMargin: -root.shadowOffset
anchors.topMargin: root.shadowOffset
anchors.bottomMargin: -root.shadowOffset
color: root.shadowColor
}
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
anchors.fill: parent
text: root.text
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.
@@ -83,14 +94,14 @@ Item {
SequentialAnimation {
id: glitchAnim
NumberAnimation {
target: root; property: "split"
to: 5.5; duration: Theme.durFast
target: root; property: "kick"
to: 1; duration: Theme.durFast
easing.type: Easing.OutExpo
}
NumberAnimation {
target: root; property: "split"
to: 1.6; duration: Theme.durSlow
easing.type: Easing.OutBack
target: root; property: "kick"
to: 0; duration: Theme.durSlow
easing.type: Easing.OutCubic
}
}
}
+8 -2
View File
@@ -1,11 +1,17 @@
import QtQuick
import QtQuick.Effects
// Layer effect that recolours whatever it is applied to — used to tint the
// white texture tiles without shipping a copy per colour.
// Layer effect that recolours whatever it is applied to into a flat silhouette
// 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 {
property color tintColor: "white"
brightness: 1.0
colorization: 1.0
colorizationColor: tintColor
}
+89 -65
View File
@@ -3,61 +3,73 @@ pragma Singleton
import QtQuick
import Quickshell
// Takemi palette: crimson, black, white. Nothing else.
// Mocha palette: Catppuccin Mocha ground, blue as the accent, mauve as the
// alarm. Persona geometry, softer colour.
//
// White does the work: text, icons, idle indicators. Crimson is the only accent
// and it always means the same thing — active, or wrong. Black is structure,
// used as a hard keyline rather than as a shadow. There are no gradients in
// this shell; every fill is flat.
// The Takemi branch ran monochrome-plus-crimson, where one hue carried both
// "active" and "wrong". Mocha has room for two: blue is every resting accent —
// focus, selection, the thing you are pointing at — and mauve is reserved for
// the things that bite, so destructive and error still read at a glance
// without a third colour entering the shell.
//
// The wallpaper is teal. The UI used to be teal too, and the two fought each
// other into a murky blue-grey. Letting the UI go monochrome-plus-red is what
// lets the wallpaper read as the colour in the composition.
// Mauve rather than Mocha's pink: pink is a pastel, and a slab of it over this
// ground goes chalky. Mauve is saturated enough to hold an edge against the
// near-black surfaces the Persona layout is built from.
//
// Everything else is Mocha's own neutral ramp. Fills stay flat and keylines
// stay hard; only the hues moved.
Singleton {
id: root
// ---------------------------------------------------------------- colors
readonly property color ink: "#000000"
// Crust, not pure black: it is the darkest thing in Mocha, so it keeps its
// job as the hard keyline and as text punched out of an accent fill.
readonly property color ink: "#11111b"
readonly property color base: "#0a0a0c"
readonly property color mantle: "#0d0d10"
readonly property color surface: "#141418"
readonly property color surfaceAlt: "#1c1c22"
readonly property color overlay: "#26262e"
readonly property color outline: "#3a3a44"
readonly property color base: "#1e1e2e"
readonly property color mantle: "#181825"
readonly property color surface: "#313244"
readonly property color surfaceAlt: "#45475a"
readonly property color overlay: "#585b70"
readonly property color outline: "#6c7086"
readonly property color text: "#ffffff"
readonly property color subtext: "#c8c8ce"
readonly property color muted: "#6b6b70"
readonly property color text: "#cdd6f4"
readonly property color subtext: "#a6adc8"
readonly property color muted: "#7f849c"
// ------------------------------------------------------------ THE KNOB
// The one colour in this shell. Change this line and the entire accent
// recolours — everything below is derived from it, and nothing else in the
// palette carries hue at all.
// ------------------------------------------------------------ THE KNOBS
// Two colours, two meanings, and nothing else in the shell carries hue.
//
// Keep it in step with `$accent` at the top of
// ~/.config/hypr/modules/takemi.conf, which is what colours the Hyprland
// window borders. Those are the only two places the colour is written.
readonly property color accent: "#ff2d40"
// `accent` is blue: anything live, focused or selected. `alarm` is mauve:
// 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.
//
// Keep them in step with `c.accent` / `c.alarm` in
// ~/.config/hypr/lua/palette.lua, which is what colours the Hyprland
// window borders. Those are the only places the colours are written.
readonly property color accent: "#89b4fa"
readonly property color alarm: "#cba6f7"
readonly property color accentDim: Qt.darker(root.accent, 1.9)
readonly property color accentSoft: Qt.lighter(root.accent, 1.35)
readonly property color accentDim: "#5a7dc0"
readonly property color accentSoft: "#b4befe"
// `primary` is the resting voice of the UI, which is now simply white.
// Kept under the old name so the existing call sites don't all have to move.
readonly property color primary: root.text
readonly property color primaryDim: "#9a9aa2"
readonly property color primaryDim: "#9399b2"
readonly property color deep: root.surfaceAlt
// `glow` is the hover highlight. It used to be a brighter teal, which now
// that everything rests at white would have made hover invisible — so it
// resolves to crimson. Pointing at a thing lights it up red.
// `glow` is the hover highlight. Pointing at a thing lights it up blue —
// the same signal as focus, because it is the same statement about where
// your attention is.
readonly property color glow: root.accent
// ------------------------------------------------ semantic treatments
// These tokens describe interaction meaning, not new palette entries. Each
// state is composed only from Takemi's black, white, crimson, and neutral
// surfaces so callers do not invent one-off state colors.
// state is composed only from crust, the neutral ramp, blue and mauve, so
// callers do not invent one-off state colours.
readonly property color treatmentRestFill: root.surface
readonly property color treatmentRestMark: root.outline
readonly property color treatmentRestText: root.text
@@ -66,7 +78,11 @@ Singleton {
readonly property color treatmentHoverMark: root.accent
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 treatmentFocusText: root.ink
readonly property color treatmentSelectedFill: root.treatmentFocusFill
@@ -77,38 +93,38 @@ Singleton {
readonly property color treatmentActiveMark: root.accent
readonly property color treatmentActiveText: root.text
// Crimson carries destructive/error meaning. Text and keylines stay black:
// black-on-crimson is 5.70:1, while small white-on-crimson is only 3.68:1.
readonly property color treatmentDestructiveFill: root.accent
readonly property color treatmentDestructiveMark: root.ink
readonly property color treatmentDestructiveText: root.ink
readonly property color treatmentErrorFill: root.treatmentDestructiveFill
readonly property color treatmentErrorMark: root.treatmentDestructiveMark
readonly property color treatmentErrorText: root.treatmentDestructiveText
// Mauve carries destructive/error meaning — never blue, so a dangerous
// button can never be mistaken for a merely focused one.
//
// At rest it is a keyline and a label, not a fill. The solid mauve is held
// back for the moment the pointer or the selection is actually on the
// thing — then it lands hard, with crust punched out of it at 9.1:1.
readonly property color treatmentDestructiveFill: root.surface
readonly property color treatmentDestructiveMark: root.alarm
readonly property color treatmentDestructiveText: root.alarm
readonly property color treatmentDestructiveActiveFill: root.alarm
readonly property color treatmentDestructiveActiveMark: root.ink
readonly property color treatmentDestructiveActiveText: root.ink
readonly property color treatmentErrorFill: root.treatmentDestructiveActiveFill
readonly property color treatmentErrorMark: root.treatmentDestructiveActiveMark
readonly property color treatmentErrorText: root.treatmentDestructiveActiveText
readonly property color treatmentUrgentFill: root.surface
readonly property color treatmentUrgentMark: root.accent
readonly property color treatmentUrgentMark: root.alarm
readonly property color treatmentUrgentText: root.text
readonly property color treatmentDisabledFill: root.surface
readonly property color treatmentDisabledMark: root.outline
readonly property color treatmentDisabledText: root.muted
// Retired hues. Aliases only — do not introduce new uses.
//
// `warn` used to be accentSoft, which put a washed-out pink-red on screen
// wherever something was merely notable. That reads as a faded accent, not
// as a decision. There is no half-crimson in this palette: a thing is wrong
// (accent) or it is not (text). `warn` now resolves to the real accent, so
// low battery shouts properly and everything merely informational is white.
readonly property color blue: root.primaryDim
readonly property color ok: root.text
readonly property color warn: root.accent
// Off is a state the user chose — muted, do-not-disturb, radio switched
// off. It is neither live nor wrong, so it gets no hue: painting a muted
// speaker blue said "active", and painting it mauve would say "broken".
readonly property color treatmentOffFill: root.surface
readonly property color treatmentOffMark: root.overlay
readonly property color treatmentOffText: root.muted
// Display text carries a crimson and a black offset rather than an RGB
// split. Same trick, correct palette.
readonly property color splitRed: root.accent
readonly property color splitCyan: root.ink
function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a);
@@ -122,11 +138,15 @@ Singleton {
a.a + (b.a - a.a) * k);
}
// Load readouts sit white until the number actually matters, then bleed to
// crimson. Not a hue cycle — red always means the same thing.
// Load readouts sit at text until the number actually matters, then climb
// through the accent before they reach the alarm. Going straight from text
// to mauve passes through a washed-out mid-tone that reads as a faded red
// rather than as a rising number; routing through blue keeps every step of
// the ramp a colour the shell already uses.
function heat(pct: real): color {
if (pct <= 55) return root.text;
return root.mix(root.text, root.accent, (pct - 55) / 45);
if (pct <= 45) return root.text;
if (pct <= 75) return root.mix(root.text, root.accent, (pct - 45) / 30);
return root.mix(root.accent, root.alarm, (pct - 75) / 25);
}
// ----------------------------------------------------------------- fonts
@@ -137,7 +157,9 @@ Singleton {
readonly property int weightDisplay: Font.Black
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 fsBody: 12
readonly property int fsLarge: 15
@@ -145,6 +167,8 @@ Singleton {
readonly property int fsHuge: 34
readonly property int fsMega: 92
readonly property int fsRailIcon: 17
// -------------------------------------------------------------- geometry
readonly property int barHeight: 40
readonly property int barMargin: 8
@@ -163,7 +187,7 @@ Singleton {
readonly property int cut: 10
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.
readonly property real dropOffset: 3
readonly property real keylineWidth: 2
@@ -196,5 +220,5 @@ Singleton {
// which asks hyprpaper what is actually on screen — a path written down
// here goes stale the moment the wallpaper is switched, which is exactly
// 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.
Skew {
anchors.verticalCenter: parent.verticalCenter
+5 -5
View File
@@ -68,7 +68,7 @@ Item {
// 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
// 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 {
id: backdrop
anchors.fill: parent
@@ -199,7 +199,7 @@ Item {
text: surface.failed
? (surface.status || "WRONG").toUpperCase()
: (surface.busy ? "CHECKING…" : "WHO GOES THERE")
color: surface.failed ? Theme.accent : Theme.text
color: surface.failed ? Theme.alarm : Theme.text
pixelSize: Theme.fsTitle
letterSpacing: 4
horizontalAlignment: Text.AlignHCenter
@@ -223,10 +223,10 @@ Item {
: (input.activeFocus ? Theme.primary : Theme.outline)
borderWidth: 2
slash: true
slashColor: surface.failed ? Theme.accent : Theme.primary
slashColor: surface.failed ? Theme.alarm : Theme.primary
slashWidth: 6
halftone: true
halftoneColor: surface.failed ? Theme.accent : Theme.primary
halftoneColor: surface.failed ? Theme.alarm : Theme.primary
halftoneOpacity: 0.08
Row {
@@ -241,7 +241,7 @@ Item {
required property int index
width: 10
height: 20
color: surface.failed ? Theme.accent : Theme.primary
color: surface.failed ? Theme.alarm : Theme.primary
// Each new block snaps in.
Component.onCompleted: pop.start()
+7 -11
View File
@@ -66,10 +66,12 @@ PanelWindow {
readonly property color tone: {
switch (root.kind) {
case "mic": return Audio.micMuted ? Theme.accent : Theme.blue;
// Turning the screen up is not a warning. White, like volume.
case "brightness": return Theme.text;
default: return Audio.muted ? Theme.accent : Theme.primary;
// One tone for every level: the accent while the channel is live, the
// off grey while it is muted. Muted used to be the accent too, so a
// silenced speaker flashed the same colour as a live one.
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
fill: Theme.mantle
fillOpacity: 0.97
border: Theme.alpha(root.tone, 0.85)
border: Theme.surfaceAlt
borderWidth: Theme.stroke
slash: true
slashColor: root.tone
slashWidth: 5
halftone: true
halftoneColor: root.tone
halftoneOpacity: 0.08
sheen: true
lean: 0.09
chop: 14
padding: Theme.padM
@@ -180,8 +178,6 @@ PanelWindow {
// widening readout only moves its own left edge.
text: Math.round(root.level * 100) + "%"
pixelSize: Theme.fsBody
split: 1
splitOpacity: 0.5
}
}
+10 -6
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components"
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.
PanelWindow {
id: root
@@ -13,11 +13,16 @@ PanelWindow {
required property var modelData
screen: modelData
visible: Actions.powerMenuOpen || closeAnim.running
// Only the screen that had focus when the menu was asked for shows it; the
// other copies stay dormant so the keyboard has one place to land.
readonly property bool open: Actions.powerMenuOpen
&& Actions.powerMenuScreen === modelData.name
visible: root.open || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-power"
WlrLayershell.keyboardFocus: Actions.powerMenuOpen
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
@@ -25,7 +30,6 @@ PanelWindow {
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property bool open: Actions.powerMenuOpen
property int selectedAction: 0
function focusSelectedAction(): void {
@@ -63,7 +67,7 @@ PanelWindow {
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.82)
color: Theme.alpha(Theme.base, 0.9)
opacity: 0
TapHandler {
@@ -178,7 +182,7 @@ PanelWindow {
icon: entry.modelData.icon
destructive: entry.modelData.danger
selected: root.selectedAction === entry.index
accent: entry.modelData.danger ? Theme.accent : Theme.primary
accent: entry.modelData.danger ? Theme.alarm : Theme.primary
lean: Theme.skew
lunge: -10
onClicked: root.invoke(entry.modelData.act)
+5 -6
View File
@@ -13,9 +13,9 @@ PanelWindow {
required property var modelData
screen: modelData
// Only the primary bar screen shows toasts, to avoid duplicates.
readonly property bool primaryScreen: Quickshell.screens.length === 0
|| Quickshell.screens[0] === modelData
// A toast window exists on every screen, but each notification lands only
// on the monitor that had focus when it arrived. Latched per toast rather
// than live, so a card does not hop screens while it is being read.
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-toasts"
@@ -31,7 +31,7 @@ PanelWindow {
implicitHeight: Math.max(1, stack.implicitHeight)
color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: primaryScreen && stack.children.length > 0
visible: root.live.length > 0
// Toasts currently on screen, newest first.
property list<Notification> live: []
@@ -39,9 +39,8 @@ PanelWindow {
Connections {
target: Notifs
enabled: root.primaryScreen
function onPopupRequested(notif) {
if (Actions.focusedScreenName() !== root.modelData.name) return;
root.live = [notif, ...root.live].slice(0, root.maxLive);
}
}
+2 -2
View File
@@ -19,7 +19,7 @@ Item {
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Audio.micIcon
color: Audio.micMuted ? Theme.accent : Theme.text
color: Audio.micMuted ? Theme.treatmentOffText : Theme.text
font.pixelSize: Theme.fsLarge
}
@@ -42,7 +42,7 @@ Item {
Text {
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.pixelSize: Theme.fsMicro
font.weight: Font.Bold
+1 -1
View File
@@ -51,7 +51,7 @@ Item {
Text {
text: Audio.muted ? "MUTED" : "LIVE"
color: Audio.muted ? Theme.accent : Theme.text
color: Audio.muted ? Theme.treatmentOffText : Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
+4 -94
View File
@@ -85,12 +85,12 @@ Popout {
Repeater {
model: Audio.sinks.length > 1 ? Audio.sinks : []
PickRow {
ListRow {
required property var modelData
width: column.width
label: modelData.nickname || modelData.description || modelData.name
icon: "󰓃"
selected: Audio.sink === modelData
current: Audio.sink === modelData
onClicked: Audio.setSink(modelData)
}
}
@@ -147,7 +147,7 @@ Popout {
id: levelIcon
anchors.verticalCenter: parent.verticalCenter
text: levelRow.icon
color: levelRow.muted ? Theme.accent : levelRow.accent
color: levelRow.muted ? Theme.treatmentOffText : levelRow.accent
font.pixelSize: Theme.fsLarge
TapHandler { onTapped: levelRow.toggled() }
@@ -171,7 +171,7 @@ Popout {
id: readoutText
anchors.verticalCenter: parent.verticalCenter
text: levelRow.readout
color: levelRow.muted ? Theme.accent : Theme.subtext
color: levelRow.muted ? Theme.treatmentOffText : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
@@ -189,94 +189,4 @@ Popout {
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) + "%"
pixelSize: Theme.fsHuge
color: Battery.tint
split: 2.5
splitOpacity: 0.7
}
}
@@ -68,8 +66,6 @@ Popout {
horizontalAlignment: Text.AlignRight
text: Battery.timeLabel
pixelSize: Theme.fsBody
split: 1.1
splitOpacity: 0.55
}
Text {
@@ -89,7 +85,7 @@ Popout {
text: "Health " + Math.round(Battery.health) + "%"
anchors.right: parent.right
horizontalAlignment: Text.AlignRight
color: Battery.health < 70 ? Theme.warn : Theme.muted
color: Battery.health < 70 ? Theme.alarm : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
+29 -105
View File
@@ -20,7 +20,7 @@ Popout {
PopoutSurface {
anchors.fill: parent
tone: Bt.enabled ? Theme.blue : Theme.muted
tone: Bt.enabled ? Theme.accent : Theme.muted
PopoutHeader {
id: header
@@ -29,7 +29,7 @@ Popout {
anchors.right: parent.right
title: "Bluetooth"
icon: Bt.icon
accent: Theme.blue
accent: Theme.accent
subtitle: Bt.adapter ? Bt.adapter.name : "NO ADAPTER"
}
@@ -43,7 +43,7 @@ Popout {
P5Button {
text: Bt.enabled ? "On" : "Off"
icon: Bt.enabled ? "󰂯" : "󰂲"
accent: Bt.enabled ? Theme.blue : Theme.muted
accent: Bt.enabled ? Theme.accent : Theme.muted
onClicked: Bt.toggle()
}
@@ -55,17 +55,13 @@ Popout {
}
}
Text {
SectionRule {
id: listLabel
anchors.top: controls.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
text: Bt.devices.length > 0 ? "DEVICES" : "NO DEVICES"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
anchors.right: parent.right
text: Bt.devices.length > 0 ? "Devices" : "No devices"
}
ListView {
@@ -78,113 +74,41 @@ Popout {
spacing: 2
model: Bt.devices
delegate: Item {
delegate: ListRow {
id: devRow
required property BluetoothDevice modelData
width: ListView.view.width
height: 34
Rectangle {
anchors.fill: parent
color: devHover.hovered
? Theme.alpha(Theme.blue, 0.16)
: (devRow.modelData.connected ? Theme.alpha(Theme.blue, 0.08) : "transparent")
}
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
}
}
icon: Bt.deviceIcon(modelData)
label: Bt.deviceLabel(modelData)
detail: {
if (modelData.pairing) return "pairing…";
if (modelData.connected) return "connected";
if (modelData.paired || modelData.bonded) return "paired";
return modelData.address;
}
current: modelData.connected
onClicked: Bt.toggleDevice(modelData)
onRightClicked: if (modelData.paired) modelData.forget()
// Device battery, where BlueZ exposes it.
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
spacing: 3
visible: devRow.modelData.batteryAvailable
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰥉"
color: devRow.modelData.battery > 0.25 ? Theme.ok : Theme.warn
font.pixelSize: Theme.fsSmall
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: Math.round(devRow.modelData.battery * 100) + "%"
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
text: "󰥉"
color: devRow.modelData.battery > 0.25 ? Theme.subtext : Theme.alarm
font.pixelSize: Theme.fsSmall
}
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()
Text {
anchors.verticalCenter: parent.verticalCenter
visible: devRow.modelData.batteryAvailable
text: Math.round(devRow.modelData.battery * 100) + "%"
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+9 -9
View File
@@ -4,7 +4,7 @@ import "root:/config"
import "root:/components"
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.
Popout {
id: root
@@ -99,9 +99,9 @@ Popout {
width: dayNames.width / 7
horizontalAlignment: Text.AlignHCenter
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.pixelSize: 8
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
@@ -126,7 +126,7 @@ Popout {
width: grid.width / 7
height: 30
// Today gets a leaning crimson slab.
// Today gets a leaning accent slab, inked like every selected slab.
Skew {
anchors.centerIn: parent
width: 26
@@ -139,7 +139,7 @@ Popout {
anchors.centerIn: parent
text: cell.modelData.day
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);
return Theme.subtext;
}
@@ -167,7 +167,7 @@ Popout {
text: "UPTIME"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
@@ -191,7 +191,7 @@ Popout {
text: "TOKYO"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
@@ -226,9 +226,9 @@ Popout {
Skew {
anchors.fill: parent
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent"
color: stepHover.hovered ? Theme.treatmentHoverFill : "transparent"
borderWidth: 1
borderColor: Theme.alpha(Theme.outline, 0.7)
borderColor: stepHover.hovered ? Theme.treatmentHoverMark : Theme.alpha(Theme.outline, 0.7)
}
Icon {
+15 -11
View File
@@ -80,7 +80,7 @@ Popout {
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.surface
strokeColor: Theme.alpha(Theme.primary, 0.5)
strokeColor: Theme.surfaceAlt
strokeWidth: 2
PathPolyline {
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
@@ -93,7 +93,7 @@ Popout {
visible: art.status !== Image.Ready
text: Media.sourceIcon
font.pixelSize: 42
color: Theme.alpha(Theme.primary, 0.7)
color: Theme.muted
}
// Spinning rule around the art while playing.
@@ -265,14 +265,14 @@ Popout {
: (chip.activeFocus
? Theme.treatmentFocusFill
: (chip.current
? Theme.alpha(Theme.primary, 0.22)
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")))
? Theme.treatmentActiveFill
: (chipHover.hovered ? Theme.treatmentHoverFill : "transparent")))
borderWidth: chip.activeFocus ? Theme.stroke : 1
borderColor: !chip.enabled
? Theme.treatmentDisabledMark
: (chip.activeFocus
? Theme.treatmentFocusMark
: (chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)))
: (chip.current ? Theme.treatmentActiveMark : Theme.alpha(Theme.outline, 0.7)))
}
Text {
@@ -283,7 +283,7 @@ Popout {
? Theme.treatmentDisabledText
: (chip.activeFocus
? Theme.treatmentFocusText
: (chip.current ? Theme.text : Theme.muted))
: (chip.current || chipHover.hovered ? Theme.text : Theme.subtext))
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
@@ -334,14 +334,17 @@ Popout {
color: {
if (!tb.enabled) return "transparent";
if (tb.activeFocus) return Theme.treatmentFocusFill;
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22);
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent";
// The play button is the one solid control on the panel: a
// 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)
borderColor: tb.enabled
? (tb.activeFocus
? 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)
Behavior on color {
@@ -356,8 +359,9 @@ Popout {
color: {
if (!tb.enabled) return Theme.alpha(Theme.muted, 0.5);
if (tb.activeFocus) return Theme.treatmentFocusText;
if (tbHover.hovered && tb.primary) return Theme.text;
return Theme.primary;
// Ink on the accent slab. White on light blue was barely 2:1.
if (tbHover.hovered && tb.primary) return Theme.ink;
return Theme.text;
}
}
+25 -79
View File
@@ -76,17 +76,13 @@ Popout {
}
}
Text {
SectionRule {
id: apsLabel
anchors.top: controls.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
text: Net.scanning ? "SCANNING…" : "NETWORKS"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
anchors.right: parent.right
text: Net.scanning ? "Scanning…" : "Networks"
}
ListView {
@@ -99,94 +95,44 @@ Popout {
spacing: 2
model: Net.wifiNetworks
delegate: Item {
delegate: ListRow {
id: apRow
required property var modelData
width: ListView.view.width
height: 30
icon: Net.signalIcon(modelData.signalStrength ?? 0)
label: modelData.name
current: modelData.connected
Rectangle {
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
onClicked: {
if (apRow.modelData.connected) {
apRow.modelData.disconnect();
} else {
// Saved networks connect directly; new secured ones
// need a passphrase, which NetworkManager's own
// agent will prompt for.
apRow.modelData.connect();
}
}
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.known ? "SAVED" : ""
visible: apRow.modelData.known
text: "SAVED"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
HoverHandler {
id: apHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
if (apRow.modelData.connected) {
apRow.modelData.disconnect();
} else {
// Saved networks connect directly; new secured ones
// need a passphrase, which NetworkManager's own
// agent will prompt for.
apRow.modelData.connect();
}
}
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()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
+1 -1
View File
@@ -37,7 +37,7 @@ Popout {
P5Button {
text: Notifs.dnd ? "Silenced" : "Silence"
icon: Notifs.dnd ? "󰂛" : "󰂚"
accent: Notifs.dnd ? Theme.accent : Theme.primary
accent: Notifs.dnd ? Theme.treatmentOffText : Theme.primary
onClicked: Notifs.toggleDnd()
}
+3 -5
View File
@@ -2,8 +2,8 @@ import QtQuick
import "root:/config"
import "root:/components"
// Title bar for a popout: glyph, split-ink heading, right-aligned status text,
// and a crimson rule with a chopped tail.
// Title bar for a popout: glyph, print-shadowed heading, right-aligned status
// text, and a rule with a chopped accent tail.
Item {
id: root
@@ -32,8 +32,6 @@ Item {
text: root.title.toUpperCase()
pixelSize: Theme.fsLarge
letterSpacing: 1.5
split: 1.3
splitOpacity: 0.7
}
}
@@ -43,7 +41,7 @@ Item {
anchors.topMargin: 6
visible: root.subtitle !== ""
text: root.subtitle
color: Theme.muted
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
+5 -1
View File
@@ -10,7 +10,11 @@ P5Panel {
fill: Theme.mantle
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
halftone: textured
halftoneColor: tone
+1 -1
View File
@@ -107,7 +107,7 @@ Column {
height: row.entry.isSeparator ? 0 : 26
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.
Skew {
anchors.fill: parent
+34 -1
View File
@@ -17,6 +17,7 @@ Singleton {
property bool lockRequested: false
property bool sidebarOpen: false
property bool launcherOpen: false
property bool askOpen: false
// 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
@@ -29,9 +30,23 @@ Singleton {
// keyboard focus and because a window should not hop mid-query.
property string launcherScreen: ""
// Same story for the power menu: a window per screen, one of them wanted.
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)
function togglePowerMenu() { root.powerMenuOpen = !root.powerMenuOpen; }
function togglePowerMenu() {
if (!root.powerMenuOpen)
root.powerMenuScreen = root.focusedScreenName();
root.powerMenuOpen = !root.powerMenuOpen;
}
function closePowerMenu() { root.powerMenuOpen = false; }
function lock() { root.powerMenuOpen = false; root.lockRequested = true; }
@@ -50,6 +65,22 @@ Singleton {
}
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.
function dmenuLauncher(inPath: string, outPath: string, prompt: string) {
Apps.beginDmenu(inPath, outPath, prompt);
@@ -99,6 +130,8 @@ Singleton {
function lock(): void { root.lock(); }
function power(): void { root.togglePowerMenu(); }
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 dmenu(inPath: string, outPath: string, prompt: string): void {
root.dmenuLauncher(inPath, outPath, prompt);
+11 -4
View File
@@ -304,18 +304,25 @@ Singleton {
// So when the target declares no placeholder we build the command line
// ourselves and hand uwsm an argv instead of an ID. It is still scoped and
// still supervised; we just did the expansion.
//
// The same fallback covers entries whose file name uwsm will not accept as
// an ID at all. Its check is the regex below, so `Proton Authenticator
// .desktop` and most Steam/Lutris shortcuts ("Hollow Knight.desktop") fail
// with "Invalid Desktop Entry ID" before anything runs.
readonly property var fieldRe: /%[fFuU]/
readonly property var anyFieldRe: /^%[a-zA-Z]$/
readonly property var uwsmIdRe: /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*\.desktop(:.*)?$/
function launch(item: var, arg: string): void {
root._remember(item.key);
const byId = root.uwsmIdRe.test(item.key);
if (arg === "") {
if (byId && arg === "") {
Quickshell.execDetached(["uwsm", "app", "--", item.key]);
return;
}
if (root.fieldRe.test(item.exec || "")) {
if (byId && root.fieldRe.test(item.exec || "")) {
Quickshell.execDetached(["uwsm", "app", "--", item.key, arg]);
return;
}
@@ -323,10 +330,10 @@ Singleton {
const argv = root._tokenize(item.exec || "")
.filter(t => !root.anyFieldRe.test(t));
if (argv.length === 0) {
Quickshell.execDetached(["uwsm", "app", "--", item.key]);
if (byId) Quickshell.execDetached(["uwsm", "app", "--", item.key]);
return;
}
argv.push(arg);
if (arg !== "") argv.push(arg);
// Without this the unit is named after the bare executable and every
// app launched this way collides in the unit namespace.
+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;
}
}
}
+3 -3
View File
@@ -58,9 +58,9 @@ Singleton {
}
readonly property color tint: {
if (root.charging || root.full) return Theme.ok;
if (root.critical) return Theme.accent;
if (root.low) return Theme.warn;
if (root.charging || root.full) return Theme.text;
if (root.critical) return Theme.alarm;
if (root.low) return Theme.alarm;
return Theme.primary;
}
+1 -1
View File
@@ -81,7 +81,7 @@ Singleton {
// Urgency-driven accents for the popup chrome.
function urgencyColor(u: int): color {
switch (u) {
case NotificationUrgency.Critical: return Theme.accent;
case NotificationUrgency.Critical: return Theme.alarm;
case NotificationUrgency.Low: return Theme.muted;
default: return Theme.primary;
}
+8
View File
@@ -4,6 +4,7 @@ import "root:/config"
import "root:/services"
import "root:/bar"
import "root:/launcher"
import "root:/ask"
import "root:/overlays"
import "root:/lock"
@@ -12,6 +13,7 @@ import "root:/lock"
// bar/ the top bar and its modules
// popouts/ hover panels hanging off bar modules
// launcher/ the application launcher
// ask/ the question panel, dropped from the top edge
// overlays/ power menu, notification toasts, OSD
// lock/ session lock
// services/ data sources (fluxo, pipewire, bluez, upower, mpris, …)
@@ -51,6 +53,12 @@ ShellRoot {
Launcher {}
}
Variants {
model: Quickshell.screens
AskPanel {}
}
Variants {
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/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
+14
View File
@@ -0,0 +1,14 @@
# Written by `enqs-headroom service install`.
[Unit]
Description=Router for <name>.headroom.localhost
Documentation=https://gitlab.enqs.de/npu/claude-code-enqs/-/wikis/Headroom
After=network-online.target
[Service]
Environment=ENQS_HEADROOM_ROUTER_PORT=80
ExecStart=/home/narl/.local/bin/enqs-headroom serve --port 80
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
-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
+9
View File
@@ -0,0 +1,9 @@
Net/ThemeName "catppuccin-mocha-blue-standard+default"
Net/IconThemeName "Papirus-Dark"
Gtk/CursorThemeName "Bibata-Modern-Classic"
Net/EnableEventSounds 1
EnableInputFeedbackSounds 0
Xft/Antialias 1
Xft/Hinting 1
Xft/HintStyle "hintslight"
Xft/RGBA "rgb"