Flash 119 lines
// pager — flibc's pager core, ported to Flash from its hand-written Zig. The
// scroll/line-index state machine a full-screen pager (/bin/less) drives: it
// indexes the line starts of a slurped byte buffer once, then answers the two
// questions a render loop asks every frame — "which lines are on screen?" and
// "where does the cursor scroll to?" — with every motion clamped so `top`
// never runs past the last page or before the first line.
//
// The third navigation seam, beside keys (input) and the screen writer
// (output). Pure by construction exactly like keys.Decoder and completion: no
// allocator (the line index is a caller-owned []mut u32), no module state, no SVC,
// no dependency on the kernel or the rest of flibc. The driver half — opening
// the file, screen enter/leave, readKey — lives in the tool binary; this file
// holds only the logic worth host-testing in isolation.
//
// The third pure port (after keys and completion): it adds no new grammar. It
// is the first struct to mix value receivers (`self Pager`, for the read-only
// queries) with pointer receivers (`self *mut Pager`, for the scroll
// mutators), and the first with void-returning methods, but each of those
// forms already landed in an earlier leaf. The rest is reused surface — value
// `if`-expressions, `#intCast`, the range-`for` loop, the
// slice expressions, char literals and `&&`. Its core lowers to Zig whose
// token stream matches the reference.
/// A pager view over an immutable text buffer. `init` indexes line starts into
/// the caller's `slots`; the scroll ops move `top` (the first visible line)
/// with clamping; `line(i)` returns the i-th logical line. `rows` is the
/// visible content-row count the consumer paints (its page height).
pub const Pager = struct {
text []u8,
lines []mut u32, // caller-owned; lines[0..n] are line-start byte offsets
n usize, // number of indexed lines
top usize, // index of the first visible line
rows usize, // visible content rows (page height)
/// Index the line starts of `text` into `slots` and return a Pager homed at
/// the top. Line 0 begins at offset 0; every byte after a '\n' begins the
/// next line, except a single trailing '\n' (the common case — a file that
/// ends in a newline is N lines, not N + 1). Internal blank lines are kept.
/// Indexing stops at `slots.len` lines: a pathological all-newline buffer is
/// capped rather than overrunning the caller's array (the driver caps the
/// slurp by bytes, so this bound is not normally the binding one).
pub fn init(text []u8, slots []mut u32, rows usize) Pager {
var n usize = 0
if text.len > 0 && slots.len > 0 {
slots[0] = 0
n = 1
for i in 0..text.len {
if text[i] == '\n' && i + 1 < text.len {
if n >= slots.len {
break
}
slots[n] = #intCast(i + 1)
n += 1
}
}
}
return .{ .text = text, .lines = slots, .n = n, .top = 0, .rows = rows }
}
/// The i-th logical line: from its start offset up to (not including) its
/// own '\n', with a preceding '\r' (CRLF) stripped. Computing the end by
/// scanning to the next '\n' — rather than the next index slot — keeps a
/// capped index honest: the last indexed line ends at its newline, it does
/// not swallow the un-indexed remainder. Out-of-range `i` yields an empty
/// slice so a render loop can ask past the last line without a bounds check.
pub fn line(self Pager, i usize) []u8 {
if i >= self.n {
return ""
}
start := self.lines[i]
var end usize = start
while end < self.text.len && self.text[end] != '\n' {
end += 1
}
var s = self.text[start..end]
if s.len > 0 && s[s.len - 1] == '\r' {
s = s[0 .. s.len - 1]
}
return s
}
/// The largest `top` that still fills the page — so the final screen sits at
/// the bottom with no blank overscroll. Zero when the text fits one page.
pub fn maxTop(self Pager) usize {
return if (self.n > self.rows) self.n - self.rows else 0
}
/// Scroll down `k` lines, clamped to maxTop().
pub fn down(self *mut Pager, k usize) {
mt := self.maxTop()
self.top = if (self.top + k > mt) mt else self.top + k
}
/// Scroll up `k` lines, clamped to the first line.
pub fn up(self *mut Pager, k usize) {
self.top = if (self.top > k) self.top - k else 0
}
/// Forward one page (a full window of `rows`), clamped.
pub fn pageDown(self *mut Pager) {
self.down(self.rows)
}
/// Back one page, clamped.
pub fn pageUp(self *mut Pager) {
self.up(self.rows)
}
/// Jump to the first line.
pub fn toTop(self *mut Pager) {
self.top = 0
}
/// Jump so the last line sits on the final row.
pub fn toBottom(self *mut Pager) {
self.top = self.maxTop()
}
}