Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions crates/pidgin-napi/schema/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
]
}
]
}
79 changes: 79 additions & 0 deletions crates/pidgin-napi/src/core_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TuiCoreImpl>` — no `Arc`, no `Send`/`Sync`
/// — which is exactly what lets a `!Send` core compile: it owns pi's
/// `Tui<LoggingTerminal>`, whose differential renderer holds
/// `Rc<RefCell<dyn Component>>` 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<pidgin_tui::LoggingTerminal>,
}

impl crate::generated::TuiCoreCore for TuiCoreImpl {
fn new(cols: i64, rows: i64, show_hardware_cursor: bool) -> anyhow::Result<Self> {
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<String>) {
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()
}
}
Loading