theme: calm YoRHa lock screen and power menu; nvim and btop follow

The lock screen and the power menu lose the last Persona motion: no split
display type, no grid sweeping in behind a dark column, no tilted slabs, no
skewed password blocks or hard shake. Both are now one small group of type in
the dark — the porthole mark, a line of light display type, a status line, and
either a rounded password field that fills with dots or a NieR menu whose
selected row is a pill of light (rust for the session-ending three). The lock
keeps the wallpaper almost whole; a wrong password nudges the field once and
turns it to rust. P5Backdrop and the grid texture had no users left and go.

Neovim moves to a local yorha colorscheme (nvim submodule) and btop gets a
yorha theme with hairline boxes and grey-to-light gradients.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nvrlandClaude Opus 5.5 committed 2026-10-04 21:09:07 +02:00
1 parent a8c19961e2
commit 63a910bf14
11 files changed
+558 -542

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. #* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" #* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
color_theme = "/home/narl/.config/btop/themes/catppuccin_mocha.theme" color_theme = "/home/narl/.config/btop/themes/yorha.theme"
#* If the theme set background should be shown, set to False if you want terminal background transparency. #* If the theme set background should be shown, set to False if you want terminal background transparency.
theme_background = false theme_background = false
+60
View File
@@ -0,0 +1,60 @@
# YoRHa dark — matched to ~/.config/quickshell/config/Theme.qml and
# ~/.config/alacritty/yorha.toml.
#
# The void ground and sage text of the space-1 wallpaper. Boxes are hairline
# grey, not four competing hues; the light marks what is selected. Every
# gradient climbs from the suit's grey up to the light, and only temperature
# and load run on into the hull's rust, the shell's one colour for "wrong".
theme[main_bg]="#0f110e"
theme[main_fg]="#ccd2cb"
theme[title]="#eef1ea"
theme[hi_fg]="#eef1ea"
theme[selected_bg]="#20241f"
theme[selected_fg]="#eef1ea"
theme[inactive_fg]="#545c52"
theme[graph_text]="#9fa7a2"
theme[meter_bg]="#20241f"
theme[proc_misc]="#9fa7a2"
theme[cpu_box]="#383e35"
theme[mem_box]="#383e35"
theme[net_box]="#383e35"
theme[proc_box]="#383e35"
theme[div_line]="#2a2f28"
theme[temp_start]="#8e978d"
theme[temp_mid]="#c9b27c"
theme[temp_end]="#c8684f"
theme[cpu_start]="#79847a"
theme[cpu_mid]="#bcc1bf"
theme[cpu_end]="#c8684f"
theme[free_start]="#545c52"
theme[free_mid]="#79847a"
theme[free_end]="#9fa7a2"
theme[cached_start]="#545c52"
theme[cached_mid]="#7f9aa8"
theme[cached_end]="#96afbc"
theme[available_start]="#545c52"
theme[available_mid]="#86a8a0"
theme[available_end]="#9cbdb4"
theme[used_start]="#79847a"
theme[used_mid]="#bcc1bf"
theme[used_end]="#eef1ea"
theme[download_start]="#545c52"
theme[download_mid]="#8fa37e"
theme[download_end]="#a5b893"
theme[upload_start]="#545c52"
theme[upload_mid]="#c9b27c"
theme[upload_end]="#d8c491"
theme[process_start]="#79847a"
theme[process_mid]="#9fa7a2"
theme[process_end]="#eef1ea"
+1 -1
Submodule nvim updated: 4736228253...19e0db60a1.
+21 -9
View File
@@ -52,9 +52,10 @@ its own panel.
The shell uses three deliberate surface tiers. **Chrome** (the hull and The shell uses three deliberate surface tiers. **Chrome** (the hull and
`ChromeSurface`) is the calm, texture-free bar, rail, and tooltip layer. **Transient** `ChromeSurface`) is the calm, texture-free bar, rail, and tooltip layer. **Transient**
(`PopoutSurface`) is reserved for expressive menus and detail panels, with one (`PopoutSurface`) is reserved for expressive menus and detail panels, with one
clear composition and stronger separation from the desktop. **Cinematic** clear composition and stronger separation from the desktop. **Scene**
(`P5Backdrop`) belongs only to scene-scale experiences such as power and lock; surfaces — the lock screen and the power menu — have no panel at all: the
it is never used as decoration inside ordinary chrome or popouts. desktop or the wallpaper dims, and one small group of type (the mark, a line of
thin display type, a short log, a menu or a field) stands in the dark.
``` ```
shell.qml entrypoint — hull, bar, rail, dashboard, power menu, toasts, OSD per screen shell.qml entrypoint — hull, bar, rail, dashboard, power menu, toasts, OSD per screen
@@ -217,6 +218,12 @@ Authenticates through `/etc/pam.d/hyprlock`, which on this box is just
styled in an ordinary window (`qs -p lockpreview.qml`) without locking the styled in an ordinary window (`qs -p lockpreview.qml`) without locking the
session. session.
It is the wallpaper, barely softened, with the face set in the dark of the
porthole: the mark, the time in light display type, the date, a status log
(battery, network, what is playing, unread notices), and a rounded field that
fills with dots as you type. The dots breathe while PAM checks; a wrong
password turns them and the field's rim to rust and nudges the field once.
If the shell ever dies while the session is locked, the compositor keeps the If the shell ever dies while the session is locked, the compositor keeps the
session secured by protocol and there is no unlocker left to talk to — recovery session secured by protocol and there is no unlocker left to talk to — recovery
means restarting Hyprland from a TTY. means restarting Hyprland from a TTY.
@@ -270,9 +277,9 @@ Borders are structure and stay neutral.
`Theme.tracking`, on a one-pixel soft drop. The two ghosts either side of it `Theme.tracking`, on a one-pixel soft drop. The two ghosts either side of it
appear only while `glitch()` runs — NieR's brief data-corruption flicker. appear only while `glitch()` runs — NieR's brief data-corruption flicker.
**Texture is tiered.** Chrome (bar, rail, tooltips) and transient panels **Texture is rare.** Chrome (bar, rail, tooltips), transient panels (popouts,
(popouts, OSD, toasts) are flat. The hairline grid and the dot wash live only in OSD, toasts) and the two scene surfaces are flat. Depth comes from the hull's
`P5Backdrop`, behind the lock screen and the power menu. dark, the rim hairline and the wallpaper, not from patterns.
**Type has a floor.** Nothing renders below `Theme.fsMicro` (10px). **Type has a floor.** Nothing renders below `Theme.fsMicro` (10px).
@@ -284,6 +291,8 @@ appear only while `glitch()` runs — NieR's brief data-corruption flicker.
| `hypr/lua/palette.lua` | window borders, groups | | `hypr/lua/palette.lua` | window borders, groups |
| `alacritty/yorha.toml` | the terminal | | `alacritty/yorha.toml` | the terminal |
| `fish/conf.d/yorha_theme.fish` | the prompt and pager | | `fish/conf.d/yorha_theme.fish` | the prompt and pager |
| `nvim/lua/yorha.lua` | the editor and its statusline |
| `btop/themes/yorha.theme` | the resource monitor |
Everything else derives. `glow` *is* `accent`, and `primary` is plain `text`. Everything else derives. `glow` *is* `accent`, and `primary` is plain `text`.
`Theme.heat()` is a continuous text → light → red ramp, so a CPU figure that `Theme.heat()` is a continuous text → light → red ramp, so a CPU figure that
@@ -308,9 +317,12 @@ network and bluetooth, but keep running as the secret and pairing agents.
Alacritty only live-reloads when `alacritty.toml` itself changes, so touch Alacritty only live-reloads when `alacritty.toml` itself changes, so touch
it after editing the palette. it after editing the palette.
The Catppuccin and `takemi.toml` files are left in place, unreferenced. The Catppuccin and `takemi.toml` files are left in place, unreferenced.
- **Neovim** — still the `catppuccin` plugin, flavour mocha, configured in - **Neovim** — `nvim/lua/yorha.lua` (loaded by `colors/yorha.lua`, with a
`nvim/lua/plugins/ui.lua`. Not yet moved to the YoRHa palette. matching lualine theme). Syntax in the terminal's earth hues, floats as hull
- **GTK and Qt** — still `catppuccin-mocha-blue`. Not yet moved either. surfaces with rounded rims, menus selecting with the light slab.
- **btop** — `btop/themes/yorha.theme`. Hairline boxes; gradients climb from
grey to the light, and only temperature and CPU load run on into rust.
- **GTK and Qt** — still `catppuccin-mocha-blue`. Not yet moved.
- **Fish** — `fish/conf.d/yorha_theme.fish`. - **Fish** — `fish/conf.d/yorha_theme.fish`.
- **The launcher** — no external theming to keep in step, because it is part of - **The launcher** — no external theming to keep in step, because it is part of
this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for
Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

