diff --git a/alacritty/alacritty.toml b/alacritty/alacritty.toml index cfcfbe6..d6e6ce3 100644 --- a/alacritty/alacritty.toml +++ b/alacritty/alacritty.toml @@ -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" diff --git a/alacritty/takemi.toml b/alacritty/takemi.toml new file mode 100644 index 0000000..f12e60b --- /dev/null +++ b/alacritty/takemi.toml @@ -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" diff --git a/fish/conf.d/fish_frozen_theme.fish b/fish/conf.d/fish_frozen_theme.fish deleted file mode 100644 index 2008093..0000000 --- a/fish/conf.d/fish_frozen_theme.fish +++ /dev/null @@ -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 diff --git a/fish/conf.d/takemi_theme.fish b/fish/conf.d/takemi_theme.fish new file mode 100644 index 0000000..a483c67 --- /dev/null +++ b/fish/conf.d/takemi_theme.fish @@ -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 diff --git a/fish/functions/fish_prompt.fish b/fish/functions/fish_prompt.fish index c3f0f39..a2db331 100644 --- a/fish/functions/fish_prompt.fish +++ b/fish/functions/fish_prompt.fish @@ -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. diff --git a/fish/themes/Takemi.theme b/fish/themes/Takemi.theme new file mode 100644 index 0000000..a896e3f --- /dev/null +++ b/fish/themes/Takemi.theme @@ -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 diff --git a/fuzzel/fuzzel.ini b/fuzzel/fuzzel.ini index a7a38b2..8a23f12 100644 --- a/fuzzel/fuzzel.ini +++ b/fuzzel/fuzzel.ini @@ -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 diff --git a/fuzzel/takemi.ini b/fuzzel/takemi.ini new file mode 100644 index 0000000..bcfb587 --- /dev/null +++ b/fuzzel/takemi.ini @@ -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 diff --git a/hypr/hyprpaper.conf b/hypr/hyprpaper.conf index 9e734d3..7428142 100644 --- a/hypr/hyprpaper.conf +++ b/hypr/hyprpaper.conf @@ -1,6 +1,6 @@ wallpaper { monitor = - path = ~/.wallpapers/takemi_2.jpg + path = ~/.wallpapers/takemi_1.jpg fit_mode = cover } diff --git a/hypr/modules/takemi.conf b/hypr/modules/takemi.conf index c651c5d..22369df 100644 --- a/hypr/modules/takemi.conf +++ b/hypr/modules/takemi.conf @@ -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 diff --git a/hypr/modules/theme.conf b/hypr/modules/theme.conf index 7a9d151..0355402 100644 --- a/hypr/modules/theme.conf +++ b/hypr/modules/theme.conf @@ -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 = layersOut, 1, 2, hard, 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 diff --git a/quickshell/README.md b/quickshell/README.md index 3902ab8..9261fa0 100644 --- a/quickshell/README.md +++ b/quickshell/README.md @@ -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 +│ ▐█▌│ 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. diff --git a/quickshell/bar/Bar.qml b/quickshell/bar/Bar.qml index 46412de..c35d6a2 100644 --- a/quickshell/bar/Bar.qml +++ b/quickshell/bar/Bar.qml @@ -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,87 +37,99 @@ 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 + Divider {} - Row { - id: spacesRow - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - spacing: Theme.padL + Workspaces { + id: spaces + anchors.verticalCenter: parent.verticalCenter + screenRef: root.modelData + } - Workspaces { - id: spaces - anchors.verticalCenter: parent.verticalCenter - screenRef: root.modelData - } + Divider {} - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: 1 - height: 18 - color: Theme.alpha(Theme.outline, 0.8) - } - - 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) - } - } + ActiveWindow { + anchors.verticalCenter: parent.verticalCenter + screenRef: root.modelData + maxWidth: Math.max(120, root.width + - 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 - screenRef: root.modelData + spacing: 0 + + MediaPill { + screenRef: root.modelData + } + + Divider {} + + Resources { + screenRef: root.modelData + } + + Divider {} + + Clock { + 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" } + // 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.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) + } } } } diff --git a/quickshell/bar/BarPill.qml b/quickshell/bar/BarPill.qml index 3b37bd7..3cd055d 100644 --- a/quickshell/bar/BarPill.qml +++ b/quickshell/bar/BarPill.qml @@ -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,49 +55,63 @@ Item { implicitHeight: Theme.barHeight - P5Panel { - id: panel + Loader { + id: panelLoader anchors.fill: parent - lean: root.lean - leanLeft: root.leanLeft - chop: root.chop - chopCorners: root.chopCorners - padding: root.padding - halftone: root.halftone - halftoneColor: root.accent - sheen: true + active: root.standalone - 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 + sourceComponent: P5Panel { + lean: root.lean + leanLeft: root.leanLeft + chop: root.chop + chopCorners: root.chopCorners + padding: root.padding + halftone: root.halftone + halftoneColor: root.accent - slash: root.slash || root.active - slashColor: root.accent + fill: root.active ? Theme.surfaceAlt : Theme.surface + fillOpacity: 1.0 + border: root.hovered || root.active + ? root.accent + : Theme.alpha(Theme.outline, 0.9) + borderWidth: root.hovered || root.active ? Theme.stroke : 1 - Behavior on fillOpacity { - NumberAnimation { duration: Theme.durFast } - } - Behavior on border { - ColorAnimation { duration: Theme.durBase } - } - - Item { - id: holder - anchors.fill: parent + slash: root.slash || root.active + slashColor: root.accent } } - // Lift toward the pointer. Subtle, but it makes the bar feel alive. - transform: Translate { - y: root.hovered && root.interactive ? -2 : 0 + Item { + id: holder + anchors.fill: parent + anchors.leftMargin: root.contentPad + anchors.rightMargin: root.contentPad + anchors.bottomMargin: root.ruleBand + } - Behavior on y { - NumberAnimation { - duration: Theme.durBase - easing.type: Easing.OutBack + // 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 + + 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 } } } } diff --git a/quickshell/bar/Dock.qml b/quickshell/bar/Dock.qml deleted file mode 100644 index eea95bb..0000000 --- a/quickshell/bar/Dock.qml +++ /dev/null @@ -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 - } - } -} diff --git a/quickshell/bar/Rail.qml b/quickshell/bar/Rail.qml index 492838e..02dd34c 100644 --- a/quickshell/bar/Rail.qml +++ b/quickshell/bar/Rail.qml @@ -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,131 +40,91 @@ 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 - - ShapePath { - fillColor: Theme.accent - strokeColor: "transparent" - PathPolyline { - path: [ - Qt.point(30, 30), - Qt.point(30, 4), - Qt.point(2, 30), - Qt.point(30, 30) - ] - } - } + // 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 } + 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 + spacing: 0 - // Connectivity. - RailGroup { - variant: 0 - accent: Sys.netUp ? Theme.primary : Theme.muted - implicitHeight: linkCol.implicitHeight + contentPad * 2 - - Column { - id: linkCol - anchors.centerIn: parent - spacing: Theme.padS - - RailIcon { - id: netIcon - icon: Net.icon - tint: Theme.primary - dim: !Sys.netUp - onClicked: netPopout.pinned = !netPopout.pinned - onRightClicked: Net.toggleWifi() - onMiddleClicked: Actions.openNetworkSettings() - } - - RailIcon { - id: btIcon - icon: Bt.icon - tint: Theme.primary - dim: !Bt.enabled - onClicked: btPopout.pinned = !btPopout.pinned - onRightClicked: Bt.toggle() - } - } + RailIcon { + id: netIcon + icon: Net.icon + dim: !Sys.netUp + onClicked: netPopout.pinned = !netPopout.pinned + onRightClicked: Net.toggleWifi() + onMiddleClicked: Actions.openNetworkSettings() } - // 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 - - 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) + "%" - tint: Theme.primary - onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5) - } - } + RailIcon { + id: btIcon + icon: Bt.icon + dim: !Bt.enabled + onClicked: btPopout.pinned = !btPopout.pinned + onRightClicked: Bt.toggle() } - // Battery. - RailGroup { - variant: 2 + 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 - accent: Battery.critical ? Theme.accent : Theme.primary - implicitHeight: battIcon.implicitHeight + contentPad * 2 - - RailIcon { - id: battIcon - anchors.centerIn: parent - icon: Battery.icon - readout: Math.round(Battery.percent) + "%" - tint: Battery.tint - alert: Battery.critical - pulsing: Battery.critical - onClicked: battPopout.pinned = !battPopout.pinned - } + icon: Battery.icon + readout: Math.round(Battery.percent) + "%" + alert: Battery.critical + pulsing: Battery.critical + onClicked: battPopout.pinned = !battPopout.pinned } } @@ -175,60 +132,56 @@ PanelWindow { 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 + spacing: 0 - // Tray. - RailGroup { - variant: 3 - visible: tray.visible - accent: Theme.primary - implicitHeight: tray.implicitHeight + contentPad * 2 - - Tray { - id: tray - anchors.centerIn: parent - } + Tray { + id: tray + 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 + Divider { visible: tray.visible } - Column { - id: actionCol - anchors.centerIn: parent - spacing: Theme.padS + 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: bellIcon - icon: Notifs.icon - 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: { - notifPopout.pinned = !notifPopout.pinned; - Notifs.markRead(); - } - onRightClicked: Notifs.toggleDnd() - } + RailIcon { + id: powerIcon + icon: "󰐥" + alert: Actions.powerMenuOpen + onClicked: Actions.togglePowerMenu() + } + } - RailIcon { - id: powerIcon - icon: "󰐥" - tint: Actions.powerMenuOpen ? Theme.accent : Theme.subtext - 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) } } } diff --git a/quickshell/bar/RailGroup.qml b/quickshell/bar/RailGroup.qml deleted file mode 100644 index 5d5687d..0000000 --- a/quickshell/bar/RailGroup.qml +++ /dev/null @@ -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() - } -} diff --git a/quickshell/bar/RailIcon.qml b/quickshell/bar/RailIcon.qml index 859cc97..e5e0825 100644 --- a/quickshell/bar/RailIcon.qml +++ b/quickshell/bar/RailIcon.qml @@ -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 } diff --git a/quickshell/bar/Resources.qml b/quickshell/bar/Resources.qml index d70b5f9..09f8631 100644 --- a/quickshell/bar/Resources.qml +++ b/quickshell/bar/Resources.qml @@ -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() diff --git a/quickshell/bar/Sigil.qml b/quickshell/bar/Sigil.qml index 5e6dbf0..5361b1b 100644 --- a/quickshell/bar/Sigil.qml +++ b/quickshell/bar/Sigil.qml @@ -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(); } } } diff --git a/quickshell/bar/Status.qml b/quickshell/bar/Status.qml index e93a4dd..680355a 100644 --- a/quickshell/bar/Status.qml +++ b/quickshell/bar/Status.qml @@ -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) } diff --git a/quickshell/bar/Workspaces.qml b/quickshell/bar/Workspaces.qml index a0ebf6a..bc3fd6c 100644 --- a/quickshell/bar/Workspaces.qml +++ b/quickshell/bar/Workspaces.qml @@ -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 diff --git a/quickshell/components/P5Backdrop.qml b/quickshell/components/P5Backdrop.qml new file mode 100644 index 0000000..298b41c --- /dev/null +++ b/quickshell/components/P5Backdrop.qml @@ -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 } + } +} diff --git a/quickshell/components/P5Panel.qml b/quickshell/components/P5Panel.qml index b7753bc..ab1b0a2 100644 --- a/quickshell/components/P5Panel.qml +++ b/quickshell/components/P5Panel.qml @@ -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,93 +163,77 @@ 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 { - anchors.fill: parent - source: Theme.texHalftone - fillMode: Image.Tile - opacity: root.halftoneOpacity - smooth: false - layer.enabled: true - layer.effect: MultiEffect { - 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) - } + sourceComponent: Image { + anchors.fill: parent + source: Theme.texHalftone + fillMode: Image.Tile + opacity: root.halftoneOpacity + smooth: false + layer.enabled: true + layer.effect: MultiEffect { + colorization: 1.0 + colorizationColor: root.halftoneColor + maskEnabled: true + 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 { - preferredRendererType: Shape.CurveRenderer - ShapePath { - fillColor: root.slashColor - strokeColor: "transparent" - PathPolyline { - path: { - const t = root.slashWidth; + sourceComponent: Item { + Shape { + id: slashShape + anchors.fill: parent + visible: false + layer.enabled: true + layer.smooth: true + preferredRendererType: Shape.CurveRenderer - // 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; + 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. + if (root.vertical) { + const voff = root.width * root.lean; + return root.leanLeft + ? 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(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)]); + ? 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)]); } - - 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 + } } } diff --git a/quickshell/config/Theme.qml b/quickshell/config/Theme.qml index 9782be1..9d58668 100644 --- a/quickshell/config/Theme.qml +++ b/quickshell/config/Theme.qml @@ -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" } diff --git a/quickshell/lock/Lock.qml b/quickshell/lock/Lock.qml index 1c15484..f746dc6 100644 --- a/quickshell/lock/Lock.qml +++ b/quickshell/lock/Lock.qml @@ -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 } } diff --git a/quickshell/lock/LockFace.qml b/quickshell/lock/LockFace.qml index 41d1d5d..6709e75 100644 --- a/quickshell/lock/LockFace.qml +++ b/quickshell/lock/LockFace.qml @@ -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 diff --git a/quickshell/overlays/Osd.qml b/quickshell/overlays/Osd.qml index ac6e303..2b653f4 100644 --- a/quickshell/overlays/Osd.qml +++ b/quickshell/overlays/Osd.qml @@ -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 diff --git a/quickshell/overlays/PowerMenu.qml b/quickshell/overlays/PowerMenu.qml index 2aefbf3..77c6e04 100644 --- a/quickshell/overlays/PowerMenu.qml +++ b/quickshell/overlays/PowerMenu.qml @@ -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 { diff --git a/quickshell/services/Wallpaper.qml b/quickshell/services/Wallpaper.qml new file mode 100644 index 0000000..a48ce49 --- /dev/null +++ b/quickshell/services/Wallpaper.qml @@ -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(); + } + } +} diff --git a/quickshell/shell.qml b/quickshell/shell.qml index 6457ae8..1a25566 100644 --- a/quickshell/shell.qml +++ b/quickshell/shell.qml @@ -42,12 +42,6 @@ ShellRoot { Rail {} } - Variants { - model: Quickshell.screens - - Dock {} - } - Variants { model: Quickshell.screens