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
16 changes: 16 additions & 0 deletions CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,22 @@ Miscellaneous:
the scroll wheel moves the selection. Using the mouse stops the `timeout`
countdown, as pressing a key does. On BIOS this uses the PS/2 (or emulated
PS/2) mouse; on UEFI any pointer device the firmware exposes.
* `boot_keybind` - Binds a single key to immediately boot a given menu entry,
in the form `<key> <target>`, where `target` is either a 1-based index or
an entry path, using the same syntax and escaping rules as `default_entry`.
Unlike the number keys and `default_entry`'s own numeric mode, an index
here counts every entry in the config unconditionally, regardless of
whether any sub-menu is currently expanded on screen - a `boot_keybind` is
meant for booting straight to something without looking at the menu, so
its target must not depend on menu UI state. A path is resolved by
expanding whatever directories it needs to, so it is unaffected by
expansion state either way, the same as an index. A directory itself
cannot be targeted by either form. There can be multiple of this option,
one per binding. If `keyboard_layout` remaps keystrokes, the key here must
be given in the remapped layout, not physical QWERTY. Keys already bound
to a built-in action (`1`-`9`, `e`, `s`, `u`, `b`) are reserved and cannot
be overridden. If the same key is bound more than once, the first binding
wins and later ones are ignored.

Limine interface control options:

Expand Down
167 changes: 167 additions & 0 deletions common/menu.c
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,40 @@ static void print_entry_comment(const struct menu_entry *entry, size_t row) {
FOR_TERM(TERM->scroll_enabled = true);
}

#define BOOT_KEYBIND_MAX 64
#define BOOT_KEYBIND_SCAN_LIMIT 256

// Mirrors the case labels in the switch below (digits, e, s, u, b) so a
// BOOT_KEYBIND can never shadow a built-in action.
static inline bool is_reserved_firmware_key(int k) {
return (k >= '1' && k <= '9') || k == 'e' || k == 's' || k == 'u' || k == 'b';
}

// Deliberately distinct from digit-key select / DEFAULT_ENTRY's numeric
// mode, which both go through print_tree() and are relative to what's
// currently expanded on screen. A BOOT_KEYBIND is meant for fast, unattended
// booting into a specific entry - its whole point breaks if the target
// silently shifts depending on menu UI state the user isn't even looking
// at. This walks the full tree unconditionally, ignoring ->expanded.
static struct menu_entry *find_entry_by_absolute_index(struct menu_entry *node, size_t *pos) {
for (; node != NULL; node = node->next) {
if (should_skip_entry(node)) {
continue;
}
if (*pos == 1) {
return node;
}
(*pos)--;
if (node->sub != NULL) {
struct menu_entry *found = find_entry_by_absolute_index(node->sub, pos);
if (found != NULL) {
return found;
}
}
}
return NULL;
}

noreturn void _menu(bool first_run) {
size_t data_size = (uintptr_t)data_end - (uintptr_t)data_begin;
#if defined (BIOS)
Expand Down Expand Up @@ -1835,6 +1869,64 @@ noreturn void _menu(bool first_run) {
}
}

struct {
int key;
size_t config_idx; // which BOOT_KEYBIND directive (by config_get_value
// index), so the target is re-parsed fresh on a
// match instead of every binding pre-resolving
// and storing a path up front.
} boot_keybinds[BOOT_KEYBIND_MAX];
size_t boot_keybind_count = 0;

