:: commit 81001baa52ae8d17c091095e04f12c9a6ce8aa1d

Kamila Szewczyk <k@iczelia.net> — 2026-05-15 23:08

parents: 529fc81297

feat: add support for fractional timeouts.

diff --git a/CONFIG.md b/CONFIG.md
index 4e5182e8..7d6bb473 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -75,8 +75,9 @@ Some options take *paths* as strings; these are described in the next section.
 Miscellaneous:
 
 * `timeout` - Specifies the timeout in seconds before the first *entry* is
-  automatically booted. If set to `no`, disable automatic boot. If set to `0`,
-  boots default entry instantly (see `default_entry` option).
+  automatically booted. Decimal values such as `0.25` are accepted. If set to
+  `no`, disable automatic boot. If set to `0`, boots default entry instantly
+  (see `default_entry` option).
 * `quiet` - If set to `yes`, enable quiet mode, where all screen output except
   panics and important warnings is suppressed. If `timeout` is not 0, the
   `timeout` still occurs, and pressing any key during the timeout will reveal
diff --git a/common/lib/getchar.c b/common/lib/getchar.c
index b3864fb4..a6a10a0e 100644
--- a/common/lib/getchar.c
+++ b/common/lib/getchar.c
@@ -156,12 +156,21 @@ static int input_sequence(void) {
     return 0;
 }
 
