diff --git a/boot/uefi/main.c b/boot/uefi/main.c index 644d634..e6daa22 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -112,6 +112,72 @@ serial_putc(char c) __asm__ volatile("outb %0, %1" : : "a"((UINT8)c), "d"((UINT16)0x3F8)); } +/* The menus, on the serial line. + * + * The three screens below paint into the framebuffer, which is the right place + * for them on a machine with a screen and nowhere at all on one driven over a + * serial cable. That is how this loader is usually driven, and f8_window() + * already went out of its way to support it -- it accepts 'o' as well as F8 + * precisely because "a serial console sends no scan codes at all". So the + * prompt invited a serial user in, drew them a menu they could not see, and + * then blocked for ever on a key they had no way to choose. + * + * Straight to COM1 rather than through print(): print() goes to ConOut until + * the splash is up, and ConOut on a machine with graphics draws its text over + * the menu that was just painted there. + * + * A machine with no COM1 reads 0xFF from the status register, serial_putc() + * gives up after its bounded wait, and the write goes nowhere. So this costs + * nothing where it is useless and is the whole interface where it is not. */ +static void +serial_puts(const char *s) +{ + for (; *s; s++) { + if (*s == '\n') + serial_putc('\r'); + serial_putc(*s); + } +} + +/* One item, as the selected line or not. The marker is the same width either + * way, so the list does not shift sideways as the selection moves. + * + * `numbered` is not decoration: the number is shown exactly where it is a key + * you can press. The boot menu takes 1-5; the resolution list takes only W/S + * and Enter, and is longer than nine entries -- numbering it printed ':', ';', + * '<' past the ninth, which is a character nobody can type offering to select + * something. */ +static void +serial_item(const char *const *items, int i, int sel, int numbered) +{ + serial_puts(i == sel ? " > " : " "); + if (numbered && i < 9) { + char n[4]; + n[0] = (char)('1' + i); + n[1] = '.'; + n[2] = ' '; + n[3] = 0; + serial_puts(n); + } else if (numbered) { + serial_puts(" "); /* keep the column */ + } + serial_puts(items[i]); + serial_puts("\n"); +} + +static void +serial_menu(const char *title, const char *const *items, int n, int sel, + const char *hint, int numbered) +{ + serial_puts("\n-- "); + serial_puts(title); + serial_puts(" --\n"); + for (int i = 0; i < n; i++) + serial_item(items, i, sel, numbered); + serial_puts(hint); + serial_puts("\n"); +} + /* Diagnostics: only on a verbose boot. * * These four lines say where the framebuffer is, where the kernel went and @@ -561,6 +627,7 @@ static void res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) { int sel = 0; + int echoed = -1; for (;;) { struct ui_fb uifb; @@ -578,7 +645,15 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) if (sel > RES_N) sel = RES_N; ui_menu("Resolution", items, RES_N + 1, sel, - "W/S to choose, Enter to apply, Esc to go back"); + "W/S to choose, Enter to apply, Esc or Q to go back"); + + if (echoed < 0) { + serial_menu("Resolution", items, RES_N + 1, sel, + "W/S to choose, Enter to apply, Esc or Q to go back", 0); + } else if (echoed != sel) { + serial_item(items, sel, sel, 0); + } + echoed = sel; EFI_INPUT_KEY key; for (;;) { @@ -589,7 +664,12 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) } CHAR16 c = key.UnicodeChar; - if (key.ScanCode == 0x17 || c == 27) /* Esc */ + /* Q as well as Esc, for the same reason f8_window() accepts 'o' as + * well as F8: on a serial line a lone ESC byte is indistinguishable + * from the start of an escape sequence, so the terminal driver holds + * it back waiting for the rest and it never arrives as a key. Esc is + * the obvious thing to press on a screen and unreachable on a cable. */ + if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q') return; if (key.ScanCode == 0x01 || c == L'w' || c == L'W') { sel = (sel + RES_N) % (RES_N + 1); continue; @@ -816,6 +896,16 @@ sys_info(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) ui_panel("System Information", labels, values, 7, "Any key to go back"); + serial_puts("\n-- System Information --\n"); + for (int i = 0; i < 7; i++) { + serial_puts(" "); + serial_puts(labels[i]); + serial_puts(" "); + serial_puts(values[i]); + serial_puts("\n"); + } + serial_puts("Any key to go back\n"); + EFI_INPUT_KEY key; for (;;) { if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) @@ -828,6 +918,7 @@ static int boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) { int selected = 0; + int echoed = -1; // the serial copy: header once, then moves char resbuf[48]; for (;;) { @@ -853,6 +944,18 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) ui_menu("Boot Manager", items, 5, selected, "W/S or 1-5 to choose, Enter to select"); + // The same menu on the serial line: the whole thing once, then one + // line per *move*. Reprinting on every keypress would be correct and + // unreadable -- a serial terminal has no cursor to paint over, so an + // unrecognised key would scroll the selection past again for nothing. + if (echoed < 0) { + serial_menu("Boot Manager", items, 5, selected, + "W/S or 1-5 to choose, Enter to select", 1); + } else if (echoed != selected) { + serial_item(items, selected, selected, 1); + } + echoed = selected; + EFI_INPUT_KEY key; CHAR16 c = 0; UINT16 scan = 0; diff --git a/docs/architecture/boot.md b/docs/architecture/boot.md index 8d493c6..a958c7a 100644 --- a/docs/architecture/boot.md +++ b/docs/architecture/boot.md @@ -79,6 +79,36 @@ Dropping back to protected mode to re-enter through the 32-bit multiboot path is a mode switch whose failure mode is a triple fault with no console. The 32-bit entry is still present in `entry.S` and now has no caller. +### The menu is on the serial line too + +The three F8 screens paint into the framebuffer, which is the right place for +them on a machine with a screen and nowhere at all on one driven over a serial +cable — which is how this loader is usually driven. `f8_window()` already went +out of its way to support that, accepting `o` as well as F8 because *"a serial +console sends no scan codes at all"*. So the prompt invited a serial user in, +drew them a menu they could not see, and blocked for ever on a key they had no +way to choose. + +Each screen now echoes itself to COM1: the whole menu once, then one line per +*move*. Not on every keypress — a serial terminal has no cursor to paint over, +so an unrecognised key would scroll the selection past again for nothing. + +Straight to COM1 rather than through `print()`, because `print()` goes to +`ConOut` until the splash is up, and `ConOut` on a machine with graphics draws +its text across the menu that was just painted there. + +Two details that are not cosmetic: + +- **The item number is shown only where it is a key you can press.** The boot + menu takes 1–5. The resolution list takes only W/S and Enter and is longer + than nine entries, so numbering it printed `:`, `;`, `<` past the ninth — a + character nobody can type, offering to select something. +- **Q goes back as well as Esc.** On a serial line a lone `ESC` byte is + indistinguishable from the start of an escape sequence, so the terminal + driver holds it back waiting for the rest and it never arrives as a key. Esc + is the obvious thing to press on a screen and unreachable on a cable — the + same shape of problem, and the same answer, as `o` for F8. + ## 3. `kmain()` `kernel/core/main.c`. The ordering here is not arbitrary and several steps are