diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 20f980744..3eabac2d2 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -1126,6 +1126,7 @@ Singleton { property string customPowerActionPowerOff: Spec.SPEC.customPowerActionPowerOff.def property var customPowerButtons: Spec.SPEC.customPowerButtons.def property var powerMenuBootEntries: Spec.SPEC.powerMenuBootEntries.def + property bool powerMenuFirmwareSetup: Spec.SPEC.powerMenuFirmwareSetup.def property bool updaterCheckOnStart: Spec.SPEC.updaterCheckOnStart.def property bool updaterUseCustomCommand: Spec.SPEC.updaterUseCustomCommand.def diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 791d1f82c..51a8cd603 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -1445,6 +1445,9 @@ var LOCAL_SPEC = { powerMenuBootEntries: { def: [] }, + powerMenuFirmwareSetup: { + def: false + }, updaterCheckOnStart: { def: false }, diff --git a/quickshell/Modules/Settings/PowerSleepTab.qml b/quickshell/Modules/Settings/PowerSleepTab.qml index 7fa1a9818..d642adf48 100644 --- a/quickshell/Modules/Settings/PowerSleepTab.qml +++ b/quickshell/Modules/Settings/PowerSleepTab.qml @@ -278,14 +278,24 @@ Item { SettingsCard { width: parent.width iconName: "restart_alt" - title: I18n.tr("Reboot to another OS", "settings card title, lists EFI boot entries the power menu can reboot into") + title: I18n.tr("Reboot targets", "settings card title, firmware setup and EFI boot entries the power menu can reboot into once") settingKey: "powerMenuBootEntries" - tags: ["power", "menu", "reboot", "boot", "efi", "uefi", "windows", "dual boot", "bootnext"] + tags: ["power", "menu", "reboot", "boot", "efi", "uefi", "bios", "firmware", "windows", "dual boot", "bootnext"] // Saved entries stay listed wherever this config lands, so they can always be removed - visible: SettingsData.powerMenuBootEntries.length > 0 || (BootEntryService.status !== "unknown" && BootEntryService.status !== "noEfi") + visible: SessionService.firmwareSetupSupported || SettingsData.powerMenuBootEntries.length > 0 || (BootEntryService.status !== "unknown" && BootEntryService.status !== "noEfi") Component.onCompleted: BootEntryService.refresh() + SettingsToggleRow { + settingKey: "powerMenuFirmwareSetup" + tags: ["power", "menu", "reboot", "firmware", "setup", "bios", "uefi"] + visible: SessionService.firmwareSetupSupported + iconName: "developer_board" + text: I18n.tr("Firmware setup", "settings toggle, adds a power menu action that reboots into the UEFI/BIOS settings screen") + checked: SettingsData.powerMenuFirmwareSetup + onToggled: checked => SettingsData.set("powerMenuFirmwareSetup", checked) + } + SettingsRow { visible: BootEntryService.unavailableReason !== "" title: BootEntryService.unavailableReason diff --git a/quickshell/Services/AppSearchService.qml b/quickshell/Services/AppSearchService.qml index fefe3a3d8..91833bdee 100644 --- a/quickshell/Services/AppSearchService.qml +++ b/quickshell/Services/AppSearchService.qml @@ -361,6 +361,7 @@ Singleton { hibernate: ["hibernate"], reboot: ["reboot", "restart"], softreboot: ["soft reboot"], + firmwaresetup: ["firmware", "bios", "uefi"], poweroff: ["poweroff", "shutdown", "halt"], restart: ["restart", "shell", "reload"] }) diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index dbaa0d031..d8bc5698c 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -23,6 +23,9 @@ Singleton { property bool userManagerAvailable: false property bool hibernateSupported: false readonly property bool softRebootSupported: systemctlCommandAvailable + // The firmware advertises boot-to-setup in OsIndicationsSupported, the same bit logind checks + property bool firmwareSetupAdvertised: false + readonly property bool firmwareSetupSupported: !isBSD && firmwareSetupAdvertised && (systemctlCommandAvailable || loginctlCommandAvailable) property bool inhibitorAvailable: true property bool idleInhibited: false property string inhibitReason: "Keep system awake" @@ -111,9 +114,10 @@ loginctl=0; command -v loginctl >/dev/null 2>&1 && loginctl=1 systemctl=0; usermgr=0 if command -v systemctl >/dev/null 2>&1; then systemctl=1; systemctl --user --no-pager show-environment >/dev/null 2>&1 && usermgr=1; fi hibernate=0; grep -q disk /sys/power/state 2>/dev/null && hibernate=1 +fwsetup=0; b=$(od -An -tu1 -j4 -N1 /sys/firmware/efi/efivars/OsIndicationsSupported-8be4df61-93ca-11d2-aa0d-00e098032b8c 2>/dev/null); [ -n "$b" ] && [ $(($b & 1)) = 1 ] && fwsetup=1 nvidia=; command -v prime-run >/dev/null 2>&1 && nvidia=prime-run [ -z "$nvidia" ] && command -v nvidia-offload >/dev/null 2>&1 && nvidia=nvidia-offload -printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s\nnvidia=%s\n' "$uwsm" "$elogind" "$loginctl" "$systemctl" "$usermgr" "$hibernate" "$nvidia"`] +printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s\nfwsetup=%s\nnvidia=%s\n' "$uwsm" "$elogind" "$loginctl" "$systemctl" "$usermgr" "$hibernate" "$fwsetup" "$nvidia"`] stdout: SplitParser { splitMarker: "\n" @@ -141,6 +145,9 @@ printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s case "hibernate": root.hibernateSupported = !root.isBSD && value === "1"; return; + case "fwsetup": + root.firmwareSetupAdvertised = value === "1"; + return; case "nvidia": root.nvidiaCommand = value; return; @@ -587,6 +594,27 @@ printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s Quickshell.execDetached(["systemctl", "soft-reboot"]); } + // Skips customPowerActionReboot: no systemctl or loginctl command sets the firmware flag without also rebooting. + function rebootToFirmwareSetup() { + firmwareSetupProcess.running = true; + } + + Process { + id: firmwareSetupProcess + running: false + command: root.powerManagerCommand("reboot").concat(["--firmware-setup"]) + + stderr: StdioCollector { + id: firmwareSetupErrors + } + + onExited: exitCode => { + if (exitCode === 0) + return; + ToastService.showError(I18n.tr("Failed to reboot to firmware setup", "error toast, firmware setup is the UEFI/BIOS settings screen"), firmwareSetupErrors.text.trim()); + } + } + function poweroff() { if (SettingsData.customPowerActionPowerOff.length === 0) { Quickshell.execDetached(powerManagerCommand("poweroff")); @@ -595,11 +623,12 @@ printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s } } - // Actions added in settings, listed after the built-in ones: boot entries, then custom buttons. - // Each list lines up index for index with its setting. + // Actions added in settings, listed after the built-in ones: firmware setup, boot entries, then custom buttons. + // The boot entry and custom lists line up index for index with their setting. readonly property var bootEntryActions: (SettingsData.powerMenuBootEntries || []).map(entry => "bootnext:" + entry.id) + readonly property var firmwareSetupActions: SettingsData.powerMenuFirmwareSetup && firmwareSetupSupported ? ["firmwaresetup"] : [] readonly property var customPowerActions: (SettingsData.customPowerButtons || []).map((button, i) => "custom:" + i) - readonly property var extraPowerActions: bootEntryActions.concat(customPowerActions) + readonly property var extraPowerActions: firmwareSetupActions.concat(bootEntryActions, customPowerActions) function isPowerActionSupported(action) { switch (action) { @@ -642,6 +671,11 @@ printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s case "softreboot": softReboot(); return true; + case "firmwaresetup": + if (!firmwareSetupSupported) + return false; + rebootToFirmwareSetup(); + return true; case "poweroff": poweroff(); return true; @@ -685,6 +719,12 @@ printf 'uwsm=%s\nelogind=%s\nloginctl=%s\nsystemctl=%s\nusermgr=%s\nhibernate=%s "label": I18n.tr("Soft reboot"), "key": "B" }; + case "firmwaresetup": + return { + "icon": "developer_board", + "label": I18n.tr("Reboot to firmware setup", "power menu action, firmware setup is the UEFI/BIOS settings screen"), + "key": "F" + }; case "logout": return { "icon": "logout", diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 7a1dcf2a7..33cc3d2ff 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -9302,6 +9302,32 @@ "parentLabel": "Power & battery", "description": "Fades the screen out first. Input during the fade cancels it" }, + { + "section": "powerMenuFirmwareSetup", + "label": "Firmware setup", + "tabIndex": 21, + "category": "Power & sleep", + "keywords": [ + "battery", + "bios", + "charge", + "charging", + "energy", + "firmware", + "hibernate", + "menu", + "power", + "reboot", + "restart", + "setup", + "shutdown", + "sleep", + "suspend", + "uefi" + ], + "parentLabel": "Power & battery", + "icon": "developer_board" + }, { "section": "powerMenuGridLayout", "label": "Grid layout", @@ -9486,12 +9512,12 @@ }, { "section": "powerMenuBootEntries", - "label": "Reboot to another OS", + "label": "Reboot targets", "tabIndex": 21, "category": "Power & sleep", "keywords": [ - "another", "battery", + "bios", "boot", "bootnext", "charge", @@ -9499,6 +9525,7 @@ "dual boot", "efi", "energy", + "firmware", "hibernate", "menu", "power", @@ -9507,6 +9534,7 @@ "shutdown", "sleep", "suspend", + "targets", "uefi", "windows" ],