updated theme

This commit is contained in:
2026-08-11 01:53:26 +02:00
parent 0a439d951e
commit 105cf6b3a8
31 changed files with 1218 additions and 976 deletions
+9 -5
View File
@@ -12,7 +12,10 @@ WINIT_X11_SCALE_FACTOR = "1.0"
position = "None"
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
+103 -34
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,37 +18,38 @@ Hyprland starts it from `~/.config/hypr/modules/autostart.conf`.
## Layout
The shell is spread across three surfaces rather than crammed into one bar,
because on a 1080p panel horizontal space is cheap and vertical space is not: a
48px rail costs 2.5% of the width, where the same 48px along the bottom would
have cost 4.4% of the height on top of what the top bar already takes.
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.
```
┌────────────────────────────────────────────┐
│ ◆ TAKEMI [1][2][3] window title 00:09 │ bar — 48px exclusive
│ ◆ │ 1 2 3 │ window title ♪ TRACK │ 42% │ 00:09 │ bar — 48px exclusive
├────────────────────────────────────────────┤
│ ▐█▌│ rail — 48px exclusive
│ windows ▐ ▌│
▌│
♪ TRACK ╲ ▁▃▅ 42% dock — no exclusive zone
▌│
▐ ▌│
└────────────────────────────────────────────┘
```
- **bar** — identity, workspaces, focused window, clock.
- **rail** — network, bluetooth, audio, mic, backlight, battery, tray,
- **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.
- **dock** — now playing and the CPU/MEM/GPU readout. Reserves nothing: windows
tile the full height underneath, and the input mask is clipped to the cluster
so clicks elsewhere in the corner fall through. Idles near-invisible and wakes
on hover or on playback; the crimson corner wedge is the always-on handle that
marks where it lives.
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, dock, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves, shape variance
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 three surfaces and their 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
@@ -75,8 +76,8 @@ contentPad * 2`), which keeps the shape maths out of binding loops.
| Tray | rail | activate | app menu | secondary | | |
| Bell | rail | history + mark read | do not disturb | | | history |
| Power | rail | power menu | | | | |
| Media | dock | play/pause | next | raise player | next/prev | full player |
| CPU/MEM/GPU | dock | btop | | | | resource dashboard |
| 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.
@@ -142,15 +143,83 @@ The display face is Archivo Black, set italic almost everywhere. Change
## Palette discipline
Two hues, deliberately: teal carries every resting state, crimson means alarm
and nothing else. The old blue, green and amber remain in `Theme.qml` only as
aliases onto those two families so existing call sites keep working — don't
introduce new uses. `Theme.heat()` is a continuous teal→crimson ramp rather than
a hue cycle, so a number that has gone red means the same thing as a battery
that has gone red. Tray icons are force-flattened to a single colour for the
same reason: app brand colours wreck the palette faster than anything else.
Crimson, black, white. That is the whole palette.
Geometry variance lives in the same file. `Theme.leanFor(i)`, `chopFor(i)` and
`cornersFor(i)` hand each module a different lean and chamfer by index, so
modules stop being congruent parallelograms in an even rhythm — pass a module's
index rather than accepting the defaults.
**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 -55
View File
@@ -5,13 +5,13 @@ import "root:/config"
import "root:/components"
import "root:/services"
// One bar per monitor.
// One bar per monitor, and one surface.
//
// Identity, workspaces and the focused window on the left; the clock on the
// right. Status moved to the rail and the player and metrics moved to the dock,
// which is what buys the room for this to breathe — nothing here is fighting
// for horizontal space any more, so the title gets a generous cap and the type
// can run large instead of being squeezed to fit between two clusters.
// 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
@@ -37,33 +37,39 @@ PanelWindow {
exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent"
// The slab. One panel, full width, flat fill, black keyline.
P5Panel {
id: slab
anchors.fill: parent
lean: Theme.skew
chop: Theme.cut
chopCorners: [1, 3]
padding: 0
fill: Theme.surface
fillOpacity: 1.0
border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5
}
readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM
// ------------------------------------------------------------------ left
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.padM
interactive: false
lean: Theme.leanFor(1)
chop: Theme.chopFor(1)
chopCorners: Theme.cornersFor(1)
implicitWidth: spacesRow.implicitWidth + contentPad * 2
Row {
id: spacesRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padL
Divider {}
Workspaces {
id: spaces
@@ -71,53 +77,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
// The only thing competing for this row is the clock, so the
// title can have most of the bar.
maxWidth: Math.max(120, root.width
- sigil.implicitWidth
- clock.implicitWidth
- spaces.implicitWidth
- Theme.barGap * 3
- Theme.padL * 2
- Theme.padXL
- spacesPill.contentPad * 2)
}
}
- leftGroup.x - rightGroup.width - sigil.implicitWidth
- spaces.implicitWidth - root.edgeInset * 2 - Theme.padXL)
}
}
// ----------------------------------------------------------------- right
Clock {
id: clock
Row {
id: rightGroup
anchors.right: parent.right
anchors.rightMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter
spacing: 0
MediaPill {
screenRef: root.modelData
}
// A hairline of crimson under the whole bar — the Persona 5 underscore.
Divider {}
Resources {
screenRef: root.modelData
}
Divider {}
Clock {
screenRef: root.modelData
}
}
// A hairline rule between modules, sheared to the house angle. This is what
// divides the bar now — gaps between cards are what made it look scattered.
component Divider: Item {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight
Rectangle {
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)
}
}
}
}
+60 -28
View File
@@ -2,12 +2,19 @@ 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
@@ -15,18 +22,29 @@ Item {
property real padding: Theme.padM
property bool interactive: true
property bool halftone: false
property bool standalone: false
// Chamfer, forwarded so modules can differ from one another instead of all
// sharing the panel default.
property real chop: Theme.cut
property 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
@@ -37,9 +55,12 @@ 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
@@ -47,39 +68,50 @@ Item {
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 }
}
}
}
-117
View File
@@ -1,117 +0,0 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// The floating bottom-left cluster: now playing, plus the CPU/MEM/GPU readout.
//
// This one reserves no space at all. Windows tile the full height of the screen
// underneath it, and the input mask is clipped to the cluster itself so clicks
// anywhere else in the corner fall straight through to whatever is below.
//
// At rest it sits at a quarter opacity — present enough to read at a glance,
// faint enough to ignore — and snaps to full weight when the pointer arrives.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "takemi-dock"
anchors {
bottom: true
left: true
}
margins {
bottom: Theme.dockMargin
left: Theme.dockMargin
}
implicitWidth: cluster.implicitWidth
implicitHeight: cluster.implicitHeight
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore
color: "transparent"
// Only the cluster takes pointer input; the rest of the surface is
// transparent to clicks.
mask: Region { item: cluster }
readonly property bool awake: clusterHover.hovered || Media.playing
// A crimson wedge pinned to the corner. This is the one part of the dock
// that never fades — it marks where the cluster lives so a nearly
// invisible idle state is still something you can aim at, and it doubles
// as the corner furniture the layout was missing.
Shape {
anchors.left: parent.left
anchors.bottom: parent.bottom
width: 26
height: 26
z: -1
preferredRendererType: Shape.CurveRenderer
opacity: root.awake ? 0.0 : 0.9
Behavior on opacity {
NumberAnimation { duration: Theme.durBase }
}
ShapePath {
fillColor: Theme.accent
strokeColor: "transparent"
PathPolyline {
path: [
Qt.point(0, 26),
Qt.point(0, 6),
Qt.point(26, 26),
Qt.point(0, 26)
]
}
}
}
Row {
id: cluster
spacing: Theme.barGap
opacity: root.awake ? 1.0 : Theme.dockIdleOpacity
Behavior on opacity {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
// Lift off the corner as it wakes.
transform: Translate {
y: root.awake ? 0 : 4
Behavior on y {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutBack
}
}
}
HoverHandler {
id: clusterHover
}
MediaPill {
screenRef: root.modelData
}
Resources {
screenRef: root.modelData
}
}
}
+53 -100
View File
@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
@@ -7,17 +6,15 @@ import "root:/components"
import "root:/services"
import "root:/popouts"
// The right-hand status rail.
// The right-hand status rail — one slab, not a stack of cards.
//
// Everything that used to be crammed into the right third of the top bar lives
// here instead: connectivity, audio, power, tray, notifications. 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 have been 4.4% on top of what the top bar already
// takes.
// 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.
//
// Slabs are grouped by what they do rather than spaced evenly, and each pulls a
// different lean and chamfer from the shape tables, so the column reads as a
// stack of distinct cards instead of a ladder.
// 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
@@ -43,55 +40,37 @@ PanelWindow {
exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent"
// Corner furniture. The dock pins a crimson wedge to the bottom-left; this
// is its opposite number, so the desktop is bracketed on the diagonal
// rather than merely edged. Decorative only — it sits under the slabs and
// takes no input.
Shape {
anchors.right: parent.right
anchors.bottom: parent.bottom
width: 30
height: 30
z: -1
preferredRendererType: Shape.CurveRenderer
opacity: 0.9
// 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
}
ShapePath {
fillColor: Theme.accent
strokeColor: "transparent"
PathPolyline {
path: [
Qt.point(30, 30),
Qt.point(30, 4),
Qt.point(2, 30),
Qt.point(30, 30)
]
}
}
}
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: Theme.railGap
// Connectivity.
RailGroup {
variant: 0
accent: Sys.netUp ? Theme.primary : Theme.muted
implicitHeight: linkCol.implicitHeight + contentPad * 2
Column {
id: linkCol
anchors.centerIn: parent
spacing: Theme.padS
spacing: 0
RailIcon {
id: netIcon
icon: Net.icon
tint: Theme.primary
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
@@ -101,24 +80,12 @@ PanelWindow {
RailIcon {
id: btIcon
icon: Bt.icon
tint: Theme.primary
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
}
}
// Audio and backlight.
RailGroup {
variant: 1
accent: Audio.muted || Audio.micMuted ? Theme.accent : Theme.primary
implicitHeight: audioCol.implicitHeight + contentPad * 2
Column {
id: audioCol
anchors.centerIn: parent
spacing: Theme.padS
Divider {}
RailIcon {
id: volIcon
@@ -145,67 +112,36 @@ PanelWindow {
visible: Brightness.available
icon: Brightness.icon
readout: Math.round(Brightness.percent) + "%"
tint: Theme.primary
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
}
}
// Battery.
RailGroup {
variant: 2
visible: Battery.available
accent: Battery.critical ? Theme.accent : Theme.primary
implicitHeight: battIcon.implicitHeight + contentPad * 2
Divider { visible: Battery.available }
RailIcon {
id: battIcon
anchors.centerIn: parent
visible: Battery.available
icon: Battery.icon
readout: Math.round(Battery.percent) + "%"
tint: Battery.tint
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
}
// ---------------------------------------------------------------- bottom
Column {
id: lower
anchors.bottom: parent.bottom
// Clear of the corner wedge, which would otherwise sit entirely behind
// the bottom slab and never be seen.
anchors.bottomMargin: 26
anchors.bottomMargin: root.edgeInset
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.railGap
// Tray.
RailGroup {
variant: 3
visible: tray.visible
accent: Theme.primary
implicitHeight: tray.implicitHeight + contentPad * 2
spacing: 0
Tray {
id: tray
anchors.centerIn: parent
}
anchors.horizontalCenter: parent.horizontalCenter
}
// Notifications and power — the two that act on the session rather than
// report on it, so they sit apart at the foot of the rail.
RailGroup {
variant: 4
accent: Notifs.dnd ? Theme.muted : Theme.primary
implicitHeight: actionCol.implicitHeight + contentPad * 2
Column {
id: actionCol
anchors.centerIn: parent
spacing: Theme.padS
Divider { visible: tray.visible }
RailIcon {
id: bellIcon
@@ -213,7 +149,6 @@ PanelWindow {
readout: Notifs.unread > 0
? (Notifs.unread > 9 ? "9+" : String(Notifs.unread))
: ""
tint: Notifs.hasUnread ? Theme.accent : Theme.primary
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
onClicked: {
@@ -226,10 +161,28 @@ PanelWindow {
RailIcon {
id: powerIcon
icon: "󰐥"
tint: Actions.powerMenuOpen ? Theme.accent : Theme.subtext
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)
}
}
}
-92
View File
@@ -1,92 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// A slab on the right-hand rail. The vertical counterpart to BarPill: it sizes
// itself to a stacked column rather than a row, and pulls its lean and chamfer
// from the shape-variance tables by `variant`, so no two slabs on the rail are
// congruent.
Item {
id: root
property color accent: Theme.primary
property int variant: 0
property bool active: false
property real padding: Theme.padS
property bool interactive: false
property bool halftone: false
readonly property bool hovered: hover.hovered
readonly property real contentPad: panel.contentPad
readonly property alias panelItem: panel
default property alias content: holder.data
signal clicked()
implicitWidth: Theme.railWidth
P5Panel {
id: panel
anchors.fill: parent
vertical: true
// Scaled down from the horizontal tables: a rail slab leans by a
// fraction of its 48px width, so the raw values would be far too steep.
lean: Theme.leanFor(root.variant) * 0.6
leanLeft: Theme.leanLeftFor(root.variant)
chop: Math.min(Theme.chopFor(root.variant), 10)
chopCorners: Theme.cornersFor(root.variant)
padding: root.padding
halftone: root.halftone
halftoneColor: root.accent
sheen: true
fill: root.active ? Theme.surfaceAlt : Theme.surface
fillOpacity: root.hovered ? 0.98 : 0.88
border: root.hovered || root.active
? Theme.alpha(root.accent, 0.9)
: Theme.alpha(Theme.outline, 0.7)
borderWidth: root.hovered || root.active ? Theme.stroke : 1
slash: root.active
slashColor: root.accent
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
}
Behavior on border {
ColorAnimation { duration: Theme.durBase }
}
Item {
id: holder
anchors.fill: parent
}
}
// Slabs lift toward the pointer, away from the screen edge.
transform: Translate {
x: root.hovered && root.interactive ? -3 : 0
Behavior on x {
NumberAnimation {
duration: Theme.durBase
easing.type: Easing.OutBack
}
}
}
HoverHandler {
id: hover
enabled: root.interactive
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: root.interactive
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
}
+15 -1
View File
@@ -19,6 +19,10 @@ Item {
readonly property bool hovered: hover.hovered
// 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()
@@ -68,12 +72,22 @@ Item {
// 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: root.alert ? Theme.accent : root.tint
color: Theme.glow
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
-6
View File
@@ -14,13 +14,7 @@ BarPill {
accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
// Roomier than it was on the bar. In the dock there is nothing to compete
// with, so the figures get real padding instead of being pushed up against
// the slanted edges.
padding: Theme.padM
lean: Theme.leanFor(2)
chop: Theme.chopFor(2)
chopCorners: Theme.cornersFor(2)
implicitWidth: layout.implicitWidth + contentPad * 2
onClicked: Actions.openSystemMonitor()
+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)
}
+19 -47
View File
@@ -46,9 +46,7 @@ Item {
id: row
anchors.verticalCenter: parent.verticalCenter
// Uneven gaps. An evenly spaced row of identical ticks reads as a
// progress bar; the whole point here is that it should not.
spacing: 5
spacing: 6
Repeater {
model: root.slots
@@ -65,19 +63,10 @@ Item {
readonly property bool focused: root.focusedId === wsId
readonly property bool hovered: pipHover.hovered
// Each tick pulls its own shear and its own height off the
// variance tables, and sits a little off the baseline, so the
// row is a ragged run of marks rather than a repeating comb.
readonly property real shear: Theme.leanFor(pip.index)
readonly property int baseHeight: [18, 14, 20, 16][pip.index % 4]
readonly property int drift: [0, 2, -1, 1][pip.index % 4]
width: focused ? 36 : (occupied ? 16 : 9)
// The focused tick grows past its neighbours and past the row's
// own height, so it breaks the line instead of sitting in it.
height: focused ? 24 : pip.baseHeight
y: focused ? -2 : pip.drift
// 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 {
NumberAnimation {
@@ -86,45 +75,28 @@ Item {
easing.overshoot: 1.6
}
}
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
Behavior on y {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
// Black offset copy behind the focused tick — the same
// screen-print stack the panels use, at tick scale.
Rectangle {
visible: pip.focused
anchors.fill: parent
x: 3
y: 3
color: Theme.ink
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
1, -pip.shear, 0, pip.height * pip.shear,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
}
}
Rectangle {
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(
1, -pip.shear, 0, pip.height * pip.shear,
1, -Theme.skew, 0, pip.height * Theme.skew,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
@@ -141,7 +113,7 @@ 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.fsBody
font.weight: Theme.weightDisplay
+103
View File
@@ -0,0 +1,103 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
import "root:/components"
// The shared ground for the two full-screen surfaces: a crimson wedge leaning
// at the house angle, a tiled stripe field that sweeps in from the left, and a
// halftone dot wash over the top.
//
// The lock screen and the power menu each used to draw their own version of
// this. They disagreed — the power menu had a wedge and a full stripe field,
// the lock had a 42px stripe band and no wedge, and the wedge leaned at 0.32
// where every panel in the shell leans at Theme.skew. Building both on one
// component is what makes them read as the same object seen twice rather than
// as two guesses at the same brief.
Item {
id: root
// 0 = withdrawn, 1 = fully present. Drives the stripe slide, the stripe
// opacity and the wedge together, so a caller animates one number instead
// of keeping three in sync. The power menu runs it both ways on open and
// close; the lock runs it once on entry.
property real reveal: 1
property color tone: Theme.accent
property bool wedge: true
property real wedgeOpacity: 0.35
// Bottom-left corner of the wedge and its span, as fractions of width. The
// wedge leans right going up, like every other edge in the shell, so its
// top edge sits `leanOffset` further right than its bottom.
property real wedgeLeft: 0
property real wedgeSpan: 0.34
property bool stripes: true
property real stripeOpacity: 0.10
property bool halftone: true
property real halftoneOpacity: 0.05
property color halftoneColor: Theme.primary
// ONE angle, everywhere — the same rule the panels follow.
readonly property real leanOffset: root.height * Theme.skew
// Diagonal stripe field, sliding in from off-screen left.
Image {
anchors.fill: parent
visible: root.stripes && root.reveal > 0
source: Theme.texStripes
fillMode: Image.Tile
opacity: root.stripeOpacity * root.reveal
smooth: false
layer.enabled: true
layer.effect: TintEffect {
tintColor: root.tone
}
transform: Translate {
x: -root.width * (1 - root.reveal)
}
}
// The wedge. Drawn after the stripes so it reads as a slab laid over them.
Shape {
anchors.fill: parent
visible: root.wedge && root.reveal > 0
opacity: root.reveal
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.alpha(Theme.accentDim, root.wedgeOpacity)
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: {
const x = root.wedgeLeft * root.width;
const span = root.wedgeSpan * root.width;
const off = root.leanOffset;
return Geom.closed([
Qt.point(x + off, 0),
Qt.point(x + off + span, 0),
Qt.point(x + span, root.height),
Qt.point(x, root.height)
]);
}
}
}
}
// Screen-printed dot wash over the whole thing.
Image {
anchors.fill: parent
visible: root.halftone
source: Theme.texHalftone
fillMode: Image.Tile
opacity: root.halftoneOpacity
smooth: false
layer.enabled: true
layer.effect: TintEffect { tintColor: root.halftoneColor }
}
}
+53 -44
View File
@@ -39,7 +39,10 @@ Item {
property color halftoneColor: Theme.primary
property 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
// The screen-print stack. A pure-black copy of the silhouette sits behind
@@ -71,6 +74,28 @@ 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
@@ -138,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
@@ -166,47 +174,40 @@ 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 t = root.slashWidth;
// On a rail slab the slash runs along the slanted top
// edge instead of down a side.
// 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
@@ -226,6 +227,14 @@ Item {
}
}
}
MultiEffect {
anchors.fill: parent
source: slashShape
maskEnabled: true
maskSource: silhouette
}
}
}
Item {
+74 -68
View File
@@ -3,54 +3,77 @@ pragma Singleton
import QtQuick
import Quickshell
// Takemi palette. Two hues, no exceptions: a cold teal that carries every
// resting state, and Persona 5 crimson that means alarm and nothing else.
// Anything that used to be blue/green/amber now resolves into one of those two
// families, so a splash of red in the bar always reads as "something is wrong".
// 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
// True black, for keylines and drop shapes. `base` is the near-black the
// surfaces sit on; `ink` is never tinted and never blended.
readonly property color ink: "#000000"
readonly property color 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 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 text: "#e2f4f8"
readonly property color subtext: "#93b8c4"
readonly property color muted: "#587c8a"
readonly property color text: "#ffffff"
readonly property color subtext: "#c8c8ce"
readonly property color muted: "#6b6b70"
readonly property color primary: "#34d3e6" // electric teal — the house colour
readonly property color primaryDim: "#17879b"
readonly property color glow: "#8ef2ff"
readonly property color deep: "#0d4b5e"
// ------------------------------------------------------------ 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"
readonly property color accent: "#ff2d40" // P5 crimson — alarm, only ever alarm
readonly property color accentDim: "#a8121f"
readonly property color accentSoft: "#ff6b78" // the "getting there" step below accent
readonly property color accentDim: Qt.darker(root.accent, 1.9)
readonly property color accentSoft: Qt.lighter(root.accent, 1.35)
// Retired hues, kept as aliases so call sites keep working while resolving
// into the two-hue system. Do not introduce new uses.
// `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.primary
readonly property color warn: root.accentSoft
readonly property color ok: root.text
readonly property color warn: root.accent
// Chromatic aberration pair, straight off the wallpaper's RGB split.
readonly property color splitRed: "#ff2b4d"
readonly property color splitCyan: "#25e8ff"
// 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);
}
// Linear blend, t = 0 gives a, t = 1 gives b.
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,
@@ -59,12 +82,11 @@ Singleton {
a.a + (b.a - a.a) * k);
}
// Heat ramp for load/usage readouts. Deliberately *not* a hue cycle: it sits
// flat teal until load actually matters, then bleeds continuously into
// crimson. A number that has gone red means the same thing everywhere.
// 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 <= 55) return root.primary;
return root.mix(root.primary, root.accent, (pct - 55) / 45);
if (pct <= 55) return root.text;
return root.mix(root.text, root.accent, (pct - 55) / 45);
}
// ----------------------------------------------------------------- fonts
@@ -88,24 +110,22 @@ Singleton {
readonly property int barMargin: 8
readonly property int barGap: 8
// Right-hand status rail and the floating bottom-left dock.
// The right-hand rail uses the same lean and chamfer as the bar — a
// vertical panel leans its horizontal edges by a fraction of its width,
// which works out to the same 14 degrees.
readonly property int railWidth: 48
readonly property int railGap: 10
readonly property int dockMargin: 14
// Low enough that the cluster stops competing with whatever window is
// underneath it — at 0.25 it just muddies text it happens to overlap. The
// corner wedge stays fully opaque as the thing you aim at.
readonly property real dockIdleOpacity: 0.13
readonly property int dockRevealZone: 140 // px of screen bottom that wakes it
// Horizontal offset per unit of height. tan(14deg) — every panel leans.
// ONE angle, everywhere. tan(14deg). Persona geometry is disciplined, not
// random: every edge in the shell leans by exactly this much in exactly the
// same direction, and every corner is chopped by exactly `cut`. Varying
// these per module reads as noise, not as style.
readonly property real skew: 0.249
readonly property int cut: 9 // corner chamfer, in px
readonly property int cut: 10
readonly property int stroke: 2
// Pure-black keyline copy sitting behind every panel. This is what makes a
// shape read as screen-printed rather than as a clipped rectangle.
readonly property real dropOffset: 4
// 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
@@ -116,24 +136,6 @@ Singleton {
readonly property int popoutWidth: 380
readonly property int popoutGap: 10
// ------------------------------------------------------- shape variance
// Persona geometry is never a repeating pattern, so modules pull their lean
// and chamfer from these tables by index rather than sharing one default.
// Deterministic, so the bar looks the same every launch.
readonly property var leanTable: [0.32, 0.18, 0.26, 0.0, 0.29, 0.14, 0.35, 0.21]
readonly property var chopTable: [12, 5, 18, 8, 14, 4, 10, 16]
readonly property var cornerTable: [
[1, 3], [0, 2], [1], [0, 2, 3], [3], [1, 3], [2], [0, 1, 3]
]
function leanFor(i: int): real { return root.leanTable[i % root.leanTable.length]; }
function chopFor(i: int): real { return root.chopTable[i % root.chopTable.length]; }
function cornersFor(i: int): var { return root.cornerTable[i % root.cornerTable.length]; }
// Every other module leans against its neighbour, so the row reads as
// opposing diagonals instead of a comb.
function leanLeftFor(i: int): bool { return i % 3 === 1; }
// ------------------------------------------------------------ animation
// P5 motion is snappy and slightly overshooting — never soft.
readonly property int durFast: 110
@@ -150,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
+24 -69
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
@@ -165,18 +138,10 @@ PanelWindow {
required property var modelData
required property int index
// Six identical slabs stacked at a constant width is the most
// obviously machine-made thing in the shell. Vary the width and
// push each row a different distance off the right margin, so
// the stack has a ragged edge on both sides.
readonly property int slabWidth: [286, 252, 300, 268, 312, 244][entry.index]
readonly property int nudge: [0, 22, 8, 34, 14, 26][entry.index]
readonly property int slabWidth: actions.stackWidth
// Column owns y, so the ragged edge is an explicit x offset
// measured against the widest slab in the stack.
width: entry.slabWidth
height: 52
x: actions.stackWidth - entry.slabWidth - entry.nudge
opacity: 0
transform: [
@@ -195,8 +160,8 @@ PanelWindow {
icon: entry.modelData.icon
destructive: entry.modelData.danger
accent: entry.modelData.danger ? Theme.accent : Theme.primary
lean: Theme.leanFor(entry.index)
lunge: -10 - entry.index * 2
lean: Theme.skew
lunge: -10
onClicked: root.invoke(entry.modelData.act)
}
@@ -225,10 +190,12 @@ PanelWindow {
}
// 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.
// 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: entry.index % 2 === 0 ? -4 : 4; to: 0
from: -3; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
easing.overshoot: 2.2
@@ -278,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"
@@ -304,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 {
+120
View File
@@ -0,0 +1,120 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "root:/config"
// The wallpaper currently on screen, per monitor.
//
// The lock screen used to point at a hardcoded path in Theme.qml, which meant
// it showed whatever picture was correct on the day that line was written —
// takemi_2 while hyprpaper had been switched to takemi_1. Anything that puts
// the wallpaper on screen should ask hyprpaper what it is actually displaying,
// which is what this does.
//
// hyprpaper is the source of truth at runtime; hyprpaper.conf is the fallback
// for the window between the shell starting and hyprpaper answering, and for
// the case where hyprpaper is not running at all.
Singleton {
id: root
// monitor name -> file path. Monitors hyprpaper set with a blank `monitor =`
// come back under the empty-string key.
property var byMonitor: ({})
// The wallpaper to use when no particular monitor is being asked about.
property string path: ""
readonly property url source: root.path !== ""
? Qt.resolvedUrl("file://" + root.path)
: Theme.fallbackWallpaper
// Path set from hyprpaper.conf, used until (and unless) hyprctl answers.
property string configPath: ""
signal changed()
function forMonitor(name: string): url {
const p = root.byMonitor[name];
return p ? Qt.resolvedUrl("file://" + p) : root.source;
}
function refresh() {
reader.running = true;
}
function _expand(p: string): string {
const s = p.trim();
if (s.startsWith("~/")) return Quickshell.env("HOME") + s.slice(1);
return s;
}
function _adopt(map: var, fallback: string) {
root.byMonitor = map;
// Prefer the focused monitor's wallpaper, then any monitor's, then the
// config. Object.values order is insertion order here, which is the
// order hyprpaper listed them in.
const vals = Object.keys(map).map(k => map[k]).filter(v => v !== "");
const next = vals.length > 0 ? vals[0] : fallback;
if (next !== root.path) {
root.path = next;
root.changed();
}
}
Process {
id: reader
command: ["hyprctl", "hyprpaper", "listactive"]
stdout: StdioCollector {
onStreamFinished: {
const map = {};
for (const line of text.split("\n")) {
// "eDP-1: /home/narl/.wallpapers/takemi_1.jpg". Split on the
// first colon only — the path may contain one.
const at = line.indexOf(":");
if (at === -1) continue;
const mon = line.slice(0, at).trim();
const file = line.slice(at + 1).trim();
if (file === "" || file.startsWith("invalid")) continue;
map[mon] = file;
}
root._adopt(map, root.configPath);
}
}
// hyprpaper down, or hyprctl missing. Fall back to the config file.
onExited: code => {
if (code !== 0 && root.path === "") root._adopt({}, root.configPath);
}
}
// hyprpaper has no change signal to subscribe to, so poll — but slowly. A
// wallpaper changes when a person decides to change it, not continuously.
Timer {
interval: 10000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refresh()
}
// Editing the config is the other way the wallpaper changes here, and that
// one we can watch properly.
FileView {
id: conf
path: Quickshell.env("HOME") + "/.config/hypr/hyprpaper.conf"
watchChanges: true
onFileChanged: reload()
onLoaded: {
const m = /^\s*path\s*=\s*(.+)$/m.exec(text());
if (m) root.configPath = root._expand(m[1]);
// A config edit is usually followed by a hyprpaper reload, so go
// ask what actually took effect rather than trusting the file.
root.refresh();
}
}
}
-6
View File
@@ -42,12 +42,6 @@ ShellRoot {
Rail {}
}
Variants {
model: Quickshell.screens
Dock {}
}
Variants {
model: Quickshell.screens