updated theme

This commit is contained in:
2026-08-11 01:53:26 +02:00
parent 0a439d951e
commit 105cf6b3a8
31 changed files with 1218 additions and 976 deletions
+9 -5
View File
@@ -12,7 +12,10 @@ WINIT_X11_SCALE_FACTOR = "1.0"
position = "None" position = "None"
dynamic_padding = true dynamic_padding = true
decorations = "None" decorations = "None"
opacity = 0.9
## Fully opaque, no blur. Persona surfaces are flat ink on flat ground —
## glass and translucency read as a different design language entirely.
opacity = 1.0
blur = false blur = false
startup_mode = "Windowed" startup_mode = "Windowed"
dynamic_title = true dynamic_title = true
@@ -24,10 +27,11 @@ decorations_theme_variant = "None"
columns = 82 columns = 82
lines = 24 lines = 24
## Blank space added around the window in pixels. ## Blank space added around the window in pixels. Roomier than the old 3/0 —
## the text needs to sit inside the crimson border, not against it.
[window.padding] [window.padding]
x = 3 x = 12
y = 0 y = 8
## SCROLLING ------------------------------------------------------ ## SCROLLING ------------------------------------------------------
[scrolling] [scrolling]
@@ -103,7 +107,7 @@ ipc_socket = true
## GENERAL -------------------------------------------------------- ## GENERAL --------------------------------------------------------
## Import additional configuration files. ## Import additional configuration files.
import = ["~/.config/alacritty/colors.toml", "~/.config/alacritty/fonts.toml"] import = ["~/.config/alacritty/takemi.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]] [[keyboard.bindings]]
key = "Return" key = "Return"
+86
View File
@@ -0,0 +1,86 @@
## Takemi — matched to ~/.config/quickshell/config/Theme.qml and
## ~/.config/hypr/modules/takemi.conf.
##
## Near-black ground, white text, crimson as the only saturated colour. The
## sixteen ANSI slots still have to be told apart by syntax highlighters, so the
## non-crimson hues survive — but every one of them is pulled down and
## desaturated so nothing competes with the accent. Nothing here is neon.
[colors.primary]
background = "#0a0a0c"
foreground = "#ffffff"
dim_foreground = "#6b6b70"
bright_foreground = "#ffffff"
## Crimson block cursor, black text punched out of it.
[colors.cursor]
text = "#000000"
cursor = "#ff2d40"
[colors.vi_mode_cursor]
text = "#000000"
cursor = "#ffffff"
## Selection is a solid crimson block — the same "active" signal the shell uses.
[colors.selection]
text = "#000000"
background = "#ff2d40"
[colors.search.matches]
foreground = "#000000"
background = "#c8c8ce"
[colors.search.focused_match]
foreground = "#000000"
background = "#ff2d40"
[colors.footer_bar]
foreground = "#000000"
background = "#c8c8ce"
[colors.hints.start]
foreground = "#000000"
background = "#ff2d40"
[colors.hints.end]
foreground = "#000000"
background = "#c8c8ce"
## Red is the accent, exactly. The rest are muted enough to sit behind it.
[colors.normal]
black = "#14141a"
red = "#ff2d40"
green = "#7f9b6a"
yellow = "#d6a24a"
blue = "#6f7d99"
magenta = "#c0566d"
cyan = "#7f9aa0"
white = "#c8c8ce"
[colors.bright]
black = "#3a3a44"
red = "#ff6b78"
green = "#9bb886"
yellow = "#f0c070"
blue = "#8d9bb8"
magenta = "#e07a8e"
cyan = "#9db8bd"
white = "#ffffff"
[colors.dim]
black = "#0f0f13"
red = "#8f1826"
green = "#5c704d"
yellow = "#9c7635"
blue = "#515c70"
magenta = "#8c3f4f"
cyan = "#5c7075"
white = "#6b6b70"
[[colors.indexed_colors]]
index = 16
color = "#ff6b78"
[[colors.indexed_colors]]
index = 17
color = "#ffffff"
-49
View File
@@ -1,49 +0,0 @@
# This file was created by fish when upgrading to version 4.3, to migrate
# theme variables from universal to global scope.
# Don't edit this file, as it will be written by the web-config tool (`fish_config`).
# To customize your theme, delete this file and see
# help interactive#syntax-highlighting
# or
# man fish-interactive | less +/^SYNTAX.HIGHLIGHTING
# for appropriate commands to add to ~/.config/fish/config.fish instead.
# See also the release notes for fish 4.3.0 (run `help relnotes`).
set --global fish_color_autosuggestion 6c7086
set --global fish_color_cancel f38ba8
set --global fish_color_command 89b4fa
set --global fish_color_comment 7f849c
set --global fish_color_cwd f9e2af
set --global fish_color_cwd_root red
set --global fish_color_end fab387
set --global fish_color_error f38ba8
set --global fish_color_escape eba0ac
set --global fish_color_gray 6c7086
set --global fish_color_history_current --bold
set --global fish_color_host 89b4fa
set --global fish_color_host_remote a6e3a1
set --global fish_color_keyword f38ba8
set --global fish_color_match --background=brblue
set --global fish_color_normal cdd6f4
set --global fish_color_operator f5c2e7
set --global fish_color_option a6e3a1
set --global fish_color_param f2cdcd
set --global fish_color_quote a6e3a1
set --global fish_color_redirection f5c2e7
set --global fish_color_search_match --background=313244
set --global fish_color_selection --background=313244
set --global fish_color_status f38ba8
set --global fish_color_user 94e2d5
set --global fish_color_valid_path --underline
set --global fish_pager_color_background
set --global fish_pager_color_completion cdd6f4
set --global fish_pager_color_description 6c7086
set --global fish_pager_color_prefix f5c2e7
set --global fish_pager_color_progress 6c7086
set --global fish_pager_color_secondary_background
set --global fish_pager_color_secondary_completion
set --global fish_pager_color_secondary_description
set --global fish_pager_color_secondary_prefix
set --global fish_pager_color_selected_background
set --global fish_pager_color_selected_completion
set --global fish_pager_color_selected_description
set --global fish_pager_color_selected_prefix
+50
View File
@@ -0,0 +1,50 @@
# Takemi — matched to ~/.config/quickshell/config/Theme.qml,
# ~/.config/alacritty/takemi.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.
#
# 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_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_pager_color_progress 6b6b70 --background=141418
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
+6 -2
View File
@@ -1,9 +1,13 @@
function fish_prompt --description 'Write out the prompt' function fish_prompt --description 'Write out the prompt'
set -l last_status $status set -l last_status $status
set -l normal (set_color normal) set -l normal (set_color normal)
set -l status_color (set_color brgreen) # White at rest, crimson when the last command failed — the prompt
# arrow is the one place the shell says "something is wrong", so it is
# the one place that earns the accent. Was brgreen/brpurple, which
# spent colour on states that are not worth any.
set -l status_color (set_color ffffff)
set -l cwd_color (set_color $fish_color_cwd) set -l cwd_color (set_color $fish_color_cwd)
set -l vcs_color (set_color brpurple) set -l vcs_color (set_color 6b6b70)
set -l prompt_status "" set -l prompt_status ""
# Since we display the prompt on a new line allow the directory names to be longer. # Since we display the prompt on a new line allow the directory names to be longer.
+52
View File
@@ -0,0 +1,52 @@
# name: 'Takemi'
# url: 'local — matched to ~/.config/quickshell/config/Theme.qml'
# preferred_background: 0a0a0c
# Same rules as the shell and the editor: white does the work, crimson is the
# only saturated colour and it means either "load-bearing" (keyword, prefix,
# selected) or "wrong" (error, status, root). Everything else is grey, or a hue
# pulled far enough down that it reads as grey with a temperature.
#
# Kept in sync by hand with conf.d/takemi_theme.fish, which is what actually
# takes effect at startup. This file exists so `fish_config` can preview it.
fish_color_normal ffffff
fish_color_command ffffff --bold
fish_color_keyword ff2d40
fish_color_param c8c8ce
fish_color_option 8d9bb8
fish_color_quote 7f9b6a
fish_color_escape 9db8bd
fish_color_operator c8c8ce
fish_color_redirection ff6b78
fish_color_end 6b6b70
fish_color_comment 6b6b70 --italics
fish_color_error ff2d40 --bold
fish_color_cancel ff2d40
fish_color_gray 6b6b70
fish_color_autosuggestion 4a4a52
fish_color_selection 000000 --background=ff2d40
fish_color_search_match 000000 --background=c8c8ce
fish_color_match 000000 --background=ff2d40
fish_color_history_current --bold
fish_color_valid_path --underline
fish_color_cwd ffffff --bold
fish_color_cwd_root ff2d40 --bold
fish_color_user c8c8ce
fish_color_host ffffff --bold
fish_color_host_remote ff6b78
fish_color_status ff2d40
fish_pager_color_progress 6b6b70 --background=141418
fish_pager_color_background
fish_pager_color_prefix ffffff --bold
fish_pager_color_completion c8c8ce
fish_pager_color_description 6b6b70
fish_pager_color_secondary_background --background=0d0d10
fish_pager_color_secondary_completion c8c8ce
fish_pager_color_secondary_description 6b6b70
fish_pager_color_secondary_prefix ffffff --bold
fish_pager_color_selected_background --background=ff2d40
fish_pager_color_selected_completion 000000
fish_pager_color_selected_description 000000
fish_pager_color_selected_prefix 000000 --bold
+5 -14
View File
@@ -1,8 +1,9 @@
include=~/.config/fuzzel/catppuccin-fuzzel/themes/catppuccin-mocha/red.ini include=~/.config/fuzzel/takemi.ini
include=~/.config/fuzzel/catppuccin-fuzzel/themes/catppuccin-mocha/red.ini
[main] [main]
font=FiraCode Nerd Font:size=14 # JetBrainsMono to match Theme.fontMono, so the launcher and the bar are
# visibly the same typeface.
font=JetBrainsMono Nerd Font:size=14
terminal=alacritty terminal=alacritty
dpi-aware=no dpi-aware=no
@@ -12,17 +13,7 @@ horizontal-pad=20
vertical-pad=20 vertical-pad=20
inner-pad=10 inner-pad=10
prompt=" " prompt=" "
show-actions=no show-actions=no
layer=overlay layer=overlay
[border]
radius=12
width=2
[colors]
background=1e1e2e80
# Matches the background so the counter becomes invisible for a cleaner look
counter=1e1e2eff
border=f38ba8ff
+37
View File
@@ -0,0 +1,37 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
wallpaper { wallpaper {
monitor = monitor =
path = ~/.wallpapers/takemi_2.jpg path = ~/.wallpapers/takemi_1.jpg
fit_mode = cover fit_mode = cover
} }
+36 -22
View File
@@ -1,25 +1,39 @@
# Takemi palette — kept in step with ~/.config/quickshell/config/Theme.qml. # Takemi palette — kept in step with ~/.config/quickshell/config/Theme.qml.
# Derived from ~/.wallpapers/takemi_2.jpg: cold teal over near-black, with #
# Persona 5 crimson as the accent. # Crimson, black, white. Nothing else carries hue.
$base = rgb(04070a)
$mantle = rgb(080e14)
$surface = rgb(0d1922)
$surfaceAlt = rgb(14262f)
$overlay = rgb(1d3a47)
$outline = rgb(2b5567)
$text = rgb(e2f4f8)
$subtext = rgb(93b8c4)
$muted = rgb(587c8a)
$primary = rgb(34d3e6)
$primaryDim = rgb(17879b)
$glow = rgb(8ef2ff)
$blue = rgb(3b8fd6)
$deep = rgb(0d4b5e)
# ----------------------------------------------------------------- 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) $accent = rgb(ff2d40)
$accentDim = rgb(a8121f)
$warn = rgb(ffb03a) $accentDim = rgb(8f1826)
$ok = rgb(35e08f) $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
+33 -36
View File
@@ -10,8 +10,9 @@ general {
border_size = 2 border_size = 2
# Teal into crimson, on the diagonal — the shell's two voices. # Crimson into its own shadow, on the diagonal. The accent is the only hue
col.active_border = $primary $accent 135deg # here — a gradient into a second colour is what put blue in the border.
col.active_border = $accent $accentDim 135deg
col.inactive_border = $outline col.inactive_border = $outline
resize_on_border = false resize_on_border = false
@@ -27,55 +28,51 @@ decoration {
# Persona 5 has no rounded corners. Neither do we. # Persona 5 has no rounded corners. Neither do we.
rounding = 0 rounding = 0
# Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0 active_opacity = 1.0
inactive_opacity = 0.96 inactive_opacity = 1.0
shadow { shadow {
enabled = true enabled = true
range = 18 range = 18
render_power = 3 render_power = 3
offset = 0 4 offset = 0 4
color = rgba(04070aee) color = rgba(000000ee)
color_inactive = rgba(04070a88) color_inactive = rgba(00000088)
} }
# https://wiki.hyprland.org/Configuring/Variables/#blur # 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 { blur {
enabled = true enabled = false
size = 6
passes = 3
new_optimizations = true
ignore_opacity = true
xray = false
noise = 0.02
contrast = 1.1
brightness = 0.85
vibrancy = 0.25
vibrancy_darkness = 0.3
# The bar and its popouts do their own thing.
popups = true
popups_ignorealpha = 0.4
} }
} }
# https://wiki.hyprland.org/Configuring/Animations/ # https://wiki.hyprland.org/Configuring/Animations/
# Snappy with a little overshoot — nothing here should feel soft. # 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 { animations {
enabled = yes, please :) enabled = yes, please :)
bezier = snap, 0.16, 1.00, 0.30, 1.00 bezier = snap, 0.16, 1.00, 0.30, 1.00
bezier = overshoot, 0.34, 1.56, 0.64, 1.00 bezier = overshoot, 0.34, 1.12, 0.64, 1.00
bezier = hard, 0.55, 0.00, 0.90, 0.35 bezier = hard, 0.55, 0.00, 0.90, 0.35
bezier = linear, 0, 0, 1, 1 bezier = linear, 0, 0, 1, 1
animation = global, 1, 4, snap animation = global, 1, 4, snap
animation = border, 1, 6, snap animation = border, 1, 5, snap
animation = borderangle, 1, 60, linear, loop
animation = windows, 1, 4, overshoot animation = windows, 1, 4, snap
animation = windowsIn, 1, 4.2, overshoot, popin 88% animation = windowsIn, 1, 4, overshoot, popin 96%
animation = windowsOut, 1, 2.4, hard, popin 90% animation = windowsOut, 1, 2.6, hard, popin 97%
animation = windowsMove, 1, 3.6, snap animation = windowsMove, 1, 3.6, snap
animation = fade, 1, 3, snap animation = fade, 1, 3, snap
@@ -85,15 +82,15 @@ animations {
animation = fadeShadow, 1, 3, snap animation = fadeShadow, 1, 3, snap
animation = fadeDim, 1, 2.4, snap animation = fadeDim, 1, 2.4, snap
animation = layers, 1, 3, overshoot animation = layers, 1, 3, snap
animation = layersIn, 1, 3.4, overshoot, slide animation = layersIn, 1, 3, snap, slide
animation = layersOut, 1, 2, hard, slide animation = layersOut, 1, 2, hard, slide
animation = workspaces, 1, 3.4, overshoot, slidevert animation = workspaces, 1, 3, snap, slidevert
animation = workspacesIn, 1, 3.4, overshoot, slidevert animation = workspacesIn, 1, 3, snap, slidevert
animation = workspacesOut, 1, 2.6, hard, slidevert animation = workspacesOut, 1, 2.4, hard, slidevert
animation = specialWorkspace, 1, 3.6, overshoot, slidevert animation = specialWorkspace, 1, 3, snap, slidevert
} }
# See https://wiki.hyprland.org/Configuring/Master-Layout/ for more # See https://wiki.hyprland.org/Configuring/Master-Layout/ for more
@@ -117,7 +114,7 @@ misc {
} }
group { group {
col.border_active = $primary $accent 135deg col.border_active = $accent $accentDim 135deg
col.border_inactive = $outline col.border_inactive = $outline
groupbar { groupbar {
@@ -128,8 +125,8 @@ group {
gradients = false gradients = false
# --- Active Tab --- # --- Active Tab ---
col.active = $primary col.active = $accent
text_color = $base text_color = $ink
# --- Inactive Tab --- # --- Inactive Tab ---
col.inactive = $surface col.inactive = $surface
+103 -34
View File
@@ -1,9 +1,9 @@
# Takemi # Takemi
A Persona 5 flavoured Quickshell desktop for Hyprland. Cold teal and near-black A Persona 5 flavoured Quickshell desktop for Hyprland. Crimson, black and white,
pulled from `~/.wallpapers/takemi_2.jpg`, with crimson as the accent voice. with no gradients anywhere. Everything leans by the same 14°, corners are
Everything leans, corners are chopped rather than rounded, and headings carry chopped rather than rounded, and every surface is one continuous slab divided by
the wallpaper's RGB split. hairlines rather than a cluster of floating cards.
Replaces Waybar, dunst and hyprlock. Replaces Waybar, dunst and hyprlock.
@@ -18,37 +18,38 @@ Hyprland starts it from `~/.config/hypr/modules/autostart.conf`.
## Layout ## Layout
The shell is spread across three surfaces rather than crammed into one bar, Two surfaces. Status moves off the bar and onto a vertical rail, because on a
because on a 1080p panel horizontal space is cheap and vertical space is not: a 1080p panel horizontal space is cheap and vertical space is not: a 48px rail
48px rail costs 2.5% of the width, where the same 48px along the bottom would costs 2.5% of the width, where another 48px strip along the bottom would have
have cost 4.4% of the height on top of what the top bar already takes. cost 4.4% of the height on top of what the bar already takes.
``` ```
┌────────────────────────────────────────────┐ ┌────────────────────────────────────────────┐
│ ◆ TAKEMI [1][2][3] window title 00:09 │ bar — 48px exclusive │ ◆ │ 1 2 3 │ window title ♪ TRACK │ 42% │ 00:09 │ bar — 48px exclusive
├────────────────────────────────────────────┤ ├────────────────────────────────────────────┤
│ ▐█▌│ rail — 48px exclusive │ ▐█▌│ rail — 48px exclusive
│ windows ▐ ▌│ │ windows ▐ ▌│
▌│ ▌│
♪ TRACK ╲ ▁▃▅ 42% dock — no exclusive zone ▐ ▌│
└────────────────────────────────────────────┘ └────────────────────────────────────────────┘
``` ```
- **bar** — identity, workspaces, focused window, clock. - **bar** — one slab: identity and workspaces on the left; the focused window,
- **rail** — network, bluetooth, audio, mic, backlight, battery, tray, the player, the CPU/MEM/GPU readout and the clock on the right.
- **rail** — one slab: network, bluetooth, audio, mic, backlight, battery, tray,
notifications, power. Popouts open leftward, into the screen. notifications, power. Popouts open leftward, into the screen.
- **dock** — now playing and the CPU/MEM/GPU readout. Reserves nothing: windows
tile the full height underneath, and the input mask is clipped to the cluster Each surface is a single continuous `P5Panel` with modules divided by sheared
so clicks elsewhere in the corner fall through. Idles near-invisible and wakes hairlines. `BarPill` is chromeless by default — a module is *content inside* a
on hover or on playback; the crimson corner wedge is the always-on handle that surface, not a surface of its own — and marks hover with a crimson underline.
marks where it lives. Pass `standalone: true` for the rare thing that really is its own panel.
``` ```
shell.qml entrypoint — bar, rail, dock, power menu, toasts, OSD per screen shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves, shape variance config/ palette, type scale, geometry, motion curves
components/ the P5 primitives: panels, gauges, sliders, split-ink text components/ the P5 primitives: panels, gauges, sliders, split-ink text
services/ data sources, all singletons services/ data sources, all singletons
bar/ the three surfaces and their modules bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar modules popouts/ hover panels hanging off rail and bar modules
overlays/ power menu, notification toasts, volume/brightness OSD overlays/ power menu, notification toasts, volume/brightness OSD
lock/ session lock lock/ session lock
@@ -75,8 +76,8 @@ contentPad * 2`), which keeps the shape maths out of binding loops.
| Tray | rail | activate | app menu | secondary | | | | Tray | rail | activate | app menu | secondary | | |
| Bell | rail | history + mark read | do not disturb | | | history | | Bell | rail | history + mark read | do not disturb | | | history |
| Power | rail | power menu | | | | | | Power | rail | power menu | | | | |
| Media | dock | play/pause | next | raise player | next/prev | full player | | Media | bar | play/pause | next | raise player | next/prev | full player |
| CPU/MEM/GPU | dock | btop | | | | resource dashboard | | CPU/MEM/GPU | bar | btop | | | | resource dashboard |
Popouts open on a short dwell and stay while the pointer is over the trigger or Popouts open on a short dwell and stay while the pointer is over the trigger or
the panel. Clicking pins one open. the panel. Clicking pins one open.
@@ -142,15 +143,83 @@ The display face is Archivo Black, set italic almost everywhere. Change
## Palette discipline ## Palette discipline
Two hues, deliberately: teal carries every resting state, crimson means alarm Crimson, black, white. That is the whole palette.
and nothing else. The old blue, green and amber remain in `Theme.qml` only as
aliases onto those two families so existing call sites keep working — don't
introduce new uses. `Theme.heat()` is a continuous teal→crimson ramp rather than
a hue cycle, so a number 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.
Geometry variance lives in the same file. `Theme.leanFor(i)`, `chopFor(i)` and **Changing the accent.** The colour is written in exactly three places, one per
`cornersFor(i)` hand each module a different lean and chamfer by index, so program:
modules stop being congruent parallelograms in an even rhythm — pass a module's
index rather than accepting the defaults. | 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 |
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.
## 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.
- **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.
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.
## Geometry discipline
**One angle, one chamfer, everywhere.** `Theme.skew` is `tan(14°)` and every
edge in the shell leans by exactly that, in the same direction; every corner is
chopped by exactly `Theme.cut`. A vertical panel (the rail) leans its horizontal
edges by a fraction of its *width*, which works out to the same 14°.
This is deliberate and was learned the hard way. An earlier pass gave every
module a different lean, chamfer and height from per-index variance tables, on
the theory that Persona geometry is "chaotic". It isn't — Persona UI is
meticulously clean, and the chaos lives in composition and motion, not in each
element being a different shape. Randomised geometry just reads as noise. If you
find yourself adding a variance table, don't.
Each surface is **one continuous slab**, with modules divided by sheared
hairline rules rather than by gaps. A row of separate floating cards reads as
clutter; that is what the dividers exist to avoid.
+67 -55
View File
@@ -5,13 +5,13 @@ import "root:/config"
import "root:/components" import "root:/components"
import "root:/services" import "root:/services"
// One bar per monitor. // One bar per monitor, and one surface.
// //
// Identity, workspaces and the focused window on the left; the clock on the // Identity and workspaces on the left, the player, metrics and clock on the
// right. Status moved to the rail and the player and metrics moved to the dock, // right, all inside a single continuous slab and separated by hairline rules
// which is what buys the room for this to breathe — nothing here is fighting // rather than by gaps. Nothing floats and nothing has its own card. Status
// for horizontal space any more, so the title gets a generous cap and the type // lives on the rail, which is likewise one slab — two surfaces, not a dozen
// can run large instead of being squeezed to fit between two clusters. // islands.
PanelWindow { PanelWindow {
id: root id: root
@@ -37,33 +37,39 @@ PanelWindow {
exclusiveZone: Theme.barHeight + Theme.barMargin exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent" color: "transparent"
// The slab. One panel, full width, flat fill, black keyline.
P5Panel {
id: slab
anchors.fill: parent
lean: Theme.skew
chop: Theme.cut
chopCorners: [1, 3]
padding: 0
fill: Theme.surface
fillOpacity: 1.0
border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5
}
readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM
// ------------------------------------------------------------------ left // ------------------------------------------------------------------ left
Row { Row {
id: leftCluster id: leftGroup
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.barGap spacing: 0
Sigil { Sigil {
id: sigil id: sigil
screenRef: root.modelData screenRef: root.modelData
} }
BarPill { Divider {}
id: spacesPill
accent: Theme.primary
padding: Theme.padM
interactive: false
lean: Theme.leanFor(1)
chop: Theme.chopFor(1)
chopCorners: Theme.cornersFor(1)
implicitWidth: spacesRow.implicitWidth + contentPad * 2
Row {
id: spacesRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padL
Workspaces { Workspaces {
id: spaces id: spaces
@@ -71,53 +77,59 @@ PanelWindow {
screenRef: root.modelData screenRef: root.modelData
} }
Rectangle { Divider {}
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 18
color: Theme.alpha(Theme.outline, 0.8)
}
ActiveWindow { ActiveWindow {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData screenRef: root.modelData
// The only thing competing for this row is the clock, so the
// title can have most of the bar.
maxWidth: Math.max(120, root.width maxWidth: Math.max(120, root.width
- sigil.implicitWidth - leftGroup.x - rightGroup.width - sigil.implicitWidth
- clock.implicitWidth - spaces.implicitWidth - root.edgeInset * 2 - Theme.padXL)
- spaces.implicitWidth
- Theme.barGap * 3
- Theme.padL * 2
- Theme.padXL
- spacesPill.contentPad * 2)
}
}
} }
} }
// ----------------------------------------------------------------- right // ----------------------------------------------------------------- right
Clock { Row {
id: clock id: rightGroup
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 0
MediaPill {
screenRef: root.modelData screenRef: root.modelData
} }
// A hairline of crimson under the whole bar — the Persona 5 underscore. Divider {}
Resources {
screenRef: root.modelData
}
Divider {}
Clock {
screenRef: root.modelData
}
}
// A hairline rule between modules, sheared to the house angle. This is what
// divides the bar now — gaps between cards are what made it look scattered.
component Divider: Item {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight
Rectangle { Rectangle {
anchors.bottom: parent.bottom anchors.centerIn: parent
anchors.left: parent.left width: 1
anchors.right: parent.right height: 20
height: 1 color: Theme.alpha(Theme.outline, 0.9)
gradient: Gradient {
orientation: Gradient.Horizontal transform: Matrix4x4 {
GradientStop { position: 0.0; color: "transparent" } matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
GradientStop { position: 0.18; color: Theme.alpha(Theme.accent, 0.55) } 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
GradientStop { position: 0.5; color: Theme.alpha(Theme.primary, 0.35) } }
GradientStop { position: 0.82; color: Theme.alpha(Theme.accent, 0.55) }
GradientStop { position: 1.0; color: "transparent" }
} }
} }
} }
+60 -28
View File
@@ -2,12 +2,19 @@ import QtQuick
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// Shared chrome for every bar module: a leaning slab that lifts and lights up // Shared chrome for every bar module.
// under the pointer, and forwards clicks and scrolls to the widget. //
// 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
// underline rather than by lighting up a card. That is what stops the bar from
// reading as a row of floating islands.
//
// `standalone: true` brings back the individual leaning slab, for the few
// places that genuinely are separate surfaces.
Item { Item {
id: root id: root
property color accent: Theme.primary property color accent: Theme.accent
property bool active: false property bool active: false
property bool slash: false property bool slash: false
property real lean: Theme.skew property real lean: Theme.skew
@@ -15,18 +22,29 @@ Item {
property real padding: Theme.padM property real padding: Theme.padM
property bool interactive: true property bool interactive: true
property bool halftone: false property bool halftone: false
property bool standalone: false
// Chamfer, forwarded so modules can differ from one another instead of all
// sharing the panel default.
property real chop: Theme.cut property real chop: Theme.cut
property var chopCorners: [1, 3] property var chopCorners: [1, 3]
readonly property bool hovered: hover.hovered readonly property bool hovered: hover.hovered
readonly property real leanInset: panel.leanInset readonly property real leanInset: standalone ? panelLoader.item?.leanInset ?? 0 : 0
// Total horizontal inset a pill must add on top of its content width. // The hover rule's own strip, reserved at the bottom of the module.
readonly property real contentPad: panel.contentPad //
readonly property alias panelItem: panel // 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
// and centring content in what is left is what keeps the rule under the
// module instead of across it.
readonly property real ruleHeight: 2
readonly property real ruleInset: 5
readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset
// Total horizontal inset a module must add on top of its content width.
readonly property real contentPad: standalone
? (panelLoader.item?.contentPad ?? root.padding)
: root.padding
default property alias content: holder.data default property alias content: holder.data
@@ -37,9 +55,12 @@ Item {
implicitHeight: Theme.barHeight implicitHeight: Theme.barHeight
P5Panel { Loader {
id: panel id: panelLoader
anchors.fill: parent anchors.fill: parent
active: root.standalone
sourceComponent: P5Panel {
lean: root.lean lean: root.lean
leanLeft: root.leanLeft leanLeft: root.leanLeft
chop: root.chop chop: root.chop
@@ -47,39 +68,50 @@ Item {
padding: root.padding padding: root.padding
halftone: root.halftone halftone: root.halftone
halftoneColor: root.accent halftoneColor: root.accent
sheen: true
fill: root.active ? Theme.surfaceAlt : Theme.surface fill: root.active ? Theme.surfaceAlt : Theme.surface
fillOpacity: root.hovered ? 0.98 : 0.88 fillOpacity: 1.0
border: root.hovered || root.active border: root.hovered || root.active
? Theme.alpha(root.accent, 0.9) ? root.accent
: Theme.alpha(Theme.outline, 0.7) : Theme.alpha(Theme.outline, 0.9)
borderWidth: root.hovered || root.active ? Theme.stroke : 1 borderWidth: root.hovered || root.active ? Theme.stroke : 1
slash: root.slash || root.active slash: root.slash || root.active
slashColor: root.accent slashColor: root.accent
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
} }
Behavior on border {
ColorAnimation { duration: Theme.durBase }
} }
Item { Item {
id: holder id: holder
anchors.fill: parent anchors.fill: parent
} anchors.leftMargin: root.contentPad
anchors.rightMargin: root.contentPad
anchors.bottomMargin: root.ruleBand
} }
// Lift toward the pointer. Subtle, but it makes the bar feel alive. // Hover mark for in-bar modules: a crimson rule under the content, wiping
transform: Translate { // out from the leading edge. Sheared to match the one angle everything uses.
y: root.hovered && root.interactive ? -2 : 0 Item {
visible: !root.standalone && root.interactive
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: root.ruleInset
height: root.ruleHeight
clip: true
Behavior on y { Rectangle {
NumberAnimation { height: parent.height
duration: Theme.durBase width: root.hovered || root.active ? parent.width : 0
easing.type: Easing.OutBack color: root.accent
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 2 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
} }
} }
} }
-117
View File
@@ -1,117 +0,0 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// The floating bottom-left cluster: now playing, plus the CPU/MEM/GPU readout.
//
// This one reserves no space at all. Windows tile the full height of the screen
// underneath it, and the input mask is clipped to the cluster itself so clicks
// anywhere else in the corner fall straight through to whatever is below.
//
// At rest it sits at a quarter opacity — present enough to read at a glance,
// faint enough to ignore — and snaps to full weight when the pointer arrives.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "takemi-dock"
anchors {
bottom: true
left: true
}
margins {
bottom: Theme.dockMargin
left: Theme.dockMargin
}
implicitWidth: cluster.implicitWidth
implicitHeight: cluster.implicitHeight
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore
color: "transparent"
// Only the cluster takes pointer input; the rest of the surface is
// transparent to clicks.
mask: Region { item: cluster }
readonly property bool awake: clusterHover.hovered || Media.playing
// A crimson wedge pinned to the corner. This is the one part of the dock
// that never fades — it marks where the cluster lives so a nearly
// invisible idle state is still something you can aim at, and it doubles
// as the corner furniture the layout was missing.
Shape {
anchors.left: parent.left
anchors.bottom: parent.bottom
width: 26
height: 26
z: -1
preferredRendererType: Shape.CurveRenderer
opacity: root.awake ? 0.0 : 0.9
Behavior on opacity {
NumberAnimation { duration: Theme.durBase }
}
ShapePath {
fillColor: Theme.accent
strokeColor: "transparent"
PathPolyline {
path: [
Qt.point(0, 26),
Qt.point(0, 6),
Qt.point(26, 26),
Qt.point(0, 26)
]
}
}
}
Row {
id: cluster
spacing: Theme.barGap
opacity: root.awake ? 1.0 : Theme.dockIdleOpacity
Behavior on opacity {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
// Lift off the corner as it wakes.
transform: Translate {
y: root.awake ? 0 : 4
Behavior on y {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutBack
}
}
}
HoverHandler {
id: clusterHover
}
MediaPill {
screenRef: root.modelData
}
Resources {
screenRef: root.modelData
}
}
}
+53 -100
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Shapes
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import "root:/config" import "root:/config"
@@ -7,17 +6,15 @@ import "root:/components"
import "root:/services" import "root:/services"
import "root:/popouts" import "root:/popouts"
// The right-hand status rail. // The right-hand status rail — one slab, not a stack of cards.
// //
// Everything that used to be crammed into the right third of the top bar lives // Connectivity, audio, power, tray and notifications all sit inside a single
// here instead: connectivity, audio, power, tray, notifications. On a 1080p // continuous column, divided by hairline rules the same way the bar divides its
// panel this is the cheap edge — 48px of width is 2.5% of the screen, where the // modules. Same lean, same chamfer, same keyline as the bar and the dock, so
// same 48px of height would have been 4.4% on top of what the top bar already // the three surfaces read as one system.
// takes.
// //
// Slabs are grouped by what they do rather than spaced evenly, and each pulls a // On a 1080p panel this is the cheap edge: 48px of width is 2.5% of the screen,
// different lean and chamfer from the shape tables, so the column reads as a // where the same 48px of height would be 4.4% on top of what the bar takes.
// stack of distinct cards instead of a ladder.
PanelWindow { PanelWindow {
id: root id: root
@@ -43,55 +40,37 @@ PanelWindow {
exclusiveZone: Theme.railWidth + Theme.barMargin exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent" color: "transparent"
// Corner furniture. The dock pins a crimson wedge to the bottom-left; this // The slab.
// is its opposite number, so the desktop is bracketed on the diagonal P5Panel {
// rather than merely edged. Decorative only — it sits under the slabs and id: slab
// takes no input. anchors.fill: parent
Shape { vertical: true
anchors.right: parent.right lean: Theme.skew
anchors.bottom: parent.bottom chop: Theme.cut
width: 30 chopCorners: [1, 3]
height: 30 padding: 0
z: -1 fill: Theme.surface
preferredRendererType: Shape.CurveRenderer fillOpacity: 1.0
opacity: 0.9 border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5
}
ShapePath { readonly property real edgeInset: Theme.railWidth * Theme.skew + Theme.padM
fillColor: Theme.accent
strokeColor: "transparent"
PathPolyline {
path: [
Qt.point(30, 30),
Qt.point(30, 4),
Qt.point(2, 30),
Qt.point(30, 30)
]
}
}
}
// ------------------------------------------------------------------- top // ------------------------------------------------------------------- top
Column { Column {
id: upper id: upper
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: root.edgeInset
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.railGap spacing: 0
// Connectivity.
RailGroup {
variant: 0
accent: Sys.netUp ? Theme.primary : Theme.muted
implicitHeight: linkCol.implicitHeight + contentPad * 2
Column {
id: linkCol
anchors.centerIn: parent
spacing: Theme.padS
RailIcon { RailIcon {
id: netIcon id: netIcon
icon: Net.icon icon: Net.icon
tint: Theme.primary
dim: !Sys.netUp dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi() onRightClicked: Net.toggleWifi()
@@ -101,24 +80,12 @@ PanelWindow {
RailIcon { RailIcon {
id: btIcon id: btIcon
icon: Bt.icon icon: Bt.icon
tint: Theme.primary
dim: !Bt.enabled dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle() onRightClicked: Bt.toggle()
} }
}
}
// Audio and backlight. Divider {}
RailGroup {
variant: 1
accent: Audio.muted || Audio.micMuted ? Theme.accent : Theme.primary
implicitHeight: audioCol.implicitHeight + contentPad * 2
Column {
id: audioCol
anchors.centerIn: parent
spacing: Theme.padS
RailIcon { RailIcon {
id: volIcon id: volIcon
@@ -145,67 +112,36 @@ PanelWindow {
visible: Brightness.available visible: Brightness.available
icon: Brightness.icon icon: Brightness.icon
readout: Math.round(Brightness.percent) + "%" readout: Math.round(Brightness.percent) + "%"
tint: Theme.primary
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5) onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
} }
}
}
// Battery. Divider { visible: Battery.available }
RailGroup {
variant: 2
visible: Battery.available
accent: Battery.critical ? Theme.accent : Theme.primary
implicitHeight: battIcon.implicitHeight + contentPad * 2
RailIcon { RailIcon {
id: battIcon id: battIcon
anchors.centerIn: parent visible: Battery.available
icon: Battery.icon icon: Battery.icon
readout: Math.round(Battery.percent) + "%" readout: Math.round(Battery.percent) + "%"
tint: Battery.tint
alert: Battery.critical alert: Battery.critical
pulsing: Battery.critical pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned onClicked: battPopout.pinned = !battPopout.pinned
} }
} }
}
// ---------------------------------------------------------------- bottom // ---------------------------------------------------------------- bottom
Column { Column {
id: lower id: lower
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: root.edgeInset
// Clear of the corner wedge, which would otherwise sit entirely behind
// the bottom slab and never be seen.
anchors.bottomMargin: 26
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.railGap spacing: 0
// Tray.
RailGroup {
variant: 3
visible: tray.visible
accent: Theme.primary
implicitHeight: tray.implicitHeight + contentPad * 2
Tray { Tray {
id: tray id: tray
anchors.centerIn: parent anchors.horizontalCenter: parent.horizontalCenter
}
} }
// Notifications and power — the two that act on the session rather than Divider { visible: tray.visible }
// report on it, so they sit apart at the foot of the rail.
RailGroup {
variant: 4
accent: Notifs.dnd ? Theme.muted : Theme.primary
implicitHeight: actionCol.implicitHeight + contentPad * 2
Column {
id: actionCol
anchors.centerIn: parent
spacing: Theme.padS
RailIcon { RailIcon {
id: bellIcon id: bellIcon
@@ -213,7 +149,6 @@ PanelWindow {
readout: Notifs.unread > 0 readout: Notifs.unread > 0
? (Notifs.unread > 9 ? "9+" : String(Notifs.unread)) ? (Notifs.unread > 9 ? "9+" : String(Notifs.unread))
: "" : ""
tint: Notifs.hasUnread ? Theme.accent : Theme.primary
dim: Notifs.dnd dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd alert: Notifs.hasUnread && !Notifs.dnd
onClicked: { onClicked: {
@@ -226,10 +161,28 @@ PanelWindow {
RailIcon { RailIcon {
id: powerIcon id: powerIcon
icon: "󰐥" icon: "󰐥"
tint: Actions.powerMenuOpen ? Theme.accent : Theme.subtext alert: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu() onClicked: Actions.togglePowerMenu()
} }
} }
// The rail's counterpart to the bar's divider: a horizontal hairline,
// sheared to the same angle the rail's own edges lean at.
component Divider: Item {
implicitWidth: Theme.railWidth
implicitHeight: Theme.padM + 4
Rectangle {
anchors.centerIn: parent
width: 22
height: 1
color: Theme.alpha(Theme.outline, 0.9)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, 0, 0, 0,
-Theme.skew, 1, 0, 22 * Theme.skew / 2,
0, 0, 1, 0, 0, 0, 0, 1)
}
} }
} }
-92
View File
@@ -1,92 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// A slab on the right-hand rail. The vertical counterpart to BarPill: it sizes
// itself to a stacked column rather than a row, and pulls its lean and chamfer
// from the shape-variance tables by `variant`, so no two slabs on the rail are
// congruent.
Item {
id: root
property color accent: Theme.primary
property int variant: 0
property bool active: false
property real padding: Theme.padS
property bool interactive: false
property bool halftone: false
readonly property bool hovered: hover.hovered
readonly property real contentPad: panel.contentPad
readonly property alias panelItem: panel
default property alias content: holder.data
signal clicked()
implicitWidth: Theme.railWidth
P5Panel {
id: panel
anchors.fill: parent
vertical: true
// Scaled down from the horizontal tables: a rail slab leans by a
// fraction of its 48px width, so the raw values would be far too steep.
lean: Theme.leanFor(root.variant) * 0.6
leanLeft: Theme.leanLeftFor(root.variant)
chop: Math.min(Theme.chopFor(root.variant), 10)
chopCorners: Theme.cornersFor(root.variant)
padding: root.padding
halftone: root.halftone
halftoneColor: root.accent
sheen: true
fill: root.active ? Theme.surfaceAlt : Theme.surface
fillOpacity: root.hovered ? 0.98 : 0.88
border: root.hovered || root.active
? Theme.alpha(root.accent, 0.9)
: Theme.alpha(Theme.outline, 0.7)
borderWidth: root.hovered || root.active ? Theme.stroke : 1
slash: root.active
slashColor: root.accent
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
}
Behavior on border {
ColorAnimation { duration: Theme.durBase }
}
Item {
id: holder
anchors.fill: parent
}
}
// Slabs lift toward the pointer, away from the screen edge.
transform: Translate {
x: root.hovered && root.interactive ? -3 : 0
Behavior on x {
NumberAnimation {
duration: Theme.durBase
easing.type: Easing.OutBack
}
}
}
HoverHandler {
id: hover
enabled: root.interactive
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: root.interactive
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
}
+15 -1
View File
@@ -19,6 +19,10 @@ Item {
readonly property bool hovered: hover.hovered readonly property bool hovered: hover.hovered
// Clearance between the wipe and the rail's left edge, chosen so the bar
// stays inside the slab once `scale` has expanded the icon about its centre.
readonly property real wipeInset: 5
signal clicked() signal clicked()
signal rightClicked() signal rightClicked()
signal middleClicked() signal middleClicked()
@@ -68,12 +72,22 @@ Item {
// Leading-edge wipe. On the rail this runs vertically down the left side, // Leading-edge wipe. On the rail this runs vertically down the left side,
// mirroring the slash P5Panel puts on horizontal modules. // mirroring the slash P5Panel puts on horizontal modules.
//
// Held off the edge by `wipeInset`: the icon spans the rail's full width, so
// at x = 0 this landed exactly on the slab's own border, and the 1.08 hover
// 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.
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: root.wipeInset
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 2 width: 2
height: root.hovered ? layout.implicitHeight : 0 height: root.hovered ? layout.implicitHeight : 0
color: root.alert ? Theme.accent : root.tint color: Theme.glow
Behavior on height { Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo } NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
-6
View File
@@ -14,13 +14,7 @@ BarPill {
accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent)) accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
// Roomier than it was on the bar. In the dock there is nothing to compete
// with, so the figures get real padding instead of being pushed up against
// the slanted edges.
padding: Theme.padM padding: Theme.padM
lean: Theme.leanFor(2)
chop: Theme.chopFor(2)
chopCorners: Theme.cornersFor(2)
implicitWidth: layout.implicitWidth + contentPad * 2 implicitWidth: layout.implicitWidth + contentPad * 2
onClicked: Actions.openSystemMonitor() onClicked: Actions.openSystemMonitor()
+13 -14
View File
@@ -20,26 +20,25 @@ BarPill {
onClicked: Actions.launch("fuzzel") onClicked: Actions.launch("fuzzel")
onRightClicked: Actions.reloadShell() onRightClicked: Actions.reloadShell()
Icon { // 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
// offset every other heading carries, so the house mark is built out of
// the same parts as the rest of the bar.
SplitText {
id: mark id: mark
anchors.centerIn: parent anchors.centerIn: parent
text: "󰋠" text: "P5"
font.pixelSize: Theme.fsTitle pixelSize: Theme.fsLarge
letterSpacing: 0
color: root.hovered ? Theme.accent : Theme.primary color: root.hovered ? Theme.accent : Theme.primary
} }
// Spin the mark on click, then settle. // Kick the ghosts out and let them snap back. The skull could spin a full
// turn and still read as a skull; a wordmark upside down just reads as
// broken, so the click gets the glitch instead.
Connections { Connections {
target: root target: root
function onClicked() { spin.restart(); } function onClicked() { mark.glitch(); }
}
SequentialAnimation {
id: spin
NumberAnimation {
target: mark; property: "rotation"
from: 0; to: 360; duration: Theme.durSlow
easing.type: Easing.OutExpo
}
} }
} }
+3 -1
View File
@@ -71,7 +71,9 @@ BarPill {
icon: Brightness.icon icon: Brightness.icon
label: Math.round(Brightness.percent) + "%" label: Math.round(Brightness.percent) + "%"
labelWidth: 34 labelWidth: 34
tint: Theme.warn // 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) onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
} }
+19 -47
View File
@@ -46,9 +46,7 @@ Item {
id: row id: row
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// Uneven gaps. An evenly spaced row of identical ticks reads as a spacing: 6
// progress bar; the whole point here is that it should not.
spacing: 5
Repeater { Repeater {
model: root.slots model: root.slots
@@ -65,19 +63,10 @@ Item {
readonly property bool focused: root.focusedId === wsId readonly property bool focused: root.focusedId === wsId
readonly property bool hovered: pipHover.hovered readonly property bool hovered: pipHover.hovered
// Each tick pulls its own shear and its own height off the // Every tick is the same height and the same shear. Only the
// variance tables, and sits a little off the baseline, so the // width changes, and only to say which workspace is focused.
// row is a ragged run of marks rather than a repeating comb. width: focused ? 32 : (occupied ? 16 : 9)
readonly property real shear: Theme.leanFor(pip.index) height: 18
readonly property int baseHeight: [18, 14, 20, 16][pip.index % 4]
readonly property int drift: [0, 2, -1, 1][pip.index % 4]
width: focused ? 36 : (occupied ? 16 : 9)
// The focused tick grows past its neighbours and past the row's
// own height, so it breaks the line instead of sitting in it.
height: focused ? 24 : pip.baseHeight
y: focused ? -2 : pip.drift
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
@@ -86,45 +75,28 @@ Item {
easing.overshoot: 1.6 easing.overshoot: 1.6
} }
} }
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
Behavior on y {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
// Black offset copy behind the focused tick — the same
// screen-print stack the panels use, at tick scale.
Rectangle {
visible: pip.focused
anchors.fill: parent
x: 3
y: 3
color: Theme.ink
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
1, -pip.shear, 0, pip.height * pip.shear,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
}
}
Rectangle { Rectangle {
id: tick id: tick
anchors.fill: parent 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.
color: { color: {
if (pip.urgent) return Theme.accent; if (pip.focused || pip.urgent) return Theme.accent;
if (pip.focused) return Theme.primary; if (pip.hovered) return Theme.text;
if (pip.hovered) return Theme.glow; if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
if (pip.occupied) return Theme.alpha(Theme.primary, 0.55); return "transparent";
return Theme.alpha(Theme.outline, 0.75);
} }
border.color: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
border.width: 1
transform: Matrix4x4 { transform: Matrix4x4 {
matrix: Qt.matrix4x4( matrix: Qt.matrix4x4(
1, -pip.shear, 0, pip.height * pip.shear, 1, -Theme.skew, 0, pip.height * Theme.skew,
0, 1, 0, 0, 0, 1, 0, 0,
0, 0, 1, 0, 0, 0, 1, 0,
0, 0, 0, 1) 0, 0, 0, 1)
@@ -141,7 +113,7 @@ Item {
opacity: pip.focused ? 1 : 0 opacity: pip.focused ? 1 : 0
visible: opacity > 0.01 visible: opacity > 0.01
text: pip.wsId text: pip.wsId
color: Theme.base color: Theme.ink
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
+103
View File
@@ -0,0 +1,103 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
import "root:/components"
// The shared ground for the two full-screen surfaces: a crimson wedge leaning
// at the house angle, a tiled stripe field that sweeps in from the left, and a
// halftone dot wash over the top.
//
// The lock screen and the power menu each used to draw their own version of
// this. They disagreed — the power menu had a wedge and a full stripe field,
// the lock had a 42px stripe band and no wedge, and the wedge leaned at 0.32
// where every panel in the shell leans at Theme.skew. Building both on one
// component is what makes them read as the same object seen twice rather than
// as two guesses at the same brief.
Item {
id: root
// 0 = withdrawn, 1 = fully present. Drives the stripe slide, the stripe
// opacity and the wedge together, so a caller animates one number instead
// of keeping three in sync. The power menu runs it both ways on open and
// close; the lock runs it once on entry.
property real reveal: 1
property color tone: Theme.accent
property bool wedge: true
property real wedgeOpacity: 0.35
// Bottom-left corner of the wedge and its span, as fractions of width. The
// wedge leans right going up, like every other edge in the shell, so its
// top edge sits `leanOffset` further right than its bottom.
property real wedgeLeft: 0
property real wedgeSpan: 0.34
property bool stripes: true
property real stripeOpacity: 0.10
property bool halftone: true
property real halftoneOpacity: 0.05
property color halftoneColor: Theme.primary
// ONE angle, everywhere — the same rule the panels follow.
readonly property real leanOffset: root.height * Theme.skew
// Diagonal stripe field, sliding in from off-screen left.
Image {
anchors.fill: parent
visible: root.stripes && root.reveal > 0
source: Theme.texStripes
fillMode: Image.Tile
opacity: root.stripeOpacity * root.reveal
smooth: false
layer.enabled: true
layer.effect: TintEffect {
tintColor: root.tone
}
transform: Translate {
x: -root.width * (1 - root.reveal)
}
}
// The wedge. Drawn after the stripes so it reads as a slab laid over them.
Shape {
anchors.fill: parent
visible: root.wedge && root.reveal > 0
opacity: root.reveal
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.alpha(Theme.accentDim, root.wedgeOpacity)
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: {
const x = root.wedgeLeft * root.width;
const span = root.wedgeSpan * root.width;
const off = root.leanOffset;
return Geom.closed([
Qt.point(x + off, 0),
Qt.point(x + off + span, 0),
Qt.point(x + span, root.height),
Qt.point(x, root.height)
]);
}
}
}
}
// Screen-printed dot wash over the whole thing.
Image {
anchors.fill: parent
visible: root.halftone
source: Theme.texHalftone
fillMode: Image.Tile
opacity: root.halftoneOpacity
smooth: false
layer.enabled: true
layer.effect: TintEffect { tintColor: root.halftoneColor }
}
}
+53 -44
View File
@@ -39,7 +39,10 @@ Item {
property color halftoneColor: Theme.primary property color halftoneColor: Theme.primary
property real halftoneOpacity: 0.07 property real halftoneOpacity: 0.07
// Inner glow along the top edge, for panels that should feel backlit. // Accepted and ignored. This used to paint a teal-to-transparent gradient
// down the top of every panel in the shell — the single biggest source of
// the murky blue cast. Fills are flat now. Kept as a no-op property so the
// call sites that still set it don't have to be chased down.
property bool sheen: false property bool sheen: false
// The screen-print stack. A pure-black copy of the silhouette sits behind // The screen-print stack. A pure-black copy of the silhouette sits behind
@@ -71,6 +74,28 @@ Item {
readonly property real contentPad: padding + leanInset readonly property real contentPad: padding + leanInset
// The panel outline as a mask texture. Anything that is painted from the
// raw polygon rather than from the chamfered one has to be clipped to this,
// or it escapes at the chopped corners.
Shape {
id: silhouette
anchors.fill: parent
visible: false
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "white"
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
// Offset black copy of the silhouette, behind everything. // Offset black copy of the silhouette, behind everything.
Loader { Loader {
anchors.fill: parent anchors.fill: parent
@@ -138,24 +163,7 @@ Item {
anchors.fill: parent anchors.fill: parent
active: root.halftone active: root.halftone
asynchronous: true asynchronous: true
sourceComponent: Item { sourceComponent: Image {
Shape {
id: maskShape
anchors.fill: parent
visible: false
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "white"
strokeColor: "transparent"
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
Image {
anchors.fill: parent anchors.fill: parent
source: Theme.texHalftone source: Theme.texHalftone
fillMode: Image.Tile fillMode: Image.Tile
@@ -166,47 +174,40 @@ Item {
colorization: 1.0 colorization: 1.0
colorizationColor: root.halftoneColor colorizationColor: root.halftoneColor
maskEnabled: true maskEnabled: true
maskSource: maskShape maskSource: silhouette
}
}
}
}
// Backlit top edge.
Loader {
anchors.fill: parent
active: root.sheen
sourceComponent: Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: "transparent"
fillGradient: LinearGradient {
x1: 0; y1: 0; x2: 0; y2: root.height
GradientStop { position: 0.0; color: Theme.alpha(Theme.primary, 0.16) }
GradientStop { position: 0.55; color: "transparent" }
}
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
} }
} }
} }
// Leading-edge slash. // Leading-edge slash.
//
// 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,
// at an angle that matched nothing. It is masked to the outline now, so the
// slash ends exactly where the chamfer does.
Loader { Loader {
anchors.fill: parent anchors.fill: parent
active: root.slash active: root.slash
sourceComponent: Shape { sourceComponent: Item {
Shape {
id: slashShape
anchors.fill: parent
visible: false
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
fillColor: root.slashColor fillColor: root.slashColor
strokeColor: "transparent" strokeColor: "transparent"
PathPolyline { PathPolyline {
path: { path: {
const t = root.slashWidth; const t = root.slashWidth;
// On a rail slab the slash runs along the slanted top // On a rail slab the slash runs along the slanted
// edge instead of down a side. // top edge instead of down a side.
if (root.vertical) { if (root.vertical) {
const voff = root.width * root.lean; const voff = root.width * root.lean;
return root.leanLeft return root.leanLeft
@@ -226,6 +227,14 @@ Item {
} }
} }
} }
MultiEffect {
anchors.fill: parent
source: slashShape
maskEnabled: true
maskSource: silhouette
}
}
} }
Item { Item {
+74 -68
View File
@@ -3,54 +3,77 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
// Takemi palette. Two hues, no exceptions: a cold teal that carries every // Takemi palette: crimson, black, white. Nothing else.
// resting state, and Persona 5 crimson that means alarm and nothing else. //
// Anything that used to be blue/green/amber now resolves into one of those two // White does the work: text, icons, idle indicators. Crimson is the only accent
// families, so a splash of red in the bar always reads as "something is wrong". // and it always means the same thing — active, or wrong. Black is structure,
// Derived from ~/.wallpapers/takemi_2.jpg. // used as a hard keyline rather than as a shadow. There are no gradients in
// this shell; every fill is flat.
//
// 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.
Singleton { Singleton {
id: root id: root
// ---------------------------------------------------------------- colors // ---------------------------------------------------------------- colors
// True black, for keylines and drop shapes. `base` is the near-black the
// surfaces sit on; `ink` is never tinted and never blended.
readonly property color ink: "#000000" readonly property color ink: "#000000"
readonly property color base: "#04070a" readonly property color base: "#0a0a0c"
readonly property color mantle: "#080e14" readonly property color mantle: "#0d0d10"
readonly property color surface: "#0d1922" readonly property color surface: "#141418"
readonly property color surfaceAlt: "#14262f" readonly property color surfaceAlt: "#1c1c22"
readonly property color overlay: "#1d3a47" readonly property color overlay: "#26262e"
readonly property color outline: "#2b5567" readonly property color outline: "#3a3a44"
readonly property color text: "#e2f4f8" readonly property color text: "#ffffff"
readonly property color subtext: "#93b8c4" readonly property color subtext: "#c8c8ce"
readonly property color muted: "#587c8a" readonly property color muted: "#6b6b70"
readonly property color primary: "#34d3e6" // electric teal — the house colour // ------------------------------------------------------------ THE KNOB
readonly property color primaryDim: "#17879b" // The one colour in this shell. Change this line and the entire accent
readonly property color glow: "#8ef2ff" // recolours — everything below is derived from it, and nothing else in the
readonly property color deep: "#0d4b5e" // palette carries hue at all.
//
// 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"
readonly property color accent: "#ff2d40" // P5 crimson — alarm, only ever alarm readonly property color accentDim: Qt.darker(root.accent, 1.9)
readonly property color accentDim: "#a8121f" readonly property color accentSoft: Qt.lighter(root.accent, 1.35)
readonly property color accentSoft: "#ff6b78" // the "getting there" step below accent
// Retired hues, kept as aliases so call sites keep working while resolving // `primary` is the resting voice of the UI, which is now simply white.
// into the two-hue system. Do not introduce new uses. // 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 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.
readonly property color glow: root.accent
// 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 blue: root.primaryDim
readonly property color ok: root.primary readonly property color ok: root.text
readonly property color warn: root.accentSoft readonly property color warn: root.accent
// Chromatic aberration pair, straight off the wallpaper's RGB split. // Display text carries a crimson and a black offset rather than an RGB
readonly property color splitRed: "#ff2b4d" // split. Same trick, correct palette.
readonly property color splitCyan: "#25e8ff" readonly property color splitRed: root.accent
readonly property color splitCyan: root.ink
function alpha(c: color, a: real): color { function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a); return Qt.rgba(c.r, c.g, c.b, a);
} }
// Linear blend, t = 0 gives a, t = 1 gives b.
function mix(a: color, b: color, t: real): color { function mix(a: color, b: color, t: real): color {
const k = Math.max(0, Math.min(1, t)); const k = Math.max(0, Math.min(1, t));
return Qt.rgba(a.r + (b.r - a.r) * k, return Qt.rgba(a.r + (b.r - a.r) * k,
@@ -59,12 +82,11 @@ Singleton {
a.a + (b.a - a.a) * k); a.a + (b.a - a.a) * k);
} }
// Heat ramp for load/usage readouts. Deliberately *not* a hue cycle: it sits // Load readouts sit white until the number actually matters, then bleed to
// flat teal until load actually matters, then bleeds continuously into // crimson. Not a hue cycle — red always means the same thing.
// crimson. A number that has gone red means the same thing everywhere.
function heat(pct: real): color { function heat(pct: real): color {
if (pct <= 55) return root.primary; if (pct <= 55) return root.text;
return root.mix(root.primary, root.accent, (pct - 55) / 45); return root.mix(root.text, root.accent, (pct - 55) / 45);
} }
// ----------------------------------------------------------------- fonts // ----------------------------------------------------------------- fonts
@@ -88,24 +110,22 @@ Singleton {
readonly property int barMargin: 8 readonly property int barMargin: 8
readonly property int barGap: 8 readonly property int barGap: 8
// Right-hand status rail and the floating bottom-left dock. // The right-hand rail uses the same lean and chamfer as the bar — a
// vertical panel leans its horizontal edges by a fraction of its width,
// which works out to the same 14 degrees.
readonly property int railWidth: 48 readonly property int railWidth: 48
readonly property int railGap: 10
readonly property int dockMargin: 14
// Low enough that the cluster stops competing with whatever window is
// underneath it — at 0.25 it just muddies text it happens to overlap. The
// corner wedge stays fully opaque as the thing you aim at.
readonly property real dockIdleOpacity: 0.13
readonly property int dockRevealZone: 140 // px of screen bottom that wakes it
// Horizontal offset per unit of height. tan(14deg) — every panel leans. // ONE angle, everywhere. tan(14deg). Persona geometry is disciplined, not
// random: every edge in the shell leans by exactly this much in exactly the
// same direction, and every corner is chopped by exactly `cut`. Varying
// these per module reads as noise, not as style.
readonly property real skew: 0.249 readonly property real skew: 0.249
readonly property int cut: 9 // corner chamfer, in px readonly property int cut: 10
readonly property int stroke: 2 readonly property int stroke: 2
// Pure-black keyline copy sitting behind every panel. This is what makes a // Pure-black keyline and offset copy behind panels. This is the one piece of
// shape read as screen-printed rather than as a clipped rectangle. // layering the style needs; it is not a soft shadow and never blurs.
readonly property real dropOffset: 4 readonly property real dropOffset: 3
readonly property real keylineWidth: 2 readonly property real keylineWidth: 2
readonly property int padS: 6 readonly property int padS: 6
@@ -116,24 +136,6 @@ Singleton {
readonly property int popoutWidth: 380 readonly property int popoutWidth: 380
readonly property int popoutGap: 10 readonly property int popoutGap: 10
// ------------------------------------------------------- shape variance
// Persona geometry is never a repeating pattern, so modules pull their lean
// and chamfer from these tables by index rather than sharing one default.
// Deterministic, so the bar looks the same every launch.
readonly property var leanTable: [0.32, 0.18, 0.26, 0.0, 0.29, 0.14, 0.35, 0.21]
readonly property var chopTable: [12, 5, 18, 8, 14, 4, 10, 16]
readonly property var cornerTable: [
[1, 3], [0, 2], [1], [0, 2, 3], [3], [1, 3], [2], [0, 1, 3]
]
function leanFor(i: int): real { return root.leanTable[i % root.leanTable.length]; }
function chopFor(i: int): real { return root.chopTable[i % root.chopTable.length]; }
function cornersFor(i: int): var { return root.cornerTable[i % root.cornerTable.length]; }
// Every other module leans against its neighbour, so the row reads as
// opposing diagonals instead of a comb.
function leanLeftFor(i: int): bool { return i % 3 === 1; }
// ------------------------------------------------------------ animation // ------------------------------------------------------------ animation
// P5 motion is snappy and slightly overshooting — never soft. // P5 motion is snappy and slightly overshooting — never soft.
readonly property int durFast: 110 readonly property int durFast: 110
@@ -150,5 +152,9 @@ Singleton {
readonly property url texStripes: "root:/assets/stripes.png" readonly property url texStripes: "root:/assets/stripes.png"
readonly property url texScanline: "root:/assets/scanline.png" readonly property url texScanline: "root:/assets/scanline.png"
readonly property url wallpaper: "file:///home/narl/.wallpapers/takemi_2.jpg" // Last resort only. The live wallpaper comes from the Wallpaper 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"
} }
+4
View File
@@ -15,10 +15,14 @@ WlSessionLock {
locked: Actions.lockRequested locked: Actions.lockRequested
WlSessionLockSurface { WlSessionLockSurface {
id: lockSurface
color: "black" color: "black"
LockFace { LockFace {
anchors.fill: parent anchors.fill: parent
// So each screen blurs its own wallpaper rather than all of them
// sharing whichever one hyprpaper happened to list first.
screenName: lockSurface.screen ? lockSurface.screen.name : ""
onUnlocked: Actions.lockRequested = false onUnlocked: Actions.lockRequested = false
} }
} }
+40 -30
View File
@@ -27,10 +27,17 @@ Item {
} }
// ------------------------------------------------------- background // ------------------------------------------------------- background
// Whatever hyprpaper is actually displaying, not a path written down once
// and left to rot. `screenName` lets a multi-monitor lock show each screen
// its own picture; it falls back to the first wallpaper hyprpaper reports.
property string screenName: ""
Image { Image {
id: wall id: wall
anchors.fill: parent anchors.fill: parent
source: Theme.wallpaper source: surface.screenName !== ""
? Wallpaper.forMonitor(surface.screenName)
: Wallpaper.source
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: false asynchronous: false
cache: true cache: true
@@ -45,7 +52,9 @@ Item {
} }
} }
// Teal wash, pulling the photo toward the shell's palette. // Wash pulling the photo down toward the shell's ground. Was described as
// a teal wash back when the UI was teal; the colours it actually uses are
// the neutral surfaces, which is what it should have been all along.
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
gradient: Gradient { gradient: Gradient {
@@ -55,35 +64,16 @@ Item {
} }
} }
Image { // 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.
P5Backdrop {
id: backdrop
anchors.fill: parent anchors.fill: parent
source: Theme.texHalftone reveal: 0
fillMode: Image.Tile wedgeSpan: 0.30
opacity: 0.05 wedgeOpacity: 0.30
smooth: false stripeOpacity: 0.07
layer.enabled: true
layer.effect: TintEffect { tintColor: Theme.primary }
}
// Crimson stripe band running behind the prompt — the Persona 5 title
// treatment. Declared before the auth block so it stays underneath it.
Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: authBlock.top
anchors.topMargin: -6
height: 42
clip: true
opacity: 0.5
Image {
anchors.fill: parent
source: Theme.texStripes
fillMode: Image.Tile
smooth: false
layer.enabled: true
layer.effect: TintEffect { tintColor: Theme.accent }
}
} }
// ------------------------------------------------------------ clock // ------------------------------------------------------------ clock
@@ -92,6 +82,20 @@ Item {
precision: SystemClock.Minutes precision: SystemClock.Minutes
} }
// The house mark, same wordmark the bar carries far left. Small, above the
// clock — the lock's hero is the time, not the branding.
SplitText {
id: mark
anchors.left: timeBlock.left
anchors.bottom: timeBlock.top
anchors.bottomMargin: 6
text: "P5"
pixelSize: Theme.fsTitle
split: 2
letterSpacing: 0
opacity: timeBlock.opacity
}
Column { Column {
id: timeBlock id: timeBlock
anchors.left: parent.left anchors.left: parent.left
@@ -403,6 +407,12 @@ Item {
ParallelAnimation { ParallelAnimation {
id: intro id: intro
// The stripes sweep in under everything, the same move the power menu
// opens with.
NumberAnimation {
target: backdrop; property: "reveal"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation { NumberAnimation {
target: timeBlock; property: "opacity" target: timeBlock; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
+24 -7
View File
@@ -67,7 +67,8 @@ PanelWindow {
readonly property color tone: { readonly property color tone: {
switch (root.kind) { switch (root.kind) {
case "mic": return Audio.micMuted ? Theme.accent : Theme.blue; case "mic": return Audio.micMuted ? Theme.accent : Theme.blue;
case "brightness": return Theme.warn; // Turning the screen up is not a warning. White, like volume.
case "brightness": return Theme.text;
default: return Audio.muted ? Theme.accent : Theme.primary; default: return Audio.muted ? Theme.accent : Theme.primary;
} }
} }
@@ -143,24 +144,40 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 4 spacing: 4
Row { // Title left, readout hard right. This used to be a Row with a
// `parent.width - 90` spacer, which assumed the two labels never
// needed more than 90px between them — "BRIGHTNESS" plus "100%"
// does, and the percentage fell off the right edge. Anchoring the
// readout to the right and letting the title elide into whatever
// is left makes the widest case the safe one.
Item {
width: parent.width width: parent.width
height: Math.max(titleLabel.implicitHeight, readout.implicitHeight)
Text { Text {
id: titleLabel
anchors.left: parent.left
anchors.right: readout.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: root.title.toUpperCase() text: root.title.toUpperCase()
color: Theme.subtext color: Theme.subtext
elide: Text.ElideRight
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.letterSpacing: 2 font.letterSpacing: 2
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Item {
width: parent.width - 90
height: 1
}
SplitText { SplitText {
id: readout
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
// Natural width, not a fixed one — SplitText offsets its
// ghosts with left/right anchor margins, so pinning the
// width and right-aligning the text would flatten the RGB
// split away. The title's left edge is anchored, so a
// widening readout only moves its own left edge.
text: Math.round(root.level * 100) + "%" text: Math.round(root.level * 100) + "%"
pixelSize: Theme.fsBody pixelSize: Theme.fsBody
split: 1 split: 1
+24 -69
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Shapes
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import "root:/config" import "root:/config"
@@ -48,46 +47,20 @@ PanelWindow {
} }
} }
// Diagonal stripe field sweeping in from the left. // Stripes and wedge, shared with the lock screen. The wedge used to lean at
Image { // 0.32 here, which was its own angle and nothing else's; P5Backdrop leans
id: stripes // it at Theme.skew like every other edge in the shell.
P5Backdrop {
id: ground
anchors.fill: parent anchors.fill: parent
source: Theme.texStripes reveal: 0
fillMode: Image.Tile stripeOpacity: 0.10
opacity: 0 wedgeOpacity: 0.35
smooth: false wedgeSpan: 0.34
// The live desktop is still visible behind this, so a dot wash would be
layer.enabled: true // laid over other people's windows rather than over a ground of our
layer.effect: TintEffect { // own. The lock owns the halftone; here it just muddies things.
tintColor: Theme.accent halftone: false
}
transform: Translate {
id: stripeShift
x: -root.width
}
}
// A crimson wedge behind the buttons.
Shape {
id: wedge
anchors.fill: parent
opacity: 0
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.alpha(Theme.accentDim, 0.35)
strokeColor: "transparent"
PathPolyline {
path: [
Qt.point(root.width * 0.18, 0),
Qt.point(root.width * 0.52, 0),
Qt.point(root.width * 0.34, root.height),
Qt.point(0, root.height),
Qt.point(root.width * 0.18, 0)
]
}
}
} }
// ---------------------------------------------------------------- title // ---------------------------------------------------------------- title
@@ -165,18 +138,10 @@ PanelWindow {
required property var modelData required property var modelData
required property int index required property int index
// Six identical slabs stacked at a constant width is the most readonly property int slabWidth: actions.stackWidth
// obviously machine-made thing in the shell. Vary the width and
// push each row a different distance off the right margin, so
// the stack has a ragged edge on both sides.
readonly property int slabWidth: [286, 252, 300, 268, 312, 244][entry.index]
readonly property int nudge: [0, 22, 8, 34, 14, 26][entry.index]
// Column owns y, so the ragged edge is an explicit x offset
// measured against the widest slab in the stack.
width: entry.slabWidth width: entry.slabWidth
height: 52 height: 52
x: actions.stackWidth - entry.slabWidth - entry.nudge
opacity: 0 opacity: 0
transform: [ transform: [
@@ -195,8 +160,8 @@ PanelWindow {
icon: entry.modelData.icon icon: entry.modelData.icon
destructive: entry.modelData.danger destructive: entry.modelData.danger
accent: entry.modelData.danger ? Theme.accent : Theme.primary accent: entry.modelData.danger ? Theme.accent : Theme.primary
lean: Theme.leanFor(entry.index) lean: Theme.skew
lunge: -10 - entry.index * 2 lunge: -10
onClicked: root.invoke(entry.modelData.act) onClicked: root.invoke(entry.modelData.act)
} }
@@ -225,10 +190,12 @@ PanelWindow {
} }
// Slabs land out of true and snap square. Sliding alone // Slabs land out of true and snap square. Sliding alone
// reads like a slide deck; the tilt is what makes it // reads like a slide deck; the tilt is what makes it
// land like a card being thrown down. // land like a card being thrown down. Same direction
// every time — the motion is the flourish, the geometry
// stays disciplined.
NumberAnimation { NumberAnimation {
target: tilt; property: "angle" target: tilt; property: "angle"
from: entry.index % 2 === 0 ? -4 : 4; to: 0 from: -3; to: 0
duration: Theme.durSlow duration: Theme.durSlow
easing.type: Easing.OutBack easing.type: Easing.OutBack
easing.overshoot: 2.2 easing.overshoot: 2.2
@@ -278,18 +245,10 @@ PanelWindow {
target: backdrop; property: "opacity" target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
} }
// One number now drives the slide, the stripe opacity and the wedge.
NumberAnimation { NumberAnimation {
target: stripes; property: "opacity" target: ground; property: "reveal"
to: 0.1; duration: Theme.durSlow from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: stripeShift; property: "x"
from: -root.width; to: 0
duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: wedge; property: "opacity"
to: 1; duration: Theme.durSlow; easing.type: Easing.OutExpo
} }
NumberAnimation { NumberAnimation {
target: title; property: "opacity" target: title; property: "opacity"
@@ -304,11 +263,7 @@ PanelWindow {
to: 0; duration: Theme.durFast to: 0; duration: Theme.durFast
} }
NumberAnimation { NumberAnimation {
target: stripes; property: "opacity" target: ground; property: "reveal"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: wedge; property: "opacity"
to: 0; duration: Theme.durFast to: 0; duration: Theme.durFast
} }
NumberAnimation { NumberAnimation {
+120
View File
@@ -0,0 +1,120 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "root:/config"
// The wallpaper currently on screen, per monitor.
//
// The lock screen used to point at a hardcoded path in Theme.qml, which meant
// it showed whatever picture was correct on the day that line was written —
// takemi_2 while hyprpaper had been switched to takemi_1. Anything that puts
// the wallpaper on screen should ask hyprpaper what it is actually displaying,
// which is what this does.
//
// hyprpaper is the source of truth at runtime; hyprpaper.conf is the fallback
// for the window between the shell starting and hyprpaper answering, and for
// the case where hyprpaper is not running at all.
Singleton {
id: root
// monitor name -> file path. Monitors hyprpaper set with a blank `monitor =`
// come back under the empty-string key.
property var byMonitor: ({})
// The wallpaper to use when no particular monitor is being asked about.
property string path: ""
readonly property url source: root.path !== ""
? Qt.resolvedUrl("file://" + root.path)
: Theme.fallbackWallpaper
// Path set from hyprpaper.conf, used until (and unless) hyprctl answers.
property string configPath: ""
signal changed()
function forMonitor(name: string): url {
const p = root.byMonitor[name];
return p ? Qt.resolvedUrl("file://" + p) : root.source;
}
function refresh() {
reader.running = true;
}
function _expand(p: string): string {
const s = p.trim();
if (s.startsWith("~/")) return Quickshell.env("HOME") + s.slice(1);
return s;
}
function _adopt(map: var, fallback: string) {
root.byMonitor = map;
// Prefer the focused monitor's wallpaper, then any monitor's, then the
// config. Object.values order is insertion order here, which is the
// order hyprpaper listed them in.
const vals = Object.keys(map).map(k => map[k]).filter(v => v !== "");
const next = vals.length > 0 ? vals[0] : fallback;
if (next !== root.path) {
root.path = next;
root.changed();
}
}
Process {
id: reader
command: ["hyprctl", "hyprpaper", "listactive"]
stdout: StdioCollector {
onStreamFinished: {
const map = {};
for (const line of text.split("\n")) {
// "eDP-1: /home/narl/.wallpapers/takemi_1.jpg". Split on the
// first colon only — the path may contain one.
const at = line.indexOf(":");
if (at === -1) continue;
const mon = line.slice(0, at).trim();
const file = line.slice(at + 1).trim();
if (file === "" || file.startsWith("invalid")) continue;
map[mon] = file;
}
root._adopt(map, root.configPath);
}
}
// hyprpaper down, or hyprctl missing. Fall back to the config file.
onExited: code => {
if (code !== 0 && root.path === "") root._adopt({}, root.configPath);
}
}
// hyprpaper has no change signal to subscribe to, so poll — but slowly. A
// wallpaper changes when a person decides to change it, not continuously.
Timer {
interval: 10000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refresh()
}
// Editing the config is the other way the wallpaper changes here, and that
// one we can watch properly.
FileView {
id: conf
path: Quickshell.env("HOME") + "/.config/hypr/hyprpaper.conf"
watchChanges: true
onFileChanged: reload()
onLoaded: {
const m = /^\s*path\s*=\s*(.+)$/m.exec(text());
if (m) root.configPath = root._expand(m[1]);
// A config edit is usually followed by a hyprpaper reload, so go
// ask what actually took effect rather than trusting the file.
root.refresh();
}
}
}
-6
View File
@@ -42,12 +42,6 @@ ShellRoot {
Rail {} Rail {}
} }
Variants {
model: Quickshell.screens
Dock {}
}
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens