diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 13:01:13 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 17:13:54 -0300 |
| commit | 939e3a7288d6782139cac36f091ccd1d41cdfc0a (patch) | |
| tree | 6b7b86c2ba4ade9f34c8f174354425a5d31a150a /src/pardes.zig | |
| parent | e5f9e172330bc4500995ddd5954ed94f94e07af6 (diff) | |
| download | pardes-939e3a7288d6782139cac36f091ccd1d41cdfc0a.tar.gz pardes-939e3a7288d6782139cac36f091ccd1d41cdfc0a.zip | |
A fourth platform: pardes as ESP32-P4 firmware, bytes in and bytes out
`-Dplatform=p4 -Dtarget=riscv32-freestanding` emits a single freestanding OBJECT
exporting a seven-function C ABI, not an executable. The board's toolchain
(../05-zig-p4) owns `_start`, the linker script and the UART driver and links this
in. The seam is bytes rather than types, so neither side can accidentally depend
on the other's internals, and a signature that drifts fails at link time.
The serial line is the whole of the I/O. `src/p4.zig` drives vaxis unchanged over
it: the renderer is a byte writer and `queryTerminalSend` is a byte writer, so the
terminal emulator on the host answers the capability handshake and the firmware
sees a real terminal. Measured going out over the wire on attach: alt screen,
in-band resize, cursor report, kitty keyboard, kitty graphics, DA1.
THREE WORDS EXIST ONLY HERE. `src/board_memory.zig` implements `Peek`, `Poke` and
`Hexdump`, gated on `builtin.os.tag == .freestanding and !isWasm()` - derived from
the TARGET, because they are a property of running with no OS under you rather
than a product option, and because wasm is freestanding too and is exactly what
must be excluded: in a browser an address is an offset into the linear memory this
editor's own heap lives in. Every access goes through `*allowzero volatile`: a
peripheral register is not memory, and address 0 is an ordinary unmapped address
on this bus. One 4 KiB cap per command, set by the console rather than the memory -
an unbounded dump would wedge the only console the board has for eleven hours.
Measured on ESP32-P4 rev v1.3 silicon, driven from a host terminal:
Peek 0x501101a4 0x0e63ce71, then 0xaeaa6919 on a second read - the
RNG register, so the volatile loads are not folded
Poke 0x5011002c 0xdeadbeef LP_STORE0; a later Peek returned 0xdeadbeef
Hexdump 0x5011002c 32 16 bytes a row, hex columns and an ASCII gutter
Peek 0x50110001 `peek: MisalignedAddress` on the message row
That last line is the one that matters. A misaligned 32-bit access traps, and a
trap in firmware is a watchdog reset that takes the session with it, so the check
that turns it into a message is the reason the file is hand-written rather than a
generic reader.
BARE METAL BOOTS AN EMPTY OUTPUT BUFFER. Every other boot layout in `init` makes a
shell, and on this platform that is not a preference but an impossibility: nothing
to fork, no pty to give a terminal pane. Booting one anyway produced precisely what
that describes - a pane whose tag ends in `Filter`, no gutter, no buffer, and every
keystroke vanishing into the Fallback's silent pty. An output buffer is also what
the platform's own words want, since Peek, Poke and Hexdump each fill one.
Sized for the board rather than for a desktop:
* `allocators.zig` gains a p4 tier that is ALL fallback - every capacity is zero,
so each arena spills immediately to the 384 KiB heap the firmware hands over,
and no megabyte-shaped static reservation lands in `.bss`.
* `source_manifest.zig`'s allowlist is EMPTY on p4. The table is ~0.95 MiB of
rodata against a 1.5 MiB flash partition; the firmware's filesystem is the
serial host's, through the Host vtable.
* The grid is clamped and the clamp is measured, not guessed: every cell is paid
for four times (vaxis Screen + InternalScreen, pardes Surface + previous_cells),
so 40x12 fits and 80x24 exhausts the heap during `Pardes.init`.
* `Vaxis.resize` deinits both screens before allocating replacements, so a failed
resize leaves vaxis rendering nothing. The p4 shell keeps the previous geometry
on failure instead of leaving a half-applied one.
Also here: `output_pane_integration_test.zig` had an exhaustive switch over
`Platform` that adding `.p4` left unhandled, which broke `zig build unit-test`
outright - the native test binary is the one consumer no platform build compiles.
346 tests pass again.
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 193 |
1 files changed, 140 insertions, 53 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index c5eda6aa..91811cc9 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -22,7 +22,6 @@ const std = @import("std"); pub const animation = @import("animation.zig"); pub const panel_animation = @import("panel_animation.zig"); -const ghostty_vt = @import("ghostty-vt"); const uucode = @import("uucode"); const vaxis = @import("vaxis"); const mvzr = @import("mvzr"); @@ -63,7 +62,10 @@ pub const fallback_dump_path = host_mod.fallback_dump_path; /// unless -Dtracy names a Tracy checkout. pub const frameMark = tracy.frameMark; -pub const Platform = enum { tty, gui, web, macos }; +/// `p4` is ESP32-P4 firmware: a riscv32-freestanding core whose whole host is +/// a serial line. It joins `web` in having no filesystem, no ptys and no +/// config directory, which is what `hosted` below is for. +pub const Platform = enum { tty, gui, web, macos, p4 }; pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config").platform)); /// A build with no host but its display: the embedded source filesystem, the @@ -78,6 +80,29 @@ pub const isolated = @import("pardes_isolation").isolated; /// disabled so much as meaningless — see builtins.zig. pub const font_picker = platform == .gui or platform == .macos; +/// Platforms whose host is a real operating system: a filesystem to open, a +/// pty to fork, a config directory to watch. The browser and the P4 firmware +/// have none of the three, and every gate that used to read `platform != .web` +/// reads this instead so a third such platform cannot forget one of them. +pub const hosted = platform == .tty or platform == .gui or platform == .macos; + +/// Builds that HAVE terminal panes: a pane whose content is a live ghostty-vt +/// emulator being fed pty bytes. The P4 firmware has no processes, no ptys and +/// nothing that could produce a VT byte, so there the emulator is ~400 KiB of +/// flash and a PageList of RAM spent parsing input that cannot arrive — and it +/// drags a pile of freestanding root hooks in behind it (os.PATH_MAX, +/// os.heap.page_allocator, a cwd handle), none of which the core itself wants. +/// False means ghostty-vt is not in the module graph at all: build.zig never +/// even asks for the dependency. +/// +/// A PLATFORM gate and deliberately NOT one derived from the target: `web` is +/// freestanding too and KEEPS the emulator, because the browser shell renders +/// terminal panes back out of a replayed dump. Every gate in the core keys off +/// this one name, and src/term_pane.zig re-exports it as `enabled` and owns +/// the whole seam — the two Pane slots included — so ghostty-vt ends up +/// imported by exactly one file. +pub const terminal_panes = platform != .p4; + /// ...and the one fact about that face the core keeps: the name `Font` last /// resolved, which the Debug overlay prints. Behind the same comptime shim /// builtins.zig and macos.zig import this file with, so a tty or web binary @@ -104,7 +129,9 @@ pub const pdf_raster_policy: PdfRasterPolicy = switch (platform) { // Both pixel shells rasterize for a real display and can afford it; the // wire-bandwidth argument that shapes the Kitty policy does not apply. .gui, .macos => sdl_pdf_raster_policy, - .web => kitty_pdf_raster_policy, + // Neither hostless platform rasterizes a PDF at all (mupdf is compiled + // out), so the cheaper policy is the honest placeholder. + .web, .p4 => kitty_pdf_raster_policy, }; // The capacities and the two heights that are STRUCTURE, not taste: the @@ -2245,7 +2272,37 @@ fn mix(a: [3]u8, b: [3]u8) [3]u8 { const TAG_TAIL_CAP = dump.max_tag_tail; // one editable command line; extra input is refused const TTY_REPLAY_CAP = 1024 * 1024; // oldest bytes are evicted from the dump/replay record -const EFFECT_CAP = 4096; // one update may queue 256 KiB of ordered 64-byte writes + +/// How many effects the ring holds. SHRUNK, not moved to the heap, on the +/// board: `pump` drains this to empty on every iteration with an +/// unconditional `while (nextEffect())` — including effects `perform` itself +/// queues — so no capacity can deadlock the drain, and the only question a +/// capacity answers is how big a single-pump BURST may be before `emit` +/// refuses the overflow. The one producer that can burst is `emitWrite`, +/// which chunks arbitrary bytes into 64-byte `.write` effects for a pty, and +/// a build with `terminal_panes == false` has no pty to write to. Everything +/// else queues O(1) effects per event, and `in_q` holds at most 64 events per +/// pump, so 128 leaves two effects per queued event. +/// +/// A 1.0625 MiB inline ring cannot live in the board's 384 KiB heap at all; +/// 128 entries is 34 KiB. NOTE THE BEHAVIOUR CHANGE: `emit` has always +/// refused (not evicted) once full, so on p4 a burst larger than 128 effects +/// now drops its tail where 4096 would have held it — reachable only through +/// `emitWrite`, i.e. only if a pty ever appears on this platform. +const EFFECT_CAP = if (platform == .p4) 128 else 4096; + +/// Rows the per-pane soft-wrap map covers. `wrapWidth` refuses to wrap a pane +/// taller than this (it reads the array's own length), so shrinking it cannot +/// truncate a map — a taller pane renders unwrapped, exactly as documented on +/// `Pane.wrap_line`. A serial console is not 128 rows tall. +const WRAP_ROWS = if (platform == .p4) 128 else 256; + +/// A shell's reported working directory, owned inline by the pane. Zero-sized +/// where there are no processes to report one: the `PdfSlot` rule, applied to +/// a capacity whose sole producer (`Pardes.setCwd`, fed by a pty's prompt +/// report) does not exist without terminal panes. `setOwnedCwd` clamps, so a +/// zero cap reads as "no directory known" — which is the truth here. +const CWD_BUF_CAP = if (terminal_panes) 1024 else 0; /// Everything a theme repaints. `bg`/`fg` null = leave the host terminal's own /// default cell showing (the native-dark shape); `palette` null = let a child's @@ -3635,6 +3692,19 @@ pub const SelRange = struct { const PdfSlot = if (pdf_enabled) ?pdf_pane.State else void; +/// The emulator's raw-byte dump/replay ring, and NOTHING where there is no pty +/// to read bytes from — same shape as `PdfSlot`, for a much harder reason. It +/// is a MEGABYTE inline in every Pane: on the P4 the whole heap is 384 KiB, so +/// carrying it would make `gpa.create(Pane)` fail before anything could ask +/// for a grid, and `Pardes.init` — which creates a pane unconditionally — +/// could not return. +const ReplaySlot = if (terminal_panes) [TTY_REPLAY_CAP]u8 else void; + +/// The staging buffer for the query replies ghostty computes. Its only writer +/// is term_pane's `ptyReport` callback, which does not exist without an +/// emulator, so `reply_len` there is permanently 0 and `sync` never reads it. +const ReplySlot = if (terminal_panes) [256]u8 else void; + fn hasPdf(pane: *const Pane) bool { return if (comptime pdf_enabled) pane.pdf != null else false; } @@ -3658,8 +3728,8 @@ pub const Pane = struct { /// A pane's working directory. `.inherited` is a live `*Pane` link kept /// valid by deferred teardown (see PaneAllocator) + reapPanes' fixup. pub const Cwd = union(enum) { none, inherited: *Pane, owned: []const u8 }; - vt: ghostty_vt.Terminal, - stream: ghostty_vt.TerminalStream, + vt: term_pane.VtSlot, + stream: term_pane.StreamSlot, /// The same allocator Pardes holds. A pane already owns heap (its content, /// its emulator, its undo stacks) and Pardes frees all of it; this is here /// so the pane methods that need the file's LINE INDEX — scrollBy and @@ -3767,7 +3837,7 @@ pub const Pane = struct { /// link to the pane it was opened from (.inherited), or unknown (.none). /// The inherited pointer is kept valid by deferred pane teardown + fixup. cwd: Cwd = .none, - cwd_buf: [1024]u8 = undefined, + cwd_buf: [CWD_BUF_CAP]u8 = undefined, /// modal cursor, at ABSOLUTE body rows of the pane's SURFACE (file lines, /// or the terminal's shell rows with its edit buffer standing in). Tracks /// the shell cursor until pinned by a click or a key. @@ -3795,20 +3865,22 @@ pub const Pane = struct { /// renderPane call. Nothing else may write it; a second writer is a second /// truth, and the first click on a stale row is how you find out. /// - /// ponytail: a fixed 256 rows. A pane taller than that does not wrap at - /// all — bodyText leaves wrap_n at 0 and clips the way it always did — - /// rather than half-recording a mapping every site here would then have to - /// distrust. Grow the arrays the day a 256-row window turns up. - wrap_line: [256]i32 = undefined, - wrap_col: [256]i32 = undefined, + /// ponytail: a fixed `WRAP_ROWS` rows (256; 128 on the board). A pane + /// taller than that does not wrap at all — bodyText leaves wrap_n at 0 and + /// clips the way it always did — rather than half-recording a mapping + /// every site here would then have to distrust. `wrapWidth` derives that + /// refusal from `wrap_line.len` itself, so the bound follows the array. + /// Grow the arrays the day a taller window turns up. + wrap_line: [WRAP_ROWS]i32 = undefined, + wrap_col: [WRAP_ROWS]i32 = undefined, wrap_n: u16 = 0, sel: [3]Sel = @splat(.{}), /// terminals only: the typed-text buffer standing in for shell rows ovl: ?term_pane.EditBuffer = null, /// every raw pty byte, in order — a bounded dump/replay ring. Once full, /// new output evicts the oldest bytes while the live terminal still sees - /// every byte. - tty_stream: [TTY_REPLAY_CAP]u8 = undefined, + /// every byte. A megabyte, inline: see `ReplaySlot`. + tty_stream: ReplaySlot = if (terminal_panes) undefined else {}, tty_stream_head: usize = 0, tty_stream_len: usize = 0, /// Terminal-only, pane-local presentation mode. Ghostty remains the owner @@ -3818,7 +3890,7 @@ pub const Pane = struct { /// query replies ghostty computed (DSR, DA, kitty); the stream handler has /// no path to the effect queue, so they land here and sync() drains them /// into write effects. Bounded: replies are tiny escape sequences. - reply: [256]u8 = undefined, + reply: ReplySlot = if (terminal_panes) undefined else {}, reply_len: u16 = 0, /// THE TRANSIENT MESSAGE: what just happened to this pane, drawn on its /// LAST row until the next key or mouse event wipes it (see update). Fixed @@ -3948,7 +4020,7 @@ pub const Pane = struct { pub fn scroll(pane: *Pane) i32 { if (pane.file) |f| return @intCast(f.scroll); if (comptime pdf_enabled) if (pane.pdf) |pv| return @intCast(pv.text_scroll); - return pane.surfRow(@intCast(pane.vt.screens.active.pages.scrollbar().offset)); + return pane.surfRow(term_pane.gridOffset(pane)); } /// The document position a BODY ROW begins at — `vr` 0 is the first row @@ -4012,8 +4084,8 @@ pub const Pane = struct { } } else { // the vt scrolls in SHELL rows; convert through the edit buffer - const off: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset); - pane.vt.screens.active.scroll(.{ .delta_row = pane.gridRow(pane.surfRow(off) + delta) - off }); + const off = term_pane.gridOffset(pane); + term_pane.scrollGrid(pane, pane.gridRow(pane.surfRow(off) + delta) - off); } } @@ -4088,9 +4160,9 @@ pub const Pane = struct { pane.cur_row = pane.scroll(); pane.cur_col = 0; } else { - const goff: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset); - pane.cur_row = pane.surfRow(@as(i32, @intCast(pane.vt.screens.active.cursor.y)) + goff); - pane.cur_col = @intCast(pane.vt.screens.active.cursor.x); + const cur = term_pane.gridCursor(pane); + pane.cur_row = pane.surfRow(@as(i32, cur.y) + term_pane.gridOffset(pane)); + pane.cur_col = @intCast(cur.x); } pane.cur_pinned = true; } @@ -5442,7 +5514,11 @@ pub const Pardes = struct { /// request stale as soon as another ThemeFile/Theme/NextColor command wins. custom_theme: ?Theme = null, custom_theme_active: bool = false, - theme_file_path: runtime_cfg.Text(4095) = .{}, + /// Sized by `runtime_cfg.host_path_cap`, which is 0 where the platform has + /// no filesystem to hold a theme file: `set` then refuses every non-empty + /// path and `themeFileRequest` answers `PathTooLong`, which is the honest + /// answer on a board whose only IO is a UART. + theme_file_path: runtime_cfg.Text(runtime_cfg.host_path_cap) = .{}, theme_file_generation: u32 = 0, theme_file_pane: u8 = 0, chrome_animation: ChromeAnimation = ChromeAnimation.init(initial_chrome), @@ -5630,6 +5706,27 @@ pub const Pardes = struct { p.ncol = 1; p.col_n[0] = 1; p.col_terms[0][0] = 0; + } else if (comptime platform == .p4) { + // BARE METAL BOOTS AN EMPTY OUTPUT BUFFER, and a shell is not a layout preference + // here but an impossibility: there is no operating system under this, so there is + // nothing to fork and no pty to give a terminal pane. Booting one anyway produced + // exactly what that describes - a pane whose tag ends in `Filter`, whose pty is the + // Fallback's silent one, with no gutter, no buffer, and no key that reaches anything. + // Measured on an ESP32-P4 over the serial line: every keystroke vanished. + // + // An output buffer is the right default rather than a file pane, and not only because + // `opts.file` cannot work here (the P4's embedded allowlist is empty by design - see + // source_manifest.zig - so `look.readFile` has nothing to resolve a path against). It + // is what the platform's own words WANT: `Peek`, `Poke` and `Hexdump` each fill an + // output buffer, so booting into one means the first dump lands in the same kind of + // pane the boot pane already is. It is editable text with no file behind it, which is + // the honest description of a buffer on a board with no filesystem. + const content = try p.gpa.dupe(u8, ""); + errdefer p.gpa.free(content); + _ = try output_pane.open(p, 0, "", .{ .cmd = .New }, "", content); + p.ncol = 1; + p.col_n[0] = 1; + p.col_terms[0][0] = 0; } else if (opts.tty_only) { _ = try p.newShell(0, ""); p.panes[0].?.mode = .tty; @@ -5760,8 +5857,7 @@ pub const Pardes = struct { if (pane.ovl) |o| p.gpa.free(o.text); for (pane.ed_undo[0..pane.ed_undo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text); for (pane.ed_redo[0..pane.ed_redo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text); - pane.stream.deinit(); - pane.vt.deinit(p.gpa); + term_pane.deinitEmulator(pane, p.gpa); p.gpa.destroy(pane); } @@ -5814,20 +5910,11 @@ pub const Pardes = struct { return pane; } - /// a doc pane (file/image): no pty, no spawn; a stub 1x1 emulator only - /// because the shared machinery touches its allocator-owned bits. + /// a doc pane (file/image/PDF): no pty and no spawn. Whatever emulator half + /// it still needs is term_pane's business — see `createDoc` there. pub fn newDocPane(p: *Pardes, id: usize) !*Pane { std.debug.assert(p.panes[id] == null); - const pane = try p.gpa.create(Pane); - errdefer p.gpa.destroy(pane); - pane.* = .{ - .vt = try ghostty_vt.Terminal.init(term_pane.terminalIo(), p.gpa, .{ .cols = 1, .rows = 1 }), - .stream = undefined, - .gpa = p.gpa, - .cols = p.screen_w, - .rows = p.screen_h, - }; - pane.stream = pane.vt.vtStream(); + const pane = try term_pane.createDoc(p.gpa, p.screen_w, p.screen_h); p.installPane(id, pane); return pane; } @@ -6052,7 +6139,7 @@ pub const Pardes = struct { /// core. Relative paths belong to the per-user config directory; absolute /// paths remain useful for trying a file elsewhere. pub fn requestThemeFile(p: *Pardes, id: usize, argument: []const u8) void { - if (comptime platform == .web) return; + if (comptime !hosted) return; const input = std.mem.trim(u8, argument, " \t\r\n"); if (input.len == 0 or !std.mem.endsWith(u8, input, ".zon")) { p.reportError(id, "theme file", error.InvalidThemePath); @@ -6329,7 +6416,7 @@ pub const Pardes = struct { if (pane.serial != st.serial) return; // a recycled slot: not ours if (pane.file) |f| return p.hostWriteFile(st.pane, st.path.slice(), f.content); if (!pane.isTerminal()) return; - const text = pane.vt.screens.active.dumpStringAlloc(p.gpa, .{ .screen = .{} }) catch return; + const text = term_pane.screenTextAlloc(pane, p.gpa) catch return; defer p.gpa.free(text); p.hostWriteFile(st.pane, st.path.slice(), text); }, @@ -6655,7 +6742,7 @@ pub const Pardes = struct { /// multi-line paste would run every line but the last. pub fn typeToTty(p: *Pardes, id: usize, pane: *const Pane, text: []const u8) void { if (text.len == 0) return; - if (pane.vt.modes.get(.bracketed_paste)) { + if (term_pane.bracketedPaste(pane)) { p.emitWrite(id, "\x1b[200~"); p.emitWrite(id, text); p.emitWrite(id, "\x1b[201~"); @@ -12660,15 +12747,14 @@ pub const Pardes = struct { if (cut) return; const y = p.yank orelse return; if (y.len == 0) return; - const m = &pane.vt.modes; - if (m.get(.mouse_event_normal) or m.get(.mouse_event_button) or m.get(.mouse_event_any)) { + if (term_pane.reportsMouse(pane)) { // dragUpdate keeps sel[sel_slot] tracking the held select button, so // the click lands where the mouse is at this tap; 1-based, // body-relative (the tag row is ours, not the app's) const col: u16 = @intCast(std.math.clamp(pane.sel[sel_slot].c1 + 1, 1, 9999)); const row: u16 = @intCast(std.math.clamp(pane.sel[sel_slot].r1 - @as(i32, BOX_H) + 1, 1, 9999)); var mb: [32]u8 = undefined; - if (m.get(.mouse_format_sgr)) { + if (term_pane.mouseFormatSgr(pane)) { p.emitWrite(s.id, std.fmt.bufPrint(&mb, "\x1b[<0;{d};{d}M\x1b[<0;{d};{d}m", .{ col, row, col, row }) catch return); } else { // ponytail: legacy X10 bytes; add utf8/urxvt formats if an app ever wants them @@ -12926,7 +13012,7 @@ pub const Pardes = struct { const src = p.panes[src_id] orelse return; const src_h = p.rects[src_id].h; const body: u16 = if (src_h > BOX_H) src_h - BOX_H else 1; - const cur: u16 = if (!src.isTerminal()) body / 2 else @as(u16, @intCast(src.vt.screens.active.cursor.y)) + 1; + const cur: u16 = if (!src.isTerminal()) body / 2 else term_pane.gridCursor(src).y + 1; // cap keep so a content-full source still leaves the new pane a tag + // a few body rows (an Alt-n from a full shell was born 0 rows tall) const keep = std.math.clamp(cur, 1, @max(1, body -| (BOX_H + 3))); @@ -12968,8 +13054,8 @@ pub const Pardes = struct { const tt = p.panes[tty_id] orelse return; // No typing, cursor on the first prompt line, no scrollback: this is // the throwaway boot placeholder a document may replace. - if (tt.ovl != null or tt.vt.screens.active.cursor.y != 0 or - tt.vt.screens.active.pages.scrollbar().total > tt.rows) return; + if (tt.ovl != null or term_pane.gridCursor(tt).y != 0 or + term_pane.scrollbar(tt).total > tt.rows) return; p.computeGeom(); // a just-stacked doc has no rect yet; absorb snaps to rows p.absorbVWeight(tty_id); p.layoutRemove(tty_id); @@ -14013,7 +14099,7 @@ pub const Pardes = struct { p.trackJump(); for (&p.panes, 0..) |*slot, id| { const pane = slot.* orelse continue; - if (pane.reply_len > 0) { + if (comptime terminal_panes) if (pane.reply_len > 0) { var off: u16 = 0; while (off < pane.reply_len) { const n = @min(pane.reply_len - off, 64); @@ -14021,7 +14107,7 @@ pub const Pardes = struct { off += n; } pane.reply_len = 0; - } + }; const r = p.rects[id]; const cols = @max(1, r.w -| config.GUTTER); const rows = @max(1, r.h -| BOX_H); // the tag steals the top row @@ -14032,7 +14118,7 @@ pub const Pardes = struct { // doc panes have no pty/emulator grid to reflow; just record // the size so bodyText renders the right number of rows if (pane.isTerminal()) { - pane.vt.resize(p.gpa, .{ .cols = cols, .rows = rows }) catch {}; + term_pane.resizeGrid(pane, p.gpa, cols, rows); p.shell_rows.markStale(pane); // reflow moved every row p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } }); } @@ -14659,7 +14745,7 @@ pub const Pardes = struct { sb_off = page; sb_total = if (comptime pdf_enabled) at.pdf.?.page_count else 0; } else { - const sb = at.vt.screens.active.pages.scrollbar(); + const sb = term_pane.scrollbar(at); sb_off = sb.offset; sb_total = sb.total; } @@ -15294,9 +15380,9 @@ pub const Pardes = struct { // cursor: tracks the shell cursor until pinned by a click or a key // (the tag cursor above wins while the tag is focused) if (active and !pane.tag_edit) { - const cur = pane.vt.screens.active.cursor; + const cur = term_pane.gridCursor(pane); if (pane.mode != .tty) { - const goff: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset); + const goff = term_pane.gridOffset(pane); const crow = if (pane.cur_pinned) pane.cur_row else pane.surfRow(@as(i32, @intCast(cur.y)) + goff); const ccol = if (pane.cur_pinned) pane.cur_col else @as(i32, @intCast(cur.x)); // which ROW of a wrapped line the cursor is on, and which byte @@ -15354,7 +15440,7 @@ pub const Pardes = struct { .offset = page, .len = 1, } else blk: { - const gsb = pane.vt.screens.active.pages.scrollbar(); + const gsb = term_pane.scrollbar(pane); break :blk .{ .total = gsb.total, .offset = gsb.offset, .len = gsb.len }; }; const track_h: usize = r.h - BOX_H; @@ -15830,3 +15916,4 @@ test "entering tty walks the shell cursor to the column clicked past the prompt" try std.testing.expectEqual(@as(usize, 0), rights); try std.testing.expectEqual(@as(usize, 7), lefts); } + |
