forked from Shinonome/dots-hyprland
Rearrange for tidier structure (#2212)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.lock
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
function unlockKeyring() {
|
||||
Quickshell.execDetached({
|
||||
environment: ({
|
||||
UNLOCK_PASSWORD: root.currentText
|
||||
}),
|
||||
command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")]
|
||||
})
|
||||
}
|
||||
|
||||
// This stores all the information shared between the lock surfaces on each screen.
|
||||
// https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen
|
||||
LockContext {
|
||||
id: lockContext
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onScreenLockedChanged() {
|
||||
if (GlobalStates.screenLocked) lockContext.reset();
|
||||
}
|
||||
}
|
||||
|
||||
onUnlocked: (targetAction) => {
|
||||
// Perform the target action if it's not just unlocking
|
||||
if (targetAction == LockContext.ActionEnum.Poweroff) {
|
||||
Session.poweroff();
|
||||
return;
|
||||
} else if (targetAction == LockContext.ActionEnum.Reboot) {
|
||||
Session.reboot();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlock the keyring if configured to do so
|
||||
if (Config.options.lock.security.unlockKeyring) root.unlockKeyring();
|
||||
|
||||
// Unlock the screen before exiting, or the compositor will display a
|
||||
// fallback lock you can't interact with.
|
||||
GlobalStates.screenLocked = false;
|
||||
|
||||
// Refocus last focused window on unlock (hack)
|
||||
Quickshell.execDetached(["bash", "-c", `sleep 0.2; hyprctl --batch "dispatch togglespecialworkspace; dispatch togglespecialworkspace"`])
|
||||
|
||||
// Reset
|
||||
lockContext.reset();
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lock
|
||||
locked: GlobalStates.screenLocked
|
||||
|
||||
WlSessionLockSurface {
|
||||
color: "transparent"
|
||||
Loader {
|
||||
active: GlobalStates.screenLocked
|
||||
anchors.fill: parent
|
||||
opacity: active ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
sourceComponent: LockSurface {
|
||||
context: lockContext
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blur layer hack
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: Scope {
|
||||
required property ShellScreen modelData
|
||||
property bool shouldPush: GlobalStates.screenLocked
|
||||
property string targetMonitorName: modelData.name
|
||||
property int verticalMovementDistance: modelData.height
|
||||
property int horizontalSqueeze: modelData.width * 0.2
|
||||
onShouldPushChanged: {
|
||||
if (shouldPush) {
|
||||
Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, ${verticalMovementDistance}, ${-verticalMovementDistance}, ${horizontalSqueeze}, ${horizontalSqueeze}`])
|
||||
} else {
|
||||
Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, 0, 0, 0, 0`])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "lock"
|
||||
|
||||
function activate(): void {
|
||||
GlobalStates.screenLocked = true;
|
||||
}
|
||||
function focus(): void {
|
||||
lockContext.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "lock"
|
||||
description: "Locks the screen"
|
||||
|
||||
onPressed: {
|
||||
if (Config.options.lock.useHyprlock) {
|
||||
Quickshell.execDetached(["hyprlock"])
|
||||
return;
|
||||
}
|
||||
GlobalStates.screenLocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "lockFocus"
|
||||
description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason"
|
||||
+ "decides to keyboard-unfocus the lock screen"
|
||||
|
||||
onPressed: {
|
||||
lockContext.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Config
|
||||
function onReadyChanged() {
|
||||
if (Config.options.lock.launchOnStartup && Config.ready && Persistent.ready && Persistent.isNewHyprlandInstance) {
|
||||
Hyprland.dispatch("global quickshell:lock")
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Persistent
|
||||
function onReadyChanged() {
|
||||
if (Config.options.lock.launchOnStartup && Config.ready && Persistent.ready && Persistent.isNewHyprlandInstance) {
|
||||
Hyprland.dispatch("global quickshell:lock")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pam
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
enum ActionEnum { Unlock, Poweroff, Reboot }
|
||||
|
||||
signal shouldReFocus()
|
||||
signal unlocked(targetAction: var)
|
||||
signal failed()
|
||||
|
||||
// These properties are in the context and not individual lock surfaces
|
||||
// so all surfaces can share the same state.
|
||||
property string currentText: ""
|
||||
property bool unlockInProgress: false
|
||||
property bool showFailure: false
|
||||
property var targetAction: LockContext.ActionEnum.Unlock
|
||||
|
||||
function resetTargetAction() {
|
||||
root.targetAction = LockContext.ActionEnum.Unlock;
|
||||
}
|
||||
|
||||
function clearText() {
|
||||
root.currentText = "";
|
||||
}
|
||||
|
||||
function resetClearTimer() {
|
||||
passwordClearTimer.restart();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
root.resetTargetAction();
|
||||
root.clearText();
|
||||
root.unlockInProgress = false;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: passwordClearTimer
|
||||
interval: 10000
|
||||
onTriggered: {
|
||||
root.reset();
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentTextChanged: {
|
||||
if (currentText.length > 0) {
|
||||
showFailure = false;
|
||||
GlobalStates.screenUnlockFailed = false;
|
||||
}
|
||||
GlobalStates.screenLockContainsCharacters = currentText.length > 0;
|
||||
passwordClearTimer.restart();
|
||||
}
|
||||
|
||||
function tryUnlock() {
|
||||
root.unlockInProgress = true;
|
||||
pam.start();
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: pam
|
||||
|
||||
// pam_unix will ask for a response for the password prompt
|
||||
onPamMessage: {
|
||||
if (this.responseRequired) {
|
||||
this.respond(root.currentText);
|
||||
}
|
||||
}
|
||||
|
||||
// pam_unix won't send any important messages so all we need is the completion status.
|
||||
onCompleted: result => {
|
||||
if (result == PamResult.Success) {
|
||||
root.unlocked(root.targetAction);
|
||||
} else {
|
||||
root.clearText();
|
||||
root.unlockInProgress = false;
|
||||
GlobalStates.screenUnlockFailed = true;
|
||||
root.showFailure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.UPower
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.bar as Bar
|
||||
import Quickshell.Services.SystemTray
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
required property LockContext context
|
||||
property bool active: false
|
||||
property bool showInputField: active || context.currentText.length > 0
|
||||
readonly property bool requirePasswordToPower: Config.options.lock.security.requirePasswordToPower
|
||||
|
||||
// Force focus on entry
|
||||
function forceFieldFocus() {
|
||||
passwordBox.forceActiveFocus();
|
||||
}
|
||||
Connections {
|
||||
target: context
|
||||
function onShouldReFocus() {
|
||||
forceFieldFocus();
|
||||
}
|
||||
}
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onPressed: mouse => {
|
||||
forceFieldFocus();
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
forceFieldFocus();
|
||||
}
|
||||
|
||||
// Toolbar appearing animation
|
||||
property real toolbarScale: 0.9
|
||||
property real toolbarOpacity: 0
|
||||
Behavior on toolbarScale {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
Behavior on toolbarOpacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
// Init
|
||||
Component.onCompleted: {
|
||||
forceFieldFocus();
|
||||
toolbarScale = 1;
|
||||
toolbarOpacity = 1;
|
||||
}
|
||||
|
||||
// Key presses
|
||||
Keys.onPressed: event => {
|
||||
root.context.resetClearTimer();
|
||||
if (event.key === Qt.Key_Escape) { // Esc to clear
|
||||
root.context.currentText = "";
|
||||
}
|
||||
forceFieldFocus();
|
||||
}
|
||||
|
||||
// RippleButton {
|
||||
// anchors {
|
||||
// top: parent.top
|
||||
// left: parent.left
|
||||
// leftMargin: 10
|
||||
// topMargin: 10
|
||||
// }
|
||||
// implicitHeight: 40
|
||||
// colBackground: Appearance.colors.colLayer2
|
||||
// onClicked: {
|
||||
// context.unlocked(LockContext.ActionEnum.Unlock);
|
||||
// GlobalStates.screenLocked = false;
|
||||
// }
|
||||
// contentItem: StyledText {
|
||||
// text: "[[ DEBUG BYPASS ]]"
|
||||
// }
|
||||
// }
|
||||
|
||||
// Main toolbar: password box
|
||||
Toolbar {
|
||||
id: mainIsland
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 20
|
||||
}
|
||||
Behavior on anchors.bottomMargin {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
scale: root.toolbarScale
|
||||
opacity: root.toolbarOpacity
|
||||
|
||||
ToolbarTextField {
|
||||
id: passwordBox
|
||||
placeholderText: GlobalStates.screenUnlockFailed ? Translation.tr("Incorrect password") : Translation.tr("Enter password")
|
||||
|
||||
// Style
|
||||
clip: true
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
|
||||
// Password
|
||||
enabled: !root.context.unlockInProgress
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
|
||||
// Synchronizing (across monitors) and unlocking
|
||||
onTextChanged: root.context.currentText = this.text
|
||||
onAccepted: root.context.tryUnlock()
|
||||
Connections {
|
||||
target: root.context
|
||||
function onCurrentTextChanged() {
|
||||
passwordBox.text = root.context.currentText;
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
root.context.resetClearTimer();
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
id: confirmButton
|
||||
implicitWidth: height
|
||||
toggled: true
|
||||
enabled: !root.context.unlockInProgress
|
||||
colBackgroundToggled: Appearance.colors.colPrimary
|
||||
|
||||
onClicked: root.context.tryUnlock()
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 24
|
||||
text: {
|
||||
if (root.context.targetAction === LockContext.ActionEnum.Unlock) {
|
||||
return "arrow_right_alt";
|
||||
} else if (root.context.targetAction === LockContext.ActionEnum.Poweroff) {
|
||||
return "power_settings_new";
|
||||
} else if (root.context.targetAction === LockContext.ActionEnum.Reboot) {
|
||||
return "restart_alt";
|
||||
}
|
||||
}
|
||||
color: confirmButton.enabled ? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Left toolbar
|
||||
Toolbar {
|
||||
id: leftIsland
|
||||
anchors {
|
||||
right: mainIsland.left
|
||||
top: mainIsland.top
|
||||
bottom: mainIsland.bottom
|
||||
rightMargin: 10
|
||||
}
|
||||
scale: root.toolbarScale
|
||||
opacity: root.toolbarOpacity
|
||||
|
||||
// Username
|
||||
IconAndTextPair {
|
||||
Layout.leftMargin: 8
|
||||
icon: "account_circle"
|
||||
text: SystemInfo.username
|
||||
}
|
||||
|
||||
// Keyboard layout (Xkb)
|
||||
Loader {
|
||||
Layout.rightMargin: 8
|
||||
Layout.fillHeight: true
|
||||
|
||||
active: true
|
||||
visible: active
|
||||
|
||||
sourceComponent: Row {
|
||||
spacing: 8
|
||||
|
||||
MaterialSymbol {
|
||||
id: keyboardIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fill: 1
|
||||
text: "keyboard_alt"
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sourceComponent: StyledText {
|
||||
text: HyprlandXkb.currentLayoutCode
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
animateChange: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard layout (Fcitx)
|
||||
Bar.SysTray {
|
||||
Layout.rightMargin: 10
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
showSeparator: false
|
||||
showOverflowMenu: false
|
||||
pinnedItems: SystemTray.items.values.filter(i => i.id == "Fcitx")
|
||||
visible: pinnedItems.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Right toolbar
|
||||
Toolbar {
|
||||
id: rightIsland
|
||||
anchors {
|
||||
left: mainIsland.right
|
||||
top: mainIsland.top
|
||||
bottom: mainIsland.bottom
|
||||
leftMargin: 10
|
||||
}
|
||||
|
||||
scale: root.toolbarScale
|
||||
opacity: root.toolbarOpacity
|
||||
|
||||
IconAndTextPair {
|
||||
visible: UPower.displayDevice.isLaptopBattery
|
||||
icon: Battery.isCharging ? "bolt" : "battery_android_full"
|
||||
text: Math.round(Battery.percentage * 100)
|
||||
color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
|
||||
ActionToolbarIconButton {
|
||||
id: sleepButton
|
||||
onClicked: Session.suspend()
|
||||
text: "dark_mode"
|
||||
}
|
||||
|
||||
PasswordGuardedActionToolbarIconButton {
|
||||
id: powerButton
|
||||
text: "power_settings_new"
|
||||
targetAction: LockContext.ActionEnum.Poweroff
|
||||
}
|
||||
|
||||
PasswordGuardedActionToolbarIconButton {
|
||||
id: rebootButton
|
||||
text: "restart_alt"
|
||||
targetAction: LockContext.ActionEnum.Reboot
|
||||
}
|
||||
}
|
||||
|
||||
component PasswordGuardedActionToolbarIconButton: ActionToolbarIconButton {
|
||||
id: guardedBtn
|
||||
required property var targetAction
|
||||
|
||||
toggled: root.context.targetAction === guardedBtn.targetAction
|
||||
|
||||
onClicked: {
|
||||
if (!root.requirePasswordToPower) {
|
||||
root.context.unlocked(guardedBtn.targetAction);
|
||||
return;
|
||||
}
|
||||
if (root.context.targetAction === guardedBtn.targetAction) {
|
||||
root.context.resetTargetAction();
|
||||
} else {
|
||||
root.context.targetAction = guardedBtn.targetAction;
|
||||
root.context.shouldReFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ActionToolbarIconButton: ToolbarButton {
|
||||
id: iconBtn
|
||||
implicitWidth: height
|
||||
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 24
|
||||
text: iconBtn.text
|
||||
color: iconBtn.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
component IconAndTextPair: Row {
|
||||
id: pair
|
||||
required property string icon
|
||||
required property string text
|
||||
property color color: Appearance.colors.colOnSurfaceVariant
|
||||
|
||||
spacing: 4
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fill: 1
|
||||
text: pair.icon
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
animateChange: true
|
||||
color: pair.color
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: pair.text
|
||||
color: pair.color
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user