Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfc0c49877 | ||
|
|
c2eddc2c5b |
@@ -136,7 +136,7 @@ docs/ Settings reference, and the design specs behind the work
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
153 of them, under `tests/`. Run the lot, or a subset by pattern:
|
156 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
panama test # everything
|
panama test # everything
|
||||||
|
|||||||
@@ -92,6 +92,14 @@ Singleton {
|
|||||||
label: "Battery",
|
label: "Battery",
|
||||||
detail: "Show the charge level in the bar, on machines that have a battery"
|
detail: "Show the charge level in the bar, on machines that have a battery"
|
||||||
},
|
},
|
||||||
|
// The number beside the icon, GNOME's "Show Battery Percentage".
|
||||||
|
// Off by default for the same reason GNOME ships it off: the icon
|
||||||
|
// already says what matters, and the number is for people who want it.
|
||||||
|
{
|
||||||
|
key: "showBatteryPercent", type: "bool", def: false, group: "vitals",
|
||||||
|
label: "Battery percentage",
|
||||||
|
detail: "Show the exact number beside the battery icon"
|
||||||
|
},
|
||||||
|
|
||||||
// Off by default: this is a coding-tool readout, not something a
|
// Off by default: this is a coding-tool readout, not something a
|
||||||
// general-purpose desktop should show without being asked.
|
// general-purpose desktop should show without being asked.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Singleton {
|
|||||||
readonly property bool showMemory: DesktopPreferences.get("showMemory")
|
readonly property bool showMemory: DesktopPreferences.get("showMemory")
|
||||||
readonly property bool showGpu: DesktopPreferences.get("showGpu")
|
readonly property bool showGpu: DesktopPreferences.get("showGpu")
|
||||||
readonly property bool showBattery: DesktopPreferences.get("showBattery")
|
readonly property bool showBattery: DesktopPreferences.get("showBattery")
|
||||||
|
readonly property bool showBatteryPercent: DesktopPreferences.get("showBatteryPercent")
|
||||||
readonly property bool showAgentUsage: DesktopPreferences.get("showAgentUsage")
|
readonly property bool showAgentUsage: DesktopPreferences.get("showAgentUsage")
|
||||||
|
|
||||||
// ── Battery ─────────────────────────────────────────────────────────────
|
// ── Battery ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -130,6 +130,16 @@ Pill {
|
|||||||
// battery has actually been read, so this is not a zero that looks like a
|
// battery has actually been read, so this is not a zero that looks like a
|
||||||
// flat cell. Same shape as the graphics field in VitalsWidget, which gates
|
// flat cell. Same shape as the graphics field in VitalsWidget, which gates
|
||||||
// on both the preference and the hardware.
|
// on both the preference and the hardware.
|
||||||
|
//
|
||||||
|
// One color for the icon and the number beside it: two different colors
|
||||||
|
// would read as two indicators.
|
||||||
|
readonly property color batteryColor: {
|
||||||
|
if (Battery.critical) return Theme.danger;
|
||||||
|
if (Battery.low) return Theme.warn;
|
||||||
|
if (Battery.charging) return Theme.ok;
|
||||||
|
return Theme.fg;
|
||||||
|
}
|
||||||
|
|
||||||
StatusGlyph {
|
StatusGlyph {
|
||||||
visible: Settings.showBattery && Battery.available
|
visible: Settings.showBattery && Battery.available
|
||||||
glyph: {
|
glyph: {
|
||||||
@@ -141,11 +151,24 @@ Pill {
|
|||||||
// md-battery_10 .. md-battery_90 are consecutive from F007A.
|
// md-battery_10 .. md-battery_90 are consecutive from F007A.
|
||||||
return String.fromCodePoint(0xF007A + (level / 10) - 1);
|
return String.fromCodePoint(0xF007A + (level / 10) - 1);
|
||||||
}
|
}
|
||||||
color: {
|
color: root.batteryColor
|
||||||
if (Battery.critical) return Theme.danger;
|
}
|
||||||
if (Battery.low) return Theme.warn;
|
|
||||||
if (Battery.charging) return Theme.ok;
|
// The exact number, for the people who ask the icon to be more specific --
|
||||||
return Theme.fg;
|
// GNOME's "Show Battery Percentage", living under the same gates as the
|
||||||
|
// icon it annotates.
|
||||||
|
Text {
|
||||||
|
visible: Settings.showBattery && Settings.showBatteryPercent && Battery.available
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: Math.round(Battery.percent) + "%"
|
||||||
|
color: root.batteryColor
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.durFast
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ SettingsPage {
|
|||||||
// Only where there is a battery to report on. A desktop should not be
|
// Only where there is a battery to report on. A desktop should not be
|
||||||
// offered a switch for a readout it can never show.
|
// offered a switch for a readout it can never show.
|
||||||
ToggleRow { setting: "showBattery"; visible: Battery.available }
|
ToggleRow { setting: "showBattery"; visible: Battery.available }
|
||||||
|
ToggleRow { setting: "showBatteryPercent"; visible: Battery.available && Settings.showBattery }
|
||||||
ToggleRow { setting: "showAgentUsage"; divider: true }
|
ToggleRow { setting: "showAgentUsage"; divider: true }
|
||||||
// Refresh interval was on the Home page, which split one concept across
|
// Refresh interval was on the Home page, which split one concept across
|
||||||
// two pages -- what the vitals show here, how often they update there.
|
// two pages -- what the vitals show here, how often they update there.
|
||||||
|
|||||||
@@ -121,6 +121,25 @@ SettingsPage {
|
|||||||
SliderRow { setting: "suspendMinutesBattery"; zeroLabel: "Never"; divider: false }
|
SliderRow { setting: "suspendMinutesBattery"; zeroLabel: "Never"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What the lid does. Informational by design: the decision follows what
|
||||||
|
// is connected (see services/LidPolicy.qml), and the deliberate absence
|
||||||
|
// of an override is part of the design -- a lid switch set to "never
|
||||||
|
// suspend" is a laptop that cooks in a bag. Saying so here beats leaving
|
||||||
|
// the behavior undiscoverable.
|
||||||
|
SettingsCard {
|
||||||
|
visible: Battery.available
|
||||||
|
title: "When the lid closes"
|
||||||
|
subtitle: "Decided by what is connected rather than by a setting: with an external display attached the machine is docked and keeps running; on its own it suspends, locking on the way down."
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Right now"
|
||||||
|
value: LidPolicy.inhibited
|
||||||
|
? "Stays awake — an external display is connected"
|
||||||
|
: "Suspends"
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only shown when the numbers are actually contradictory, rather than as a
|
// Only shown when the numbers are actually contradictory, rather than as a
|
||||||
// permanent warning nobody reads.
|
// permanent warning nobody reads.
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ SettingsPage {
|
|||||||
root.newUserIsAdministrator = false;
|
root.newUserIsAdministrator = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: UserAccounts.refresh()
|
Component.onCompleted: {
|
||||||
|
UserAccounts.refresh();
|
||||||
|
// Probing fprintd bus-activates it, so it waits for the page rather
|
||||||
|
// than costing every shell launch.
|
||||||
|
Fingerprint.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
TextRow {
|
TextRow {
|
||||||
visible: UserAccounts.lastError !== ""
|
visible: UserAccounts.lastError !== ""
|
||||||
@@ -260,6 +265,58 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Fingerprint ──────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Hidden in full on a machine with no reader. Two systems make this work
|
||||||
|
// and the card keeps them honest with each other: fprintd holds the
|
||||||
|
// enrolled prints (GNOME's Users panel owns that dialog, so enrollment
|
||||||
|
// hands off the same way password-adjacent panels do), and authselect
|
||||||
|
// decides whether PAM asks the reader at all -- a print enrolled while
|
||||||
|
// that is off does nothing, which reads as "fingerprint is broken".
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: Fingerprint.readerPresent
|
||||||
|
title: "Fingerprint"
|
||||||
|
subtitle: Fingerprint.readerName !== ""
|
||||||
|
? Fingerprint.readerName
|
||||||
|
: "A fingerprint reader is present."
|
||||||
|
|
||||||
|
SwitchRow {
|
||||||
|
label: "Unlock with a fingerprint"
|
||||||
|
detail: {
|
||||||
|
if (Fingerprint.enrolled.length === 0)
|
||||||
|
return "Enroll a finger below first; until then the password is the only way in";
|
||||||
|
return Fingerprint.pamEnabled
|
||||||
|
? "The lock screen and sudo accept an enrolled finger, with the password as fallback"
|
||||||
|
: "Enrolled fingers are ignored until this is on";
|
||||||
|
}
|
||||||
|
checked: Fingerprint.pamEnabled
|
||||||
|
enabled: !Fingerprint.busy
|
||||||
|
onToggled: value => Fingerprint.setUnlockEnabled(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Enrolled fingers"
|
||||||
|
detail: Fingerprint.enrolled.length === 0
|
||||||
|
? "None yet"
|
||||||
|
: Fingerprint.enrolled.map(finger => Fingerprint.fingerLabel(finger)).join(", ")
|
||||||
|
action: "Manage…"
|
||||||
|
divider: Fingerprint.lastError !== ""
|
||||||
|
// GNOME's Users panel owns the enrollment dialog; growing our own
|
||||||
|
// means reimplementing a guided capture flow fprintd already has
|
||||||
|
// a good one of.
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("system", "users")
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
visible: Fingerprint.lastError !== ""
|
||||||
|
label: "Fingerprint needs attention"
|
||||||
|
detail: Fingerprint.lastError
|
||||||
|
value: ""
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Everyone else ────────────────────────────────────────────────────────
|
// ── Everyone else ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Fingerprint state and the one privileged switch, for the Users page.
|
||||||
|
#
|
||||||
|
# Two independent facts make a working fingerprint login, and conflating them
|
||||||
|
# is how the feature usually confuses people: fprintd must hold at least one
|
||||||
|
# enrolled print (GNOME's Users panel owns that dialog, and Panama hands off
|
||||||
|
# to it), and PAM must be told to ask the reader at all, which on Fedora is
|
||||||
|
# authselect's `with-fingerprint` feature. This helper reports both and can
|
||||||
|
# flip the second.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# panama-fingerprint status -> {"reader":bool,"readerName":"","enrolled":[],"pamEnabled":bool,"error":""}
|
||||||
|
# panama-fingerprint set-unlock on|off (prompts through panama-sudo/polkit)
|
||||||
|
#
|
||||||
|
# authselect is baseline Fedora (it manages PAM for the whole install), and
|
||||||
|
# fprintd ships with Workstation; a machine with neither simply reports no
|
||||||
|
# reader, which hides the card.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
|
||||||
|
emit() {
|
||||||
|
jq -cn \
|
||||||
|
--argjson reader "$1" \
|
||||||
|
--arg readerName "$2" \
|
||||||
|
--argjson enrolled "$3" \
|
||||||
|
--argjson pamEnabled "$4" \
|
||||||
|
--arg error "$5" \
|
||||||
|
'{reader: $reader, readerName: $readerName, enrolled: $enrolled,
|
||||||
|
pamEnabled: $pamEnabled, error: $error}'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
command -v fprintd-list >/dev/null 2>&1 || { emit false "" '[]' false ""; return; }
|
||||||
|
|
||||||
|
# fprintd-list both answers "is there a reader" (fprintd is bus-activated,
|
||||||
|
# so this also copes with the daemon not running yet) and names the
|
||||||
|
# enrolled fingers in one call.
|
||||||
|
local listing
|
||||||
|
if ! listing="$(timeout 10 fprintd-list "$USER" 2>&1)"; then
|
||||||
|
# "No devices available" is the normal no-reader machine; anything
|
||||||
|
# else is a real problem worth surfacing.
|
||||||
|
if grep -qi 'no devices' <<<"$listing"; then
|
||||||
|
emit false "" '[]' false ""
|
||||||
|
else
|
||||||
|
emit false "" '[]' false "fprintd did not answer: $(head -1 <<<"$listing")"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# "Fingerprints for user gib on FocalTech ... (press):" carries the reader
|
||||||
|
# product name; " - #0: right-index-finger" lines carry the enrollment.
|
||||||
|
local name enrolled pam
|
||||||
|
name="$(sed -n 's/^Fingerprints for user [^ ]* on \(.*\) (\w*):$/\1/p' <<<"$listing" | head -1)"
|
||||||
|
enrolled="$(sed -n 's/^ *- #[0-9]*: //p' <<<"$listing" | jq -Rn '[inputs]')"
|
||||||
|
pam=false
|
||||||
|
authselect current 2>/dev/null | grep -q 'with-fingerprint' && pam=true
|
||||||
|
|
||||||
|
emit true "$name" "$enrolled" "$pam" ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_set_unlock() {
|
||||||
|
local verb reason
|
||||||
|
case "$1" in
|
||||||
|
on) verb=enable-feature
|
||||||
|
reason="Turning on fingerprint login: telling PAM (via authselect) to ask the fingerprint reader when unlocking" ;;
|
||||||
|
off) verb=disable-feature
|
||||||
|
reason="Turning off fingerprint login: telling PAM (via authselect) to stop asking the fingerprint reader" ;;
|
||||||
|
*) echo 'panama-fingerprint set-unlock takes on|off' >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local sudo_cmd=(sudo)
|
||||||
|
[[ -x "$PANAMA_PATH/bin/panama-sudo" ]] && sudo_cmd=(
|
||||||
|
"$PANAMA_PATH/bin/panama-sudo" --reason "$reason" --
|
||||||
|
)
|
||||||
|
"${sudo_cmd[@]}" authselect "$verb" with-fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
status) cmd_status ;;
|
||||||
|
set-unlock) [[ -n "${2:-}" ]] || { echo 'panama-fingerprint set-unlock takes on|off' >&2; exit 1; }
|
||||||
|
cmd_set_unlock "$2" ;;
|
||||||
|
*) echo 'usage: panama-fingerprint status | set-unlock on|off' >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
// The fingerprint reader, for the Users page.
|
||||||
|
//
|
||||||
|
// Two facts, owned by two different systems: fprintd holds the enrolled
|
||||||
|
// prints (GNOME's Users panel owns the enrollment dialog and Panama hands off
|
||||||
|
// to it), and authselect decides whether PAM asks the reader at unlock. Both
|
||||||
|
// come through scripts/panama-fingerprint, and the one privileged change --
|
||||||
|
// flipping authselect's with-fingerprint feature -- prompts through polkit
|
||||||
|
// with a stated reason, like everything else on that page.
|
||||||
|
//
|
||||||
|
// Read when the page opens rather than at shell startup: probing fprintd
|
||||||
|
// bus-activates the daemon, and most sessions never open this page.
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-fingerprint"
|
||||||
|
|
||||||
|
property bool readerPresent: false
|
||||||
|
property string readerName: ""
|
||||||
|
property var enrolled: []
|
||||||
|
property bool pamEnabled: false
|
||||||
|
property bool scanned: false
|
||||||
|
property bool busy: false
|
||||||
|
property string lastError: ""
|
||||||
|
|
||||||
|
// "right-index-finger" -> "Right index finger". Presentation lives here
|
||||||
|
// rather than in the page, the way PowerProfiles.label does, so nothing
|
||||||
|
// can disagree about what a finger is called.
|
||||||
|
function fingerLabel(finger: string): string {
|
||||||
|
const words = String(finger).split("-").join(" ");
|
||||||
|
return words.slice(0, 1).toUpperCase() + words.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh(): void {
|
||||||
|
if (!query.running)
|
||||||
|
query.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUnlockEnabled(on: bool): void {
|
||||||
|
if (root.busy)
|
||||||
|
return;
|
||||||
|
root.busy = true;
|
||||||
|
root.lastError = "";
|
||||||
|
apply.command = [root.helperPath, "set-unlock", on ? "on" : "off"];
|
||||||
|
apply.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: query
|
||||||
|
command: [root.helperPath, "status"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.text);
|
||||||
|
root.readerPresent = parsed.reader === true;
|
||||||
|
root.readerName = String(parsed.readerName ?? "");
|
||||||
|
root.enrolled = Array.isArray(parsed.enrolled) ? parsed.enrolled : [];
|
||||||
|
root.pamEnabled = parsed.pamEnabled === true;
|
||||||
|
if (String(parsed.error ?? "") !== "")
|
||||||
|
root.lastError = String(parsed.error);
|
||||||
|
} catch (error) {
|
||||||
|
root.readerPresent = false;
|
||||||
|
root.lastError = "Could not read the fingerprint helper's output.";
|
||||||
|
console.warn("Fingerprint: could not parse helper output:", error);
|
||||||
|
}
|
||||||
|
root.scanned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: apply
|
||||||
|
stderr: StdioCollector {
|
||||||
|
// A dismissed polkit prompt is a normal outcome on this page, not
|
||||||
|
// a failure to report.
|
||||||
|
onStreamFinished: {
|
||||||
|
const text = this.text.trim();
|
||||||
|
if (text !== "" && !/dismissed|not authorized/i.test(text))
|
||||||
|
root.lastError = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Re-read rather than assuming: authselect may refuse, and the
|
||||||
|
// prompt may have been dismissed.
|
||||||
|
onExited: {
|
||||||
|
root.busy = false;
|
||||||
|
root.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
# @vicinae.mode silent
|
# @vicinae.mode silent
|
||||||
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
# @vicinae.description Open Appearance in Settings.
|
# @vicinae.description Open Appearance in Settings.
|
||||||
# @vicinae.keywords ["settings", "24-hour time", "show seconds", "show weekday", "processor", "memory", "graphics", "battery", "claude usage", "inner gaps", "outer gaps", "border width", "corner radius"]
|
# @vicinae.keywords ["settings", "24-hour time", "show seconds", "show weekday", "processor", "memory", "graphics", "battery", "battery percentage", "claude usage", "inner gaps", "outer gaps", "border width"]
|
||||||
|
|
||||||
exec "$HOME/.config/quickshell/scripts/panama-action" settings-page appearance
|
exec "$HOME/.config/quickshell/scripts/panama-action" settings-page appearance
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,7 @@
|
|||||||
Do not edit this file. Run `quickshell/scripts/panama-settings-docs`
|
Do not edit this file. Run `quickshell/scripts/panama-settings-docs`
|
||||||
after changing the schema; a contract fails when this copy is stale.
|
after changing the schema; a contract fails when this copy is stale.
|
||||||
|
|
||||||
147 settings across 29 groups. 70 of them are applied to the compositor and confirmed by reading the value back.
|
148 settings across 29 groups. 70 of them are applied to the compositor and confirmed by reading the value back.
|
||||||
|
|
||||||
## accessibility
|
## accessibility
|
||||||
|
|
||||||
@@ -317,6 +317,7 @@ Found on **Appearance**.
|
|||||||
| **Memory**<br>`showMemory` | true | Show memory usage beside the workspace indicator |
|
| **Memory**<br>`showMemory` | true | Show memory usage beside the workspace indicator |
|
||||||
| **Graphics**<br>`showGpu` | true | Show graphics usage beside the workspace indicator |
|
| **Graphics**<br>`showGpu` | true | Show graphics usage beside the workspace indicator |
|
||||||
| **Battery**<br>`showBattery` | true | Show the charge level in the bar, on machines that have a battery |
|
| **Battery**<br>`showBattery` | true | Show the charge level in the bar, on machines that have a battery |
|
||||||
|
| **Battery percentage**<br>`showBatteryPercent` | false | Show the exact number beside the battery icon |
|
||||||
| **Claude usage**<br>`showAgentUsage` | false | Show how much of the Claude subscription has been used, beside the other vitals |
|
| **Claude usage**<br>`showAgentUsage` | false | Show how much of the Claude subscription has been used, beside the other vitals |
|
||||||
| **Vitals refresh**<br>`vitalsIntervalMs` | 2000 ms | How often processor, memory, and graphics usage update. Range 500–10000. |
|
| **Vitals refresh**<br>`vitalsIntervalMs` | 2000 ms | How often processor, memory, and graphics usage update. Range 500–10000. |
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ cleanup() {
|
|||||||
# Deleted on every exit path, including Ctrl-C. The answers are transient by
|
# Deleted on every exit path, including Ctrl-C. The answers are transient by
|
||||||
# design, and one of them is an email address.
|
# design, and one of them is an email address.
|
||||||
[[ -n "${PANAMA_ANSWERS:-}" ]] && rm -f "$PANAMA_ANSWERS"
|
[[ -n "${PANAMA_ANSWERS:-}" ]] && rm -f "$PANAMA_ANSWERS"
|
||||||
|
[[ -n "${SUDO_KEEPALIVE:-}" ]] && kill "$SUDO_KEEPALIVE" 2>/dev/null
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
# A bare `trap cleanup INT` is not an abort: bash runs the handler and then
|
# A bare `trap cleanup INT` is not an abort: bash runs the handler and then
|
||||||
@@ -87,10 +88,20 @@ export PANAMA_HOSTNAME PANAMA_GIT_NAME PANAMA_GIT_EMAIL PANAMA_GIT_EDITOR \
|
|||||||
PANAMA_GH_LOGIN PANAMA_SSH_KEY PANAMA_NVIDIA PANAMA_MOK_HASH \
|
PANAMA_GH_LOGIN PANAMA_SSH_KEY PANAMA_NVIDIA PANAMA_MOK_HASH \
|
||||||
PANAMA_DEBLOAT PANAMA_FIRMWARE PANAMA_EXTRAS PANAMA_USER_CONTENT
|
PANAMA_DEBLOAT PANAMA_FIRMWARE PANAMA_EXTRAS PANAMA_USER_CONTENT
|
||||||
|
|
||||||
|
# One password, before anything long runs, and then never again. The stages
|
||||||
|
# call sudo dozens of times across twenty-plus minutes, and the timestamp
|
||||||
|
# expires five minutes after whichever call came last -- so a single dnf step
|
||||||
|
# that outlasts it turned the next stage into a password prompt nobody was
|
||||||
|
# there to answer. The refresher holds the timestamp open for exactly as long
|
||||||
|
# as this script lives; cleanup() kills it on every exit path, so nothing
|
||||||
|
# outlives the install with ambient credentials.
|
||||||
|
echo "Panama needs administrator rights for the rest of the run."
|
||||||
|
sudo -v || exit 1
|
||||||
|
( while kill -0 "$$" 2>/dev/null; do sudo -n true 2>/dev/null || true; sleep 60; done ) &
|
||||||
|
SUDO_KEEPALIVE=$!
|
||||||
|
|
||||||
# Applied here rather than in a stage, and applied early: it needs sudo, and
|
# Applied here rather than in a stage, and applied early: it needs sudo, and
|
||||||
# sudo is warm right now. At the end of a long unattended run the timestamp has
|
# sudo is warm right now.
|
||||||
# expired, and a password prompt then is exactly the interruption the interview
|
|
||||||
# exists to prevent.
|
|
||||||
if [[ -n "${PANAMA_HOSTNAME:-}" ]]; then
|
if [[ -n "${PANAMA_HOSTNAME:-}" ]]; then
|
||||||
sudo hostnamectl set-hostname "$PANAMA_HOSTNAME"
|
sudo hostnamectl set-hostname "$PANAMA_HOSTNAME"
|
||||||
echo "Hostname set to: $(hostname)"
|
echo "Hostname set to: $(hostname)"
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ adwaita-icon-theme
|
|||||||
adwaita-sans-fonts
|
adwaita-sans-fonts
|
||||||
brightnessctl
|
brightnessctl
|
||||||
ddcutil
|
ddcutil
|
||||||
|
# The Users page's fingerprint card reads enrollment through fprintd-list.
|
||||||
|
# Workstation ships fprintd; declaring it keeps the card from silently
|
||||||
|
# missing on an install that started from less.
|
||||||
|
fprintd
|
||||||
gpu-screen-recorder
|
gpu-screen-recorder
|
||||||
grim
|
grim
|
||||||
grimblast
|
grimblast
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ bat
|
|||||||
btop
|
btop
|
||||||
cargo
|
cargo
|
||||||
curl
|
curl
|
||||||
|
# `dnf copr`, which install-packages needs to enable the Hyprland COPR. A
|
||||||
|
# machine that has used copr before has it; a fresh Workstation install does
|
||||||
|
# not, and the failure would cost the entire desktop.
|
||||||
|
dnf5-plugins
|
||||||
eza
|
eza
|
||||||
# install-packages installs flatpaks with it; Fedora Workstation ships it,
|
# install-packages installs flatpaks with it; Fedora Workstation ships it,
|
||||||
# but a minimal install does not.
|
# but a minimal install does not.
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ package_for() {
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
zbarimg) printf 'zbar' ;;
|
zbarimg) printf 'zbar' ;;
|
||||||
fc-list|fc-match) printf 'fontconfig' ;;
|
fc-list|fc-match) printf 'fontconfig' ;;
|
||||||
|
fprintd-list) printf 'fprintd' ;;
|
||||||
lspci) printf 'pciutils' ;;
|
lspci) printf 'pciutils' ;;
|
||||||
getenforce) printf 'libselinux-utils' ;;
|
getenforce) printf 'libselinux-utils' ;;
|
||||||
nmcli) printf 'NetworkManager' ;;
|
nmcli) printf 'NetworkManager' ;;
|
||||||
|
|||||||
Executable
+110
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Fingerprint login, which is two systems that must be kept honest with each
|
||||||
|
# other: fprintd holds the enrolled prints, authselect decides whether PAM
|
||||||
|
# asks the reader. A print enrolled while with-fingerprint is off does
|
||||||
|
# nothing, and that silence -- "I enrolled a finger and nothing happened" --
|
||||||
|
# is the failure this card exists to name.
|
||||||
|
#
|
||||||
|
# The helper is the parse surface, so it runs for real against stub fprintd
|
||||||
|
# and authselect; the page and service checks are structural.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-fingerprint"
|
||||||
|
service="$repo_dir/config/dot/quickshell/services/Fingerprint.qml"
|
||||||
|
page="$repo_dir/config/dot/quickshell/modules/settings/UsersPage.qml"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'fingerprint contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Wiring ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
rg -Fq 'visible: Fingerprint.readerPresent' "$page" \
|
||||||
|
|| fail 'the card is not hidden on machines with no reader'
|
||||||
|
rg -Fq 'onToggled: value => Fingerprint.setUnlockEnabled(value)' "$page" \
|
||||||
|
|| fail 'the unlock switch does not drive authselect'
|
||||||
|
rg -Fq 'SystemSettings.openGnomePanel("system", "users")' "$page" \
|
||||||
|
|| fail 'enrollment does not hand off to the GNOME Users panel'
|
||||||
|
rg -Fq 'Fingerprint.refresh()' "$page" \
|
||||||
|
|| fail 'the page never reads the fingerprint state'
|
||||||
|
rg -Fq 'authselect' "$helper" && rg -Fq 'with-fingerprint' "$helper" \
|
||||||
|
|| fail 'the helper does not manage the authselect feature'
|
||||||
|
rg -Fq -- '--reason' "$helper" \
|
||||||
|
|| fail 'the privileged change carries no stated reason'
|
||||||
|
rg -Fq 'function fingerLabel' "$service" \
|
||||||
|
|| fail 'finger names have no single place to be presented from'
|
||||||
|
|
||||||
|
# ── The helper against stub fprintd and authselect ───────────────────────────
|
||||||
|
|
||||||
|
stub_dir="$(mktemp -d)"
|
||||||
|
state_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$stub_dir" "$state_dir"' EXIT
|
||||||
|
|
||||||
|
cat >"$stub_dir/fprintd-list" <<STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [[ -e "$state_dir/no-reader" ]]; then
|
||||||
|
echo 'Impossible to enumerate devices: No devices available'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cat <<'OUT'
|
||||||
|
found 1 devices
|
||||||
|
Device at /net/reactivated/Fprint/Device/0
|
||||||
|
Using device /net/reactivated/Fprint/Device/0
|
||||||
|
Fingerprints for user gib on Goodix MOC Fingerprint Sensor (press):
|
||||||
|
- #0: right-index-finger
|
||||||
|
- #1: left-thumb
|
||||||
|
OUT
|
||||||
|
STUB
|
||||||
|
|
||||||
|
cat >"$stub_dir/authselect" <<STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "\$*" >>"$state_dir/authselect-log"
|
||||||
|
if [[ "\$1" == "current" ]]; then
|
||||||
|
echo 'Profile ID: local'
|
||||||
|
[[ -e "$state_dir/pam-on" ]] && echo '- with-fingerprint'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
STUB
|
||||||
|
|
||||||
|
# PANAMA_PATH pointed at an empty directory forces the plain-sudo fallback,
|
||||||
|
# which the stub records instead of escalating.
|
||||||
|
cat >"$stub_dir/sudo" <<STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "\$*" >>"$state_dir/sudo-log"
|
||||||
|
exec "\$@"
|
||||||
|
STUB
|
||||||
|
chmod +x "$stub_dir"/fprintd-list "$stub_dir"/authselect "$stub_dir"/sudo
|
||||||
|
|
||||||
|
run() { PANAMA_PATH="$state_dir" PATH="$stub_dir:$PATH" "$helper" "$@"; }
|
||||||
|
|
||||||
|
status="$(run status)"
|
||||||
|
jq -e '.reader and .readerName == "Goodix MOC Fingerprint Sensor"' <<<"$status" >/dev/null \
|
||||||
|
|| fail "the reader name did not parse: $status"
|
||||||
|
jq -e '.enrolled == ["right-index-finger", "left-thumb"]' <<<"$status" >/dev/null \
|
||||||
|
|| fail "enrolled fingers did not parse: $status"
|
||||||
|
jq -e '.pamEnabled == false and .error == ""' <<<"$status" >/dev/null \
|
||||||
|
|| fail "authselect state misread as enabled: $status"
|
||||||
|
|
||||||
|
touch "$state_dir/pam-on"
|
||||||
|
jq -e '.pamEnabled == true' <<<"$(run status)" >/dev/null \
|
||||||
|
|| fail 'with-fingerprint enabled was not detected'
|
||||||
|
|
||||||
|
# No reader is a normal machine, not an error.
|
||||||
|
touch "$state_dir/no-reader"
|
||||||
|
jq -e '.reader == false and .error == ""' <<<"$(run status)" >/dev/null \
|
||||||
|
|| fail "a readerless machine was reported as a problem: $(run status)"
|
||||||
|
rm -f "$state_dir/no-reader"
|
||||||
|
|
||||||
|
# The privileged change goes through, with the right feature name.
|
||||||
|
run set-unlock on >/dev/null
|
||||||
|
grep -Fq 'authselect enable-feature with-fingerprint' "$state_dir/sudo-log" \
|
||||||
|
|| fail 'set-unlock on did not enable the authselect feature'
|
||||||
|
run set-unlock off >/dev/null
|
||||||
|
grep -Fq 'authselect disable-feature with-fingerprint' "$state_dir/sudo-log" \
|
||||||
|
|| fail 'set-unlock off did not disable the authselect feature'
|
||||||
|
|
||||||
|
printf 'fingerprint contract: ok\n'
|
||||||
@@ -49,6 +49,7 @@ declare -A OWNED=(
|
|||||||
# Anything here must be justified, not merely tolerated.
|
# Anything here must be justified, not merely tolerated.
|
||||||
declare -A ALLOWED=(
|
declare -A ALLOWED=(
|
||||||
["OnlineAccountsPage.qml:online-accounts"]="adding an account requires GOA's own dialog"
|
["OnlineAccountsPage.qml:online-accounts"]="adding an account requires GOA's own dialog"
|
||||||
|
["UsersPage.qml:system-users"]="fingerprint enrollment requires fprintd's guided capture flow, and GNOME's Users panel carries the only good dialog for it"
|
||||||
)
|
)
|
||||||
|
|
||||||
pages="$(grep -oE '\{ *page: "[a-z-]+"' "$sidebar" | sed 's/.*"\(.*\)"/\1/' | sort -u)"
|
pages="$(grep -oE '\{ *page: "[a-z-]+"' "$sidebar" | sed 's/.*"\(.*\)"/\1/' | sort -u)"
|
||||||
|
|||||||
Reference in New Issue
Block a user