Compare commits

3 Commits
Author SHA1 Message Date
nvrl 08146fe16f final theme 2026-08-11 02:05:46 +02:00
nvrl 105cf6b3a8 updated theme 2026-08-11 01:53:26 +02:00
nvrl 0a439d951e updated style 2026-08-11 00:22:59 +02:00
35 changed files with 1888 additions and 642 deletions
+9 -5
View File
@@ -12,7 +12,10 @@ WINIT_X11_SCALE_FACTOR = "1.0"
position = "None"
dynamic_padding = true
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
startup_mode = "Windowed"
dynamic_title = true
@@ -24,10 +27,11 @@ decorations_theme_variant = "None"
columns = 82
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]
x = 3
y = 0
x = 12
y = 8
## SCROLLING ------------------------------------------------------
[scrolling]
@@ -103,7 +107,7 @@ ipc_socket = true
## GENERAL --------------------------------------------------------
## Import additional configuration files.
import = ["~/.config/alacritty/colors.toml", "~/.config/alacritty/fonts.toml"]
import = ["~/.config/alacritty/takemi.toml", "~/.config/alacritty/fonts.toml"]
[[keyboard.bindings]]
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'
set -l last_status $status
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 vcs_color (set_color brpurple)
set -l vcs_color (set_color 6b6b70)
set -l prompt_status ""
# 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/catppuccin-fuzzel/themes/catppuccin-mocha/red.ini
include=~/.config/fuzzel/takemi.ini
[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
dpi-aware=no
@@ -12,17 +13,7 @@ horizontal-pad=20
vertical-pad=20
inner-pad=10
prompt=" "
prompt=" "
show-actions=no
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 {
monitor =
path = ~/.wallpapers/takemi_2.jpg
path = ~/.wallpapers/takemi_1.jpg
fit_mode = cover
}
+36 -22
View File
@@ -1,25 +1,39 @@
# 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.
$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)
#
# Crimson, black, white. Nothing else carries hue.
# ----------------------------------------------------------------- THE KNOB
# The one colour in this setup. Change this line and the window borders,
# group indicators and focus states all recolour with it.
#
# Keep it in step with `readonly property color accent` in
# ~/.config/quickshell/config/Theme.qml, which is what colours the shell.
# Those are the only two places the colour is written.
$accent = rgb(ff2d40)
$accentDim = rgb(a8121f)
$warn = rgb(ffb03a)
$ok = rgb(35e08f)
$accentDim = rgb(8f1826)
$accentSoft = rgb(ff6b78)
# ------------------------------------------------------------------ neutrals
$ink = rgb(000000)
$base = rgb(0a0a0c)
$mantle = rgb(0d0d10)
$surface = rgb(141418)
$surfaceAlt = rgb(1c1c22)
$overlay = rgb(26262e)
$outline = rgb(3a3a44)
$text = rgb(ffffff)
$subtext = rgb(c8c8ce)
$muted = rgb(6b6b70)
# Retired hues, kept as aliases so existing references resolve. The shell has
# no teal any more — the wallpaper is the colour in the composition.
$primary = $text
$primaryDim = rgb(9a9aa2)
$glow = $accent
$blue = $primaryDim
$deep = $surfaceAlt
$warn = $accentSoft
$ok = $text
+33 -36
View File
@@ -10,8 +10,9 @@ general {
border_size = 2
# Teal into crimson, on the diagonal — the shell's two voices.
col.active_border = $primary $accent 135deg
# Crimson into its own shadow, on the diagonal. The accent is the only hue
# here — a gradient into a second colour is what put blue in the border.
col.active_border = $accent $accentDim 135deg
col.inactive_border = $outline
resize_on_border = false
@@ -27,55 +28,51 @@ decoration {
# Persona 5 has no rounded corners. Neither do we.
rounding = 0
# Fully opaque, both states. Flat ink on flat ground.
active_opacity = 1.0
inactive_opacity = 0.96
inactive_opacity = 1.0
shadow {
enabled = true
range = 18
render_power = 3
offset = 0 4
color = rgba(04070aee)
color_inactive = rgba(04070a88)
color = rgba(000000ee)
color_inactive = rgba(00000088)
}
# https://wiki.hyprland.org/Configuring/Variables/#blur
#
# Off. Blur is the signature of the glass-and-frost look, which is the
# opposite of what this theme is doing — every surface here is opaque, flat
# and hard-edged. With nothing translucent left to blur it also costs three
# render passes for no visible result.
blur {
enabled = true
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
enabled = false
}
}
# 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 {
enabled = yes, please :)
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 = linear, 0, 0, 1, 1
animation = global, 1, 4, snap
animation = border, 1, 6, snap
animation = borderangle, 1, 60, linear, loop
animation = border, 1, 5, snap
animation = windows, 1, 4, overshoot
animation = windowsIn, 1, 4.2, overshoot, popin 88%
animation = windowsOut, 1, 2.4, hard, popin 90%
animation = windows, 1, 4, snap
animation = windowsIn, 1, 4, overshoot, popin 96%
animation = windowsOut, 1, 2.6, hard, popin 97%
animation = windowsMove, 1, 3.6, snap
animation = fade, 1, 3, snap
@@ -85,15 +82,15 @@ animations {
animation = fadeShadow, 1, 3, snap
animation = fadeDim, 1, 2.4, snap
animation = layers, 1, 3, overshoot
animation = layersIn, 1, 3.4, overshoot, slide
animation = layers, 1, 3, snap
animation = layersIn, 1, 3, snap, slide
animation = layersOut, 1, 2, hard, slide
animation = workspaces, 1, 3.4, overshoot, slidevert
animation = workspacesIn, 1, 3.4, overshoot, slidevert
animation = workspacesOut, 1, 2.6, hard, slidevert
animation = workspaces, 1, 3, snap, slidevert
animation = workspacesIn, 1, 3, snap, 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
@@ -117,7 +114,7 @@ misc {
}
group {
col.border_active = $primary $accent 135deg
col.border_active = $accent $accentDim 135deg
col.border_inactive = $outline
groupbar {
@@ -128,8 +125,8 @@ group {
gradients = false
# --- Active Tab ---
col.active = $primary
text_color = $base
col.active = $accent
text_color = $ink
# --- Inactive Tab ---
col.inactive = $surface
+135 -28
View File
@@ -1,9 +1,9 @@
# Takemi
A Persona 5 flavoured Quickshell desktop for Hyprland. Cold teal and near-black
pulled from `~/.wallpapers/takemi_2.jpg`, with crimson as the accent voice.
Everything leans, corners are chopped rather than rounded, and headings carry
the wallpaper's RGB split.
A Persona 5 flavoured Quickshell desktop for Hyprland. Crimson, black and white,
with no gradients anywhere. Everything leans by the same 14°, corners are
chopped rather than rounded, and every surface is one continuous slab divided by
hairlines rather than a cluster of floating cards.
Replaces Waybar, dunst and hyprlock.
@@ -18,13 +18,39 @@ Hyprland starts it from `~/.config/hypr/modules/autostart.conf`.
## Layout
Two surfaces. Status moves off the bar and onto a vertical rail, because on a
1080p panel horizontal space is cheap and vertical space is not: a 48px rail
costs 2.5% of the width, where another 48px strip along the bottom would have
cost 4.4% of the height on top of what the bar already takes.
```
shell.qml entrypoint — one bar, power menu, toasts and OSD per screen
┌────────────────────────────────────────────┐
│ ◆ │ 1 2 3 │ window title ♪ TRACK │ 42% │ 00:09 │ bar — 48px exclusive
├────────────────────────────────────────────┤
│ ▐█▌│ rail — 48px exclusive
│ windows ▐ ▌│
│ ▐ ▌│
│ ▐ ▌│
└────────────────────────────────────────────┘
```
- **bar** — one slab: identity and workspaces on the left; the focused window,
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.
Each surface is a single continuous `P5Panel` with modules divided by sheared
hairlines. `BarPill` is chromeless by default — a module is *content inside* a
surface, not a surface of its own — and marks hover with a crimson underline.
Pass `standalone: true` for the rare thing that really is its own panel.
```
shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves
components/ the P5 primitives: panels, gauges, sliders, split-ink text
services/ data sources, all singletons
bar/ the bar and its modules
popouts/ hover panels hanging off bar modules
bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar modules
overlays/ power menu, notification toasts, volume/brightness OSD
lock/ session lock
assets/ halftone and stripe tiles, generated with ImageMagick
@@ -34,24 +60,24 @@ assets/ halftone and stripe tiles, generated with ImageMagick
callers set implicit sizes from their own layout (`row.implicitWidth +
contentPad * 2`), which keeps the shape maths out of binding loops.
## Bar
## Modules
| Module | Left click | Right click | Middle | Scroll | Hover |
|---|---|---|---|---|---|
| Sigil | app menu | reload shell | | | |
| Workspaces | switch | | | | |
| CPU/MEM/GPU | btop | | | | resource dashboard |
| Media | play/pause | next | raise player | next/prev | full player |
| Clock | pin calendar | | | | calendar |
| Tray | activate | app menu | secondary | | |
| Network | pin panel | toggle Wi-Fi | nm-connection-editor | | networks |
| Bluetooth | pin panel | toggle adapter | | | devices |
| Volume | mute | pin panel | pavucontrol | volume | audio mixer |
| Mic | mute | pin panel | | mic volume | audio mixer |
| Brightness | | | | brightness | |
| Battery | pin panel | | | | power detail |
| Bell | history + mark read | do not disturb | | | history |
| Power | power menu | | | | |
| Module | Where | Left click | Right click | Middle | Scroll | Hover |
|---|---|---|---|---|---|---|
| Sigil | bar | app menu | reload shell | | | |
| Workspaces | bar | switch | | | | |
| Clock | bar | pin calendar | | | | calendar |
| Network | rail | pin panel | toggle Wi-Fi | nm-connection-editor | | networks |
| Bluetooth | rail | pin panel | toggle adapter | | | devices |
| Volume | rail | mute | pin panel | pavucontrol | volume | audio mixer |
| Mic | rail | mute | pin panel | | mic volume | audio mixer |
| Brightness | rail | | | | brightness | |
| Battery | rail | pin panel | | | | power detail |
| Tray | rail | activate | app menu | secondary | | |
| Bell | rail | history + mark read | do not disturb | | | history |
| Power | rail | power menu | | | | |
| Media | bar | play/pause | next | raise player | next/prev | full player |
| CPU/MEM/GPU | bar | btop | | | | resource dashboard |
Popouts open on a short dwell and stay while the pointer is over the trigger or
the panel. Clicking pins one open.
@@ -112,7 +138,88 @@ means restarting Hyprland from a TTY.
`power-profiles-daemon` is not installed (TLP is), so the power-profile
switching in `services/Battery.qml` stays dormant. Nothing else depends on it.
The display face is Adwaita Sans Black Italic, which is what was already on the
box. A heavy condensed grotesque — Anton, Archivo Black, Oswald — would land
closer to the real Persona 5 type. Change `Theme.fontDisplay` and everything
follows.
The display face is Archivo Black, set italic almost everywhere. Change
`Theme.fontDisplay` and everything follows.
## Palette discipline
Crimson, black, white. That is the whole palette.
**Changing the accent.** The colour is written in exactly three places, one per
program:
| 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 -83
View File
@@ -5,12 +5,13 @@ import "root:/config"
import "root:/components"
import "root:/services"
// One bar per monitor.
// One bar per monitor, and one surface.
//
// Layout is three clusters: identity and system state on the left, the clock
// and player dead centre, controls on the right. The centre is genuinely
// centred, so the left cluster's window title is capped at whatever space is
// actually left over — otherwise the clusters overlap on a 1080p panel.
// Identity and workspaces on the left, the player, metrics and clock on the
// right, all inside a single continuous slab and separated by hairline rules
// rather than by gaps. Nothing floats and nothing has its own card. Status
// lives on the rail, which is likewise one slab — two surfaces, not a dozen
// islands.
PanelWindow {
id: root
@@ -36,34 +37,47 @@ PanelWindow {
exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent"
// Half the space not taken by the centre cluster, minus breathing room.
readonly property real sideBudget:
(width - centreCluster.width) / 2 - Theme.barGap * 2
// 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
// No offset copy on the big slabs. The 3px black silhouette behind the
// fill is a print-misregistration trick, and at this scale it stops
// reading as one: on a 1900px bar you only ever see it at the chopped
// corners, where it turns the single clean cut into a staggered double
// edge that looks like a rendering fault. The black keyline still
// carries the heavy outer contour, which is what the corner needs.
drop: false
}
readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM
// ------------------------------------------------------------------ left
Row {
id: leftCluster
id: leftGroup
anchors.left: parent.left
anchors.leftMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.barGap
spacing: 0
Sigil {
id: sigil
screenRef: root.modelData
}
BarPill {
id: spacesPill
accent: Theme.primary
padding: Theme.padS
interactive: false
implicitWidth: spacesRow.implicitWidth + contentPad * 2
Row {
id: spacesRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
Divider {}
Workspaces {
id: spaces
@@ -71,89 +85,59 @@ PanelWindow {
screenRef: root.modelData
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 18
color: Theme.alpha(Theme.outline, 0.8)
}
Divider {}
ActiveWindow {
anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData
// Everything else on this side is fixed width, so the title
// gets whatever remains.
maxWidth: Math.max(70, root.sideBudget
- sigil.implicitWidth
- resources.implicitWidth
- spaces.implicitWidth
- Theme.barGap * 2
- Theme.padM * 3
- spacesPill.leanInset)
}
}
}
Resources {
id: resources
screenRef: root.modelData
maxWidth: Math.max(120, root.width
- leftGroup.x - rightGroup.width - sigil.implicitWidth
- spaces.implicitWidth - root.edgeInset * 2 - Theme.padXL)
}
}
// ---------------------------------------------------------------- centre
// ----------------------------------------------------------------- right
Row {
id: centreCluster
anchors.horizontalCenter: parent.horizontalCenter
id: rightGroup
anchors.right: parent.right
anchors.rightMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.barGap
spacing: 0
MediaPill {
screenRef: root.modelData
}
Divider {}
Resources {
screenRef: root.modelData
}
Divider {}
Clock {
screenRef: root.modelData
}
}
// ----------------------------------------------------------------- right
Row {
id: rightCluster
anchors.right: parent.right
// 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
spacing: Theme.barGap
implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight
Tray {
screenRef: root.modelData
}
Status {
screenRef: root.modelData
}
NotifButton {
screenRef: root.modelData
}
PowerButton {
screenRef: root.modelData
}
}
// A hairline of crimson under the whole bar — the Persona 5 underscore.
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 0.18; color: Theme.alpha(Theme.accent, 0.55) }
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" }
anchors.centerIn: parent
width: 1
height: 20
color: Theme.alpha(Theme.outline, 0.9)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
}
}
+67 -26
View File
@@ -2,25 +2,49 @@ import QtQuick
import "root:/config"
import "root:/components"
// Shared chrome for every bar module: a leaning slab that lifts and lights up
// under the pointer, and forwards clicks and scrolls to the widget.
// Shared chrome for every bar module.
//
// The bar is one continuous slab, so by default a module draws no panel of its
// own — it is content inside the bar's surface, and marks hover with a crimson
// 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 {
id: root
property color accent: Theme.primary
property color accent: Theme.accent
property bool active: false
property bool slash: false
property real lean: Theme.skew
property bool leanLeft: false
property real padding: Theme.padM
property bool interactive: true
property bool halftone: false
property bool standalone: false
property real chop: Theme.cut
property var chopCorners: [1, 3]
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.
readonly property real contentPad: panel.contentPad
readonly property alias panelItem: panel
// The hover rule's own strip, reserved at the bottom of the module.
//
// 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
@@ -31,46 +55,63 @@ Item {
implicitHeight: Theme.barHeight
P5Panel {
id: panel
Loader {
id: panelLoader
anchors.fill: parent
active: root.standalone
sourceComponent: P5Panel {
lean: root.lean
leanLeft: root.leanLeft
chop: root.chop
chopCorners: root.chopCorners
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
fillOpacity: 1.0
border: root.hovered || root.active
? Theme.alpha(root.accent, 0.9)
: Theme.alpha(Theme.outline, 0.7)
? root.accent
: Theme.alpha(Theme.outline, 0.9)
borderWidth: root.hovered || root.active ? Theme.stroke : 1
slash: root.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
}
anchors.leftMargin: root.contentPad
anchors.rightMargin: root.contentPad
anchors.bottomMargin: root.ruleBand
}
// Lift toward the pointer. Subtle, but it makes the bar feel alive.
transform: Translate {
y: root.hovered && root.interactive ? -2 : 0
// Hover mark for in-bar modules: a crimson rule under the content, wiping
// out from the leading edge. Sheared to match the one angle everything uses.
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 {
NumberAnimation {
duration: Theme.durBase
easing.type: Easing.OutBack
Rectangle {
height: parent.height
width: root.hovered || root.active ? parent.width : 0
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 }
}
}
}
+231
View File
@@ -0,0 +1,231 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// The right-hand status rail — one slab, not a stack of cards.
//
// Connectivity, audio, power, tray and notifications all sit inside a single
// continuous column, divided by hairline rules the same way the bar divides its
// modules. Same lean, same chamfer, same keyline as the bar and the dock, so
// the three surfaces read as one system.
//
// On a 1080p panel this is the cheap edge: 48px of width is 2.5% of the screen,
// where the same 48px of height would be 4.4% on top of what the bar takes.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "takemi-rail"
anchors {
top: true
bottom: true
right: true
}
margins {
top: Theme.barHeight + Theme.barMargin * 2
bottom: Theme.barMargin
right: Theme.barMargin
}
implicitWidth: Theme.railWidth
exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent"
// The slab.
P5Panel {
id: slab
anchors.fill: parent
vertical: true
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
// Same as the bar — see the note there. The doubled corner is worse on
// the rail, because its chopped corners sit against the wallpaper's
// high-contrast stripes rather than against other chrome.
drop: false
}
readonly property real edgeInset: Theme.railWidth * Theme.skew + Theme.padM
// ------------------------------------------------------------------- top
Column {
id: upper
anchors.top: parent.top
anchors.topMargin: root.edgeInset
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
RailIcon {
id: netIcon
icon: Net.icon
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
RailIcon {
id: btIcon
icon: Bt.icon
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
Divider {}
RailIcon {
id: volIcon
icon: Audio.icon
readout: Audio.muted ? "" : Audio.volumePercent + "%"
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
RailIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
RailIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
readout: Math.round(Brightness.percent) + "%"
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
Divider { visible: Battery.available }
RailIcon {
id: battIcon
visible: Battery.available
icon: Battery.icon
readout: Math.round(Battery.percent) + "%"
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
// ---------------------------------------------------------------- bottom
Column {
id: lower
anchors.bottom: parent.bottom
anchors.bottomMargin: root.edgeInset
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
Tray {
id: tray
anchors.horizontalCenter: parent.horizontalCenter
}
Divider { visible: tray.visible }
RailIcon {
id: bellIcon
icon: Notifs.icon
readout: Notifs.unread > 0
? (Notifs.unread > 9 ? "9+" : String(Notifs.unread))
: ""
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
onClicked: {
notifPopout.pinned = !notifPopout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
}
RailIcon {
id: powerIcon
icon: "󰐥"
alert: Actions.powerMenuOpen
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)
}
}
}
// ------------------------------------------------------------- popouts
// All of these hang off the left edge of the rail, opening into the screen.
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
fromEdge: Edges.Left
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
fromEdge: Edges.Left
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
fromEdge: Edges.Left
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
fromEdge: Edges.Left
visible: Battery.available && shown
}
NotifPopout {
id: notifPopout
anchorItem: bellIcon
triggerHovered: bellIcon.hovered
fromEdge: Edges.Left
}
}
+125
View File
@@ -0,0 +1,125 @@
import QtQuick
import "root:/config"
import "root:/components"
// One slot on the right-hand rail: a glyph with an optional micro readout
// stacked beneath it, and a crimson bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits
// under the glyph rather than beside it, and anything longer than a percentage
// belongs in the popout instead.
Item {
id: root
property string icon: ""
property string readout: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
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 rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: Theme.railWidth
implicitHeight: root.readout !== "" ? 42 : 32
Column {
id: layout
anchors.centerIn: parent
spacing: 1
Icon {
id: glyph
anchors.horizontalCenter: parent.horizontalCenter
text: root.icon
pulsing: root.pulsing
font.pixelSize: Theme.fsLarge
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.readout !== ""
text: root.readout
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
// Leading-edge wipe. On the rail this runs vertically down the left side,
// 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 {
anchors.left: parent.left
anchors.leftMargin: root.wipeInset
anchors.verticalCenter: parent.verticalCenter
width: 2
height: root.hovered ? layout.implicitHeight : 0
color: Theme.glow
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.08 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+12 -20
View File
@@ -13,7 +13,8 @@ BarPill {
required property var screenRef
accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
padding: Theme.padS
padding: Theme.padM
implicitWidth: layout.implicitWidth + contentPad * 2
onClicked: Actions.openSystemMonitor()
@@ -59,6 +60,10 @@ BarPill {
}
}
// The CPU/MEM/GPU labels are gone: three characters of chrome per metric,
// repeated three times, in the most cramped part of the shell. The number
// over its meter is legible on its own, and dropping them buys the room to
// set the figure in display italic instead of squeezing it into mono.
component Metric: Column {
id: metric
@@ -67,35 +72,22 @@ BarPill {
property string readout: ""
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Row {
spacing: 4
spacing: 2
Text {
anchors.verticalCenter: parent.verticalCenter
text: metric.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: metric.readout
color: Theme.heat(metric.value)
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
MeterBar {
value: metric.value
+13 -14
View File
@@ -20,26 +20,25 @@ BarPill {
onClicked: Actions.launch("fuzzel")
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
anchors.centerIn: parent
text: "󰋠"
font.pixelSize: Theme.fsTitle
text: "P5"
pixelSize: Theme.fsLarge
letterSpacing: 0
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 {
target: root
function onClicked() { spin.restart(); }
}
SequentialAnimation {
id: spin
NumberAnimation {
target: mark; property: "rotation"
from: 0; to: 360; duration: Theme.durSlow
easing.type: Easing.OutExpo
}
function onClicked() { mark.glitch(); }
}
}
+3 -1
View File
@@ -71,7 +71,9 @@ BarPill {
icon: Brightness.icon
label: Math.round(Brightness.percent) + "%"
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)
}
+30 -20
View File
@@ -1,28 +1,20 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "root:/config"
import "root:/components"
// StatusNotifier tray. Left click activates, right click opens the app's own
// menu, middle click is the secondary action.
BarPill {
// StatusNotifier tray, as a vertical column for the right-hand rail. Left click
// activates, right click opens the app's own menu, middle click is the
// secondary action. The surrounding slab is the caller's business — this is
// just the stack of icons.
Column {
id: root
required property var screenRef
accent: Theme.blue
padding: Theme.padS
interactive: false
spacing: Theme.padS + 2
visible: SystemTray.items.values.length > 0
implicitWidth: visible ? layout.implicitWidth + contentPad * 2 : 0
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
Repeater {
model: SystemTray.items
@@ -34,13 +26,31 @@ BarPill {
width: 18
height: 18
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
// App icons arrive in their own brand colours, which wrecks the
// two-hue palette faster than anything else in the shell. Flatten
// every one of them to a single-colour silhouette: white at rest,
// crimson under the pointer. Detail inside the glyph is lost on
// purpose — that flattening *is* the screen-printed look.
IconImage {
id: img
anchors.fill: parent
source: entry.modelData.icon
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: itemHover.hovered
? Theme.accent
: Theme.text
Behavior on colorizationColor {
ColorAnimation { duration: Theme.durFast }
}
}
opacity: itemHover.hovered ? 1.0 : 0.82
Behavior on opacity {
@@ -79,14 +89,14 @@ BarPill {
onTapped: entry.modelData.secondaryActivate()
}
// Menus open to the left, into the screen rather than off its edge.
QsMenuAnchor {
id: menuAnchor
menu: entry.modelData.menu
anchor.item: entry
anchor.edges: Edges.Bottom
anchor.gravity: Edges.Bottom
anchor.margins.top: Theme.popoutGap
}
anchor.edges: Edges.Left
anchor.gravity: Edges.Left
anchor.margins.right: Theme.popoutGap
}
}
}
+20 -9
View File
@@ -40,11 +40,12 @@ Item {
}
implicitWidth: row.implicitWidth
implicitHeight: 20
implicitHeight: 24
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: 6
Repeater {
@@ -54,6 +55,7 @@ Item {
id: pip
required property var modelData
required property int index
readonly property int wsId: modelData
readonly property var info: root.occupancy[wsId]
readonly property bool occupied: info !== undefined && info.count > 0
@@ -61,7 +63,9 @@ Item {
readonly property bool focused: root.focusedId === wsId
readonly property bool hovered: pipHover.hovered
width: focused ? 34 : (occupied ? 16 : 10)
// Every tick is the same height and the same shear. Only the
// width changes, and only to say which workspace is focused.
width: focused ? 32 : (occupied ? 16 : 9)
height: 18
Behavior on width {
@@ -75,13 +79,20 @@ Item {
Rectangle {
id: tick
anchors.fill: parent
// Focused is a solid crimson block. Occupied is a white
// block at half weight. Empty is an outline. Three states,
// two colours, no ambiguity.
color: {
if (pip.urgent) return Theme.accent;
if (pip.focused) return Theme.primary;
if (pip.hovered) return Theme.glow;
if (pip.occupied) return Theme.alpha(Theme.primary, 0.55);
return Theme.alpha(Theme.outline, 0.75);
if (pip.focused || pip.urgent) return Theme.accent;
if (pip.hovered) return Theme.text;
if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
return "transparent";
}
border.color: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
border.width: 1
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
@@ -102,9 +113,9 @@ Item {
opacity: pip.focused ? 1 : 0
visible: opacity > 0.01
text: pip.wsId
color: Theme.base
color: Theme.ink
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
+23
View File
@@ -31,6 +31,29 @@ Singleton {
];
}
// Vertical counterpart: the *horizontal* edges slant instead, by a fraction
// of width. A tall rail slab has to lean this way — leaning its vertical
// edges by a fraction of its height would shear it clean off the screen.
function parallelogramV(w: real, h: real, lean: real): var {
const off = w * lean;
return [
Qt.point(0, off),
Qt.point(w, 0),
Qt.point(w, h - off),
Qt.point(0, h)
];
}
function parallelogramVL(w: real, h: real, lean: real): var {
const off = w * lean;
return [
Qt.point(0, 0),
Qt.point(w, off),
Qt.point(w, h),
Qt.point(0, h - off)
];
}
function rect(w: real, h: real): var {
return [Qt.point(0, 0), Qt.point(w, 0), Qt.point(w, h), Qt.point(0, h)];
}
+91
View File
@@ -0,0 +1,91 @@
import QtQuick
import "root:/config"
import "root:/components"
// One metric in a popout: a label and its reading on a line, with a wide
// sheared segmented meter underneath.
//
// This replaces the radial Gauge. A 270° dial was the only curved thing in a
// shell whose whole geometry is sheared slabs and hard chamfers, so it read as
// though it had been imported from another design. The meter says the same
// thing in the language the bar and the OSD already speak, and it gets far more
// resolution across a popout's width than a 88px dial did.
Item {
id: root
property string label: ""
property real value: 0 // 0..100
property string readout: Math.round(root.value) + "%"
property string sub: ""
property color tone: Theme.heat(root.value)
property int segments: 24
property real segmentSpacing: 3
implicitHeight: head.height + Theme.padS + meter.height
Item {
id: head
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: Math.max(name.implicitHeight, figure.implicitHeight)
Text {
id: name
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.label.toUpperCase()
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
// The secondary figure — temperature, used-of-total. Sits next to the
// label rather than under the number so the right edge stays a single
// clean column of readings.
Text {
anchors.left: name.right
anchors.leftMargin: Theme.padM
anchors.right: figure.left
anchors.rightMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
visible: root.sub !== ""
text: root.sub
color: Theme.muted
elide: Text.ElideRight
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
SplitText {
// Not `value` — an id by that name shadows the `value` property, and
// the defaults above then round a Text item instead of a number.
id: figure
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: root.readout
pixelSize: Theme.fsBody
color: Theme.text
split: 1
splitOpacity: 0.5
}
}
MeterBar {
id: meter
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 12
segments: root.segments
spacing: root.segmentSpacing
// Fill the row exactly rather than leaving a ragged tail on the right.
segmentWidth: (width - (root.segments - 1) * root.segmentSpacing) / root.segments
value: root.value
activeColor: root.tone
}
}
+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 }
}
}
+139 -56
View File
@@ -18,6 +18,11 @@ Item {
property real lean: Theme.skew
property bool leanLeft: false
// Tall panels (the right-hand rail) slant their horizontal edges instead,
// by a fraction of width, and inset their content top/bottom rather than
// left/right.
property bool vertical: false
// Corner chop size, and which corners get chopped (indices into the
// polygon, clockwise from the top-left). Default chops the two corners that
// read as "cut" against the lean.
@@ -34,13 +39,30 @@ Item {
property color halftoneColor: Theme.primary
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
readonly property real leanInset: height * Math.abs(lean)
readonly property var polygon: leanLeft
? Geom.parallelogramL(width, height, lean)
: Geom.parallelogram(width, height, lean)
// The screen-print stack. A pure-black copy of the silhouette sits behind
// the fill, offset down-and-right, and the fill carries a black keyline
// outside its coloured border. Together these are what stop the panel from
// reading as a flat clip-path.
property bool drop: true
property real dropOffset: Theme.dropOffset
property color dropColor: Theme.ink
property bool keyline: true
property real keylineWidth: Theme.keylineWidth
readonly property real leanInset:
(vertical ? width : height) * Math.abs(lean)
readonly property var polygon: vertical
? (leanLeft ? Geom.parallelogramVL(width, height, lean)
: Geom.parallelogramV(width, height, lean))
: (leanLeft ? Geom.parallelogramL(width, height, lean)
: Geom.parallelogram(width, height, lean))
// Child content lives inside `contentItem`, inset on both sides far enough
// to clear the slanted edges. Panels do not auto-size: callers set implicit
@@ -52,14 +74,79 @@ Item {
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.
Loader {
anchors.fill: parent
active: root.drop && root.dropOffset > 0
sourceComponent: Shape {
preferredRendererType: Shape.CurveRenderer
asynchronous: false
transform: Translate {
x: root.dropOffset * (root.leanLeft ? -1 : 1)
y: root.dropOffset
}
ShapePath {
fillColor: root.dropColor
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
}
Shape {
id: body
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: false
// Fill, carrying the black keyline. The stroke is centred on the path,
// so half of it sits outside the silhouette — which is exactly the
// heavy outer contour the style wants.
ShapePath {
fillColor: Theme.alpha(root.fill, root.fillOpacity)
strokeColor: root.keyline ? root.dropColor : "transparent"
strokeWidth: root.keyline
? root.borderWidth + root.keylineWidth * 2
: 0
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
// Coloured border, drawn on top of the keyline so it reads as an inlay.
ShapePath {
fillColor: "transparent"
strokeColor: root.borderWidth > 0 ? root.border : "transparent"
strokeWidth: root.borderWidth
joinStyle: ShapePath.MiterJoin
@@ -76,24 +163,7 @@ Item {
anchors.fill: parent
active: root.halftone
asynchronous: true
sourceComponent: Item {
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 {
sourceComponent: Image {
anchors.fill: parent
source: Theme.texHalftone
fillMode: Image.Tile
@@ -104,62 +174,75 @@ Item {
colorization: 1.0
colorizationColor: root.halftoneColor
maskEnabled: true
maskSource: maskShape
}
}
}
}
// 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)
}
maskSource: silhouette
}
}
}
// 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 {
anchors.fill: parent
active: root.slash
sourceComponent: Shape {
sourceComponent: Item {
Shape {
id: slashShape
anchors.fill: parent
visible: false
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: root.slashColor
strokeColor: "transparent"
PathPolyline {
path: {
const off = root.height * root.lean;
const w = root.slashWidth;
const t = root.slashWidth;
// On a rail slab the slash runs along the slanted
// top edge instead of down a side.
if (root.vertical) {
const voff = root.width * root.lean;
return root.leanLeft
? Geom.closed([Qt.point(0, 0), Qt.point(w, 0),
Qt.point(off + w, root.height), Qt.point(off, root.height)])
: Geom.closed([Qt.point(off, 0), Qt.point(off + w, 0),
Qt.point(w, root.height), Qt.point(0, root.height)]);
? Geom.closed([Qt.point(0, 0), Qt.point(root.width, voff),
Qt.point(root.width, voff + t), Qt.point(0, t)])
: Geom.closed([Qt.point(0, voff), Qt.point(root.width, 0),
Qt.point(root.width, t), Qt.point(0, voff + t)]);
}
const off = root.height * root.lean;
return root.leanLeft
? Geom.closed([Qt.point(0, 0), Qt.point(t, 0),
Qt.point(off + t, root.height), Qt.point(off, root.height)])
: Geom.closed([Qt.point(off, 0), Qt.point(off + t, 0),
Qt.point(t, root.height), Qt.point(0, root.height)]);
}
}
}
}
MultiEffect {
anchors.fill: parent
source: slashShape
maskEnabled: true
maskSource: silhouette
}
}
}
Item {
id: contentHolder
anchors.fill: parent
anchors.leftMargin: root.contentPad
anchors.rightMargin: root.contentPad
anchors.topMargin: root.padding
anchors.bottomMargin: root.padding
anchors.leftMargin: root.vertical ? root.padding : root.contentPad
anchors.rightMargin: root.vertical ? root.padding : root.contentPad
anchors.topMargin: root.vertical ? root.contentPad : root.padding
anchors.bottomMargin: root.vertical ? root.contentPad : root.padding
}
}
+28 -6
View File
@@ -27,12 +27,25 @@ PopupWindow {
signal dismissedFully()
// Panels hanging off the vertical rail open sideways, so the entry slide has
// to follow whichever axis the popout actually travels along, and start on
// the side nearest its trigger.
readonly property bool horizontal:
fromEdge === Edges.Left || fromEdge === Edges.Right
readonly property string slideProperty: horizontal ? "x" : "y"
readonly property real slideOrigin: bleed + (fromEdge === Edges.Left
|| fromEdge === Edges.Top ? 14 : -14)
anchor.item: anchorItem
anchor.edges: fromEdge
anchor.gravity: fromEdge
anchor.adjustment: PopupAdjustment.SlideX
anchor.adjustment: horizontal
? PopupAdjustment.SlideY
: PopupAdjustment.SlideX
anchor.margins.top: Theme.popoutGap
anchor.margins.bottom: Theme.popoutGap
anchor.margins.left: Theme.popoutGap
anchor.margins.right: Theme.popoutGap
implicitWidth: contentWidth + bleed * 2
implicitHeight: contentHeight + bleed * 2
@@ -83,7 +96,14 @@ PopupWindow {
height: root.contentHeight
opacity: 0
transformOrigin: Item.Top
transformOrigin: {
switch (root.fromEdge) {
case Edges.Left: return Item.Right;
case Edges.Right: return Item.Left;
case Edges.Top: return Item.Bottom;
default: return Item.Top;
}
}
scale: 0.94
// The handler has to live on the content item, not the window: a
@@ -105,8 +125,8 @@ PopupWindow {
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
}
NumberAnimation {
target: contentHolder; property: "y"
from: root.bleed - 14; to: root.bleed
target: contentHolder; property: root.slideProperty
from: root.slideOrigin; to: root.bleed
duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
@@ -122,12 +142,14 @@ PopupWindow {
to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad
}
NumberAnimation {
target: contentHolder; property: "y"
to: root.bleed - 8; duration: Theme.durFast; easing.type: Easing.InQuad
target: contentHolder; property: root.slideProperty
to: root.bleed + (root.slideOrigin - root.bleed) * 0.6
duration: Theme.durFast; easing.type: Easing.InQuad
}
onFinished: {
root.shown = false;
root.pinned = false;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed;
root.dismissedFully();
}
+90 -31
View File
@@ -3,48 +3,90 @@ pragma Singleton
import QtQuick
import Quickshell
// Takemi palette: cold teal/cyan over near-black, with Persona 5 crimson as the
// alert/accent voice. Derived from ~/.wallpapers/takemi_2.jpg.
// Takemi palette: crimson, black, white. Nothing else.
//
// White does the work: text, icons, idle indicators. Crimson is the only accent
// and it always means the same thing — active, or wrong. Black is structure,
// used as a hard keyline rather than as a shadow. There are no gradients in
// this shell; every fill is flat.
//
// The 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 {
id: root
// ---------------------------------------------------------------- colors
readonly property color base: "#04070a"
readonly property color mantle: "#080e14"
readonly property color surface: "#0d1922"
readonly property color surfaceAlt: "#14262f"
readonly property color overlay: "#1d3a47"
readonly property color outline: "#2b5567"
readonly property color ink: "#000000"
readonly property color text: "#e2f4f8"
readonly property color subtext: "#93b8c4"
readonly property color muted: "#587c8a"
readonly property color base: "#0a0a0c"
readonly property color mantle: "#0d0d10"
readonly property color surface: "#141418"
readonly property color surfaceAlt: "#1c1c22"
readonly property color overlay: "#26262e"
readonly property color outline: "#3a3a44"
readonly property color primary: "#34d3e6" // electric teal — the house colour
readonly property color primaryDim: "#17879b"
readonly property color glow: "#8ef2ff"
readonly property color blue: "#3b8fd6"
readonly property color deep: "#0d4b5e"
readonly property color text: "#ffffff"
readonly property color subtext: "#c8c8ce"
readonly property color muted: "#6b6b70"
readonly property color accent: "#ff2d40" // P5 crimson
readonly property color accentDim: "#a8121f"
readonly property color warn: "#ffb03a"
readonly property color ok: "#35e08f"
// ------------------------------------------------------------ THE KNOB
// The one colour in this shell. Change this line and the entire accent
// recolours — everything below is derived from it, and nothing else in the
// palette carries hue at all.
//
// 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"
// Chromatic aberration pair, straight off the wallpaper's RGB split.
readonly property color splitRed: "#ff2b4d"
readonly property color splitCyan: "#25e8ff"
readonly property color accentDim: Qt.darker(root.accent, 1.9)
readonly property color accentSoft: Qt.lighter(root.accent, 1.35)
// `primary` is the resting voice of the UI, which is now simply white.
// Kept under the old name so the existing call sites don't all have to move.
readonly property color primary: root.text
readonly property color primaryDim: "#9a9aa2"
readonly property color 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 ok: root.text
readonly property color warn: root.accent
// Display text carries a crimson and a black offset rather than an RGB
// split. Same trick, correct palette.
readonly property color splitRed: root.accent
readonly property color splitCyan: root.ink
function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a);
}
// Heat ramp for load/usage readouts.
function mix(a: color, b: color, t: real): color {
const k = Math.max(0, Math.min(1, t));
return Qt.rgba(a.r + (b.r - a.r) * k,
a.g + (b.g - a.g) * k,
a.b + (b.b - a.b) * k,
a.a + (b.a - a.a) * k);
}
// Load readouts sit white until the number actually matters, then bleed to
// crimson. Not a hue cycle — red always means the same thing.
function heat(pct: real): color {
if (pct >= 90) return root.accent;
if (pct >= 75) return root.warn;
if (pct >= 45) return root.glow;
return root.primary;
if (pct <= 55) return root.text;
return root.mix(root.text, root.accent, (pct - 55) / 45);
}
// ----------------------------------------------------------------- fonts
@@ -68,11 +110,24 @@ Singleton {
readonly property int barMargin: 8
readonly property int barGap: 8
// Horizontal offset per unit of height. tan(14deg) — every panel leans.
// 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
// 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 int cut: 9 // corner chamfer, in px
readonly property int cut: 10
readonly property int stroke: 2
// Pure-black keyline and offset copy behind panels. This is the one piece of
// layering the style needs; it is not a soft shadow and never blurs.
readonly property real dropOffset: 3
readonly property real keylineWidth: 2
readonly property int padS: 6
readonly property int padM: 12
readonly property int padL: 20
@@ -97,5 +152,9 @@ Singleton {
readonly property url texStripes: "root:/assets/stripes.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
WlSessionLockSurface {
id: lockSurface
color: "black"
LockFace {
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
}
}
+40 -30
View File
@@ -27,10 +27,17 @@ Item {
}
// ------------------------------------------------------- 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 {
id: wall
anchors.fill: parent
source: Theme.wallpaper
source: surface.screenName !== ""
? Wallpaper.forMonitor(surface.screenName)
: Wallpaper.source
fillMode: Image.PreserveAspectCrop
asynchronous: false
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 {
anchors.fill: parent
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
source: Theme.texHalftone
fillMode: Image.Tile
opacity: 0.05
smooth: false
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 }
}
reveal: 0
wedgeSpan: 0.30
wedgeOpacity: 0.30
stripeOpacity: 0.07
}
// ------------------------------------------------------------ clock
@@ -92,6 +82,20 @@ Item {
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 {
id: timeBlock
anchors.left: parent.left
@@ -403,6 +407,12 @@ Item {
ParallelAnimation {
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 {
target: timeBlock; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
+24 -7
View File
@@ -67,7 +67,8 @@ PanelWindow {
readonly property color tone: {
switch (root.kind) {
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;
}
}
@@ -143,24 +144,40 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
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
height: Math.max(titleLabel.implicitHeight, readout.implicitHeight)
Text {
id: titleLabel
anchors.left: parent.left
anchors.right: readout.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: root.title.toUpperCase()
color: Theme.subtext
elide: Text.ElideRight
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Item {
width: parent.width - 90
height: 1
}
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) + "%"
pixelSize: Theme.fsBody
split: 1
+47 -58
View File
@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
@@ -48,46 +47,20 @@ PanelWindow {
}
}
// Diagonal stripe field sweeping in from the left.
Image {
id: stripes
// Stripes and wedge, shared with the lock screen. The wedge used to lean at
// 0.32 here, which was its own angle and nothing else's; P5Backdrop leans
// it at Theme.skew like every other edge in the shell.
P5Backdrop {
id: ground
anchors.fill: parent
source: Theme.texStripes
fillMode: Image.Tile
opacity: 0
smooth: false
layer.enabled: true
layer.effect: TintEffect {
tintColor: Theme.accent
}
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)
]
}
}
reveal: 0
stripeOpacity: 0.10
wedgeOpacity: 0.35
wedgeSpan: 0.34
// The live desktop is still visible behind this, so a dot wash would be
// laid over other people's windows rather than over a ground of our
// own. The lock owns the halftone; here it just muddies things.
halftone: false
}
// ---------------------------------------------------------------- title
@@ -141,6 +114,11 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padM
// The widest slab in the stack. Fixed rather than derived so the ragged
// x offsets below cannot feed back into the Column's own width.
readonly property int stackWidth: 312
width: stackWidth
Repeater {
id: buttonRepeater
@@ -160,11 +138,21 @@ PanelWindow {
required property var modelData
required property int index
width: 280
readonly property int slabWidth: actions.stackWidth
width: entry.slabWidth
height: 52
opacity: 0
transform: Translate { id: slide; x: 90 }
transform: [
Translate { id: slide; x: 90 },
Rotation {
id: tilt
angle: -3
origin.x: entry.slabWidth / 2
origin.y: 26
}
]
P5Button {
anchors.fill: parent
@@ -172,6 +160,7 @@ PanelWindow {
icon: entry.modelData.icon
destructive: entry.modelData.danger
accent: entry.modelData.danger ? Theme.accent : Theme.primary
lean: Theme.skew
lunge: -10
onClicked: root.invoke(entry.modelData.act)
}
@@ -199,6 +188,18 @@ PanelWindow {
duration: Theme.durSlow
easing.type: Easing.OutBack
}
// Slabs land out of true and snap square. Sliding alone
// reads like a slide deck; the tilt is what makes it
// land like a card being thrown down. Same direction
// every time — the motion is the flourish, the geometry
// stays disciplined.
NumberAnimation {
target: tilt; property: "angle"
from: -3; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
easing.overshoot: 2.2
}
}
}
}
@@ -244,18 +245,10 @@ PanelWindow {
target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
}
// One number now drives the slide, the stripe opacity and the wedge.
NumberAnimation {
target: stripes; property: "opacity"
to: 0.1; duration: Theme.durSlow
}
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
target: ground; property: "reveal"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: title; property: "opacity"
@@ -270,11 +263,7 @@ PanelWindow {
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: stripes; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: wedge; property: "opacity"
target: ground; property: "reveal"
to: 0; duration: Theme.durFast
}
NumberAnimation {
+50 -26
View File
@@ -9,7 +9,11 @@ Popout {
id: root
contentWidth: 310
contentHeight: 296
// Derived: the charge block is a different height from the dial it
// replaced, and the health line comes and goes.
contentHeight: header.height + Theme.padM + readout.height + Theme.padM
+ bar.height + Theme.padM + detailText.implicitHeight + Theme.padL
+ suspendButton.height + Theme.padM * 2
PopoutSurface {
anchors.fill: parent
@@ -26,27 +30,42 @@ Popout {
subtitle: Battery.charging ? "CHARGING" : (Battery.onBattery ? "ON BATTERY" : "AC")
}
Row {
// The dial that used to sit here duplicated the meter below it and was
// the only curve in the shell. The charge is now simply set large, and
// the MeterBar underneath is left to carry the level on its own.
Item {
id: readout
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
height: Math.max(charge.height, detail.height)
Gauge {
width: 92; height: 92
value: Battery.percent
label: "CHARGE"
arcColor: Battery.tint
readout: Math.round(Battery.percent) + "%"
Column {
id: charge
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
// No "CHARGE" caption under the number. It landed directly on
// top of the meter, and between the header and a full-width
// charge bar there was nothing left for it to disambiguate.
SplitText {
text: Math.round(Battery.percent) + "%"
pixelSize: Theme.fsHuge
color: Battery.tint
split: 2.5
splitOpacity: 0.7
}
}
Column {
id: detail
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
SplitText {
anchors.right: parent.right
horizontalAlignment: Text.AlignRight
text: Battery.timeLabel
pixelSize: Theme.fsBody
split: 1.1
@@ -57,6 +76,8 @@ Popout {
text: Battery.changeRate > 0
? Battery.changeRate.toFixed(1) + " W " + (Battery.charging ? "in" : "draw")
: "idle"
anchors.right: parent.right
horizontalAlignment: Text.AlignRight
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
@@ -66,6 +87,8 @@ Popout {
Text {
visible: Battery.health > 0
text: "Health " + Math.round(Battery.health) + "%"
anchors.right: parent.right
horizontalAlignment: Text.AlignRight
color: Battery.health < 70 ? Theme.warn : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
@@ -88,9 +111,26 @@ Popout {
activeColor: Battery.tint
}
Text {
id: detailText
anchors.top: bar.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
text: Battery.detail
wrapMode: Text.WordWrap
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
// Follows the detail line rather than being pinned to the bottom of a
// fixed-height panel, which is what left a dead band above it.
P5Button {
id: suspendButton
anchors.bottom: parent.bottom
anchors.top: detailText.bottom
anchors.topMargin: Theme.padL
anchors.left: parent.left
text: "Suspend"
icon: "󰤄"
@@ -99,21 +139,5 @@ Popout {
Actions.suspend();
}
}
Text {
anchors.top: bar.bottom
anchors.topMargin: Theme.padS
anchors.bottom: suspendButton.top
anchors.bottomMargin: Theme.padS
anchors.left: parent.left
anchors.right: parent.right
text: Battery.detail
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignTop
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+19 -12
View File
@@ -9,7 +9,12 @@ Popout {
id: root
contentWidth: 420
contentHeight: 268
// Derived rather than a fixed number: the meters are taller than the dials
// they replaced, and the GPU block disappears entirely on machines without
// one. A hardcoded height would either clip or leave a hole.
contentHeight: header.height + Theme.padM
+ gauges.implicitHeight + Theme.padL
+ details.implicitHeight + Theme.padM * 2
PopoutSurface {
anchors.fill: parent
@@ -26,37 +31,38 @@ Popout {
subtitle: "UP " + Sys.uptime + " · " + Sys.procs + " PROCS"
}
Row {
Column {
id: gauges
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.horizontalCenter: parent.horizontalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
Gauge {
width: 88; height: 88
MetricRow {
width: parent.width
value: Sys.cpuUsage
label: "CPU"
sub: Math.round(Sys.cpuTemp) + "°C"
}
Gauge {
width: 88; height: 88
MetricRow {
width: parent.width
value: Sys.memPercent
label: "MEM"
sub: Sys.gb(Sys.memUsed) + " / " + Sys.gb(Sys.memTotal)
}
Gauge {
width: 88; height: 88
MetricRow {
width: parent.width
visible: Sys.gpuAvailable
value: Sys.gpuUsage
label: "GPU"
sub: Math.round(Sys.gpuTemp) + "°C"
}
Gauge {
width: 88; height: 88
MetricRow {
width: parent.width
value: Sys.diskPercent
label: "DISK"
sub: Sys.diskFree + " free"
@@ -64,8 +70,9 @@ Popout {
}
Column {
id: details
anchors.top: gauges.bottom
anchors.topMargin: Theme.padM
anchors.topMargin: Theme.padL
anchors.left: parent.left
anchors.right: parent.right
spacing: 3
+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
@@ -36,6 +36,12 @@ ShellRoot {
Bar {}
}
Variants {
model: Quickshell.screens
Rail {}
}
Variants {
model: Quickshell.screens