diff --git a/nextboot-selector/README.md b/nextboot-selector/README.md new file mode 100644 index 0000000..0e54338 --- /dev/null +++ b/nextboot-selector/README.md @@ -0,0 +1,36 @@ +# Next Boot Selector + +Next Boot Selector selects which boot entry to boot for the next reboot only. Useful if you're dual-booting and goes back and forth to/from Windows. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `avivbintangaringga/nextboot-selector` | +| Entries | Bar widget: `nextboot-selector`; panel: `panel` | + +## Requirements + +This plugin mainly requires `efibootmgr` but need to escalate using `pkexec` or `sudo`. Note that if you use `sudo`, you need to set the option `NOPASSWD` for the `efibootmgr` command so it doesn't requires password to run. `reboot` command is also required if reboot on select is enabled. + +## Usage + +Add the `nextboot-selector` widget to a bar. Click it to show a panel that lists all boot entries on your computer. Click one of the entry to change the next boot to it. + +Open the panel directly with: + +```sh +noctalia msg panel-toggle avivbintangaringga/nextboot-selector:panel +``` + +You can add more keywords to the exclusion list in the settings to hide unwanted entries. You also can make it automatically reboots after selecting an entry. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `excluded_keywords` | `string_list` | `["pxe", "uefi os", "uefi shell", "shell", "ipv4", "ipv6", "network", "diagnostics", "setup", "recovery"]` | Keywords to exclude unwanted boot entries | +| `privilege_command` | `string` | `pkexec` | Command to escalate the privilege of the `efibootmgr` command | +| `close_on_select` | `bool` | `false` | Whether to close the panel after selecting an entry | +| `reboot_on_select` | `bool` | `false` | Whether to automatically reboot after selecting an entry | +| `reboot_command` | `string` | `reboot` | Command to be executed when reboot on select is enabled | diff --git a/nextboot-selector/nextboot_selector.luau b/nextboot-selector/nextboot_selector.luau new file mode 100644 index 0000000..33fdc96 --- /dev/null +++ b/nextboot-selector/nextboot_selector.luau @@ -0,0 +1,9 @@ +noctalia.setUpdateInterval(1000) + +function update() + barWidget.setGlyph("refresh-dot") +end + +function onClick() + noctalia.togglePanel("avivbintangaringga/nextboot-selector:panel") +end diff --git a/nextboot-selector/panel.luau b/nextboot-selector/panel.luau new file mode 100644 index 0000000..ff110bd --- /dev/null +++ b/nextboot-selector/panel.luau @@ -0,0 +1,149 @@ +function tr(key) + return noctalia.tr(key) +end + +function cfg(key) + return noctalia.getConfig(key) +end + +local env = getfenv() +local excludedKeywords = cfg("excluded_keywords") +local privilegeCmd = cfg("privilege_command") +local closeOnSelect = cfg("close_on_select") +local rebootOnSelect = cfg("reboot_on_select") +local rebootCmd = cfg("reboot_command") + +function parseEntries(output) + local entries = {} + + local bootNext = output:match("BootNext:%s*(%x+)") + local bootCurrent = output:match("BootCurrent:%s*(%x+)") + + local selectedId = bootNext or bootCurrent + if selectedId then + selectedId = selectedId:upper() + end + + for line in output:gmatch("[^\r\n]+") do + local id, active, name = line:match("^Boot(%x%x%x%x)(%*?)%s+([^\t]+)") + if id then + id = id:upper() + table.insert(entries, { + id = id, + name = name, + isActive = active == "*", + isSelected = selectedId ~= nil and id == selectedId, + }) + end + end + + return entries +end + +function excludeEntries(entries, keywords) + local filtered = {} + for _, entry in ipairs(entries) do + local haystack = entry.name:lower() + local excluded = false + for _, word in ipairs(keywords) do + if haystack:find(word:lower(), 1, true) then + excluded = true + break + end + end + if not excluded then + table.insert(filtered, entry) + end + end + return filtered +end + +function resetBootNext() + noctalia.runAsync(`{privilegeCmd} efibootmgr --delete-bootnext`, function(result) + load() + end, 5000) +end + +function render(entries) + local entriesButton = {} + for i, entry in ipairs(entries) do + local callBackFn = `setBootNext_{entry.id}` + env[callBackFn] = function() + noctalia.runAsync(`{privilegeCmd} efibootmgr --bootnext {entry.id}`, function(result) + if not result.timedOut then + if result.exitCode == 0 then + load() + + if rebootOnSelect then + noctalia.runAsync(rebootCmd) + end + + if closeOnSelect then + panel.close() + end + else + noctalia.notify(tr("notification.failed_to_set_next_boot"), result.stderr) + end + else + noctalia.notify(tr("notification.failed_to_set_next_boot"), tr("notification.timed_out")) + end + end, 60 * 1000) + end + + table.insert( + entriesButton, + ui.button({ + text = entry.name, + selected = entry.isSelected, + enabled = entry.isActive, + onClick = callBackFn, + contentAlign = "start", + glyph = if entry.isSelected then "circle-check" else "circle", + glyphSize = 18, + }) + ) + end + + panel.render(ui.column({ + flexGrow = 1, + }, { + ui.row({ + justify = "space_between", + align = "center" + }, { + ui.label({ + text = tr("panel.title"), + fontSize = 16 + }), + ui.button({ + text = tr("panel.reset"), + glyph = "reload", + variant = "ghost", + controlSize = "sm", + onClick = "resetBootNext" + }) + }), + ui.separator({ + thickness = 2, + orientation = "horizontal", + spacing = 8, + }), + ui.scroll({ + flexGrow = 1, + gap = 8, + }, entriesButton), + })) +end + +function load() + noctalia.runAsync("efibootmgr", function(result) + if result.exitCode == 0 then + local entries = parseEntries(result.stdout) + render(excludeEntries(entries, excludedKeywords)) + end + end, 5000) +end + +function onOpen() + load() +end diff --git a/nextboot-selector/plugin.toml b/nextboot-selector/plugin.toml new file mode 100644 index 0000000..a94fbbb --- /dev/null +++ b/nextboot-selector/plugin.toml @@ -0,0 +1,58 @@ +id = "avivbintangaringga/nextboot-selector" +name = "Next Boot Selector" +version = "1.0.0" +plugin_api = 3 +icon = "refresh-dot" +author = "avivbintangaringga" +description = "Select boot entry for the next reboot" +tags = [ "utility" ] +dependencies = [ "efibootmgr", "pkexec", "reboot", "sudo" ] +license = "MIT" + +[[widget]] +id = "nextboot-selector" +entry = "nextboot_selector.luau" + +[[panel]] +id = "panel" +entry = "panel.luau" +placement = "attached" +position = "auto" +open_near_click = true +width = 350 +height = 250 + +[[setting]] +key = "excluded_keywords" +type = "string_list" +label_key = "setting.excluded_keywords.label" +description_key = "setting.excluded_keywords.description" +default = ["pxe", "uefi os", "uefi shell", "shell", "ipv4", "ipv6", "network", "diagnostics", "setup", "recovery"] + +[[setting]] +key = "privilege_command" +type = "string" +label_key = "setting.privilege_command.label" +description_key = "setting.privilege_command.description" +default = "pkexec" + +[[setting]] +key = "close_on_select" +type = "bool" +label_key = "setting.close_on_select.label" +description_key = "setting.close_on_select.description" +default = false + +[[setting]] +key = "reboot_on_select" +type = "bool" +label_key = "setting.reboot_on_select.label" +description_key = "setting.reboot_on_select.description" +default = false + +[[setting]] +key = "reboot_command" +type = "string" +label_key = "setting.reboot_command.label" +description_key = "setting.reboot_command.description" +default = "reboot" diff --git a/nextboot-selector/thumbnail.webp b/nextboot-selector/thumbnail.webp new file mode 100644 index 0000000..2520218 Binary files /dev/null and b/nextboot-selector/thumbnail.webp differ diff --git a/nextboot-selector/translations/en.json b/nextboot-selector/translations/en.json new file mode 100644 index 0000000..a0cd4ea --- /dev/null +++ b/nextboot-selector/translations/en.json @@ -0,0 +1,16 @@ +{ + "panel.title": "Next Boot Selector", + "panel.reset": "Reset", + "setting.excluded_keywords.label": "Excluded keywords", + "setting.excluded_keywords.description": "Keywords to exclude entries (case insensitive)", + "setting.privilege_command.label": "Privilege command", + "setting.privilege_command.description": "Command to escalate the privilege of the efibootmgr command", + "setting.close_on_select.label": "Close on select", + "setting.close_on_select.description": "Whether to close the panel after selecting an entry", + "setting.reboot_on_select.label": "Reboot on select", + "setting.reboot_on_select.description": "Whether to automatically reboot after selecting an entry", + "setting.reboot_command.label": "Reboot command", + "setting.reboot_command.description": "Command to be executed when reboot on select is enabled", + "notification.failed_to_set_next_boot": "Failed to set next boot", + "notification.timed_out": "Timed out" +}