From d65b329cb63e2c93284bcec7f047b053c8957726 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 21:01:00 +0000 Subject: [PATCH] feat(tui): generate TuiCore via fluessig single_threaded handle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the hand-written `TuiCore` napi class (src/tui.rs, the native half of pi's differential renderer, packages/tui/src/tui.ts) for a fluessig-generated one — the largest UI core yet (ctor + 12 sync ops), extending the #80 single_threaded (thread-confined `!Send`) handle path proven by InputCore (#341) and SelectListCore: - Author the ctor + twelve sync ops in schema/api.json, marking the interface `single_threaded: true`. fluessig lowers it to a THREAD-CONFINED handle: `pub struct TuiCore { core: RefCell }` — no `Arc`, no `Send`/`Sync` — and the `TuiCoreCore` trait sheds its `Send + Sync` supertraits (`Sized + 'static`) with `&mut self` ops reached through `borrow_mut()`. - This is what lets a `!Send` core compile: `TuiCoreImpl` (core_impl.rs) owns pi's `Tui`, whose differential renderer holds `Rc>` children and non-`Send` closures. Every op is sync — no streaming/async — so nothing is carved `@manual`. The `tick` fallibility (`Result<()>`) rides the trait's `anyhow::Result` seam; the `to_usize` dimension clamp moves with it. - regen src/generated.rs (fluessig e056bb9); delete src/tui.rs and its `mod tui;` in lib.rs; keep `pub mod generated;`. Self-mutability is invisible to the TS type, so the RefCell-vs-`&mut self` handle is byte-transparent: the generated `TuiCore` class `.d.ts` block, the `.js` export, and the nativeBinding entry are all byte-identical to the pre-swap build. pi's own test/tui-render.test.ts (23/23) and test/tui-shrink.test.ts (1/1) oracles pass against the locally-built addon. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_017xL2W7dkhPsTSRuXQNCDeY --- crates/pidgin-napi/schema/api.json | 231 ++++++++++++++++++++++++++++ crates/pidgin-napi/src/core_impl.rs | 79 ++++++++++ crates/pidgin-napi/src/generated.rs | 121 +++++++++++++++ crates/pidgin-napi/src/lib.rs | 9 +- crates/pidgin-napi/src/tui.rs | 149 ------------------ 5 files changed, 438 insertions(+), 151 deletions(-) delete mode 100644 crates/pidgin-napi/src/tui.rs diff --git a/crates/pidgin-napi/schema/api.json b/crates/pidgin-napi/schema/api.json index d9a0538a..6016f52b 100644 --- a/crates/pidgin-napi/schema/api.json +++ b/crates/pidgin-napi/schema/api.json @@ -1632,6 +1632,237 @@ } } ] + }, + { + "name": "TuiCore", + "doc": "The Rust-backed differential renderer, exposed to JavaScript as `TuiCore`.\n\nThe JS `TUI` shim owns one of these, keeps pi's component tree / overlay /\nfocus / input logic in JS, and per frame: renders its components to lines,\npushes them via [`TuiCore::set_base_lines`], drives one render with\n[`TuiCore::tick`], drains the write stream with [`TuiCore::take_writes`], and\nforwards it to the JS terminal. The renderer's cross-frame diff state\n(previous lines, cursor rows, full-redraw ladder) lives entirely in the\nwrapped [`Tui`].", + "single_threaded": true, + "ops": [ + { + "name": "new", + "doc": "Build a renderer over an in-memory logging terminal of `cols` x `rows`.\n`show_hardware_cursor` mirrors pi's `PI_HARDWARE_CURSOR` opt-in.", + "shape": "ctor", + "params": [ + { + "name": "cols", + "type": "int64" + }, + { + "name": "rows", + "type": "int64" + }, + { + "name": "showHardwareCursor", + "type": "boolean" + } + ], + "returns": "void" + }, + { + "name": "setSize", + "doc": "Update the terminal dimensions the next render reads (pi reads\n`terminal.columns`/`rows` each `doRender`; the width/height-change full\nredraw is decided inside `tick` by comparing against the previous frame).", + "shape": "unary", + "infallible": true, + "readonly": false, + "params": [ + { + "name": "cols", + "type": "int64" + }, + { + "name": "rows", + "type": "int64" + } + ], + "returns": "void", + "bindings": { + "node": { + "name": "setSize" + } + } + }, + { + "name": "setClearOnShrink", + "doc": "pi's `setClearOnShrink`: clear empty rows when content shrinks.", + "shape": "unary", + "infallible": true, + "readonly": false, + "params": [ + { + "name": "enabled", + "type": "boolean" + } + ], + "returns": "void", + "bindings": { + "node": { + "name": "setClearOnShrink" + } + } + }, + { + "name": "setTermux", + "doc": "Model `isTermuxSession()`: on Termux, height changes do not force a full\nredraw. The shim calls this per frame from `process.env.TERMUX_VERSION`.", + "shape": "unary", + "infallible": true, + "readonly": false, + "params": [ + { + "name": "termux", + "type": "boolean" + } + ], + "returns": "void", + "bindings": { + "node": { + "name": "setTermux" + } + } + }, + { + "name": "setImagesCapable", + "doc": "Model terminal image capability (gates pi's `queryCellSize`). Not needed\nfor the base render diff, but kept for surface parity.", + "shape": "unary", + "infallible": true, + "readonly": false, + "params": [ + { + "name": "capable", + "type": "boolean" + } + ], + "returns": "void", + "bindings": { + "node": { + "name": "setImagesCapable" + } + } + }, + { + "name": "setBaseLines", + "doc": "Replace the entire base frame with pre-rendered `lines` (pi's TS\ncomponents rendered to `string[]`). Backed by a single line-buffer child.", + "shape": "unary", + "infallible": true, + "readonly": false, + "params": [ + { + "name": "lines", + "type": { + "list": "string" + } + } + ], + "returns": "void", + "bindings": { + "node": { + "name": "setBaseLines" + } + } + }, + { + "name": "tick", + "doc": "Drive exactly one render: `request_render(force)` then `flush()`. This is\nthe deterministic, timer-free equivalent of a single fire of pi's\ncoalesced scheduler (one `waitForRender()`). `force` resets the diff state\nfor a full redraw (pi's `requestRender(true)`).", + "shape": "unary", + "infallible": false, + "readonly": false, + "params": [ + { + "name": "force", + "type": "boolean" + } + ], + "returns": "void", + "bindings": { + "node": { + "name": "tick" + } + } + }, + { + "name": "takeWrites", + "doc": "Drain the accumulated write stream (pi's\n`LoggingVirtualTerminal.getWrites()` + `clearWrites()`): the exact bytes\nthis frame emitted, then reset the sink.", + "shape": "unary", + "infallible": true, + "readonly": false, + "params": [], + "returns": "string", + "bindings": { + "node": { + "name": "takeWrites" + } + } + }, + { + "name": "fullRedraws", + "doc": "Number of full redraws performed (pi's `fullRedraws` getter).", + "shape": "unary", + "infallible": true, + "readonly": true, + "params": [], + "returns": "int64", + "bindings": { + "node": { + "name": "fullRedraws" + } + } + }, + { + "name": "cursorRow", + "doc": "Logical cursor row (end of rendered content).", + "shape": "unary", + "infallible": true, + "readonly": true, + "params": [], + "returns": "int64", + "bindings": { + "node": { + "name": "cursorRow" + } + } + }, + { + "name": "hardwareCursorRow", + "doc": "Actual hardware cursor row (pi's `hardwareCursorRow`).", + "shape": "unary", + "infallible": true, + "readonly": true, + "params": [], + "returns": "int64", + "bindings": { + "node": { + "name": "hardwareCursorRow" + } + } + }, + { + "name": "previousViewportTop", + "doc": "Previous viewport top (resize-aware cursor bookkeeping).", + "shape": "unary", + "infallible": true, + "readonly": true, + "params": [], + "returns": "int64", + "bindings": { + "node": { + "name": "previousViewportTop" + } + } + }, + { + "name": "maxLinesRendered", + "doc": "High-water working area (max lines ever rendered since last clear).", + "shape": "unary", + "infallible": true, + "readonly": true, + "params": [], + "returns": "int64", + "bindings": { + "node": { + "name": "maxLinesRendered" + } + } + } + ] } ] } diff --git a/crates/pidgin-napi/src/core_impl.rs b/crates/pidgin-napi/src/core_impl.rs index a6b754ca..545f706a 100644 --- a/crates/pidgin-napi/src/core_impl.rs +++ b/crates/pidgin-napi/src/core_impl.rs @@ -1096,3 +1096,82 @@ impl crate::generated::SelectListCoreCore for SelectListCoreImpl { self.inner.render_lines(width as usize) } } + +/// Clamp a JS-supplied dimension to a terminal size: negatives become 0. +fn tui_to_usize(value: i64) -> usize { + value.max(0) as usize +} + +/// The engine-backed implementation of the generated `TuiCore` contract. +/// +/// Authored `#[fluessig(single_threaded)]`, so the generated handle holds this +/// core THREAD-CONFINED in a `RefCell` — no `Arc`, no `Send`/`Sync` +/// — which is exactly what lets a `!Send` core compile: it owns pi's +/// `Tui`, whose differential renderer holds +/// `Rc>` children and non-`Send` closures. Every op is +/// `&mut self`, reached from the handle through `RefCell::borrow_mut()`, so the +/// JS-visible behavior is byte-for-byte unchanged from the pre-swap hand-written +/// class. +pub struct TuiCoreImpl { + tui: pidgin_tui::Tui, +} + +impl crate::generated::TuiCoreCore for TuiCoreImpl { + fn new(cols: i64, rows: i64, show_hardware_cursor: bool) -> anyhow::Result { + let terminal = pidgin_tui::LoggingTerminal::new(tui_to_usize(cols), tui_to_usize(rows)); + Ok(Self { + tui: pidgin_tui::Tui::new(terminal, show_hardware_cursor), + }) + } + + fn set_size(&mut self, cols: i64, rows: i64) { + self.tui + .terminal_mut() + .resize(tui_to_usize(cols), tui_to_usize(rows)); + } + + fn set_clear_on_shrink(&mut self, enabled: bool) { + self.tui.set_clear_on_shrink(enabled); + } + + fn set_termux(&mut self, termux: bool) { + self.tui.set_termux(termux); + } + + fn set_images_capable(&mut self, capable: bool) { + self.tui.set_images_capable(capable); + } + + fn set_base_lines(&mut self, lines: Vec) { + self.tui.set_base_lines(lines); + } + + fn tick(&mut self, force: bool) -> anyhow::Result<()> { + self.tui.request_render(force); + self.tui.flush().map_err(|e| anyhow::anyhow!(e.to_string())) + } + + fn take_writes(&mut self) -> String { + self.tui.take_writes() + } + + fn full_redraws(&mut self) -> i64 { + self.tui.full_redraws() as i64 + } + + fn cursor_row(&mut self) -> i64 { + self.tui.cursor_row() + } + + fn hardware_cursor_row(&mut self) -> i64 { + self.tui.hardware_cursor_row() + } + + fn previous_viewport_top(&mut self) -> i64 { + self.tui.previous_viewport_top() + } + + fn max_lines_rendered(&mut self) -> i64 { + self.tui.max_lines_rendered() + } +} diff --git a/crates/pidgin-napi/src/generated.rs b/crates/pidgin-napi/src/generated.rs index 8d250396..1efecd3d 100644 --- a/crates/pidgin-napi/src/generated.rs +++ b/crates/pidgin-napi/src/generated.rs @@ -205,6 +205,23 @@ pub trait SelectListCoreCore: Sized + 'static { fn render(&mut self, width: u32) -> Vec; } +/// The `TuiCore` contract — implement over the engine in `crate::core_impl`. +pub trait TuiCoreCore: Sized + 'static { + fn new(cols: i64, rows: i64, show_hardware_cursor: bool) -> anyhow::Result; + fn set_size(&mut self, cols: i64, rows: i64) -> (); + fn set_clear_on_shrink(&mut self, enabled: bool) -> (); + fn set_termux(&mut self, termux: bool) -> (); + fn set_images_capable(&mut self, capable: bool) -> (); + fn set_base_lines(&mut self, lines: Vec) -> (); + fn tick(&mut self, force: bool) -> anyhow::Result<()>; + fn take_writes(&mut self) -> String; + fn full_redraws(&mut self) -> i64; + fn cursor_row(&mut self) -> i64; + fn hardware_cursor_row(&mut self) -> i64; + fn previous_viewport_top(&mut self) -> i64; + fn max_lines_rendered(&mut self) -> i64; +} + /// Returns the crate version. Proves the native addon builds and loads. /// /// Exported to JavaScript as `pidginNativeVersion`. @@ -849,3 +866,107 @@ impl SelectListCore { self.core.borrow_mut().render(width) } } + +/// The Rust-backed differential renderer, exposed to JavaScript as `TuiCore`. +/// +/// The JS `TUI` shim owns one of these, keeps pi's component tree / overlay / +/// focus / input logic in JS, and per frame: renders its components to lines, +/// pushes them via [`TuiCore::set_base_lines`], drives one render with +/// [`TuiCore::tick`], drains the write stream with [`TuiCore::take_writes`], and +/// forwards it to the JS terminal. The renderer's cross-frame diff state +/// (previous lines, cursor rows, full-redraw ladder) lives entirely in the +/// wrapped [`Tui`]. +#[napi] +pub struct TuiCore { + // pub(crate): the @manual ops in lib.rs extend this class and need the core + pub(crate) core: RefCell, +} + +#[napi] +impl TuiCore { + /// Build a renderer over an in-memory logging terminal of `cols` x `rows`. + /// `show_hardware_cursor` mirrors pi's `PI_HARDWARE_CURSOR` opt-in. + #[napi(constructor)] + pub fn new(cols: i64, rows: i64, show_hardware_cursor: bool) -> Result { + Ok(Self { + core: RefCell::new( + ::new( + cols, + rows, + show_hardware_cursor, + ) + .map_err(err)?, + ), + }) + } + /// Update the terminal dimensions the next render reads (pi reads + /// `terminal.columns`/`rows` each `doRender`; the width/height-change full + /// redraw is decided inside `tick` by comparing against the previous frame). + #[napi(js_name = "setSize")] + pub fn set_size(&self, cols: i64, rows: i64) -> () { + self.core.borrow_mut().set_size(cols, rows) + } + /// pi's `setClearOnShrink`: clear empty rows when content shrinks. + #[napi(js_name = "setClearOnShrink")] + pub fn set_clear_on_shrink(&self, enabled: bool) -> () { + self.core.borrow_mut().set_clear_on_shrink(enabled) + } + /// Model `isTermuxSession()`: on Termux, height changes do not force a full + /// redraw. The shim calls this per frame from `process.env.TERMUX_VERSION`. + #[napi(js_name = "setTermux")] + pub fn set_termux(&self, termux: bool) -> () { + self.core.borrow_mut().set_termux(termux) + } + /// Model terminal image capability (gates pi's `queryCellSize`). Not needed + /// for the base render diff, but kept for surface parity. + #[napi(js_name = "setImagesCapable")] + pub fn set_images_capable(&self, capable: bool) -> () { + self.core.borrow_mut().set_images_capable(capable) + } + /// Replace the entire base frame with pre-rendered `lines` (pi's TS + /// components rendered to `string[]`). Backed by a single line-buffer child. + #[napi(js_name = "setBaseLines")] + pub fn set_base_lines(&self, lines: Vec) -> () { + self.core.borrow_mut().set_base_lines(lines) + } + /// Drive exactly one render: `request_render(force)` then `flush()`. This is + /// the deterministic, timer-free equivalent of a single fire of pi's + /// coalesced scheduler (one `waitForRender()`). `force` resets the diff state + /// for a full redraw (pi's `requestRender(true)`). + #[napi(js_name = "tick")] + pub fn tick(&self, force: bool) -> Result<()> { + self.core.borrow_mut().tick(force).map_err(err) + } + /// Drain the accumulated write stream (pi's + /// `LoggingVirtualTerminal.getWrites()` + `clearWrites()`): the exact bytes + /// this frame emitted, then reset the sink. + #[napi(js_name = "takeWrites")] + pub fn take_writes(&self) -> String { + self.core.borrow_mut().take_writes() + } + /// Number of full redraws performed (pi's `fullRedraws` getter). + #[napi(js_name = "fullRedraws")] + pub fn full_redraws(&self) -> i64 { + self.core.borrow_mut().full_redraws() + } + /// Logical cursor row (end of rendered content). + #[napi(js_name = "cursorRow")] + pub fn cursor_row(&self) -> i64 { + self.core.borrow_mut().cursor_row() + } + /// Actual hardware cursor row (pi's `hardwareCursorRow`). + #[napi(js_name = "hardwareCursorRow")] + pub fn hardware_cursor_row(&self) -> i64 { + self.core.borrow_mut().hardware_cursor_row() + } + /// Previous viewport top (resize-aware cursor bookkeeping). + #[napi(js_name = "previousViewportTop")] + pub fn previous_viewport_top(&self) -> i64 { + self.core.borrow_mut().previous_viewport_top() + } + /// High-water working area (max lines ever rendered since last clear). + #[napi(js_name = "maxLinesRendered")] + pub fn max_lines_rendered(&self) -> i64 { + self.core.borrow_mut().max_lines_rendered() + } +} diff --git a/crates/pidgin-napi/src/lib.rs b/crates/pidgin-napi/src/lib.rs index 890f57e7..5b17e96a 100644 --- a/crates/pidgin-napi/src/lib.rs +++ b/crates/pidgin-napi/src/lib.rs @@ -19,8 +19,13 @@ use serde_json::Value; // TUI renderer surface (`TuiCore`): drives pi's differential render path // (`TUI::doRender`) natively. The JS shim feeds pre-rendered lines in and drains -// the write stream out; overlays/focus/input stay in pi's TS. Additive. -mod tui; +// the write stream out; overlays/focus/input stay in pi's TS. Now generated from +// the fluessig api schema through `crate::generated` + `crate::core_impl` (the +// `TuiCoreImpl` engine seam). It is authored `#[fluessig(single_threaded)]`, so +// the generated handle holds the `!Send` core (pi's `Tui`, with +// `Rc>` children and non-`Send` closures) THREAD-CONFINED +// in a `RefCell` — no `Arc`, no `Send`/`Sync` bound. See src/generated.rs +// and schema/api.json. Additive. // TUI stdin-buffer surface (`StdinBufferCore`): drives pi's `StdinBuffer` // escape-sequence splitter / bracketed-paste / Kitty-dedup state machine diff --git a/crates/pidgin-napi/src/tui.rs b/crates/pidgin-napi/src/tui.rs deleted file mode 100644 index 2fbb7d8e..00000000 --- a/crates/pidgin-napi/src/tui.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! Node-API surface for the differential TUI renderer (`TuiCore`). -//! -//! This exposes the Rust renderer ([`pidgin_tui::Tui`], backed by the in-memory -//! [`pidgin_tui::LoggingTerminal`] sink) to pi's `packages/tui` renderer tests. -//! It is the native half of pi's `TUI` differential render path -//! (`vendor/pi/packages/tui/src/tui.ts`, `TUI::doRender`). -//! -//! # The seam: pre-rendered lines, not a component tree -//! -//! The Rust renderer consumes **pre-rendered lines**, never a component -//! prop-tree: pi's TS components render themselves to `string[]` on the JS side -//! (exactly as they do today), and the shim feeds those lines into Rust via -//! [`TuiCore::set_base_lines`]. Rust then does the diff / composite / write-stream -//! that `doRender` performs, and the shim drains the resulting bytes with -//! [`TuiCore::take_writes`] and forwards them to pi's `VirtualTerminal` (an xterm -//! emulator) so `getViewport`/cell assertions replay JS-side. -//! -//! # Why no threadsafe callback is needed here -//! -//! The base render tree crosses as an already-flattened `string[]` per frame, and -//! the write stream comes back as a `String`. There is no re-render of props in -//! Rust and no callback into JS during a render, so the boundary is plain -//! synchronous calls — no `ThreadsafeFunction`. Overlays (which Rust would -//! re-render at a resolved width, requiring a JS callback) are intentionally out -//! of scope: the shim keeps pi's overlay/focus/input path in TS and only routes -//! the base render path through this core. - -use napi::bindgen_prelude::*; -use napi_derive::napi; - -use pidgin_tui::{LoggingTerminal, Tui}; - -/// The Rust-backed differential renderer, exposed to JavaScript as `TuiCore`. -/// -/// The JS `TUI` shim owns one of these, keeps pi's component tree / overlay / -/// focus / input logic in JS, and per frame: renders its components to lines, -/// pushes them via [`TuiCore::set_base_lines`], drives one render with -/// [`TuiCore::tick`], drains the write stream with [`TuiCore::take_writes`], and -/// forwards it to the JS terminal. The renderer's cross-frame diff state -/// (previous lines, cursor rows, full-redraw ladder) lives entirely in the -/// wrapped [`Tui`]. -#[napi(js_name = "TuiCore")] -pub struct TuiCore { - tui: Tui, -} - -fn to_usize(value: i64) -> usize { - value.max(0) as usize -} - -#[napi] -impl TuiCore { - /// Build a renderer over an in-memory logging terminal of `cols` x `rows`. - /// `show_hardware_cursor` mirrors pi's `PI_HARDWARE_CURSOR` opt-in. - #[napi(constructor)] - pub fn new(cols: i64, rows: i64, show_hardware_cursor: bool) -> Self { - let terminal = LoggingTerminal::new(to_usize(cols), to_usize(rows)); - Self { - tui: Tui::new(terminal, show_hardware_cursor), - } - } - - /// Update the terminal dimensions the next render reads (pi reads - /// `terminal.columns`/`rows` each `doRender`; the width/height-change full - /// redraw is decided inside `tick` by comparing against the previous frame). - #[napi(js_name = "setSize")] - pub fn set_size(&mut self, cols: i64, rows: i64) { - self.tui - .terminal_mut() - .resize(to_usize(cols), to_usize(rows)); - } - - /// pi's `setClearOnShrink`: clear empty rows when content shrinks. - #[napi(js_name = "setClearOnShrink")] - pub fn set_clear_on_shrink(&mut self, enabled: bool) { - self.tui.set_clear_on_shrink(enabled); - } - - /// Model `isTermuxSession()`: on Termux, height changes do not force a full - /// redraw. The shim calls this per frame from `process.env.TERMUX_VERSION`. - #[napi(js_name = "setTermux")] - pub fn set_termux(&mut self, termux: bool) { - self.tui.set_termux(termux); - } - - /// Model terminal image capability (gates pi's `queryCellSize`). Not needed - /// for the base render diff, but kept for surface parity. - #[napi(js_name = "setImagesCapable")] - pub fn set_images_capable(&mut self, capable: bool) { - self.tui.set_images_capable(capable); - } - - /// Replace the entire base frame with pre-rendered `lines` (pi's TS - /// components rendered to `string[]`). Backed by a single line-buffer child. - #[napi(js_name = "setBaseLines")] - pub fn set_base_lines(&mut self, lines: Vec) { - self.tui.set_base_lines(lines); - } - - /// Drive exactly one render: `request_render(force)` then `flush()`. This is - /// the deterministic, timer-free equivalent of a single fire of pi's - /// coalesced scheduler (one `waitForRender()`). `force` resets the diff state - /// for a full redraw (pi's `requestRender(true)`). - #[napi(js_name = "tick")] - pub fn tick(&mut self, force: bool) -> Result<()> { - self.tui.request_render(force); - self.tui - .flush() - .map_err(|e| Error::from_reason(e.to_string())) - } - - /// Drain the accumulated write stream (pi's - /// `LoggingVirtualTerminal.getWrites()` + `clearWrites()`): the exact bytes - /// this frame emitted, then reset the sink. - #[napi(js_name = "takeWrites")] - pub fn take_writes(&mut self) -> String { - self.tui.take_writes() - } - - /// Number of full redraws performed (pi's `fullRedraws` getter). - #[napi(js_name = "fullRedraws")] - pub fn full_redraws(&self) -> i64 { - self.tui.full_redraws() as i64 - } - - /// Logical cursor row (end of rendered content). - #[napi(js_name = "cursorRow")] - pub fn cursor_row(&self) -> i64 { - self.tui.cursor_row() - } - - /// Actual hardware cursor row (pi's `hardwareCursorRow`). - #[napi(js_name = "hardwareCursorRow")] - pub fn hardware_cursor_row(&self) -> i64 { - self.tui.hardware_cursor_row() - } - - /// Previous viewport top (resize-aware cursor bookkeeping). - #[napi(js_name = "previousViewportTop")] - pub fn previous_viewport_top(&self) -> i64 { - self.tui.previous_viewport_top() - } - - /// High-water working area (max lines ever rendered since last clear). - #[napi(js_name = "maxLinesRendered")] - pub fn max_lines_rendered(&self) -> i64 { - self.tui.max_lines_rendered() - } -}