Files
dotfiles/quickshell/lock/LockFace.qml
T
nvrlandClaude Opus 5.5 18091a7109 theme: YoRHa dark — NieR:Automata interface on the space-1 wallpaper
Replaces the Persona x Mocha rice. NieR's menu language is kept whole —
plain rectangles, hairline rules, letter-spaced upright capitals, a square
marker on the current row, corner ticks round panels, and selection shown
by inverting the row — but the colours come from ~/.wallpapers/space-1.jpg
rather than from the game's beige: the grey-green void as the ground, the
suit's sage grey as text, the planet's white as the one accent, and the
hull's red indicator lights as the alarm.

The Persona geometry is switched off rather than ripped out: Theme.skew
and Theme.cut are 0, so P5Panel, Skew and Geom collapse to rectangles.
Overshoot easing, italics and black weights are gone shell-wide.

New, borrowed from caelestia's screen border: bar/Frame.qml draws the bar,
the rail and a thin left/bottom band as one hull on the bottom layer, with
the desktop as a rounded opening cut out of it. Frame also owns all four
exclusive zones, so the bar and rail are placed from the screen edge.

Hyprland moves to 1px borders lit from the planet white into grey, a soft
wide shadow and gaps measured from the opening. Alacritty, fish and dunst
get matching YoRHa palettes. Neovim, GTK/Qt and btop are still Mocha.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:10:53 +02:00

424 lines
13 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Pam
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/lock"
// The lock screen's whole face, kept separate from WlSessionLockSurface so it
// can be instantiated in an ordinary window for development without actually
// locking the session.
Item {
id: surface
// Emitted once PAM has accepted the password.
signal unlocked()
property string entry: ""
property string status: ""
property bool failed: false
property bool busy: pam.active
readonly property real uiScale: Math.max(0.72, Math.min(1.15,
Math.min(width / 1920, height / 1080)))
Rectangle {
anchors.fill: parent
color: Theme.base
}
// ------------------------------------------------------- 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: surface.screenName !== ""
? Wallpaper.forMonitor(surface.screenName)
: Wallpaper.source
fillMode: Image.PreserveAspectCrop
asynchronous: false
cache: true
layer.enabled: true
layer.effect: MultiEffect {
blurEnabled: true
blur: 0.55
blurMax: 48
saturation: -0.15
brightness: -0.35
}
}
// 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 {
GradientStop { position: 0.0; color: Theme.alpha(Theme.deep, 0.55) }
GradientStop { position: 0.6; color: Theme.alpha(Theme.base, 0.75) }
GradientStop { position: 1.0; color: Theme.alpha(Theme.base, 0.92) }
}
}
// 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 accent mass in the same place.
P5Backdrop {
id: backdrop
anchors.fill: parent
reveal: 0
wedgeSpan: 0.30
wedgeOpacity: 0.30
stripeOpacity: 0.04
}
// ------------------------------------------------------------ clock
SystemClock {
id: clock
precision: SystemClock.Minutes
}
// The house mark, the same porthole the bar carries far left. Small, above
// the clock — the lock's hero is the time, not the branding.
Porthole {
id: mark
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: Qt.formatDateTime(clock.date, "dddd, dd MMMM").toUpperCase()
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsBody
font.letterSpacing: 6
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
renderType: Text.NativeRendering
}
// Busy sweep.
Rectangle {
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
width: 60
height: 3
color: Theme.primary
visible: surface.busy
XAnimator on x {
running: surface.busy
loops: Animation.Infinite
from: 20
to: field.width - 80
duration: 700
easing.type: Easing.InOutQuad
}
}
// The real input, invisible but focused.
TextInput {
id: input
anchors.fill: parent
opacity: 0
focus: true
echoMode: TextInput.Password
enabled: !surface.busy
activeFocusOnTab: true
onTextChanged: {
surface.entry = text;
if (surface.failed && text.length === 0) surface.failed = false;
}
onAccepted: surface.submit()
Keys.onEscapePressed: {
input.text = "";
surface.failed = false;
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: field.bottom
anchors.topMargin: Theme.padM
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰌾"
color: Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: Quickshell.env("USER") || "USER"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 3
renderType: Text.NativeRendering
}
}
}
// Keep focus on the field no matter what.
Timer {
interval: 250
running: true
repeat: true
onTriggered: if (!input.activeFocus && !surface.busy) input.forceActiveFocus()
}
function submit() {
if (surface.busy || surface.entry.length === 0) return;
surface.failed = false;
surface.status = "";
pam.start();
}
PamContext {
id: pam
config: "hyprlock"
user: Quickshell.env("USER") || "USER"
onPamMessage: {
if (pam.responseRequired) pam.respond(surface.entry);
else if (pam.message) surface.status = pam.message;
}
onCompleted: result => {
if (result === PamResult.Success) {
surface.unlocked();
input.text = "";
surface.entry = "";
surface.failed = false;
} else {
surface.failed = true;
if (surface.status === "") surface.status = "Access denied";
input.text = "";
surface.entry = "";
shake.restart();
}
}
onError: err => {
surface.failed = true;
surface.status = "PAM error";
input.text = "";
surface.entry = "";
shake.restart();
}
}
SequentialAnimation {
id: shake
loops: 2
NumberAnimation {
target: shakeShift; property: "x"
to: -16; duration: 55; easing.type: Easing.OutQuad
}
NumberAnimation {
target: shakeShift; property: "x"
to: 16; duration: 55; easing.type: Easing.OutQuad
}
NumberAnimation {
target: shakeShift; property: "x"
to: 0; duration: 90; easing.type: Easing.OutCubic
}
}
// Entry flourish when the lock comes up.
Component.onCompleted: intro.start()
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
}
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
}
}
}