lock: add caps/num lock indicator

Closes #485
This commit is contained in:
2 * r + 2 * t 2025-08-28 15:13:40 +10:00
parent be4047201f
commit f2b79757e2
2 changed files with 193 additions and 105 deletions

View file

@ -213,124 +213,182 @@ ColumnLayout {
} }
} }
StyledText { Item {
id: message
readonly property Pam pam: root.lock.pam
readonly property string msg: {
if (pam.fprintState === "error")
return qsTr("ERROR: %1").arg(pam.fprint.message);
if (pam.state === "error")
return qsTr("ERROR: %1").arg(pam.passwd.message);
if (pam.lockMessage)
return pam.lockMessage;
if (pam.state === "max" && pam.fprintState === "max")
return qsTr("Maximum password and fingerprint attempts reached.");
if (pam.state === "max") {
if (pam.fprint.available)
return qsTr("Maximum password attempts reached. Please use fingerprint.");
return qsTr("Maximum password attempts reached.");
}
if (pam.fprintState === "max")
return qsTr("Maximum fingerprint attempts reached. Please use password.");
if (pam.state === "fail") {
if (pam.fprint.available)
return qsTr("Incorrect password. Please try again or use fingerprint.");
return qsTr("Incorrect password. Please try again.");
}
if (pam.fprintState === "fail")
return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries);
return "";
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: -Appearance.spacing.large Layout.topMargin: -Appearance.spacing.large
scale: 0.7 implicitHeight: stateMessage.implicitHeight
opacity: 0
color: Colours.palette.m3error
font.pointSize: Appearance.font.size.small StyledText {
font.family: Appearance.font.family.mono id: stateMessage
horizontalAlignment: Qt.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
onMsgChanged: { readonly property string msg: {
if (msg) { if (Hypr.capsLock && Hypr.numLock)
if (opacity > 0) { return qsTr("Caps lock and Num lock are ON.");
animate = true; if (Hypr.capsLock)
text = msg; return qsTr("Caps lock is ON.");
animate = false; if (Hypr.numLock)
return qsTr("Num lock is ON.");
return "";
}
property bool shouldBeVisible
onMsgChanged: {
if (msg) {
if (opacity > 0) {
animate = true;
text = msg;
animate = false;
} else {
text = msg;
}
shouldBeVisible = true;
} else {
shouldBeVisible = false;
}
}
anchors.left: parent.left
anchors.right: parent.right
scale: shouldBeVisible && !message.msg ? 1 : 0.7
opacity: shouldBeVisible && !message.msg ? 1 : 0
color: Colours.palette.m3onSurfaceVariant
animateProp: "opacity"
font.family: Appearance.font.family.mono
horizontalAlignment: Qt.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on scale {
Anim {}
}
Behavior on opacity {
Anim {}
}
}
StyledText {
id: message
readonly property Pam pam: root.lock.pam
readonly property string msg: {
if (pam.fprintState === "error")
return qsTr("ERROR: %1").arg(pam.fprint.message);
if (pam.state === "error")
return qsTr("ERROR: %1").arg(pam.passwd.message);
if (pam.lockMessage)
return pam.lockMessage;
if (pam.state === "max" && pam.fprintState === "max")
return qsTr("Maximum password and fingerprint attempts reached.");
if (pam.state === "max") {
if (pam.fprint.available)
return qsTr("Maximum password attempts reached. Please use fingerprint.");
return qsTr("Maximum password attempts reached.");
}
if (pam.fprintState === "max")
return qsTr("Maximum fingerprint attempts reached. Please use password.");
if (pam.state === "fail") {
if (pam.fprint.available)
return qsTr("Incorrect password. Please try again or use fingerprint.");
return qsTr("Incorrect password. Please try again.");
}
if (pam.fprintState === "fail")
return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries);
return "";
}
anchors.left: parent.left
anchors.right: parent.right
scale: 0.7
opacity: 0
color: Colours.palette.m3error
font.pointSize: Appearance.font.size.small
font.family: Appearance.font.family.mono
horizontalAlignment: Qt.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
onMsgChanged: {
if (msg) {
if (opacity > 0) {
animate = true;
text = msg;
animate = false;
exitAnim.stop();
if (scale < 1)
appearAnim.restart();
else
flashAnim.restart();
} else {
text = msg;
exitAnim.stop();
appearAnim.restart();
}
} else {
appearAnim.stop();
flashAnim.stop();
exitAnim.start();
}
}
Connections {
target: root.lock.pam
function onFlashMsg(): void {
exitAnim.stop(); exitAnim.stop();
if (scale < 1) if (message.scale < 1)
appearAnim.restart(); appearAnim.restart();
else else
flashAnim.restart(); flashAnim.restart();
} else {
text = msg;
exitAnim.stop();
appearAnim.restart();
} }
} else {
appearAnim.stop();
flashAnim.stop();
exitAnim.start();
} }
}
Connections { Anim {
target: root.lock.pam id: appearAnim
function onFlashMsg(): void { target: message
exitAnim.stop(); properties: "scale,opacity"
if (message.scale < 1)
appearAnim.restart();
else
flashAnim.restart();
}
}
Anim {
id: appearAnim
target: message
properties: "scale,opacity"
to: 1
onFinished: flashAnim.restart()
}
SequentialAnimation {
id: flashAnim
loops: 2
FlashAnim {
to: 0.3
}
FlashAnim {
to: 1 to: 1
onFinished: flashAnim.restart()
} }
}
ParallelAnimation { SequentialAnimation {
id: exitAnim id: flashAnim
Anim { loops: 2
target: message
property: "scale" FlashAnim {
to: 0.7 to: 0.3
duration: Appearance.anim.durations.large }
FlashAnim {
to: 1
}
} }
Anim {
target: message ParallelAnimation {
property: "opacity" id: exitAnim
to: 0
duration: Appearance.anim.durations.large Anim {
target: message
property: "scale"
to: 0.7
duration: Appearance.anim.durations.large
}
Anim {
target: message
property: "opacity"
to: 0
duration: Appearance.anim.durations.large
}
} }
} }
} }

