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

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

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

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

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

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

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

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

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

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

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

Removes fuzzel entirely: its config, its theme submodule, the package,
and the last consumers (the bar sigil, fluxo's menu_command). Also drops
hypr/scripts/powermenu.sh, which was fuzzel-based and bound to nothing.
2026-09-16 15:46:51 +02:00
nvrl 9e73c1338f fixed some bugs 2026-08-15 02:11:03 +02:00
nvrl 9cf510e93b fix(qs,fluxo) performance fix for repaints 2026-08-12 20:18:56 +02:00
nvrl 86e90b0213 chore(hypr) migrated to lua confs 2026-08-12 18:05:43 +02:00
126 changed files with 4328 additions and 1938 deletions

No files matched your search

+16 -1
View File
@@ -12,7 +12,6 @@
!waybar/
!zathura/
!mimeapps.list
!fuzzel/
!fish/
!packages
!brave-flags.conf
@@ -29,3 +28,19 @@
fish/fish_variables
waybar/fluxo-rs
!fluxo/
# Desktop theming. These live outside the per-app configs above, but they are
# what makes an app that is neither GTK-aware nor shell-aware match the rice,
# so the branch carries them.
!gtk-3.0/
gtk-3.0/*
!gtk-3.0/settings.ini
!gtk-3.0/gtk.css
!gtk-4.0/
gtk-4.0/*
!gtk-4.0/settings.ini
!gtk-4.0/gtk.css
!xsettingsd/
!qt6ct/
qt6ct/*
!qt6ct/qt6ct.conf
-3
View File
@@ -19,9 +19,6 @@
[submodule "btop/catppuccin-btop"]
path = btop/catppuccin-btop
url = https://github.com/catppuccin/btop.git
[submodule "fuzzel/catppuccin-fuzzel"]
path = fuzzel/catppuccin-fuzzel
url = https://github.com/catppuccin/fuzzel.git
[submodule "nvim"]
path = nvim
url = https://git.narl.io/nvrl/nvim-config.git
+1 -1
View File
@@ -107,7 +107,7 @@ ipc_socket = true
## GENERAL --------------------------------------------------------
## Import additional configuration files.
import = ["~/.config/alacritty/takemi.toml", "~/.config/alacritty/fonts.toml"]
import = ["~/.config/alacritty/catppuccin-mocha.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]]
key = "Return"
+4 -2
View File
@@ -4,9 +4,11 @@ foreground = "#cdd6f4"
dim_foreground = "#7f849c"
bright_foreground = "#cdd6f4"
# Cursor in the shell accent (Mocha blue) rather than stock rosewater, so the
# terminal caret matches the focus colour everywhere else on the desktop.
[colors.cursor]
text = "#1e1e2e"
cursor = "#f5e0dc"
text = "#11111b"
cursor = "#89b4fa"
[colors.vi_mode_cursor]
text = "#1e1e2e"
+1 -1
View File
@@ -68,7 +68,7 @@ update_ms = 1000
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
proc_sorting = "memory"
proc_sorting = "cpu direct"
#* Reverse sorting order, True or False.
proc_reversed = false
+2 -2
View File
@@ -1,10 +1,10 @@
[global]
corner_radius = 4
transparency = 90
frame_color = "#f38ba8"
frame_color = "#89b4fa"
frame_width = 1
separator_color= frame
highlight = "#89dceb"
highlight = "#cba6f7"
[urgency_low]
background = "#1e1e2e"
+100
View File
@@ -0,0 +1,100 @@
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_claude_account_global_optspecs
string join \n h/help V/version
end
function __fish_claude_account_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_claude_account_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end
function __fish_claude_account_using_subcommand
set -l cmd (__fish_claude_account_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end
complete -c claude-account -n "__fish_claude_account_needs_command" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_needs_command" -s V -l version -d 'Print version'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "status" -d 'Show every account, every proxy, every name and everything that is wrong'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "list" -d 'List the accounts, one per line'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "switch" -d 'Make an account the one `claude` runs as'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "add" -d 'Register another account, with a profile and a proxy of its own'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "remove" -d 'Forget an account. Its profile directory is left where it is'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "current" -d 'Print the name of the active account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "dir" -d 'Print the profile directory of an account, for `CLAUDE_CONFIG_DIR`'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "env" -d 'Print the shell lines that put this shell into an account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "url" -d 'Print the address a name resolves to'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "setup" -d 'Install everything at once: the wrapper, a proxy per account, the router'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "adopt" -d 'Take over the proxy services that were written by hand'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "doctor" -d 'Report every problem and exit non-zero when there is one'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "shim" -d 'The `claude` wrapper in ~/.local/bin'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "headroom" -d 'The proxy of one account'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "router" -d 'The router that resolves <name>.headroom.localhost'
complete -c claude-account -n "__fish_claude_account_needs_command" -f -a "completions" -d 'Tab completion, with the account names in it'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s v -l verbose -d 'Add the last lines each proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand status" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -l names -d 'Print the names alone, for scripts and for tab completion'
complete -c claude-account -n "__fish_claude_account_using_subcommand list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand switch" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l dir -d 'A profile directory elsewhere' -r -F
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l port -d 'A port for its proxy; the next free one by default' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -l switch -d 'Make it the account `claude` runs as straight away'
complete -c claude-account -n "__fish_claude_account_using_subcommand add" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand remove" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand current" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand dir" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand env" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand url" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand setup" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand adopt" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand doctor" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "install" -d 'Write ~/.local/bin/claude'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and not __fish_seen_subcommand_from install uninstall" -f -a "uninstall" -d 'Remove it again'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -l force -d 'Overwrite a `claude` that this tool did not write'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand shim; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "install" -d 'Write the service for an account and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "start" -d 'Start the proxy of an account'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "stop" -d 'Stop it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "restart" -d 'Stop and start it'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "port" -d 'Give an account another port, and write it everywhere it is named'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and not __fish_seen_subcommand_from install start stop restart uninstall port logs" -f -a "logs" -d 'Print what one proxy logged'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from start" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from stop" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from restart" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s n -l lines -d 'How many lines' -r
complete -c claude-account -n "__fish_claude_account_using_subcommand headroom; and __fish_seen_subcommand_from logs" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "serve" -d 'Serve the names until stopped'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "install" -d 'Install the router as a user service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "uninstall" -d 'Stop it and remove its service'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "port" -d 'Move the router to another port'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and not __fish_seen_subcommand_from serve install uninstall port list" -f -a "list" -d 'List the names that resolve'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -l port -r
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from serve" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from install" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from port" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand router; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -l install -d 'Write the file where the shell reads it, instead of printing it'
complete -c claude-account -n "__fish_claude_account_using_subcommand completions" -s h -l help -d 'Print help'
# The accounts as they are right now, asked for when Tab is pressed.
complete -c claude-account -n '__fish_seen_subcommand_from switch dir env url remove start stop restart logs install uninstall port' -f -a '(claude-account list --names)'
+1
View File
@@ -0,0 +1 @@
complete --keep-order --exclusive --command enqs-guard --arguments "(COMPLETE=fish enqs-guard -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
+1
View File
@@ -0,0 +1 @@
complete --keep-order --exclusive --command enqs-headroom --arguments "(COMPLETE=fish enqs-headroom -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
+1
View File
@@ -0,0 +1 @@
complete --keep-order --exclusive --command enqs-jail --arguments "(COMPLETE=fish enqs-jail -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))"
+43 -41
View File
@@ -1,50 +1,52 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua.
# Mocha — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/catppuccin-mocha.toml and ~/.config/nvim/lua/takemi.lua.
#
# Replaces the catppuccin fish_frozen_theme.fish that fish 4.3 generated on
# upgrade. Same content as themes/Takemi.theme — that copy exists only so
# `fish_config` can preview it; this one is what actually loads.
# Catppuccin Mocha's own fish theme (themes/Catppuccin Mocha.theme), with the
# selection and pager rows carried over from the Takemi layout: a solid accent
# block with crust punched out of it, rather than a tinted surface. The shell
# selects with a blue slab and marks with mauve; the prompt should not
# disagree.
#
# Note that `fish_config` writing a theme from the web UI will recreate
# fish_frozen_theme.fish, which sorts before this file and so would lose.
set --global fish_color_normal ffffff
set --global fish_color_command ffffff --bold
set --global fish_color_keyword ff2d40
set --global fish_color_param c8c8ce
set --global fish_color_option 8d9bb8
set --global fish_color_quote 7f9b6a
set --global fish_color_escape 9db8bd
set --global fish_color_operator c8c8ce
set --global fish_color_redirection ff6b78
set --global fish_color_end 6b6b70
set --global fish_color_comment 6b6b70 --italics
set --global fish_color_error ff2d40 --bold
set --global fish_color_cancel ff2d40
set --global fish_color_gray 6b6b70
set --global fish_color_autosuggestion 4a4a52
set --global fish_color_selection 000000 --background=ff2d40
set --global fish_color_search_match 000000 --background=c8c8ce
set --global fish_color_match 000000 --background=ff2d40
set --global fish_color_normal cdd6f4
set --global fish_color_command 89b4fa
set --global fish_color_keyword f38ba8
set --global fish_color_param f2cdcd
set --global fish_color_option a6e3a1
set --global fish_color_quote a6e3a1
set --global fish_color_escape eba0ac
set --global fish_color_operator f5c2e7
set --global fish_color_redirection f5c2e7
set --global fish_color_end fab387
set --global fish_color_comment 7f849c --italics
set --global fish_color_error f38ba8 --bold
set --global fish_color_cancel f38ba8
set --global fish_color_gray 6c7086
set --global fish_color_autosuggestion 6c7086
set --global fish_color_selection 11111b --background=89b4fa
set --global fish_color_search_match 11111b --background=cba6f7
set --global fish_color_match 11111b --background=89b4fa
set --global fish_color_history_current --bold
set --global fish_color_valid_path --underline
set --global fish_color_cwd ffffff --bold
set --global fish_color_cwd_root ff2d40 --bold
set --global fish_color_user c8c8ce
set --global fish_color_host ffffff --bold
set --global fish_color_host_remote ff6b78
set --global fish_color_status ff2d40
set --global fish_color_cwd f9e2af --bold
set --global fish_color_cwd_root f38ba8 --bold
set --global fish_color_user 94e2d5
set --global fish_color_host 89b4fa --bold
set --global fish_color_host_remote a6e3a1
set --global fish_color_status f38ba8
set --global fish_pager_color_progress 6b6b70 --background=141418
set --global fish_pager_color_progress 6c7086 --background=313244
set --global fish_pager_color_background
set --global fish_pager_color_prefix ffffff --bold
set --global fish_pager_color_completion c8c8ce
set --global fish_pager_color_description 6b6b70
set --global fish_pager_color_secondary_background --background=0d0d10
set --global fish_pager_color_secondary_completion c8c8ce
set --global fish_pager_color_secondary_description 6b6b70
set --global fish_pager_color_secondary_prefix ffffff --bold
set --global fish_pager_color_selected_background --background=ff2d40
set --global fish_pager_color_selected_completion 000000
set --global fish_pager_color_selected_description 000000
set --global fish_pager_color_selected_prefix 000000 --bold
set --global fish_pager_color_prefix cba6f7 --bold
set --global fish_pager_color_completion cdd6f4
set --global fish_pager_color_description 6c7086
set --global fish_pager_color_secondary_background --background=181825
set --global fish_pager_color_secondary_completion cdd6f4
set --global fish_pager_color_secondary_description 6c7086
set --global fish_pager_color_secondary_prefix cba6f7 --bold
set --global fish_pager_color_selected_background --background=89b4fa
set --global fish_pager_color_selected_completion 11111b
set --global fish_pager_color_selected_description 11111b
set --global fish_pager_color_selected_prefix 11111b --bold
-3
View File
@@ -38,6 +38,3 @@ if status is-login && test -z "$DISPLAY" -a -z "$WAYLAND_DISPLAY"
end
end
# Claude environments
alias claude="CLAUDE_CONFIG_DIR=~/.claude command claude"
alias claude-work="CLAUDE_CONFIG_DIR=~/.claude-work command claude"
+25 -3
View File
@@ -5,9 +5,16 @@
# strings. The Quickshell config (~/.config/quickshell/services/Sys.qml) parses
# them and does its own formatting. The previous human-readable Waybar formats
# are preserved in config.toml.waybar-bak.
#
# Quickshell only consumes cpu, mem, gpu, disk, net, sys and power from fluxo.
# Everything else it gets natively — Quickshell.Services.Mpris, .Pipewire,
# .UPower, .Notifications, Quickshell.Bluetooth — and Brightness.qml reads
# sysfs directly. Those fluxo modules are therefore switched off below: left on,
# they held a libpulse context, a BlueZ D-Bus poll every 2 s and an MPRIS
# marquee ticker firing twice a second, all feeding a bar that does not exist.
[general]
menu_command = "fuzzel --dmenu --prompt \"$FLUXO_PROMPT\""
menu_command = "takemi-dmenu --prompt \"$FLUXO_PROMPT\""
[signals]
network = 1
@@ -44,12 +51,18 @@ format = "{mount}|{used:.1}|{total:.1}"
format = "{percentage}"
[audio]
# Quickshell.Services.Pipewire drives Audio.qml; leaving this on kept a
# libpulse context and its callbacks alive for nothing.
enabled = false
format_sink_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>"
format_sink_muted = "{name} <span size='large'> {icon}</span>"
format_source_unmuted = "{name} {volume:>3}% <span size='large'>{icon}</span>"
format_source_muted = "{name} <span size='large'>{icon}</span>"
[bt]
# Quickshell.Bluetooth drives Bt.qml. This module polled BlueZ over D-Bus every
# 2 seconds regardless.
enabled = false
format_plugin = "{alias} [{left}|{right}] {anc} <span size='large'>󰂰</span>"
format_connected = "{alias} <span size='large'>󰂰</span>"
format_disconnected = "Disconnected <span size='large'>󰂯</span>"
@@ -60,6 +73,11 @@ format_active = "<span size='large'>󰊖</span>"
format_inactive = "<span size='large'></span>"
[mpris]
# Quickshell.Services.Mpris drives Media.qml, and MediaPill/MediaPopout do their
# own eliding and scrolling. This module was the single most expensive thing in
# the daemon: `scroll` woke a ticker every `scroll_speed` ms to advance a marquee
# offset for a Waybar that is not running, and each tick drove the signaler.
enabled = false
format = "{artist} - {title}"
max_length = 20
scroll = true
@@ -81,10 +99,14 @@ enabled = false
format = "{layout}"
[backlight]
enable = true
# Was `enable = true` — not a key fluxo reads, so this section was silently
# running on its default of enabled. Brightness.qml reads
# /sys/class/backlight directly, so the D-Bus watcher here is redundant.
enabled = false
format = "{percentage}"
[dnd]
enabled = true
# Notifs.qml owns do-not-disturb via Quickshell.Services.Notifications.
enabled = false
format_dnd = "<span size='large'>󰂛</span>"
format_normal = "<span size='large'>󰂚</span>"
-19
View File
@@ -1,19 +0,0 @@
include=~/.config/fuzzel/takemi.ini
[main]
# JetBrainsMono to match Theme.fontMono, so the launcher and the bar are
# visibly the same typeface.
font=JetBrainsMono Nerd Font:size=14
terminal=alacritty
dpi-aware=no
width=45
lines=8
horizontal-pad=20
vertical-pad=20
inner-pad=10
prompt=" "
show-actions=no
layer=overlay
-37
View File
@@ -1,37 +0,0 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.toml and ~/.config/nvim/lua/takemi.lua.
#
# Near-black ground, white text, crimson as the only saturated colour. The
# selection is a solid crimson block with the text punched out of it in black,
# which is the same "active" signal the bar and the terminal use.
[colors]
# Flat and fully opaque. The Persona surfaces are ink on ground — translucency
# and blur read as a different design language, so the alpha stays at ff.
background=0a0a0cff
text=c8c8ceff
input=ffffffff
placeholder=6b6b70ff
prompt=ff2d40ff
# Fuzzy-match characters in an unselected row: crimson, since they are the
# reason the row is on screen at all.
match=ff2d40ff
selection=ff2d40ff
selection-text=000000ff
# White rather than black, so the matched characters still read as marked once
# the row is already crimson.
selection-match=ffffffff
# Deliberately the same as the background — hides the match counter.
counter=0a0a0cff
border=ff2d40ff
[border]
# Square. Every corner in this style is either a right angle or a hard chamfer;
# nothing in the shell is rounded.
radius=0
# Theme.stroke.
width=2
+4
View File
@@ -0,0 +1,4 @@
/* Catppuccin Mocha Blue carries the whole GTK3 palette itself, so there is
* nothing to override here — the Takemi branch only needed this file because
* it was painting Fluent's greys black. Kept as a hook for the next time the
* rice disagrees with stock Catppuccin. */
+7
View File
@@ -0,0 +1,7 @@
[Settings]
gtk-theme-name=catppuccin-mocha-blue-standard+default
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
+4
View File
@@ -0,0 +1,4 @@
/* Catppuccin Mocha Blue, straight from the installed theme. The Takemi branch
* overrode Fluent's greys with black here; Mocha needs no override — its own
* surfaces are the palette the rest of the rice is built on. */
@import url("/usr/share/themes/catppuccin-mocha-blue-standard+default/gtk-4.0/gtk.css");
+7
View File
@@ -0,0 +1,7 @@
[Settings]
gtk-theme-name=catppuccin-mocha-blue-standard+default
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Adwaita Sans 11
gtk-cursor-theme-name=Bibata-Modern-Classic
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime.version": "Lua 5.4",
"workspace.library": [
"/usr/share/hypr/stubs"
],
"diagnostics.globals": [
"hl"
]
}
-18
View File
@@ -1,18 +0,0 @@
# narls hyprland dotfiles
source = ~/.config/hypr/modules/monitors.conf
source = ~/.config/hypr/modules/programs.conf
source = ~/.config/hypr/modules/autostart.conf
source = ~/.config/hypr/modules/env.conf
source = ~/.config/hypr/modules/theme.conf
source = ~/.config/hypr/modules/input.conf
source = ~/.config/hypr/modules/keybinds.conf
source = ~/.config/hypr/modules/window_rules.conf
+36
View File
@@ -0,0 +1,36 @@
-- narls hyprland dotfiles
--
-- Hyprland 0.55+ reads this file instead of hyprland.conf. Every part lives in
-- lua/, and each require() is its own scope: an error in one file does not stop
-- the others from loading.
--
-- Order matters only in two places: palette must exist before anything paints
-- with it, and monitors must exist before the lid handler binds to them. Both
-- are handled by the requires below, top to bottom.
-- what the desktop looks like
require("lua/palette")
require("lua/look")
require("lua/animations")
-- what it runs on
require("lua/env")
require("lua/monitors")
require("lua/lid")
require("lua/input")
-- what it runs
require("lua/apps")
require("lua/autostart")
-- how windows behave
require("lua/rules/windows")
require("lua/rules/layers")
-- how it is driven
require("lua/binds/apps")
require("lua/binds/windows")
require("lua/binds/workspaces")
require("lua/binds/shell")
require("lua/binds/media")
require("lua/binds/capture")
+1 -1
View File
@@ -1,6 +1,6 @@
wallpaper {
monitor =
path = ~/.wallpapers/takemi_1.jpg
path = ~/.wallpapers/arch.png
fit_mode = cover
}
-1
View File
@@ -1 +0,0 @@
+47
View File
@@ -0,0 +1,47 @@
-- https://wiki.hypr.land/Configuring/Advanced-and-Cool/Animations/
--
-- Quick and clean. Motion should get out of the way, not perform.
--
-- An earlier pass overdid it: a 1.56 overshoot on every window, an 88% popin
-- scale, and a borderangle looping forever so the focused border was in
-- constant motion. The bounce is dialled back to a hint, the popin barely
-- scales, and the border angle does not animate at all.
hl.config({
animations = {
enabled = true,
},
})
-- ------------------------------------------------------------------- curves
hl.curve("snap", { type = "bezier", points = { { 0.16, 1.00 }, { 0.30, 1.00 } } })
hl.curve("overshoot", { type = "bezier", points = { { 0.34, 1.12 }, { 0.64, 1.00 } } })
hl.curve("hard", { type = "bezier", points = { { 0.55, 0.00 }, { 0.90, 0.35 } } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
-- --------------------------------------------------------------------- tree
-- speed is in ds (1ds = 100ms). Unset leaves inherit their parent.
hl.animation({ leaf = "global", enabled = true, speed = 4, bezier = "snap" })
hl.animation({ leaf = "border", enabled = true, speed = 5, bezier = "snap" })
hl.animation({ leaf = "windows", enabled = true, speed = 4, bezier = "snap" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4, bezier = "overshoot", style = "popin 96%" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 2.6, bezier = "hard", style = "popin 97%" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 3.6, bezier = "snap" })
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "fadeIn", enabled = true, speed = 2.4, bezier = "snap" })
hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.8, bezier = "hard" })
hl.animation({ leaf = "fadeSwitch", enabled = true, speed = 2, bezier = "snap" })
hl.animation({ leaf = "fadeShadow", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "fadeDim", enabled = true, speed = 2.4, bezier = "snap" })
hl.animation({ leaf = "layers", enabled = true, speed = 3, bezier = "snap" })
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "snap", style = "slide" })
hl.animation({ leaf = "layersOut", enabled = true, speed = 2, bezier = "hard", style = "slide" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 2.4, bezier = "hard", style = "slidevert" })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "snap", style = "slidevert" })
+24
View File
@@ -0,0 +1,24 @@
-- The programs the binds reach for. Exported so lua/binds/* name them once.
return {
terminal = "alacritty",
fileManager = "nautilus",
-- The launcher lives in the Quickshell process (quickshell/launcher/), so
-- the bind toggles it over IPC instead of spawning anything. Nothing to
-- start up means it is on screen on the same frame as the keypress.
menu = "qs ipc call shell launcher",
-- Clipboard history. Opens the launcher already in its ":" mode, which
-- reads cliphist itself and copies the chosen entry back — so the picker is
-- the same window, with the same keys, as everything else.
clipboard = "qs ipc call shell prompt :",
-- Proton Drive search. --fzf re-queries the daemon on every keystroke, so
-- results appear as you type; a dmenu could only filter a list it was
-- handed once, which made searching a two-step. It spawns its own terminal
-- (class pdfs-prompt, floated in lua/rules/windows.lua).
pdfs = "pdfs-prompt --gtk",
-- bluetooth menu
bluetooth = "fluxo bt menu",
}
+30
View File
@@ -0,0 +1,30 @@
-- https://wiki.hypr.land/Configuring/Basics/Autostart/
--
-- Everything that used to be exec-once now runs off the start event.
hl.on("hyprland.start", function()
-- Takemi shell — bar, popouts, notifications, OSD, power menu and lock
-- screen. Config lives in ~/.config/quickshell. It owns
-- org.freedesktop.Notifications, so dunst must stay masked
-- (systemctl --user mask dunst.service).
hl.exec_cmd("uwsm app -- qs")
hl.exec_cmd("uwsm app -- nm-applet --indicator")
hl.exec_cmd("uwsm app -- hyprpaper")
-- clipboard history, text and images
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
-- pdfs-prompt has no hidden/resident mode, so the first search of the
-- session is cold. Run one throwaway query once the Proton Drive daemon
-- is up to pull the binary into the page cache and warm the daemon's index.
hl.exec_cmd("sh -c 'sleep 15; pdfs-prompt --feed a >/dev/null 2>&1'")
-- hl.exec_cmd("uwsm app -- nextcloud --background")
-- hl.exec_cmd("uwsm app -- rclone mount google_drive: ~/gdrive")
-- hl.exec_cmd("uwsm app -- protonvpn-app")
-- hl.exec_cmd("uwsm app -- /usr/bin/discord --enable-features=UseOzonePlatform --ozone-platform=wayland --start-minimized")
-- hl.exec_cmd("uwsm app -- /usr/lib/xdg-desktop-portal-hyprland")
-- hl.exec_cmd("sleep 5 && ~/.config/hypr/scripts/replay-ctrl.sh start")
end)
+14
View File
@@ -0,0 +1,14 @@
-- Launching things.
-- https://wiki.hypr.land/Configuring/Basics/Binds/
local apps = require("lua/apps")
local mod = "SUPER" -- Sets "Windows" key as main modifier
hl.bind(mod .. " + RETURN", hl.dsp.exec_cmd(apps.terminal), { description = "terminal" })
hl.bind(mod .. " + E", hl.dsp.exec_cmd(apps.fileManager), { description = "file manager" })
hl.bind(mod .. " + SPACE", hl.dsp.exec_cmd(apps.menu), { description = "app launcher" })
hl.bind(mod .. " + M", hl.dsp.exec_cmd(apps.pdfs), { description = "pdf picker" })
hl.bind(mod .. " + B", hl.dsp.exec_cmd(apps.bluetooth), { description = "bluetooth menu" })
-- Clipboard history. On release so the picker does not inherit the held ALT.
hl.bind("ALT + m", hl.dsp.exec_cmd(apps.clipboard), { release = true, description = "clipboard history" })
+19
View File
@@ -0,0 +1,19 @@
-- Screenshots, replay buffer, webcam. Everything that records something.
local scripts = os.getenv("HOME") .. "/.config/hypr/scripts/"
-- ------------------------------------------------------------- screenshots
-- PRINT selects a region, CTRL grabs the focused window, SHIFT saves to disk
-- instead of the clipboard.
hl.bind("PRINT", hl.dsp.exec_cmd('grim -g "$(slurp)" - | wl-copy'))
hl.bind("SHIFT + PRINT", hl.dsp.exec_cmd('grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +\'%Y-%m-%d_%H-%M-%S\').png'))
hl.bind("CTRL + PRINT", hl.dsp.exec_cmd(scripts .. "screenshot_window.sh copy"))
hl.bind("CTRL + SHIFT + PRINT", hl.dsp.exec_cmd(scripts .. "screenshot_window.sh save"))
-- ------------------------------------------------------- replay and camera
-- On release, so the shortcut is not caught mid-chord.
local on_release = { release = true }
hl.bind("ALT + z", hl.dsp.exec_cmd(scripts .. "replay-ctrl.sh toggle"), on_release)
hl.bind("ALT + SHIFT + z", hl.dsp.exec_cmd(scripts .. "replay-ctrl.sh save"), on_release)
hl.bind("ALT + SHIFT + c", hl.dsp.exec_cmd(scripts .. "droidcam-ctrl.sh toggle"), on_release)
+20
View File
@@ -0,0 +1,20 @@
-- Laptop multimedia keys: volume, mic, brightness, transport.
-- `locked` keeps them working over the lock screen.
-- Volume and brightness repeat when held.
local held = { locked = true, repeating = true }
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("fluxo vol up 5"), held)
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("fluxo vol down 5"), held)
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("fluxo vol mute"), held)
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("fluxo mic mute"), held)
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl s +10%"), held)
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl s 10%-"), held)
-- Transport. Requires playerctl.
local locked = { locked = true }
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), locked)
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), locked)
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), locked)
hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), locked)
+18
View File
@@ -0,0 +1,18 @@
-- Takemi shell (quickshell) IPC. Config lives in ~/.config/quickshell.
local mod = "SUPER"
local function shell(call, opts)
hl.bind(opts.key, hl.dsp.exec_cmd("qs ipc call shell " .. call), { description = opts.description })
end
shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
shell("power", { key = mod .. " + P", description = "power menu" })
shell("ask", { key = mod .. " + A", description = "ask claude" })
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
-- Not a shell call, but it lives with the other session toggles.
local mirror = require("lua/mirror")
hl.bind(mod .. " + SHIFT + P", mirror.toggle, { description = "mirror laptop to all screens" })
+30
View File
@@ -0,0 +1,30 @@
-- Focus, layout and groups — everything that acts on the window under you.
local mod = "SUPER"
-- ------------------------------------------------------------------- window
hl.bind(mod .. " + SHIFT + Q", hl.dsp.window.close())
hl.bind(mod .. " + V", hl.dsp.window.float())
hl.bind(mod .. " + F", hl.dsp.window.fullscreen())
-- hl.bind(mod .. " + t", hl.dsp.layout("togglesplit")) -- dwindle
-- -------------------------------------------------------------------- focus
local directions = { h = "l", l = "r", k = "u", j = "d" }
for key, direction in pairs(directions) do
hl.bind(mod .. " + " .. key, hl.dsp.focus({ direction = direction }))
hl.bind(mod .. " + SHIFT + " .. key:upper(), hl.dsp.window.move({ direction = direction, group_aware = true }))
end
-- ------------------------------------------------------------------- groups
hl.bind(mod .. " + n", hl.dsp.group.toggle())
hl.bind("ALT + Tab", hl.dsp.group.next())
-- hl.bind("ALT + Tab", hl.dsp.group.prev())
-- -------------------------------------------------------------------- mouse
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
-- ----------------------------------------------------------------- keyboard
-- cycle keyboard layout (dvorak-intl <-> us-intl), see lua/input.lua
hl.bind(mod .. " + ALT + SPACE", hl.dsp.exec_cmd("hyprctl switchxkblayout all next"))
+22
View File
@@ -0,0 +1,22 @@
-- Workspaces, including the two scratchpads.
local mod = "SUPER"
-- Switch with mod + [0-9], move the active window with mod + SHIFT + [0-9].
for i = 1, 10 do
local key = i % 10 -- 10 maps to key 0
hl.bind(mod .. " + " .. key, hl.dsp.focus({ workspace = i }))
hl.bind(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }))
end
-- Scroll through existing workspaces with mod + scroll
hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
-- Scratchpads: S holds anything virtual, D holds chat.
local special = { S = "virtual", D = "discord" }
for key, name in pairs(special) do
hl.bind(mod .. " + " .. key, hl.dsp.workspace.toggle_special(name))
hl.bind(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = "special:" .. name }))
end
+38
View File
@@ -0,0 +1,38 @@
-- https://wiki.hypr.land/Configuring/Advanced-and-Cool/Environment-variables/
--
-- Note: this session starts through uwsm, which would rather have these in
-- ~/.config/uwsm/env (toolkit, cursor, GPU) and ~/.config/uwsm/env-hyprland
-- (HYPR*, AQ_*). They are kept here for now so the config stays self-contained.
-- cursor
hl.env("HYPRCURSOR_THEME", "Bibata-Modern-Classic")
hl.env("HYPRCURSOR_SIZE", "24")
hl.env("XCURSOR_THEME", "Bibata-Modern-Classic")
hl.env("XCURSOR_SIZE", "24")
-- session
hl.env("XDG_CURRENT_DESKTOP", "Hyprland")
-- toolkits
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct")
hl.env("MOZ_ENABLE_WAYLAND", "1")
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "wayland")
-- AMD graphics / video acceleration
hl.env("LIBVA_DRIVER_NAME", "radeonsi")
hl.env("VDPAU_DRIVER", "radeonsi")
hl.env("AMD_VULKAN_ICD", "RADV")
-- misc
hl.env("EDITOR", "nvim")
-- hl.env("INTEL_DEBUG", "noccs")
-- hl.env("WLR_DRM_NO_ATOMIC", "1")
-- hl.env("debug:full_cm_proto", "true")
-- headroom
hl.env("HEADROOM_ROLLOUT_CHANNEL", "beta")
hl.env("HEADROOM_OUTPUT_SHAPER", "1")
+32
View File
@@ -0,0 +1,32 @@
-- https://wiki.hypr.land/Configuring/Basics/Variables/#input
hl.config({
input = {
-- index 0 = dvorak-intl (default), index 1 = us-intl
-- switch with $mainMod ALT, SPACE (see lua/binds/windows.lua)
kb_layout = "us,us",
kb_variant = "dvorak-intl,intl",
kb_model = "",
kb_rules = "",
kb_options = "caps:backspace",
follow_mouse = 0,
accel_profile = "flat",
sensitivity = 0, -- -1.0 - 1.0, 0 means no modification.
touchpad = {
natural_scroll = false,
},
},
})
-- Layouts are inherited from the global input block so that layout switching
-- applies to these too. Only bind resolution differs here.
-- https://wiki.hypr.land/Configuring/Advanced-and-Cool/Devices/
for _, name in ipairs({
"at-translated-set-2-keyboard",
"topre-corporation-hhkb-professional",
}) do
hl.device({ name = name, resolve_binds_by_sym = true })
end
+129
View File
@@ -0,0 +1,129 @@
-- Laptop lid handling.
--
-- One rule, applied from several triggers:
--
-- panel off <=> lid is shut AND something else is plugged in
--
-- Both halves are re-checked every time, so the session can never end up with
-- zero outputs. Undocking with the lid shut brings the panel back; booting
-- alone after a docked shutdown just works, because nothing about the previous
-- session is remembered.
--
-- That last part is the fix for the old design: scripts/lid_handler.sh wrote a
-- lid_state.conf that got sourced on the next start, so a laptop that was shut
-- while docked came up believing it still had an external display and blanked
-- its own panel. Lid state now comes from the kernel instead, and the external
-- check is a live one.
local monitors = require("lua/monitors")
-- ------------------------------------------------------------- reading state
-- The kernel's own view of the hinge. Present on this machine as
-- /proc/acpi/button/lid/LID/state; the device name differs per vendor, hence
-- the glob. Returns nil if the file is missing or unparseable.
local function acpi_lid_closed()
local pipe = io.popen("cat /proc/acpi/button/lid/*/state 2>/dev/null")
if not pipe then
return nil
end
local out = pipe:read("a") or ""
pipe:close()
if out:find("closed", 1, true) then
return true
elseif out:find("open", 1, true) then
return false
end
return nil
end
-- Whatever the last switch event said, used only when ACPI has nothing for us.
local last_switch_closed = nil
local function lid_closed()
local acpi = acpi_lid_closed()
if acpi ~= nil then
return acpi
end
return last_switch_closed == true
end
-- ---------------------------------------------------------------- reconciling
-- nil until the first decision, so the first pass always applies.
local panel_off = nil
-- `ignore` is the output being unplugged: during monitor.removed it can still
-- be listed, and counting it would keep the panel switched off.
local function reconcile(ignore)
-- Nothing enumerated yet means we are still parsing the config on a cold
-- start. Leave the defaults from lua/monitors.lua alone and wait for the
-- start event.
local all = hl.get_monitors()
if #all == 0 then
return
end
local externals = 0
for _, mon in ipairs(monitors.externals_present()) do
if mon.name ~= ignore then
externals = externals + 1
end
end
local want_off = lid_closed() and externals > 0
if want_off == panel_off then
return
end
-- First decision of the session, and it agrees with what lua/monitors.lua
-- already applied: nothing actually moved, so no daemon needs poking.
local settled = panel_off == nil and not want_off
panel_off = want_off
if want_off then
monitors.disable_builtin()
else
monitors.enable_builtin()
end
monitors.apply_external()
-- Quickshell follows monitor hotplug by itself; only the wallpaper daemon
-- needs a kick.
if not settled then
hl.exec_cmd("systemctl restart --user hyprpaper")
end
end
-- ------------------------------------------------------------------ triggers
hl.bind("switch:on:Lid Switch", function()
last_switch_closed = true
reconcile()
end, { locked = true })
hl.bind("switch:off:Lid Switch", function()
last_switch_closed = false
reconcile()
end, { locked = true })
-- Docking and undocking. Removal passes the output that is going away.
hl.on("monitor.added", function()
reconcile()
end)
hl.on("monitor.removed", function(mon)
reconcile(mon and mon.name)
end)
-- Cold start: outputs are not up yet at this point, so this settles once they
-- are. On `hyprctl reload` the start event does not fire again, but the
-- monitor list is already populated, so run it inline too.
hl.on("hyprland.start", function()
reconcile()
end)
reconcile()
+109
View File
@@ -0,0 +1,109 @@
-- Persona × Mocha — Persona 5 shapes on the Catppuccin Mocha palette, matched
-- to ~/.config/quickshell.
--
-- Everything static about how the desktop is painted. Motion lives next door
-- in lua/animations.lua.
local c = require("lua/palette")
hl.config({
-- https://wiki.hypr.land/Configuring/Basics/Variables/
general = {
gaps_in = 5,
gaps_out = { top = 6, right = 8, bottom = 8, left = 8 },
border_size = 2,
col = {
active_border = c.activeBorder,
-- Surface-level, not outline: an idle window's frame is structure,
-- and the outline grey was bright enough to compete with the
-- focused border.
inactive_border = c.surfaceAlt,
},
resize_on_border = false,
-- Please see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Tearing/
-- before you turn this on
allow_tearing = false,
layout = "dwindle",
},
decoration = {
-- Persona 5 has no rounded corners. Neither do we.
rounding = 0,
-- Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0,
inactive_opacity = 1.0,
-- A hard crust offset, the same printed drop the shell's panels
-- carry, rather than a soft blur. Sharp with a 1px range leaves a
-- clean band down the right and bottom edges only.
shadow = {
enabled = true,
sharp = true,
range = 1,
offset = { 4, 4 },
color = c.ink,
color_inactive = c.ink,
},
-- Off. Blur is the signature of the glass-and-frost look, which is the
-- opposite of what this theme is doing — every surface here is opaque,
-- flat and hard-edged. With nothing translucent left to blur it also
-- costs three render passes for no visible result.
blur = {
enabled = false,
},
},
misc = {
force_default_wallpaper = 0,
disable_hyprland_logo = true,
-- 0 = off, 1 = always, 2 = fullscreen only (G7 is a 240Hz VRR panel)
vrr = 0,
font_family = "FiraCode Nerd Font",
background_color = c.base,
focus_on_activate = true,
},
group = {
col = {
border_active = c.activeBorder,
border_inactive = c.surfaceAlt,
},
groupbar = {
font_family = "JetBrainsMono Nerd Font",
font_size = 11,
height = 24,
gradients = false,
-- --- Active tab ---
col = {
active = c.accent,
inactive = c.surface,
},
text_color = c.ink,
-- --- Inactive tab ---
text_color_inactive = c.muted,
indicator_height = 3,
},
},
-- https://wiki.hypr.land/Configuring/Layouts/Dwindle-Layout/
dwindle = {
preserve_split = true,
},
-- https://wiki.hypr.land/Configuring/Layouts/Master-Layout/
master = {
new_status = "master",
},
})
+107
View File
@@ -0,0 +1,107 @@
-- Presentation mode: every external output shows the laptop panel.
--
-- Toggled from lua/binds/shell.lua. Mirroring is just a `mirror` field on the
-- monitor rule, so switching it off clears that field and re-applies the specs
-- from lua/monitors.lua to put the Samsung back where it belongs.
--
-- Anything plugged in while mirroring is on gets mirrored too, which is the
-- usual order at a talk: press the key, then find the HDMI cable.
local monitors = require("lua/monitors")
local M = {}
local function notify(text)
hl.notification.create({ text = text, timeout = 3000 })
end
-- The panel is off while docked with the lid shut, see lua/lid.lua. Mirroring
-- a disabled output would leave every screen black.
local function builtin()
for _, mon in ipairs(hl.get_monitors()) do
if mon.name == monitors.laptop then
return mon
end
end
return nil
end
-- A mirroring output drops out of hl.get_monitors(), so the only place left
-- to find it is the panel's own `mirrors` list. That list is also the state:
-- a Lua flag would reset on every config reload while the outputs stay
-- mirrored.
local function mirroring(panel)
local names = {}
for _, mon in ipairs(panel and panel.mirrors or {}) do
table.insert(names, mon.name)
end
return names
end
-- Outputs keep their own mode while mirroring, and the panel image gets
-- rescaled to fit it every frame. The Dells at 1920x1200 flicker with that,
-- so match the panel's resolution; Hyprland falls back to the preferred mode
-- if the output cannot do it.
local function mirror_all(panel)
local mode = panel.width .. "x" .. panel.height
for _, mon in ipairs(monitors.externals_present()) do
hl.monitor({ output = mon.name, mirror = monitors.laptop, mode = mode, position = "auto" })
end
end
-- The bar and popouts stay on the outputs they were created for, so
-- respawn the shell the same way lua/autostart.lua starts it.
local function restart_clients()
hl.exec_cmd("systemctl restart --user hyprpaper")
hl.exec_cmd("sh -c 'qs kill; exec uwsm app -- qs'")
end
-- hl.monitor() only takes effect after the calling handler returns, so
-- anything that has to see the new layout goes on a timer.
local function later(fn)
hl.timer(fn, { timeout = 500, type = "oneshot" })
end
-- Clearing `mirror` alone brings the image back, but Hyprland never
-- re-announces the output to clients: hyprpaper and Quickshell keep seeing
-- only the panel, however often they restart. Cycling the output through
-- disabled makes it a fresh wl_output. Undeclared outputs get their auto
-- defaults back, apply_external() then overrides what lua/monitors.lua knows
-- better.
local function unmirror_all(names)
for _, name in ipairs(names) do
hl.monitor({ output = name, mirror = "", disabled = true })
end
later(function()
for _, name in ipairs(names) do
hl.monitor({ output = name, disabled = false, mode = "preferred", position = "auto" })
end
monitors.apply_external()
later(restart_clients)
end)
end
function M.toggle()
local panel = builtin()
local mirrored = mirroring(panel)
if #mirrored > 0 then
unmirror_all(mirrored)
notify("Mirroring off")
elseif panel then
mirror_all(panel)
later(restart_clients)
notify("Mirroring " .. monitors.laptop .. " to all screens")
else
notify("Laptop panel is off, open the lid first")
end
end
hl.on("monitor.added", function()
local panel = builtin()
if #mirroring(panel) > 0 then
mirror_all(panel)
end
end)
return M
+62
View File
@@ -0,0 +1,62 @@
-- https://wiki.hypr.land/Configuring/Basics/Monitors/
--
-- Declares every display once and exports the table, so lua/lid.lua can turn
-- the laptop panel on and off without duplicating any of these numbers.
--
-- Load-time behaviour is deliberately dumb: the built-in panel is always
-- enabled here. Deciding to switch it off needs to know whether anything else
-- is actually plugged in, and during a cold start the outputs are not
-- enumerated yet — so that decision lives in lua/lid.lua, which re-runs it on
-- every lid and hotplug event. Worst case the panel flickers on for a moment
-- while docked; the alternative failure is a black screen.
local M = {}
M.laptop = "eDP-1"
M.external = {
-- samsung home monitor. Other modes it has run at, if you ever need them:
-- "2560x1440@144", "1920x1080@144", "1280x720@240"
{ output = "desc:Samsung Electric Company LC27G7xT H4ZRA00734", mode = "2560x1440@240", position = "-2560x0", scale = 1 },
}
-- `disabled = false` is load-bearing, not decoration. hl.monitor() merges into
-- whatever rule the output already has, so a spec that simply omits the flag
-- leaves an earlier `disabled = true` in place: the panel stays dark and the
-- mode/position are applied to a monitor nobody can see. Opening the lid while
-- docked hit exactly that.
M.builtin = { output = M.laptop, mode = "1920x1080@60", position = "0x0", scale = 1, disabled = false }
-- Anything not declared above falls back to Hyprland's auto placement, so a
-- borrowed projector or a monitor at the office still lights up.
function M.apply_external()
for _, mon in ipairs(M.external) do
hl.monitor(mon)
end
end
function M.enable_builtin()
hl.monitor(M.builtin)
end
function M.disable_builtin()
hl.monitor({ output = M.laptop, disabled = true })
end
-- Every connected output except the laptop panel. Empty during early startup,
-- which callers have to treat as "do not know yet", never as "nothing here".
function M.externals_present()
local found = {}
for _, mon in ipairs(hl.get_monitors()) do
if mon.name ~= M.laptop then
table.insert(found, mon)
end
end
return found
end
M.enable_builtin()
M.apply_external()
return M
+52
View File
@@ -0,0 +1,52 @@
-- Mocha palette — kept in step with ~/.config/quickshell/config/Theme.qml.
--
-- Catppuccin Mocha ground, blue accent, mauve alarm. Nothing else carries hue.
--
-- Returned as a table, so every module that paints something does
-- local c = require("lua/palette")
-- and there is exactly one place the colours are written.
local c = {}
-- ---------------------------------------------------------------- THE KNOBS
-- Two colours in this setup: blue for anything live (window borders, group
-- indicators, focus), mauve for anything wrong. Change these lines and every
-- painted edge recolours with them.
--
-- Keep them in step with `accent` / `alarm` in
-- ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
-- Those are the only places the colours are written.
c.accent = "rgb(89b4fa)"
c.alarm = "rgb(cba6f7)"
c.accentDim = "rgb(5a7dc0)"
c.accentSoft = "rgb(b4befe)"
-- ------------------------------------------------------------------ neutrals
c.ink = "rgb(11111b)"
c.base = "rgb(1e1e2e)"
c.mantle = "rgb(181825)"
c.surface = "rgb(313244)"
c.surfaceAlt = "rgb(45475a)"
c.overlay = "rgb(585b70)"
c.outline = "rgb(6c7086)"
c.text = "rgb(cdd6f4)"
c.subtext = "rgb(a6adc8)"
c.muted = "rgb(7f849c)"
-- Semantic aliases, so existing references resolve without inventing hues.
c.primary = c.text
c.primaryDim = "rgb(9399b2)"
c.glow = c.accent
c.blue = c.accent
c.deep = c.surfaceAlt
c.warn = c.alarm
c.ok = c.text
-- Blue into lavender, on the diagonal. Both ends are the accent's own family,
-- so the border never reads as two colours arguing.
c.activeBorder = { colors = { c.accent, c.accentSoft }, angle = 135 }
return c
+13
View File
@@ -0,0 +1,13 @@
-- https://wiki.hypr.land/Configuring/Basics/Window-Rules/#layer-rules
--
-- Quickshell surfaces: the bar, popouts, the OSD and the launcher. Note
-- decoration.blur is off in lua/look.lua, so these only take effect if blur is
-- switched back on.
for _, namespace in ipairs({ "bottom", "top", "takemi-launcher" }) do
hl.layer_rule({
match = { namespace = namespace },
blur = true,
ignore_alpha = 0.5,
})
end
+78
View File
@@ -0,0 +1,78 @@
-- https://wiki.hypr.land/Configuring/Basics/Window-Rules/
--
-- Rules are evaluated top to bottom and the last match wins, so order matters.
-- Named rules are all evaluated before anonymous ones.
-- ==========================================
-- MATCHERS
-- ==========================================
-- Dialogs and utility apps that should float in the center
local DIALOG_TITLES = "^(Open Form|Open File|Select a File|Choose a file|Open Workspace|Choose Directory.*|Save As.*|Save File.*|branchdialog|pinentry-gtk-2|Confirm to replace files|File Operation Progress|Open Files.*|Anmelden.*|File Upload.*|TRuDI-Export laden)$"
-- pdfs-prompt is the Proton Drive search: io.narl.* is its GTK window, plain
-- pdfs-prompt the terminal `pdfs-prompt --fzf` spawns for itself. Every
-- terminal it knows how to start sets that same class, so this one rule holds
-- whichever is installed.
local DIALOG_CLASSES = "^(pavucontrol|blueman-manager|nm-connection-editor|org.pulseaudio.pavucontrol|io.narl.proton-drive-linux-prompt|pdfs-prompt)$"
local STEAM = "^(steam)$"
local GAMESCOPE = "^(gamescope)$"
-- ==========================================
-- GENERAL & FIXES
-- ==========================================
-- Ignore maximize requests from apps. You'll probably like this.
-- hl.window_rule({ match = { class = ".*" }, suppress_event = "maximize" })
-- Fix some dragging issues with XWayland
-- hl.window_rule({
-- match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false },
-- no_focus = true,
-- })
hl.window_rule({ match = { class = "^$", title = "^$" }, no_blur = true })
-- ==========================================
-- FLOATING & CENTERED DIALOGS
-- ==========================================
-- Float, size, center, no blur — one rule per matcher instead of four.
for _, match in ipairs({ { title = DIALOG_TITLES }, { class = DIALOG_CLASSES } }) do
hl.window_rule({
match = match,
float = true,
size = { 800, 600 },
center = true,
no_blur = true,
})
end
-- ==========================================
-- WORKSPACE ASSIGNMENTS
-- ==========================================
-- hl.window_rule({ match = { class = "^(firefox)$" }, workspace = "1" })
-- hl.window_rule({ match = { class = "^(kitty)$" }, workspace = "2" })
-- hl.window_rule({ match = { class = "^(Code)$" }, workspace = "3" })
hl.window_rule({ match = { class = GAMESCOPE }, workspace = "1" })
-- Special workspaces
hl.window_rule({ match = { class = "^(Spotify|spotify)$" }, workspace = "special:virtual" })
hl.window_rule({ match = { class = "^(discord|vesktop)$" }, workspace = "special:discord" })
-- ==========================================
-- STEAM & GAMING
-- ==========================================
-- hl.window_rule({ match = { class = STEAM, title = "^(Steam)$" }, workspace = "3 silent" })
-- Fixed sizes for specific Steam windows
hl.window_rule({ match = { class = STEAM, title = "^(Friends List)$" }, size = { 400, 800 } })
hl.window_rule({ match = { class = STEAM, title = "^(Steam Settings)$" }, size = { 1000, 800 } })
hl.window_rule({ match = { class = STEAM, title = "^(Add Non-Steam Game)$" }, size = { 1000, 800 } })
-- Float and disable blur for Steam windows that aren't the main window
hl.window_rule({
match = { class = STEAM, title = "negative:^(Steam)$" },
float = true,
no_blur = true,
-- center = true,
})
-- Allow tearing for games started with Gamescope
hl.window_rule({ match = { class = GAMESCOPE }, immediate = true })
-21
View File
@@ -1,21 +0,0 @@
# Autostart necessary processes (like notifications daemons, status bars, etc.)
# Or execute your favorite apps at launch like this:
# Takemi shell — bar, popouts, notifications, OSD, power menu and lock screen.
# Config lives in ~/.config/quickshell. It owns org.freedesktop.Notifications,
# so dunst must stay masked (systemctl --user mask dunst.service).
exec-once = uwsm app -- qs
# exec-once = uwsm app -- waybar
# exec-once = uwsm app -- hyprpanel
# exec-once = uwsm app -- nextcloud --background
exec-once = uwsm app -- nm-applet --indicator
# exec-once = uwsm app -- rclone mount google_drive: ~/gdrive
# exec-once = uwsm app -- protonvpn-app
exec-once = uwsm app -- hyprpaper
# exec-once = uwsm app -- /usr/bin/discord --enable-features=UseOzonePlatform --ozone-platform=wayland --start-minimized &> /dev/null
# exec-once = sleep 5 && ~/.config/hypr/scripts/replay-ctrl.sh start
# exec-once = uwsm app -- /usr/lib/xdg-desktop-portal-hyprland
exec-once = wl-paste --type text --watch cliphist store # Stores only text data
exec-once = wl-paste --type image --watch cliphist store # Stores only image data
-18
View File
@@ -1,18 +0,0 @@
# See https://wiki.hyprland.org/Configuring/Environment-variables/
# env = INTEL_DEBUG,noccs
# env = WLR_DRM_NO_ATOMIC,1
env = HYPRCURSOR_THEME,Bibata-Modern-Classic
env = HYPRCURSOR_SIZE,24
env = XCURSOR_SIZE,24
env = XCURSOR_THEME,Bibata-Modern-Classic
env = XDG_CURRENT_DESKTOP,Hyprland
# env = debug:full_cm_proto,true
env = QT_QPA_PLATFORMTHEME,qt6ct
env = LIBVA_DRIVER_NAME,radeonsi
env = VDPAU_DRIVER,radeonsi
env = AMD_VULKAN_ICD,RADV
env = MOZ_ENABLE_WAYLAND,1
env = ELECTRON_OZONE_PLATFORM_HINT,wayland
env = EDITOR,nvim
-32
View File
@@ -1,32 +0,0 @@
# https://wiki.hyprland.org/Configuring/Variables/#input
input {
# index 0 = dvorak-intl (default), index 1 = us-intl
# switch with $mainMod ALT, SPACE (see keybinds.conf)
kb_layout = us,us
kb_variant = dvorak-intl,intl
kb_model =
kb_rules =
kb_options = caps:backspace
follow_mouse = 0
accel_profile = flat
sensitivity = 0 # -1.0 - 1.0, 0 means no modification.
touchpad {
natural_scroll = false
}
}
# Layouts are inherited from the global input block so that layout
# switching applies to these too. Only bind resolution differs here.
device {
name = at-translated-set-2-keyboard
resolve_binds_by_sym = 1
}
device {
name = topre-corporation-hhkb-professional
resolve_binds_by_sym = 1
}
-103
View File
@@ -1,103 +0,0 @@
$mainMod = SUPER # Sets "Windows" key as main modifier
bindl=,switch:on:Lid Switch,exec,~/.config/hypr/scripts/lid_handler.sh close
bindl=,switch:off:Lid Switch,exec,~/.config/hypr/scripts/lid_handler.sh open
bindr = ALT, m, exec, cliphist list | fuzzel --dmenu | cliphist decode | wl-copy
bind = , PRINT, exec, grim -g "$(slurp)" - | wl-copy
bind = SHIFT, PRINT, exec, grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +'%Y-%m-%d_%H-%M-%S').png
bind = CTRL, PRINT, exec, ~/.config/hypr/scripts/screenshot_window.sh copy
bind = CTRL SHIFT, PRINT, exec, ~/.config/hypr/scripts/screenshot_window.sh save
bindr = ALT SHIFT, z, exec, ~/.config/hypr/scripts/replay-ctrl.sh save
bindr = ALT, z, exec, ~/.config/hypr/scripts/replay-ctrl.sh toggle
bindr = ALT SHIFT, c, exec, ~/.config/hypr/scripts/droidcam-ctrl.sh toggle
# general binds
bind = $mainMod, M, exec, pdfs-prompt --dmenu --menu 'fuzzel --dmenu --width 60'
bind = $mainMod, B, exec, fluxo bt menu
bind = $mainMod, RETURN, exec, $terminal
bind = $mainMod SHIFT, Q, killactive,
bind = $mainMod CTRL, L, exec, qs ipc call shell lock
bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating,
bind = $mainMod, F, fullscreen,
bind = $mainMod, SPACE, exec, $menu
bind = $mainMod, P, exec, qs ipc call shell power
# Takemi shell
bind = $mainMod SHIFT, N, exec, qs ipc call shell toggleDnd
bind = $mainMod CTRL SHIFT, N, exec, qs ipc call shell clearNotifications
bind = $mainMod SHIFT, R, exec, qs ipc call shell reloadConfig
# cycle keyboard layout (dvorak-intl <-> us-intl)
bind = $mainMod ALT, SPACE, exec, hyprctl switchxkblayout all next
# bind = $mainMod, t, togglesplit, # dwindle
bind = $mainMod, n, togglegroup
# Move focus with mainMod + arrow keys
bind = $mainMod, h, movefocus, l
bind = $mainMod, l, movefocus, r
bind = $mainMod, k, movefocus, u
bind = $mainMod, j, movefocus, d
bind = $mainMod SHIFT, H, movewindoworgroup, l
bind = $mainMod SHIFT, L, movewindoworgroup, r
bind = $mainMod SHIFT, K, movewindoworgroup, u
bind = $mainMod SHIFT, J, movewindoworgroup, d
# Switch workspaces with mainMod + [0-9]
bind = $mainMod, 1, workspace, 1
bind = $mainMod, 2, workspace, 2
bind = $mainMod, 3, workspace, 3
bind = $mainMod, 4, workspace, 4
bind = $mainMod, 5, workspace, 5
bind = $mainMod, 6, workspace, 6
bind = $mainMod, 7, workspace, 7
bind = $mainMod, 8, workspace, 8
bind = $mainMod, 9, workspace, 9
bind = $mainMod, 0, workspace, 10
# Move active window to a workspace with mainMod + SHIFT + [0-9]
bind = $mainMod SHIFT, 1, movetoworkspace, 1
bind = $mainMod SHIFT, 2, movetoworkspace, 2
bind = $mainMod SHIFT, 3, movetoworkspace, 3
bind = $mainMod SHIFT, 4, movetoworkspace, 4
bind = $mainMod SHIFT, 5, movetoworkspace, 5
bind = $mainMod SHIFT, 6, movetoworkspace, 6
bind = $mainMod SHIFT, 7, movetoworkspace, 7
bind = $mainMod SHIFT, 8, movetoworkspace, 8
bind = $mainMod SHIFT, 9, movetoworkspace, 9
bind = $mainMod SHIFT, 0, movetoworkspace, 10
# groups
# bind = ALT, Tab, changegroupactive, prev
bind = ALT, Tab, changegroupactive, next
# Example special workspace (scratchpad)
bind = $mainMod, S, togglespecialworkspace, virtual
bind = $mainMod SHIFT, S, movetoworkspace, special:virtual
bind = $mainMod, D, togglespecialworkspace, discord
bind = $mainMod SHIFT, D, movetoworkspace, special:discord
# Scroll through existing workspaces with mainMod + scroll
bind = $mainMod, mouse_down, workspace, e+1
bind = $mainMod, mouse_up, workspace, e-1
# Move/resize windows with mainMod + LMB/RMB and dragging
bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:273, resizewindow
# Laptop multimedia keys for volume and LCD brightness
bindel = ,XF86AudioRaiseVolume, exec, fluxo vol up 5
bindel = ,XF86AudioLowerVolume, exec, fluxo vol down 5
bindel = ,XF86AudioMute, exec, fluxo vol mute
bindel = ,XF86AudioMicMute, exec, fluxo mic mute
bindel = ,XF86MonBrightnessUp, exec, brightnessctl s +10%
bindel = ,XF86MonBrightnessDown, exec, brightnessctl s 10%-
# Requires playerctl
bindl = , XF86AudioNext, exec, playerctl next
bindl = , XF86AudioPause, exec, playerctl play-pause
bindl = , XF86AudioPlay, exec, playerctl play-pause
bindl = , XF86AudioPrev, exec, playerctl previous
-13
View File
@@ -1,13 +0,0 @@
# laptop screen
monitor = eDP-1, 1920x1080@60, 0x0, 1
# samsung home monitor
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 1280x720@240, 0x0, 1
monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@240, -2560x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@144, 0x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 1920x1080@144, 0x0, 1
# monitor = desc:Samsung Electric Company LC27G7xT H4ZRA00734, 2560x1440@60, 0x0, 1
# lid_state fallback
source = ~/.config/hypr/lid_state.conf
-7
View File
@@ -1,7 +0,0 @@
# See https://wiki.hyprland.org/Configuring/Keywords/
# Set programs that you use
$terminal = alacritty
$fileManager = nautilus
$menu = fuzzel
-39
View File
@@ -1,39 +0,0 @@
# Takemi palette — kept in step with ~/.config/quickshell/config/Theme.qml.
#
# Crimson, black, white. Nothing else carries hue.
# ----------------------------------------------------------------- THE KNOB
# The one colour in this setup. Change this line and the window borders,
# group indicators and focus states all recolour with it.
#
# Keep it in step with `readonly property color accent` in
# ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
# Those are the only two places the colour is written.
$accent = rgb(ff2d40)
$accentDim = rgb(8f1826)
$accentSoft = rgb(ff6b78)
# ------------------------------------------------------------------ neutrals
$ink = rgb(000000)
$base = rgb(0a0a0c)
$mantle = rgb(0d0d10)
$surface = rgb(141418)
$surfaceAlt = rgb(1c1c22)
$overlay = rgb(26262e)
$outline = rgb(3a3a44)
$text = rgb(ffffff)
$subtext = rgb(c8c8ce)
$muted = rgb(6b6b70)
# Retired hues, kept as aliases so existing references resolve. The shell has
# no teal any more — the wallpaper is the colour in the composition.
$primary = $text
$primaryDim = rgb(9a9aa2)
$glow = $accent
$blue = $primaryDim
$deep = $surfaceAlt
$warn = $accentSoft
$ok = $text
-137
View File
@@ -1,137 +0,0 @@
# Takemi — Persona 5 flavoured theme, matched to ~/.config/quickshell.
source = ~/.config/hypr/modules/takemi.conf
# Refer to https://wiki.hyprland.org/Configuring/Variables/
# https://wiki.hyprland.org/Configuring/Variables/#general
general {
gaps_in = 5
gaps_out = 6, 8, 8, 8
border_size = 2
# Crimson into its own shadow, on the diagonal. The accent is the only hue
# here — a gradient into a second colour is what put blue in the border.
col.active_border = $accent $accentDim 135deg
col.inactive_border = $outline
resize_on_border = false
# Please see https://wiki.hyprland.org/Configuring/Tearing/ before you turn this on
allow_tearing = false
layout = dwindle
}
# https://wiki.hyprland.org/Configuring/Variables/#decoration
decoration {
# Persona 5 has no rounded corners. Neither do we.
rounding = 0
# Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0
inactive_opacity = 1.0
shadow {
enabled = true
range = 18
render_power = 3
offset = 0 4
color = rgba(000000ee)
color_inactive = rgba(00000088)
}
# https://wiki.hyprland.org/Configuring/Variables/#blur
#
# Off. Blur is the signature of the glass-and-frost look, which is the
# opposite of what this theme is doing — every surface here is opaque, flat
# and hard-edged. With nothing translucent left to blur it also costs three
# render passes for no visible result.
blur {
enabled = false
}
}
# https://wiki.hyprland.org/Configuring/Animations/
# Quick and clean. Motion should get out of the way, not perform.
#
# The previous pass overdid it: a 1.56 overshoot on every window, an 88% popin
# scale, and a borderangle looping forever so the focused border was in constant
# motion. The bounce is dialled back to a hint, the popin barely scales, and the
# border angle no longer animates at all.
animations {
enabled = yes, please :)
bezier = snap, 0.16, 1.00, 0.30, 1.00
bezier = overshoot, 0.34, 1.12, 0.64, 1.00
bezier = hard, 0.55, 0.00, 0.90, 0.35
bezier = linear, 0, 0, 1, 1
animation = global, 1, 4, snap
animation = border, 1, 5, snap
animation = windows, 1, 4, snap
animation = windowsIn, 1, 4, overshoot, popin 96%
animation = windowsOut, 1, 2.6, hard, popin 97%
animation = windowsMove, 1, 3.6, snap
animation = fade, 1, 3, snap
animation = fadeIn, 1, 2.4, snap
animation = fadeOut, 1, 1.8, hard
animation = fadeSwitch, 1, 2, snap
animation = fadeShadow, 1, 3, snap
animation = fadeDim, 1, 2.4, snap
animation = layers, 1, 3, snap
animation = layersIn, 1, 3, snap, slide
animation = layersOut, 1, 2, hard, slide
animation = workspaces, 1, 3, snap, slidevert
animation = workspacesIn, 1, 3, snap, slidevert
animation = workspacesOut, 1, 2.4, hard, slidevert
animation = specialWorkspace, 1, 3, snap, slidevert
}
# See https://wiki.hyprland.org/Configuring/Master-Layout/ for more
master {
new_status = master
}
dwindle {
preserve_split = true
}
# https://wiki.hyprland.org/Configuring/Variables/#misc
misc {
force_default_wallpaper = 0
disable_hyprland_logo = true
# 0 = off, 1 = always, 2 = fullscreen only (G7 is a 240Hz VRR panel)
vrr = 2
font_family = FiraCode Nerd Font
background_color = $base
focus_on_activate = true
}
group {
col.border_active = $accent $accentDim 135deg
col.border_inactive = $outline
groupbar {
font_family = JetBrainsMono Nerd Font
font_size = 11
height = 24
gradients = false
# --- Active Tab ---
col.active = $accent
text_color = $ink
# --- Inactive Tab ---
col.inactive = $surface
text_color_inactive = $muted
indicator_height = 3
}
}
-89
View File
@@ -1,89 +0,0 @@
# See https://wiki.hyprland.org/Configuring/Window-Rules/ for more
# See https://wiki.hyprland.org/Configuring/Workspace-Rules/ for workspace rules
# ==========================================
# VARIABLES
# ==========================================
# Dialogs and utility apps that should float in the center
$dialog_titles = ^(Open Form|Open File|Select a File|Choose a file|Open Workspace|Choose Directory.*|Save As.*|Save File.*|branchdialog|pinentry-gtk-2|Confirm to replace files|File Operation Progress|Open Files.*|Anmelden.*|File Upload.*|TRuDI-Export laden)$
$dialog_classes = ^(pavucontrol|blueman-manager|nm-connection-editor|org.pulseaudio.pavucontrol|io.narl.proton-drive-linux-prompt)$
# App classes
$steam = ^(steam)$
$gamescope = ^(gamescope)$
# ==========================================
# GENERAL & FIXES
# ==========================================
# Ignore maximize requests from apps. You'll probably like this.
# windowrule = suppress_event maximize, match:class .*
# Fix some dragging issues with XWayland
# windowrule = no_focus 1, match:class ^$, match:title ^$, match:xwayland 1, match:floating 1, match:fullscreen 0, match:pinned 0
windowrule = no_blur 1, match:class ^$, match:title ^$
# ==========================================
# FLOATING & CENTERED DIALOGS
# ==========================================
# Make them float
windowrule = float 1, match:title $dialog_titles
windowrule = float 1, match:class $dialog_classes
# Set to 800x600
windowrule = size 800 600, match:title $dialog_titles
windowrule = size 800 600, match:class $dialog_classes
# Center them on the screen
windowrule = center 1, match:title $dialog_titles
windowrule = center 1, match:class $dialog_classes
# Disable blur
windowrule = no_blur 1, match:title $dialog_titles
windowrule = no_blur 1, match:class $dialog_classes
# ==========================================
# WORKSPACE ASSIGNMENTS
# ==========================================
# Normal Workspaces
# windowrule = workspace 1, match:class ^(firefox)$
# windowrule = workspace 2, match:class ^(kitty)$
# windowrule = workspace 3, match:class ^(Code)$
windowrule = workspace 1, match:class $gamescope
# Special Workspaces
windowrule = workspace special:virtual, match:class ^(Spotify|spotify)$
windowrule = workspace special:discord, match:class ^(discord|vesktop)$
# ==========================================
# APP-SPECIFIC RULES: STEAM & GAMING
# ==========================================
# windowrule = workspace 3 silent, match:class $steam, match:title ^(Steam)$
# Fixed sizes for specific Steam windows
windowrule = size 400 800, match:title ^(Friends List)$, match:class $steam
windowrule = size 1000 800, match:title ^(Steam Settings)$, match:class $steam
windowrule = size 1000 800, match:title ^(Add Non-Steam Game)$, match:class $steam
# Float and disable blur for Steam windows that aren't the main window
windowrule = float 1, match:class $steam, match:title negative:^(Steam)$
windowrule = no_blur 1, match:class $steam, match:title negative:^(Steam)$
# windowrule = center 1, match:class $steam, match:title negative:^(Steam)$
# Allow tearing for games started with Gamescope
windowrule = immediate 1, match:class $gamescope
# Layer rules
# waybar blur
layerrule = blur on, match:namespace bottom
layerrule = blur on, match:namespace top
layerrule = ignore_alpha 0.5, match:namespace bottom
layerrule = ignore_alpha 0.5, match:namespace top
# launcher blur
layerrule = blur on, match:namespace launcher
layerrule = ignore_alpha 0.5, match:namespace launcher
-54
View File
@@ -1,54 +0,0 @@
#!/bin/bash
# ~/.config/hypr/scripts/lid_handler.sh
# The file that tells Hyprland to keep the lid off during reloads
LID_STATE_FILE="$HOME/.config/hypr/lid_state.conf"
restart_ui() {
# The Quickshell bar follows monitor hotplug on its own, so only the
# wallpaper daemon needs a kick.
systemctl restart --user hyprpaper
}
if [[ "$1" == "close" ]]; then
# Check if ANY external monitor is connected and active
if hyprctl monitors all | grep -qE "Monitor (DP|HDMI|Type-C)-"; then
# Prevent laptop screen from turning on during manual config reloads
echo "monitor=eDP-1, disable" > "$LID_STATE_FILE"
# Extract the CURRENT live settings of all external monitors using jq
# This grabs the active resolution, refresh rate, position, and scale.
LIVE_MONITORS=$(hyprctl -j monitors | jq -c '.[] | select(.name != "eDP-1")')
# 3. Disable the laptop screen
hyprctl keyword monitor "eDP-1, disable"
# 4. Re-apply the live settings to external monitors so they don't reset
echo "$LIVE_MONITORS" | while read -r mon_json; do
NAME=$(echo "$mon_json" | jq -r '.name')
WIDTH=$(echo "$mon_json" | jq -r '.width')
HEIGHT=$(echo "$mon_json" | jq -r '.height')
REFRESH=$(echo "$mon_json" | jq -r '.refreshRate')
X=$(echo "$mon_json" | jq -r '.x')
Y=$(echo "$mon_json" | jq -r '.y')
SCALE=$(echo "$mon_json" | jq -r '.scale')
# Formats it exactly as Hyprland expects: DP-1, 1920x1080@240, 0x0, 1
hyprctl keyword monitor "$NAME, ${WIDTH}x${HEIGHT}@${REFRESH}, ${X}x${Y}, $SCALE"
done
# restart ui
restart_ui
fi
elif [[ "$1" == "open" ]]; then
# Clear the override file so the laptop screen is allowed to turn on again
echo "" > "$LID_STATE_FILE"
# Let Hyprland reload itself.
# This automatically re-enables eDP-1 based on your hardcoded hyprland.conf!
hyprctl reload
restart_ui
fi
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
options="Shutdown\nReboot\nLock\nLogout"
selected=$(echo -e $options | fuzzel --dmenu --prompt='Power:')
case $selected in
"Shutdown")
systemctl poweroff
;;
"Reboot")
systemctl reboot
;;
"Lock")
hyprlock
;;
"Logout")
hyprctl dispatch exit
;;
esac
+3 -1
View File
@@ -21,7 +21,8 @@ x-scheme-handler/claude-cli=claude-code-url-handler.desktop
x-scheme-handler/msteams=teams-for-linux.desktop
x-scheme-handler/slack=slack.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]
application/pdf=org.pwmt.zathura-pdf-poppler.desktop;
@@ -40,3 +41,4 @@ application/xhtml+xml=zen.desktop;
application/x-extension-xhtml=zen.desktop;
application/x-extension-xht=zen.desktop;
audio/flac=mpv.desktop;
text/plain=org.neovim.nvim.desktop;
+1 -1
Submodule nvim updated: 8d657d0f8d...4736228253.
-1
View File
@@ -178,7 +178,6 @@ fribidi
fuse-common
fuse2
fuse3
fuzzel
fzf
gawk
gc
+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()
+122 -65
View File
@@ -1,20 +1,21 @@
# Takemi
# Takemi — Persona × Mocha
A Persona 5 flavoured Quickshell desktop for Hyprland. Crimson, black and white,
with no gradients anywhere. Everything leans by the same 14°, corners are
chopped rather than rounded, and every surface is one continuous slab divided by
hairlines rather than a cluster of floating cards.
A Persona 5 flavoured Quickshell desktop for Hyprland, painted in Catppuccin
Mocha. Blue for anything live, mauve for anything wrong, the Mocha greys for
everything else, and no gradients anywhere. Everything leans by the same 14°,
corners are chopped rather than rounded, and every surface is one continuous
slab divided by hairlines rather than a cluster of floating cards.
Replaces Waybar, dunst and hyprlock.
## Running
```sh
qs # starts the shell (Hyprland does this via autostart.conf)
qs # starts the shell (Hyprland does this via autostart.lua)
qs -p lockpreview.qml # the lock screen in an ordinary window, for styling
```
Hyprland starts it from `~/.config/hypr/modules/autostart.conf`.
Hyprland starts it from `~/.config/hypr/lua/autostart.lua`.
## Layout
@@ -41,8 +42,8 @@ cost 4.4% of the height on top of what the bar already takes.
Each permanent surface is a single continuous `ChromeSurface` with modules
divided by sheared hairlines. `BarPill` is chromeless by default — a module is
*content inside* a surface, not a surface of its own — and marks hover with a
crimson underline. Pass `standalone: true` for the rare thing that really is its
*content inside* a surface, not a surface of its own — and marks hover with an
accent underline. Pass `standalone: true` for the rare thing that really is its
own panel.
The shell uses three deliberate surface tiers. **Chrome** (`ChromeSurface`) is
@@ -55,7 +56,7 @@ it is never used as decoration inside ordinary chrome or popouts.
```
shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves
components/ the P5 primitives: panels, gauges, sliders, split-ink text
components/ the P5 primitives: panels, sliders, list rows, print-shadowed text
services/ data sources, all singletons
bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar modules
@@ -106,12 +107,13 @@ dismisses the screen.
## Keybinds
Set in `~/.config/hypr/modules/keybinds.conf`:
Set in `~/.config/hypr/lua/binds/shell.lua`:
- `Super+Ctrl+L` — lock
- `Super+P` — power menu
- `Super+Shift+N` — toggle do not disturb
- `Super+Ctrl+Shift+N` — clear notifications
- `Super+A` — ask panel
- `Super+Shift+R` — reload the shell
All of these go through Quickshell's IPC, so they work from any script:
@@ -120,10 +122,52 @@ All of these go through Quickshell's IPC, so they work from any script:
qs ipc call shell lock
qs ipc call shell power
qs ipc call shell toggleDnd
qs ipc call shell ask
qs ipc call shell askAbout "why is the sky blue"
qs ipc call shell clearNotifications
qs ipc call shell reloadConfig
```
## Ask
`Super+A` drops a panel off the top edge and hands whatever you type to
`claude -p`, streamed token by token. It is the subscription that answers —
the CLI already holds the credentials, so nothing here has an API key and
nothing here has to be given one.
The panel is a chat, not a coding session. Settings sources, MCP servers and
project files are switched off by default, which keeps a question asked from
the desktop from arriving with the last project's hooks, `CLAUDE.md` and tone
attached. The thread survives closing the panel; `Ctrl+L` starts a new one.
| Key | Does |
|---|---|
| `Enter` | ask |
| `Shift+Enter` | newline |
| `Ctrl+M` | next model, written back to `ask.json` |
| `Ctrl+Y` | copy the last answer |
| `Ctrl+L` | new conversation |
| `Ctrl+S` | stop the answer |
| `Escape` | hide, keeping the thread |
Configured in `ask.json`, watched and reloaded live:
```json
{
"model": "sonnet",
"systemPrompt": "…",
"keepContext": true,
"settingSources": "",
"allowedTools": ["WebSearch", "WebFetch"],
"disallowedTools": ["Bash", "Edit", "Write"],
"workingDirectory": "/home/narl",
"maxTurns": 6
}
```
`settingSources` is the escape hatch: set it to `"user,project,local"` and the
panel becomes your ordinary Claude Code, hooks and all.
## Data sources
Hardware telemetry comes from `fluxo`, whose hardware modules are configured to
@@ -165,68 +209,81 @@ The display face is Archivo Black, set italic almost everywhere. Change
## Palette discipline
Crimson, black, white. That is the whole palette.
Catppuccin Mocha, spent on two hues. **Hue is meaning**; nothing gets a colour
for decoration.
**Changing the accent.** The colour is written in exactly three places, one per
program:
- **Blue (`accent`, Mocha blue)** means live: the focused workspace, the
selected row, the hovered control, a channel that is on, an unread badge.
- **Mauve (`alarm`, Mocha mauve)** means wrong or destructive: critical battery,
an urgent workspace, a critical notification, reboot and shut down.
- **Grey** is everything else, and that includes *off*. A muted speaker, a
muted mic and Do Not Disturb drop to `muted` grey with their off glyph. They
used to be painted in the accent, which made a silenced channel look the most
alive thing on the rail.
- **Crust (`ink`)** is structure: the hard keyline, the offset under display
type and panels, the text on an accent slab.
| File | Line | Covers |
|---|---|---|
| `quickshell/config/Theme.qml` | `readonly property color accent: "#ff2d40"` | the shell |
| `hypr/modules/takemi.conf` | `$accent = rgb(ff2d40)` | window borders, groups |
| `alacritty/takemi.toml` | `red`, cursor, selection | the terminal |
**Selection is an inverted slab.** Focus and selection fill with the accent and
print their text in crust (about 9:1). Hover stays on the surface tone with an
accent mark. Destructive-active is the same slab in mauve. The `treatment*`
tokens in `Theme.qml` encode all of this; use them rather than picking colours
per widget.
Everything else derives. `accentDim` and `accentSoft` come off `accent` via
`Qt.darker`/`Qt.lighter`, `glow` (the hover highlight) *is* `accent`, and every
Hyprland border, group indicator and focus state references `$accent`. Change
those lines and the whole setup recolours — swap in Persona 4's yellow or
Persona 3's blue and nothing else needs touching.
**Print, not blur.** Display text (`SplitText`) carries one hard crust offset.
The blue and mauve ghosts appear only while `glitch()` runs. Drawn permanently
1.6px off the body, they read as a focus problem on Mocha's pastels rather than
as misregistered print. The lock screen and the power menu are the exceptions;
they are the cinematic surfaces and keep their split.
**Texture is tiered.** Chrome (bar, rail, tooltips) and transient panels
(popouts, OSD, toasts) are flat. Stripes and halftone live only in
`P5Backdrop`, behind the lock screen and the power menu.
**Type has a floor.** Nothing renders below `Theme.fsMicro` (10px).
**Changing the palette.** The colours are written once per program:
| File | Covers |
|---|---|
| `quickshell/config/Theme.qml` | the shell: `accent`, `alarm` and the neutral ramp |
| `hypr/lua/palette.lua` | window borders, groups, shadows |
| `alacritty/catppuccin-mocha.toml` | the terminal (stock Mocha, cursor in the accent) |
Everything else derives. `accentDim` and `accentSoft` come off `accent`, `glow`
*is* `accent`, and `primary` is plain `text`. `Theme.heat()` is a continuous
text → blue → mauve ramp, so a CPU figure that has gone mauve means the same
thing as a battery that has gone mauve. Tray icons are flattened to one colour
(subtext, the accent on hover) for the same reason: app brand colours wreck the
palette faster than anything else in the shell. nm-applet and blueman are
hidden from the tray, since the rail already shows network and bluetooth, but
keep running as the secret and pairing agents.
## The rest of the desktop
- **Alacritty** — `alacritty/takemi.toml`, selected by the `import` in
`alacritty.toml`. The Catppuccin files are left in place, just unreferenced.
Near-black ground, white text, crimson cursor and selection. The sixteen ANSI
slots keep their hues because syntax highlighters need to tell them apart, but
every one is desaturated so nothing competes with the accent.
- **Neovim** — `nvim/lua/takemi.lua`, loaded through `nvim/colors/takemi.lua`
so `:colorscheme takemi` and `require("takemi").lualine()` both work. Same
palette, same rule: crimson is spent only on keywords, types and errors,
functions are plain white-bold, and `terminal_color_0..15` mirror
`takemi.toml` slot for slot so `:terminal` matches the shell around it.
- **Fish** — `fish/conf.d/takemi_theme.fish`, with a preview copy in
`fish/themes/Takemi.theme` for `fish_config`. Replaced the Catppuccin
`fish_frozen_theme.fish` that fish 4.3 generated on upgrade. The prompt arrow
is white and turns crimson on a non-zero exit — the shell's one "wrong"
signal, and so the one place that earns the accent.
- **Fuzzel** — `fuzzel/takemi.ini`, selected by the `include` in `fuzzel.ini`.
Square corners rather than the stock radius, and a fully opaque ground.
- **Hyprland** — `hypr/lua/look.lua`. No rounding, no blur, both opacities at
`1.0`. The active border runs blue into lavender and the inactive border is
`surfaceAlt`. The window shadow is a sharp 4px crust offset, the same printed
drop the shell's panels carry, not a soft glow.
- **Alacritty** — `alacritty/catppuccin-mocha.toml`, selected by the `import`
in `alacritty.toml`. Stock Mocha except for the cursor, which is the accent.
`takemi.toml` is left in place, unreferenced.
- **Neovim** — the `catppuccin` plugin, flavour mocha, configured in
`nvim/lua/plugins/ui.lua`. Blue is the accent there too. `colors/takemi.lua`
is left in place, unused.
- **Fish** — `fish/conf.d/takemi_theme.fish` is Catppuccin Mocha's fish theme
with the Takemi selection rows: a solid accent block with crust punched out.
- **The launcher** — no external theming to keep in step, because it is part of
this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for
`fuzzel --dmenu` where a script needs a picker.
- **No glass anywhere.** Alacritty is `opacity = 1.0, blur = false`, and
Hyprland's `decoration:blur` is disabled with both opacities at `1.0`. Blur is
the signature of the frosted-glass look, which is the opposite of flat ink on
flat ground — and with nothing translucent left it was costing three render
passes for no visible result.
Hyprland's `decoration:blur` is disabled. Blur is the signature of the
frosted-glass look, which is the opposite of flat ink on flat ground — and
with nothing translucent left it was costing three render passes for no
visible result.
White is the resting voice — text, icons, idle indicators. Crimson is the only
accent and always means one thing: active, or wrong. Black is structure, used as
a hard keyline rather than as a soft shadow. **There are no gradients in this
shell**; every fill is flat. `Theme.sheen` still exists as a no-op property
because a teal-to-transparent wash on every panel was the single biggest source
of the murky blue cast the design used to have.
`primary`, `glow`, `blue`, `ok` and `warn` survive in `Theme.qml` only as
aliases onto white, crimson and grey, so the existing call sites keep working —
don't introduce new uses. `warn` used to alias `accentSoft`, which put a
washed-out pink-red wherever something was merely notable; there is no
half-crimson in this palette, so it aliases the real `accent` now and anything
merely informational (brightness, most readouts) is white instead. `Theme.heat()` is a continuous white→crimson ramp, so
a CPU figure that has gone red means the same thing as a battery that has gone
red. Tray icons are force-flattened to a single colour for the same reason: app
brand colours wreck the palette faster than anything else in the shell.
The wallpaper is teal. The UI used to be teal too, and the two fought each other
into a blue-grey mush. Going monochrome-plus-red is what lets the wallpaper be
the colour in the composition.
There are **no gradients in this shell**; every fill is flat. `Theme.sheen`
still exists as a no-op property because a teal-to-transparent wash on every
panel was once the biggest source of murk in the design.
## Geometry discipline
+11
View File
@@ -0,0 +1,11 @@
{
"model": "sonnet",
"fallbackModel": "",
"systemPrompt": "You are a desktop answer panel. Answer the question directly and compactly, in markdown, with no preamble. Prefer a few sentences over a list, and a list over an essay. Say plainly when you do not know.",
"keepContext": true,
"settingSources": "",
"allowedTools": ["WebSearch", "WebFetch"],
"disallowedTools": ["Bash", "Edit", "Write", "NotebookEdit", "Agent", "Task"],
"workingDirectory": "/home/narl",
"maxTurns": 6
}
+416
View File
@@ -0,0 +1,416 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/popouts"
import "root:/services"
// The ask panel. It comes down off the top edge, takes the keyboard, and hands
// a question to Claude.
//
// Deliberately not the launcher: the launcher is a list you pick from in one
// keystroke, this is a conversation you read. The shape says so — full width of
// the question, answer below it, and the thing drops from the top rather than
// rising into the middle so the two are never mistaken for each other mid-type.
PanelWindow {
id: root
required property var modelData
screen: modelData
// One screen at a time, latched at open — same reason as the launcher: the
// window takes focus, and a prompt should not hop monitors mid-question.
readonly property bool open: Actions.askOpen
&& Actions.askScreen === modelData.name
visible: root.open || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-ask"
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property int panelWidth: Math.min(860, root.width - Theme.padXL * 2)
readonly property int maxTranscript: Math.round(root.height * 0.55)
onOpenChanged: {
if (root.open) {
PopupCoordinator.dismissAll();
input.text = Actions.askSeed;
openAnim.restart();
input.forceActiveFocus();
input.cursorPosition = input.text.length;
if (Actions.askSeed !== "") root.send();
} else {
closeAnim.restart();
}
}
function send(): void {
const q = input.text.trim();
if (q === "" || Ask.busy) return;
Ask.ask(q);
input.text = "";
}
function copyLast(): void {
for (let i = Ask.turns.length - 1; i >= 0; i--) {
if (Ask.turns[i].role === "assistant" && Ask.turns[i].text !== "") {
Actions.copyText(Ask.turns[i].text);
return;
}
}
}
// -------------------------------------------------------------- backdrop
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.6)
opacity: 0
TapHandler { onTapped: Actions.closeAsk() }
}
// ----------------------------------------------------------------- panel
Item {
id: shell
width: root.panelWidth
height: header.height + divider.height + body.height + hint.height
anchors.horizontalCenter: parent.horizontalCenter
y: 0
opacity: 0
// The drop. The offset is animated, never bound: binding it to the
// panel's own height means every height change — every streamed line —
// snaps the panel back off the top of the screen.
transform: Translate { id: drop; y: 0 }
PopoutSurface {
anchors.fill: parent
tone: Ask.busy ? Theme.accent : Theme.primary
padding: 0
slashWidth: Ask.busy ? 8 : 5
// -------------------------------------------------------- header
Item {
id: header
width: parent.width
height: 62
anchors.top: parent.top
Row {
anchors.fill: parent
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
spacing: Theme.padM
// Pulses rather than spins. Native text rendering is
// subpixel-antialiased, so a rotated glyph smears into
// colour fringes; scale keeps it on the pixel grid.
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰭹"
color: Ask.busy ? Theme.accent : Theme.primary
font.pixelSize: Theme.fsTitle
pulsing: Ask.busy
pulseScale: 1.12
}
TextInput {
id: input
anchors.verticalCenter: parent.verticalCenter
width: parent.width - x - modelChip.width - Theme.padM
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsLarge
selectionColor: Theme.accent
selectedTextColor: Theme.ink
clip: true
focus: true
renderType: Text.NativeRendering
Keys.priority: Keys.BeforeItem
Keys.onPressed: event => root.handleKey(event)
Text {
anchors.verticalCenter: parent.verticalCenter
visible: input.text === ""
text: Ask.busy ? "thinking…"
: Ask.turns.length > 0 ? "follow up…" : "ask anything"
color: Theme.muted
font: input.font
renderType: Text.NativeRendering
}
}
// The model, which is also the control for it: Ctrl+M walks
// the ring and writes the choice back to ask.json, so the
// panel and the file never disagree.
Item {
id: modelChip
anchors.verticalCenter: parent.verticalCenter
width: modelText.width + Theme.padM * 2
height: 24
Skew {
anchors.fill: parent
color: Theme.surface
}
Text {
id: modelText
anchors.centerIn: parent
text: Ask.model.toUpperCase()
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Ask.cycleModel()
}
}
}
}
Skew {
id: divider
anchors.top: header.bottom
width: parent.width
height: Theme.stroke
color: Theme.alpha(Theme.outline, 0.8)
lean: 0
}
// --------------------------------------------------- transcript
Item {
id: body
anchors.top: divider.bottom
width: parent.width
height: Math.min(transcript.contentHeight + Theme.padM * 2,
root.maxTranscript)
visible: Ask.turns.length > 0 || Ask.error !== ""
clip: true
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
ListView {
id: transcript
anchors.fill: parent
anchors.margins: Theme.padM
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
model: Ask.turns
spacing: Theme.padM
boundsBehavior: Flickable.StopAtBounds
// Stay at the bottom while an answer streams in, but leave
// the view alone the moment it is scrolled by hand.
property bool pinned: true
onMovementEnded: pinned = atYEnd
onContentHeightChanged: if (pinned) positionViewAtEnd()
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: Column {
required property var modelData
width: transcript.width
spacing: Theme.padS
Text {
visible: modelData.role === "user"
text: "YOU"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
}
Text {
width: parent.width
text: modelData.text
textFormat: modelData.role === "assistant"
? Text.MarkdownText : Text.PlainText
color: modelData.role === "user" ? Theme.subtext : Theme.text
linkColor: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsBody
wrapMode: Text.Wrap
renderType: Text.NativeRendering
onLinkActivated: link => Actions.run(["xdg-open", link])
}
}
footer: Text {
width: transcript.width
visible: Ask.error !== ""
topPadding: visible ? Theme.padM : 0
text: Ask.error
color: Theme.alarm
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
}
// -------------------------------------------------------- hints
Item {
id: hint
anchors.top: body.visible ? body.bottom : divider.bottom
width: parent.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
text: Ask.busy
? "⏎ QUEUE CTRL+S STOP ESC HIDE"
: "⏎ ASK CTRL+M MODEL CTRL+Y COPY CTRL+L NEW ESC HIDE"
}
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
visible: Ask.turns.length > 0
text: Math.floor(Ask.turns.length / 2) + " TURN"
+ (Ask.turns.length > 2 ? "S" : "")
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
}
}
// ------------------------------------------------------------------ keys
function handleKey(event: var): void {
if (!root.open) return;
const ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
if (event.key === Qt.Key_Escape || (ctrl && event.key === Qt.Key_C)) {
// Hide, do not clear. The thread outlives the panel so that closing
// it to read something and reopening to ask about that is one
// conversation, not two.
Actions.closeAsk();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
// Shift+Enter is a newline; a multi-line question is still one
// question. Plain Enter sends.
if ((event.modifiers & Qt.ShiftModifier) === 0) {
root.send();
event.accepted = true;
}
return;
}
if (ctrl && event.key === Qt.Key_L) {
Ask.clear();
input.text = "";
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_S) {
Ask.stop();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_M) {
Ask.cycleModel();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_Y) {
root.copyLast();
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_U) {
input.text = "";
event.accepted = true;
return;
}
// The transcript scrolls on the arrows, because the input is one line
// and has nowhere for them to go.
if (event.key === Qt.Key_PageDown || (ctrl && event.key === Qt.Key_J)) {
transcript.flick(0, -900);
event.accepted = true;
return;
}
if (event.key === Qt.Key_PageUp || (ctrl && event.key === Qt.Key_K)) {
transcript.flick(0, 900);
event.accepted = true;
return;
}
}
// ------------------------------------------------------------- animation
ParallelAnimation {
id: openAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
}
NumberAnimation {
target: shell; property: "opacity"
to: 1; duration: Theme.durFast
}
NumberAnimation {
target: drop; property: "y"
from: -shell.height; to: 0
duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: shell; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: drop; property: "y"
to: -shell.height; duration: Theme.durFast; easing.type: Easing.InQuad
}
}
}
+38 -6
View File
@@ -22,6 +22,11 @@ Item {
readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—"
property string displayedTitle: "—"
// Which toplevel the landing transition last played for. The landing is a
// focus-change stamp, so it has to key off window identity rather than off
// the caption text — see commitTitle().
property var landedToplevel: null
// The bar shrinks this when the three clusters would otherwise collide.
property real maxWidth: 240
@@ -51,7 +56,7 @@ Item {
renderType: Text.NativeRendering
}
// Crimson duplicate: visible only during the landing transition. The
// Accent duplicate: visible only during the landing transition. The
// small opposing offset gives the title Persona's rough print snap
// without leaving permanent motion in the bar.
Text {
@@ -67,11 +72,28 @@ Item {
}
}
// Adopt the settled caption, and stamp it only when focus actually moved.
//
// The landing used to restart on any title change, which is not the same
// thing: plenty of windows rewrite their own title on a timer — a terminal
// running a task with a spinner or a percentage, a browser tab with a live
// clock — and each rewrite restarted seven overlapping animations totalling
// over half a second. At a title churning twice a second on a 240 Hz screen
// the bar never stopped animating, and repainting it that hard measured at
// roughly 18% of a CPU core with nothing else happening. Keying the stamp to
// window identity restores what the effect was described as doing.
function commitTitle(): void {
if (root.displayedTitle === root.incomingTitle)
const focusMoved = root.toplevel !== root.landedToplevel;
if (root.displayedTitle !== root.incomingTitle)
root.displayedTitle = root.incomingTitle;
else if (!focusMoved)
return;
root.displayedTitle = root.incomingTitle;
titleLanding.restart();
if (focusMoved) {
root.landedToplevel = root.toplevel;
titleLanding.restart();
}
}
// Browser tabs, terminals and editors can update titles several times in a
@@ -85,9 +107,19 @@ Item {
}
onIncomingTitleChanged: titleSettle.restart()
Component.onCompleted: displayedTitle = incomingTitle
// A quick crimson wipe under the title on focus change.
// Focus can move to a window whose caption happens to match the outgoing
// one, which changes no title and so would otherwise never land.
onToplevelChanged: titleSettle.restart()
Component.onCompleted: {
displayedTitle = incomingTitle;
// Adopt the current window silently, so a config reload does not play a
// focus-change stamp for a focus that did not change.
landedToplevel = toplevel;
}
// A quick accent wipe under the title on focus change.
Rectangle {
id: wipe
anchors.bottom: parent.bottom
+10 -3
View File
@@ -115,11 +115,18 @@ PanelWindow {
implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight
// A leaning hairline needs a box wide enough to hold the lean: at 20px
// tall the top edge sits 20 * skew ≈ 5px right of the bottom one, so a
// 1px-wide Skew folded into a bowtie instead. Width is the lean plus the
// stroke the rule should actually read as.
readonly property real ruleWeight: 1.5
readonly property real ruleHeight: 20
Skew {
anchors.centerIn: parent
width: 1
height: 20
color: Theme.alpha(Theme.outline, 0.9)
width: parent.ruleHeight * Theme.skew + parent.ruleWeight
height: parent.ruleHeight
color: Theme.alpha(Theme.outline, 0.5)
}
}
}
+20 -9
View File
@@ -5,7 +5,7 @@ import "root:/components"
// Shared chrome for every bar module.
//
// The bar is one continuous slab, so by default a module draws no panel of its
// own — it is content inside the bar's surface, and marks hover with a crimson
// own — it is content inside the bar's surface, and marks hover with an accent
// underline rather than by lighting up a card. That is what stops the bar from
// reading as a row of floating islands.
//
@@ -34,12 +34,18 @@ Item {
//
// The rule used to be drawn into the same box as the content, so anything
// taller than about 32px — the media pill's art, the metric meters — had a
// crimson line struck through its bottom edge on hover. Reserving the band
// accent line struck through its bottom edge on hover. Reserving the band
// and centring content in what is left is what keeps the rule under the
// module instead of across it.
// The band reserved the full rule plus its inset — 7px off a 40px module,
// all of it taken from the bottom — so every module's content was centred
// 3.5px above the slab's own centre and the whole bar read as top-heavy.
// The rule is pushed closer to the edge now and the band keeps only enough
// clearance to stop the tallest content (the media pill's 24px art) from
// touching it, so content sits within a pixel of the slab's centre line.
readonly property real ruleHeight: 2
readonly property real ruleInset: 5
readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset
readonly property real ruleInset: 3
readonly property real ruleBand: root.standalone ? 0 : 2
// Modules with a notion of progress (the player's track position) draw it in
// the same band, so it can never be struck through the content. A module
@@ -96,7 +102,7 @@ Item {
anchors.bottomMargin: root.ruleBand
}
// Hover mark for in-bar modules: a crimson rule under the content, wiping
// Hover mark for in-bar modules: an accent rule under the content, wiping
// out from the leading edge. Sheared to match the one angle everything uses.
Item {
visible: !root.standalone && root.interactive
@@ -109,15 +115,20 @@ Item {
// Track progress, shown until the pointer arrives and the hover rule
// takes the band over.
//
// Deliberately untweened. MPRIS position is polled once a second, and a
// 480 ms ease on a 1 Hz input meant this rule was mid-animation roughly
// half of every second — about 115 animated frames per second on a
// 240 Hz screen, each one rebuilding this Skew's Shape geometry through
// the curve renderer and re-rendering the pill's layer textures with it.
// Stepping once per second costs 1 frame instead of 115, matches the rate
// the underlying data actually arrives at, and makes a seek land exactly
// where it was dropped instead of gliding there.
Skew {
height: parent.height
width: parent.width * Math.max(0, Math.min(1, root.progress))
color: Theme.accent
visible: root.progress >= 0 && !root.hovered && !root.active
Behavior on width {
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
}
}
Skew {
+39 -3
View File
@@ -36,7 +36,8 @@ BarPill {
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2.5
font.weight: Font.Bold
font.letterSpacing: 3
renderType: Text.NativeRendering
}
@@ -50,19 +51,54 @@ BarPill {
}
}
// Declared with room for its own lean, so the slash sits inside the box
// the Row gave it instead of hanging several px into the date column.
Skew {
anchors.verticalCenter: parent.verticalCenter
width: 2
width: 22 * Theme.skew + 3
height: 22
color: Theme.alpha(Theme.accent, 0.6)
}
// Archivo Black's digits are not all the same width, so a clock sized to
// its own text changed width as the minutes rolled over — 11:11 is
// several px narrower than 08:48 — and shoved the rest of the bar
// around. Reserve the widest possible reading and centre inside it.
TextMetrics {
id: timeMetrics
text: "88:88"
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsTitle
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
}
// Archivo Black reserves descender room the time never uses — "12:04"
// is all cap height — so centring the line box left the digits sitting
// above everything beside them. Centre the ink instead.
// `tightBoundingRect` is measured from the baseline, so the ink's top
// inside the line box is `ascent + ink.y`.
readonly property real timeInkOffset: {
const ink = timeMetrics.tightBoundingRect;
if (ink.height <= 0) return 0;
const inkCentre = timeFont.ascent + ink.y + ink.height / 2;
return Math.round(timeMetrics.boundingRect.height / 2 - inkCentre);
}
FontMetrics {
id: timeFont
font: timeMetrics.font
}
SplitText {
id: timeText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: layout.timeInkOffset
width: timeMetrics.advanceWidth
text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: Theme.fsTitle
split: 1.4
horizontalAlignment: Text.AlignHCenter
}
}
+59 -16
View File
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Shapes
import QtQuick.Window
import Quickshell
import "root:/config"
import "root:/components"
@@ -126,35 +127,77 @@ BarPill {
// Compact play-state equaliser. It moves only while audio is playing
// and freezes into three quiet bars when paused.
//
// Sampled from a timer rather than tweened by an infinite
// NumberAnimation. DP-1 runs at 240 Hz, and a frame-synced animation
// here repainted the entire bar 240 times a second: this pill sits
// inside ancestors that use layer.enabled together with
// Shape.CurveRenderer, and the album art carries a MultiEffect pass, so
// every one of those frames re-rendered several layer textures. Measured
// on its own, this one animation cost about two thirds of a CPU core
// whenever anything was playing.
//
// The timer samples a sine, it does not step through arbitrary levels: a
// low tick rate only looks like the original if consecutive ticks stay
// near each other. Each bar keeps the eased breathing motion and the
// period it had before — the curve is just read 25 times a second
// instead of 240.
Row {
id: eq
anchors.verticalCenter: parent.verticalCenter
spacing: 2
// Sampling interval — the one knob trading smoothness against cost.
// Frames are the whole expense: a full-bar repaint runs about 0.3% of
// a core (the bar spans 2544x40 on a 240 Hz output), so cost scales
// linearly with this rate. 16 ms holds 60 Hz, which is visually
// indistinguishable from the original frame-synced tween at a quarter
// of its ~75%.
readonly property int tickMs: 16
property int phase: 0
// Reproduces the original per-bar tween exactly, sampled instead of
// frame-synced. Each bar eased between two heights with its own pair
// of durations, so the three differed in both swing and period — the
// middle bar moved barely a pixel while the outer two ran in
// opposition. Giving all three the full swing, as a single symmetric
// sine would, is three times the motion and reads far busier than
// this meter is supposed to.
function level(i: int): real {
const lo = 4 + (i * 5) % 13; // old first NumberAnimation `to`
const hi = 14 - (i * 4) % 9; // old second NumberAnimation `to`
const d1 = 320 + i * 90; // ...and their durations
const d2 = 280 + i * 70;
const t = (eq.phase * eq.tickMs) % (d1 + d2);
// Easing.InOutSine, which is what both halves used.
const ease = x => (1 - Math.cos(Math.PI * x)) / 2;
return t < d1
? hi + (lo - hi) * ease(t / d1)
: lo + (hi - lo) * ease((t - d1) / d2);
}
Timer {
interval: eq.tickMs
running: Media.playing && eq.visible && (eq.Window.window?.visible ?? true)
repeat: true
onTriggered: eq.phase++
// Restart the cycle from its trough, so playback always begins
// from a settled meter rather than mid-swing.
onRunningChanged: if (!running) eq.phase = 0
}
Repeater {
model: 3
Rectangle {
required property int index
width: 3
height: 6
height: Media.playing ? eq.level(index) : 6
anchors.verticalCenter: parent.verticalCenter
color: Media.playing ? Theme.primary : Theme.muted
SequentialAnimation on height {
running: Media.playing
loops: Animation.Infinite
NumberAnimation {
to: 4 + (index * 5) % 13
duration: 320 + index * 90
easing.type: Easing.InOutSine
}
NumberAnimation {
to: 14 - (index * 4) % 9
duration: 280 + index * 70
easing.type: Easing.InOutSine
}
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
-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 }
}
}
}
+17 -8
View File
@@ -89,11 +89,12 @@ PanelWindow {
id: volIcon
icon: Audio.icon
readout: Audio.muted ? "" : Audio.volumePercent + "%"
reserveReadout: true
label: "Volume"
statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName
hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
alert: Audio.muted
dim: Audio.muted
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
@@ -105,7 +106,7 @@ PanelWindow {
statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName
hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
alert: Audio.micMuted
dim: Audio.micMuted
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
@@ -131,7 +132,7 @@ PanelWindow {
label: "Battery"
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
suppressTooltip: battPopout.detailActive
alert: Battery.critical
alarm: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.togglePinned()
}
@@ -162,8 +163,9 @@ PanelWindow {
readout: Notifs.unread > 0
? (Notifs.unread > 9 ? "9+" : String(Notifs.unread))
: ""
reserveReadout: true
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
live: Notifs.hasUnread && !Notifs.dnd
suppressTooltip: notifPopout.detailActive
onClicked: {
notifPopout.togglePinned();
@@ -176,7 +178,7 @@ PanelWindow {
icon: "󰐥"
label: "Power"
statusText: "Session actions"
alert: Actions.powerMenuOpen
live: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu()
}
}
@@ -187,12 +189,19 @@ PanelWindow {
implicitWidth: Theme.railWidth
implicitHeight: Theme.padM + 4
// Same correction as the bar's divider, on the other axis: a 22px rule
// rises 22 * skew ≈ 5px across its run, so the box has to be that tall
// plus the weight of the stroke. At height 1 the polygon inverted and
// the rule rendered as a stubby wedge sitting left of centre.
readonly property real ruleWeight: 1.5
readonly property real ruleWidth: 22
Skew {
anchors.centerIn: parent
width: 22
height: 1
width: parent.ruleWidth
height: parent.ruleWidth * Theme.skew + parent.ruleWeight
vertical: true
color: Theme.alpha(Theme.outline, 0.9)
color: Theme.alpha(Theme.outline, 0.5)
}
}
+68 -18
View File
@@ -3,10 +3,10 @@ import "root:/config"
import "root:/components"
// One slot on the right-hand rail: a glyph with an optional micro readout
// stacked beneath it, and a crimson bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits
// under the glyph rather than beside it, and anything longer than a percentage
// belongs in the popout instead.
// stacked beneath it, and an accent bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so the readout sits under the glyph
// rather than beside it, and anything longer than a percentage belongs in the
// popout instead.
Item {
id: root
@@ -17,10 +17,25 @@ Item {
property string hint: ""
property bool suppressTooltip: false
property color tint: Theme.primary
property bool alert: false
// Three states, three meanings. `live` wants attention but nothing is wrong
// (unread notifications, an open power menu): accent. `alarm` is wrong or
// critical (a dying battery): alarm. `dim` is off by choice (muted, radio
// off, do-not-disturb): grey. These used to be one `alert` flag painted in
// the accent, which made a muted speaker look like the most active thing
// on the rail.
property bool live: false
property bool alarm: false
property bool dim: false
property bool pulsing: false
// Hold the readout line open even while there is nothing to put in it.
// Volume drops its percentage when muted and the bell only carries a count
// when something is unread, so without this the slot collapsed from 42px to
// 32px and every icon below it jumped a third of its own height.
property bool reserveReadout: false
readonly property bool hasReadout: root.readout !== "" || root.reserveReadout
readonly property bool hovered: hover.hovered
// Clearance between the wipe and the rail's left edge, chosen so the bar
@@ -31,21 +46,48 @@ Item {
signal scrolled(real delta)
implicitWidth: Theme.railWidth
implicitHeight: root.readout !== "" ? 42 : 32
implicitHeight: root.hasReadout ? root.glyphBand + readoutLine.height + 1 : root.glyphBand
Column {
// The glyph always sits centred in a fixed band at the top of the slot and
// the readout hangs below it, rather than the pair being centred together.
// Centring the pair meant the icon slid up and down by a few px whenever its
// number appeared or vanished — the icons stopped lining up with each other
// exactly when something was changing and you were looking at them.
readonly property real glyphBand: 32
// Nerd Font's status glyphs are not all centred inside their own advance —
// the muted-speaker and muted-mic marks carry their cross out to the right,
// so centring the advance box left them visibly off-axis next to the wifi
// and bluetooth glyphs. Centre the ink instead, capped so a glyph with an
// odd bounding box cannot slide far off the rail's axis.
readonly property real opticalShift: {
const ink = glyphMetrics.tightBoundingRect;
if (ink.width <= 0) return 0;
const off = glyphMetrics.advanceWidth / 2 - (ink.x + ink.width / 2);
return Math.max(-4, Math.min(4, off));
}
TextMetrics {
id: glyphMetrics
text: root.icon
font.family: Theme.fontIcon
font.pixelSize: Theme.fsRailIcon
}
Item {
id: layout
anchors.centerIn: parent
spacing: 1
anchors.fill: parent
Icon {
id: glyph
anchors.horizontalCenter: parent.horizontalCenter
x: (root.width - width) / 2 + root.opticalShift
y: (root.glyphBand - height) / 2
text: root.icon
pulsing: root.pulsing
font.pixelSize: Theme.fsLarge
font.pixelSize: Theme.fsRailIcon
color: {
if (root.alert) return Theme.accent;
if (root.alarm) return Theme.alarm;
if (root.live) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
@@ -57,10 +99,17 @@ Item {
}
Text {
id: readoutLine
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: root.glyphBand + 1
visible: root.readout !== ""
text: root.readout
color: root.dim ? Theme.muted : Theme.subtext
color: {
if (root.alarm) return Theme.alarm;
if (root.dim) return Theme.muted;
return Theme.subtext;
}
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold
@@ -80,15 +129,15 @@ Item {
// scale then pushed it out past the silhouette onto the desktop. The inset
// has to survive that scale, which is why it is not simply 1 or 2.
//
// It is crimson rather than `tint` because it only ever appears on hover,
// and hover is crimson everywhere else in the shell (Theme.glow). Painting
// it white put a white bar on the rail's edge for no stated reason.
// It is the accent rather than `tint` because it only ever appears on
// hover, and hover is the accent everywhere else in the shell (Theme.glow).
// Painting it white put a white bar on the rail's edge for no stated reason.
Rectangle {
anchors.left: parent.left
anchors.leftMargin: root.wipeInset
anchors.verticalCenter: parent.verticalCenter
width: 2
height: root.hovered ? layout.implicitHeight : 0
height: root.hovered ? root.height - 4 : 0
color: Theme.glow
Behavior on height {
@@ -101,7 +150,8 @@ Item {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
onAlarmChanged: if (alarm) glyph.bump()
onLiveChanged: if (live) glyph.bump()
HoverHandler {
id: hover
+43 -8
View File
@@ -29,6 +29,7 @@ BarPill {
label: "CPU"
value: Sys.cpuUsage
readout: Math.round(Sys.cpuUsage) + "%"
widest: "100%"
}
Divider {}
@@ -37,6 +38,7 @@ BarPill {
label: "MEM"
value: Sys.memPercent
readout: Sys.memUsed.toFixed(1) + "G"
widest: "88.8G"
}
Divider { visible: Sys.gpuAvailable }
@@ -46,14 +48,23 @@ BarPill {
label: "GPU"
value: Sys.gpuUsage
readout: Math.round(Sys.gpuUsage) + "%"
widest: "100%"
}
}
component Divider: Skew {
component Divider: Item {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 18
color: Theme.alpha(Theme.outline, 0.8)
implicitWidth: rule.width
implicitHeight: 18
// Sized to hold its own lean — see the note on the bar's divider.
Skew {
id: rule
anchors.centerIn: parent
width: parent.height * Theme.skew + 1
height: parent.height
color: Theme.alpha(Theme.outline, 0.5)
}
}
// A tiny fixed label makes every number self-describing without turning the
@@ -65,16 +76,37 @@ BarPill {
property real value: 0
property string readout: ""
// The widest reading this metric will ever show. The number column is
// sized from this rather than from the live text, because sizing to the
// live text is what made the whole right-hand group breathe in and out:
// "9%" and "100%" are ~14px apart in Archivo Black, so every tick of the
// CPU sampler nudged the metrics, the media pill and the clock sideways.
// Reserving the maximum costs a few px of blank and buys a bar that
// never moves.
property string widest: "100%"
readonly property real numberWidth: Math.max(gauge.width, metrics.advanceWidth)
readonly property real headWidth: labelText.implicitWidth + head.spacing + numberWidth
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: Math.max(headWidth, 44)
TextMetrics {
id: metrics
text: metric.widest
font: gauge.font
}
Row {
id: head
spacing: 4
Text {
anchors.baseline: readoutText.baseline
id: labelText
anchors.baseline: gauge.baseline
text: metric.label
color: Theme.muted
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
@@ -83,7 +115,7 @@ BarPill {
}
Text {
id: readoutText
id: gauge
text: metric.readout
color: Theme.heat(metric.value)
font.family: Theme.fontDisplay
@@ -99,12 +131,15 @@ BarPill {
}
}
// Ends flush with the reading above it instead of stopping short at a
// fixed 43px, so each metric reads as one block.
MeterBar {
value: metric.value
segments: 9
segmentWidth: 3
spacing: 2
height: 5
width: metric.width
segmentWidth: (width - 8 * spacing) / 9
}
}
+2 -2
View File
@@ -17,12 +17,12 @@ BarPill {
lean: Theme.skew
implicitWidth: Theme.barHeight + contentPad
onClicked: Actions.launch("fuzzel")
onClicked: Actions.toggleLauncher()
onRightClicked: Actions.reloadShell()
// The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a
// pictogram in a shell that otherwise says everything in heavy italic
// type. SplitText puts it in the display face with the crimson-and-black
// type. SplitText puts it in the display face with the ink print
// offset every other heading carries, so the house mark is built out of
// the same parts as the rest of the bar.
SplitText {
-117
View File
@@ -1,117 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Network, bluetooth, audio, mic, backlight and battery, in one slab. Each
// glyph carries its own popout.
BarPill {
id: root
required property var screenRef
accent: Theme.primary
padding: Theme.padS
interactive: false
implicitWidth: layout.implicitWidth + contentPad * 2
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
StatusIcon {
id: netIcon
icon: Net.icon
label: Sys.netUp ? Sys.rate(Sys.netRx + Sys.netTx) : ""
labelWidth: 62
tint: Theme.primary
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
StatusIcon {
id: btIcon
icon: Bt.icon
tint: Bt.connected.length > 0 ? Theme.blue : Theme.primary
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
StatusIcon {
id: volIcon
icon: Audio.icon
label: Audio.muted ? "" : Audio.volumePercent + "%"
labelWidth: Audio.muted ? 0 : 34
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
StatusIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
StatusIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
label: Math.round(Brightness.percent) + "%"
labelWidth: 34
// Brightness is never an alarm — it stays white and lets the
// battery be the only thing in the cluster that can go crimson.
tint: Theme.text
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
StatusIcon {
id: battIcon
visible: Battery.available
icon: Battery.icon
label: Math.round(Battery.percent) + "%"
labelWidth: 34
tint: Battery.tint
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
visible: Battery.available && shown
}
}
-108
View File
@@ -1,108 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// One indicator inside the status cluster: a glyph, an optional readout, and a
// crimson underline that wipes in on hover.
Item {
id: root
property string icon: ""
property string label: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
// Fixed label width. Readouts that change width constantly (throughput,
// percentages) would otherwise shove the whole cluster around.
property real labelWidth: 0
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: layout.implicitWidth
implicitHeight: Theme.barHeight - Theme.padS * 2
Row {
id: layout
anchors.centerIn: parent
spacing: 5
Icon {
id: glyph
anchors.verticalCenter: parent.verticalCenter
text: root.icon
pulsing: root.pulsing
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.label !== ""
text: root.label
width: root.labelWidth > 0 ? root.labelWidth : implicitWidth
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: 2
width: root.hovered ? layout.implicitWidth : 0
color: root.alert ? Theme.accent : root.tint
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.06 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+35 -17
View File
@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
@@ -15,10 +14,36 @@ Column {
id: root
spacing: Theme.padS + 2
visible: SystemTray.items.values.length > 0
visible: root.shownItems.length > 0
// Applets whose whole job the rail already does. blueman keeps running as
// the pairing agent, but its icon was a second bluetooth glyph a few slots
// below the rail's own.
readonly property var hiddenIds: ["blueman"]
readonly property var shownItems: SystemTray.items.values
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
// Electron and Chromium apps send only a full-colour pixmap, and flattening
// that loses everything inside the outline: Brave's lion became a plain
// shield. The icon theme often ships a panel glyph drawn for exactly this
// job, so look one up by the app's name and use the pixmap only when none
// exists. The id is no help here ("chrome_status_icon_1" for Brave and
// Proton Pass alike), so the tooltip title is the name we go by.
function themedIcon(item: SystemTrayItem): string {
if (item.icon.startsWith("image://icon/")) return item.icon;
const name = (item.tooltipTitle || item.title || item.id)
.toLowerCase().replace(/[^a-z0-9]+/g, "-");
for (const suffix of ["-tray", "-panel", "-symbolic"]) {
const path = Quickshell.iconPath(name + suffix, true);
if (path !== "") return path;
}
return item.icon;
}
Repeater {
model: SystemTray.items
model: root.shownItems
Item {
id: entry
@@ -31,32 +56,25 @@ Column {
// App icons arrive in their own brand colours, which wrecks the
// two-hue palette faster than anything else in the shell. Flatten
// every one of them to a single-colour silhouette: white at rest,
// crimson under the pointer. Detail inside the glyph is lost on
// every one of them to a single-colour silhouette: subtext at rest,
// a notch under the rail's own glyphs, and accent under the pointer. Detail inside the glyph is lost on
// purpose — that flattening *is* the screen-printed look.
IconImage {
id: img
anchors.fill: parent
source: entry.modelData.icon
source: root.themedIcon(entry.modelData)
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: itemHover.hovered
layer.effect: TintEffect {
tintColor: itemHover.hovered
? Theme.accent
: Theme.text
: Theme.subtext
Behavior on colorizationColor {
Behavior on tintColor {
ColorAnimation { duration: Theme.durFast }
}
}
opacity: itemHover.hovered ? 1.0 : 0.82
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
}
scale: itemHover.hovered ? 1.18 : 1.0
+33 -15
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components"
// Workspace indicator: leaning ticks that stretch open when focused. Occupied
// workspaces glow teal, urgent ones snap to crimson.
// workspaces fill in, urgent ones snap to the alarm colour.
Item {
id: root
@@ -65,8 +65,12 @@ Item {
// Every tick is the same height and the same shear. Only the
// width changes, and only to say which workspace is focused.
width: focused ? 32 : (occupied ? 16 : 9)
height: 18
//
// Kept well short of the bar's 20px divider rules. At 18px an
// empty outline tick was the same height, lean and weight as a
// divider, and the workspace strip read as a row of rules.
width: focused ? 30 : (occupied ? 16 : 8)
height: 14
Behavior on width {
NumberAnimation {
@@ -80,23 +84,32 @@ Item {
id: tick
anchors.fill: parent
// Focused is a solid crimson block. Occupied is a white
// block at half weight. Empty is an outline. Three states,
// two colours, no ambiguity.
// Focused is a solid accent block. Urgent is a solid alarm
// block — it used to be the accent too, which made an
// urgent workspace indistinguishable from the focused one.
// Occupied is a subtext block. Empty is a flat recessed
// slot, filled rather than outlined so it cannot pass for a
// divider.
color: {
if (pip.focused || pip.urgent) return Theme.accent;
if (pip.focused) return Theme.accent;
if (pip.urgent) return Theme.alarm;
if (pip.hovered) return Theme.text;
if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
return "transparent";
if (pip.occupied) return Theme.subtext;
return Theme.surfaceAlt;
}
borderColor: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
borderWidth: 1
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
// Urgent is the one pip that has to be seen peripherally.
SequentialAnimation on opacity {
running: pip.urgent && !pip.focused
loops: Animation.Infinite
alwaysRunToEnd: true
NumberAnimation { to: 0.4; duration: 520; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 520; easing.type: Easing.InOutQuad }
}
}
// The focused workspace wears its number.
@@ -107,7 +120,7 @@ Item {
text: pip.wsId
color: Theme.ink
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
@@ -132,8 +145,13 @@ Item {
cursorShape: Qt.PointingHandCursor
}
// Hyprland evaluates IPC `dispatch` payloads as Lua now that the
// config is Lua (hyprland.lua rather than hyprland.conf), so the
// old `dispatch workspace 3` string is a Lua syntax error and the
// click silently does nothing. Dispatchers have to be called the
// same way lua/binds/workspaces.lua calls them.
TapHandler {
onTapped: Hyprland.dispatch("workspace " + pip.wsId)
onTapped: Hyprland.dispatch("hl.dsp.focus({workspace = " + pip.wsId + "})")
}
}
}
-120
View File
@@ -1,120 +0,0 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
// Radial gauge used across the resource popout. Sweeps a 270° arc with a
// tick-marked rail behind it and the reading set in the middle.
Item {
id: root
property real value: 0 // 0..100
property string label: ""
property string readout: Math.round(value) + "%"
property string sub: ""
property color arcColor: Theme.heat(value)
property real thickness: 7
property real startAngle: 135
property real sweep: 270
property int ticks: 24
implicitWidth: 96
implicitHeight: 96
readonly property real _r: Math.min(width, height) / 2 - thickness / 2 - 4
readonly property real _cx: width / 2
readonly property real _cy: height / 2
// Rail
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Theme.alpha(Theme.outline, 0.5)
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep
}
}
}
// Tick marks around the rail.
Repeater {
model: root.ticks
Rectangle {
required property int index
readonly property real a: (root.startAngle + root.sweep * (index / (root.ticks - 1))) * Math.PI / 180
readonly property real rr: root._r + root.thickness / 2 + 3
width: 2
height: index % 4 === 0 ? 6 : 3
color: Theme.alpha(Theme.primary, index % 4 === 0 ? 0.55 : 0.25)
x: root._cx + Math.cos(a) * rr - width / 2
y: root._cy + Math.sin(a) * rr - height / 2
rotation: (root.startAngle + root.sweep * (index / (root.ticks - 1))) + 90
}
}
// Value arc
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: root.arcColor
strokeWidth: root.thickness
capStyle: ShapePath.FlatCap
PathAngleArc {
id: valueArc
centerX: root._cx; centerY: root._cy
radiusX: root._r; radiusY: root._r
startAngle: root.startAngle
sweepAngle: root.sweep * Math.max(0, Math.min(100, root.value)) / 100
Behavior on sweepAngle {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
}
}
}
Column {
anchors.centerIn: parent
spacing: -2
SplitText {
anchors.horizontalCenter: parent.horizontalCenter
text: root.readout
pixelSize: Theme.fsLarge
color: Theme.text
split: 1.2
splitOpacity: 0.6
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.sub !== ""
text: root.sub
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+118
View File
@@ -0,0 +1,118 @@
import QtQuick
import "root:/config"
// One row of a popout list — a Wi-Fi network, a bluetooth device, an audio
// output. The pickers used to draw their own rows, each with its own height and
// its own translucent white wash for hover, which on Mocha's blue-grey ground
// came out as a faint milky film. This is the one row they share now.
//
// Rest is bare. Hover lifts the row onto the surface tone. The current item —
// connected, in use — carries an accent tick down its leading edge and an
// accent glyph, and its label goes to full weight.
Item {
id: root
property string icon: ""
property string label: ""
// Optional mono line under the label: a device state, an address.
property string detail: ""
property bool current: false
// Right-aligned trailing content — a lock, a battery level, a tag.
default property alias trailing: trailingRow.data
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
implicitHeight: root.detail !== "" ? 36 : 30
Rectangle {
anchors.fill: parent
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.current ? root.height - 10 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Icon {
id: glyph
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
width: 18
visible: root.icon !== ""
text: root.icon
color: root.current ? Theme.accent : (root.hovered ? Theme.text : Theme.subtext)
font.pixelSize: Theme.fsLarge
}
Column {
anchors.left: glyph.visible ? glyph.right : parent.left
anchors.leftMargin: glyph.visible ? Theme.padS : Theme.padM
anchors.right: trailingRow.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: -1
Text {
width: parent.width
text: root.label
elide: Text.ElideRight
color: root.current || root.hovered ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: root.current ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Text {
width: parent.width
visible: text !== ""
text: root.detail
elide: Text.ElideRight
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
Row {
id: trailingRow
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: 4
}
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
}
+9 -1
View File
@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Window
import "root:/config"
// Horizontally scrolling text that only moves when it actually overflows, and
@@ -18,6 +19,13 @@ Item {
readonly property bool overflowing: label.implicitWidth > width + 1
// An Item inside an unmapped PopupWindow still reports itself visible, so a
// marquee in a closed popout would keep its infinite animation alive. That
// animation writes QML properties on every frame — at 240 Hz, three closed
// popout marquees are enough to keep the whole animation driver spinning and
// burn a fifth of a core with nothing on screen. Follow the window instead.
readonly property bool onScreen: Window.window?.visible ?? true
clip: true
implicitHeight: label.implicitHeight
implicitWidth: label.implicitWidth
@@ -38,7 +46,7 @@ Item {
SequentialAnimation {
id: scroll
running: root.running && root.overflowing && root.visible
running: root.running && root.overflowing && root.visible && root.onScreen
loops: Animation.Infinite
PauseAnimation { duration: root.pause }
-2
View File
@@ -70,8 +70,6 @@ Item {
text: root.readout
pixelSize: Theme.fsBody
color: Theme.text
split: 1
splitOpacity: 0.5
}
}
+11 -10
View File
@@ -34,13 +34,16 @@ Item {
chop: root.chop
fill: Theme.surface
fillOpacity: 0.97
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5)
// Urgency lives in the slash, and in the frame only when critical. A
// tone-tinted frame on every card drew a pale outline round ordinary
// notifications, and the critical halftone was painted in the accent,
// so the most urgent card wore the colour that means "live".
border: root.notif?.urgency === NotificationUrgency.Critical
? root.tone
: (hover.hovered ? Theme.treatmentHoverMark : Theme.surfaceAlt)
slash: true
slashColor: root.tone
slashWidth: 4
halftone: root.notif?.urgency === NotificationUrgency.Critical
halftoneColor: Theme.accent
halftoneOpacity: 0.12
padding: Theme.padM
Column {
@@ -66,7 +69,7 @@ Item {
text: (root.notif?.appName || "System").toUpperCase()
color: root.tone
font.family: Theme.fontMono
font.pixelSize: 8
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
@@ -126,18 +129,16 @@ Item {
Skew {
anchors.fill: parent
color: actionHover.hovered
? Theme.alpha(root.tone, 0.3)
: Theme.alpha(root.tone, 0.1)
color: actionHover.hovered ? Theme.accent : Theme.surfaceAlt
borderWidth: 1
borderColor: Theme.alpha(root.tone, 0.6)
borderColor: actionHover.hovered ? Theme.accent : Theme.overlay
}
Text {
id: actionLabel
anchors.centerIn: parent
text: actionChip.modelData.text
color: Theme.text
color: actionHover.hovered ? Theme.ink : Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMicro
font.italic: true
+4 -2
View File
@@ -3,7 +3,7 @@ import QtQuick.Shapes
import "root:/config"
import "root:/components"
// The shared ground for the two full-screen surfaces: a crimson wedge leaning
// The shared ground for the two full-screen surfaces: an accent wedge leaning
// at the house angle, a tiled stripe field that sweeps in from the left, and a
// halftone dot wash over the top.
//
@@ -33,7 +33,9 @@ Item {
property real wedgeSpan: 0.34
property bool stripes: true
property real stripeOpacity: 0.10
// Tuned down from 0.10 for Mocha. Crust stripes on the blue-grey base read
// far heavier than they did on black, and buzzed against the text.
property real stripeOpacity: 0.07
property bool halftone: true
property real halftoneOpacity: 0.05
+23 -8
View File
@@ -21,6 +21,12 @@ FocusScope {
readonly property bool focusedOrSelected: root.activeFocus || root.selected
readonly property bool highlighted: root.hovered || root.focusedOrSelected
// Keyboard focus and roving selection invert the slab to solid accent.
// Pointer hover stays a keyline change, so the two never look alike and a
// mouse user never sees the "selected" treatment by accident. Destructive
// buttons keep their own mauve inversion for both.
readonly property bool inverted: root.enabled && !root.destructive && root.focusedOrSelected
// Pointer activation must not turn a transient action into the black/white
// keyboard-focus treatment. Tab still reaches it, and programmatic focus
// (power-menu roving selection / pinned popouts) still works.
@@ -46,23 +52,26 @@ FocusScope {
padding: Theme.padS
fill: {
if (!root.enabled) return Theme.treatmentDisabledFill;
if (root.destructive) return Theme.treatmentDestructiveFill;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveFill
: Theme.treatmentDestructiveFill;
if (root.inverted) return Theme.treatmentFocusFill;
if (root.highlighted) return Theme.treatmentHoverFill;
return Theme.treatmentRestFill;
}
fillOpacity: root.enabled ? 1.0 : 0.72
border: {
if (!root.enabled) return Theme.treatmentDisabledMark;
if (root.destructive) return Theme.treatmentFocusMark;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveMark
: Theme.treatmentDestructiveMark;
if (root.inverted) return Theme.treatmentFocusMark;
if (root.highlighted) return Theme.treatmentHoverMark;
return Theme.alpha(root.accent, 0.55);
}
borderWidth: Theme.stroke
slash: !root.highlighted
slashColor: root.destructive ? Theme.treatmentDestructiveMark : root.accent
halftone: root.enabled && root.highlighted
halftoneColor: Theme.ink
halftoneOpacity: 0.14
slashColor: root.destructive ? Theme.alarm : root.accent
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
@@ -81,7 +90,10 @@ FocusScope {
font.pixelSize: Theme.fsTitle
color: {
if (!root.enabled) return Theme.treatmentDisabledText;
if (root.destructive) return Theme.treatmentDestructiveText;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText;
return root.accent;
}
@@ -92,7 +104,10 @@ FocusScope {
text: root.text
color: {
if (!root.enabled) return Theme.treatmentDisabledText;
if (root.destructive) return Theme.treatmentDestructiveText;
if (root.destructive)
return root.highlighted ? Theme.treatmentDestructiveActiveText
: Theme.treatmentDestructiveText;
if (root.inverted) return Theme.treatmentFocusText;
if (root.highlighted) return Theme.treatmentHoverText;
return Theme.treatmentRestText;
}
+3 -3
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components"
// The base surface for everything in this shell: a leaning, corner-chopped slab
// with an optional halftone wash and a crimson slash along its leading edge.
// with an optional halftone wash and a accent slash along its leading edge.
Item {
id: root
@@ -29,7 +29,7 @@ Item {
property real chop: Theme.cut
property var chopCorners: [1, 3]
// Crimson slash down the leading edge — the Persona 5 tell.
// Tone slash down the leading edge — the Persona 5 tell.
property bool slash: false
property color slashColor: Theme.accent
property real slashWidth: 4
@@ -183,7 +183,7 @@ Item {
//
// Its polygon is built from the panel's raw corners, so on a panel with a
// chopped corner the slash used to run straight through the cut and out the
// far side — a crimson wedge sitting a few pixels outside the silhouette,
// far side — an accent wedge sitting a few pixels outside the silhouette,
// at an angle that matched nothing. It is masked to the outline now, so the
// slash ends exactly where the chamfer does.
Loader {
+5 -2
View File
@@ -31,10 +31,13 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
height: root.activeFocus ? 10 : 6
// Focus widens the rail and rings it in the accent rather than filling
// it: the level fill is often the accent itself, and a solid accent
// rail swallowed it.
color: root.activeFocus
? Theme.treatmentFocusFill
? Theme.surfaceAlt
: Theme.alpha(root.enabled ? Theme.outline : Theme.treatmentDisabledMark, 0.7)
borderColor: root.activeFocus ? Theme.treatmentFocusMark : "transparent"
borderColor: root.activeFocus ? Theme.accent : "transparent"
borderWidth: root.activeFocus ? Theme.stroke : 0
Behavior on height {
+35
View File
@@ -0,0 +1,35 @@
import QtQuick
import "root:/config"
// Section caption for popouts: a spaced mono label running into a hairline.
// Every panel used to label its lists differently — a bare caption here, a
// caption and rule there — which is part of why no two popouts looked related.
Item {
id: root
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: label
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: label.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.5)
}
}
+14
View File
@@ -31,6 +31,20 @@ Shape {
// rail slab.
property bool vertical: false
// NOTE ON SIZING. The lean shifts one edge by `lean * height` (or by
// `lean * width` when vertical), and the piece that actually gets painted is
// what is left: a Skew declared 1px wide and 20px tall does not draw a 1px
// rule at an angle, it draws a rule |1 - 20 * 0.249| = 4px thick that hangs
// 4px to the *left* of the box it was given. That is why the bar's and the
// rail's hairlines came out fat and off-centre. A thin leaning rule has to be
// declared with room for its own lean — `height * Theme.skew + weight` — and
// the call sites that draw one now do exactly that.
//
// Zero-sized pieces still have a lean offset, so a collapsed hover wipe or a
// meter at 0 drew a sub-pixel wedge of pure accent — the stray red dot under
// the metrics. Nothing with no extent should paint at all.
visible: root.width > 0 && root.height > 0
preferredRendererType: Shape.CurveRenderer
asynchronous: false
+55 -44
View File
@@ -1,8 +1,14 @@
import QtQuick
import "root:/config"
// Display text with the wallpaper's RGB split baked in: a crimson and a cyan
// ghost sitting a pixel off the white body. Drives most headings in the shell.
// Display text, screen-printed: the body sits on a hard crust copy knocked a
// pixel and a half down-right — the same offset `P5Panel` puts behind every
// slab. Drives most headings in the shell.
//
// This used to carry a permanent RGB split, a blue and a mauve ghost either
// side of the body. On crimson-and-black that read as misregistered print; on
// Mocha's pastels it read as blur, and it turned every heading in the shell
// soft. The two ghosts still exist, but only for the moment `glitch()` runs.
Item {
id: root
@@ -12,23 +18,34 @@ Item {
property string family: Theme.fontDisplay
property bool italic: true
property color color: Theme.text
property real split: 1.6
property real splitOpacity: 0.85
property int elide: Text.ElideNone
property int horizontalAlignment: Text.AlignLeft
property real letterSpacing: 0.5
// Print shadow. Scales gently with size so a 34px heading does not sit on
// the same hairline offset as an 11px label.
property real shadowOffset: Math.max(1, Math.round(root.pixelSize / 10))
property color shadowColor: Theme.ink
// Ghost offset and resting strength. Resting strength is zero, so the
// ghosts are invisible until `glitch()` kicks them out. A caller that sets
// `splitOpacity` is asking for the old permanent split and still gets it.
property real split: 1.6
property real splitOpacity: 0
// 0..1, driven only by `glitch()`. Kept apart from `split` and
// `splitOpacity` so the animation never has to remember a caller's values.
property real kick: 0
readonly property real ghostOffset: root.split + root.kick * 4
readonly property real ghostOpacity: Math.max(root.splitOpacity, root.kick * 0.85)
implicitWidth: body.implicitWidth
implicitHeight: body.implicitHeight
Text {
id: ghostRed
component Layer: Text {
anchors.fill: parent
anchors.leftMargin: -root.split
anchors.topMargin: root.split * 0.4
text: root.text
color: Theme.splitRed
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
@@ -40,39 +57,33 @@ Item {
renderType: Text.NativeRendering
}
Text {
id: ghostCyan
anchors.fill: parent
anchors.leftMargin: root.split
anchors.topMargin: -root.split * 0.4
text: root.text
color: Theme.splitCyan
opacity: root.splitOpacity
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
Layer {
anchors.leftMargin: root.shadowOffset
anchors.rightMargin: -root.shadowOffset
anchors.topMargin: root.shadowOffset
anchors.bottomMargin: -root.shadowOffset
color: root.shadowColor
}
Text {
Layer {
visible: root.ghostOpacity > 0.01
anchors.leftMargin: -root.ghostOffset
anchors.topMargin: root.ghostOffset * 0.4
color: Theme.accent
opacity: root.ghostOpacity
}
Layer {
visible: root.ghostOpacity > 0.01
anchors.leftMargin: root.ghostOffset
anchors.topMargin: -root.ghostOffset * 0.4
color: Theme.alarm
opacity: root.ghostOpacity
}
Layer {
id: body
anchors.fill: parent
text: root.text
color: root.color
elide: root.elide
horizontalAlignment: root.horizontalAlignment
verticalAlignment: Text.AlignVCenter
font.family: root.family
font.pixelSize: root.pixelSize
font.weight: root.weight
font.italic: root.italic
font.letterSpacing: root.letterSpacing
renderType: Text.NativeRendering
}
// Kick the ghosts outward briefly — used on track changes, mode flips, etc.
@@ -83,14 +94,14 @@ Item {
SequentialAnimation {
id: glitchAnim
NumberAnimation {
target: root; property: "split"
to: 5.5; duration: Theme.durFast
target: root; property: "kick"
to: 1; duration: Theme.durFast
easing.type: Easing.OutExpo
}
NumberAnimation {
target: root; property: "split"
to: 1.6; duration: Theme.durSlow
easing.type: Easing.OutBack
target: root; property: "kick"
to: 0; duration: Theme.durSlow
easing.type: Easing.OutCubic
}
}
}
+8 -2
View File
@@ -1,11 +1,17 @@
import QtQuick
import QtQuick.Effects
// Layer effect that recolours whatever it is applied to — used to tint the
// white texture tiles without shipping a copy per colour.
// Layer effect that recolours whatever it is applied to into a flat silhouette
// of `tintColor` — used for the white texture tiles and for tray icons.
//
// Colorization alone keeps the source's lightness, so a dark icon came out as a
// dark shade of the tint and all but vanished against the rail. Brightness runs
// first and lifts every pixel to white, which leaves colorization nothing to
// preserve but the alpha.
MultiEffect {
property color tintColor: "white"
brightness: 1.0
colorization: 1.0
colorizationColor: tintColor
}
+89 -65
View File
@@ -3,61 +3,73 @@ pragma Singleton
import QtQuick
import Quickshell
// Takemi palette: crimson, black, white. Nothing else.
// Mocha palette: Catppuccin Mocha ground, blue as the accent, mauve as the
// alarm. Persona geometry, softer colour.
//
// White does the work: text, icons, idle indicators. Crimson is the only accent
// and it always means the same thing — active, or wrong. Black is structure,
// used as a hard keyline rather than as a shadow. There are no gradients in
// this shell; every fill is flat.
// The Takemi branch ran monochrome-plus-crimson, where one hue carried both
// "active" and "wrong". Mocha has room for two: blue is every resting accent —
// focus, selection, the thing you are pointing at — and mauve is reserved for
// the things that bite, so destructive and error still read at a glance
// without a third colour entering the shell.
//
// The wallpaper is teal. The UI used to be teal too, and the two fought each
// other into a murky blue-grey. Letting the UI go monochrome-plus-red is what
// lets the wallpaper read as the colour in the composition.
// Mauve rather than Mocha's pink: pink is a pastel, and a slab of it over this
// ground goes chalky. Mauve is saturated enough to hold an edge against the
// near-black surfaces the Persona layout is built from.
//
// Everything else is Mocha's own neutral ramp. Fills stay flat and keylines
// stay hard; only the hues moved.
Singleton {
id: root
// ---------------------------------------------------------------- colors
readonly property color ink: "#000000"
// Crust, not pure black: it is the darkest thing in Mocha, so it keeps its
// job as the hard keyline and as text punched out of an accent fill.
readonly property color ink: "#11111b"
readonly property color base: "#0a0a0c"
readonly property color mantle: "#0d0d10"
readonly property color surface: "#141418"
readonly property color surfaceAlt: "#1c1c22"
readonly property color overlay: "#26262e"
readonly property color outline: "#3a3a44"
readonly property color base: "#1e1e2e"
readonly property color mantle: "#181825"
readonly property color surface: "#313244"
readonly property color surfaceAlt: "#45475a"
readonly property color overlay: "#585b70"
readonly property color outline: "#6c7086"
readonly property color text: "#ffffff"
readonly property color subtext: "#c8c8ce"
readonly property color muted: "#6b6b70"
readonly property color text: "#cdd6f4"
readonly property color subtext: "#a6adc8"
readonly property color muted: "#7f849c"
// ------------------------------------------------------------ THE KNOB
// The one colour in this shell. Change this line and the entire accent
// recolours — everything below is derived from it, and nothing else in the
// palette carries hue at all.
// ------------------------------------------------------------ THE KNOBS
// Two colours, two meanings, and nothing else in the shell carries hue.
//
// Keep it in step with `$accent` at the top of
// ~/.config/hypr/modules/takemi.conf, which is what colours the Hyprland
// window borders. Those are the only two places the colour is written.
readonly property color accent: "#ff2d40"
// `accent` is blue: anything live, focused or selected. `alarm` is mauve:
// anything destructive, failed or critical. Things that are merely off —
// muted, disabled, do-not-disturb — carry no hue at all; see the `Off`
// treatment below. Swap either line and everything derived
// from it recolours.
//
// Keep them in step with `c.accent` / `c.alarm` in
// ~/.config/hypr/lua/palette.lua, which is what colours the Hyprland
// window borders. Those are the only places the colours are written.
readonly property color accent: "#89b4fa"
readonly property color alarm: "#cba6f7"
readonly property color accentDim: Qt.darker(root.accent, 1.9)
readonly property color accentSoft: Qt.lighter(root.accent, 1.35)
readonly property color accentDim: "#5a7dc0"
readonly property color accentSoft: "#b4befe"
// `primary` is the resting voice of the UI, which is now simply white.
// Kept under the old name so the existing call sites don't all have to move.
readonly property color primary: root.text
readonly property color primaryDim: "#9a9aa2"
readonly property color primaryDim: "#9399b2"
readonly property color deep: root.surfaceAlt
// `glow` is the hover highlight. It used to be a brighter teal, which now
// that everything rests at white would have made hover invisible — so it
// resolves to crimson. Pointing at a thing lights it up red.
// `glow` is the hover highlight. Pointing at a thing lights it up blue —
// the same signal as focus, because it is the same statement about where
// your attention is.
readonly property color glow: root.accent
// ------------------------------------------------ semantic treatments
// These tokens describe interaction meaning, not new palette entries. Each
// state is composed only from Takemi's black, white, crimson, and neutral
// surfaces so callers do not invent one-off state colors.
// state is composed only from crust, the neutral ramp, blue and mauve, so
// callers do not invent one-off state colours.
readonly property color treatmentRestFill: root.surface
readonly property color treatmentRestMark: root.outline
readonly property color treatmentRestText: root.text
@@ -66,7 +78,11 @@ Singleton {
readonly property color treatmentHoverMark: root.accent
readonly property color treatmentHoverText: root.text
readonly property color treatmentFocusFill: root.text
// Focus and selection are an inverted slab: solid blue with crust punched
// out of it (9:1). This used to be a white fill, which was right against
// Takemi's black but glares as a lavender-white block on Mocha — and it
// made "selected" a third look that was neither hover nor accent.
readonly property color treatmentFocusFill: root.accent
readonly property color treatmentFocusMark: root.ink
readonly property color treatmentFocusText: root.ink
readonly property color treatmentSelectedFill: root.treatmentFocusFill
@@ -77,38 +93,38 @@ Singleton {
readonly property color treatmentActiveMark: root.accent
readonly property color treatmentActiveText: root.text
// Crimson carries destructive/error meaning. Text and keylines stay black:
// black-on-crimson is 5.70:1, while small white-on-crimson is only 3.68:1.
readonly property color treatmentDestructiveFill: root.accent
readonly property color treatmentDestructiveMark: root.ink
readonly property color treatmentDestructiveText: root.ink
readonly property color treatmentErrorFill: root.treatmentDestructiveFill
readonly property color treatmentErrorMark: root.treatmentDestructiveMark
readonly property color treatmentErrorText: root.treatmentDestructiveText
// Mauve carries destructive/error meaning — never blue, so a dangerous
// button can never be mistaken for a merely focused one.
//
// At rest it is a keyline and a label, not a fill. The solid mauve is held
// back for the moment the pointer or the selection is actually on the
// thing — then it lands hard, with crust punched out of it at 9.1:1.
readonly property color treatmentDestructiveFill: root.surface
readonly property color treatmentDestructiveMark: root.alarm
readonly property color treatmentDestructiveText: root.alarm
readonly property color treatmentDestructiveActiveFill: root.alarm
readonly property color treatmentDestructiveActiveMark: root.ink
readonly property color treatmentDestructiveActiveText: root.ink
readonly property color treatmentErrorFill: root.treatmentDestructiveActiveFill
readonly property color treatmentErrorMark: root.treatmentDestructiveActiveMark
readonly property color treatmentErrorText: root.treatmentDestructiveActiveText
readonly property color treatmentUrgentFill: root.surface
readonly property color treatmentUrgentMark: root.accent
readonly property color treatmentUrgentMark: root.alarm
readonly property color treatmentUrgentText: root.text
readonly property color treatmentDisabledFill: root.surface
readonly property color treatmentDisabledMark: root.outline
readonly property color treatmentDisabledText: root.muted
// Retired hues. Aliases only — do not introduce new uses.
//
// `warn` used to be accentSoft, which put a washed-out pink-red on screen
// wherever something was merely notable. That reads as a faded accent, not
// as a decision. There is no half-crimson in this palette: a thing is wrong
// (accent) or it is not (text). `warn` now resolves to the real accent, so
// low battery shouts properly and everything merely informational is white.
readonly property color blue: root.primaryDim
readonly property color ok: root.text
readonly property color warn: root.accent
// Off is a state the user chose — muted, do-not-disturb, radio switched
// off. It is neither live nor wrong, so it gets no hue: painting a muted
// speaker blue said "active", and painting it mauve would say "broken".
readonly property color treatmentOffFill: root.surface
readonly property color treatmentOffMark: root.overlay
readonly property color treatmentOffText: root.muted
// Display text carries a crimson and a black offset rather than an RGB
// split. Same trick, correct palette.
readonly property color splitRed: root.accent
readonly property color splitCyan: root.ink
function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a);
@@ -122,11 +138,15 @@ Singleton {
a.a + (b.a - a.a) * k);
}
// Load readouts sit white until the number actually matters, then bleed to
// crimson. Not a hue cycle — red always means the same thing.
// Load readouts sit at text until the number actually matters, then climb
// through the accent before they reach the alarm. Going straight from text
// to mauve passes through a washed-out mid-tone that reads as a faded red
// rather than as a rising number; routing through blue keeps every step of
// the ramp a colour the shell already uses.
function heat(pct: real): color {
if (pct <= 55) return root.text;
return root.mix(root.text, root.accent, (pct - 55) / 45);
if (pct <= 45) return root.text;
if (pct <= 75) return root.mix(root.text, root.accent, (pct - 45) / 30);
return root.mix(root.accent, root.alarm, (pct - 75) / 25);
}
// ----------------------------------------------------------------- fonts
@@ -137,7 +157,9 @@ Singleton {
readonly property int weightDisplay: Font.Black
readonly property int weightBody: Font.Medium
readonly property int fsMicro: 9
// Nothing in the shell renders below this. 9px mono was legible on
// crimson-on-black; on Mocha's lower-contrast ramp it was not.
readonly property int fsMicro: 10
readonly property int fsSmall: 11
readonly property int fsBody: 12
readonly property int fsLarge: 15
@@ -145,6 +167,8 @@ Singleton {
readonly property int fsHuge: 34
readonly property int fsMega: 92
readonly property int fsRailIcon: 17
// -------------------------------------------------------------- geometry
readonly property int barHeight: 40
readonly property int barMargin: 8
@@ -163,7 +187,7 @@ Singleton {
readonly property int cut: 10
readonly property int stroke: 2
// Pure-black keyline and offset copy behind panels. This is the one piece of
// Crust keyline and offset copy behind panels. This is the one piece of
// layering the style needs; it is not a soft shadow and never blurs.
readonly property real dropOffset: 3
readonly property real keylineWidth: 2
@@ -196,5 +220,5 @@ Singleton {
// which asks hyprpaper what is actually on screen — a path written down
// here goes stale the moment the wallpaper is switched, which is exactly
// what happened to the lock screen.
readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/takemi_1.jpg"
readonly property url fallbackWallpaper: "file:///home/narl/.wallpapers/arch.png"
}
+549
View File
@@ -0,0 +1,549 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import "root:/config"
import "root:/components"
import "root:/popouts"
import "root:/services"
// The launcher. One input, one list, and a small amount of grammar.
//
// 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,
// which hands h/j/k/l over to navigation. That is the compromise the vim keys
// need: a launcher whose input eats `j` is a launcher 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
// into "open this with…", already sorted so the system default is row zero —
// so the fast path is type, Enter, done, and the slow path is one arrow key.
// Tab pins whatever is selected, which does the same thing for arguments that
// do not announce themselves as arguments.
PanelWindow {
id: root
required property var 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
&& Actions.launcherScreen === modelData.name
visible: root.open || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-launcher"
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
// ----------------------------------------------------------------- state
property string query: ""
property var pinned: null // an index item, frozen as the verb
property bool selectMode: false
property int selected: 0
readonly property int maxRows: 8
readonly property int rowHeight: 46
// --------------------------------------------------------------- grammar
readonly property string prefix: root.pinned ? "" : root.query.slice(0, 1)
// A dmenu request owns the window outright — there is no app search to
// fall back to, because the caller is waiting on a choice from its own
// list.
readonly property string mode: Apps.dmenuActive ? "dmenu"
: root.prefix === ">" ? "shell"
: root.prefix === "=" ? "calc"
: root.prefix === ":" ? "clip"
: "app"
readonly property bool prefixed: root.mode === "shell"
|| root.mode === "calc" || root.mode === "clip"
readonly property string body: root.prefixed
? root.query.slice(1)
: root.query
// With an app pinned, everything you type is the argument. Without one, an
// argument only exists if what you typed is self-evidently an object.
readonly property string argKind: root.pinned
? (Apps.classify(root.body) || "text")
: Apps.classify(root.body)
readonly property string arg: root.mode !== "app" || root.body.trim() === ""
? ""
: (root.pinned || root.argKind !== "")
? Apps.normalizeArg(root.body, root.argKind)
: ""
// What is left to match app names against. When the whole query has been
// claimed as an argument there is nothing left, and the list falls back to
// "every app, best handler first".
readonly property string term: root.mode !== "app" || root.pinned || root.arg !== ""
? ""
: root.body
readonly property var results: {
if (!root.open) return [];
if (root.mode === "calc") {
const v = Apps.calc(root.body);
if (v === "") return [];
return [{ kind: "calc", name: v, sub: "copy to clipboard",
icon: "accessories-calculator", value: v }];
}
if (root.mode === "dmenu") return Apps.searchDmenu(root.body);
if (root.mode === "clip") return Apps.searchClips(root.body);
if (root.mode === "shell") {
if (root.body.trim() === "") return [];
return [
{ kind: "shell", name: root.body, sub: "run", icon: "utilities-terminal" },
{ kind: "shellterm", name: root.body, sub: "run in a terminal",
icon: "utilities-terminal" }
];
}
// Pinned: the verb is decided, so the list narrows to that app and the
// things it can do — which is where "Brave, but a private window"
// lives. Same list, one step further in.
if (root.pinned) {
const id = root.pinned.id;
return Apps.index.filter(e => e.id === id)
.sort((a, b) => (a.kind === "app" ? 0 : 1) - (b.kind === "app" ? 0 : 1));
}
return Apps.search(root.term, root.arg, root.argKind);
}
onResultsChanged: root.selected = 0
// Typing the ":" prefix is the other way into the clipboard, and the
// history may have moved on since the window opened.
onModeChanged: if (root.mode === "clip") Apps.refreshClips()
onOpenChanged: {
if (root.open) {
PopupCoordinator.dismissAll();
root.reset();
openAnim.restart();
input.forceActiveFocus();
input.cursorPosition = input.text.length;
Apps.refreshClips();
} else {
// Dismissing a dmenu is a valid answer — an empty one. Without this
// the caller blocks until its own timeout.
Apps.finishDmenu("");
closeAnim.restart();
}
}
function reset(): void {
root.query = Actions.launcherSeed;
root.pinned = null;
root.selectMode = false;
root.selected = 0;
}
function move(delta: int): void {
const n = root.results.length;
if (n === 0) return;
root.selected = (root.selected + delta + n) % n;
list.positionViewAtIndex(root.selected, ListView.Contain);
}
function pin(): void {
const item = root.results[root.selected];
if (!item || (item.kind !== "app" && item.kind !== "action")) return;
root.pinned = item;
root.query = "";
root.selectMode = false;
input.forceActiveFocus();
}
function unpin(): void {
if (!root.pinned) return;
root.query = root.pinned.name.split(" · ")[0];
root.pinned = null;
input.forceActiveFocus();
}
function activate(): void {
const item = root.results[root.selected];
if (!item) return;
switch (item.kind) {
case "dmenu": Apps.finishDmenu(item.name); break;
case "clip": Apps.pasteClip(item); break;
case "calc": Apps.copy(item.value); break;
case "shell": Apps.runShell(root.body, false); break;
case "shellterm": Apps.runShell(root.body, true); break;
default: Apps.launch(item, root.arg); break;
}
Actions.closeLauncher();
}
// -------------------------------------------------------------- backdrop
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.72)
opacity: 0
TapHandler { onTapped: Actions.closeLauncher() }
}
// ----------------------------------------------------------------- panel
Item {
id: shell
width: 680
height: header.height + divider.height + listBox.height + hint.height
anchors.horizontalCenter: parent.horizontalCenter
// Sits above centre. A launcher pinned to the exact middle of a 1440p
// screen puts the list below the eyeline and the input above it; the
// input is the thing you are looking at.
y: parent.height * 0.22
opacity: 0
transform: Translate { id: rise; y: 24 }
PopoutSurface {
anchors.fill: parent
tone: root.arg !== "" ? Theme.accent : Theme.primary
padding: 0
slashWidth: root.arg !== "" ? 8 : 5
// ------------------------------------------------------- header
Item {
id: header
width: parent.width
height: 66
anchors.top: parent.top
Row {
anchors.fill: parent
anchors.leftMargin: Theme.padL
anchors.rightMargin: Theme.padL
spacing: Theme.padM
Icon {
anchors.verticalCenter: parent.verticalCenter
text: root.mode === "shell" ? "" : root.mode === "calc" ? "󰃬"
: root.mode === "clip" ? "󰅇"
: root.mode === "dmenu" ? "󰘳"
: root.arg !== "" ? "󰏌" : "󰍉"
color: root.arg !== "" ? Theme.accent : Theme.primary
font.pixelSize: Theme.fsTitle
}
// The pin chip. Once this is on screen the input means
// "argument", and the chip is the only thing saying so.
Item {
anchors.verticalCenter: parent.verticalCenter
visible: root.pinned !== null
width: visible ? chipText.width + Theme.padM * 2 : 0
height: 26
Skew {
anchors.fill: parent
color: Theme.accent
}
Text {
id: chipText
anchors.centerIn: parent
text: root.pinned ? root.pinned.name.toUpperCase() : ""
color: Theme.treatmentDestructiveText
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
}
TextInput {
id: input
anchors.verticalCenter: parent.verticalCenter
width: parent.width - x
text: root.query
onTextChanged: root.query = text
color: root.selectMode ? Theme.muted : Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsLarge
selectionColor: Theme.accent
selectedTextColor: Theme.ink
clip: true
focus: true
renderType: Text.NativeRendering
// Ours must run before TextInput's, or select mode's
// h/j/k/l would be typed into the field before we ever
// saw them.
Keys.priority: Keys.BeforeItem
Keys.onPressed: event => root.handleKey(event)
Text {
anchors.verticalCenter: parent.verticalCenter
visible: input.text === ""
text: root.pinned ? "argument…"
: root.mode === "shell" ? "command…"
: root.mode === "calc" ? "expression…"
: root.mode === "clip" ? "clipboard history…"
: root.mode === "dmenu"
? (Apps.dmenuPrompt || "choose…")
: "search, or paste a link"
color: Theme.muted
font: input.font
renderType: Text.NativeRendering
}
}
}
// Count, right-aligned over the input's tail.
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
text: root.results.length > 0
? (root.selected + 1) + "/" + root.results.length
: ""
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
Skew {
id: divider
anchors.top: header.bottom
width: parent.width
height: Theme.stroke
color: Theme.alpha(Theme.outline, 0.8)
lean: 0
}
// -------------------------------------------------------- list
Item {
id: listBox
anchors.top: divider.bottom
width: parent.width
height: Math.min(root.results.length, root.maxRows) * root.rowHeight
+ (root.results.length > 0 ? Theme.padS * 2 : 0)
clip: true
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
ListView {
id: list
anchors.fill: parent
anchors.topMargin: Theme.padS
anchors.bottomMargin: Theme.padS
anchors.leftMargin: Theme.padM
anchors.rightMargin: Theme.padM
model: root.results
currentIndex: root.selected
boundsBehavior: Flickable.StopAtBounds
reuseItems: true
delegate: ResultRow {
required property var modelData
required property int index
width: list.width
entry: modelData
selected: index === root.selected
badge: root.arg !== "" ? "OPEN ⏎" : "⏎"
onActivated: {
root.selected = index;
root.activate();
}
onPinRequested: {
root.selected = index;
root.pin();
}
}
}
// Empty state. Says which of the two ways to be empty it is,
// because "no results" and "nothing typed" want different
// things from you.
Text {
anchors.centerIn: parent
visible: root.results.length === 0 && root.query !== ""
text: root.mode === "calc" ? "NOT AN EXPRESSION" : "NO MATCH"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 4
renderType: Text.NativeRendering
}
}
// -------------------------------------------------------- hints
Item {
id: hint
anchors.top: listBox.bottom
width: parent.width
height: 30
Text {
anchors.centerIn: parent
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 3
renderType: Text.NativeRendering
text: root.selectMode
? "J K MOVE L PIN H UNPIN i TYPE ESC QUIT"
: root.mode === "dmenu"
? "⏎ CHOOSE ESC SELECT ↑↓ MOVE"
: root.mode === "clip"
? "⏎ COPY ESC SELECT ↑↓ MOVE"
: root.pinned
? "⏎ OPEN ESC SELECT ↑↓ PICK"
: "ESC SELECT TAB PIN > SHELL = CALC : CLIP"
}
}
}
}
// ------------------------------------------------------------------ keys
function handleKey(event: var): void {
if (!root.open) return;
const ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
// Escape is the mode key, and it means what it means in vim: leave
// 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 (!root.selectMode && root.results.length > 0) root.selectMode = true;
else Actions.closeLauncher();
event.accepted = true;
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) {
root.activate();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Tab) {
root.pin();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Down || (ctrl && event.key === Qt.Key_J)
|| (ctrl && event.key === Qt.Key_N)) {
root.move(1);
event.accepted = true;
return;
}
if (event.key === Qt.Key_Up || (ctrl && event.key === Qt.Key_K)
|| (ctrl && event.key === Qt.Key_P)) {
root.move(-1);
event.accepted = true;
return;
}
if (ctrl && event.key === Qt.Key_U) {
root.query = "";
event.accepted = true;
return;
}
if (!root.selectMode) return;
// Select mode. The letters are commands, not text: `i` and `a` go back
// to insert without leaving an `i` behind, and everything unclaimed is
// swallowed rather than typed, so a stray `d` costs nothing at all.
switch (event.key) {
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_L: root.pin(); 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:
root.selected = (event.modifiers & Qt.ShiftModifier)
? root.results.length - 1 : 0;
list.positionViewAtIndex(root.selected, ListView.Contain);
event.accepted = true;
return;
case Qt.Key_Backspace:
root.selectMode = false;
input.forceActiveFocus();
event.accepted = true;
return;
}
// Anything else in select mode is a command nobody bound. Eat it.
if (event.text !== "") event.accepted = true;
}
// ------------------------------------------------------------- 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.durBase
}
NumberAnimation {
target: rise; property: "y"
from: 24; 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: rise; property: "y"
to: 16; duration: Theme.durFast; easing.type: Easing.InQuad
}
}
}
+132
View File
@@ -0,0 +1,132 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import "root:/config"
import "root:/components"
// One line in the result list. Rest is bare — no fill, no border — so a list of
// forty apps reads as a column of text rather than forty boxes. Only the
// selected row gets a slab, and it takes the full inverted treatment: white
// fill, black text. That is the strongest contrast the palette has, which is
// what the row you are about to launch should be wearing.
Item {
id: root
required property var entry
required property bool selected
property bool hovered: false
property string badge: ""
signal activated
signal pinRequested
implicitHeight: 46
// Selection slab. Leans like everything else; drawn as a Skew so the
// diagonals get analytic antialiasing instead of a pixel staircase.
Skew {
anchors.fill: parent
anchors.leftMargin: -Theme.padS
anchors.rightMargin: -Theme.padS
color: root.selected ? Theme.treatmentSelectedFill : "transparent"
opacity: root.selected ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutQuad }
}
}
// Hover is an accent tick in the gutter rather than a second slab, so
// pointing at a row cannot be mistaken for selecting it.
Skew {
anchors.verticalCenter: parent.verticalCenter
x: -Theme.padS
width: 3
height: root.hovered && !root.selected ? parent.height - 10 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
}
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.padM
anchors.rightMargin: Theme.padM
spacing: Theme.padM
IconImage {
anchors.verticalCenter: parent.verticalCenter
implicitSize: 26
asynchronous: true
source: root.entry.icon ? Quickshell.iconPath(root.entry.icon, true) : ""
visible: source !== ""
}
// Entries with no icon still need to occupy the same gutter, or their
// labels wander left and the column stops being a column.
Icon {
anchors.verticalCenter: parent.verticalCenter
width: 26
text: "󰣆"
color: root.selected ? Theme.treatmentSelectedText : Theme.muted
visible: !root.entry.icon && root.entry.kind !== "dmenu"
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 26 - Theme.padM * 2 - trailing.width
spacing: -1
Text {
width: parent.width
text: root.entry.name
elide: Text.ElideRight
color: root.selected ? Theme.treatmentSelectedText : Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsBody
font.weight: root.selected ? Font.Bold : Theme.weightBody
renderType: Text.NativeRendering
}
Text {
width: parent.width
text: root.entry.sub
visible: text !== ""
elide: Text.ElideRight
color: root.selected ? Theme.alpha(Theme.ink, 0.6) : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
// Trailing hint: the key that fires this row. Only the selected row shows
// one, because a column of ⏎ glyphs tells you nothing.
Text {
id: trailing
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Theme.padM
text: root.selected ? root.badge : ""
color: Theme.alpha(Theme.ink, 0.55)
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
HoverHandler {
onHoveredChanged: root.hovered = hovered
}
TapHandler {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onTapped: (point, button) => {
if (button === Qt.RightButton) root.pinRequested();
else root.activated();
}
}
}
+5 -5
View File
@@ -68,7 +68,7 @@ Item {
// The same wedge-and-stripes ground the power menu stands on. The wedge
// sits under the clock, which is where the power menu puts it under the
// title — so the two screens land the crimson mass in the same place.
// title — so the two screens land the accent mass in the same place.
P5Backdrop {
id: backdrop
anchors.fill: parent
@@ -199,7 +199,7 @@ Item {
text: surface.failed
? (surface.status || "WRONG").toUpperCase()
: (surface.busy ? "CHECKING…" : "WHO GOES THERE")
color: surface.failed ? Theme.accent : Theme.text
color: surface.failed ? Theme.alarm : Theme.text
pixelSize: Theme.fsTitle
letterSpacing: 4
horizontalAlignment: Text.AlignHCenter
@@ -223,10 +223,10 @@ Item {
: (input.activeFocus ? Theme.primary : Theme.outline)
borderWidth: 2
slash: true
slashColor: surface.failed ? Theme.accent : Theme.primary
slashColor: surface.failed ? Theme.alarm : Theme.primary
slashWidth: 6
halftone: true
halftoneColor: surface.failed ? Theme.accent : Theme.primary
halftoneColor: surface.failed ? Theme.alarm : Theme.primary
halftoneOpacity: 0.08
Row {
@@ -241,7 +241,7 @@ Item {
required property int index
width: 10
height: 20
color: surface.failed ? Theme.accent : Theme.primary
color: surface.failed ? Theme.alarm : Theme.primary
// Each new block snaps in.
Component.onCompleted: pop.start()
+7 -11
View File
@@ -66,10 +66,12 @@ PanelWindow {
readonly property color tone: {
switch (root.kind) {
case "mic": return Audio.micMuted ? Theme.accent : Theme.blue;
// Turning the screen up is not a warning. White, like volume.
case "brightness": return Theme.text;
default: return Audio.muted ? Theme.accent : Theme.primary;
// One tone for every level: the accent while the channel is live, the
// off grey while it is muted. Muted used to be the accent too, so a
// silenced speaker flashed the same colour as a live one.
case "mic": return Audio.micMuted ? Theme.treatmentOffText : Theme.accent;
case "brightness": return Theme.accent;
default: return Audio.muted ? Theme.treatmentOffText : Theme.accent;
}
}
@@ -104,15 +106,11 @@ PanelWindow {
anchors.margins: 4
fill: Theme.mantle
fillOpacity: 0.97
border: Theme.alpha(root.tone, 0.85)
border: Theme.surfaceAlt
borderWidth: Theme.stroke
slash: true
slashColor: root.tone
slashWidth: 5
halftone: true
halftoneColor: root.tone
halftoneOpacity: 0.08
sheen: true
lean: 0.09
chop: 14
padding: Theme.padM
@@ -180,8 +178,6 @@ PanelWindow {
// widening readout only moves its own left edge.
text: Math.round(root.level * 100) + "%"
pixelSize: Theme.fsBody
split: 1
splitOpacity: 0.5
}
}
+10 -6
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components"
import "root:/services"
// Full-screen power menu. Crimson stripes sweep in behind a stack of leaning
// Full-screen power menu. An accent wedge and stripes sweep in behind a stack of leaning
// buttons; Escape or a click on the backdrop dismisses it.
PanelWindow {
id: root
@@ -13,11 +13,16 @@ PanelWindow {
required property var modelData
screen: modelData
visible: Actions.powerMenuOpen || closeAnim.running
// Only the screen that had focus when the menu was asked for shows it; the
// other copies stay dormant so the keyboard has one place to land.
readonly property bool open: Actions.powerMenuOpen
&& Actions.powerMenuScreen === modelData.name
visible: root.open || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-power"
WlrLayershell.keyboardFocus: Actions.powerMenuOpen
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
@@ -25,7 +30,6 @@ PanelWindow {
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property bool open: Actions.powerMenuOpen
property int selectedAction: 0
function focusSelectedAction(): void {
@@ -63,7 +67,7 @@ PanelWindow {
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.82)
color: Theme.alpha(Theme.base, 0.9)
opacity: 0
TapHandler {
@@ -178,7 +182,7 @@ PanelWindow {
icon: entry.modelData.icon
destructive: entry.modelData.danger
selected: root.selectedAction === entry.index
accent: entry.modelData.danger ? Theme.accent : Theme.primary
accent: entry.modelData.danger ? Theme.alarm : Theme.primary
lean: Theme.skew
lunge: -10
onClicked: root.invoke(entry.modelData.act)
+5 -6
View File
@@ -13,9 +13,9 @@ PanelWindow {
required property var modelData
screen: modelData
// Only the primary bar screen shows toasts, to avoid duplicates.
readonly property bool primaryScreen: Quickshell.screens.length === 0
|| Quickshell.screens[0] === modelData
// A toast window exists on every screen, but each notification lands only
// on the monitor that had focus when it arrived. Latched per toast rather
// than live, so a card does not hop screens while it is being read.
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-toasts"
@@ -31,7 +31,7 @@ PanelWindow {
implicitHeight: Math.max(1, stack.implicitHeight)
color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: primaryScreen && stack.children.length > 0
visible: root.live.length > 0
// Toasts currently on screen, newest first.
property list<Notification> live: []
@@ -39,9 +39,8 @@ PanelWindow {
Connections {
target: Notifs
enabled: root.primaryScreen
function onPopupRequested(notif) {
if (Actions.focusedScreenName() !== root.modelData.name) return;
root.live = [notif, ...root.live].slice(0, root.maxLive);
}
}
+2 -2
View File
@@ -19,7 +19,7 @@ Item {
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Audio.micIcon
color: Audio.micMuted ? Theme.accent : Theme.text
color: Audio.micMuted ? Theme.treatmentOffText : Theme.text
font.pixelSize: Theme.fsLarge
}
@@ -42,7 +42,7 @@ Item {
Text {
text: Audio.micMuted ? "MUTED" : Audio.micPercent + "%"
color: Audio.micMuted ? Theme.accent : Theme.subtext
color: Audio.micMuted ? Theme.treatmentOffText : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
+1 -1
View File
@@ -51,7 +51,7 @@ Item {
Text {
text: Audio.muted ? "MUTED" : "LIVE"
color: Audio.muted ? Theme.accent : Theme.text
color: Audio.muted ? Theme.treatmentOffText : Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more