Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/pam.d/howdy
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#%PAM-1.0
auth required pam_howdy.so max-tries=1
1 change: 1 addition & 0 deletions config/LockConfig.qml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ JsonObject {
property bool recolourLogo: false
property bool enableFprint: true
property int maxFprintTries: 3
property bool enableHowdy: true
property Sizes sizes: Sizes {}

component Sizes: JsonObject {
Expand Down
21 changes: 18 additions & 3 deletions modules/IdleMonitors.qml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ Scope {
if (Config.general.idle.lockBeforeSleep)
root.lock.lock.locked = true;
}
onLockRequested: root.lock.lock.locked = true
onUnlockRequested: root.lock.lock.unlock()
onLockRequested: root.lock.locked = true
onUnlockRequested: root.lock.unlock()
}

Variants {
Expand All @@ -45,7 +45,22 @@ Scope {
enabled: root.enabled && (modelData.enabled ?? true)
timeout: modelData.timeout
respectInhibitors: modelData.respectInhibitors ?? true
onIsIdleChanged: root.handleIdleAction(isIdle ? modelData.idleAction : modelData.returnAction)

onIsIdleChanged: {
root.handleIdleAction(isIdle ? modelData.idleAction : modelData.returnAction);

let idleActionString = "";
if (typeof modelData.idleAction === "string")
idleActionString = modelData.idleAction;
else if (Array.isArray(modelData.idleAction))
idleActionString = modelData.idleAction.join(" ");

if (idleActionString.includes("dpms off")) {
if (root.lock.pam) {
root.lock.pam.screenIsIdle = isIdle;
}
}
}
}
}
}
28 changes: 24 additions & 4 deletions modules/lock/Center.qml
Original file line number Diff line number Diff line change
Expand Up @@ -152,21 +152,27 @@ ColumnLayout {

Item {
implicitWidth: implicitHeight
implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2
implicitHeight: authIcon.implicitHeight + Appearance.padding.small * 2

MaterialIcon {
id: fprintIcon
id: authIcon

anchors.centerIn: parent
animate: true
text: {
// if (root.lock.pam.howdy.tries >= Config.lock.maxHowdyTries)
// return "visibility_off";
if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries)
return "fingerprint_off";

if (root.lock.pam.howdy.active)
return "visibility";
if (root.lock.pam.fprint.active)
return "fingerprint";

return "lock";
}
color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface
color: (root.lock.pam.howdy.tries >= Config.lock.maxHowdyTries || root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) ? Colours.palette.m3error : Colours.palette.m3onSurface
opacity: root.lock.pam.passwd.active ? 0 : 1

Behavior on opacity {
Expand Down Expand Up @@ -291,6 +297,8 @@ ColumnLayout {

readonly property Pam pam: root.lock.pam
readonly property string msg: {
if (pam.howdyState === "error")
return qsTr("Howdy ERROR: %1").arg(pam.howdy.message);
if (pam.fprintState === "error")
return qsTr("FP ERROR: %1").arg(pam.fprint.message);
if (pam.state === "error")
Expand All @@ -301,17 +309,29 @@ ColumnLayout {

if (pam.state === "max" && pam.fprintState === "max")
return qsTr("Maximum password and fingerprint attempts reached.");

if (pam.state === "max") {
if (pam.fprint.available && pam.howdy.available)
return qsTr("Maximum password attempts reached. Please use fingerprint or face.");
if (pam.fprint.available)
return qsTr("Maximum password attempts reached. Please use fingerprint.");
if (pam.howdy.available)
return qsTr("Maximum password attempts reached. Please use face.");
return qsTr("Maximum password attempts reached.");
}
if (pam.fprintState === "max")
if (pam.fprintState === "max") {
if (pam.howdy.available)
return qsTr("Maximum fingerprint attempts reached. Please use face or password.");
return qsTr("Maximum fingerprint attempts reached. Please use password.");
}

if (pam.state === "fail") {
if (pam.fprint.available && pam.howdy.available)
return qsTr("Incorrect password. Please try again or use fingerprint/face.");
if (pam.fprint.available)
return qsTr("Incorrect password. Please try again or use fingerprint.");
if (pam.howdy.available)
return qsTr("Incorrect password. Please try again or use face.");
return qsTr("Incorrect password. Please try again.");
}
if (pam.fprintState === "fail")
Expand Down
1 change: 1 addition & 0 deletions modules/lock/Lock.qml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Quickshell.Wayland

Scope {
property alias lock: lock
property alias pam: pam

WlSessionLock {
id: lock
Expand Down
129 changes: 126 additions & 3 deletions modules/lock/Pam.qml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ Scope {

readonly property alias passwd: passwd
readonly property alias fprint: fprint
readonly property alias howdy: howdy
property string lockMessage
property string state
property string fprintState
property string howdyState
property string buffer

property bool screenIsIdle: false

signal flashMsg

function handleKey(event: KeyEvent): void {
Expand Down Expand Up @@ -53,7 +57,6 @@ Scope {
onResponseRequiredChanged: {
if (!responseRequired)
return;

respond(root.buffer);
root.buffer = "";
}
Expand Down Expand Up @@ -128,6 +131,60 @@ Scope {
}
}

PamContext {
id: howdy

property bool available
property int tries
property int errorTries

function checkAvail(): void {
if (!available || !Config.lock.enableHowdy || !root.lock.secure) {
abort();
return;
}

tries = 0;
errorTries = 0;

if (root.screenIsIdle) {
return;
}

start();
}

config: "howdy"
configDirectory: Quickshell.shellDir + "/assets/pam.d"

onCompleted: res => {
if (!available)
return;

if (res === PamResult.Success) {
return root.lock.unlock();
}

if (res === PamResult.Error) {
root.howdyState = "error";
errorTries++;
if (errorTries < 5) {
abort();
howdyErrorRetry.restart();
}
} else if (res === PamResult.MaxTries || res === PamResult.Failed) {
tries++;
root.howdyState = "fail";
if (!root.screenIsIdle) {
start();
} else {}
}

root.flashMsg();
howdyStateReset.restart();
}
}

Process {
id: availProc

Expand All @@ -138,6 +195,16 @@ Scope {
}
}

Process {
id: howdyAvailProc

command: ["sh", "-c", "command -v howdy"]
onExited: code => {
howdy.available = code === 0;
howdy.checkAvail();
}
}

Timer {
id: errorRetry

Expand Down Expand Up @@ -165,21 +232,58 @@ Scope {
}
}

Timer {
id: howdyErrorRetry

interval: 800
onTriggered: howdy.start()
}

Timer {
id: howdyStateReset

interval: 4000
onTriggered: {
root.howdyState = "";
howdy.errorTries = 0;
}
}

// Timer to delay the start of Howdy after unlock
Timer {
id: howdyStartDelayTimer

interval: 2000
repeat: false
onTriggered: {
howdyAvailProc.running = true;
}
}

Connections {
target: root.lock

function onSecureChanged(): void {
if (root.lock.secure) {
availProc.running = true;
howdyStartDelayTimer.start();
root.buffer = "";
root.state = "";
root.fprintState = "";
root.howdyState = "";
root.lockMessage = "";
} else {
root.screenIsIdle = false;
howdyStartDelayTimer.stop();
}
}

function onUnlock(): void {
fprint.abort();
howdyStartDelayTimer.stop();

if (fprint.active)
fprint.abort();
if (howdy.active)
howdy.abort();
}
}

Expand All @@ -189,5 +293,24 @@ Scope {
function onEnableFprintChanged(): void {
fprint.checkAvail();
}
function onEnableHowdyChanged(): void {
howdy.checkAvail();
}
}

Connections {
target: root

function onScreenIsIdleChanged() {
if (root.screenIsIdle) {
if (howdy.available && howdy.active) {
howdy.abort();
}
} else {
if (howdy.available && root.lock.secure && !howdy.active) {
howdy.checkAvail();
}
}
}
}
}
2 changes: 2 additions & 0 deletions nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ in
prePatch = ''
substituteInPlace assets/pam.d/fprint \
--replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so
substituteInPlace assets/pam.d/howdy \
--replace-fail pam_howdy.so /run/current-system/sw/lib/security/pam_howdy.so
substituteInPlace shell.qml \
--replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false'
'';
Expand Down