View file

@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
import qs.components.misc
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
@ -15,11 +16,14 @@ Singleton {
readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel?.wayland?.activated ? Hyprland.activeToplevel : null readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel?.wayland?.activated ? Hyprland.activeToplevel : null
readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace
readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor
readonly property int activeWsId: focusedWorkspace?.id ?? 1 readonly property int activeWsId: focusedWorkspace?.id ?? 1
property var keyboard
readonly property bool capsLock: keyboard?.capsLock ?? false
readonly property bool numLock: keyboard?.numLock ?? false
readonly property string kbLayoutFull: keyboard?.active_keymap ?? "?"
readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? "??" readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? "??"
property string kbLayoutFull: "?" readonly property var kbMap: new Map()
property var kbMap: new Map()
function dispatch(request: string): void { function dispatch(request: string): void {
Hyprland.dispatch(request); Hyprland.dispatch(request);
@ -37,8 +41,10 @@ Singleton {
if (n.endsWith("v2")) if (n.endsWith("v2"))
return; return;
if (n === "activelayout") { if (n === "configreloaded") {
root.kbLayoutFull = event.parse(2)[1]; setDynamicConfsProc.running = true;
} else if (n === "activelayout") {
devicesProc.running = true;
} else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) {
Hyprland.refreshWorkspaces(); Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors(); Hyprland.refreshMonitors();
@ -73,10 +79,34 @@ Singleton {
} }
Process { Process {
id: devicesProc
running: true running: true
command: ["hyprctl", "-j", "devices"] command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: root.kbLayoutFull = JSON.parse(text).keyboards.find(k => k.main).active_keymap onStreamFinished: root.keyboard = JSON.parse(text).keyboards.find(k => k.main)
} }
} }
Process {
id: setDynamicConfsProc
running: true
command: ["hyprctl", "--batch", "keyword bindln ,Caps_Lock,global,caelestia:reloadDevices;keyword bindln ,Num_Lock,global,caelestia:reloadDevices"]
}
IpcHandler {
target: "hypr"
function reloadDevices(): void {
devicesProc.running = true;
}
}
CustomShortcut {
name: "reloadDevices"
description: "Reload devices"
onPressed: devicesProc.running = true
onReleased: devicesProc.running = true
}
} }