// less — the full-screen text pager for /bin/less. // // The first consumer of the navigation scaffold's full-screen half: where // sysinfo proved console_ui.screen's kv() renderer print-and-exit, less proves // the interactive loop — it takes over the console with screen.enter(), reads // keys through flibc.readKey()'s VT100 decoder, scrolls with the pure // flibc.Pager core, and restores the shell view with screen.leave() on every // exit path. Output is screen.panelTop for the title bar plus raw content rows; // input is the arrow / page / quit keys a pager needs. // // Scope is a proof, like sysinfo: it pages a single named file, slurps up to // BUF_MAX bytes onto its own stack (rule 1 — no heap, no .bss), indexes the // first MAX_LINES lines, and assumes a 24x80 serial terminal (no window-size // ioctl exists yet). A file larger than the slurp shows a "(more)" marker. // Reading a pipe is out of scope: fd 0 is the key source, so `cmd | less` would // have nowhere to read keys from (a /dev/tty concern for later). // // Same coreutil recipe as ls / sysinfo: flibc _start shim, flibc_mem, single // R+X PT_LOAD, stack buffers only. Kept out of the CI FSH_SCRIPT — it is // interactive and the free-page baseline must stay deterministic. // // Alignment note: under SCTLR_EL1.A strict-align, a generic build target would // vectorize byte copies and materialize >16-byte by-value struct returns with a // 16-byte `str q`, which faults on an only-8-aligned slot. So the Pager (a // >16-byte value) is returned into an `align(16)` slot, and the one string // concat (the title) writes through a volatile pointer so it is never widened. // Everything else emits source slices straight to the sink — no copies. use flibc use console_ui link "flibc_start" link "flibc_mem" // Assumed serial-terminal geometry. One header row (panelTop), one status row, // the rest content. No window-size query exists, so these are fixed. const ROWS usize = 24 const STATUS usize = 1 const HEADER usize = 1 const PAGE usize = ROWS - HEADER - STATUS // visible content rows const COLS usize = 80 // clip width — keep each rendered row to one line const BUF_MAX usize = 16384 // file slurp cap (on this frame) const MAX_LINES usize = 2048 // line-index slots const TITLE_MAX usize = COLS // "less: " scratch fn sink(bytes []u8) void { _ = flibc.sys.write_fd(1, bytes.ptr, bytes.len) } export fn main(argc usize, argv argv) noreturn { if argc < 2 || argv[1] == null { sink("usage: less \n") flibc.exit() } path := argv[1].? fd := flibc.sys.open(path) if fd < 0 { sink("less: cannot open file\n") flibc.exit() } // Slurp up to BUF_MAX bytes; `truncated` if the file filled the buffer (it // may hold more — best-effort, this is a proof pager). var buf [BUF_MAX]u8 = undefined var n usize = 0 while n < buf.len { r := flibc.sys.read(fd, buf[n..].ptr, buf.len - n) if r <= 0 { break } n += #intCast(r) } _ = flibc.sys.close(fd) truncated := (n == buf.len) // Title bar text, built once (the only string copy — volatile dest so the // strict-align target cannot vectorize it into a faulting `str q`). var title_buf [TITLE_MAX]u8 = undefined title := buildTitle(&title_buf, baseName(path)) // Pager value is >16 bytes; land the by-value return on a 16-aligned slot so // its sret store is not a misaligned `str q`. var slots [MAX_LINES]u32 = undefined var pg flibc.Pager align(16) = flibc.Pager.init(buf[0..n], &slots, PAGE) // Take over the console: echo off (mode 0) so typed keys do not leak onto // the alt-screen, then the alternate buffer + hidden cursor. _ = flibc.sys.set_console_mode(0) console_ui.screen.enter(sink) render(&pg, title, truncated) while true { ev := flibc.readKey() var quit bool = false switch ev.key { .eof, .escape, .ctrl_c, .ctrl_d => { quit = true }, .up => pg.up(1), .down, .enter => pg.down(1), .char => switch ev.ch { 'q' => { quit = true }, 'j' => pg.down(1), 'k' => pg.up(1), ' ', 'f' => pg.pageDown(), 'b' => pg.pageUp(), 'g' => pg.toTop(), 'G' => pg.toBottom(), else => {}, }, else => {}, // left/right/tab/backspace/none — ignored } if quit { break } render(&pg, title, truncated) } // Every exit path restores the shell view. The console is left in mode 0 // (echo off) — the shell's own baseline, where readline does its own echo — // so there is deliberately no mode restore here (mode 1 would double-echo // the next prompt); fsh also re-asserts mode 0 after wait() as a backstop. console_ui.screen.leave(sink) flibc.exit() } // Repaint the whole screen: title bar, PAGE content rows (clipped to COLS, '~' // past EOF), then the status row. Full clear + repaint each frame keeps the // renderer trivial — fine for a serial console. fn render(pg *flibc.Pager, title []u8, truncated bool) void { console_ui.screen.clear(sink) // home + erase console_ui.screen.panelTop(sink, .{ .title = title, .width = COLS }) var row usize = 0 while row < pg.rows { idx := pg.top + row if idx < pg.n { l := pg.line(idx) sink(if (l.len <= COLS) l else l[0..COLS]) } else { sink("~") } sink("\n") row += 1 } statusLine(pg, truncated) } // Position + key legend on the final row. No trailing newline so the alt-screen // does not scroll. The filename already rides the title bar. fn statusLine(pg *flibc.Pager, truncated bool) void { shown := if (pg.n > pg.top) #min(pg.rows, pg.n - pg.top) else 0 const first u64 = if (pg.n == 0) 0 else pg.top + 1 const last u64 = pg.top + shown sink(" ") emitDec(first) sink("-") emitDec(last) sink("/") emitDec(pg.n) if truncated { sink(" (more)") } sink(" q=quit space=page b=back g/G=ends") } // Last path component, as a slice into the argv string (no copy). "/a/b" -> "b", // "x" -> "x". fn baseName(path cstr) []u8 { var len usize = 0 while path[len] != 0 { len += 1 } var start usize = 0 var i usize = 0 while i < len { if path[i] == '/' { start = i + 1 } i += 1 } return path[start..len] } // "less: " into `buf`, clipped to its length. The destination is volatile // so the strict-align target never widens the byte stores into a `str q`. fn buildTitle(buf []mut u8, name []u8) []u8 { const dst [*]mut volatile u8 = buf.ptr var i usize = 0 prefix := "less: " for c in prefix { dst[i] = c i += 1 } for c in name { if i >= buf.len { break } dst[i] = c i += 1 } return buf[0..i] } // Emit `v` as decimal ASCII (mirrors sysinfo's u64dec — a proven small reversal // that the strict-align target does not vectorize). fn emitDec(v u64) void { var buf [20]u8 = undefined sink(buf[0..u64dec(&buf, v)]) } fn u64dec(out []mut u8, v u64) usize { if v == 0 { out[0] = '0' return 1 } var tmp [20]u8 = undefined var n usize = 0 var x u64 = v while x != 0 { tmp[n] = '0' + #as(u8, #intCast(x % 10)) n += 1 x /= 10 } var i usize = 0 while i < n { out[i] = tmp[n - 1 - i] i += 1 } return n }