loader: the F8 menu on the serial line

The three F8 screens painted into the framebuffer and nowhere else. That is
right on a machine with a screen and useless on one driven over a serial cable
-- which is how this loader is usually driven, and which f8_window() already
went out of its way to support: it accepts 'o' as well as F8 because, in its
own words, "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. The whole visible
behaviour was:

    Press F8 for boot options...
    <nothing, for ever>

Each screen echoes itself to COM1 now -- the boot menu, the resolution list and
System Information. The whole thing once on entry, then one line per *move*;
not on every keypress, because a serial terminal has no cursor to paint over
and an unrecognised key would scroll the selection past again for nothing.

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 across the
menu that was just painted there.

Two details found by actually driving it, neither 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.** A lone ESC byte on a serial line 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 as F8, and the same answer.

Driven end to end over serial: open the menu, walk it with W/S, open System
Information and come back, open Resolution and come back with Q, then boot.
The graphical menu is unchanged apart from the hint now naming Q.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-09-22 12:10:37 +02:00
commit 3f2c0bdf05
2 changed files with 135 additions and 2 deletions

View file

@ -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;