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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 01:53:56 +02:00
nvrlandClaude Opus 5 1cf5a4b68d chore: add enqs-headroom user service, set nvim as text/plain handler
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 01:52:50 +02:00
nvrl 5f3a44f82d fixed launcher nav + headroom completion 2026-09-17 12:21:09 +02:00
90 changed files with 1963 additions and 1348 deletions

No files matched your search

+16
View File
@@ -28,3 +28,19 @@
fish/fish_variables fish/fish_variables
waybar/fluxo-rs waybar/fluxo-rs
!fluxo/ !fluxo/
# Desktop theming. These live outside the per-app configs above, but they are
# what makes an app that is neither GTK-aware nor shell-aware match the rice,
# so the branch carries them.
!gtk-3.0/
gtk-3.0/*
!gtk-3.0/settings.ini
!gtk-3.0/gtk.css
!gtk-4.0/
gtk-4.0/*
!gtk-4.0/settings.ini
!gtk-4.0/gtk.css
!xsettingsd/
!qt6ct/
qt6ct/*
!qt6ct/qt6ct.conf
+1 -1
View File
@@ -107,7 +107,7 @@ ipc_socket = true
## GENERAL -------------------------------------------------------- ## GENERAL --------------------------------------------------------
## Import additional configuration files. ## Import additional configuration files.
import = ["~/.config/alacritty/takemi.toml", "~/.config/alacritty/fonts.toml"] import = ["~/.config/alacritty/catppuccin-mocha.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]] [[keyboard.bindings]]
key = "Return" key = "Return"
+4 -2
View File
@@ -4,9 +4,11 @@ foreground = "#cdd6f4"
dim_foreground = "#7f849c" dim_foreground = "#7f849c"
bright_foreground = "#cdd6f4" bright_foreground = "#cdd6f4"
# Cursor in the shell accent (Mocha blue) rather than stock rosewater, so the
# terminal caret matches the focus colour everywhere else on the desktop.
[colors.cursor] [colors.cursor]
text = "#1e1e2e" text = "#11111b"
cursor = "#f5e0dc" cursor = "#89b4fa"
[colors.vi_mode_cursor] [colors.vi_mode_cursor]
text = "#1e1e2e" text = "#1e1e2e"
+2 -2
View File
@@ -1,10 +1,10 @@
[global] [global]
corner_radius = 4 corner_radius = 4
transparency = 90 transparency = 90
frame_color = "#f38ba8" frame_color = "#89b4fa"
frame_width = 1 frame_width = 1
separator_color= frame separator_color= frame
highlight = "#89dceb" highlight = "#cba6f7"
[urgency_low] [urgency_low]
background = "#1e1e2e" background = "#1e1e2e"
+100
View File
@@ -0,0 +1,100 @@
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_claude_account_global_optspecs
string join \n h/help V/version
end
function __fish_claude_account_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_claude_account_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end
function __fish_claude_account_using_subcommand
set -l cmd (__fish_claude_account_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end
complete -c claude-account -n "__fish_claude_account_needs_command" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_needs_command" -s V -l version -d 'Print version'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "status" -d 'Show every account, every proxy, every name and everything that is wrong'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "list" -d 'List the accounts, one per line'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "switch" -d 'Make an account the one `claude` runs as'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "add" -d 'Register another account, with a profile and a proxy of its own'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "remove" -d 'Forget an account. Its profile directory is left where it is'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "current" -d 'Print the name of the active account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "dir" -d 'Print the profile directory of an account, for `CLAUDE_CONFIG_DIR`'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "env" -d 'Print the shell lines that put this shell into an account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "url" -d 'Print the address a name resolves to'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "setup" -d 'Install everything at once: the wrapper, a proxy per account, the router'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "adopt" -d 'Take over the proxy services that were written by hand'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "doctor" -d 'Report every problem and exit non-zero when there is one'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "shim" -d 'The `claude` wrapper in ~/.local/bin'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "headroom" -d 'The proxy of one account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "router" -d 'The router that resolves <name>.headroom.localhost'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "completions" -d 'Tab completion, with the account names in it'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s v -l verbose -d 'Add the last lines each proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -l names -d 'Print the names alone, for scripts and for tab completion'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand switch" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l dir -d 'A profile directory elsewhere' -r -F
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l port -d 'A port for its proxy; the next free one by default' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l switch -d 'Make it the account `claude` runs as straight away'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand remove" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand current" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand dir" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand env" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand url" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand setup" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand adopt" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand doctor" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "install" -d 'Write ~/.local/bin/claude'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "uninstall" -d 'Remove it again'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -l force -d 'Overwrite a `claude` that this tool did not write'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "install" -d 'Write the service for an account and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "start" -d 'Start the proxy of an account'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "stop" -d 'Stop it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "restart" -d 'Stop and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "port" -d 'Give an account another port, and write it everywhere it is named'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "logs" -d 'Print what one proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from start" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from stop" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from restart" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s n -l lines -d 'How many lines' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "serve" -d 'Serve the names until stopped'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "install" -d 'Install the router as a user service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "port" -d 'Move the router to another port'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "list" -d 'List the names that resolve'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -l port -r
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -l install -d 'Write the file where the shell reads it, instead of printing it'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -s h -l help -d 'Print help'
# The accounts as they are right now, asked for when Tab is pressed.
complete -c claude-account -n '__fish_seen_subcommand_from switch dir env url remove start stop restart logs install uninstall port' -f -a '(claude-account list --names)'
+1 -46
View File
@@ -1,46 +1 @@
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. complete --keep-order --exclusive --command enqs-guard --arguments "(COMPLETE=fish enqs-guard -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
function __fish_enqs_guard_global_optspecs
string join \n h/help V/version
end
function __fish_enqs_guard_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_enqs_guard_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_enqs_guard_using_subcommand
set -l cmd (__fish_enqs_guard_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -s V -l version -d 'Print version'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "hook" -d 'Answer as a PreToolUse hook; reads the event from stdin (Claude Code)'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "session-start" -d 'Answer as a SessionStart hook (Claude Code)'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "status" -d 'Show the current guard state'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "init" -d 'Create a configuration template'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "mode" -d 'Set what the guard does'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "add" -d 'Put a project on one of the lists'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "remove" -d 'Take a project off one of the lists'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "check" -d 'Check whether a path is blocked'
complete -c enqs-guard -n "__fish_enqs_guard_needs_command" -f -a "completions" -d 'Print a tab completion script for a shell'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand hook" -s h -l help -d 'Print help'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand session-start" -s h -l help -d 'Print help'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand status" -s h -l help -d 'Print help'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand init" -s h -l help -d 'Print help'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand mode" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand add" -s h -l help -d 'Print help'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand remove" -s h -l help -d 'Print help'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand check" -s h -l help -d 'Print help'
complete -c enqs-guard -n "__fish_enqs_guard_using_subcommand completions" -s h -l help -d 'Print help'
+1
View File
@@ -0,0 +1 @@
complete --keep-order --exclusive --command enqs-headroom --arguments "(COMPLETE=fish enqs-headroom -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
+1 -70
View File
@@ -1,70 +1 @@
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. complete --keep-order --exclusive --command enqs-jail --arguments "(COMPLETE=fish enqs-jail -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
function __fish_enqs_jail_global_optspecs
string join \n h/help V/version
end
function __fish_enqs_jail_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_enqs_jail_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_enqs_jail_using_subcommand
set -l cmd (__fish_enqs_jail_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -s V -l version -d 'Print version'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "register" -d 'Register a directory as a jail'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "unregister" -d 'Remove a registration'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "list" -d 'Show the registered jails'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "show" -d 'Show the settings a jail would run under'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "check" -d 'Check the prerequisites'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "allow" -d 'Re-open one path inside a jail'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "escape" -d 'Let one command run outside the sandbox, leaving the jail'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "revoke" -d 'Take a grant back'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "grants" -d 'Show the exceptions in force for a jail'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "completions" -d 'Print a tab completion script for a shell'
complete -c enqs-jail -n "__fish_enqs_jail_needs_command" -f -a "run" -d 'Start a session in a registered jail'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l name -d 'The name of the jail; the directory name by default' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l toolchain -d 'Toolchain whose caches stay readable; detected from the project when not given' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l deny-read -d 'Path hidden inside the jail; replaces the default list' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l allow-read -d 'Path that stays readable inside the jail (repeatable)' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l allow-write -d 'Path that stays writable inside the jail (repeatable)' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l add-dir -d 'Additional working directory inside the jail (repeatable)' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l claude-config-dir -d 'Claude profile the session runs under, e.g. ~/.claude-work' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l no-git -d 'Do not release the git configuration'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l no-default-deny -d 'Do not use the default deny list'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -l force -d 'Overwrite an existing registration of this name'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand register" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand unregister" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand list" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand show" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand check" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand allow" -l for -d 'How long it should last, e.g. 30m, 2h, 7d' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand allow" -l reason -d 'Why it was needed, kept with the grant' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand allow" -l jail -d 'The jail; the current directory when left out' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand allow" -l write -d 'Make it writable as well as readable'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand allow" -l once -d 'Only for the next session'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand allow" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand escape" -l for -d 'How long it should last, e.g. 30m, 2h, 7d' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand escape" -l reason -d 'Why it was needed, kept with the grant' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand escape" -l jail -d 'The jail; the current directory when left out' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand escape" -l once -d 'Only for the next session'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand escape" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand revoke" -l jail -d 'The jail; the current directory when left out' -r
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand revoke" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand grants" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand completions" -s h -l help -d 'Print help'
complete -c enqs-jail -n "__fish_enqs_jail_using_subcommand run" -s h -l help -d 'Print help'
+43 -41
View File
@@ -1,50 +1,52 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml, # Mocha — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua. # ~/.config/alacritty/catppuccin-mocha.toml and ~/.config/nvim/lua/takemi.lua.
# #
# Replaces the catppuccin fish_frozen_theme.fish that fish 4.3 generated on # Catppuccin Mocha's own fish theme (themes/Catppuccin Mocha.theme), with the
# upgrade. Same content as themes/Takemi.theme — that copy exists only so # selection and pager rows carried over from the Takemi layout: a solid accent
# `fish_config` can preview it; this one is what actually loads. # block with crust punched out of it, rather than a tinted surface. The shell
# selects with a blue slab and marks with mauve; the prompt should not
# disagree.
# #
# Note that `fish_config` writing a theme from the web UI will recreate # Note that `fish_config` writing a theme from the web UI will recreate
# fish_frozen_theme.fish, which sorts before this file and so would lose. # fish_frozen_theme.fish, which sorts before this file and so would lose.
set --global fish_color_normal ffffff set --global fish_color_normal cdd6f4
set --global fish_color_command ffffff --bold set --global fish_color_command 89b4fa
set --global fish_color_keyword ff2d40 set --global fish_color_keyword f38ba8
set --global fish_color_param c8c8ce set --global fish_color_param f2cdcd
set --global fish_color_option 8d9bb8 set --global fish_color_option a6e3a1
set --global fish_color_quote 7f9b6a set --global fish_color_quote a6e3a1
set --global fish_color_escape 9db8bd set --global fish_color_escape eba0ac
set --global fish_color_operator c8c8ce set --global fish_color_operator f5c2e7
set --global fish_color_redirection ff6b78 set --global fish_color_redirection f5c2e7
set --global fish_color_end 6b6b70 set --global fish_color_end fab387
set --global fish_color_comment 6b6b70 --italics set --global fish_color_comment 7f849c --italics
set --global fish_color_error ff2d40 --bold set --global fish_color_error f38ba8 --bold
set --global fish_color_cancel ff2d40 set --global fish_color_cancel f38ba8
set --global fish_color_gray 6b6b70 set --global fish_color_gray 6c7086
set --global fish_color_autosuggestion 4a4a52 set --global fish_color_autosuggestion 6c7086
set --global fish_color_selection 000000 --background=ff2d40 set --global fish_color_selection 11111b --background=89b4fa
set --global fish_color_search_match 000000 --background=c8c8ce set --global fish_color_search_match 11111b --background=cba6f7
set --global fish_color_match 000000 --background=ff2d40 set --global fish_color_match 11111b --background=89b4fa
set --global fish_color_history_current --bold set --global fish_color_history_current --bold
set --global fish_color_valid_path --underline set --global fish_color_valid_path --underline
set --global fish_color_cwd ffffff --bold set --global fish_color_cwd f9e2af --bold
set --global fish_color_cwd_root ff2d40 --bold set --global fish_color_cwd_root f38ba8 --bold
set --global fish_color_user c8c8ce set --global fish_color_user 94e2d5
set --global fish_color_host ffffff --bold set --global fish_color_host 89b4fa --bold
set --global fish_color_host_remote ff6b78 set --global fish_color_host_remote a6e3a1
set --global fish_color_status ff2d40 set --global fish_color_status f38ba8
set --global fish_pager_color_progress 6b6b70 --background=141418 set --global fish_pager_color_progress 6c7086 --background=313244
set --global fish_pager_color_background set --global fish_pager_color_background
set --global fish_pager_color_prefix ffffff --bold set --global fish_pager_color_prefix cba6f7 --bold
set --global fish_pager_color_completion c8c8ce set --global fish_pager_color_completion cdd6f4
set --global fish_pager_color_description 6b6b70 set --global fish_pager_color_description 6c7086
set --global fish_pager_color_secondary_background --background=0d0d10 set --global fish_pager_color_secondary_background --background=181825
set --global fish_pager_color_secondary_completion c8c8ce set --global fish_pager_color_secondary_completion cdd6f4
set --global fish_pager_color_secondary_description 6b6b70 set --global fish_pager_color_secondary_description 6c7086
set --global fish_pager_color_secondary_prefix ffffff --bold set --global fish_pager_color_secondary_prefix cba6f7 --bold
set --global fish_pager_color_selected_background --background=ff2d40 set --global fish_pager_color_selected_background --background=89b4fa
set --global fish_pager_color_selected_completion 000000 set --global fish_pager_color_selected_completion 11111b
set --global fish_pager_color_selected_description 000000 set --global fish_pager_color_selected_description 11111b
set --global fish_pager_color_selected_prefix 000000 --bold set --global fish_pager_color_selected_prefix 11111b --bold
-3
View File
@@ -38,6 +38,3 @@ if status is-login && test -z "$DISPLAY" -a -z "$WAYLAND_DISPLAY"
end end
end end
# Claude environments
alias claude="CLAUDE_CONFIG_DIR=~/.claude command claude"
alias claude-work="CLAUDE_CONFIG_DIR=~/.claude-work command claude"
+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 { wallpaper {
monitor = monitor =
path = ~/.wallpapers/takemi_3.jpg path = ~/.wallpapers/arch.png
fit_mode = cover 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 -- 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 -- handed once, which made searching a two-step. It spawns its own terminal
-- (class pdfs-prompt, floated in lua/rules/windows.lua). -- (class pdfs-prompt, floated in lua/rules/windows.lua).
pdfs = "pdfs-prompt --fzf", pdfs = "pdfs-prompt --gtk",
-- bluetooth menu -- bluetooth menu
bluetooth = "fluxo bt 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 text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --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 -- nextcloud --background")
-- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive") -- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive")
-- hl.exec_cmd("uwsm app -- protonvpn-app") -- 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("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
shell("power", { key = mod .. " + P", description = "power menu" }) shell("power", { key = mod .. " + P", description = "power menu" })
shell("ask", { key = mod .. " + A", description = "ask claude" })
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" }) shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" }) shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" }) shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
-- 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("INTEL_DEBUG", "noccs")
-- hl.env("WLR_DRM_NO_ATOMIC", "1") -- hl.env("WLR_DRM_NO_ATOMIC", "1")
-- hl.env("debug:full_cm_proto", "true") -- hl.env("debug:full_cm_proto", "true")
-- headroom
hl.env("HEADROOM_ROLLOUT_CHANNEL", "beta")
hl.env("HEADROOM_OUTPUT_SHAPER", "1")
+15 -8
View File
@@ -1,4 +1,5 @@
-- Takemi — Persona 5 flavoured theme, matched to ~/.config/quickshell. -- Persona × Mocha — Persona 5 shapes on the Catppuccin Mocha palette, matched
-- to ~/.config/quickshell.
-- --
-- Everything static about how the desktop is painted. Motion lives next door -- Everything static about how the desktop is painted. Motion lives next door
-- in lua/animations.lua. -- in lua/animations.lua.
@@ -15,7 +16,10 @@ hl.config({
col = { col = {
active_border = c.activeBorder, active_border = c.activeBorder,
inactive_border = c.outline, -- Surface-level, not outline: an idle window's frame is structure,
-- and the outline grey was bright enough to compete with the
-- focused border.
inactive_border = c.surfaceAlt,
}, },
resize_on_border = false, resize_on_border = false,
@@ -35,13 +39,16 @@ hl.config({
active_opacity = 1.0, active_opacity = 1.0,
inactive_opacity = 1.0, inactive_opacity = 1.0,
-- A hard crust offset, the same printed drop the shell's panels
-- carry, rather than a soft blur. Sharp with a 1px range leaves a
-- clean band down the right and bottom edges only.
shadow = { shadow = {
enabled = true, enabled = true,
range = 18, sharp = true,
render_power = 3, range = 1,
offset = { 0, 4 }, offset = { 4, 4 },
color = "rgba(000000ee)", color = c.ink,
color_inactive = "rgba(00000088)", color_inactive = c.ink,
}, },
-- Off. Blur is the signature of the glass-and-frost look, which is the -- Off. Blur is the signature of the glass-and-frost look, which is the
@@ -66,7 +73,7 @@ hl.config({
group = { group = {
col = { col = {
border_active = c.activeBorder, border_active = c.activeBorder,
border_inactive = c.outline, border_inactive = c.surfaceAlt,
}, },
groupbar = { groupbar = {
+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 -- Returned as a table, so every module that paints something does
-- local c = require("lua/palette") -- local c = require("lua/palette")
@@ -8,44 +8,45 @@
local c = {} local c = {}
-- ----------------------------------------------------------------- THE KNOB -- ---------------------------------------------------------------- THE KNOBS
-- The one colour in this setup. Change this line and the window borders, -- Two colours in this setup: blue for anything live (window borders, group
-- group indicators and focus states all recolour with it. -- 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. -- ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
-- Those are the only two places the colour is written. -- Those are the only places the colours are written.
c.accent = "rgb(ff2d40)" c.accent = "rgb(89b4fa)"
c.alarm = "rgb(cba6f7)"
c.accentDim = "rgb(8f1826)" c.accentDim = "rgb(5a7dc0)"
c.accentSoft = "rgb(ff6b78)" c.accentSoft = "rgb(b4befe)"
-- ------------------------------------------------------------------ neutrals -- ------------------------------------------------------------------ neutrals
c.ink = "rgb(000000)" c.ink = "rgb(11111b)"
c.base = "rgb(0a0a0c)" c.base = "rgb(1e1e2e)"
c.mantle = "rgb(0d0d10)" c.mantle = "rgb(181825)"
c.surface = "rgb(141418)" c.surface = "rgb(313244)"
c.surfaceAlt = "rgb(1c1c22)" c.surfaceAlt = "rgb(45475a)"
c.overlay = "rgb(26262e)" c.overlay = "rgb(585b70)"
c.outline = "rgb(3a3a44)" c.outline = "rgb(6c7086)"
c.text = "rgb(ffffff)" c.text = "rgb(cdd6f4)"
c.subtext = "rgb(c8c8ce)" c.subtext = "rgb(a6adc8)"
c.muted = "rgb(6b6b70)" c.muted = "rgb(7f849c)"
-- Retired hues, kept as aliases so existing references resolve. The shell has -- Semantic aliases, so existing references resolve without inventing hues.
-- no teal any more — the wallpaper is the colour in the composition.
c.primary = c.text c.primary = c.text
c.primaryDim = "rgb(9a9aa2)" c.primaryDim = "rgb(9399b2)"
c.glow = c.accent c.glow = c.accent
c.blue = c.primaryDim c.blue = c.accent
c.deep = c.surfaceAlt c.deep = c.surfaceAlt
c.warn = c.accentSoft c.warn = c.alarm
c.ok = c.text c.ok = c.text
-- Crimson into its own shadow, on the diagonal. The accent is the only hue -- Blue into lavender, on the diagonal. Both ends are the accent's own family,
-- here — a gradient into a second colour is what put blue in the border. -- so the border never reads as two colours arguing.
c.activeBorder = { colors = { c.accent, c.accentDim }, angle = 135 } c.activeBorder = { colors = { c.accent, c.accentSoft }, angle = 135 }
return c return c
+2
View File
@@ -22,6 +22,7 @@ x-scheme-handler/msteams=teams-for-linux.desktop
x-scheme-handler/slack=slack.desktop x-scheme-handler/slack=slack.desktop
audio/flac=mpv.desktop audio/flac=mpv.desktop
x-scheme-handler/codex=chatgpt.desktop x-scheme-handler/codex=chatgpt.desktop
x-scheme-handler/proton-inbox=proton-mail.desktop
[Added Associations] [Added Associations]
application/pdf=org.pwmt.zathura-pdf-poppler.desktop; application/pdf=org.pwmt.zathura-pdf-poppler.desktop;
@@ -40,3 +41,4 @@ application/xhtml+xml=zen.desktop;
application/x-extension-xhtml=zen.desktop; application/x-extension-xhtml=zen.desktop;
application/x-extension-xht=zen.desktop; application/x-extension-xht=zen.desktop;
audio/flac=mpv.desktop; audio/flac=mpv.desktop;
text/plain=org.neovim.nvim.desktop;
+1 -1
Submodule nvim updated: 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, A Persona 5 flavoured Quickshell desktop for Hyprland, painted in Catppuccin
with no gradients anywhere. Everything leans by the same 14°, corners are Mocha. Blue for anything live, mauve for anything wrong, the Mocha greys for
chopped rather than rounded, and every surface is one continuous slab divided by everything else, and no gradients anywhere. Everything leans by the same 14°,
hairlines rather than a cluster of floating cards. corners are chopped rather than rounded, and every surface is one continuous
slab divided by hairlines rather than a cluster of floating cards.
Replaces Waybar, dunst and hyprlock. Replaces Waybar, dunst and hyprlock.
## Running ## Running
```sh ```sh
qs # starts the shell (Hyprland does this via autostart.conf) qs # starts the shell (Hyprland does this via autostart.lua)
qs -p lockpreview.qml # the lock screen in an ordinary window, for styling qs -p lockpreview.qml # the lock screen in an ordinary window, for styling
``` ```
Hyprland starts it from `~/.config/hypr/modules/autostart.conf`. Hyprland starts it from `~/.config/hypr/lua/autostart.lua`.
## Layout ## Layout
@@ -41,8 +42,8 @@ cost 4.4% of the height on top of what the bar already takes.
Each permanent surface is a single continuous `ChromeSurface` with modules Each permanent surface is a single continuous `ChromeSurface` with modules
divided by sheared hairlines. `BarPill` is chromeless by default — a module is divided by sheared hairlines. `BarPill` is chromeless by default — a module is
*content inside* a surface, not a surface of its own — and marks hover with a *content inside* a surface, not a surface of its own — and marks hover with an
crimson underline. Pass `standalone: true` for the rare thing that really is its accent underline. Pass `standalone: true` for the rare thing that really is its
own panel. own panel.
The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is
@@ -55,7 +56,7 @@ it is never used as decoration inside ordinary chrome or popouts.
``` ```
shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves config/ palette, type scale, geometry, motion curves
components/ the P5 primitives: panels, gauges, sliders, split-ink text components/ the P5 primitives: panels, sliders, list rows, print-shadowed text
services/ data sources, all singletons services/ data sources, all singletons
bar/ the two surfaces and their modules bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar modules popouts/ hover panels hanging off rail and bar modules
@@ -106,12 +107,13 @@ dismisses the screen.
## Keybinds ## Keybinds
Set in `~/.config/hypr/modules/keybinds.conf`: Set in `~/.config/hypr/lua/binds/shell.lua`:
- `Super+Ctrl+L` — lock - `Super+Ctrl+L` — lock
- `Super+P` — power menu - `Super+P` — power menu
- `Super+Shift+N` — toggle do not disturb - `Super+Shift+N` — toggle do not disturb
- `Super+Ctrl+Shift+N` — clear notifications - `Super+Ctrl+Shift+N` — clear notifications
- `Super+A` — ask panel
- `Super+Shift+R` — reload the shell - `Super+Shift+R` — reload the shell
All of these go through Quickshell's IPC, so they work from any script: All of these go through Quickshell's IPC, so they work from any script:
@@ -120,10 +122,52 @@ All of these go through Quickshell's IPC, so they work from any script:
qs ipc call shell lock qs ipc call shell lock
qs ipc call shell power qs ipc call shell power
qs ipc call shell toggleDnd qs ipc call shell toggleDnd
qs ipc call shell ask
qs ipc call shell askAbout "why is the sky blue"
qs ipc call shell clearNotifications qs ipc call shell clearNotifications
qs ipc call shell reloadConfig qs ipc call shell reloadConfig
``` ```
## Ask
`Super+A` drops a panel off the top edge and hands whatever you type to
`claude -p`, streamed token by token. It is the subscription that answers —
the CLI already holds the credentials, so nothing here has an API key and
nothing here has to be given one.
The panel is a chat, not a coding session. Settings sources, MCP servers and
project files are switched off by default, which keeps a question asked from
the desktop from arriving with the last project's hooks, `CLAUDE.md` and tone
attached. The thread survives closing the panel; `Ctrl+L` starts a new one.
| Key | Does |
|---|---|
| `Enter` | ask |
| `Shift+Enter` | newline |
| `Ctrl+M` | next model, written back to `ask.json` |
| `Ctrl+Y` | copy the last answer |
| `Ctrl+L` | new conversation |
| `Ctrl+S` | stop the answer |
| `Escape` | hide, keeping the thread |
Configured in `ask.json`, watched and reloaded live:
```json
{
"model": "sonnet",
"systemPrompt": "…",
"keepContext": true,
"settingSources": "",
"allowedTools": ["WebSearch", "WebFetch"],
"disallowedTools": ["Bash", "Edit", "Write"],
"workingDirectory": "/home/narl",
"maxTurns": 6
}
```
`settingSources` is the escape hatch: set it to `"user,project,local"` and the
panel becomes your ordinary Claude Code, hooks and all.
## Data sources ## Data sources
Hardware telemetry comes from `fluxo`, whose hardware modules are configured to Hardware telemetry comes from `fluxo`, whose hardware modules are configured to
@@ -165,69 +209,81 @@ The display face is Archivo Black, set italic almost everywhere. Change
## Palette discipline ## Palette discipline
Crimson, black, white. That is the whole palette. Catppuccin Mocha, spent on two hues. **Hue is meaning**; nothing gets a colour
for decoration.
**Changing the accent.** The colour is written in exactly three places, one per - **Blue (`accent`, Mocha blue)** means live: the focused workspace, the
program: selected row, the hovered control, a channel that is on, an unread badge.
- **Mauve (`alarm`, Mocha mauve)** means wrong or destructive: critical battery,
an urgent workspace, a critical notification, reboot and shut down.
- **Grey** is everything else, and that includes *off*. A muted speaker, a
muted mic and Do Not Disturb drop to `muted` grey with their off glyph. They
used to be painted in the accent, which made a silenced channel look the most
alive thing on the rail.
- **Crust (`ink`)** is structure: the hard keyline, the offset under display
type and panels, the text on an accent slab.
| File | Line | Covers | **Selection is an inverted slab.** Focus and selection fill with the accent and
|---|---|---| print their text in crust (about 9:1). Hover stays on the surface tone with an
| `quickshell/config/Theme.qml` | `readonly property color accent: "#ff2d40"` | the shell | accent mark. Destructive-active is the same slab in mauve. The `treatment*`
| `hypr/modules/takemi.conf` | `$accent = rgb(ff2d40)` | window borders, groups | tokens in `Theme.qml` encode all of this; use them rather than picking colours
| `alacritty/takemi.toml` | `red`, cursor, selection | the terminal | per widget.
Everything else derives. `accentDim` and `accentSoft` come off `accent` via **Print, not blur.** Display text (`SplitText`) carries one hard crust offset.
`Qt.darker`/`Qt.lighter`, `glow` (the hover highlight) *is* `accent`, and every The blue and mauve ghosts appear only while `glitch()` runs. Drawn permanently
Hyprland border, group indicator and focus state references `$accent`. Change 1.6px off the body, they read as a focus problem on Mocha's pastels rather than
those lines and the whole setup recolours — swap in Persona 4's yellow or as misregistered print. The lock screen and the power menu are the exceptions;
Persona 3's blue and nothing else needs touching. they are the cinematic surfaces and keep their split.
**Texture is tiered.** Chrome (bar, rail, tooltips) and transient panels
(popouts, OSD, toasts) are flat. Stripes and halftone live only in
`P5Backdrop`, behind the lock screen and the power menu.
**Type has a floor.** Nothing renders below `Theme.fsMicro` (10px).
**Changing the palette.** The colours are written once per program:
| File | Covers |
|---|---|
| `quickshell/config/Theme.qml` | the shell: `accent`, `alarm` and the neutral ramp |
| `hypr/lua/palette.lua` | window borders, groups, shadows |
| `alacritty/catppuccin-mocha.toml` | the terminal (stock Mocha, cursor in the accent) |
Everything else derives. `accentDim` and `accentSoft` come off `accent`, `glow`
*is* `accent`, and `primary` is plain `text`. `Theme.heat()` is a continuous
text → blue → mauve ramp, so a CPU figure that has gone mauve means the same
thing as a battery that has gone mauve. Tray icons are flattened to one colour
(subtext, the accent on hover) for the same reason: app brand colours wreck the
palette faster than anything else in the shell. nm-applet and blueman are
hidden from the tray, since the rail already shows network and bluetooth, but
keep running as the secret and pairing agents.
## The rest of the desktop ## The rest of the desktop
- **Alacritty** — `alacritty/takemi.toml`, selected by the `import` in - **Hyprland** — `hypr/lua/look.lua`. No rounding, no blur, both opacities at
`alacritty.toml`. The Catppuccin files are left in place, just unreferenced. `1.0`. The active border runs blue into lavender and the inactive border is
Near-black ground, white text, crimson cursor and selection. The sixteen ANSI `surfaceAlt`. The window shadow is a sharp 4px crust offset, the same printed
slots keep their hues because syntax highlighters need to tell them apart, but drop the shell's panels carry, not a soft glow.
every one is desaturated so nothing competes with the accent. - **Alacritty** — `alacritty/catppuccin-mocha.toml`, selected by the `import`
- **Neovim** — `nvim/lua/takemi.lua`, loaded through `nvim/colors/takemi.lua` in `alacritty.toml`. Stock Mocha except for the cursor, which is the accent.
so `:colorscheme takemi` and `require("takemi").lualine()` both work. Same `takemi.toml` is left in place, unreferenced.
palette, same rule: crimson is spent only on keywords, types and errors, - **Neovim** — the `catppuccin` plugin, flavour mocha, configured in
functions are plain white-bold, and `terminal_color_0..15` mirror `nvim/lua/plugins/ui.lua`. Blue is the accent there too. `colors/takemi.lua`
`takemi.toml` slot for slot so `:terminal` matches the shell around it. is left in place, unused.
- **Fish** — `fish/conf.d/takemi_theme.fish`, with a preview copy in - **Fish** — `fish/conf.d/takemi_theme.fish` is Catppuccin Mocha's fish theme
`fish/themes/Takemi.theme` for `fish_config`. Replaced the Catppuccin with the Takemi selection rows: a solid accent block with crust punched out.
`fish_frozen_theme.fish` that fish 4.3 generated on upgrade. The prompt arrow
is white and turns crimson on a non-zero exit — the shell's one "wrong"
signal, and so the one place that earns the accent.
- **The launcher** — no external theming to keep in step, because it is part of - **The launcher** — no external theming to keep in step, because it is part of
this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for
`fuzzel --dmenu` where a script needs a picker. `fuzzel --dmenu` where a script needs a picker.
- **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and - **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and
Hyprland's `decoration:blur` is disabled with both opacities at `1.0`. Blur is Hyprland's `decoration:blur` is disabled. Blur is the signature of the
the signature of the frosted-glass look, which is the opposite of flat ink on frosted-glass look, which is the opposite of flat ink on flat ground — and
flat ground — and with nothing translucent left it was costing three render with nothing translucent left it was costing three render passes for no
passes for no visible result. visible result.
White is the resting voice — text, icons, idle indicators. Crimson is the only There are **no gradients in this shell**; every fill is flat. `Theme.sheen`
accent and always means one thing: active, or wrong. Black is structure, used as still exists as a no-op property because a teal-to-transparent wash on every
a hard keyline rather than as a soft shadow. **There are no gradients in this panel was once the biggest source of murk in the design.
shell**; every fill is flat. `Theme.sheen` still exists as a no-op property
because a teal-to-transparent wash on every panel was the single biggest source
of the murky blue cast the design used to have.
`primary`, `glow`, `blue`, `ok` and `warn` survive in `Theme.qml` only as
aliases onto white, crimson and grey, so the existing call sites keep working —
don't introduce new uses. `warn` used to alias `accentSoft`, which put a
washed-out pink-red wherever something was merely notable; there is no
half-crimson in this palette, so it aliases the real `accent` now and anything
merely informational (brightness, most readouts) is white instead. `Theme.heat()` is a continuous white→crimson ramp, so
a CPU figure that has gone red means the same thing as a battery that has gone
red. Tray icons are force-flattened to a single colour for the same reason: app
brand colours wreck the palette faster than anything else in the shell.
The wallpaper is teal. The UI used to be teal too, and the two fought each other
into a blue-grey mush. Going monochrome-plus-red is what lets the wallpaper be
the colour in the composition.
## Geometry discipline ## Geometry discipline
+11
View File
@@ -0,0 +1,11 @@
{
"model": "sonnet",
"fallbackModel": "",
"systemPrompt": "You are a desktop answer panel. Answer the question directly and compactly, in markdown, with no preamble. Prefer a few sentences over a list, and a list over an essay. Say plainly when you do not know.",
"keepContext": true,
"settingSources": "",
"allowedTools": ["WebSearch", "WebFetch"],
"disallowedTools": ["Bash", "Edit", "Write", "NotebookEdit", "Agent", "Task"],
"workingDirectory": "/home/narl",
"maxTurns": 6
}
+416
View File
@@ -0,0 +1,416 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/popouts"
import "root:/services"
// The ask panel. It comes down off the top edge, takes the keyboard, and hands
// a question to Claude.
//
// Deliberately not the launcher: the launcher is a list you pick from in one
// keystroke, this is a conversation you read. The shape says so — full width of
// the question, answer below it, and the thing drops from the top rather than
// rising into the middle so the two are never mistaken for each other mid-type.
PanelWindow {
id: root
required property var modelData
screen: modelData
// One screen at a time, latched at open — same reason as the launcher: the
// window takes focus, and a prompt should not hop monitors mid-question.
readonly property bool open: Actions.askOpen
&& Actions.askScreen === modelData.name
visible: root.open || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-ask"
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property int panelWidth: Math.min(860, root.width - Theme.padXL * 2)
readonly property int maxTranscript: Math.round(root.height * 0.55)
onOpenChanged: {
if (root.open) {
PopupCoordinator.dismissAll();
input.text = Actions.askSeed;
openAnim.restart();
input.forceActiveFocus();
input.cursorPosition = input.text.length;
if (Actions.askSeed !== "") root.send();
} else {
closeAnim.restart();
}
}
function send(): void {
const q = input.text.trim();
if (q === "" || Ask.busy) return;
Ask.ask(q);
input.text = "";
}
function copyLast(): void {
for (let i = Ask.turns.length - 1; i >= 0; i--) {
if (Ask.turns[i].role === "assistant" && Ask.turns[i].text !== "") {
Actions.copyText(Ask.turns[i].text);
return;
}
}
}
// -------------------------------------------------------------- backdrop
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.6)
opacity: 0
TapHandler { onTapped: Actions.closeAsk() }
}
// ----------------------------------------------------------------- panel
Item {
id: shell
width: root.panelWidth
height: header.height + divider.height + body.height + hint.height
anchors.horizontalCenter: parent.horizontalCenter
y: 0
opacity: 0
// The drop. The offset is animated, never bound: binding it to the
// panel's own height means every height change — every streamed line —
// snaps the panel back off the top of the screen.
transform: Translate { id: drop; y: 0 }
PopoutSurface {
anchors.fill: parent
tone: Ask.busy ? Theme.accent : Theme.primary
padding: 0
slashWidth: Ask.busy ? 8 : 5
// -------------------------------------------------------- header
Item {
id: header
width: parent.width
height: 62
anchors.top: parent.top
Row {
anchors.fill: parent
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
spacing: Theme.padM
// Pulses rather than spins. Native text rendering is
// subpixel-antialiased, so a rotated glyph smears into
// colour fringes; scale keeps it on the pixel grid.
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰭹"
color: Ask.busy ? Theme.accent : Theme.primary
font.pixelSize: Theme.fsTitle
pulsing: Ask.busy
pulseScale: 1.12
}
TextInput {
id: input
anchors.verticalCenter: parent.verticalCenter
width: parent.width - x - modelChip.width - Theme.padM
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsLarge
selectionColor: Theme.accent
selectedTextColor: Theme.ink
clip: true
focus: true
renderType: Text.NativeRendering
Keys.priority: Keys.BeforeItem
Keys.onPressed: event => root.handleKey(event)
Text {
anchors.verticalCenter: parent.verticalCenter
visible: input.text === ""
text: Ask.busy ? "thinking…"
: Ask.turns.length > 0 ? "follow up…" : "ask anything"
color: Theme.muted
font: input.font
renderType: Text.NativeRendering
}
}
// The model, which is also the control for it: Ctrl+M walks
// the ring and writes the choice back to ask.json, so the
// panel and the file never disagree.
Item {
id: modelChip
anchors.verticalCenter: parent.verticalCenter
width: modelText.width + Theme.padM * 2
height: 24
Skew {
anchors.fill: parent
color: Theme.surface
}
Text {
id: modelText
anchors.centerIn: parent
text: Ask.model.toUpperCase()
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Ask.cycleModel()
}
}
}
}
Skew {
id: divider
anchors.top: header.bottom
width: parent.width
height: Theme.stroke
color: Theme.alpha(Theme.outline, 0.8)
lean: 0
}
// --------------------------------------------------- transcript
Item {
id: body
anchors.top: divider.bottom
width: parent.width
height: Math.min(transcript.contentHeight + Theme.padM * 2,
root.maxTranscript)
visible: Ask.turns.length > 0 || Ask.error !== ""
clip: true
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
ListView {
id: transcript
anchors.fill: parent
anchors.margins: Theme.padM
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
model: Ask.turns
spacing: Theme.padM
boundsBehavior: Flickable.StopAtBounds
// Stay at the bottom while an answer streams in, but leave
// the view alone the moment it is scrolled by hand.
property bool pinned: true
onMovementEnded: pinned = atYEnd
onContentHeightChanged: if (pinned) positionViewAtEnd()
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: Column {
required property var modelData
width: transcript.width
spacing: Theme.padS
Text {
visible: modelData.role === "user"
text: "YOU"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
}
Text {
width: parent.width
text: modelData.text
textFormat: modelData.role === "assistant"
? Text.MarkdownText : Text.PlainText
color: modelData.role === "user" ? Theme.subtext : Theme.text
linkColor: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsBody
wrapMode: Text.Wrap
renderType: Text.NativeRendering
onLinkActivated: link => Actions.run(["xdg-open", link])
}
}
footer: Text {
width: transcript.width
visible: Ask.error !== ""
topPadding: visible ? Theme.padM : 0
text: Ask.error
color: Theme.alarm
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
}
// -------------------------------------------------------- hints
Item {
id: hint
anchors.top: body.visible ? body.bottom : divider.bottom
width: parent.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
text: Ask.busy
? "⏎ QUEUE CTRL+S STOP ESC HIDE"
: "⏎ ASK CTRL+M MODEL CTRL+Y COPY CTRL+L NEW ESC HIDE"
}
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
visible: Ask.turns.length > 0
text: Math.floor(Ask.turns.length / 2) + " TURN"
+ (Ask.turns.length > 2 ? "S" : "")
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
}
}
// ------------------------------------------------------------------ keys
function handleKey(event: var): void {
if (!root.open) return;
const ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
if (event.key === Qt.Key_Escape || (ctrl && event.key === Qt.Key_C)) {
// Hide, do not clear. The thread outlives the panel so that closing
// it to read something and reopening to ask about that is one
// conversation, not two.
Actions.closeAsk();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
// Shift+Enter is a newline; a multi-line question is still one
// question. Plain Enter sends.
if ((event.modifiers & Qt.ShiftModifier) === 0) {
root.send();
event.accepted = true;
}
return;
}
if (ctrl && event.key === Qt.Key_L) {
Ask.clear();
input.text = "";
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_S) {
Ask.stop();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_M) {
Ask.cycleModel();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_Y) {
root.copyLast();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_U) {
input.text = "";
event.accepted = true;
return;
}
// The transcript scrolls on the arrows, because the input is one line
// and has nowhere for them to go.
if (event.key === Qt.Key_PageDown || (ctrl && event.key === Qt.Key_J)) {
transcript.flick(0, -900);
event.accepted = true;
return;
}
if (event.key === Qt.Key_PageUp || (ctrl && event.key === Qt.Key_K)) {
transcript.flick(0, 900);
event.accepted = true;
return;
}
}
// ------------------------------------------------------------- animation
ParallelAnimation {
id: openAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
}
NumberAnimation {
target: shell; property: "opacity"
to: 1; duration: Theme.durFast
}
NumberAnimation {
target: drop; property: "y"
from: -shell.height; to: 0
duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: shell; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: drop; property: "y"
to: -shell.height; duration: Theme.durFast; easing.type: Easing.InQuad
}
}
}
+2 -2
View File
@@ -56,7 +56,7 @@ Item {
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
// Crimson duplicate: visible only during the landing transition. The // Accent duplicate: visible only during the landing transition. The
// small opposing offset gives the title Persona's rough print snap // small opposing offset gives the title Persona's rough print snap
// without leaving permanent motion in the bar. // without leaving permanent motion in the bar.
Text { Text {
@@ -119,7 +119,7 @@ Item {
landedToplevel = toplevel; landedToplevel = toplevel;
} }
// A quick crimson wipe under the title on focus change. // A quick accent wipe under the title on focus change.
Rectangle { Rectangle {
id: wipe id: wipe
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
+1 -1
View File
@@ -126,7 +126,7 @@ PanelWindow {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.ruleHeight * Theme.skew + parent.ruleWeight width: parent.ruleHeight * Theme.skew + parent.ruleWeight
height: parent.ruleHeight height: parent.ruleHeight
color: Theme.alpha(Theme.outline, 0.9) color: Theme.alpha(Theme.outline, 0.5)
} }
} }
} }
+3 -3
View File
@@ -5,7 +5,7 @@ import "root:/components"
// Shared chrome for every bar module. // Shared chrome for every bar module.
// //
// The bar is one continuous slab, so by default a module draws no panel of its // The bar is one continuous slab, so by default a module draws no panel of its
// own — it is content inside the bar's surface, and marks hover with a crimson // own — it is content inside the bar's surface, and marks hover with an accent
// underline rather than by lighting up a card. That is what stops the bar from // underline rather than by lighting up a card. That is what stops the bar from
// reading as a row of floating islands. // reading as a row of floating islands.
// //
@@ -34,7 +34,7 @@ Item {
// //
// The rule used to be drawn into the same box as the content, so anything // The rule used to be drawn into the same box as the content, so anything
// taller than about 32px — the media pill's art, the metric meters — had a // taller than about 32px — the media pill's art, the metric meters — had a
// crimson line struck through its bottom edge on hover. Reserving the band // accent line struck through its bottom edge on hover. Reserving the band
// and centring content in what is left is what keeps the rule under the // and centring content in what is left is what keeps the rule under the
// module instead of across it. // module instead of across it.
// The band reserved the full rule plus its inset — 7px off a 40px module, // The band reserved the full rule plus its inset — 7px off a 40px module,
@@ -102,7 +102,7 @@ Item {
anchors.bottomMargin: root.ruleBand anchors.bottomMargin: root.ruleBand
} }
// Hover mark for in-bar modules: a crimson rule under the content, wiping // Hover mark for in-bar modules: an accent rule under the content, wiping
// out from the leading edge. Sheared to match the one angle everything uses. // out from the leading edge. Sheared to match the one angle everything uses.
Item { Item {
visible: !root.standalone && root.interactive visible: !root.standalone && root.interactive
+2 -2
View File
@@ -36,7 +36,8 @@ BarPill {
color: Theme.accent color: Theme.accent
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.letterSpacing: 2.5 font.weight: Font.Bold
font.letterSpacing: 3
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -98,7 +99,6 @@ BarPill {
text: Qt.formatDateTime(clock.date, "HH:mm") text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: Theme.fsTitle pixelSize: Theme.fsTitle
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
split: 1.4
} }
} }
-72
View File
@@ -1,72 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Notification bell with an unread badge. Click opens the history, right click
// toggles do-not-disturb.
BarPill {
id: root
required property var screenRef
accent: Notifs.dnd ? Theme.muted : Theme.primary
padding: Theme.padS
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: {
popout.pinned = !popout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
Icon {
id: bell
anchors.centerIn: parent
text: Notifs.icon
font.pixelSize: Theme.fsLarge
color: {
if (Notifs.dnd) return Theme.muted;
if (Notifs.hasUnread) return Theme.accent;
return root.hovered ? Theme.glow : Theme.primary;
}
}
// Unread badge, chopped like everything else.
Skew {
visible: Notifs.unread > 0 && !Notifs.dnd
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: parent.top
anchors.topMargin: 4
width: Math.max(12, badge.implicitWidth + 5)
height: 12
color: Theme.accent
// The badge count stays upright. It used to inherit the shear from the
// transform on the block below, which at 8px left the digits smeared.
Text {
id: badge
anchors.centerIn: parent
text: Notifs.unread > 9 ? "9+" : Notifs.unread
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: 8
font.weight: Font.Bold
renderType: Text.NativeRendering
}
}
Connections {
target: Notifs
function onPopupRequested() { bell.bump(); }
}
NotifPopout {
id: popout
anchorItem: root
triggerHovered: root.hovered
}
}
-42
View File
@@ -1,42 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Far right. Opens the full-screen power menu.
BarPill {
id: root
required property var screenRef
accent: Theme.accent
slash: true
padding: Theme.padS
active: Actions.powerMenuOpen
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: Actions.togglePowerMenu()
Icon {
id: glyph
anchors.centerIn: parent
text: "󰐥"
font.pixelSize: Theme.fsLarge
color: root.hovered || Actions.powerMenuOpen ? Theme.accent : Theme.subtext
}
// A crimson halo that swells under the pointer.
Rectangle {
anchors.centerIn: glyph
z: -1
width: root.hovered ? 26 : 0
height: width
radius: width / 2
color: Theme.alpha(Theme.accent, 0.18)
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
}
+6 -6
View File
@@ -94,7 +94,7 @@ PanelWindow {
statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName
hint: "Scroll to adjust" hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive suppressTooltip: audioPopout.detailActive
alert: Audio.muted dim: Audio.muted
onClicked: audioPopout.togglePinned() onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05) onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
} }
@@ -106,7 +106,7 @@ PanelWindow {
statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName
hint: "Scroll to adjust" hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive suppressTooltip: audioPopout.detailActive
alert: Audio.micMuted dim: Audio.micMuted
onClicked: audioPopout.togglePinned() onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05)) onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
} }
@@ -132,7 +132,7 @@ PanelWindow {
label: "Battery" label: "Battery"
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
suppressTooltip: battPopout.detailActive suppressTooltip: battPopout.detailActive
alert: Battery.critical alarm: Battery.critical
pulsing: Battery.critical pulsing: Battery.critical
onClicked: battPopout.togglePinned() onClicked: battPopout.togglePinned()
} }
@@ -165,7 +165,7 @@ PanelWindow {
: "" : ""
reserveReadout: true reserveReadout: true
dim: Notifs.dnd dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd live: Notifs.hasUnread && !Notifs.dnd
suppressTooltip: notifPopout.detailActive suppressTooltip: notifPopout.detailActive
onClicked: { onClicked: {
notifPopout.togglePinned(); notifPopout.togglePinned();
@@ -178,7 +178,7 @@ PanelWindow {
icon: "󰐥" icon: "󰐥"
label: "Power" label: "Power"
statusText: "Session actions" statusText: "Session actions"
alert: Actions.powerMenuOpen live: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu() onClicked: Actions.togglePowerMenu()
} }
} }
@@ -201,7 +201,7 @@ PanelWindow {
width: parent.ruleWidth width: parent.ruleWidth
height: parent.ruleWidth * Theme.skew + parent.ruleWeight height: parent.ruleWidth * Theme.skew + parent.ruleWeight
vertical: true vertical: true
color: Theme.alpha(Theme.outline, 0.9) color: Theme.alpha(Theme.outline, 0.5)
} }
} }
+27 -13
View File
@@ -3,10 +3,10 @@ import "root:/config"
import "root:/components" import "root:/components"
// One slot on the right-hand rail: a glyph with an optional micro readout // One slot on the right-hand rail: a glyph with an optional micro readout
// stacked beneath it, and a crimson bar that wipes down the leading edge on // stacked beneath it, and an accent bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits // hover. The rail is only 48px wide, so the readout sits under the glyph
// under the glyph rather than beside it, and anything longer than a percentage // rather than beside it, and anything longer than a percentage belongs in the
// belongs in the popout instead. // popout instead.
Item { Item {
id: root id: root
@@ -17,7 +17,15 @@ Item {
property string hint: "" property string hint: ""
property bool suppressTooltip: false property bool suppressTooltip: false
property color tint: Theme.primary property color tint: Theme.primary
property bool alert: false
// Three states, three meanings. `live` wants attention but nothing is wrong
// (unread notifications, an open power menu): accent. `alarm` is wrong or
// critical (a dying battery): alarm. `dim` is off by choice (muted, radio
// off, do-not-disturb): grey. These used to be one `alert` flag painted in
// the accent, which made a muted speaker look like the most active thing
// on the rail.
property bool live: false
property bool alarm: false
property bool dim: false property bool dim: false
property bool pulsing: false property bool pulsing: false
@@ -63,7 +71,7 @@ Item {
id: glyphMetrics id: glyphMetrics
text: root.icon text: root.icon
font.family: Theme.fontIcon font.family: Theme.fontIcon
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsRailIcon
} }
Item { Item {
@@ -76,9 +84,10 @@ Item {
y: (root.glyphBand - height) / 2 y: (root.glyphBand - height) / 2
text: root.icon text: root.icon
pulsing: root.pulsing pulsing: root.pulsing
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsRailIcon
color: { color: {
if (root.alert) return Theme.accent; if (root.alarm) return Theme.alarm;
if (root.live) return Theme.accent;
if (root.dim) return Theme.muted; if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow; if (root.hovered) return Theme.glow;
return root.tint; return root.tint;
@@ -96,7 +105,11 @@ Item {
anchors.topMargin: root.glyphBand + 1 anchors.topMargin: root.glyphBand + 1
visible: root.readout !== "" visible: root.readout !== ""
text: root.readout text: root.readout
color: root.dim ? Theme.muted : Theme.subtext color: {
if (root.alarm) return Theme.alarm;
if (root.dim) return Theme.muted;
return Theme.subtext;
}
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold font.weight: Font.DemiBold
@@ -116,9 +129,9 @@ Item {
// scale then pushed it out past the silhouette onto the desktop. The inset // scale then pushed it out past the silhouette onto the desktop. The inset
// has to survive that scale, which is why it is not simply 1 or 2. // has to survive that scale, which is why it is not simply 1 or 2.
// //
// It is crimson rather than `tint` because it only ever appears on hover, // It is the accent rather than `tint` because it only ever appears on
// and hover is crimson everywhere else in the shell (Theme.glow). Painting // hover, and hover is the accent everywhere else in the shell (Theme.glow).
// it white put a white bar on the rail's edge for no stated reason. // Painting it white put a white bar on the rail's edge for no stated reason.
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: root.wipeInset anchors.leftMargin: root.wipeInset
@@ -137,7 +150,8 @@ Item {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack } NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
} }
onAlertChanged: if (alert) glyph.bump() onAlarmChanged: if (alarm) glyph.bump()
onLiveChanged: if (live) glyph.bump()
HoverHandler { HoverHandler {
id: hover id: hover
+2 -2
View File
@@ -63,7 +63,7 @@ BarPill {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.height * Theme.skew + 1 width: parent.height * Theme.skew + 1
height: parent.height height: parent.height
color: Theme.alpha(Theme.outline, 0.8) color: Theme.alpha(Theme.outline, 0.5)
} }
} }
@@ -106,7 +106,7 @@ BarPill {
id: labelText id: labelText
anchors.baseline: gauge.baseline anchors.baseline: gauge.baseline
text: metric.label text: metric.label
color: Theme.muted color: Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
+1 -1
View File
@@ -22,7 +22,7 @@ BarPill {
// The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a // The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a
// pictogram in a shell that otherwise says everything in heavy italic // pictogram in a shell that otherwise says everything in heavy italic
// type. SplitText puts it in the display face with the crimson-and-black // type. SplitText puts it in the display face with the ink print
// offset every other heading carries, so the house mark is built out of // offset every other heading carries, so the house mark is built out of
// the same parts as the rest of the bar. // the same parts as the rest of the bar.
SplitText { SplitText {
-117
View File
@@ -1,117 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Network, bluetooth, audio, mic, backlight and battery, in one slab. Each
// glyph carries its own popout.
BarPill {
id: root
required property var screenRef
accent: Theme.primary
padding: Theme.padS
interactive: false
implicitWidth: layout.implicitWidth + contentPad * 2
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
StatusIcon {
id: netIcon
icon: Net.icon
label: Sys.netUp ? Sys.rate(Sys.netRx + Sys.netTx) : ""
labelWidth: 62
tint: Theme.primary
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
StatusIcon {
id: btIcon
icon: Bt.icon
tint: Bt.connected.length > 0 ? Theme.blue : Theme.primary
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
StatusIcon {
id: volIcon
icon: Audio.icon
label: Audio.muted ? "" : Audio.volumePercent + "%"
labelWidth: Audio.muted ? 0 : 34
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
StatusIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
StatusIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
label: Math.round(Brightness.percent) + "%"
labelWidth: 34
// Brightness is never an alarm — it stays white and lets the
// battery be the only thing in the cluster that can go crimson.
tint: Theme.text
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
StatusIcon {
id: battIcon
visible: Battery.available
icon: Battery.icon
label: Math.round(Battery.percent) + "%"
labelWidth: 34
tint: Battery.tint
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
visible: Battery.available && shown
}
}
-108
View File
@@ -1,108 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// One indicator inside the status cluster: a glyph, an optional readout, and a
// crimson underline that wipes in on hover.
Item {
id: root
property string icon: ""
property string label: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
// Fixed label width. Readouts that change width constantly (throughput,
// percentages) would otherwise shove the whole cluster around.
property real labelWidth: 0
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: layout.implicitWidth
implicitHeight: Theme.barHeight - Theme.padS * 2
Row {
id: layout
anchors.centerIn: parent
spacing: 5
Icon {
id: glyph
anchors.verticalCenter: parent.verticalCenter
text: root.icon
pulsing: root.pulsing
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.label !== ""
text: root.label
width: root.labelWidth > 0 ? root.labelWidth : implicitWidth
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: 2
width: root.hovered ? layout.implicitWidth : 0
color: root.alert ? Theme.accent : root.tint
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.06 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+35 -17
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
@@ -15,10 +14,36 @@ Column {
id: root id: root
spacing: Theme.padS + 2 spacing: Theme.padS + 2
visible: SystemTray.items.values.length > 0 visible: root.shownItems.length > 0
// Applets whose whole job the rail already does. blueman keeps running as
// the pairing agent, but its icon was a second bluetooth glyph a few slots
// below the rail's own.
readonly property var hiddenIds: ["blueman"]
readonly property var shownItems: SystemTray.items.values
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
// Electron and Chromium apps send only a full-colour pixmap, and flattening
// that loses everything inside the outline: Brave's lion became a plain
// shield. The icon theme often ships a panel glyph drawn for exactly this
// job, so look one up by the app's name and use the pixmap only when none
// exists. The id is no help here ("chrome_status_icon_1" for Brave and
// Proton Pass alike), so the tooltip title is the name we go by.
function themedIcon(item: SystemTrayItem): string {
if (item.icon.startsWith("image://icon/")) return item.icon;
const name = (item.tooltipTitle || item.title || item.id)
.toLowerCase().replace(/[^a-z0-9]+/g, "-");
for (const suffix of ["-tray", "-panel", "-symbolic"]) {
const path = Quickshell.iconPath(name + suffix, true);
if (path !== "") return path;
}
return item.icon;
}
Repeater { Repeater {
model: SystemTray.items model: root.shownItems
Item { Item {
id: entry id: entry
@@ -31,32 +56,25 @@ Column {
// App icons arrive in their own brand colours, which wrecks the // App icons arrive in their own brand colours, which wrecks the
// two-hue palette faster than anything else in the shell. Flatten // two-hue palette faster than anything else in the shell. Flatten
// every one of them to a single-colour silhouette: white at rest, // every one of them to a single-colour silhouette: subtext at rest,
// crimson under the pointer. Detail inside the glyph is lost on // a notch under the rail's own glyphs, and accent under the pointer. Detail inside the glyph is lost on
// purpose — that flattening *is* the screen-printed look. // purpose — that flattening *is* the screen-printed look.
IconImage { IconImage {
id: img id: img
anchors.fill: parent anchors.fill: parent
source: entry.modelData.icon source: root.themedIcon(entry.modelData)
asynchronous: true asynchronous: true
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: TintEffect {
colorization: 1.0 tintColor: itemHover.hovered
colorizationColor: itemHover.hovered
? Theme.accent ? Theme.accent
: Theme.text : Theme.subtext
Behavior on colorizationColor { Behavior on tintColor {
ColorAnimation { duration: Theme.durFast } ColorAnimation { duration: Theme.durFast }
} }
} }
opacity: itemHover.hovered ? 1.0 : 0.82
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
} }
scale: itemHover.hovered ? 1.18 : 1.0 scale: itemHover.hovered ? 1.18 : 1.0
+27 -14
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components" import "root:/components"
// Workspace indicator: leaning ticks that stretch open when focused. Occupied // Workspace indicator: leaning ticks that stretch open when focused. Occupied
// workspaces glow teal, urgent ones snap to crimson. // workspaces fill in, urgent ones snap to the alarm colour.
Item { Item {
id: root id: root
@@ -65,8 +65,12 @@ Item {
// Every tick is the same height and the same shear. Only the // Every tick is the same height and the same shear. Only the
// width changes, and only to say which workspace is focused. // width changes, and only to say which workspace is focused.
width: focused ? 32 : (occupied ? 16 : 9) //
height: 18 // Kept well short of the bar's 20px divider rules. At 18px an
// empty outline tick was the same height, lean and weight as a
// divider, and the workspace strip read as a row of rules.
width: focused ? 30 : (occupied ? 16 : 8)
height: 14
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
@@ -80,23 +84,32 @@ Item {
id: tick id: tick
anchors.fill: parent anchors.fill: parent
// Focused is a solid crimson block. Occupied is a white // Focused is a solid accent block. Urgent is a solid alarm
// block at half weight. Empty is an outline. Three states, // block — it used to be the accent too, which made an
// two colours, no ambiguity. // urgent workspace indistinguishable from the focused one.
// Occupied is a subtext block. Empty is a flat recessed
// slot, filled rather than outlined so it cannot pass for a
// divider.
color: { color: {
if (pip.focused || pip.urgent) return Theme.accent; if (pip.focused) return Theme.accent;
if (pip.urgent) return Theme.alarm;
if (pip.hovered) return Theme.text; if (pip.hovered) return Theme.text;
if (pip.occupied) return Theme.alpha(Theme.text, 0.5); if (pip.occupied) return Theme.subtext;
return "transparent"; return Theme.surfaceAlt;
} }
borderColor: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
borderWidth: 1
Behavior on color { Behavior on color {
ColorAnimation { duration: Theme.durBase } ColorAnimation { duration: Theme.durBase }
} }
// Urgent is the one pip that has to be seen peripherally.
SequentialAnimation on opacity {
running: pip.urgent && !pip.focused
loops: Animation.Infinite
alwaysRunToEnd: true
NumberAnimation { to: 0.4; duration: 520; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 520; easing.type: Easing.InOutQuad }
}
} }
// The focused workspace wears its number. // The focused workspace wears its number.
@@ -107,7 +120,7 @@ Item {
text: pip.wsId text: pip.wsId
color: Theme.ink color: Theme.ink
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.italic: true font.italic: true
renderType: Text.NativeRendering renderType: Text.NativeRendering
-120
View File
@@ -1,120 +0,0 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
// Radial gauge used across the resource popout. Sweeps a 270° arc with a
// tick-marked rail behind it and the reading set in the middle.
Item {
id: root
property real value: 0 // 0..100
property string label: ""
property string readout: Math.round(value) + "%"
property string sub: ""
property color arcColor: Theme.heat(value)
property real thickness: 7
property real startAngle: 135
property real sweep: 270
property int ticks: 24
implicitWidth: 96
implicitHeight: 96
readonly property real _r: Math.min(width, height) / 2 - thickness / 2 - 4
readonly property real _cx: width / 2
readonly property real _cy: height / 2
// Rail
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Theme.alpha(Theme.outline, 0.5)
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep
}
}
}
// Tick marks around the rail.
Repeater {
model: root.ticks
Rectangle {
required property int index
readonly property real a: (root.startAngle + root.sweep * (index / (root.ticks - 1))) * Math.PI / 180
readonly property real rr: root._r + root.thickness / 2 + 3
width: 2
height: index % 4 === 0 ? 6 : 3
color: Theme.alpha(Theme.primary, index % 4 === 0 ? 0.55 : 0.25)
x: root._cx + Math.cos(a) * rr - width / 2
y: root._cy + Math.sin(a) * rr - height / 2
rotation: (root.startAngle + root.sweep * (index / (root.ticks - 1))) + 90
}
}
// Value arc
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: root.arcColor
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
id: valueArc
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep * Math.max(0, Math.min(100, root.value)) / 100
Behavior on sweepAngle {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
}
}
}
Column {
anchors.centerIn: parent
spacing: -2
SplitText {
anchors.horizontalCenter: parent.horizontalCenter
text: root.readout
pixelSize: Theme.fsLarge
color: Theme.text
split: 1.2
splitOpacity: 0.6
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.sub !== ""
text: root.sub
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+118
View File
@@ -0,0 +1,118 @@
import QtQuick
import "root:/config"
// One row of a popout list — a Wi-Fi network, a bluetooth device, an audio
// output. The pickers used to draw their own rows, each with its own height and
// its own translucent white wash for hover, which on Mocha's blue-grey ground
// came out as a faint milky film. This is the one row they share now.
//
// Rest is bare. Hover lifts the row onto the surface tone. The current item —
// connected, in use — carries an accent tick down its leading edge and an
// accent glyph, and its label goes to full weight.
Item {
id: root
property string icon: ""
property string label: ""
// Optional mono line under the label: a device state, an address.
property string detail: ""
property bool current: false
// Right-aligned trailing content — a lock, a battery level, a tag.
default property alias trailing: trailingRow.data
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
implicitHeight: root.detail !== "" ? 36 : 30
Rectangle {
anchors.fill: parent
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.current ? root.height - 10 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Icon {
id: glyph
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
width: 18
visible: root.icon !== ""
text: root.icon
color: root.current ? Theme.accent : (root.hovered ? Theme.text : Theme.subtext)
font.pixelSize: Theme.fsLarge
}
Column {
anchors.left: glyph.visible ? glyph.right : parent.left
anchors.leftMargin: glyph.visible ? Theme.padS : Theme.padM
anchors.right: trailingRow.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: -1
Text {
width: parent.width
text: root.label
elide: Text.ElideRight
color: root.current || root.hovered ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: root.current ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Text {
width: parent.width
visible: text !== ""
text: root.detail
elide: Text.ElideRight
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
Row {
id: trailingRow
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: 4
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
}
-2
View File
@@ -70,8 +70,6 @@ Item {
text: root.readout text: root.readout
pixelSize: Theme.fsBody pixelSize: Theme.fsBody
color: Theme.text color: Theme.text
split: 1
splitOpacity: 0.5
} }
} }
+11 -10
View File
@@ -34,13 +34,16 @@ Item {
chop: root.chop chop: root.chop
fill: Theme.surface fill: Theme.surface
fillOpacity: 0.97 fillOpacity: 0.97
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5) // Urgency lives in the slash, and in the frame only when critical. A
// tone-tinted frame on every card drew a pale outline round ordinary
// notifications, and the critical halftone was painted in the accent,
// so the most urgent card wore the colour that means "live".
border: root.notif?.urgency === NotificationUrgency.Critical
? root.tone
: (hover.hovered ? Theme.treatmentHoverMark : Theme.surfaceAlt)
slash: true slash: true
slashColor: root.tone slashColor: root.tone
slashWidth: 4 slashWidth: 4
halftone: root.notif?.urgency === NotificationUrgency.Critical
halftoneColor: Theme.accent
halftoneOpacity: 0.12
padding: Theme.padM padding: Theme.padM
Column { Column {
@@ -66,7 +69,7 @@ Item {
text: (root.notif?.appName || "System").toUpperCase() text: (root.notif?.appName || "System").toUpperCase()
color: root.tone color: root.tone
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 2 font.letterSpacing: 2
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -126,18 +129,16 @@ Item {
Skew { Skew {
anchors.fill: parent anchors.fill: parent
color: actionHover.hovered color: actionHover.hovered ? Theme.accent : Theme.surfaceAlt
? Theme.alpha(root.tone, 0.3)
: Theme.alpha(root.tone, 0.1)
borderWidth: 1 borderWidth: 1
borderColor: Theme.alpha(root.tone, 0.6) borderColor: actionHover.hovered ? Theme.accent : Theme.overlay
} }
Text { Text {
id: actionLabel id: actionLabel
anchors.centerIn: parent anchors.centerIn: parent
text: actionChip.modelData.text text: actionChip.modelData.text
color: Theme.text color: actionHover.hovered ? Theme.ink : Theme.text
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.italic: true font.italic: true
+4 -2
View File
@@ -3,7 +3,7 @@ import QtQuick.Shapes
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// The shared ground for the two full-screen surfaces: a crimson wedge leaning // The shared ground for the two full-screen surfaces: an accent wedge leaning
// at the house angle, a tiled stripe field that sweeps in from the left, and a // at the house angle, a tiled stripe field that sweeps in from the left, and a
// halftone dot wash over the top. // halftone dot wash over the top.
// //
@@ -33,7 +33,9 @@ Item {
property real wedgeSpan: 0.34 property real wedgeSpan: 0.34
property bool stripes: true property bool stripes: true
property real stripeOpacity: 0.10 // Tuned down from 0.10 for Mocha. Crust stripes on the blue-grey base read
// far heavier than they did on black, and buzzed against the text.
property real stripeOpacity: 0.07
property bool halftone: true property bool halftone: true
property real halftoneOpacity: 0.05 property real halftoneOpacity: 0.05
+23 -8
View File
@@ -21,6 +21,12 @@ FocusScope {
readonly property bool focusedOrSelected: root.activeFocus || root.selected readonly property bool focusedOrSelected: root.activeFocus || root.selected
readonly property bool highlighted: root.hovered || root.focusedOrSelected readonly property bool highlighted: root.hovered || root.focusedOrSelected
// Keyboard focus and roving selection invert the slab to solid accent.
// Pointer hover stays a keyline change, so the two never look alike and a
// mouse user never sees the "selected" treatment by accident. Destructive
// buttons keep their own mauve inversion for both.
readonly property bool inverted: root.enabled && !root.destructive && root.focusedOrSelected
// Pointer activation must not turn a transient action into the black/white // Pointer activation must not turn a transient action into the black/white
// keyboard-focus treatment. Tab still reaches it, and programmatic focus // keyboard-focus treatment. Tab still reaches it, and programmatic focus
// (power-menu roving selection / pinned popouts) still works. // (power-menu roving selection / pinned popouts) still works.
@@ -46,23 +52,26 @@ FocusScope {
padding: Theme.padS padding: Theme.padS
fill: { fill: {
if (!root.enabled) return Theme.treatmentDisabledFill; 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; if (root.highlighted) return Theme.treatmentHoverFill;
return Theme.treatmentRestFill; return Theme.treatmentRestFill;
} }
fillOpacity: root.enabled ? 1.0 : 0.72 fillOpacity: root.enabled ? 1.0 : 0.72
border: { border: {
if (!root.enabled) return Theme.treatmentDisabledMark; 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; if (root.highlighted) return Theme.treatmentHoverMark;
return Theme.alpha(root.accent, 0.55); return Theme.alpha(root.accent, 0.55);
} }
borderWidth: Theme.stroke borderWidth: Theme.stroke
slash: !root.highlighted slash: !root.highlighted
slashColor: root.destructive ? Theme.treatmentDestructiveMark : root.accent slashColor: root.destructive ? Theme.alarm : root.accent
halftone: root.enabled && root.highlighted
halftoneColor: Theme.ink
halftoneOpacity: 0.14
Behavior on fillOpacity { Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast } NumberAnimation { duration: Theme.durFast }
@@ -81,7 +90,10 @@ FocusScope {
font.pixelSize: Theme.fsTitle font.pixelSize: Theme.fsTitle
color: { color: {
if (!root.enabled) return Theme.treatmentDisabledText; 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; if (root.highlighted) return Theme.treatmentHoverText;
return root.accent; return root.accent;
} }
@@ -92,7 +104,10 @@ FocusScope {
text: root.text text: root.text
color: { color: {
if (!root.enabled) return Theme.treatmentDisabledText; 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; if (root.highlighted) return Theme.treatmentHoverText;
return Theme.treatmentRestText; return Theme.treatmentRestText;
} }
+3 -3
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components" import "root:/components"
// The base surface for everything in this shell: a leaning, corner-chopped slab // The base surface for everything in this shell: a leaning, corner-chopped slab
// with an optional halftone wash and a crimson slash along its leading edge. // with an optional halftone wash and a accent slash along its leading edge.
Item { Item {
id: root id: root
@@ -29,7 +29,7 @@ Item {
property real chop: Theme.cut property real chop: Theme.cut
property var chopCorners: [1, 3] property var chopCorners: [1, 3]
// Crimson slash down the leading edge — the Persona 5 tell. // Tone slash down the leading edge — the Persona 5 tell.
property bool slash: false property bool slash: false
property color slashColor: Theme.accent property color slashColor: Theme.accent
property real slashWidth: 4 property real slashWidth: 4
@@ -183,7 +183,7 @@ Item {
// //
// Its polygon is built from the panel's raw corners, so on a panel with a // Its polygon is built from the panel's raw corners, so on a panel with a
// chopped corner the slash used to run straight through the cut and out the // chopped corner the slash used to run straight through the cut and out the
// far side — a crimson wedge sitting a few pixels outside the silhouette, // far side — an accent wedge sitting a few pixels outside the silhouette,
// at an angle that matched nothing. It is masked to the outline now, so the // at an angle that matched nothing. It is masked to the outline now, so the
// slash ends exactly where the chamfer does. // slash ends exactly where the chamfer does.
Loader { Loader {
+5 -2
View File
@@ -31,10 +31,13 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: root.activeFocus ? 10 : 6 height: root.activeFocus ? 10 : 6
// Focus widens the rail and rings it in the accent rather than filling
// it: the level fill is often the accent itself, and a solid accent
// rail swallowed it.
color: root.activeFocus color: root.activeFocus
? Theme.treatmentFocusFill ? Theme.surfaceAlt
: Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7) : Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7)
borderColor: root.activeFocus ? Theme.treatmentFocusMark : "transparent" borderColor: root.activeFocus ? Theme.accent : "transparent"
borderWidth: root.activeFocus ? Theme.stroke : 0 borderWidth: root.activeFocus ? Theme.stroke : 0
Behavior on height { Behavior on height {
+35
View File
@@ -0,0 +1,35 @@
import QtQuick
import "root:/config"
// Section caption for popouts: a spaced mono label running into a hairline.
// Every panel used to label its lists differently — a bare caption here, a
// caption and rule there — which is part of why no two popouts looked related.
Item {
id: root
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: label
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: label.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.5)
}
}
+55 -44
View File
@@ -1,8 +1,14 @@
import QtQuick import QtQuick
import "root:/config" import "root:/config"
// Display text with the wallpaper's RGB split baked in: a crimson and a cyan // Display text, screen-printed: the body sits on a hard crust copy knocked a
// ghost sitting a pixel off the white body. Drives most headings in the shell. // pixel and a half down-right — the same offset `P5Panel` puts behind every
// slab. Drives most headings in the shell.
//
// This used to carry a permanent RGB split, a blue and a mauve ghost either
// side of the body. On crimson-and-black that read as misregistered print; on
// Mocha's pastels it read as blur, and it turned every heading in the shell
// soft. The two ghosts still exist, but only for the moment `glitch()` runs.
Item { Item {
id: root id: root
@@ -12,23 +18,34 @@ Item {
property string family: Theme.fontDisplay property string family: Theme.fontDisplay
property bool italic: true property bool italic: true
property color color: Theme.text property color color: Theme.text
property real split: 1.6
property real splitOpacity: 0.85
property int elide: Text.ElideNone property int elide: Text.ElideNone
property int horizontalAlignment: Text.AlignLeft property int horizontalAlignment: Text.AlignLeft
property real letterSpacing: 0.5 property real letterSpacing: 0.5
// Print shadow. Scales gently with size so a 34px heading does not sit on
// the same hairline offset as an 11px label.
property real shadowOffset: Math.max(1, Math.round(root.pixelSize / 10))
property color shadowColor: Theme.ink
// Ghost offset and resting strength. Resting strength is zero, so the
// ghosts are invisible until `glitch()` kicks them out. A caller that sets
// `splitOpacity` is asking for the old permanent split and still gets it.
property real split: 1.6
property real splitOpacity: 0
// 0..1, driven only by `glitch()`. Kept apart from `split` and
// `splitOpacity` so the animation never has to remember a caller's values.
property real kick: 0
readonly property real ghostOffset: root.split + root.kick * 4
readonly property real ghostOpacity: Math.max(root.splitOpacity, root.kick * 0.85)
implicitWidth: body.implicitWidth implicitWidth: body.implicitWidth
implicitHeight: body.implicitHeight implicitHeight: body.implicitHeight
Text { component Layer: Text {
id: ghostRed
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: -root.split
anchors.topMargin: root.split * 0.4
text: root.text text: root.text
color: Theme.splitRed
opacity: root.splitOpacity
elide: root.elide elide: root.elide
horizontalAlignment: root.horizontalAlignment horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -40,39 +57,33 @@ Item {
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Text { Layer {
id: ghostCyan anchors.leftMargin: root.shadowOffset
anchors.fill: parent anchors.rightMargin: -root.shadowOffset
anchors.leftMargin: root.split anchors.topMargin: root.shadowOffset
anchors.topMargin: -root.split * 0.4 anchors.bottomMargin: -root.shadowOffset
text: root.text color: root.shadowColor
color: Theme.splitCyan
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
} }
Text { Layer {
visible: root.ghostOpacity > 0.01
anchors.leftMargin: -root.ghostOffset
anchors.topMargin: root.ghostOffset * 0.4
color: Theme.accent
opacity: root.ghostOpacity
}
Layer {
visible: root.ghostOpacity > 0.01
anchors.leftMargin: root.ghostOffset
anchors.topMargin: -root.ghostOffset * 0.4
color: Theme.alarm
opacity: root.ghostOpacity
}
Layer {
id: body id: body
anchors.fill: parent
text: root.text
color: root.color color: root.color
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
} }
// Kick the ghosts outward briefly — used on track changes, mode flips, etc. // Kick the ghosts outward briefly — used on track changes, mode flips, etc.
@@ -83,14 +94,14 @@ Item {
SequentialAnimation { SequentialAnimation {
id: glitchAnim id: glitchAnim
NumberAnimation { NumberAnimation {
target: root; property: "split" target: root; property: "kick"
to: 5.5; duration: Theme.durFast to: 1; duration: Theme.durFast
easing.type: Easing.OutExpo easing.type: Easing.OutExpo
} }
NumberAnimation { NumberAnimation {
target: root; property: "split" target: root; property: "kick"
to: 1.6; duration: Theme.durSlow to: 0; duration: Theme.durSlow
easing.type: Easing.OutBack easing.type: Easing.OutCubic
} }
} }
} }
+8 -2
View File
@@ -1,11 +1,17 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
// Layer effect that recolours whatever it is applied to — used to tint the // Layer effect that recolours whatever it is applied to into a flat silhouette
// white texture tiles without shipping a copy per colour. // of `tintColor` — used for the white texture tiles and for tray icons.
//
// Colorization alone keeps the source's lightness, so a dark icon came out as a
// dark shade of the tint and all but vanished against the rail. Brightness runs
// first and lifts every pixel to white, which leaves colorization nothing to
// preserve but the alpha.
MultiEffect { MultiEffect {
property color tintColor: "white" property color tintColor: "white"
brightness: 1.0
colorization: 1.0 colorization: 1.0
colorizationColor: tintColor colorizationColor: tintColor
} }
+89 -65
View File
@@ -3,61 +3,73 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell 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 // The Takemi branch ran monochrome-plus-crimson, where one hue carried both
// and it always means the same thing — active, or wrong. Black is structure, // "active" and "wrong". Mocha has room for two: blue is every resting accent —
// used as a hard keyline rather than as a shadow. There are no gradients in // focus, selection, the thing you are pointing at — and mauve is reserved for
// this shell; every fill is flat. // 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 // Mauve rather than Mocha's pink: pink is a pastel, and a slab of it over this
// other into a murky blue-grey. Letting the UI go monochrome-plus-red is what // ground goes chalky. Mauve is saturated enough to hold an edge against the
// lets the wallpaper read as the colour in the composition. // 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 { Singleton {
id: root id: root
// ---------------------------------------------------------------- colors // ---------------------------------------------------------------- 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 base: "#1e1e2e"
readonly property color mantle: "#0d0d10" readonly property color mantle: "#181825"
readonly property color surface: "#141418" readonly property color surface: "#313244"
readonly property color surfaceAlt: "#1c1c22" readonly property color surfaceAlt: "#45475a"
readonly property color overlay: "#26262e" readonly property color overlay: "#585b70"
readonly property color outline: "#3a3a44" readonly property color outline: "#6c7086"
readonly property color text: "#ffffff" readonly property color text: "#cdd6f4"
readonly property color subtext: "#c8c8ce" readonly property color subtext: "#a6adc8"
readonly property color muted: "#6b6b70" readonly property color muted: "#7f849c"
// ------------------------------------------------------------ THE KNOB // ------------------------------------------------------------ THE KNOBS
// The one colour in this shell. Change this line and the entire accent // Two colours, two meanings, and nothing else in the shell carries hue.
// recolours — everything below is derived from it, and nothing else in the
// palette carries hue at all.
// //
// Keep it in step with `$accent` at the top of // `accent` is blue: anything live, focused or selected. `alarm` is mauve:
// ~/.config/hypr/modules/takemi.conf, which is what colours the Hyprland // anything destructive, failed or critical. Things that are merely off —
// window borders. Those are the only two places the colour is written. // muted, disabled, do-not-disturb — carry no hue at all; see the `Off`
readonly property color accent: "#ff2d40" // 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 accentDim: "#5a7dc0"
readonly property color accentSoft: Qt.lighter(root.accent, 1.35) readonly property color accentSoft: "#b4befe"
// `primary` is the resting voice of the UI, which is now simply white. // `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. // 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 primary: root.text
readonly property color primaryDim: "#9a9aa2" readonly property color primaryDim: "#9399b2"
readonly property color deep: root.surfaceAlt readonly property color deep: root.surfaceAlt
// `glow` is the hover highlight. It used to be a brighter teal, which now // `glow` is the hover highlight. Pointing at a thing lights it up blue —
// that everything rests at white would have made hover invisible — so it // the same signal as focus, because it is the same statement about where
// resolves to crimson. Pointing at a thing lights it up red. // your attention is.
readonly property color glow: root.accent readonly property color glow: root.accent
// ------------------------------------------------ semantic treatments // ------------------------------------------------ semantic treatments
// These tokens describe interaction meaning, not new palette entries. Each // These tokens describe interaction meaning, not new palette entries. Each
// state is composed only from Takemi's black, white, crimson, and neutral // state is composed only from crust, the neutral ramp, blue and mauve, so
// surfaces so callers do not invent one-off state colors. // callers do not invent one-off state colours.
readonly property color treatmentRestFill: root.surface readonly property color treatmentRestFill: root.surface
readonly property color treatmentRestMark: root.outline readonly property color treatmentRestMark: root.outline
readonly property color treatmentRestText: root.text readonly property color treatmentRestText: root.text
@@ -66,7 +78,11 @@ Singleton {
readonly property color treatmentHoverMark: root.accent readonly property color treatmentHoverMark: root.accent
readonly property color treatmentHoverText: root.text readonly property color treatmentHoverText: root.text
readonly property color treatmentFocusFill: root.text // Focus and selection are an inverted slab: solid blue with crust punched
// out of it (9:1). This used to be a white fill, which was right against
// Takemi's black but glares as a lavender-white block on Mocha — and it
// made "selected" a third look that was neither hover nor accent.
readonly property color treatmentFocusFill: root.accent
readonly property color treatmentFocusMark: root.ink readonly property color treatmentFocusMark: root.ink
readonly property color treatmentFocusText: root.ink readonly property color treatmentFocusText: root.ink
readonly property color treatmentSelectedFill: root.treatmentFocusFill readonly property color treatmentSelectedFill: root.treatmentFocusFill
@@ -77,38 +93,38 @@ Singleton {
readonly property color treatmentActiveMark: root.accent readonly property color treatmentActiveMark: root.accent
readonly property color treatmentActiveText: root.text readonly property color treatmentActiveText: root.text
// Crimson carries destructive/error meaning. Text and keylines stay black: // Mauve carries destructive/error meaning — never blue, so a dangerous
// black-on-crimson is 5.70:1, while small white-on-crimson is only 3.68:1. // button can never be mistaken for a merely focused one.
readonly property color treatmentDestructiveFill: root.accent //
readonly property color treatmentDestructiveMark: root.ink // At rest it is a keyline and a label, not a fill. The solid mauve is held
readonly property color treatmentDestructiveText: root.ink // back for the moment the pointer or the selection is actually on the
readonly property color treatmentErrorFill: root.treatmentDestructiveFill // thing — then it lands hard, with crust punched out of it at 9.1:1.
readonly property color treatmentErrorMark: root.treatmentDestructiveMark readonly property color treatmentDestructiveFill: root.surface
readonly property color treatmentErrorText: root.treatmentDestructiveText 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 treatmentUrgentFill: root.surface
readonly property color treatmentUrgentMark: root.accent readonly property color treatmentUrgentMark: root.alarm
readonly property color treatmentUrgentText: root.text readonly property color treatmentUrgentText: root.text
readonly property color treatmentDisabledFill: root.surface readonly property color treatmentDisabledFill: root.surface
readonly property color treatmentDisabledMark: root.outline readonly property color treatmentDisabledMark: root.outline
readonly property color treatmentDisabledText: root.muted readonly property color treatmentDisabledText: root.muted
// Retired hues. Aliases only — do not introduce new uses. // 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
// `warn` used to be accentSoft, which put a washed-out pink-red on screen // speaker blue said "active", and painting it mauve would say "broken".
// wherever something was merely notable. That reads as a faded accent, not readonly property color treatmentOffFill: root.surface
// as a decision. There is no half-crimson in this palette: a thing is wrong readonly property color treatmentOffMark: root.overlay
// (accent) or it is not (text). `warn` now resolves to the real accent, so readonly property color treatmentOffText: root.muted
// 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
// 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 { function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a); return Qt.rgba(c.r, c.g, c.b, a);
@@ -122,11 +138,15 @@ Singleton {
a.a + (b.a - a.a) * k); a.a + (b.a - a.a) * k);
} }
// Load readouts sit white until the number actually matters, then bleed to // Load readouts sit at text until the number actually matters, then climb
// crimson. Not a hue cycle — red always means the same thing. // 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 { function heat(pct: real): color {
if (pct <= 55) return root.text; if (pct <= 45) return root.text;
return root.mix(root.text, root.accent, (pct - 55) / 45); if (pct <= 75) return root.mix(root.text, root.accent, (pct - 45) / 30);
return root.mix(root.accent, root.alarm, (pct - 75) / 25);
} }
// ----------------------------------------------------------------- fonts // ----------------------------------------------------------------- fonts
@@ -137,7 +157,9 @@ Singleton {
readonly property int weightDisplay: Font.Black readonly property int weightDisplay: Font.Black
readonly property int weightBody: Font.Medium readonly property int weightBody: Font.Medium
readonly property int fsMicro: 9 // Nothing in the shell renders below this. 9px mono was legible on
// crimson-on-black; on Mocha's lower-contrast ramp it was not.
readonly property int fsMicro: 10
readonly property int fsSmall: 11 readonly property int fsSmall: 11
readonly property int fsBody: 12 readonly property int fsBody: 12
readonly property int fsLarge: 15 readonly property int fsLarge: 15
@@ -145,6 +167,8 @@ Singleton {
readonly property int fsHuge: 34 readonly property int fsHuge: 34
readonly property int fsMega: 92 readonly property int fsMega: 92
readonly property int fsRailIcon: 17
// -------------------------------------------------------------- geometry // -------------------------------------------------------------- geometry
readonly property int barHeight: 40 readonly property int barHeight: 40
readonly property int barMargin: 8 readonly property int barMargin: 8
@@ -163,7 +187,7 @@ Singleton {
readonly property int cut: 10 readonly property int cut: 10
readonly property int stroke: 2 readonly property int stroke: 2
// Pure-black keyline and offset copy behind panels. This is the one piece of // Crust keyline and offset copy behind panels. This is the one piece of
// layering the style needs; it is not a soft shadow and never blurs. // layering the style needs; it is not a soft shadow and never blurs.
readonly property real dropOffset: 3 readonly property real dropOffset: 3
readonly property real keylineWidth: 2 readonly property real keylineWidth: 2
@@ -196,5 +220,5 @@ Singleton {
// which asks hyprpaper what is actually on screen — a path written down // which asks hyprpaper what is actually on screen — a path written down
// here goes stale the moment the wallpaper is switched, which is exactly // here goes stale the moment the wallpaper is switched, which is exactly
// what happened to the lock screen. // what happened to the lock screen.
readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/takemi_1.jpg" readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/arch.png"
} }
+42 -19
View File
@@ -11,10 +11,14 @@ import "root:/services"
// //
// TYPING is always typing. Navigation lives on the arrows and on Ctrl+J/K so // TYPING is always typing. Navigation lives on the arrows and on Ctrl+J/K so
// that nothing you can type is ever stolen from you — except in select mode, // that nothing you can type is ever stolen from you — except in select mode,
// which you enter deliberately (the first arrow press does it) and which hands // which hands h/j/k/l over to navigation. That is the compromise the vim keys
// h/j/k/l over to navigation until you press Escape or Backspace. That is the // need: a launcher whose input eats `j` is a launcher you cannot use to find
// compromise the vim keys need: a launcher whose input eats `j` is a launcher // Jellyfin.
// you cannot use to find Jellyfin. //
// Select mode is vim's normal mode, and the modes change the way vim changes
// them: Escape leaves insert, `i` (or `a`) goes back into it without leaving
// an `i` behind, and Escape from select mode — with no insert left to leave —
// ends the prompt. Unbound keys in select mode are eaten, not typed.
// //
// The other half of the grammar is the argument. Type a URL and the list turns // The other half of the grammar is the argument. Type a URL and the list turns
// into "open this with…", already sorted so the system default is row zero — // into "open this with…", already sorted so the system default is row zero —
@@ -27,7 +31,10 @@ PanelWindow {
required property var modelData required property var modelData
screen: modelData screen: modelData
// Only the screen that had focus when the launcher was asked for shows it;
// the other copies stay dormant so the query is never typed into thin air.
readonly property bool open: Actions.launcherOpen readonly property bool open: Actions.launcherOpen
&& Actions.launcherScreen === modelData.name
visible: root.open || closeAnim.running visible: root.open || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
@@ -403,14 +410,14 @@ PanelWindow {
font.letterSpacing: 3 font.letterSpacing: 3
renderType: Text.NativeRendering renderType: Text.NativeRendering
text: root.selectMode text: root.selectMode
? "H J K L MOVE L PIN ESC TYPE" ? "J K MOVE L PIN H UNPIN i TYPE ESC QUIT"
: root.mode === "dmenu" : root.mode === "dmenu"
? "⏎ CHOOSE ESC CANCEL ↑↓ SELECT" ? "⏎ CHOOSE ESC SELECT ↑↓ MOVE"
: root.mode === "clip" : root.mode === "clip"
? "⏎ COPY ESC BACK ↑↓ SELECT" ? "⏎ COPY ESC SELECT ↑↓ MOVE"
: root.pinned : root.pinned
? "⏎ OPEN ESC UNPIN ↑↓ PICK" ? "⏎ OPEN ESC SELECT ↑↓ PICK"
: "TAB PIN > SHELL = CALC : CLIPBOARD" : "ESC SELECT TAB PIN > SHELL = CALC : CLIP"
} }
} }
} }
@@ -422,16 +429,23 @@ PanelWindow {
const ctrl = (event.modifiers & Qt.ControlModifier) !== 0; const ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
// Escape peels one layer at a time: select mode, then the pin, then // Escape is the mode key, and it means what it means in vim: leave
// the launcher. Nothing about it is ever a surprise. // insert. From select mode there is nothing left to leave but the
// launcher itself, so the second press ends the prompt.
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (root.selectMode) root.selectMode = false; if (!root.selectMode && root.results.length > 0) root.selectMode = true;
else if (root.pinned) root.unpin();
else Actions.closeLauncher(); else Actions.closeLauncher();
event.accepted = true; event.accepted = true;
return; return;
} }
// …and Ctrl+C still walks out in one press, from wherever you are.
if (ctrl && event.key === Qt.Key_C) {
Actions.closeLauncher();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.activate(); root.activate();
event.accepted = true; event.accepted = true;
@@ -446,7 +460,6 @@ PanelWindow {
if (event.key === Qt.Key_Down || (ctrl && event.key === Qt.Key_J) if (event.key === Qt.Key_Down || (ctrl && event.key === Qt.Key_J)
|| (ctrl && event.key === Qt.Key_N)) { || (ctrl && event.key === Qt.Key_N)) {
root.selectMode = true;
root.move(1); root.move(1);
event.accepted = true; event.accepted = true;
return; return;
@@ -454,7 +467,6 @@ PanelWindow {
if (event.key === Qt.Key_Up || (ctrl && event.key === Qt.Key_K) if (event.key === Qt.Key_Up || (ctrl && event.key === Qt.Key_K)
|| (ctrl && event.key === Qt.Key_P)) { || (ctrl && event.key === Qt.Key_P)) {
root.selectMode = true;
root.move(-1); root.move(-1);
event.accepted = true; event.accepted = true;
return; return;
@@ -468,14 +480,23 @@ PanelWindow {
if (!root.selectMode) return; if (!root.selectMode) return;
// Select mode. The letters navigate; anything else drops you back into // Select mode. The letters are commands, not text: `i` and `a` go back
// the input, having typed nothing, so a mistyped `d` costs one press // to insert without leaving an `i` behind, and everything unclaimed is
// rather than a mangled query. // swallowed rather than typed, so a stray `d` costs nothing at all.
switch (event.key) { switch (event.key) {
case Qt.Key_J: root.move(1); event.accepted = true; return; case Qt.Key_J: root.move(1); event.accepted = true; return;
case Qt.Key_K: root.move(-1); event.accepted = true; return; case Qt.Key_K: root.move(-1); event.accepted = true; return;
case Qt.Key_L: root.pin(); event.accepted = true; return; case Qt.Key_L: root.pin(); event.accepted = true; return;
case Qt.Key_H: root.unpin(); event.accepted = true; return; case Qt.Key_H: root.unpin(); event.accepted = true; return;
case Qt.Key_I:
case Qt.Key_A:
root.selectMode = false;
input.forceActiveFocus();
// `a` appends, `i` leaves the cursor where it was — the same
// distinction, for the same reason.
if (event.key === Qt.Key_A) input.cursorPosition = input.text.length;
event.accepted = true;
return;
case Qt.Key_G: case Qt.Key_G:
root.selected = (event.modifiers & Qt.ShiftModifier) root.selected = (event.modifiers & Qt.ShiftModifier)
? root.results.length - 1 : 0; ? root.results.length - 1 : 0;
@@ -484,11 +505,13 @@ PanelWindow {
return; return;
case Qt.Key_Backspace: case Qt.Key_Backspace:
root.selectMode = false; root.selectMode = false;
input.forceActiveFocus();
event.accepted = true; event.accepted = true;
return; return;
} }
if (event.text !== "") root.selectMode = false; // Anything else in select mode is a command nobody bound. Eat it.
if (event.text !== "") event.accepted = true;
} }
// ------------------------------------------------------------- animation // ------------------------------------------------------------- animation
+1 -1
View File
@@ -36,7 +36,7 @@ Item {
} }
} }
// Hover is a crimson tick in the gutter rather than a second slab, so // Hover is an accent tick in the gutter rather than a second slab, so
// pointing at a row cannot be mistaken for selecting it. // pointing at a row cannot be mistaken for selecting it.
Skew { Skew {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
+5 -5
View File
@@ -68,7 +68,7 @@ Item {
// The same wedge-and-stripes ground the power menu stands on. The wedge // The same wedge-and-stripes ground the power menu stands on. The wedge
// sits under the clock, which is where the power menu puts it under the // sits under the clock, which is where the power menu puts it under the
// title — so the two screens land the crimson mass in the same place. // title — so the two screens land the accent mass in the same place.
P5Backdrop { P5Backdrop {
id: backdrop id: backdrop
anchors.fill: parent anchors.fill: parent
@@ -199,7 +199,7 @@ Item {
text: surface.failed text: surface.failed
? (surface.status || "WRONG").toUpperCase() ? (surface.status || "WRONG").toUpperCase()
: (surface.busy ? "CHECKING…" : "WHO GOES THERE") : (surface.busy ? "CHECKING…" : "WHO GOES THERE")
color: surface.failed ? Theme.accent : Theme.text color: surface.failed ? Theme.alarm : Theme.text
pixelSize: Theme.fsTitle pixelSize: Theme.fsTitle
letterSpacing: 4 letterSpacing: 4
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -223,10 +223,10 @@ Item {
: (input.activeFocus ? Theme.primary : Theme.outline) : (input.activeFocus ? Theme.primary : Theme.outline)
borderWidth: 2 borderWidth: 2
slash: true slash: true
slashColor: surface.failed ? Theme.accent : Theme.primary slashColor: surface.failed ? Theme.alarm : Theme.primary
slashWidth: 6 slashWidth: 6
halftone: true halftone: true
halftoneColor: surface.failed ? Theme.accent : Theme.primary halftoneColor: surface.failed ? Theme.alarm : Theme.primary
halftoneOpacity: 0.08 halftoneOpacity: 0.08
Row { Row {
@@ -241,7 +241,7 @@ Item {
required property int index required property int index
width: 10 width: 10
height: 20 height: 20
color: surface.failed ? Theme.accent : Theme.primary color: surface.failed ? Theme.alarm : Theme.primary
// Each new block snaps in. // Each new block snaps in.
Component.onCompleted: pop.start() Component.onCompleted: pop.start()
+7 -11
View File
@@ -66,10 +66,12 @@ PanelWindow {
readonly property color tone: { readonly property color tone: {
switch (root.kind) { switch (root.kind) {
case "mic": return Audio.micMuted ? Theme.accent : Theme.blue; // One tone for every level: the accent while the channel is live, the
// Turning the screen up is not a warning. White, like volume. // off grey while it is muted. Muted used to be the accent too, so a
case "brightness": return Theme.text; // silenced speaker flashed the same colour as a live one.
default: return Audio.muted ? Theme.accent : Theme.primary; case "mic": return Audio.micMuted ? Theme.treatmentOffText : Theme.accent;
case "brightness": return Theme.accent;
default: return Audio.muted ? Theme.treatmentOffText : Theme.accent;
} }
} }
@@ -104,15 +106,11 @@ PanelWindow {
anchors.margins: 4 anchors.margins: 4
fill: Theme.mantle fill: Theme.mantle
fillOpacity: 0.97 fillOpacity: 0.97
border: Theme.alpha(root.tone, 0.85) border: Theme.surfaceAlt
borderWidth: Theme.stroke borderWidth: Theme.stroke
slash: true slash: true
slashColor: root.tone slashColor: root.tone
slashWidth: 5 slashWidth: 5
halftone: true
halftoneColor: root.tone
halftoneOpacity: 0.08
sheen: true
lean: 0.09 lean: 0.09
chop: 14 chop: 14
padding: Theme.padM padding: Theme.padM
@@ -180,8 +178,6 @@ PanelWindow {
// widening readout only moves its own left edge. // widening readout only moves its own left edge.
text: Math.round(root.level * 100) + "%" text: Math.round(root.level * 100) + "%"
pixelSize: Theme.fsBody pixelSize: Theme.fsBody
split: 1
splitOpacity: 0.5
} }
} }
+10 -6
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components" import "root:/components"
import "root:/services" import "root:/services"
// Full-screen power menu. Crimson stripes sweep in behind a stack of leaning // Full-screen power menu. An accent wedge and stripes sweep in behind a stack of leaning
// buttons; Escape or a click on the backdrop dismisses it. // buttons; Escape or a click on the backdrop dismisses it.
PanelWindow { PanelWindow {
id: root id: root
@@ -13,11 +13,16 @@ PanelWindow {
required property var modelData required property var modelData
screen: 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.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-power" WlrLayershell.namespace: "takemi-power"
WlrLayershell.keyboardFocus: Actions.powerMenuOpen WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive ? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None : WlrKeyboardFocus.None
@@ -25,7 +30,6 @@ PanelWindow {
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
readonly property bool open: Actions.powerMenuOpen
property int selectedAction: 0 property int selectedAction: 0
function focusSelectedAction(): void { function focusSelectedAction(): void {
@@ -63,7 +67,7 @@ PanelWindow {
Rectangle { Rectangle {
id: backdrop id: backdrop
anchors.fill: parent anchors.fill: parent
color: Theme.alpha(Theme.base, 0.82) color: Theme.alpha(Theme.base, 0.9)
opacity: 0 opacity: 0
TapHandler { TapHandler {
@@ -178,7 +182,7 @@ PanelWindow {
icon: entry.modelData.icon icon: entry.modelData.icon
destructive: entry.modelData.danger destructive: entry.modelData.danger
selected: root.selectedAction === entry.index selected: root.selectedAction === entry.index
accent: entry.modelData.danger ? Theme.accent : Theme.primary accent: entry.modelData.danger ? Theme.alarm : Theme.primary
lean: Theme.skew lean: Theme.skew
lunge: -10 lunge: -10
onClicked: root.invoke(entry.modelData.act) onClicked: root.invoke(entry.modelData.act)
+5 -6
View File
@@ -13,9 +13,9 @@ PanelWindow {
required property var modelData required property var modelData
screen: modelData screen: modelData
// Only the primary bar screen shows toasts, to avoid duplicates. // A toast window exists on every screen, but each notification lands only
readonly property bool primaryScreen: Quickshell.screens.length === 0 // on the monitor that had focus when it arrived. Latched per toast rather
|| Quickshell.screens[0] === modelData // than live, so a card does not hop screens while it is being read.
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-toasts" WlrLayershell.namespace: "takemi-toasts"
@@ -31,7 +31,7 @@ PanelWindow {
implicitHeight: Math.max(1, stack.implicitHeight) implicitHeight: Math.max(1, stack.implicitHeight)
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
visible: primaryScreen && stack.children.length > 0 visible: root.live.length > 0
// Toasts currently on screen, newest first. // Toasts currently on screen, newest first.
property list<Notification> live: [] property list<Notification> live: []
@@ -39,9 +39,8 @@ PanelWindow {
Connections { Connections {
target: Notifs target: Notifs
enabled: root.primaryScreen
function onPopupRequested(notif) { function onPopupRequested(notif) {
if (Actions.focusedScreenName() !== root.modelData.name) return;
root.live = [notif, ...root.live].slice(0, root.maxLive); root.live = [notif, ...root.live].slice(0, root.maxLive);
} }
} }
+2 -2
View File
@@ -19,7 +19,7 @@ Item {
Icon { Icon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Audio.micIcon text: Audio.micIcon
color: Audio.micMuted ? Theme.accent : Theme.text color: Audio.micMuted ? Theme.treatmentOffText : Theme.text
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsLarge
} }
@@ -42,7 +42,7 @@ Item {
Text { Text {
text: Audio.micMuted ? "MUTED" : Audio.micPercent + "%" text: Audio.micMuted ? "MUTED" : Audio.micPercent + "%"
color: Audio.micMuted ? Theme.accent : Theme.subtext color: Audio.micMuted ? Theme.treatmentOffText : Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
+1 -1
View File
@@ -51,7 +51,7 @@ Item {
Text { Text {
text: Audio.muted ? "MUTED" : "LIVE" text: Audio.muted ? "MUTED" : "LIVE"
color: Audio.muted ? Theme.accent : Theme.text color: Audio.muted ? Theme.treatmentOffText : Theme.accent
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
+4 -94
View File
@@ -85,12 +85,12 @@ Popout {
Repeater { Repeater {
model: Audio.sinks.length > 1 ? Audio.sinks : [] model: Audio.sinks.length > 1 ? Audio.sinks : []
PickRow { ListRow {
required property var modelData required property var modelData
width: column.width width: column.width
label: modelData.nickname || modelData.description || modelData.name label: modelData.nickname || modelData.description || modelData.name
icon: "󰓃" icon: "󰓃"
selected: Audio.sink === modelData current: Audio.sink === modelData
onClicked: Audio.setSink(modelData) onClicked: Audio.setSink(modelData)
} }
} }
@@ -147,7 +147,7 @@ Popout {
id: levelIcon id: levelIcon
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: levelRow.icon text: levelRow.icon
color: levelRow.muted ? Theme.accent : levelRow.accent color: levelRow.muted ? Theme.treatmentOffText : levelRow.accent
font.pixelSize: Theme.fsLarge font.pixelSize: Theme.fsLarge
TapHandler { onTapped: levelRow.toggled() } TapHandler { onTapped: levelRow.toggled() }
@@ -171,7 +171,7 @@ Popout {
id: readoutText id: readoutText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: levelRow.readout text: levelRow.readout
color: levelRow.muted ? Theme.accent : Theme.subtext color: levelRow.muted ? Theme.treatmentOffText : Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.weight: Font.Bold font.weight: Font.Bold
@@ -189,94 +189,4 @@ Popout {
onMoved: v => levelRow.moved(v) onMoved: v => levelRow.moved(v)
} }
} }
component SectionRule: Item {
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: sectionLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: parent.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: sectionLabel.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.6)
}
}
component PickRow: Item {
id: pickRow
property string label: ""
property string icon: ""
property bool selected: false
signal clicked()
implicitHeight: 26
Rectangle {
anchors.fill: parent
color: pickHover.hovered
? Theme.alpha(Theme.primary, 0.14)
: (pickRow.selected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: pickRow.selected ? 18 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.icon
color: pickRow.selected ? Theme.primary : Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.label
color: pickRow.selected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
renderType: Text.NativeRendering
}
}
HoverHandler {
id: pickHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: pickRow.clicked()
}
}
} }
+1 -5
View File
@@ -52,8 +52,6 @@ Popout {
text: Math.round(Battery.percent) + "%" text: Math.round(Battery.percent) + "%"
pixelSize: Theme.fsHuge pixelSize: Theme.fsHuge
color: Battery.tint color: Battery.tint
split: 2.5
splitOpacity: 0.7
} }
} }
@@ -68,8 +66,6 @@ Popout {
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
text: Battery.timeLabel text: Battery.timeLabel
pixelSize: Theme.fsBody pixelSize: Theme.fsBody
split: 1.1
splitOpacity: 0.55
} }
Text { Text {
@@ -89,7 +85,7 @@ Popout {
text: "Health " + Math.round(Battery.health) + "%" text: "Health " + Math.round(Battery.health) + "%"
anchors.right: parent.right anchors.right: parent.right
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
color: Battery.health < 70 ? Theme.warn : Theme.muted color: Battery.health < 70 ? Theme.alarm : Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering renderType: Text.NativeRendering
+29 -105
View File
@@ -20,7 +20,7 @@ Popout {
PopoutSurface { PopoutSurface {
anchors.fill: parent anchors.fill: parent
tone: Bt.enabled ? Theme.blue : Theme.muted tone: Bt.enabled ? Theme.accent : Theme.muted
PopoutHeader { PopoutHeader {
id: header id: header
@@ -29,7 +29,7 @@ Popout {
anchors.right: parent.right anchors.right: parent.right
title: "Bluetooth" title: "Bluetooth"
icon: Bt.icon icon: Bt.icon
accent: Theme.blue accent: Theme.accent
subtitle: Bt.adapter ? Bt.adapter.name : "NO ADAPTER" subtitle: Bt.adapter ? Bt.adapter.name : "NO ADAPTER"
} }
@@ -43,7 +43,7 @@ Popout {
P5Button { P5Button {
text: Bt.enabled ? "On" : "Off" text: Bt.enabled ? "On" : "Off"
icon: Bt.enabled ? "󰂯" : "󰂲" icon: Bt.enabled ? "󰂯" : "󰂲"
accent: Bt.enabled ? Theme.blue : Theme.muted accent: Bt.enabled ? Theme.accent : Theme.muted
onClicked: Bt.toggle() onClicked: Bt.toggle()
} }
@@ -55,17 +55,13 @@ Popout {
} }
} }
Text { SectionRule {
id: listLabel id: listLabel
anchors.top: controls.bottom anchors.top: controls.bottom
anchors.topMargin: Theme.padM anchors.topMargin: Theme.padM
anchors.left: parent.left anchors.left: parent.left
text: Bt.devices.length > 0 ? "DEVICES" : "NO DEVICES" anchors.right: parent.right
color: Theme.muted text: Bt.devices.length > 0 ? "Devices" : "No devices"
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
} }
ListView { ListView {
@@ -78,113 +74,41 @@ Popout {
spacing: 2 spacing: 2
model: Bt.devices model: Bt.devices
delegate: Item { delegate: ListRow {
id: devRow id: devRow
required property BluetoothDevice modelData required property BluetoothDevice modelData
width: ListView.view.width width: ListView.view.width
height: 34 icon: Bt.deviceIcon(modelData)
label: Bt.deviceLabel(modelData)
Rectangle { detail: {
anchors.fill: parent if (modelData.pairing) return "pairing…";
color: devHover.hovered if (modelData.connected) return "connected";
? Theme.alpha(Theme.blue, 0.16) if (modelData.paired || modelData.bonded) return "paired";
: (devRow.modelData.connected ? Theme.alpha(Theme.blue, 0.08) : "transparent") return modelData.address;
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: devRow.modelData.connected ? 22 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Bt.deviceIcon(devRow.modelData)
color: devRow.modelData.connected ? Theme.blue : Theme.subtext
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -2
Text {
text: Bt.deviceLabel(devRow.modelData)
color: devRow.modelData.connected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: devRow.modelData.connected ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Text {
text: {
if (devRow.modelData.pairing) return "pairing…";
if (devRow.modelData.connected) return "connected";
if (devRow.modelData.paired || devRow.modelData.bonded) return "paired";
return devRow.modelData.address;
}
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
renderType: Text.NativeRendering
}
}
} }
current: modelData.connected
onClicked: Bt.toggleDevice(modelData)
onRightClicked: if (modelData.paired) modelData.forget()
// Device battery, where BlueZ exposes it. // Device battery, where BlueZ exposes it.
Row { Icon {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 3
visible: devRow.modelData.batteryAvailable visible: devRow.modelData.batteryAvailable
text: "󰥉"
Icon { color: devRow.modelData.battery > 0.25 ? Theme.subtext : Theme.alarm
anchors.verticalCenter: parent.verticalCenter font.pixelSize: Theme.fsSmall
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
}
} }
HoverHandler { Text {
id: devHover anchors.verticalCenter: parent.verticalCenter
cursorShape: Qt.PointingHandCursor visible: devRow.modelData.batteryAvailable
} text: Math.round(devRow.modelData.battery * 100) + "%"
color: Theme.subtext
TapHandler { font.family: Theme.fontMono
acceptedButtons: Qt.LeftButton font.pixelSize: Theme.fsMicro
onTapped: Bt.toggleDevice(devRow.modelData) renderType: Text.NativeRendering
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: if (devRow.modelData.paired) devRow.modelData.forget()
} }
} }
} }
+9 -9
View File
@@ -4,7 +4,7 @@ import "root:/config"
import "root:/components" import "root:/components"
import "root:/services" import "root:/services"
// Month grid with the current day slashed in crimson, plus uptime and a couple // Month grid with the current day slashed in the accent, plus uptime and a couple
// of world clocks. // of world clocks.
Popout { Popout {
id: root id: root
@@ -99,9 +99,9 @@ Popout {
width: dayNames.width / 7 width: dayNames.width / 7
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: modelData text: modelData
color: index >= 5 ? Theme.alpha(Theme.accent, 0.8) : Theme.muted color: index >= 5 ? Theme.subtext : Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1 font.letterSpacing: 1
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -126,7 +126,7 @@ Popout {
width: grid.width / 7 width: grid.width / 7
height: 30 height: 30
// Today gets a leaning crimson slab. // Today gets a leaning accent slab, inked like every selected slab.
Skew { Skew {
anchors.centerIn: parent anchors.centerIn: parent
width: 26 width: 26
@@ -139,7 +139,7 @@ Popout {
anchors.centerIn: parent anchors.centerIn: parent
text: cell.modelData.day text: cell.modelData.day
color: { color: {
if (cell.modelData.today) return Theme.text; if (cell.modelData.today) return Theme.treatmentSelectedText;
if (!cell.modelData.inMonth) return Theme.alpha(Theme.muted, 0.45); if (!cell.modelData.inMonth) return Theme.alpha(Theme.muted, 0.45);
return Theme.subtext; return Theme.subtext;
} }
@@ -167,7 +167,7 @@ Popout {
text: "UPTIME" text: "UPTIME"
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5 font.letterSpacing: 1.5
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -191,7 +191,7 @@ Popout {
text: "TOKYO" text: "TOKYO"
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5 font.letterSpacing: 1.5
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -226,9 +226,9 @@ Popout {
Skew { Skew {
anchors.fill: parent anchors.fill: parent
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent" color: stepHover.hovered ? Theme.treatmentHoverFill : "transparent"
borderWidth: 1 borderWidth: 1
borderColor: Theme.alpha(Theme.outline, 0.7) borderColor: stepHover.hovered ? Theme.treatmentHoverMark : Theme.alpha(Theme.outline, 0.7)
} }
Icon { Icon {
+15 -11
View File
@@ -80,7 +80,7 @@ Popout {
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
fillColor: Theme.surface fillColor: Theme.surface
strokeColor: Theme.alpha(Theme.primary, 0.5) strokeColor: Theme.surfaceAlt
strokeWidth: 2 strokeWidth: 2
PathPolyline { PathPolyline {
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3]) path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
@@ -93,7 +93,7 @@ Popout {
visible: art.status !== Image.Ready visible: art.status !== Image.Ready
text: Media.sourceIcon text: Media.sourceIcon
font.pixelSize: 42 font.pixelSize: 42
color: Theme.alpha(Theme.primary, 0.7) color: Theme.muted
} }
// Spinning rule around the art while playing. // Spinning rule around the art while playing.
@@ -265,14 +265,14 @@ Popout {
: (chip.activeFocus : (chip.activeFocus
? Theme.treatmentFocusFill ? Theme.treatmentFocusFill
: (chip.current : (chip.current
? Theme.alpha(Theme.primary, 0.22) ? Theme.treatmentActiveFill
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent"))) : (chipHover.hovered ? Theme.treatmentHoverFill : "transparent")))
borderWidth: chip.activeFocus ? Theme.stroke : 1 borderWidth: chip.activeFocus ? Theme.stroke : 1
borderColor: !chip.enabled borderColor: !chip.enabled
? Theme.treatmentDisabledMark ? Theme.treatmentDisabledMark
: (chip.activeFocus : (chip.activeFocus
? Theme.treatmentFocusMark ? Theme.treatmentFocusMark
: (chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7))) : (chip.current ? Theme.treatmentActiveMark : Theme.alpha(Theme.outline, 0.7)))
} }
Text { Text {
@@ -283,7 +283,7 @@ Popout {
? Theme.treatmentDisabledText ? Theme.treatmentDisabledText
: (chip.activeFocus : (chip.activeFocus
? Theme.treatmentFocusText ? Theme.treatmentFocusText
: (chip.current ? Theme.text : Theme.muted)) : (chip.current || chipHover.hovered ? Theme.text : Theme.subtext))
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering renderType: Text.NativeRendering
@@ -334,14 +334,17 @@ Popout {
color: { color: {
if (!tb.enabled) return "transparent"; if (!tb.enabled) return "transparent";
if (tb.activeFocus) return Theme.treatmentFocusFill; if (tb.activeFocus) return Theme.treatmentFocusFill;
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22); // The play button is the one solid control on the panel: a
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent"; // surface slab at rest, the accent slab under the pointer. The
// others stay bare until hovered.
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.treatmentHoverFill;
return tb.primary ? Theme.surface : "transparent";
} }
borderWidth: tb.activeFocus ? Theme.stroke * 2 : (tb.primary ? 2 : 1) borderWidth: tb.activeFocus ? Theme.stroke * 2 : (tb.primary ? 2 : 1)
borderColor: tb.enabled borderColor: tb.enabled
? (tb.activeFocus ? (tb.activeFocus
? Theme.treatmentFocusMark ? Theme.treatmentFocusMark
: (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))) : (tbHover.hovered ? Theme.treatmentHoverMark : (tb.primary ? Theme.overlay : Theme.surfaceAlt)))
: Theme.alpha(Theme.outline, 0.4) : Theme.alpha(Theme.outline, 0.4)
Behavior on color { Behavior on color {
@@ -356,8 +359,9 @@ Popout {
color: { color: {
if (!tb.enabled) return Theme.alpha(Theme.muted, 0.5); if (!tb.enabled) return Theme.alpha(Theme.muted, 0.5);
if (tb.activeFocus) return Theme.treatmentFocusText; if (tb.activeFocus) return Theme.treatmentFocusText;
if (tbHover.hovered && tb.primary) return Theme.text; // Ink on the accent slab. White on light blue was barely 2:1.
return Theme.primary; if (tbHover.hovered && tb.primary) return Theme.ink;
return Theme.text;
} }
} }
+25 -79
View File
@@ -76,17 +76,13 @@ Popout {
} }
} }
Text { SectionRule {
id: apsLabel id: apsLabel
anchors.top: controls.bottom anchors.top: controls.bottom
anchors.topMargin: Theme.padM anchors.topMargin: Theme.padM
anchors.left: parent.left anchors.left: parent.left
text: Net.scanning ? "SCANNING…" : "NETWORKS" anchors.right: parent.right
color: Theme.muted text: Net.scanning ? "Scanning…" : "Networks"
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
} }
ListView { ListView {
@@ -99,94 +95,44 @@ Popout {
spacing: 2 spacing: 2
model: Net.wifiNetworks model: Net.wifiNetworks
delegate: Item { delegate: ListRow {
id: apRow id: apRow
required property var modelData required property var modelData
width: ListView.view.width width: ListView.view.width
height: 30 icon: Net.signalIcon(modelData.signalStrength ?? 0)
label: modelData.name
current: modelData.connected
Rectangle { onClicked: {
anchors.fill: parent if (apRow.modelData.connected) {
color: apHover.hovered apRow.modelData.disconnect();
? Theme.alpha(Theme.primary, 0.14) } else {
: (apRow.modelData.connected ? Theme.alpha(Theme.primary, 0.07) : "transparent") // Saved networks connect directly; new secured ones
} // need a passphrase, which NetworkManager's own
// agent will prompt for.
Rectangle { apRow.modelData.connect();
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: apRow.modelData.connected ? 20 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Net.signalIcon(apRow.modelData.signalStrength ?? 0)
color: apRow.modelData.connected ? Theme.primary : Theme.subtext
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.name
color: apRow.modelData.connected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: apRow.modelData.connected ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: Net.secured(apRow.modelData)
text: "󰌾"
color: Theme.muted
font.pixelSize: Theme.fsMicro
} }
} }
Text { Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.known ? "SAVED" : "" visible: apRow.modelData.known
text: "SAVED"
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1 font.letterSpacing: 1
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
HoverHandler { Icon {
id: apHover anchors.verticalCenter: parent.verticalCenter
cursorShape: Qt.PointingHandCursor visible: Net.secured(apRow.modelData)
} text: "󰌾"
color: Theme.muted
TapHandler { font.pixelSize: Theme.fsSmall
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();
}
}
} }
} }
} }
@@ -228,7 +174,7 @@ Popout {
text: rateBlock.label.toUpperCase() text: rateBlock.label.toUpperCase()
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: 8 font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5 font.letterSpacing: 1.5
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
+1 -1
View File
@@ -37,7 +37,7 @@ Popout {
P5Button { P5Button {
text: Notifs.dnd ? "Silenced" : "Silence" text: Notifs.dnd ? "Silenced" : "Silence"
icon: Notifs.dnd ? "󰂛" : "󰂚" icon: Notifs.dnd ? "󰂛" : "󰂚"
accent: Notifs.dnd ? Theme.accent : Theme.primary accent: Notifs.dnd ? Theme.treatmentOffText : Theme.primary
onClicked: Notifs.toggleDnd() onClicked: Notifs.toggleDnd()
} }
+3 -5
View File
@@ -2,8 +2,8 @@ import QtQuick
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// Title bar for a popout: glyph, split-ink heading, right-aligned status text, // Title bar for a popout: glyph, print-shadowed heading, right-aligned status
// and a crimson rule with a chopped tail. // text, and a rule with a chopped accent tail.
Item { Item {
id: root id: root
@@ -32,8 +32,6 @@ Item {
text: root.title.toUpperCase() text: root.title.toUpperCase()
pixelSize: Theme.fsLarge pixelSize: Theme.fsLarge
letterSpacing: 1.5 letterSpacing: 1.5
split: 1.3
splitOpacity: 0.7
} }
} }
@@ -43,7 +41,7 @@ Item {
anchors.topMargin: 6 anchors.topMargin: 6
visible: root.subtitle !== "" visible: root.subtitle !== ""
text: root.subtitle text: root.subtitle
color: Theme.muted color: Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.letterSpacing: 1 font.letterSpacing: 1
+5 -1
View File
@@ -10,7 +10,11 @@ P5Panel {
fill: Theme.mantle fill: Theme.mantle
fillOpacity: 0.97 fillOpacity: 0.97
border: Theme.alpha(tone, 0.75) // A neutral keyline. `tone` used to colour the whole border at 75%, and
// since most panels pass the resting text colour that put a pale 2px frame
// around every popout — the loudest thing on screen for no reason. The
// tone lives in the slash now; the frame is structure.
border: Theme.surfaceAlt
borderWidth: Theme.stroke borderWidth: Theme.stroke
halftone: textured halftone: textured
halftoneColor: tone halftoneColor: tone
+1 -1
View File
@@ -107,7 +107,7 @@ Column {
height: row.entry.isSeparator ? 0 : 26 height: row.entry.isSeparator ? 0 : 26
visible: !row.entry.isSeparator visible: !row.entry.isSeparator
// Hover is a solid crimson block, the same statement the // Hover is a solid accent block, the same statement the
// workspace ticks and the rail icons make. // workspace ticks and the rail icons make.
Skew { Skew {
anchors.fill: parent anchors.fill: parent
+58 -15
View File
@@ -3,6 +3,7 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland
import "root:/services" import "root:/services"
// Shell-wide UI state and the handful of external commands the shell fires. // Shell-wide UI state and the handful of external commands the shell fires.
@@ -16,53 +17,93 @@ Singleton {
property bool lockRequested: false property bool lockRequested: false
property bool sidebarOpen: false property bool sidebarOpen: false
property bool launcherOpen: false property bool launcherOpen: false
property bool askOpen: false
// Seed text for the next launcher open. Lets a keybind or a script hand the // Seed text for the next launcher open. Lets a keybind or a script hand the
// launcher a query it did not have to type — `qs ipc call shell prompt // launcher a query it did not have to type — `qs ipc call shell prompt
// "https://…"` is an open-with dialog for anything that can run a command. // "https://…"` is an open-with dialog for anything that can run a command.
property string launcherSeed: "" property string launcherSeed: ""
// A launcher window exists on every screen, but only one of them is being
// typed at. Latch the focused monitor at open time so the window appears
// there alone — latched rather than live, because the launcher itself takes
// 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) 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 closePowerMenu() { root.powerMenuOpen = false; }
function lock() { root.powerMenuOpen = false; root.lockRequested = true; } function lock() { root.powerMenuOpen = false; root.lockRequested = true; }
function focusedScreenName(): string {
return Hyprland.focusedMonitor?.name
?? Quickshell.screens[0]?.name
?? "";
}
function toggleLauncher() { function toggleLauncher() {
if (!root.launcherOpen) root.launcherSeed = ""; if (!root.launcherOpen) {
root.launcherSeed = "";
root.launcherScreen = root.focusedScreenName();
}
root.launcherOpen = !root.launcherOpen; root.launcherOpen = !root.launcherOpen;
} }
function closeLauncher() { root.launcherOpen = false; } function closeLauncher() { root.launcherOpen = false; }
function toggleAsk() {
if (!root.askOpen) {
root.askSeed = "";
root.askScreen = root.focusedScreenName();
}
root.askOpen = !root.askOpen;
}
function closeAsk() { root.askOpen = false; }
function askAbout(seed: string) {
root.askSeed = seed;
root.askScreen = root.focusedScreenName();
root.askOpen = false;
root.askOpen = true;
}
// Stand in for `fuzzel --dmenu`. See Apps.beginDmenu for the transport. // Stand in for `fuzzel --dmenu`. See Apps.beginDmenu for the transport.
function dmenuLauncher(inPath: string, outPath: string, prompt: string) { function dmenuLauncher(inPath: string, outPath: string, prompt: string) {
Apps.beginDmenu(inPath, outPath, prompt); Apps.beginDmenu(inPath, outPath, prompt);
root.launcherSeed = ""; root.launcherSeed = "";
root.launcherScreen = root.focusedScreenName();
root.launcherOpen = false; root.launcherOpen = false;
root.launcherOpen = true; root.launcherOpen = true;
} }
function promptLauncher(seed: string) { function promptLauncher(seed: string) {
root.launcherSeed = seed; root.launcherSeed = seed;
root.launcherScreen = root.focusedScreenName();
root.launcherOpen = false; root.launcherOpen = false;
root.launcherOpen = true; root.launcherOpen = true;
} }
// ------------------------------------------------------------- commands // ------------------------------------------------------------- commands
// Each slot gets its own Process so a slow one cannot cancel another. // Detached, because nothing here is ever read back. Rotating a fixed set of
Process { id: slotA } // `Process` slots only moves the collision: `uwsm app` lives as long as the
Process { id: slotB } // app it started, so the fourth launch reclaims the first slot and kills the
Process { id: slotC } // app still holding it — and a shell reload kills every one of them at once.
property int _slot: 0
function run(argv: var) { function run(argv: var) {
const slots = [slotA, slotB, slotC]; Quickshell.execDetached(argv);
const p = slots[root._slot % slots.length];
root._slot++;
p.running = false;
p.command = argv;
p.running = true;
} }
function launch(app: string) { function launch(app: string) {
@@ -89,6 +130,8 @@ Singleton {
function lock(): void { root.lock(); } function lock(): void { root.lock(); }
function power(): void { root.togglePowerMenu(); } function power(): void { root.togglePowerMenu(); }
function launcher(): void { root.toggleLauncher(); } function launcher(): void { root.toggleLauncher(); }
function ask(): void { root.toggleAsk(); }
function askAbout(question: string): void { root.askAbout(question); }
function prompt(seed: string): void { root.promptLauncher(seed); } function prompt(seed: string): void { root.promptLauncher(seed); }
function dmenu(inPath: string, outPath: string, prompt: string): void { function dmenu(inPath: string, outPath: string, prompt: string): void {
root.dmenuLauncher(inPath, outPath, prompt); root.dmenuLauncher(inPath, outPath, prompt);
+28 -14
View File
@@ -304,36 +304,43 @@ Singleton {
// So when the target declares no placeholder we build the command line // 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 // ourselves and hand uwsm an argv instead of an ID. It is still scoped and
// still supervised; we just did the expansion. // 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 fieldRe: /%[fFuU]/
readonly property var anyFieldRe: /^%[a-zA-Z]$/ 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 { function launch(item: var, arg: string): void {
root._remember(item.key); root._remember(item.key);
const byId = root.uwsmIdRe.test(item.key);
if (arg === "") { if (byId && arg === "") {
proc.exec(["uwsm", "app", "--", item.key]); Quickshell.execDetached(["uwsm", "app", "--", item.key]);
return; return;
} }
if (root.fieldRe.test(item.exec || "")) { if (byId && root.fieldRe.test(item.exec || "")) {
proc.exec(["uwsm", "app", "--", item.key, arg]); Quickshell.execDetached(["uwsm", "app", "--", item.key, arg]);
return; return;
} }
const argv = root._tokenize(item.exec || "") const argv = root._tokenize(item.exec || "")
.filter(t => !root.anyFieldRe.test(t)); .filter(t => !root.anyFieldRe.test(t));
if (argv.length === 0) { if (argv.length === 0) {
proc.exec(["uwsm", "app", "--", item.key]); if (byId) Quickshell.execDetached(["uwsm", "app", "--", item.key]);
return; return;
} }
argv.push(arg); if (arg !== "") argv.push(arg);
// Without this the unit is named after the bare executable and every // Without this the unit is named after the bare executable and every
// app launched this way collides in the unit namespace. // app launched this way collides in the unit namespace.
const name = item.key.replace(".desktop", "").replace(/[^a-zA-Z0-9_-]/g, "-"); const name = item.key.replace(".desktop", "").replace(/[^a-zA-Z0-9_-]/g, "-");
const head = ["uwsm", "app", "-a", name]; const head = ["uwsm", "app", "-a", name];
if (item.terminal) head.push("-T"); if (item.terminal) head.push("-T");
proc.exec(head.concat(["--"]).concat(argv)); Quickshell.execDetached(head.concat(["--"]).concat(argv));
} }
// Exec lines are shell-ish but not shell: the spec allows double quotes and // Exec lines are shell-ish but not shell: the spec allows double quotes and
@@ -367,17 +374,24 @@ Singleton {
function runShell(line: string, inTerminal: bool): void { function runShell(line: string, inTerminal: bool): void {
if (line.trim() === "") return; if (line.trim() === "") return;
if (inTerminal) proc.exec(["uwsm", "app", "-T", "--", "sh", "-c", line]); if (inTerminal) Quickshell.execDetached(["uwsm", "app", "-T", "--", "sh", "-c", line]);
else proc.exec(["uwsm", "app", "--", "sh", "-c", line]); else Quickshell.execDetached(["uwsm", "app", "--", "sh", "-c", line]);
} }
function copy(text: string): void { function copy(text: string): void {
proc.exec(["wl-copy", "--", text]); Quickshell.execDetached(["wl-copy", "--", text]);
} }
// Fire-and-forget. Each call replaces the last, which is fine because uwsm // Everything above spawns through `Quickshell.execDetached` rather than a
// has already handed the app off to systemd by the time it exits. // `Process`, and it has to. `uwsm app` does not exit once the scope is up —
Process { id: proc } // it stays on as the scope's main process for as long as the app lives. A
// shared `Process` therefore never goes idle, so the next `exec()` kills the
// previous uwsm, systemd tears down the scope with it, and launching the
// second app closes the first. Shell reload did the same thing to every app
// at once, since `manageLifetime` kills the child when the object goes away.
//
// A detached spawn is owned by nobody here, which is exactly right: once
// uwsm has the scope, the shell has no further business in that lifetime.
// ----------------------------------------------------------------- dmenu // ----------------------------------------------------------------- dmenu
// The launcher can stand in for `fuzzel --dmenu`, which is what lets fuzzel // The launcher can stand in for `fuzzel --dmenu`, which is what lets fuzzel
@@ -494,7 +508,7 @@ Singleton {
// wl-copy without being round-tripped through a QML string. // wl-copy without being round-tripped through a QML string.
function pasteClip(item: var): void { function pasteClip(item: var): void {
if (!/^[0-9]+$/.test(item.clipId)) return; if (!/^[0-9]+$/.test(item.clipId)) return;
proc.exec(["sh", "-c", "cliphist decode " + item.clipId + " | wl-copy"]); Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipId + " | wl-copy"]);
} }
function searchClips(term: string): var { function searchClips(term: string): var {
+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: { readonly property color tint: {
if (root.charging || root.full) return Theme.ok; if (root.charging || root.full) return Theme.text;
if (root.critical) return Theme.accent; if (root.critical) return Theme.alarm;
if (root.low) return Theme.warn; if (root.low) return Theme.alarm;
return Theme.primary; return Theme.primary;
} }
+1 -1
View File
@@ -81,7 +81,7 @@ Singleton {
// Urgency-driven accents for the popup chrome. // Urgency-driven accents for the popup chrome.
function urgencyColor(u: int): color { function urgencyColor(u: int): color {
switch (u) { switch (u) {
case NotificationUrgency.Critical: return Theme.accent; case NotificationUrgency.Critical: return Theme.alarm;
case NotificationUrgency.Low: return Theme.muted; case NotificationUrgency.Low: return Theme.muted;
default: return Theme.primary; default: return Theme.primary;
} }
+8
View File
@@ -4,6 +4,7 @@ import "root:/config"
import "root:/services" import "root:/services"
import "root:/bar" import "root:/bar"
import "root:/launcher" import "root:/launcher"
import "root:/ask"
import "root:/overlays" import "root:/overlays"
import "root:/lock" import "root:/lock"
@@ -12,6 +13,7 @@ import "root:/lock"
// bar/ the top bar and its modules // bar/ the top bar and its modules
// popouts/ hover panels hanging off bar modules // popouts/ hover panels hanging off bar modules
// launcher/ the application launcher // launcher/ the application launcher
// ask/ the question panel, dropped from the top edge
// overlays/ power menu, notification toasts, OSD // overlays/ power menu, notification toasts, OSD
// lock/ session lock // lock/ session lock
// services/ data sources (fluxo, pipewire, bluez, upower, mpris, …) // services/ data sources (fluxo, pipewire, bluez, upower, mpris, …)
@@ -51,6 +53,12 @@ ShellRoot {
Launcher {} Launcher {}
} }
Variants {
model: Quickshell.screens
AskPanel {}
}
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens
@@ -0,0 +1,12 @@
# Written by `claude-account router install`. Do not edit by hand.
[Unit]
Description=Router for <name>.headroom.localhost
After=network-online.target
[Service]
ExecStart=/home/narl/.local/bin/claude-account router serve --port 80
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
+13
View File
@@ -0,0 +1,13 @@
# Written by `claude-account headroom install`. Do not edit by hand.
[Unit]
Description=Headroom proxy for the %i Claude account
After=network-online.target
[Service]
EnvironmentFile=%h/.config/claude-account/headroom-%i.env
ExecStart=/home/narl/.local/share/headroom-venv/bin/headroom proxy --host 127.0.0.1 --port ${CLAUDE_ACCOUNT_PORT} --mode cache --no-telemetry
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
@@ -0,0 +1 @@
/home/narl/.config/systemd/user/claude-account-router.service
@@ -0,0 +1 @@
/home/narl/.config/systemd/user/claude-headroom@.service
@@ -0,0 +1 @@
/home/narl/.config/systemd/user/claude-headroom@.service
@@ -1 +0,0 @@
/home/narl/.config/systemd/user/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"