Give a laptop its first week: the keys, the lid, the battery's endgame
The audit's fourth tier -- what a Framework owner reaches for in the first week and found missing. The power button stops being an instant, unconfirmed poweroff: a shipped logind drop-in tells the daemon to stand down and the compositor binds the key to the power menu, the way GNOME turns it into a question. Holding it still hard-cuts through firmware. change-settings restarts logind so the change applies without waiting for a boot, and the Power page says what the button does now. The function row fills in: F10 (XF86RFKill) toggles airplane mode through a new panama-osd verb that blocks or unblocks every radio and says which way it went; F9 (XF86Display) opens the Displays page, the honest action until mirroring exists. And the lid becomes a switch bind: closing a docked lid turns the internal panel off so nothing renders inside a closed shell and no workspace strands on an invisible output, and opening it restores the panel with the mode and scale chosen in Settings. panama-lid owns both decisions; undocked machines suspend via logind before any of it matters. The battery gets an endgame. On battery the screen dims to 30% two-thirds of the way to blanking -- GNOME's single largest idle battery saver -- and restores exactly the level it saved. At the urgent threshold the machine suspends after a fifteen-second grace, cancelled by plugging in, because a suspend preserves the session for days and a hard cut at 0% preserves nothing; "Only warn" remains a choice on the Power page. Hibernate joins the power menu, but only where logind answers CanHibernate with yes -- an entry that fails silently is worse than none. And brightness stops being two code paths: the Displays page now embeds the same control the quick-settings panel uses, so the built-in backlight and DDC/CI monitors share one surface that withdraws itself where neither exists. The lid contract narrows to what its principle protects -- a HandleLidSwitch drop-in -- so deliberate policy for other keys can ship. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
# A stray press of the power button should ask, not act. Fedora's logind
|
||||||
|
# default is HandlePowerKey=poweroff -- an instant, unconfirmed shutdown.
|
||||||
|
# Panama binds the key in the compositor to open its power menu instead
|
||||||
|
# (keybinds.lua), the way GNOME turns the key into a dialog, so logind is
|
||||||
|
# told to stand down. Holding the button still hard-cuts through firmware,
|
||||||
|
# which remains the escape hatch this file cannot and should not remove.
|
||||||
|
[Login]
|
||||||
|
HandlePowerKey=ignore
|
||||||
@@ -34,6 +34,10 @@ local osd = function(action)
|
|||||||
return "$HOME/.config/quickshell/scripts/panama-osd " .. action
|
return "$HOME/.config/quickshell/scripts/panama-osd " .. action
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local lid = function(action)
|
||||||
|
return "$HOME/.config/quickshell/scripts/panama-lid " .. action
|
||||||
|
end
|
||||||
|
|
||||||
local dictate = function(action)
|
local dictate = function(action)
|
||||||
return "$HOME/.config/quickshell/scripts/panama-dictate " .. action
|
return "$HOME/.config/quickshell/scripts/panama-dictate " .. action
|
||||||
end
|
end
|
||||||
@@ -458,6 +462,30 @@ bind("XF86AudioStop", hl.dsp.exec_cmd(osd("media stop")), { locked = true , desc
|
|||||||
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(osd("brightness up 5")), { locked = true, repeating = true , description = "Brightness up" })
|
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(osd("brightness up 5")), { locked = true, repeating = true , description = "Brightness up" })
|
||||||
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd(osd("brightness down 5")), { locked = true, repeating = true , description = "Brightness down" })
|
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd(osd("brightness down 5")), { locked = true, repeating = true , description = "Brightness down" })
|
||||||
|
|
||||||
|
-- Airplane mode. The Framework's F10 emits exactly this keysym, and for a
|
||||||
|
-- while it emitted it into silence. panama-osd owns the toggle so the OSD
|
||||||
|
-- can say which way it went.
|
||||||
|
bind("XF86RFKill", hl.dsp.exec_cmd(osd("airplane toggle")), { locked = true, description = "Airplane mode" })
|
||||||
|
|
||||||
|
-- F9 on the same row. GNOME shows a display-switching OSD here; until
|
||||||
|
-- mirroring exists (DESKTOP-PARITY gap), the honest action is the page where
|
||||||
|
-- displays are actually arranged.
|
||||||
|
bind("XF86Display", hl.dsp.exec_cmd("qs ipc call settings page displays"), { description = "Display settings" })
|
||||||
|
|
||||||
|
-- The power button. logind is told to ignore it (config/copy ships the
|
||||||
|
-- drop-in) so a stray press is a question, not an instant poweroff -- the
|
||||||
|
-- menu this opens is the question. Until the next boot after that drop-in
|
||||||
|
-- lands, logind still acts on the key; this bind costs nothing extra then.
|
||||||
|
bind("XF86PowerOff", hl.dsp.exec_cmd("qs ipc call powermenu toggle"), { locked = true, description = "Power menu" })
|
||||||
|
|
||||||
|
-- The lid, as a switch rather than a key. Closing a docked lid turns the
|
||||||
|
-- internal panel off so nothing renders inside a closed shell and no
|
||||||
|
-- workspace strands on an invisible output; opening it turns the panel back
|
||||||
|
-- on. panama-lid owns the decision -- undocked machines suspend via logind
|
||||||
|
-- before this matters, and the guard's inhibitor handles staying awake.
|
||||||
|
bind("switch:on:Lid Switch", hl.dsp.exec_cmd(lid("close")), { locked = true, description = "Lid closed" })
|
||||||
|
bind("switch:off:Lid Switch", hl.dsp.exec_cmd(lid("open")), { locked = true, description = "Lid opened" })
|
||||||
|
|
||||||
category("Applications")
|
category("Applications")
|
||||||
-- Hardware keys GNOME mapped that have obvious equivalents.
|
-- Hardware keys GNOME mapped that have obvious equivalents.
|
||||||
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
||||||
|
|||||||
@@ -125,6 +125,20 @@ Singleton {
|
|||||||
label: "Urgent at",
|
label: "Urgent at",
|
||||||
detail: "Interrupt at this level, even during Do Not Disturb"
|
detail: "Interrupt at this level, even during Do Not Disturb"
|
||||||
},
|
},
|
||||||
|
// What the desktop DOES at the critical threshold, beyond interrupting.
|
||||||
|
// Suspend by default, which is GNOME's behavior: sleep preserves the
|
||||||
|
// session at a level the firmware can hold for days, and the
|
||||||
|
// alternative -- a hard cut at 0% -- preserves nothing.
|
||||||
|
{
|
||||||
|
key: "batteryCriticalAction", type: "enum", def: "suspend", group: "battery",
|
||||||
|
label: "At the urgent level",
|
||||||
|
detail: "What happens when the battery reaches the urgent threshold while discharging",
|
||||||
|
options: [
|
||||||
|
{ value: "suspend", label: "Suspend" },
|
||||||
|
{ value: "nothing", label: "Only warn" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// Only offered where the firmware exposes a ceiling; the Power page
|
// Only offered where the firmware exposes a ceiling; the Power page
|
||||||
// hides the control entirely otherwise. 100 means charge to full.
|
// hides the control entirely otherwise. 100 means charge to full.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// (see PowerButton); Lock and Suspend are one press because neither loses work.
|
// (see PowerButton); Lock and Suspend are one press because neither loses work.
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.config
|
import qs.config
|
||||||
@@ -31,6 +32,23 @@ PanelWindow {
|
|||||||
|
|
||||||
property int currentIndex: 0
|
property int currentIndex: 0
|
||||||
|
|
||||||
|
// Hibernate appears only where it can deliver: logind answers "yes" only
|
||||||
|
// with a resume-capable swap configured, and a menu entry that fails
|
||||||
|
// silently is worse than none. Checked once per shell run -- swap does
|
||||||
|
// not come and go.
|
||||||
|
property bool canHibernate: false
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: hibernateProbe
|
||||||
|
command: ["busctl", "call", "org.freedesktop.login1",
|
||||||
|
"/org/freedesktop/login1", "org.freedesktop.login1.Manager",
|
||||||
|
"CanHibernate"]
|
||||||
|
running: true
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: win.canHibernate = this.text.includes('"yes"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A uwsm-managed session logs out by stopping the uwsm unit; a plain
|
// A uwsm-managed session logs out by stopping the uwsm unit; a plain
|
||||||
// "Hyprland" session must ask the compositor to exit instead. The branch
|
// "Hyprland" session must ask the compositor to exit instead. The branch
|
||||||
// used to test merely that the uwsm BINARY existed -- but uwsm is always
|
// used to test merely that the uwsm BINARY existed -- but uwsm is always
|
||||||
@@ -41,7 +59,11 @@ PanelWindow {
|
|||||||
// compositor unit, wayland-wm@*.service, so that is what decides.
|
// compositor unit, wayland-wm@*.service, so that is what decides.
|
||||||
readonly property string logoutScript: 'if systemctl --user list-units --no-legend --state=active "wayland-wm@*.service" 2>/dev/null | grep -q .; then exec uwsm stop; else exec hyprctl dispatch "hl.dsp.exit()"; fi'
|
readonly property string logoutScript: 'if systemctl --user list-units --no-legend --state=active "wayland-wm@*.service" 2>/dev/null | grep -q .; then exec uwsm stop; else exec hyprctl dispatch "hl.dsp.exit()"; fi'
|
||||||
|
|
||||||
readonly property var entries: [
|
// The rows actually shown: an entry may declare `available: false` to
|
||||||
|
// withdraw itself (hibernate on a machine with no resume swap).
|
||||||
|
readonly property var entries: allEntries.filter(entry => entry.available !== false)
|
||||||
|
|
||||||
|
readonly property var allEntries: [
|
||||||
{
|
{
|
||||||
glyph: "",
|
glyph: "",
|
||||||
label: "Lock",
|
label: "Lock",
|
||||||
@@ -60,6 +82,13 @@ PanelWindow {
|
|||||||
destructive: false,
|
destructive: false,
|
||||||
cmd: ["systemctl", "suspend"]
|
cmd: ["systemctl", "suspend"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
glyph: "",
|
||||||
|
label: "Hibernate",
|
||||||
|
destructive: false,
|
||||||
|
available: win.canHibernate,
|
||||||
|
cmd: ["systemctl", "hibernate"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
glyph: "",
|
glyph: "",
|
||||||
label: "Restart",
|
label: "Restart",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.config
|
import qs.config
|
||||||
import qs.services
|
import qs.services
|
||||||
|
import qs.modules.quicksettings
|
||||||
import qs.widgets
|
import qs.widgets
|
||||||
|
|
||||||
SettingsPage {
|
SettingsPage {
|
||||||
@@ -289,35 +290,21 @@ SettingsPage {
|
|||||||
// writing it into settings.json would mean restoring a value the panel had
|
// writing it into settings.json would mean restoring a value the panel had
|
||||||
// already moved on from. So there is no schema key here and no SliderRow --
|
// already moved on from. So there is no schema key here and no SliderRow --
|
||||||
// the rows read and write the display directly.
|
// the rows read and write the display directly.
|
||||||
|
// One brightness surface for both kinds this machine may have, shared
|
||||||
|
// with the quick-settings panel so the two can never disagree. This card
|
||||||
|
// was DDC/CI-only for a while, which on a laptop meant Settings showed
|
||||||
|
// an error about external-monitor brightness while the panel's backlight
|
||||||
|
// worked fine one panel over.
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
visible: Brightness.available || Brightness.lastError !== ""
|
visible: pageBrightness.visible || Brightness.lastError !== ""
|
||||||
title: "Brightness"
|
title: "Brightness"
|
||||||
subtitle: Brightness.available
|
subtitle: pageBrightness.visible
|
||||||
? "Sent to the monitor over DDC/CI, the same channel its buttons use."
|
? "The built-in panel through its backlight; external monitors over DDC/CI, the same channel their buttons use."
|
||||||
: Brightness.lastError
|
: Brightness.lastError
|
||||||
|
|
||||||
Repeater {
|
BrightnessControl {
|
||||||
model: Brightness.displays
|
id: pageBrightness
|
||||||
|
|
||||||
SettingRow {
|
|
||||||
id: brightnessRow
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
|
||||||
detail: modelData.connector ? modelData.connector + " · " + modelData.value + "%"
|
|
||||||
: modelData.value + "%"
|
|
||||||
divider: brightnessRow.index < Brightness.displays.length - 1
|
|
||||||
controlWidth: 190
|
|
||||||
|
|
||||||
ValueSlider {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.right: parent.right
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
value: brightnessRow.modelData.value / 100
|
|
||||||
onMoved: v => Brightness.set(brightnessRow.modelData.bus, Math.round(v * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ SettingsPage {
|
|||||||
// page rendered them -- search delivered people to this card and the
|
// page rendered them -- search delivered people to this card and the
|
||||||
// controls were not here.
|
// controls were not here.
|
||||||
SliderRow { setting: "batteryLowPercent" }
|
SliderRow { setting: "batteryLowPercent" }
|
||||||
SliderRow {
|
SliderRow { setting: "batteryCriticalPercent" }
|
||||||
setting: "batteryCriticalPercent"
|
ChoiceRow {
|
||||||
|
setting: "batteryCriticalAction"
|
||||||
divider: Battery.chargeLimitSupported
|
divider: Battery.chargeLimitSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +150,15 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Informational like the lid card above it: the behavior is a logind
|
||||||
|
// drop-in plus a compositor bind (see keybinds.lua), not a preference,
|
||||||
|
// and saying what a physical button does beats leaving it to be
|
||||||
|
// discovered by pressing it.
|
||||||
|
SettingsCard {
|
||||||
|
title: "The power button"
|
||||||
|
subtitle: "A press opens the power menu instead of powering off immediately. Holding it still forces power off through the firmware, as ever."
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|||||||
@@ -117,6 +117,25 @@ generate() {
|
|||||||
printf ' inhibit_sleep = 2\n'
|
printf ' inhibit_sleep = 2\n'
|
||||||
printf '}\n'
|
printf '}\n'
|
||||||
|
|
||||||
|
# On battery, dim before blanking -- GNOME's single largest idle
|
||||||
|
# battery saver, and the gentle warning that the screen is about to
|
||||||
|
# go. Two-thirds of the way to blank, floor of 30 seconds; -s saves
|
||||||
|
# the level and -r restores exactly it, so a person's brightness
|
||||||
|
# choice survives the round trip. Only where a real backlight exists:
|
||||||
|
# a desktop's DDC monitor is not worth waking over this.
|
||||||
|
if on_battery && (( blank_min > 0 )) \
|
||||||
|
&& command -v brightnessctl >/dev/null 2>&1 \
|
||||||
|
&& [[ -n "$(ls /sys/class/backlight 2>/dev/null)" ]]; then
|
||||||
|
dim_at=$(( blank_min * 60 * 2 / 3 ))
|
||||||
|
(( dim_at < 30 )) && dim_at=30
|
||||||
|
printf '\n# %s seconds -> dim, undone on activity.\n' "$dim_at"
|
||||||
|
printf 'listener {\n'
|
||||||
|
printf ' timeout = %s\n' "$dim_at"
|
||||||
|
printf ' on-timeout = brightnessctl -c backlight -s set 30%%\n'
|
||||||
|
printf ' on-resume = brightnessctl -c backlight -r\n'
|
||||||
|
printf '}\n'
|
||||||
|
fi
|
||||||
|
|
||||||
if (( blank_min > 0 )); then
|
if (( blank_min > 0 )); then
|
||||||
printf '\n# %s minutes -> screen off.\n' "$blank_min"
|
printf '\n# %s minutes -> screen off.\n' "$blank_min"
|
||||||
printf 'listener {\n'
|
printf 'listener {\n'
|
||||||
|
|||||||
@@ -68,16 +68,64 @@ cmd_guard() {
|
|||||||
sleep infinity
|
sleep infinity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The internal panel's connector name, from the compositor: eDP, LVDS and
|
||||||
|
# DSI are built in, everything else arrived through a cable -- the same rule
|
||||||
|
# panama-hw and LidPolicy use.
|
||||||
|
internal_connector() {
|
||||||
|
hyprctl -j monitors all 2>/dev/null \
|
||||||
|
| jq -r '[.[] | select(.name | test("^(eDP|LVDS|DSI)"; "i"))][0].name // empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bound to the lid switch by keybinds.lua. Closing a DOCKED lid turns the
|
||||||
|
# internal panel off, so nothing renders inside a closed shell and no
|
||||||
|
# workspace strands on an invisible output; an undocked lid does nothing here
|
||||||
|
# because logind is already suspending the machine. Failure direction: if the
|
||||||
|
# disable never runs, the panel stays lit inside the shell -- wasteful, not
|
||||||
|
# harmful.
|
||||||
|
cmd_close() {
|
||||||
|
"$HW" laptop || exit 0
|
||||||
|
"$HW" external-monitor || exit 0
|
||||||
|
local internal
|
||||||
|
internal="$(internal_connector)"
|
||||||
|
[[ -n "$internal" ]] || exit 0
|
||||||
|
hyprctl eval "hl.monitor({ output = \"$internal\", disable = true })" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-enable on open, preferring what the person chose for this panel in
|
||||||
|
# Settings (the same displays store monitors.lua reads at startup) and
|
||||||
|
# falling back to the panel's preferred mode.
|
||||||
|
cmd_open() {
|
||||||
|
"$HW" laptop || exit 0
|
||||||
|
local internal entry mode scale
|
||||||
|
internal="$(internal_connector)"
|
||||||
|
[[ -n "$internal" ]] || exit 0
|
||||||
|
entry="$(jq -c --arg name "$internal" '.displays[$name] // empty' \
|
||||||
|
"${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" 2>/dev/null)"
|
||||||
|
if [[ -n "$entry" ]]; then
|
||||||
|
mode="$(jq -r '.mode' <<<"$entry")"
|
||||||
|
scale="$(jq -r '.scale' <<<"$entry")"
|
||||||
|
hyprctl eval "hl.monitor({ output = \"$internal\", mode = \"$mode\", position = \"auto\", scale = $scale })" >/dev/null
|
||||||
|
else
|
||||||
|
hyprctl eval "hl.monitor({ output = \"$internal\", mode = \"preferred\", position = \"auto\", scale = \"auto\" })" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
case "${1:-status}" in
|
case "${1:-status}" in
|
||||||
status) cmd_status ;;
|
status) cmd_status ;;
|
||||||
guard) cmd_guard ;;
|
guard) cmd_guard ;;
|
||||||
|
close) cmd_close ;;
|
||||||
|
open) cmd_open ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
cat <<'USAGE'
|
cat <<'USAGE'
|
||||||
usage: panama-lid [status|guard]
|
usage: panama-lid [status|guard|close|open]
|
||||||
|
|
||||||
status what closing the lid would do right now, as JSON
|
status what closing the lid would do right now, as JSON
|
||||||
guard hold a handle-lid-switch inhibitor while an external display is
|
guard hold a handle-lid-switch inhibitor while an external display is
|
||||||
connected; exits immediately on a machine that needs none
|
connected; exits immediately on a machine that needs none
|
||||||
|
close docked lid closed: turn the internal panel off (bound to the lid
|
||||||
|
switch by keybinds.lua); does nothing undocked
|
||||||
|
open lid opened: turn the internal panel back on, restoring the mode
|
||||||
|
and scale chosen in Settings
|
||||||
USAGE
|
USAGE
|
||||||
;;
|
;;
|
||||||
*) printf 'panama-lid: unknown command: %s\n' "$1" >&2; exit 2 ;;
|
*) printf 'panama-lid: unknown command: %s\n' "$1" >&2; exit 2 ;;
|
||||||
|
|||||||
@@ -240,11 +240,34 @@ media_action() {
|
|||||||
show_message "$kind" "$label"
|
show_message "$kind" "$label"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Airplane mode: one rfkill state for every radio, with an OSD saying which
|
||||||
|
# way it went. "Any radio unblocked" reads as radios-on, so toggle means
|
||||||
|
# "block everything" from a mixed state -- the direction a person reaching
|
||||||
|
# for airplane mode wants.
|
||||||
|
airplane_action() {
|
||||||
|
case "${1:-toggle}" in
|
||||||
|
toggle)
|
||||||
|
if rfkill list 2>/dev/null | grep -q 'Soft blocked: no'; then
|
||||||
|
rfkill block all 2>/dev/null \
|
||||||
|
&& show_message "airplane-mode-symbolic" "Airplane mode on"
|
||||||
|
else
|
||||||
|
rfkill unblock all 2>/dev/null \
|
||||||
|
&& show_message "network-wireless-symbolic" "Airplane mode off"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'Usage: panama-osd airplane toggle\n' >&2
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
volume) shift; adjust_volume "$@" ;;
|
volume) shift; adjust_volume "$@" ;;
|
||||||
microphone) shift; adjust_microphone "$@" ;;
|
microphone) shift; adjust_microphone "$@" ;;
|
||||||
brightness) shift; adjust_brightness "$@" ;;
|
brightness) shift; adjust_brightness "$@" ;;
|
||||||
media) shift; media_action "$@" ;;
|
media) shift; media_action "$@" ;;
|
||||||
|
airplane) shift; airplane_action "$@" ;;
|
||||||
message)
|
message)
|
||||||
shift
|
shift
|
||||||
kind="${1:-}"
|
kind="${1:-}"
|
||||||
@@ -256,7 +279,7 @@ case "${1:-}" in
|
|||||||
show_message "$kind" "$*"
|
show_message "$kind" "$*"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
printf 'Usage: panama-osd volume|microphone|brightness|media|message ACTION [value]\n' >&2
|
printf 'Usage: panama-osd volume|microphone|brightness|media|airplane|message ACTION [value]\n' >&2
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Quickshell.Bluetooth
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -146,11 +147,38 @@ Singleton {
|
|||||||
// Running out is not ambient. Published at a priority Do Not Disturb does
|
// Running out is not ambient. Published at a priority Do Not Disturb does
|
||||||
// not silence, because the one notification you must not miss is the one
|
// not silence, because the one notification you must not miss is the one
|
||||||
// saying the machine is about to stop.
|
// saying the machine is about to stop.
|
||||||
|
//
|
||||||
|
// And then, unless told otherwise, the machine is put to sleep: a suspend
|
||||||
|
// at the urgent threshold preserves the session for days, a hard cut at
|
||||||
|
// 0% preserves nothing. The grace timer is what makes the warning above
|
||||||
|
// readable and the action cancelable -- plugging in during it stands
|
||||||
|
// everything down.
|
||||||
|
Timer {
|
||||||
|
id: criticalSuspend
|
||||||
|
interval: 15000
|
||||||
|
onTriggered: {
|
||||||
|
if (Battery.critical && !Battery.acOnline
|
||||||
|
&& DesktopPreferences.get("batteryCriticalAction") === "suspend")
|
||||||
|
Quickshell.execDetached(["systemctl", "suspend"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Battery
|
||||||
|
function onAcOnlineChanged(): void {
|
||||||
|
if (Battery.acOnline)
|
||||||
|
criticalSuspend.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Battery
|
target: Battery
|
||||||
function onCriticalChanged(): void {
|
function onCriticalChanged(): void {
|
||||||
if (!root.powerInitialized || !Battery.critical)
|
if (!root.powerInitialized || !Battery.critical)
|
||||||
return;
|
return;
|
||||||
|
if (!Battery.acOnline
|
||||||
|
&& DesktopPreferences.get("batteryCriticalAction") === "suspend")
|
||||||
|
criticalSuspend.restart();
|
||||||
StatusEvents.publish({
|
StatusEvents.publish({
|
||||||
key: "battery-critical",
|
key: "battery-critical",
|
||||||
glyph: "\u{F0083}",
|
glyph: "\u{F0083}",
|
||||||
|
|||||||
@@ -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 Power & Lock in Settings.
|
# @vicinae.description Open Power & Lock in Settings.
|
||||||
# @vicinae.keywords ["settings", "warn at", "urgent at", "stop charging at", "turn the screen off after", "lock the screen after", "suspend after", "lock before sleeping", "lock after"]
|
# @vicinae.keywords ["settings", "warn at", "urgent at", "at the urgent level", "stop charging at", "turn the screen off after", "lock the screen after", "suspend after", "lock before sleeping", "lock after"]
|
||||||
|
|
||||||
exec "$HOME/.config/quickshell/scripts/panama-action" settings-page power
|
exec "$HOME/.config/quickshell/scripts/panama-action" settings-page power
|
||||||
|
|||||||
+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.
|
||||||
|
|
||||||
149 settings across 30 groups. 70 of them are applied to the compositor and confirmed by reading the value back.
|
150 settings across 30 groups. 70 of them are applied to the compositor and confirmed by reading the value back.
|
||||||
|
|
||||||
## accessibility
|
## accessibility
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ Found on **Power & Lock**.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Warn at**<br>`batteryLowPercent` | 20 % | Mention the battery once it drops this low. Range 5–50. |
|
| **Warn at**<br>`batteryLowPercent` | 20 % | Mention the battery once it drops this low. Range 5–50. |
|
||||||
| **Urgent at**<br>`batteryCriticalPercent` | 5 % | Interrupt at this level, even during Do Not Disturb. Range 1–25. |
|
| **Urgent at**<br>`batteryCriticalPercent` | 5 % | Interrupt at this level, even during Do Not Disturb. Range 1–25. |
|
||||||
|
| **At the urgent level**<br>`batteryCriticalAction` | suspend | What happens when the battery reaches the urgent threshold while discharging Choices: Suspend, Only warn. |
|
||||||
| **Stop charging at**<br>`batteryChargeLimit` | 100 % | Charging to less than full is easier on the battery over years. Range 50–100. |
|
| **Stop charging at**<br>`batteryChargeLimit` | 100 % | Charging to less than full is easier on the battery over years. Range 50–100. |
|
||||||
|
|
||||||
## capture
|
## capture
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ if [[ -f /etc/udev/rules.d/99-panama-gpu.rules ]]; then
|
|||||||
sudo udevadm trigger --subsystem-match=drm
|
sudo udevadm trigger --subsystem-match=drm
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The power-key drop-in lands via the same copy; logind reads its config only
|
||||||
|
# at start, so without this the button keeps hard-powering-off until the next
|
||||||
|
# boot. Restarting logind is what Fedora's own updates do and current systemd
|
||||||
|
# preserves sessions across it.
|
||||||
|
if [[ -f /etc/systemd/logind.conf.d/panama-power-key.conf ]]; then
|
||||||
|
log "Reloading logind so the power button opens the menu instead of powering off"
|
||||||
|
sudo systemctl try-restart systemd-logind 2>/dev/null \
|
||||||
|
|| log "Could not restart logind; the power-button change applies at next boot"
|
||||||
|
fi
|
||||||
|
|
||||||
# The document-portal drop-in lands via the same copy. A user unit's drop-ins
|
# The document-portal drop-in lands via the same copy. A user unit's drop-ins
|
||||||
# are read at daemon-reload, so without this the guard applies from the next
|
# are read at daemon-reload, so without this the guard applies from the next
|
||||||
# login rather than from now -- and the failure it guards against is one you
|
# login rather than from now -- and the failure it guards against is one you
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ SHELL_WORDS='^(if|then|else|elif|fi|for|while|until|do|done|case|esac|in|functio
|
|||||||
# authselect is on the list for the same reason: it manages Fedora's PAM and
|
# authselect is on the list for the same reason: it manages Fedora's PAM and
|
||||||
# nsswitch profiles and arrives with fprintd-pam, realmd and nss-mdns, so the
|
# nsswitch profiles and arrives with fprintd-pam, realmd and nss-mdns, so the
|
||||||
# fingerprint aliases in config/bash can rely on it without declaring it.
|
# fingerprint aliases in config/bash can rely on it without declaring it.
|
||||||
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|lsof|authselect|setsid|nohup|grub2-mkconfig)$'
|
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|ls|rfkill|lsof|authselect|setsid|nohup|grub2-mkconfig)$'
|
||||||
|
|
||||||
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
|
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,12 @@ mkdir -p "$fake/bin"
|
|||||||
|
|
||||||
# ── 1. Panama never takes the lid over ───────────────────────────────────────
|
# ── 1. Panama never takes the lid over ───────────────────────────────────────
|
||||||
|
|
||||||
if compgen -G "$repo_dir/config/copy/etc/systemd/logind.conf.d/*" >/dev/null 2>&1; then
|
# Specifically the LID. Other keys may be configured there -- the power-key
|
||||||
note 'a logind drop-in is shipped; the inhibitor approach exists so the lid is never left doing nothing'
|
# drop-in is deliberate policy with a compositor bind behind it -- but a
|
||||||
|
# HandleLidSwitch line would replace the inhibitor design this file protects.
|
||||||
|
if compgen -G "$repo_dir/config/copy/etc/systemd/logind.conf.d/*" >/dev/null 2>&1 \
|
||||||
|
&& sed 's/#.*//' "$repo_dir"/config/copy/etc/systemd/logind.conf.d/* | grep -q 'HandleLidSwitch'; then
|
||||||
|
note 'a logind drop-in sets HandleLidSwitch; the inhibitor approach exists so the lid is never left doing nothing'
|
||||||
fi
|
fi
|
||||||
# Comments are stripped first: these files explain at length what they
|
# Comments are stripped first: these files explain at length what they
|
||||||
# deliberately do NOT do, and prose naming HandleLidSwitch or lock-session is
|
# deliberately do NOT do, and prose naming HandleLidSwitch or lock-session is
|
||||||
|
|||||||
Reference in New Issue
Block a user