- Idle is handled in the shell instead of hypridle: the screen dims slowly at 4 minutes, locks at 5, powers down at 6, and suspends at 15 on battery. Idle inhibitors hold every stage off; Super+Shift+I does the same by hand and puts a cup on the rail. - The shell draws the wallpaper (backdrop/), so hyprpaper is gone. The picture is drawn a little larger than the screen and drifts with the workspace a beat after the windows, and dims a shade while windows are open. The path lives in quickshell/wallpaper. - The lock screen and power menu say "Take your time." instead of the NieR slogan. - hyprpaper is no longer launched twice, the media module keeps the player that played last instead of an empty browser tab, and grouped windows show thin untitled tabs with their place in the group (2/3) in the bar title. - qt6ct uses Darkly; the package list gains adw-gtk-theme and darkly-bin.
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: "Take your time."
|
|
color: Theme.accent
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsHuge
|
|
font.weight: Font.Light
|
|
font.letterSpacing: 1
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
Text {
|
|
topPadding: Theme.padS
|
|
leftPadding: 2
|
|
text: [(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
|
|
}
|
|
}
|
|
}
|