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>
319 lines
11 KiB
QML
319 lines
11 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Io
|
|
import "root:/config"
|
|
import "root:/components"
|
|
import "root:/services"
|
|
|
|
// Full-screen power menu, built like the lock screen: the desktop dims to the
|
|
// 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 {
|
|
id: root
|
|
|
|
required property var modelData
|
|
screen: modelData
|
|
|
|
// Only the screen that had focus when the menu was asked for shows it; the
|
|
// other copies stay dormant so the keyboard has one place to land.
|
|
readonly property bool open: Actions.powerMenuOpen
|
|
&& Actions.powerMenuScreen === modelData.name
|
|
|
|
visible: root.open || closeAnim.running
|
|
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.namespace: "takemi-power"
|
|
WlrLayershell.keyboardFocus: root.open
|
|
? WlrKeyboardFocus.Exclusive
|
|
: WlrKeyboardFocus.None
|
|
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
color: "transparent"
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
property int selectedAction: 0
|
|
|
|
function moveSelection(delta: int): void {
|
|
root.selectedAction = (root.selectedAction + delta + root.actionModel.length)
|
|
% root.actionModel.length;
|
|
}
|
|
|
|
readonly property var actionModel: [
|
|
{ label: "Lock", icon: "", danger: false, act: "lock" },
|
|
{ label: "Suspend", icon: "", danger: false, act: "suspend" },
|
|
{ label: "Hibernate", icon: "", danger: false, act: "hibernate" },
|
|
{ label: "Log Out", icon: "", danger: true, act: "logout" },
|
|
{ label: "Reboot", icon: "", danger: true, act: "reboot" },
|
|
{ label: "Shut Down", icon: "", danger: true, act: "shutdown" }
|
|
]
|
|
|
|
FileView {
|
|
id: hostFile
|
|
path: "/etc/hostname"
|
|
}
|
|
|
|
onOpenChanged: {
|
|
if (open) {
|
|
PopupCoordinator.dismissAll();
|
|
selectedAction = 0;
|
|
openAnim.restart();
|
|
} else {
|
|
closeAnim.restart();
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------- backdrop
|
|
// The desktop stays faintly there behind the dark, deepest on the left
|
|
// where the menu stands.
|
|
Rectangle {
|
|
id: backdrop
|
|
anchors.fill: parent
|
|
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 {
|
|
onTapped: Actions.closePowerMenu()
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- group
|
|
Column {
|
|
id: group
|
|
x: root.width * 0.14
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 340
|
|
opacity: 0
|
|
|
|
transform: Translate { id: drift }
|
|
|
|
Porthole {
|
|
size: 26
|
|
}
|
|
|
|
Text {
|
|
topPadding: Theme.padM
|
|
text: "GLORY TO MANKIND."
|
|
color: Theme.accent
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsHuge
|
|
font.weight: Font.Light
|
|
font.letterSpacing: 6
|
|
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
|
|
}
|
|
|
|
Item { width: 1; height: Theme.padXL }
|
|
|
|
// ------------------------------------------------------------ menu
|
|
Repeater {
|
|
id: buttonRepeater
|
|
|
|
model: root.actionModel
|
|
|
|
Item {
|
|
id: entry
|
|
required property var modelData
|
|
required property int index
|
|
|
|
readonly property bool selected: root.selectedAction === entry.index
|
|
readonly property color tone: entry.modelData.danger ? Theme.alarm : Theme.accent
|
|
|
|
width: group.width
|
|
height: 44
|
|
opacity: 0
|
|
|
|
transform: Translate { id: rise; y: 6 }
|
|
|
|
// 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.topMargin: 3
|
|
anchors.bottomMargin: 3
|
|
radius: height / 2
|
|
color: entry.selected ? entry.tone : "transparent"
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
|
|
}
|
|
}
|
|
|
|
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 {
|
|
target: root
|
|
function onOpenChanged() {
|
|
if (root.open) entryAnim.restart();
|
|
else entry.opacity = 0;
|
|
}
|
|
}
|
|
|
|
SequentialAnimation {
|
|
id: entryAnim
|
|
PauseAnimation { duration: 120 + entry.index * 40 }
|
|
ParallelAnimation {
|
|
NumberAnimation {
|
|
target: entry; property: "opacity"
|
|
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
|
|
}
|
|
NumberAnimation {
|
|
target: rise; property: "y"
|
|
from: 6; to: 0; duration: Theme.durSlow; easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
topPadding: Theme.padXL
|
|
leftPadding: Theme.padL
|
|
text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL"
|
|
color: Theme.overlay
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fsMicro
|
|
font.letterSpacing: 2
|
|
renderType: Text.NativeRendering
|
|
}
|
|
}
|
|
|
|
function invoke(what: string) {
|
|
Actions.closePowerMenu();
|
|
switch (what) {
|
|
case "lock": Actions.lock(); break;
|
|
case "suspend": Actions.suspend(); break;
|
|
case "hibernate": Actions.hibernate(); break;
|
|
case "logout": Actions.logout(); break;
|
|
case "reboot": Actions.reboot(); break;
|
|
case "shutdown": Actions.shutdown(); break;
|
|
}
|
|
}
|
|
|
|
// Escape closes. The layer takes exclusive focus while open.
|
|
Item {
|
|
id: keyController
|
|
anchors.fill: parent
|
|
focus: root.open
|
|
Keys.onEscapePressed: Actions.closePowerMenu()
|
|
Keys.onPressed: event => {
|
|
if (!root.open) return;
|
|
if (event.key === Qt.Key_Up || event.key === Qt.Key_K) {
|
|
root.moveSelection(-1);
|
|
event.accepted = true;
|
|
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_J) {
|
|
root.moveSelection(1);
|
|
event.accepted = true;
|
|
} else if (keyController.activeFocus
|
|
&& !event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Return || event.key === Qt.Key_Enter
|
|
|| event.key === Qt.Key_Space)) {
|
|
root.invoke(root.actionModel[root.selectedAction].act);
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
ParallelAnimation {
|
|
id: openAnim
|
|
NumberAnimation {
|
|
target: backdrop; property: "opacity"
|
|
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
|
|
}
|
|
NumberAnimation {
|
|
target: group; property: "opacity"
|
|
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
|
|
}
|
|
}
|
|
|
|
ParallelAnimation {
|
|
id: closeAnim
|
|
NumberAnimation {
|
|
target: backdrop; property: "opacity"
|
|
to: 0; duration: Theme.durBase; easing.type: Easing.InCubic
|
|
}
|
|
NumberAnimation {
|
|
target: group; property: "opacity"
|
|
to: 0; duration: Theme.durFast
|
|
}
|
|
}
|
|
}
|