-int pit_sleep_and_quit_on_keypress(int seconds) {
+int pit_sleep_ms_and_quit_on_keypress(uint64_t milliseconds) {
+    uint64_t ticks64 = milliseconds > (UINT64_MAX - 999) / 18
+                     ? UINT64_MAX
+                     : (milliseconds * 18 + 999) / 1000;
+    uint32_t ticks = ticks64 > UINT32_MAX ? UINT32_MAX : ticks64;
+
+    if (ticks == 0) {
+        return 0;
+    }
+
     if (!serial) {
-        return _pit_sleep_and_quit_on_keypress(seconds * 18);
+        return _pit_sleep_and_quit_on_keypress(ticks);
     }
 
-    for (int i = 0; i < seconds * 18; i++) {
+    for (uint32_t i = 0; i < ticks; i++) {
         int ret = _pit_sleep_and_quit_on_keypress(1);
 
         if (ret != 0) {
@@ -195,6 +204,10 @@ again:
 
     return 0;
 }
+
+int pit_sleep_and_quit_on_keypress(int seconds) {
+    return pit_sleep_ms_and_quit_on_keypress((uint64_t)seconds * 1000);
+}
 #endif
 
 #if defined (UEFI)
@@ -254,7 +267,7 @@ static int input_sequence(bool ext,
     return 0;
 }
 
-int pit_sleep_and_quit_on_keypress(int seconds) {
+int pit_sleep_ms_and_quit_on_keypress(uint64_t milliseconds) {
     EFI_KEY_DATA kd;
 
     UINTN which;
@@ -287,7 +300,8 @@ int pit_sleep_and_quit_on_keypress(int seconds) {
 restart:
     gBS->CreateEvent(EVT_TIMER, TPL_CALLBACK, NULL, NULL, &events[1]);
 
-    gBS->SetTimer(events[1], TimerRelative, (uint64_t)10000000 * seconds);
+    gBS->SetTimer(events[1], TimerRelative,
+                  milliseconds > UINT64_MAX / 10000 ? UINT64_MAX : milliseconds * 10000);
 
 again:
     memset(&kd, 0, sizeof(EFI_KEY_DATA));
@@ -362,4 +376,8 @@ again:
     gBS->CloseEvent(events[1]);
     return ret;
 }
+
+int pit_sleep_and_quit_on_keypress(int seconds) {
+    return pit_sleep_ms_and_quit_on_keypress((uint64_t)seconds * 1000);
+}
 #endif
diff --git a/common/lib/misc.h b/common/lib/misc.h
index c5a20c1d..8ea9d504 100644
--- a/common/lib/misc.h
+++ b/common/lib/misc.h
@@ -56,6 +56,7 @@ uint8_t int_to_bcd(uint8_t val);
 noreturn void panic(bool allow_menu, const char *fmt, ...);
 
 int pit_sleep_and_quit_on_keypress(int seconds);
+int pit_sleep_ms_and_quit_on_keypress(uint64_t milliseconds);
 
 uint64_t strtoui(const char *s, const char **end, int base);
 
diff --git a/common/menu.c b/common/menu.c
index b465e826..2fadeaa4 100644
--- a/common/menu.c
+++ b/common/menu.c
@@ -40,6 +40,7 @@ EFI_GUID limine_efi_vendor_guid =
 #define TOK_VALUE 2
 #define TOK_BADKEY 3
 #define TOK_COMMENT 4
+#define TIMEOUT_MAX_MS (UINT64_C(9999) * 1000)
 
 static char interface_help_colour[24] = "\e[38;2;0;170;0m";
 static char interface_help_colour_bright[24] = "\e[38;2;85;255;85m";
@@ -47,6 +48,22 @@ static char menu_branding_colour[24] = "\e[38;2;0;170;170m";
 
 static char *menu_branding = NULL;
 
+static char *append_uint_dec(char *p, uint64_t val) {
+    char buf[20];
+    size_t i = 0;
+
+    do {
+        buf[i++] = '0' + (val % 10);
+        val /= 10;
+    } while (val != 0);
+
+    while (i != 0) {
+        *p++ = buf[--i];
+    }
+    *p = '\0';
+    return p;
+}
+
 static char *write_uint8_dec(char *p, uint8_t v) {
     if (v >= 100) {
         *p++ = '0' + v / 100;
@@ -61,6 +78,73 @@ static char *write_uint8_dec(char *p, uint8_t v) {
     return p;
 }
 
+static uint64_t parse_timeout_ms(const char *str) {
+    uint64_t seconds = 0;
+    uint64_t milliseconds = 0;
+    bool any = false;
+
+    while (isdigit(*str)) {
+        any = true;
+        if (seconds <= TIMEOUT_MAX_MS / 1000) {
+            seconds *= 10;
+            seconds += *str - '0';
+        }
+        str++;
+    }
+
+    if (*str == '.') {
+        uint64_t multiplier = 100;
+
+        str++;
+
+        while (isdigit(*str)) {
+            any = true;
+
+            if (multiplier != 0) {
+                milliseconds += (*str - '0') * multiplier;
+                multiplier /= 10;
+            } else if (*str != '0' && milliseconds < 999) {
+                milliseconds++;
+            }
+
+            str++;
+        }
+    }
+
+    if (!any) {
+        return 0;
+    }
+
+    if (seconds > TIMEOUT_MAX_MS / 1000) {
+        return UINT64_MAX;
+    }
+
+    return seconds * 1000 + milliseconds;
+}
+
+static size_t format_timeout_ms(char *buf, uint64_t milliseconds) {
+    char *p = append_uint_dec(buf, milliseconds / 1000);
+    uint64_t subsecond = milliseconds % 1000;
+
+    if (subsecond != 0) {
+        char *last;
+
+        *p++ = '.';
+        *p++ = '0' + subsecond / 100;
+        *p++ = '0' + (subsecond / 10) % 10;
+        *p++ = '0' + subsecond % 10;
+
+        last = p - 1;
+        while (*last == '0') {
+            last--;
+        }
+        p = last + 1;
+    }
+
+    *p = '\0';
+    return p - buf;
+}
+
 static void format_fg_rgb_escape(char *buf, uint32_t rgb) {
     char *p = buf;
     *p++ = '\e'; *p++ = '['; *p++ = '3'; *p++ = '8'; *p++ = ';';
@@ -1113,22 +1197,6 @@ static char *append_string(char *p, const char *s) {
     return p;
 }
 
-static char *append_uint_dec(char *p, uint64_t val) {
-    char buf[20];
-    size_t i = 0;
-
-    do {
-        buf[i++] = '0' + (val % 10);
-        val /= 10;
-    } while (val != 0);
-
-    while (i != 0) {
-        *p++ = buf[--i];
-    }
-    *p = '\0';
-    return p;
-}
-
 static const char *uefi_shell_filename(void) {
 #if defined (__x86_64__)
     return "shellx64.efi";
@@ -1559,11 +1627,15 @@ noreturn void _menu(bool first_run) {
     }
 
     size_t timeout = 5;
+    uint64_t timeout_ms = timeout * 1000;
 
     bool has_timeout = false;
 
 #if defined (UEFI)
     has_timeout = bli_update_oneshot_timeout(&timeout, &skip_timeout);
+    if (has_timeout) {
+        timeout_ms = (uint64_t)timeout * 1000;
+    }
 #endif
 
     if (!has_timeout) {
@@ -1573,18 +1645,19 @@ noreturn void _menu(bool first_run) {
             if (!strcmp(timeout_config, "no"))
                 skip_timeout = true;
             else
-                timeout = strtoui(timeout_config, NULL, 10);
+                timeout_ms = parse_timeout_ms(timeout_config);
         }
     }
 
 #if defined (UEFI)
     if (!has_timeout) {
         has_timeout = bli_update_timeout(&timeout, &skip_timeout);
+        timeout_ms = (uint64_t)timeout * 1000;
     }
 #endif
 
-    if (timeout > 9999)
-        timeout = 9999;
+    if (timeout_ms > TIMEOUT_MAX_MS)
+        timeout_ms = TIMEOUT_MAX_MS;
 
 #if defined(UEFI)
     bool reboot_to_firmware_supported = reboot_to_fw_ui_supported();
@@ -1596,7 +1669,7 @@ noreturn void _menu(bool first_run) {
         skip_timeout = true;
     }
 
-    if (!skip_timeout && !timeout) {
+    if (!skip_timeout && !timeout_ms) {
         if (max_entries == 0 || selected_menu_entry == NULL || selected_menu_entry->sub != NULL) {
             quiet = false;
             print("Default entry is not valid or directory, booting to menu.\n");
@@ -1757,17 +1830,23 @@ refresh:
 
     if (skip_timeout == false) {
         print("\n\n");
-        for (size_t i = timeout; i; i--) {
-            size_t ndigits = 1;
-            for (size_t tmp = i / 10; tmp > 0; tmp /= 10) ndigits++;
-            size_t msg_len = 28 + ndigits;
+        while (timeout_ms != 0) {
+            char timeout_buf[24];
+            uint64_t sleep_ms = timeout_ms % 1000;
+            size_t timeout_len = format_timeout_ms(timeout_buf, timeout_ms);
+            size_t msg_len = 28 + timeout_len;
             set_cursor_pos_helper((terms[0]->cols - msg_len) / 2, terms[0]->rows - 2);
             FOR_TERM(TERM->scroll_enabled = false);
-            print("\e[2K%sBooting automatically in %s%U%s...\e[0m",
-                  interface_help_colour, interface_help_colour_bright, (uint64_t)i, interface_help_colour);
+            print("\e[2K%sBooting automatically in %s%s%s...\e[0m",
+                  interface_help_colour, interface_help_colour_bright, timeout_buf, interface_help_colour);
             FOR_TERM(TERM->scroll_enabled = true);
             FOR_TERM(TERM->double_buffer_flush(TERM));
-            if ((c = pit_sleep_and_quit_on_keypress(1))) {
+
+            if (sleep_ms == 0) {
+                sleep_ms = 1000;
+            }
+
+            if ((c = pit_sleep_ms_and_quit_on_keypress(sleep_ms))) {
                 skip_timeout = true;
                 if (quiet) {
                     quiet = false;
@@ -1778,6 +1857,7 @@ refresh:
                 FOR_TERM(TERM->double_buffer_flush(TERM));
                 goto timeout_aborted;
             }
+            timeout_ms -= sleep_ms;
         }
         goto autoboot;
     }
tab: 248 wrap: offon