-91
View File
@@ -1,91 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// The shared ground for the two full-screen surfaces, in the NieR menu
// pattern: a hairline grid that sweeps in from the left, a darker column down
// the left-hand side for the menu or the clock to stand in, and a dot wash
// over the top.
//
// The lock screen and the power menu both stand on this one component, which
// 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 grid slide, the grid
// opacity and the column 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
// The column: a band of the void, edged by a hairline of the light.
// `wedge*` names are kept from the Persona layout so callers did not have
// to move; there is nothing wedge-shaped left.
property bool wedge: true
property real wedgeOpacity: 0.35
property real wedgeLeft: 0
property real wedgeSpan: 0.34
property bool stripes: true
property real stripeOpacity: 0.05
property bool halftone: true
property real halftoneOpacity: 0.05
property color halftoneColor: Theme.primary
// Hairline grid, sliding in from off-screen left.
Image {
anchors.fill: parent
visible: root.stripes && root.reveal > 0
source: Theme.texGrid
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 column. Drawn after the grid so it reads as a panel laid over it.
Item {
x: root.wedgeLeft * root.width
width: root.wedgeSpan * root.width
height: root.height
visible: root.wedge && root.reveal > 0
opacity: root.reveal
Rectangle {
anchors.fill: parent
color: Theme.alpha(Theme.ink, root.wedgeOpacity)
}
// Its edge, drawn top-down as the column arrives.
Rectangle {
anchors.right: parent.right
width: 1
height: parent.height * root.reveal
color: Theme.alpha(root.tone, 0.35)
}
}
// Dot wash over the whole thing.
Image {
anchors.fill: parent
visible: root.halftone
source: Theme.texDots
fillMode: Image.Tile
opacity: root.halftoneOpacity
smooth: false
layer.enabled: true
layer.effect: TintEffect { tintColor: root.halftoneColor }
}
}
+1 -3
View File
@@ -240,10 +240,8 @@ Singleton {
readonly property var easeIn: [0.55, 0.0, 0.9, 0.35, 1, 1] readonly property var easeIn: [0.55, 0.0, 0.9, 0.35, 1, 1]
// -------------------------------------------------------------- textures // -------------------------------------------------------------- textures
// A 1px dot every 8px, and a 32px hairline grid — the two backgrounds the // A 1px dot every 8px, the ground NieR's menus are printed on.
// NieR menus are printed on.
readonly property url texDots: "root:/assets/dots.png" readonly property url texDots: "root:/assets/dots.png"
readonly property url texGrid: "root:/assets/grid.png"
readonly property url texScanline: "root:/assets/scanline.png" readonly property url texScanline: "root:/assets/scanline.png"
// Last resort only. The live wallpaper comes from the Wallpaper singleton, // Last resort only. The live wallpaper comes from the Wallpaper singleton,
+269 -242
View File
@@ -10,6 +10,13 @@ import "root:/lock"
// The lock screen's whole face, kept separate from WlSessionLockSurface so it // The lock screen's whole face, kept separate from WlSessionLockSurface so it
// can be instantiated in an ordinary window for development without actually // can be instantiated in an ordinary window for development without actually
// locking the session. // locking the session.
//
// Quiet on purpose. The wallpaper stays almost whole — the porthole, the
// planet, the girl looking out — and the face is one small group of type set
// in the dark of the porthole: the mark, the time, the date, a short status
// log, and a rounded field to type into. Nothing sweeps, splits or shakes;
// the group resolves in, and a wrong password nudges it once and turns its
// dots to rust.
Item { Item {
id: surface id: surface
@@ -20,12 +27,12 @@ Item {
property string status: "" property string status: ""
property bool failed: false property bool failed: false
property bool busy: pam.active property bool busy: pam.active
readonly property real uiScale: Math.max(0.72, Math.min(1.15, readonly property real uiScale: Math.max(0.72, Math.min(1.3,
Math.min(width / 1920, height / 1080))) Math.min(width / 1920, height / 1080)))
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Theme.base color: Theme.ink
} }
// ------------------------------------------------------- background // ------------------------------------------------------- background
@@ -43,240 +50,254 @@ Item {
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: false asynchronous: false
cache: true cache: true
opacity: 0
// Barely softened: enough that the scene steps back behind the type,
// not so much that it stops being a place.
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
blurEnabled: true blurEnabled: true
blur: 0.55 blur: 0.2
blurMax: 48 blurMax: 32
saturation: -0.15 saturation: -0.1
brightness: -0.35 brightness: -0.18
} }
} }
// Wash pulling the photo down toward the shell's ground. Was described as // Shade deepening toward the left, where the type sits in the porthole's
// a teal wash back when the UI was teal; the colours it actually uses are // dark, and a breath of it along the foot.
// the neutral surfaces, which is what it should have been all along.
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
gradient: Gradient { gradient: Gradient {
GradientStop { position: 0.0; color: Theme.alpha(Theme.deep, 0.55) } orientation: Gradient.Horizontal
GradientStop { position: 0.6; color: Theme.alpha(Theme.base, 0.75) } GradientStop { position: 0.0; color: Theme.alpha(Theme.ink, 0.72) }
GradientStop { position: 1.0; color: Theme.alpha(Theme.base, 0.92) } GradientStop { position: 0.45; color: Theme.alpha(Theme.ink, 0.28) }
GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.18) }
} }
} }
// The same wedge-and-stripes ground the power menu stands on. The wedge Rectangle {
// sits under the clock, which is where the power menu puts it under the
// title — so the two screens land the accent mass in the same place.
P5Backdrop {
id: backdrop
anchors.fill: parent anchors.fill: parent
reveal: 0 gradient: Gradient {
wedgeSpan: 0.30 GradientStop { position: 0.6; color: "transparent" }
wedgeOpacity: 0.30 GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.5) }
stripeOpacity: 0.04 }
} }
// ------------------------------------------------------------ clock // ------------------------------------------------------------- face
SystemClock { SystemClock {
id: clock id: clock
precision: SystemClock.Minutes precision: SystemClock.Minutes
} }
// The house mark, the same porthole the bar carries far left. Small, above Column {
// the clock — the lock's hero is the time, not the branding. id: face
x: parent.width * 0.14
y: parent.height * 0.2
width: 380
scale: surface.uiScale
transformOrigin: Item.TopLeft
opacity: 0
spacing: 0
transform: Translate { id: drift }
// The house mark, the same porthole the bar carries far left.
Porthole { Porthole {
id: mark size: 26
anchors.left: timeBlock.left
anchors.bottom: timeBlock.top
anchors.bottomMargin: 10
size: 28
opacity: timeBlock.opacity
}
Column {
id: timeBlock
anchors.left: parent.left
anchors.leftMargin: parent.width * 0.1
anchors.top: parent.top
anchors.topMargin: parent.height * 0.18
spacing: -18
SplitText {
text: Qt.formatDateTime(clock.date, "HH")
pixelSize: Theme.fsMega * 1.7
split: 5
}
SplitText {
text: Qt.formatDateTime(clock.date, "mm")
pixelSize: Theme.fsMega * 1.7
color: Theme.primary
split: 5
} }
Text { Text {
text: Qt.formatDateTime(clock.date, "dddd, dd MMMM").toUpperCase() topPadding: Theme.padM
text: Qt.formatDateTime(clock.date, "HH:mm")
color: Theme.accent
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsMega * 1.35
font.weight: Font.Light
font.letterSpacing: 2
// Scaled with the face, so distance-field glyphs: native hinting
// at this size under a scale transform comes out stepped.
renderType: Text.QtRendering
}
Text {
leftPadding: 4
text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase()
color: Theme.subtext color: Theme.subtext
font.family: Theme.fontMono font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Theme.fsBody
font.letterSpacing: 6 font.weight: Theme.weightDisplay
topPadding: 34
renderType: Text.NativeRendering
}
}
// ------------------------------------------------------ status rail
Column {
anchors.right: parent.right
anchors.rightMargin: parent.width * 0.1
anchors.top: parent.top
anchors.topMargin: parent.height * 0.2
spacing: Theme.padM
width: Math.min(300, parent.width * 0.28)
scale: surface.uiScale
transformOrigin: Item.TopRight
RailRow {
width: parent.width
visible: Battery.available
glyph: Battery.icon
label: Math.round(Battery.percent) + "%"
detail: Battery.timeLabel
tone: Battery.tint
}
RailRow {
width: parent.width
glyph: Net.icon
label: Net.label
detail: Sys.netUp ? Sys.netIp : "no link"
tone: Sys.netUp ? Theme.primary : Theme.muted
}
RailRow {
width: parent.width
visible: Media.hasPlayer
glyph: Media.playing ? "󰝚" : "󰏤"
label: Media.title
detail: Media.artist
tone: Theme.primary
}
RailRow {
width: parent.width
visible: Notifs.count > 0
glyph: "󱅫"
label: Notifs.count + " notification" + (Notifs.count === 1 ? "" : "s")
detail: Notifs.history.length > 0 ? Notifs.history[0].summary : ""
tone: Theme.accent
}
}
// ------------------------------------------------------- auth panel
Item {
id: authBlock
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: parent.height * 0.14
width: Math.min(460, parent.width - Theme.padXL * 2)
height: 150
scale: surface.uiScale
transformOrigin: Item.Bottom
transform: Translate { id: shakeShift }
SplitText {
id: prompt
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
text: surface.failed
? (surface.status || "WRONG").toUpperCase()
: (surface.busy ? "CHECKING…" : "WHO GOES THERE")
color: surface.failed ? Theme.alarm : Theme.text
pixelSize: Theme.fsTitle
letterSpacing: 4
horizontalAlignment: Text.AlignHCenter
split: surface.failed ? 4 : 1.6
}
// Password field: one chopped block per character.
P5Panel {
id: field
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: prompt.bottom
anchors.topMargin: Theme.padL
width: Math.min(380, authBlock.width - Theme.padL * 2)
height: 56
fill: Theme.mantle
fillOpacity: 0.94
border: surface.failed
? Theme.accent
: (input.activeFocus ? Theme.primary : Theme.outline)
borderWidth: 2
slash: true
slashColor: surface.failed ? Theme.alarm : Theme.primary
slashWidth: 6
halftone: true
halftoneColor: surface.failed ? Theme.alarm : Theme.primary
halftoneOpacity: 0.08
Row {
anchors.centerIn: parent
spacing: 6
visible: surface.entry.length > 0
Repeater {
model: Math.min(surface.entry.length, 22)
Skew {
required property int index
width: 10
height: 20
color: surface.failed ? Theme.alarm : Theme.primary
// Each new block snaps in.
Component.onCompleted: pop.start()
NumberAnimation {
id: pop
target: parent
property: "scale"
from: 0.2; to: 1
duration: Theme.durBase
easing.type: Easing.OutCubic
}
}
}
}
Text {
anchors.centerIn: parent
visible: surface.entry.length === 0 && !surface.busy
text: "PASSWORD"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 5 font.letterSpacing: 5
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
// Busy sweep. Item { width: 1; height: Theme.padXL }
Rectangle {
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
width: 60
height: 3
color: Theme.primary
visible: surface.busy
XAnimator on x { // ------------------------------------------------------ status log
Column {
width: parent.width
leftPadding: 4
StatusRow {
width: parent.width - 4
visible: Battery.available
label: "Battery"
detail: Math.round(Battery.percent) + "%"
+ (Battery.timeLabel ? " " + Battery.timeLabel : "")
tone: Battery.tint
}
StatusRow {
width: parent.width - 4
label: "Network"
detail: Sys.netUp ? Net.label : "no link"
tone: Sys.netUp ? Theme.accentSoft : Theme.overlay
}
StatusRow {
width: parent.width - 4
visible: Media.hasPlayer
label: Media.playing ? "Playing" : "Paused"
detail: Media.artist ? Media.title + " — " + Media.artist : Media.title
tone: Media.playing ? Theme.accent : Theme.primaryDim
}
StatusRow {
width: parent.width - 4
visible: Notifs.count > 0
label: "Notices"
detail: Notifs.count + (Notifs.count === 1 ? " message" : " messages")
tone: Theme.accentSoft
}
}
Item { width: 1; height: Theme.padXL }
// ------------------------------------------------------ the field
Item {
id: authBlock
width: parent.width
height: field.height + hint.height + Theme.padS
transform: Translate { id: nudge }
Rectangle {
id: field
width: parent.width
height: 48
radius: height / 2
color: Theme.alpha(Theme.hull, 0.86)
border.width: 1
border.color: surface.failed
? Theme.alarm
: (input.activeFocus && surface.entry.length > 0 ? Theme.outline : Theme.rim)
Behavior on border.color {
ColorAnimation { duration: Theme.durBase }
}
Icon {
id: lockGlyph
anchors.left: parent.left
anchors.leftMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
text: "󰌾"
color: surface.failed ? Theme.alarm : Theme.muted
font.pixelSize: Theme.fsLarge
}
Text {
anchors.left: lockGlyph.right
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
visible: surface.entry.length === 0
text: "PASSWORD"
color: Theme.muted
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.letterSpacing: Theme.tracking * 2
renderType: Text.NativeRendering
}
// One dot per character, each resolving in as it is typed.
// They breathe while PAM is checking.
Row {
id: dots
anchors.left: lockGlyph.right
anchors.leftMargin: Theme.padM
anchors.right: enter.left
anchors.rightMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: 7
clip: true
Repeater {
model: Math.min(surface.entry.length, 28)
Rectangle {
id: pip
required property int index
anchors.verticalCenter: parent.verticalCenter
width: 7
height: 7
radius: width / 2
color: surface.failed ? Theme.alarm : Theme.accent
opacity: 0
scale: 0.4
Component.onCompleted: arrive.start()
ParallelAnimation {
id: arrive
NumberAnimation {
target: pip; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutCubic
}
NumberAnimation {
target: pip; property: "scale"
to: 1; duration: Theme.durBase; easing.type: Easing.OutCubic
}
}
}
}
SequentialAnimation on opacity {
running: surface.busy running: surface.busy
loops: Animation.Infinite loops: Animation.Infinite
from: 20 alwaysRunToEnd: true
to: field.width - 80 NumberAnimation { to: 0.35; duration: 520; easing.type: Easing.InOutSine }
duration: 700 NumberAnimation { to: 1; duration: 520; easing.type: Easing.InOutSine }
easing.type: Easing.InOutQuad }
}
// Submit: a disc at the field's end that lights once there is
// something to send.
Rectangle {
id: enter
readonly property bool ready: surface.entry.length > 0 && !surface.busy
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
width: 32
height: 32
radius: width / 2
color: enter.ready ? Theme.accent : "transparent"
border.width: enter.ready ? 0 : 1
border.color: Theme.rim
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
Icon {
anchors.centerIn: parent
text: "󰁔"
color: enter.ready ? Theme.ink : Theme.overlay
font.pixelSize: Theme.fsLarge
}
TapHandler {
onTapped: surface.submit()
} }
} }
@@ -292,7 +313,7 @@ Item {
onTextChanged: { onTextChanged: {
surface.entry = text; surface.entry = text;
if (surface.failed && text.length === 0) surface.failed = false; if (surface.failed && text.length > 0) surface.failed = false;
} }
onAccepted: surface.submit() onAccepted: surface.submit()
@@ -304,30 +325,38 @@ Item {
} }
} }
// Under the field: who is locked, what is happening, or what went
// wrong — one line, never more.
Row { Row {
anchors.horizontalCenter: parent.horizontalCenter id: hint
anchors.top: field.bottom anchors.top: field.bottom
anchors.topMargin: Theme.padM anchors.topMargin: Theme.padS
anchors.left: parent.left
anchors.leftMargin: Theme.padL
spacing: Theme.padS spacing: Theme.padS
Icon { Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "󰌾" width: 4
color: Theme.muted height: 4
font.pixelSize: Theme.fsBody radius: 2
color: surface.failed ? Theme.alarm : Theme.overlay
} }
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Quickshell.env("USER") || "USER" text: surface.failed
color: Theme.muted ? (surface.status || "Access denied").toUpperCase()
: (surface.busy ? "VERIFYING" : (Quickshell.env("USER") || "user"))
color: surface.failed ? Theme.alarm : Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall font.pixelSize: Theme.fsMicro
font.letterSpacing: 3 font.letterSpacing: 2
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
} }
} }
}
// Keep focus on the field no matter what. // Keep focus on the field no matter what.
Timer { Timer {
@@ -362,62 +391,60 @@ Item {
surface.entry = ""; surface.entry = "";
surface.failed = false; surface.failed = false;
} else { } else {
surface.failed = true;
if (surface.status === "") surface.status = "Access denied";
input.text = ""; input.text = "";
surface.entry = ""; surface.entry = "";
shake.restart(); surface.failed = true;
if (surface.status === "") surface.status = "Access denied";
refuse.restart();
} }
} }
onError: err => { onError: err => {
surface.failed = true;
surface.status = "PAM error";
input.text = ""; input.text = "";
surface.entry = ""; surface.entry = "";
shake.restart(); surface.failed = true;
surface.status = "PAM error";
refuse.restart();
} }
} }
// A wrong password: the field leans away once and comes back, a head
// shaken slowly rather than a buzzer.
SequentialAnimation { SequentialAnimation {
id: shake id: refuse
loops: 2
NumberAnimation { NumberAnimation {
target: shakeShift; property: "x" target: nudge; property: "x"
to: -16; duration: 55; easing.type: Easing.OutQuad to: -7; duration: 90; easing.type: Easing.OutSine
} }
NumberAnimation { NumberAnimation {
target: shakeShift; property: "x" target: nudge; property: "x"
to: 16; duration: 55; easing.type: Easing.OutQuad to: 5; duration: 140; easing.type: Easing.InOutSine
} }
NumberAnimation { NumberAnimation {
target: shakeShift; property: "x" target: nudge; property: "x"
to: 0; duration: 90; easing.type: Easing.OutCubic to: 0; duration: 220; easing.type: Easing.OutCubic
} }
} }
// Entry flourish when the lock comes up. // The lock comes up the way the scene would if you opened your eyes: the
// picture first, then the type settling a few pixels into place.
Component.onCompleted: intro.start() Component.onCompleted: intro.start()
ParallelAnimation { SequentialAnimation {
id: intro id: intro
// The stripes sweep in under everything, the same move the power menu
// opens with.
NumberAnimation { NumberAnimation {
target: backdrop; property: "reveal" target: wall; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
}
ParallelAnimation {
NumberAnimation {
target: face; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
} }
NumberAnimation { NumberAnimation {
target: timeBlock; property: "opacity" target: drift; property: "y"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo from: 14; to: 0; duration: Theme.durLazy; easing.type: Easing.OutQuint
} }
NumberAnimation {
target: authBlock; property: "opacity"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: shakeShift; property: "y"
from: 40; to: 0; duration: Theme.durLazy; easing.type: Easing.OutCubic
} }
} }
} }
-63
View File
@@ -1,63 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// One line of the lock screen's status rail.
Item {
id: rail
property string glyph: ""
property string label: ""
property string detail: ""
property color tone: Theme.primary
implicitHeight: visible ? 38 : 0
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: 26
color: rail.tone
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: rail.glyph
color: rail.tone
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -2
Text {
text: rail.label
color: Theme.text
elide: Text.ElideRight
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
Text {
visible: rail.detail !== ""
text: rail.detail
color: Theme.muted
elide: Text.ElideRight
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
}
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import "root:/config"
import "root:/components"
// One line of the lock screen's status log, laid out like the dashboard's
// facts: a dot in the line's tone, the label tracked in caps, a hairline
// running out to the detail at the far end.
Item {
id: row
property string label: ""
property string detail: ""
property color tone: Theme.primaryDim
implicitHeight: visible ? 26 : 0
Rectangle {
id: dot
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Theme.marker
height: Theme.marker
radius: width / 2
color: row.tone
}
Text {
id: name
anchors.left: dot.right
anchors.leftMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, row.width * 0.55)
text: row.label.toUpperCase()
color: Theme.text
elide: Text.ElideRight
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
font.letterSpacing: Theme.tracking
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: name.right
anchors.leftMargin: Theme.padS
anchors.right: value.left
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.4)
}
Text {
id: value
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, row.width * 0.4)
text: row.detail
color: Theme.muted
elide: Text.ElideRight
horizontalAlignment: Text.AlignRight
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
+139 -132
View File
@@ -1,12 +1,16 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Io
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
import "root:/services" import "root:/services"
// Full-screen power menu. An accent wedge and stripes sweep in behind a stack of leaning // Full-screen power menu, built like the lock screen: the desktop dims to the
// buttons; Escape or a click on the backdrop dismisses it. // void, and one small group of type stands in it — the mark, the motto, a
// line about the session, and a NieR menu of six rows. The selected row is a
// pill of light with the text punched out of it; the three that end the
// session light in rust instead. Escape or a click on the dark dismisses it.
PanelWindow { PanelWindow {
id: root id: root
@@ -32,16 +36,9 @@ PanelWindow {
property int selectedAction: 0 property int selectedAction: 0
function focusSelectedAction(): void {
const entry = buttonRepeater.itemAt(root.selectedAction);
if (entry && entry.children.length > 0)
entry.children[0].forceActiveFocus(Qt.TabFocusReason);
}
function moveSelection(delta: int): void { function moveSelection(delta: int): void {
root.selectedAction = (root.selectedAction + delta + root.actionModel.length) root.selectedAction = (root.selectedAction + delta + root.actionModel.length)
% root.actionModel.length; % root.actionModel.length;
root.focusSelectedAction();
} }
readonly property var actionModel: [ readonly property var actionModel: [
@@ -53,6 +50,11 @@ PanelWindow {
{ label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" } { label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" }
] ]
FileView {
id: hostFile
path: "/etc/hostname"
}
onOpenChanged: { onOpenChanged: {
if (open) { if (open) {
PopupCoordinator.dismissAll(); PopupCoordinator.dismissAll();
@@ -64,128 +66,155 @@ PanelWindow {
} }
// ------------------------------------------------------------- backdrop // ------------------------------------------------------------- backdrop
// The desktop stays faintly there behind the dark, deepest on the left
// where the menu stands.
Rectangle { Rectangle {
id: backdrop id: backdrop
anchors.fill: parent anchors.fill: parent
color: Theme.alpha(Theme.base, 0.9)
opacity: 0 opacity: 0
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: Theme.alpha(Theme.ink, 0.97) }
GradientStop { position: 0.5; color: Theme.alpha(Theme.ink, 0.93) }
GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.86) }
}
TapHandler { TapHandler {
onTapped: Actions.closePowerMenu() onTapped: Actions.closePowerMenu()
} }
} }
// Grid and column, shared with the lock screen. // ---------------------------------------------------------------- group
P5Backdrop {
id: ground
anchors.fill: parent
reveal: 0
stripeOpacity: 0.05
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
Column { Column {
id: title id: group
anchors.left: parent.left x: root.width * 0.14
anchors.leftMargin: root.width * 0.12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -60 width: 340
spacing: -14
opacity: 0 opacity: 0
SplitText { transform: Translate { id: drift }
text: "GLORY"
pixelSize: Theme.fsMega
split: 4
}
SplitText { Porthole {
text: "TO" size: 26
pixelSize: Theme.fsMega
color: Theme.accent
split: 4
}
SplitText {
text: "MANKIND."
pixelSize: Theme.fsMega
split: 4
} }
Text { Text {
text: "— YORHA" topPadding: Theme.padM
color: Theme.primary text: "GLORY TO MANKIND."
font.family: Theme.fontMono color: Theme.accent
font.pixelSize: Theme.fsSmall font.family: Theme.fontDisplay
font.letterSpacing: 5 font.pixelSize: Theme.fsHuge
topPadding: 24 font.weight: Font.Light
font.letterSpacing: 6
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Text {
topPadding: Theme.padS
leftPadding: 2
text: ["YORHA", (Quickshell.env("USER") || "user") + "@" + hostFile.text().trim(),
Sys.uptime ? "UP " + Sys.uptime : ""].filter(t => t !== "").join(" · ")
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
} }
// -------------------------------------------------------------- actions Item { width: 1; height: Theme.padXL }
Column {
id: actions
anchors.right: parent.right
anchors.rightMargin: root.width * 0.12
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padM
// The widest slab in the stack. Fixed rather than derived so the ragged
// x offsets below cannot feed back into the Column's own width.
readonly property int stackWidth: 312
width: stackWidth
// ------------------------------------------------------------ menu
Repeater { Repeater {
id: buttonRepeater id: buttonRepeater
model: root.actionModel model: root.actionModel
// A carrier item owns the staggered slide so the button keeps its
// own hover transform.
Item { Item {
id: entry id: entry
required property var modelData required property var modelData
required property int index required property int index
readonly property int slabWidth: actions.stackWidth readonly property bool selected: root.selectedAction === entry.index
readonly property color tone: entry.modelData.danger ? Theme.alarm : Theme.accent
width: entry.slabWidth
height: 52
width: group.width
height: 44
opacity: 0 opacity: 0
transform: [
Translate { id: slide; x: 90 },
Rotation {
id: tilt
angle: -3
origin.x: entry.slabWidth / 2
origin.y: 26
}
]
P5Button { transform: Translate { id: rise; y: 6 }
id: actionButton
// The divider after the third row: what pauses the session
// above, what ends it below.
Rectangle {
visible: entry.index === 3
anchors.top: parent.top
x: Theme.padL
width: parent.width - Theme.padL * 2
height: 1
color: Theme.alpha(Theme.outline, 0.35)
}
Rectangle {
id: slab
anchors.fill: parent anchors.fill: parent
text: entry.modelData.label anchors.topMargin: 3
icon: entry.modelData.icon anchors.bottomMargin: 3
destructive: entry.modelData.danger radius: height / 2
selected: root.selectedAction === entry.index color: entry.selected ? entry.tone : "transparent"
accent: entry.modelData.danger ? Theme.alarm : Theme.primary
lean: Theme.skew Behavior on color {
onClicked: root.invoke(entry.modelData.act) ColorAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
onHoveredChanged: if (hovered) root.selectedAction = entry.index }
onActiveFocusChanged: if (actionButton.activeFocus) root.selectedAction = entry.index
Keys.forwardTo: [keyController]
} }
// Staggered entry, one button after another. Rectangle {
id: marker
anchors.left: parent.left
anchors.leftMargin: Theme.padL
anchors.verticalCenter: parent.verticalCenter
width: Theme.marker
height: Theme.marker
radius: width / 2
color: entry.selected ? Theme.ink
: (entry.modelData.danger ? Theme.alpha(Theme.alarm, 0.6) : Theme.overlay)
}
Icon {
id: glyph
anchors.left: marker.right
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
// Fixed width so the labels line up whatever the glyph.
width: 20
horizontalAlignment: Text.AlignHCenter
text: entry.modelData.icon
color: entry.selected ? Theme.ink : Theme.muted
font.pixelSize: Theme.fsLarge
}
Text {
anchors.left: glyph.right
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
text: entry.modelData.label.toUpperCase()
color: entry.selected ? Theme.ink : Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.letterSpacing: Theme.tracking * 1.5
renderType: Text.NativeRendering
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
onHoveredChanged: if (hovered) root.selectedAction = entry.index
}
TapHandler {
onTapped: root.invoke(entry.modelData.act)
}
// Staggered entry: each row resolves a beat after the last.
Connections { Connections {
target: root target: root
function onOpenChanged() { function onOpenChanged() {
@@ -196,49 +225,32 @@ PanelWindow {
SequentialAnimation { SequentialAnimation {
id: entryAnim id: entryAnim
PauseAnimation { duration: 40 + entry.index * 55 } PauseAnimation { duration: 120 + entry.index * 40 }
ParallelAnimation { ParallelAnimation {
NumberAnimation { NumberAnimation {
target: entry; property: "opacity" target: entry; property: "opacity"
to: 1; duration: Theme.durBase to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
} }
NumberAnimation { NumberAnimation {
target: slide; property: "x" target: rise; property: "y"
from: 90; to: 0 from: 6; to: 0; duration: Theme.durSlow; easing.type: Easing.OutCubic
duration: Theme.durSlow
easing.type: Easing.OutCubic
}
// Slabs land out of true and snap square. Sliding alone
// reads like a slide deck; the tilt is what makes it
// land like a card being thrown down. Same direction
// every time — the motion is the flourish, the geometry
// stays disciplined.
NumberAnimation {
target: tilt; property: "angle"
from: -3; to: 0
duration: Theme.durSlow
easing.type: Easing.OutCubic
easing.overshoot: 2.2
}
} }
} }
} }
} }
} }
// Hint line.
Text { Text {
anchors.bottom: parent.bottom topPadding: Theme.padXL
anchors.bottomMargin: 40 leftPadding: Theme.padL
anchors.horizontalCenter: parent.horizontalCenter
text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL" text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL"
color: Theme.muted color: Theme.overlay
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Theme.fsMicro
font.letterSpacing: 4 font.letterSpacing: 2
opacity: backdrop.opacity
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
}
function invoke(what: string) { function invoke(what: string) {
Actions.closePowerMenu(); Actions.closePowerMenu();
@@ -280,16 +292,15 @@ PanelWindow {
id: openAnim id: openAnim
NumberAnimation { NumberAnimation {
target: backdrop; property: "opacity" target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
}
// One number now drives the slide, the stripe opacity and the wedge.
NumberAnimation {
target: ground; property: "reveal"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
} }
NumberAnimation { NumberAnimation {
target: title; property: "opacity" target: group; property: "opacity"
to: 1; duration: Theme.durSlow to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
}
NumberAnimation {
target: drift; property: "y"
from: 14; to: 0; duration: Theme.durLazy; easing.type: Easing.OutQuint
} }
} }
@@ -297,14 +308,10 @@ PanelWindow {
id: closeAnim id: closeAnim
NumberAnimation { NumberAnimation {
target: backdrop; property: "opacity" target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast to: 0; duration: Theme.durBase; easing.type: Easing.InCubic
} }
NumberAnimation { NumberAnimation {
target: ground; property: "reveal" target: group; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: title; property: "opacity"
to: 0; duration: Theme.durFast to: 0; duration: Theme.durFast
} }
} }