for (size_t ki = 0; ki < BOOT_KEYBIND_SCAN_LIMIT && boot_keybind_count < BOOT_KEYBIND_MAX; ki++) {
char *val = config_get_value(NULL, ki, "boot_keybind");
if (val == NULL) {
break;
}

char val_copy[64];
size_t vlen = strlen(val);
if (vlen >= sizeof(val_copy)) {
continue;
}
memcpy(val_copy, val, vlen + 1);

char *p = val_copy;
while (*p && isspace((unsigned char)*p)) p++;
if (*p == '\0') continue;
char *q = p + strlen(p) - 1;
while (q > p && isspace((unsigned char)*q)) *q-- = '\0';

// Format: "<key> <1-based index or path>", e.g. "h 4" or "h /Linux".
// The target itself isn't parsed here; see the match site below.
char *space = strchr(p, ' ');
if (space == NULL) continue;
*space = '\0';
char *target = space + 1;
while (*target && isspace((unsigned char)*target)) target++;
if (*target == '\0') continue;

if (strlen(p) != 1) continue;
int norm_key = tolower((unsigned char)p[0]);
if (is_reserved_firmware_key(norm_key)) {
continue;
}

// First BOOT_KEYBIND for a key wins; later duplicates are ignored.
bool dup = false;
for (size_t j = 0; j < boot_keybind_count; j++) {
if (boot_keybinds[j].key == norm_key) {
dup = true;
break;
}
}
if (dup) continue;

boot_keybinds[boot_keybind_count].key = norm_key;
boot_keybinds[boot_keybind_count].config_idx = ki;
boot_keybind_count++;
}

bool skip_timeout = false;
struct menu_entry *selected_menu_entry = NULL;

Expand Down Expand Up @@ -2228,6 +2320,81 @@ noreturn void _menu(bool first_run) {

}
}

for (size_t ki = 0; ki < boot_keybind_count; ki++) {
if (tolower((unsigned char)c) != boot_keybinds[ki].key) {
continue;
}

// Re-fetch and re-parse only the matched binding's target,
// instead of every binding pre-resolving and storing a target
// up front. A keypress is a human-timescale event, so the
// extra parse work here costs nothing.
char *val = config_get_value(NULL, boot_keybinds[ki].config_idx, "boot_keybind");
if (val == NULL) {
continue;
}
char val_copy[64];
size_t vlen = strlen(val);
if (vlen >= sizeof(val_copy)) {
continue;
}
memcpy(val_copy, val, vlen + 1);
char *p = val_copy;
while (*p && isspace((unsigned char)*p)) p++;
char *q = p + strlen(p) - 1;
while (q > p && isspace((unsigned char)*q)) *q-- = '\0';
char *space = strchr(p, ' ');
if (space == NULL) continue;
*space = '\0';
char *bkey_target = space + 1;
while (*bkey_target && isspace((unsigned char)*bkey_target)) bkey_target++;
if (*bkey_target == '\0') continue;

// Same is-it-all-digits test DEFAULT_ENTRY uses to tell an
// index apart from a path.
bool is_index = true;
for (const char *t = bkey_target; *t != '\0'; t++) {
if (*t < '0' || *t > '9') {
is_index = false;
break;
}
}

struct menu_entry *target = NULL;

if (is_index) {
// strtoui saturates to UINT64_MAX on overflow; no errno here.
const char *endptr = NULL;
uint64_t parsed = strtoui(bkey_target, &endptr, 10);
if (endptr == bkey_target || *endptr != '\0' || parsed == 0
|| parsed == UINT64_MAX || parsed > SIZE_MAX) {
continue;
}
// Unlike digit-key select and DEFAULT_ENTRY's numeric mode,
// this counts every entry in the whole config regardless of
// what's currently expanded on screen - see
// find_entry_by_absolute_index's comment for why.
size_t pos = (size_t)parsed;
struct menu_entry *found = find_entry_by_absolute_index(menu_tree, &pos);
if (found != NULL) {
target = found;
}
} else {
// Unlike an index, a path is resolved with expand_dirs so it
// finds its target regardless of current expansion state -
// it doesn't drift if the menu's expanded/collapsed state
// changes, the way a plain index does.
size_t found_index = 0;
find_entry_by_path(bkey_target, menu_tree, 0, &target, &found_index, true);
}

if (target == NULL || target->sub != NULL) {
continue; // try other bindings instead of aborting
}
selected_menu_entry = target;
goto autoboot;
}
switch (c) {
case '1': case '2': case '3': case '4': case '5':
case '6': case '7': case '8': case '9': {
Expand Down