diff --git a/example/c-vt-cmake-cross/src/main.c b/example/c-vt-cmake-cross/src/main.c index 992586451..3f4a8fb40 100644 --- a/example/c-vt-cmake-cross/src/main.c +++ b/example/c-vt-cmake-cross/src/main.c @@ -7,12 +7,7 @@ int main() { // Create a terminal with a small grid GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24); assert(result == GHOSTTY_SUCCESS); // Write some VT-encoded content into the terminal diff --git a/example/c-vt-cmake-static/src/main.c b/example/c-vt-cmake-static/src/main.c index 233bd34d1..c9e566d5b 100644 --- a/example/c-vt-cmake-static/src/main.c +++ b/example/c-vt-cmake-static/src/main.c @@ -7,12 +7,7 @@ int main() { // Create a terminal with a small grid GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24); assert(result == GHOSTTY_SUCCESS); // Write some VT-encoded content into the terminal diff --git a/example/c-vt-cmake/src/main.c b/example/c-vt-cmake/src/main.c index 992586451..3f4a8fb40 100644 --- a/example/c-vt-cmake/src/main.c +++ b/example/c-vt-cmake/src/main.c @@ -7,12 +7,7 @@ int main() { // Create a terminal with a small grid GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24); assert(result == GHOSTTY_SUCCESS); // Write some VT-encoded content into the terminal diff --git a/example/c-vt-colors/src/main.c b/example/c-vt-colors/src/main.c index 6838527f2..19c1e7b46 100644 --- a/example/c-vt-colors/src/main.c +++ b/example/c-vt-colors/src/main.c @@ -87,12 +87,7 @@ void print_all_colors(GhosttyTerminal terminal, const char* label) { int main() { // Create a terminal GhosttyTerminal terminal = NULL; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) { + if (ghostty_terminal_new(NULL, &terminal, 80, 24) != GHOSTTY_SUCCESS) { fprintf(stderr, "Failed to create terminal\n"); return 1; } diff --git a/example/c-vt-compression/src/main.c b/example/c-vt-compression/src/main.c index f3269b410..2eb3afc06 100644 --- a/example/c-vt-compression/src/main.c +++ b/example/c-vt-compression/src/main.c @@ -30,12 +30,14 @@ static bool compression_idle_step(GhosttyTerminal terminal) { int main(void) { GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 10 * 1024 * 1024, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24); + assert(result == GHOSTTY_SUCCESS); + + size_t max_scrollback_bytes = 10 * 1024 * 1024; + result = ghostty_terminal_set( + terminal, + GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES, + &max_scrollback_bytes); assert(result == GHOSTTY_SUCCESS); //! [compression-activity] diff --git a/example/c-vt-effects/src/main.c b/example/c-vt-effects/src/main.c index cefee2ada..c40f8fa88 100644 --- a/example/c-vt-effects/src/main.c +++ b/example/c-vt-effects/src/main.c @@ -74,12 +74,7 @@ GhosttyClipboardWriteResult on_clipboard_write( int main() { // Create a terminal GhosttyTerminal terminal = NULL; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) { + if (ghostty_terminal_new(NULL, &terminal, 80, 24) != GHOSTTY_SUCCESS) { fprintf(stderr, "Failed to create terminal\n"); return 1; } diff --git a/example/c-vt-formatter/src/main.c b/example/c-vt-formatter/src/main.c index 56f9d1220..9e7d250b9 100644 --- a/example/c-vt-formatter/src/main.c +++ b/example/c-vt-formatter/src/main.c @@ -7,12 +7,7 @@ int main() { // Create a terminal with a small grid GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24); assert(result == GHOSTTY_SUCCESS); // Write VT-encoded content into the terminal to exercise various diff --git a/example/c-vt-grid-ref-tracked/src/main.c b/example/c-vt-grid-ref-tracked/src/main.c index a914a9727..104943d8d 100644 --- a/example/c-vt-grid-ref-tracked/src/main.c +++ b/example/c-vt-grid-ref-tracked/src/main.c @@ -25,12 +25,7 @@ static uint32_t codepoint_at_tracked_ref(GhosttyTrackedGridRef tracked) { int main() { GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 8, - .rows = 3, - .max_scrollback = 100, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 8, 3); assert(result == GHOSTTY_SUCCESS); const char *text = "alpha\r\n" diff --git a/example/c-vt-grid-traverse/src/main.c b/example/c-vt-grid-traverse/src/main.c index f07169eb6..fa9ce30ec 100644 --- a/example/c-vt-grid-traverse/src/main.c +++ b/example/c-vt-grid-traverse/src/main.c @@ -7,12 +7,7 @@ int main() { // Create a small terminal GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 10, - .rows = 3, - .max_scrollback = 0, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 10, 3); assert(result == GHOSTTY_SUCCESS); // Write some content so the grid has interesting data diff --git a/example/c-vt-kitty-graphics/src/main.c b/example/c-vt-kitty-graphics/src/main.c index d0d891928..049f7ab35 100644 --- a/example/c-vt-kitty-graphics/src/main.c +++ b/example/c-vt-kitty-graphics/src/main.c @@ -72,12 +72,7 @@ int main() { /* Create a terminal with Kitty graphics enabled. */ GhosttyTerminal terminal = NULL; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) { + if (ghostty_terminal_new(NULL, &terminal, 80, 24) != GHOSTTY_SUCCESS) { fprintf(stderr, "Failed to create terminal\n"); return 1; } diff --git a/example/c-vt-render/src/main.c b/example/c-vt-render/src/main.c index feb3628d4..cb9e7e8c2 100644 --- a/example/c-vt-render/src/main.c +++ b/example/c-vt-render/src/main.c @@ -26,12 +26,7 @@ int main(void) { // from the terminal. The render state captures a snapshot of everything // needed to draw a frame. GhosttyTerminal terminal = NULL; - GhosttyTerminalOptions terminal_opts = { - .cols = 40, - .rows = 5, - .max_scrollback = 10000, - }; - result = ghostty_terminal_new(NULL, &terminal, terminal_opts); + result = ghostty_terminal_new(NULL, &terminal, 40, 5); assert(result == GHOSTTY_SUCCESS); GhosttyRenderState render_state = NULL; diff --git a/example/c-vt-selection-gesture/src/main.c b/example/c-vt-selection-gesture/src/main.c index 050e9a3b1..341c7547d 100644 --- a/example/c-vt-selection-gesture/src/main.c +++ b/example/c-vt-selection-gesture/src/main.c @@ -54,12 +54,7 @@ static GhosttySelectionGestureEvent new_event( int main() { GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 20, - .rows = 4, - .max_scrollback = 100, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 20, 4); assert(result == GHOSTTY_SUCCESS); vt_write(terminal, "hello world\r\nsecond line"); diff --git a/example/c-vt-selection/src/main.c b/example/c-vt-selection/src/main.c index 83384ec15..0b3ff28cc 100644 --- a/example/c-vt-selection/src/main.c +++ b/example/c-vt-selection/src/main.c @@ -49,12 +49,7 @@ static void print_selection( int main() { GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 8, - .max_scrollback = 0, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 8); assert(result == GHOSTTY_SUCCESS); // A realistic shell transcript with OSC 133 semantic prompt markers. diff --git a/example/c-vt-stream/src/main.c b/example/c-vt-stream/src/main.c index 7063e1f14..c2db6b14b 100644 --- a/example/c-vt-stream/src/main.c +++ b/example/c-vt-stream/src/main.c @@ -7,12 +7,7 @@ int main(void) { //! [vt-stream-init] // Create a terminal GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24); assert(result == GHOSTTY_SUCCESS); //! [vt-stream-init] diff --git a/example/cpp-vt-stream/src/main.cpp b/example/cpp-vt-stream/src/main.cpp index a77f98ad5..90331e363 100644 --- a/example/cpp-vt-stream/src/main.cpp +++ b/example/cpp-vt-stream/src/main.cpp @@ -6,12 +6,7 @@ int main() { // Create a terminal GhosttyTerminal terminal; - GhosttyTerminalOptions opts = { - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }; - GhosttyResult result = ghostty_terminal_new(nullptr, &terminal, opts); + GhosttyResult result = ghostty_terminal_new(nullptr, &terminal, 80, 24); assert(result == GHOSTTY_SUCCESS); // Feed VT data into the terminal diff --git a/example/swift-vt-xcframework/Sources/main.swift b/example/swift-vt-xcframework/Sources/main.swift index d374f539f..d6cf2bb17 100644 --- a/example/swift-vt-xcframework/Sources/main.swift +++ b/example/swift-vt-xcframework/Sources/main.swift @@ -3,12 +3,7 @@ import GhosttyVt // Create a terminal with a small grid var terminal: GhosttyTerminal? -var opts = GhosttyTerminalOptions( - cols: 80, - rows: 24, - max_scrollback: 0 -) -let result = ghostty_terminal_new(nil, &terminal, opts) +let result = ghostty_terminal_new(nil, &terminal, 80, 24) guard result == GHOSTTY_SUCCESS, let terminal else { fatalError("Failed to create terminal") } diff --git a/example/wasm-vt/index.html b/example/wasm-vt/index.html index d720e2375..81a033737 100644 --- a/example/wasm-vt/index.html +++ b/example/wasm-vt/index.html @@ -199,20 +199,15 @@ const rows = parseInt(document.getElementById('rows').value, 10); const vtText = parseEscapes(document.getElementById('vtInput').value); - const TERM_OPTS_SIZE = typeLayout['GhosttyTerminalOptions'].size; - const optsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(TERM_OPTS_SIZE); - new Uint8Array(getBuffer(), optsPtr, TERM_OPTS_SIZE).fill(0); - const optsView = new DataView(getBuffer(), optsPtr, TERM_OPTS_SIZE); - setField(optsView, 'GhosttyTerminalOptions', 'cols', cols); - setField(optsView, 'GhosttyTerminalOptions', 'rows', rows); - setField(optsView, 'GhosttyTerminalOptions', 'max_scrollback', 0); - // Allocate pointer to receive the terminal handle const termPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque(); // Create terminal - const newResult = wasmInstance.exports.ghostty_terminal_new(0, termPtrPtr, optsPtr); - wasmInstance.exports.ghostty_wasm_free_u8_array(optsPtr, TERM_OPTS_SIZE); + const newResult = wasmInstance.exports.ghostty_terminal_new( + 0, + termPtrPtr, + cols, + rows); if (newResult !== GHOSTTY_SUCCESS) { throw new Error(`ghostty_terminal_new failed with result ${newResult}`); diff --git a/include/ghostty/vt/key.h b/include/ghostty/vt/key.h index 61b954753..517d0c30d 100644 --- a/include/ghostty/vt/key.h +++ b/include/ghostty/vt/key.h @@ -42,8 +42,7 @@ * @code{.c} * // Create a terminal and feed it some VT data that changes modes * GhosttyTerminal terminal; - * ghostty_terminal_new(NULL, &terminal, - * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0}); + * ghostty_terminal_new(NULL, &terminal, 80, 24); * * // Application might write data that enables Kitty keyboard protocol, etc. * ghostty_terminal_vt_write(terminal, vt_data, vt_len); diff --git a/include/ghostty/vt/mouse.h b/include/ghostty/vt/mouse.h index 4ba5f52e3..4a582a23b 100644 --- a/include/ghostty/vt/mouse.h +++ b/include/ghostty/vt/mouse.h @@ -39,8 +39,7 @@ * @code{.c} * // Create a terminal and feed it some VT data that enables mouse tracking * GhosttyTerminal terminal; - * ghostty_terminal_new(NULL, &terminal, - * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0}); + * ghostty_terminal_new(NULL, &terminal, 80, 24); * * // Application might write data that enables mouse reporting, etc. * ghostty_terminal_vt_write(terminal, vt_data, vt_len); diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 8470607b3..8aaa57e9b 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -172,26 +172,6 @@ extern "C" { * @{ */ -/** - * Terminal initialization options. - * - * @ingroup terminal - */ -typedef struct { - /** Terminal width in cells. Must be greater than zero. */ - uint16_t cols; - - /** Terminal height in cells. Must be greater than zero. */ - uint16_t rows; - - /** Maximum number of lines to keep in scrollback history. */ - size_t max_scrollback; - - // TODO: Consider ABI compatibility implications of this struct. - // We may want to artificially pad it significantly to support - // future options. -} GhosttyTerminalOptions; - /** * Amount of compression work to perform before returning. * @@ -888,6 +868,52 @@ typedef enum GHOSTTY_ENUM_TYPED { * Input type: GhosttyTerminalClipboardWriteFn */ GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE = 26, + + /** + * Set the maximum scrollback allocation in bytes. + * + * This is an estimate. Internally, libghostty only prunes bytes up + * to a "page"-granularity. A page is the minimum allocated unit of + * grid space within Ghostty. A page at the time of writing these docs + * is about 400KB, so the byte limit will be within this delta. + * + * This works alongside the line limit configuration. If both are set, + * the first-reached limit is used first. Both limits are dependent + * on external state (byte limit can be reached with less lines if + * more styles are used for example, line limit can be reached with + * a narrower terminal viewport). So, they are useful together. + * + * Lowering the limit immediately removes eligible complete historical + * pages. A value of zero disables scrollback and erases retained history. + * A NULL value pointer removes the byte limit. + * + * Input type: size_t* + */ + GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES = 27, + + /** + * Set the maximum number of physical lines retained in scrollback. + * + * This is an estimate. Internally, libghostty only prunes lines up + * to a "page"-granularity. A page is the minimum allocated unit of + * grid space within Ghostty. As a result, the actual available scrollback + * lines will almost always be higher than configured. The magnitude + * of the difference depends on the number of used styles, graphemes, etc. + * since the row-count in a page is dynamic based on that. In general, + * it ranges from dozens to a hundred or so lines. + * + * This works alongside the line limit configuration. If both are set, + * the first-reached limit is used first. Both limits are dependent + * on external state (byte limit can be reached with less lines if + * more styles are used for example, line limit can be reached with + * a narrower terminal viewport). So, they are useful together. + * + * Lowering the limit immediately removes eligible complete historical + * pages. A NULL value pointer removes the line limit. + * + * Input type: size_t* + */ + GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_LINES = 28, GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalOption; @@ -1215,22 +1241,50 @@ typedef enum GHOSTTY_ENUM_TYPED { * Output type: bool * */ GHOSTTY_TERMINAL_DATA_VT_PROCESSING_ERROR = 33, + + /** + * The configured maximum scrollback allocation in bytes. + * + * This always reports the primary screen's configured value, including + * while an alternate screen is active. Returns GHOSTTY_NO_VALUE when the + * configured byte limit is unlimited. + * + * Output type: size_t * + */ + GHOSTTY_TERMINAL_DATA_SCROLLBACK_MAX_BYTES = 34, + + /** + * The configured maximum number of physical scrollback lines. + * + * This always reports the primary screen's configured value, including + * while an alternate screen is active. Returns GHOSTTY_NO_VALUE when the + * configured line limit is unlimited. + * + * Output type: size_t * + */ + GHOSTTY_TERMINAL_DATA_SCROLLBACK_MAX_LINES = 35, GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalData; /** * Create a new terminal instance. * + * The terminal starts with various reasonable defaults e.g. around + * scrollback limits. Use ghostty_terminal_set() to change any options + * prior to using the terminal. + * * @param allocator Pointer to allocator, or NULL to use the default allocator * @param terminal Pointer to store the created terminal handle - * @param options Terminal initialization options + * @param cols Terminal width in cells (must be greater than zero) + * @param rows Terminal height in cells (must be greater than zero) * @return GHOSTTY_SUCCESS on success, or an error code on failure * * @ingroup terminal */ GHOSTTY_API GhosttyResult ghostty_terminal_new(const GhosttyAllocator* allocator, - GhosttyTerminal* terminal, - GhosttyTerminalOptions options); + GhosttyTerminal* terminal, + uint16_t cols, + uint16_t rows); /** * Free a terminal instance. @@ -1291,7 +1345,8 @@ GHOSTTY_API GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal, * write_pty callback and userdata pointer. The value is passed * directly for pointer types (callbacks, userdata) or as a pointer * to the value for non-pointer types (e.g. GhosttyString*). - * NULL clears the option to its default. + * The behavior of a NULL value is specific to each option and is + * documented by the corresponding GhosttyTerminalOption value. * * Callbacks are invoked synchronously during ghostty_terminal_vt_write(). * Callbacks must not call ghostty_terminal_vt_write() on the same diff --git a/src/inspector/widgets/pagelist.zig b/src/inspector/widgets/pagelist.zig index 7e0012896..2aecafd3e 100644 --- a/src/inspector/widgets/pagelist.zig +++ b/src/inspector/widgets/pagelist.zig @@ -162,7 +162,7 @@ fn summaryTable(pages: *const PageList) void { ); _ = cimgui.c.ImGui_TableSetColumnIndex(2); var limit_buf: [64]u8 = undefined; - const limit = formatBytes(&limit_buf, pages.maxSize()); + const limit = formatBytes(&limit_buf, pages.limits.max(.bytes)); cimgui.c.ImGui_TextUnformatted(limit.ptr); cimgui.c.ImGui_TableNextRow(); diff --git a/src/inspector/widgets/screen.zig b/src/inspector/widgets/screen.zig index 41c113e75..6e66a1423 100644 --- a/src/inspector/widgets/screen.zig +++ b/src/inspector/widgets/screen.zig @@ -353,7 +353,12 @@ pub fn internalStateTable( _ = cimgui.c.ImGui_TableSetColumnIndex(0); cimgui.c.ImGui_Text("Memory Limit"); _ = cimgui.c.ImGui_TableSetColumnIndex(1); - cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.maxSize(), units.toKibiBytes(pages.maxSize())); + const max_bytes = pages.limits.max(.bytes); + cimgui.c.ImGui_Text( + "%d bytes (%d KiB)", + max_bytes, + units.toKibiBytes(max_bytes), + ); cimgui.c.ImGui_TableNextRow(); _ = cimgui.c.ImGui_TableSetColumnIndex(0); diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index db5104c16..e6fd48bb8 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -401,23 +401,8 @@ page_size: usize, /// in the state struct. page_compression: IncrementalCompressionState = .{}, -/// Maximum size of the page allocation in bytes. This only includes pages -/// that are used ONLY for scrollback. If the active area is still partially -/// in a page that also includes scrollback, then that page is not included. -explicit_max_size: usize, - -/// This is the minimum max size that we will respect due to the rows/cols -/// of the PageList. We must always be able to fit at least the active area -/// and at least two pages for our algorithms. -min_max_size: usize, - -/// Maximum number of physical rows retained as scrollback. This excludes the -/// active area and is raised to `min_max_lines` when necessary. -explicit_max_lines: usize, - -/// Minimum line limit required to retain one standard page worth of -/// scrollback at the current column count. -min_max_lines: usize, +/// Limits for scrollback. +limits: Limits, /// The total number of rows represented by this PageList. This is used /// specifically for scrollbar information so we can have the total size. @@ -486,66 +471,6 @@ pub const Viewport = union(enum) { pin, }; -/// Returns the minimum valid "max size" for a given number of rows and cols -/// such that we can fit the active area AND at least two pages. Note we -/// need the two pages for algorithms to work properly (such as grow) but -/// we don't need to fit double the active area. -/// -/// This min size may not be totally correct in the case that a large -/// number of other dimensions makes our row size in a page very small. -/// But this gives us a nice fast heuristic for determining min/max size. -/// Therefore, if the page size is violated you should always also verify -/// that we have enough space for the active area. -fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) usize { - // Invariant required to ensure our divCeil below cannot overflow. - comptime { - const max_rows = std.math.maxInt(size.CellCountInt); - _ = std.math.divCeil(usize, max_rows, 1) catch unreachable; - } - - // Get our capacity to fit our rows. If the cols are too big, it may - // force less rows than we want meaning we need more than one page to - // represent a viewport. - const cap = initialCapacity(cols); - - // Calculate the number of standard sized pages we need to represent - // an active area. - const pages_exact = if (cap.rows >= rows) 1 else std.math.divCeil( - usize, - rows, - cap.rows, - ) catch { - // Not possible: - // - initialCapacity guarantees at least 1 row - // - numerator/denominator can't overflow because of comptime check above - unreachable; - }; - - // We always need at least one page extra so that we - // can fit partial pages to spread our active area across two pages. - // Even for caps that can't fit all rows in a single page, we add one - // because the most extra space we need at any given time is only - // the partial amount of one page. - const pages = pages_exact + 1; - assert(pages >= 2); - - // log.debug("minMaxSize cols={} rows={} cap={} pages={}", .{ - // cols, - // rows, - // cap, - // pages, - // }); - - return PagePool.item_size * pages; -} - -/// Returns the minimum line limit for a given column count. Line limits are -/// page-granular, so we always permit at least one standard page worth of -/// scrollback rows. -fn minMaxLines(cols: size.CellCountInt) usize { - return initialCapacity(cols).rows; -} - /// Calculates the initial capacity for a new page for a given column /// count. This will attempt to fit within std_size at all times so we /// can use our memory pool, but if cols is too big, this will return a @@ -680,8 +605,9 @@ pub fn init( rows, ); - // Get our minimum max size, see doc comments for more details. - const min_max_size = minMaxSize(cols, rows); + var limits: Limits = .init(cols, rows); + limits.set(.bytes, opts.max_size); + limits.set(.lines, opts.max_lines); // We always track our viewport pin to ensure this is never an allocation try tw.check(.viewport_pin); @@ -702,10 +628,7 @@ pub fn init( .page_serial = page_serial, .page_serial_epoch = 0, .page_size = page_size, - .explicit_max_size = opts.max_size orelse std.math.maxInt(usize), - .min_max_size = min_max_size, - .explicit_max_lines = opts.max_lines orelse std.math.maxInt(usize), - .min_max_lines = minMaxLines(cols), + .limits = limits, .total_rows = rows, .tracked_pins = tracked_pins, .viewport = .{ .active = {} }, @@ -884,12 +807,12 @@ fn verifyIntegrity(self: *const PageList) IntegrityError!void { // active rows. Complete historical pages are always eligible for pruning. if (self.total_rows > self.rows) { const history_rows = self.total_rows - self.rows; - if (history_rows > self.maxLines() and + if (history_rows > self.limits.max(.lines) and self.pages.first.? != self.getTopLeft(.active).node) { log.warn( "PageList integrity violation: max lines exceeded history={} max={}", - .{ history_rows, self.maxLines() }, + .{ history_rows, self.limits.max(.lines) }, ); return IntegrityError.MaxLinesExceeded; } @@ -1236,10 +1159,7 @@ pub fn clone( .page_serial = page_serial, .page_serial_epoch = 0, .page_size = page_size, - .explicit_max_size = self.explicit_max_size, - .min_max_size = self.min_max_size, - .explicit_max_lines = self.explicit_max_lines, - .min_max_lines = self.min_max_lines, + .limits = self.limits, .cols = self.cols, .rows = self.rows, .total_rows = total_rows, @@ -1270,7 +1190,7 @@ pub fn clone( } // A clone can copy more history than its inherited line limit. - result.enforceMaxLines(); + result.limits.enforce(&result, .lines); result.assertIntegrity(); return result; @@ -1328,24 +1248,18 @@ pub fn resize(self: *PageList, opts: Resize) Allocator.Error!void { try self.resizeWithoutReflow(opts); // Shrinking the active row count turns former active rows into // scrollback even without reflow, which can cross the line limit. - self.enforceMaxLines(); + self.limits.enforce(self, .lines); return; } - // Recalculate our minimum max size. This allows grow to work properly - // when increasing beyond our initial minimum max size or explicit max - // size to fit the active area. - const old_min_max_size = self.min_max_size; - self.min_max_size = minMaxSize( + // Recalculate our minimum limits. This allows grow to work properly when + // increasing beyond the explicit limits to fit the active area. + const old_limits = self.limits; + self.limits.resize( opts.cols orelse self.cols, opts.rows orelse self.rows, ); - errdefer self.min_max_size = old_min_max_size; - - // Recalculate our minimum max lines for the same reasons as above. - const old_min_max_lines = self.min_max_lines; - self.min_max_lines = minMaxLines(opts.cols orelse self.cols); - errdefer self.min_max_lines = old_min_max_lines; + errdefer self.limits = old_limits; // On reflow, the main thing that causes reflow is column changes. If // only rows change, reflow is impossible. So we change our behavior based @@ -1386,7 +1300,7 @@ pub fn resize(self: *PageList, opts: Resize) Allocator.Error!void { // Column reflow can change the physical history row count, and a row // resize can move the active boundary. Both may expose whole old pages // that are now eligible for line-limit pruning. - self.enforceMaxLines(); + self.limits.enforce(self, .lines); } /// Resize the pagelist with reflow by adding or removing columns. @@ -2413,20 +2327,14 @@ const ReflowCursor = struct { }; fn resizeWithoutReflow(self: *PageList, opts: Resize) Allocator.Error!void { - // We only set the new min_max_size if we're not reflowing. If we are - // reflowing, then resize handles this for us. - const old_min_max_size = self.min_max_size; - self.min_max_size = if (!opts.reflow) minMaxSize( + // We only set the new minimums if we're not reflowing. If we are + // reflowing, then the outer resize call handles this for us. + const old_limits = self.limits; + if (!opts.reflow) self.limits.resize( opts.cols orelse self.cols, opts.rows orelse self.rows, - ) else old_min_max_size; - errdefer self.min_max_size = old_min_max_size; - const old_min_max_lines = self.min_max_lines; - self.min_max_lines = if (!opts.reflow) - minMaxLines(opts.cols orelse self.cols) - else - old_min_max_lines; - errdefer self.min_max_lines = old_min_max_lines; + ); + errdefer self.limits = old_limits; // Important! We have to do cols first because cols may cause us to // destroy pages if we're increasing cols which will free up page_size @@ -2913,7 +2821,7 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { defer self.assertIntegrity(); // Special case no-scrollback mode to never allow scrolling. - if (self.explicit_max_size == 0) { + if (self.limits.bytes.explicit == 0) { self.viewport = .active; return; } @@ -3483,7 +3391,7 @@ pub fn scrollbar(self: *PageList) Scrollbar { // it always has SOME extra space (due to the way we allocate by page). // So even with no scrollback we have some growth. It is architecturally // much simpler to just hide that for no-scrollback cases. - if (self.explicit_max_size == 0) return .{ + if (self.limits.bytes.explicit == 0) return .{ .total = self.rows, .offset = 0, .len = self.rows, @@ -3584,74 +3492,31 @@ fn fixupViewport( } } -/// Returns the actual max size. This may be greater than the explicit -/// value if the explicit value is less than the min_max_size. +/// Change the maximum logical page allocation at runtime. Null removes the +/// explicit byte limit and zero disables scrollback. /// -/// This value is a HEURISTIC. You cannot assert on this value. We may -/// exceed this value if required to fit the active area. This may be -/// required in some cases if the active area has a large number of -/// graphemes, styles, etc. -pub fn maxSize(self: *const PageList) usize { - return @max(self.explicit_max_size, self.min_max_size); +/// Lowering the limit immediately removes eligible complete historical pages. +/// The effective limit may still be raised to fit the active area, and a page +/// which overlaps the active area is never split solely to satisfy this limit. +pub fn setMaxBytes(self: *PageList, max: ?usize) void { + defer self.assertIntegrity(); + + self.limits.set(.bytes, max); + self.limits.enforce(self, .bytes); + if (self.limits.bytes.explicit == 0) self.viewport = .active; } -/// Returns the effective scrollback row limit. At least one standard page -/// worth of rows is permitted so enforcement never has to split a page. +/// Change the maximum number of physical scrollback rows at runtime. Null +/// removes the explicit line limit. /// -/// Like `maxSize`, this is a heuristic. An unusually large page which contains -/// both history and active rows can temporarily exceed this value because only -/// complete historical pages are pruned. -fn maxLines(self: *const PageList) usize { - return @max(self.explicit_max_lines, self.min_max_lines); -} +/// Lowering the limit immediately removes eligible complete historical pages. +/// The effective limit always permits at least one standard page of history, +/// and a page which overlaps the active area is never split for enforcement. +pub fn setMaxLines(self: *PageList, max: ?usize) void { + defer self.assertIntegrity(); -/// Prune complete historical pages until the line limit is satisfied or the -/// oldest remaining page overlaps the active area. -fn enforceMaxLines(self: *PageList) void { - if (self.explicit_max_lines == std.math.maxInt(usize)) return; - - // A partially constructed clone can temporarily contain fewer rows than - // its active area. It has no scrollback to prune. - if (self.total_rows <= self.rows) return; - - // If we're lower then the limit we're good. - const max_lines = self.maxLines(); - if (self.total_rows - self.rows <= max_lines) return; - - // Removing pages changes compression. - self.page_compression.markActivity(); - - // Accumulate the row delta so viewport offsets are fixed up once - // after all eligible pages have been removed. - var removed: usize = 0; - while (self.total_rows - self.rows > max_lines) { - const first = self.pages.first.?; - - // The page containing the active top may also contain history. Keep - // that boundary page whole even if its history exceeds the heuristic. - if (first == self.getTopLeft(.active).node) break; - - const first_rows = first.rows(); - - // Automatic pruning invalidates the content represented by pins in - // the removed page. erasePage remaps them to the next page below but - // only the line-limit caller knows that their original content is - // gone, so mark them as garbage here. - for (self.tracked_pins.keys()) |p| { - if (p.node == first) p.garbage = true; - } - - // erasePage updates the list, pin targets, and byte accounting. - // Row accounting belongs to the caller because erasePage is also used - // by paths that already adjusted total_rows. - self.erasePage(first); - self.total_rows -= first_rows; - removed += first_rows; - } - - // Reconcile viewport mode and cached row offsets with the combined prefix - // removal only after every page and pin points into the final list. - if (removed > 0) self.fixupViewport(removed); + self.limits.set(.lines, max); + self.limits.enforce(self, .lines); } /// Grow the active area by exactly one row. @@ -3679,7 +3544,7 @@ pub fn grow(self: *PageList) Allocator.Error!?*List.Node { // Growing inside the last page moves the active boundary without // allocating; that alone can make the first page wholly historical. - self.enforceMaxLines(); + self.limits.enforce(self, .lines); return null; } @@ -3699,7 +3564,7 @@ pub fn grow(self: *PageList) Allocator.Error!?*List.Node { // initial allocation. if (self.pages.first != null and self.pages.first != self.pages.last and - self.page_size + PagePool.item_size > self.maxSize()) + self.page_size + PagePool.item_size > self.limits.max(.bytes)) prune: { const first = self.pages.popFirst().?; assert(first != last); @@ -3784,7 +3649,7 @@ pub fn grow(self: *PageList) Allocator.Error!?*List.Node { // Byte-limit recycling may leave history above the independent line // limit, so enforce it after the recycled page becomes the new tail. - self.enforceMaxLines(); + self.limits.enforce(self, .lines); return first; } @@ -3805,7 +3670,7 @@ pub fn grow(self: *PageList) Allocator.Error!?*List.Node { // Appending a page can cross the line limit and can make the oldest // active-boundary page wholly historical. - self.enforceMaxLines(); + self.limits.enforce(self, .lines); return next_node; } @@ -6346,6 +6211,190 @@ fn markDirty(self: *PageList, pt: point.Point) void { self.pin(pt).?.markDirty(); } +/// Runtime-configurable byte and line limits for a PageList. +const Limits = struct { + bytes: Limit, + lines: Limit, + + /// The limit keys. + pub const Key = std.meta.FieldEnum(Limits); + + pub const Limit = struct { + /// Explicit is the specified maximum value for this limit + /// by the user or maxInt otherwise. + explicit: usize = std.math.maxInt(usize), + + /// Min is the minimum valid maximum for this entry, so if + /// explicit is lower than this then min wins. + min: usize, + }; + + /// Return unlimited limits with minimums for the given PageList size. + pub fn init(cols: size.CellCountInt, rows: size.CellCountInt) Limits { + return .{ + .bytes = .{ .min = minMaxSize(cols, rows) }, + .lines = .{ .min = minMaxLines(cols) }, + }; + } + + /// Set an explicit limit. Null means unlimited. This does not enforce the + /// new value automatically; the caller must still call `enforce`. + pub fn set( + self: *Limits, + comptime key: Key, + value: ?usize, + ) void { + switch (key) { + .bytes => self.bytes.explicit = value orelse std.math.maxInt(usize), + .lines => self.lines.explicit = value orelse std.math.maxInt(usize), + } + } + + /// Recalculate the effective minimums for a new PageList size. + /// + /// This must be called whenever either PageList dimension changes so the + /// effective byte and line limits remain valid for the new size. + pub fn resize( + self: *Limits, + cols: size.CellCountInt, + rows: size.CellCountInt, + ) void { + self.bytes.min = minMaxSize(cols, rows); + self.lines.min = minMaxLines(cols); + } + + /// Return the effective maximum for a limit. + pub fn max(self: *const Limits, key: Key) usize { + return switch (key) { + .bytes => @max(self.bytes.explicit, self.bytes.min), + .lines => @max(self.lines.explicit, self.lines.min), + }; + } + + /// Whether the given limit is currently exceeded. Both limits are + /// heuristics: complete historical pages are the smallest unit that + /// enforcement removes. + pub fn exceeded( + self: *const Limits, + pagelist: *const PageList, + limit: Key, + ) bool { + return switch (limit) { + .bytes => pagelist.page_size > self.max(.bytes), + .lines => pagelist.total_rows > pagelist.rows and + pagelist.total_rows - pagelist.rows > self.max(.lines), + }; + } + + /// Prune complete historical pages until the selected limit is satisfied + /// or the oldest remaining page overlaps the active area. + pub fn enforce( + self: *const Limits, + pagelist: *PageList, + key: Key, + ) void { + if (!self.exceeded(pagelist, key)) return; + + // A partially constructed clone can temporarily contain fewer rows than + // its active area. It has no scrollback to prune. + if (pagelist.total_rows <= pagelist.rows) return; + + // Accumulate the row delta so viewport offsets are fixed up once + // after all eligible pages have been removed. + var removed: usize = 0; + while (self.exceeded(pagelist, key)) { + const first = pagelist.pages.first.?; + + // The page containing the active top may also contain history. Keep + // that boundary page whole even if its history exceeds the heuristic. + if (first == pagelist.getTopLeft(.active).node) break; + + if (removed == 0) pagelist.page_compression.markActivity(); + + const first_rows = first.rows(); + + // Automatic pruning invalidates the content represented by pins in + // the removed page. erasePage remaps them to the next page below but + // only the enforcing caller knows that their original content is gone, + // so mark them as garbage here. + for (pagelist.tracked_pins.keys()) |p| { + if (p.node == first) p.garbage = true; + } + + // erasePage updates the list, pin targets, and byte accounting. + // Row accounting belongs to the caller because erasePage is also used + // by paths that already adjusted total_rows. + pagelist.erasePage(first); + pagelist.total_rows -= first_rows; + removed += first_rows; + } + + // Reconcile viewport mode and cached row offsets with the combined prefix + // removal only after every page and pin points into the final list. + if (removed > 0) pagelist.fixupViewport(removed); + } + + /// Returns the minimum valid "max size" for a given number of rows and cols + /// such that we can fit the active area AND at least two pages. Note we + /// need the two pages for algorithms to work properly (such as grow) but + /// we don't need to fit double the active area. + /// + /// This min size may not be totally correct in the case that a large + /// number of other dimensions makes our row size in a page very small. + /// But this gives us a nice fast heuristic for determining min/max size. + /// Therefore, if the page size is violated you should always also verify + /// that we have enough space for the active area. + fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) usize { + // Invariant required to ensure our divCeil below cannot overflow. + comptime { + const max_rows = std.math.maxInt(size.CellCountInt); + _ = std.math.divCeil(usize, max_rows, 1) catch unreachable; + } + + // Get our capacity to fit our rows. If the cols are too big, it may + // force less rows than we want meaning we need more than one page to + // represent a viewport. + const cap = initialCapacity(cols); + + // Calculate the number of standard sized pages we need to represent + // an active area. + const pages_exact = if (cap.rows >= rows) 1 else std.math.divCeil( + usize, + rows, + cap.rows, + ) catch { + // Not possible: + // - initialCapacity guarantees at least 1 row + // - numerator/denominator can't overflow because of comptime check above + unreachable; + }; + + // We always need at least one page extra so that we + // can fit partial pages to spread our active area across two pages. + // Even for caps that can't fit all rows in a single page, we add one + // because the most extra space we need at any given time is only + // the partial amount of one page. + const pages = pages_exact + 1; + assert(pages >= 2); + + // log.debug("minMaxSize cols={} rows={} cap={} pages={}", .{ + // cols, + // rows, + // cap, + // pages, + // }); + + return PagePool.item_size * pages; + } + + /// Returns the minimum line limit for a given column count. Line limits are + /// page-granular, so we always permit at least one standard page worth of + /// scrollback rows. + fn minMaxLines(cols: size.CellCountInt) usize { + return initialCapacity(cols).rows; + } +}; + /// Represents an exact x/y coordinate within the screen. This is called /// a "pin" because it is a fixed point within the pagelist direct to /// a specific page pointer and memory offset. The benefit is that this @@ -9755,6 +9804,216 @@ test "PageList Cell screenPoint supports long scrollback" { } }, cell.screenPoint()); } +test "PageList set max bytes prunes immediately and can be raised" { + const testing = std.testing; + const cols: size.CellCountInt = 80; + const page_rows: usize = initialCapacity(cols).rows; + + var s = try init(testing.allocator, .{ + .cols = cols, + .rows = 1, + .max_size = null, + }); + defer s.deinit(); + + // Build four complete pages of history followed by the active row. + try s.growRows(4 * page_rows); + try testing.expectEqual(@as(usize, 5), s.totalPages()); + + const removed = s.pages.first.?; + const retained = s.pages.last.?.prev.?; + const removed_pin = try s.trackPin(.{ .node = removed }); + defer s.untrackPin(removed_pin); + const retained_pin = try s.trackPin(.{ .node = retained }); + defer s.untrackPin(retained_pin); + + s.scroll(.{ .pin = retained_pin.* }); + try testing.expectEqual(3 * page_rows, s.scrollbar().offset); + + // The active-area minimum is two pages. Lowering below that immediately + // removes all older complete historical pages. + s.setMaxBytes(PagePool.item_size); + try testing.expectEqual(PagePool.item_size, s.limits.bytes.explicit); + try testing.expectEqual(2 * PagePool.item_size, s.limits.max(.bytes)); + try testing.expectEqual(s.limits.max(.bytes), s.page_size); + try testing.expectEqual(@as(usize, 2), s.totalPages()); + try testing.expectEqual(page_rows, s.total_rows - s.rows); + try testing.expectEqual(retained, s.pages.first.?); + try testing.expectEqual(retained, removed_pin.node); + try testing.expect(removed_pin.garbage); + try testing.expectEqual(retained, retained_pin.node); + try testing.expect(!retained_pin.garbage); + try testing.expectEqual(@as(usize, 0), s.scrollbar().offset); + + // Raising the limit doesn't allocate or otherwise change retained data, + // but subsequent growth can exceed the previous effective limit. + const limited_size = s.page_size; + const limited_rows = s.total_rows; + s.setMaxBytes(8 * PagePool.item_size); + try testing.expectEqual(limited_size, s.page_size); + try testing.expectEqual(limited_rows, s.total_rows); + try s.growRows(2 * page_rows); + try testing.expect(s.page_size > limited_size); + + // Null restores unlimited growth and likewise preserves current data. + const raised_size = s.page_size; + const raised_rows = s.total_rows; + s.setMaxBytes(null); + try testing.expectEqual( + std.math.maxInt(usize), + s.limits.bytes.explicit, + ); + try testing.expectEqual(raised_size, s.page_size); + try testing.expectEqual(raised_rows, s.total_rows); + try s.growRows(5 * page_rows); + try testing.expect(s.page_size > 8 * PagePool.item_size); +} + +test "PageList set max bytes zero preserves active boundary" { + const testing = std.testing; + + var s = try init(testing.allocator, .{ + .cols = 80, + .rows = 1, + .max_size = null, + }); + defer s.deinit(); + + // Make the sole page larger than the effective zero-byte limit. Its first + // row will be history, but the same indivisible page also contains active. + while (s.page_size <= s.limits.bytes.min) { + _ = try s.increaseCapacity(s.pages.first.?, .grapheme_bytes); + } + _ = try s.grow(); + try testing.expectEqual(@as(usize, 1), s.totalPages()); + try testing.expectEqual(s.pages.first.?, s.getTopLeft(.active).node); + try testing.expect(s.getTopLeft(.active).y > 0); + + s.scroll(.top); + try testing.expect(s.viewport == .top); + + s.setMaxBytes(0); + try testing.expectEqual(@as(usize, 0), s.limits.bytes.explicit); + try testing.expect(s.page_size > s.limits.max(.bytes)); + try testing.expectEqual(@as(usize, 1), s.totalPages()); + try testing.expect(s.viewport == .active); + try testing.expectEqual(Scrollbar{ + .total = s.rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + + // No-scrollback mode cannot be moved back into the retained boundary row. + s.scroll(.top); + try testing.expect(s.viewport == .active); +} + +test "PageList set max lines prunes immediately and can be raised" { + const testing = std.testing; + const cols: size.CellCountInt = 80; + const page_rows: usize = initialCapacity(cols).rows; + const lowered_lines = page_rows + page_rows / 2; + + var s = try init(testing.allocator, .{ + .cols = cols, + .rows = 1, + .max_size = null, + .max_lines = null, + }); + defer s.deinit(); + + try s.growRows(4 * page_rows); + try testing.expectEqual(@as(usize, 5), s.totalPages()); + + const removed = s.pages.first.?; + const retained = s.pages.last.?.prev.?; + const removed_pin = try s.trackPin(.{ .node = removed }); + defer s.untrackPin(removed_pin); + const retained_pin = try s.trackPin(.{ .node = retained }); + defer s.untrackPin(retained_pin); + + s.scroll(.{ .pin = retained_pin.* }); + try testing.expectEqual(3 * page_rows, s.scrollbar().offset); + + // Whole-page enforcement undershoots a non-page-aligned line limit. + s.setMaxLines(lowered_lines); + try testing.expectEqual(lowered_lines, s.limits.lines.explicit); + try testing.expectEqual(lowered_lines, s.limits.max(.lines)); + try testing.expectEqual(page_rows, s.total_rows - s.rows); + try testing.expectEqual(@as(usize, 2), s.totalPages()); + try testing.expectEqual(retained, s.pages.first.?); + try testing.expectEqual(retained, removed_pin.node); + try testing.expect(removed_pin.garbage); + try testing.expectEqual(retained, retained_pin.node); + try testing.expect(!retained_pin.garbage); + try testing.expectEqual(@as(usize, 0), s.scrollbar().offset); + + const limited_size = s.page_size; + const limited_rows = s.total_rows; + s.setMaxLines(4 * page_rows); + try testing.expectEqual(limited_size, s.page_size); + try testing.expectEqual(limited_rows, s.total_rows); + try s.growRows(2 * page_rows); + try testing.expect(s.total_rows - s.rows > lowered_lines); + + const raised_size = s.page_size; + const raised_rows = s.total_rows; + s.setMaxLines(null); + try testing.expectEqual( + std.math.maxInt(usize), + s.limits.lines.explicit, + ); + try testing.expectEqual(raised_size, s.page_size); + try testing.expectEqual(raised_rows, s.total_rows); + try s.growRows(3 * page_rows); + try testing.expect(s.total_rows - s.rows > 4 * page_rows); +} + +test "PageList set max limits remain independent" { + const testing = std.testing; + const cols: size.CellCountInt = 80; + const page_rows: usize = initialCapacity(cols).rows; + const byte_limit = 3 * PagePool.item_size; + const line_limit = page_rows / 2; + + var s = try init(testing.allocator, .{ + .cols = cols, + .rows = 1, + .max_size = null, + .max_lines = null, + }); + defer s.deinit(); + + try s.growRows(4 * page_rows); + + // The byte setter leaves the line limit unlimited. + s.setMaxBytes(byte_limit); + try testing.expectEqual(byte_limit, s.limits.bytes.explicit); + try testing.expectEqual( + std.math.maxInt(usize), + s.limits.lines.explicit, + ); + try testing.expectEqual(@as(usize, 3), s.totalPages()); + try testing.expectEqual(2 * page_rows, s.total_rows - s.rows); + + // The smaller runtime line limit prunes one more complete page without + // changing the configured byte limit. Its effective value is raised to + // the existing one-page minimum. + s.setMaxLines(line_limit); + try testing.expectEqual(byte_limit, s.limits.bytes.explicit); + try testing.expectEqual(line_limit, s.limits.lines.explicit); + try testing.expectEqual(page_rows, s.limits.max(.lines)); + try testing.expectEqual(@as(usize, 2), s.totalPages()); + try testing.expectEqual(page_rows, s.total_rows - s.rows); + + // Removing only the line limit leaves byte enforcement in effect. + s.setMaxLines(null); + try s.growRows(3 * page_rows); + try testing.expectEqual(byte_limit, s.page_size); + try testing.expectEqual(@as(usize, 3), s.totalPages()); + try testing.expect(s.total_rows - s.rows > page_rows); +} + test "PageList max lines uses one-page minimum" { const testing = std.testing; const cols: size.CellCountInt = 80; @@ -9767,7 +10026,7 @@ test "PageList max lines uses one-page minimum" { }); defer s.deinit(); - try testing.expectEqual(page_rows, s.maxLines()); + try testing.expectEqual(page_rows, s.limits.max(.lines)); // The requested limit is below one page, so a complete page of history // remains valid. @@ -9803,7 +10062,7 @@ test "PageList max lines does not round larger limits" { }); defer s.deinit(); - try testing.expectEqual(max_lines, s.maxLines()); + try testing.expectEqual(max_lines, s.limits.max(.lines)); try s.growRows(max_lines); try testing.expectEqual(max_lines, s.total_rows - s.rows); @@ -9854,9 +10113,11 @@ test "PageList max lines and max size enforce the smaller limit" { defer s.deinit(); try s.growRows(4 * page_rows); - try testing.expect(s.total_rows - s.rows <= s.maxLines()); + try testing.expect( + s.total_rows - s.rows <= s.limits.max(.lines), + ); try testing.expect(s.totalPages() <= 2); - try testing.expect(s.page_size < s.maxSize()); + try testing.expect(s.page_size < s.limits.max(.bytes)); } // A two-page byte limit prunes before the larger line limit is reached. @@ -9870,8 +10131,10 @@ test "PageList max lines and max size enforce the smaller limit" { defer s.deinit(); try s.growRows(2 * page_rows); - try testing.expect(s.total_rows - s.rows < s.maxLines()); - try testing.expectEqual(s.maxSize(), s.page_size); + try testing.expect( + s.total_rows - s.rows < s.limits.max(.lines), + ); + try testing.expectEqual(s.limits.max(.bytes), s.page_size); } } @@ -9903,7 +10166,10 @@ test "PageList max lines applies to resize and clone" { const new_cols: size.CellCountInt = cols + 1; try s.resize(.{ .cols = new_cols, .reflow = true }); - try testing.expectEqual(minMaxLines(new_cols), s.min_max_lines); + try testing.expectEqual( + Limits.minMaxLines(new_cols), + s.limits.lines.min, + ); // Exercise the same active-row shrink through the reflow path. Reflow // completes before the newly historical complete page is pruned. @@ -9927,9 +10193,13 @@ test "PageList max lines applies to resize and clone" { .rows = 1, .reflow = true, }); - try testing.expectEqual(minMaxLines(new_cols), reflowed.min_max_lines); + try testing.expectEqual( + Limits.minMaxLines(new_cols), + reflowed.limits.lines.min, + ); try testing.expect( - reflowed.total_rows - reflowed.rows <= reflowed.maxLines() or + reflowed.total_rows - reflowed.rows <= + reflowed.limits.max(.lines) or reflowed.pages.first.? == reflowed.getTopLeft(.active).node, ); @@ -9940,13 +10210,11 @@ test "PageList max lines applies to resize and clone" { }); defer cloned.deinit(); - try testing.expectEqual(s.explicit_max_lines, cloned.explicit_max_lines); - try testing.expectEqual(s.min_max_lines, cloned.min_max_lines); - try testing.expectEqual(s.maxLines(), cloned.maxLines()); + try testing.expectEqual(s.limits, cloned.limits); - try cloned.growRows(2 * cloned.maxLines()); + try cloned.growRows(2 * cloned.limits.max(.lines)); try testing.expect( - cloned.total_rows - cloned.rows <= cloned.maxLines() or + cloned.total_rows - cloned.rows <= cloned.limits.max(.lines) or cloned.pages.first.? == cloned.getTopLeft(.active).node, ); } @@ -17142,7 +17410,7 @@ test "PageList grow reuses non-standard page without leak" { try testing.expect(s.pages.first != s.pages.last); // Continue growing until we exceed max_size AND the last page is full - while (s.page_size + PagePool.item_size <= s.maxSize() or + while (s.page_size + PagePool.item_size <= s.limits.max(.bytes) or s.pages.last.?.rows() < s.pages.last.?.capacity().rows) { _ = try s.grow(); @@ -17242,7 +17510,9 @@ test "PageList grow non-standard page prune protection" { // Verify prune path conditions are met try testing.expect(s.pages.first != s.pages.last); - try testing.expect(s.page_size + PagePool.item_size > s.maxSize()); + try testing.expect( + s.page_size + PagePool.item_size > s.limits.max(.bytes), + ); try testing.expect(s.totalRows() >= s.rows); // Verify last page is at capacity (so grow must prune or allocate new) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index b098f0923..1acb30c16 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -3796,9 +3796,9 @@ test "Screen forwards optional scrollback limits" { try testing.expectEqual( std.math.maxInt(usize), - s.pages.explicit_max_size, + s.pages.limits.bytes.explicit, ); - try testing.expectEqual(max_lines, s.pages.explicit_max_lines); + try testing.expectEqual(max_lines, s.pages.limits.lines.explicit); try testing.expect(!s.no_scrollback); } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index d8442335f..cfaf86cea 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -442,6 +442,29 @@ pub fn gpa(self: *Terminal) Allocator { return self.screens.active.alloc; } +/// Change the primary screen's maximum scrollback allocation in bytes. +/// +/// Null removes the byte limit and zero disables scrollback. Disabling +/// scrollback also immediately erases retained history and changes future +/// scrolling to use the no-scrollback path. The alternate screen is +/// intentionally unaffected because it never retains scrollback. +pub fn setScrollbackMaxBytes(self: *Terminal, max: ?usize) void { + const primary = self.screens.get(.primary).?; + primary.pages.setMaxBytes(max); + primary.no_scrollback = max == 0; + + if (primary.no_scrollback) primary.eraseHistory(null); +} + +/// Change the primary screen's maximum number of physical scrollback lines. +/// +/// Null removes the line limit. The alternate screen is intentionally +/// unaffected because it never retains scrollback. +pub fn setScrollbackMaxLines(self: *Terminal, max: ?usize) void { + const primary = self.screens.get(.primary).?; + primary.pages.setMaxLines(max); +} + /// Print UTF-8 encoded string to the terminal. pub fn printString(self: *Terminal, str: []const u8) !void { const view = try std.unicode.Utf8View.init(str); @@ -3900,14 +3923,138 @@ test "Terminal forwards optional scrollback limits" { try testing.expectEqual( std.math.maxInt(usize), - t.screens.active.pages.explicit_max_size, + t.screens.active.pages.limits.bytes.explicit, ); try testing.expectEqual( max_lines, - t.screens.active.pages.explicit_max_lines, + t.screens.active.pages.limits.lines.explicit, ); } +test "Terminal setScrollbackMaxBytes" { + var t = try init(testing.io, testing.allocator, .{ + .cols = 80, + .rows = 3, + .max_scrollback_bytes = null, + }); + defer t.deinit(testing.allocator); + + const primary = t.screens.get(.primary).?; + const page_rows: usize = primary.pages.pages.first.?.capacity().rows; + + // Build several complete pages of history so lowering the byte limit has + // existing allocations to prune immediately. + for (0..4 * page_rows) |_| try t.linefeed(); + const old_page_size = primary.pages.page_size; + t.setScrollbackMaxBytes(1); + try testing.expectEqual(@as(usize, 1), primary.pages.limits.bytes.explicit); + try testing.expect(primary.pages.page_size < old_page_size); + try testing.expect( + primary.pages.page_size <= primary.pages.limits.max(.bytes), + ); + try testing.expect(!primary.no_scrollback); + + // Zero switches Screen behavior as well as PageList accounting, discards + // all retained history, and prevents future linefeeds from recreating it. + t.setScrollbackMaxBytes(0); + try testing.expectEqual(@as(usize, 0), primary.pages.limits.bytes.explicit); + try testing.expect(primary.no_scrollback); + try testing.expectEqual( + @as(usize, primary.pages.rows), + primary.pages.total_rows, + ); + try testing.expect(primary.pages.viewport == .active); + + for (0..page_rows) |_| try t.linefeed(); + try testing.expectEqual( + @as(usize, primary.pages.rows), + primary.pages.total_rows, + ); + + // Re-enabling unlimited scrollback affects subsequent output. + t.setScrollbackMaxBytes(null); + try testing.expectEqual( + std.math.maxInt(usize), + primary.pages.limits.bytes.explicit, + ); + try testing.expect(!primary.no_scrollback); + for (0..page_rows) |_| try t.linefeed(); + try testing.expect(primary.pages.total_rows > primary.pages.rows); +} + +test "Terminal setScrollbackMaxLines" { + var t = try init(testing.io, testing.allocator, .{ + .cols = 80, + .rows = 3, + .max_scrollback_bytes = null, + .max_scrollback_lines = null, + }); + defer t.deinit(testing.allocator); + + const primary = t.screens.get(.primary).?; + const page_rows: usize = primary.pages.pages.first.?.capacity().rows; + + for (0..4 * page_rows) |_| try t.linefeed(); + const old_total_rows = primary.pages.total_rows; + t.setScrollbackMaxLines(page_rows); + try testing.expectEqual( + page_rows, + primary.pages.limits.lines.explicit, + ); + try testing.expect(primary.pages.total_rows < old_total_rows); + try testing.expect( + !primary.pages.limits.exceeded(&primary.pages, .lines), + ); + + const limited_total_rows = primary.pages.total_rows; + t.setScrollbackMaxLines(null); + try testing.expectEqual( + std.math.maxInt(usize), + primary.pages.limits.lines.explicit, + ); + try testing.expectEqual(limited_total_rows, primary.pages.total_rows); + for (0..3 * page_rows) |_| try t.linefeed(); + try testing.expect( + primary.pages.total_rows - primary.pages.rows > page_rows, + ); +} + +test "Terminal setScrollback only affects primary screen" { + var t = try init(testing.io, testing.allocator, .{ + .cols = 80, + .rows = 3, + }); + defer t.deinit(testing.allocator); + + _ = try t.switchScreen(.alternate); + const primary = t.screens.get(.primary).?; + const alternate = t.screens.get(.alternate).?; + + t.setScrollbackMaxBytes(123); + t.setScrollbackMaxLines(456); + + try testing.expectEqual( + @as(usize, 123), + primary.pages.limits.bytes.explicit, + ); + try testing.expectEqual( + @as(usize, 456), + primary.pages.limits.lines.explicit, + ); + try testing.expect(!primary.no_scrollback); + + try testing.expectEqual( + @as(usize, 0), + alternate.pages.limits.bytes.explicit, + ); + try testing.expectEqual( + std.math.maxInt(usize), + alternate.pages.limits.lines.explicit, + ); + try testing.expect(alternate.no_scrollback); + try testing.expectEqual(alternate, t.screens.active); +} + test "Terminal: resize resets synchronized output" { const alloc = testing.allocator; const io_impl = testing.io; @@ -4325,7 +4472,7 @@ pub fn switchScreen(self: *Terminal, key: ScreenSet.Key) !?*Screen { .cols = self.cols, .rows = self.rows, .max_scrollback_bytes = switch (key) { - .primary => primary.pages.explicit_max_size, + .primary => primary.pages.limits.bytes.explicit, .alternate => 0, }, diff --git a/src/terminal/c/formatter.zig b/src/terminal/c/formatter.zig index 5b4504c8c..9b3c143a4 100644 --- a/src/terminal/c/formatter.zig +++ b/src/terminal/c/formatter.zig @@ -223,7 +223,8 @@ test "terminal_new/free" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -258,7 +259,8 @@ test "format plain" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -284,7 +286,8 @@ test "format reflects terminal changes" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -316,7 +319,8 @@ test "format null returns required size" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -348,7 +352,8 @@ test "format buffer too small" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -381,7 +386,8 @@ test "format vt" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -408,7 +414,8 @@ test "format plain with selection" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -452,7 +459,8 @@ test "format html" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); diff --git a/src/terminal/c/grid_ref.zig b/src/terminal/c/grid_ref.zig index 946ca8a1e..20f49be91 100644 --- a/src/terminal/c/grid_ref.zig +++ b/src/terminal/c/grid_ref.zig @@ -205,7 +205,8 @@ test "grid_ref_hyperlink_uri no hyperlink" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -228,7 +229,8 @@ test "grid_ref_hyperlink_uri with hyperlink" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(terminal); diff --git a/src/terminal/c/grid_ref_tracked.zig b/src/terminal/c/grid_ref_tracked.zig index 60091715f..475a8e430 100644 --- a/src/terminal/c/grid_ref_tracked.zig +++ b/src/terminal/c/grid_ref_tracked.zig @@ -96,7 +96,8 @@ test "tracked_grid_ref snapshots after terminal scroll" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -128,7 +129,8 @@ test "tracked_grid_ref reports no value after reset" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -154,7 +156,8 @@ test "tracked_grid_ref reports no value after alternate screen reset" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -194,7 +197,8 @@ test "tracked_grid_ref reports no value after terminal free" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); terminal_c.vt_write(terminal, "A", 1); diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig index f270cb15a..f0aec36e7 100644 --- a/src/terminal/c/key_encode.zig +++ b/src/terminal/c/key_encode.zig @@ -257,7 +257,8 @@ test "setopt_from_terminal" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -283,7 +284,8 @@ test "setopt_from_terminal null" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); setopt_from_terminal(null, t); diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index 626cc7703..310b79cff 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -659,7 +659,8 @@ test "placement_iterator next on empty storage" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -688,7 +689,8 @@ test "placement_iterator get before next returns invalid" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -719,7 +721,8 @@ test "placement_iterator with transmit and display" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -772,7 +775,8 @@ test "placement_iterator with multiple placements" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -829,7 +833,8 @@ test "placement_iterator_set layer filter" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -919,7 +924,8 @@ test "image_get_handle returns null for missing id" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -940,7 +946,8 @@ test "image_get_handle and image_get with transmitted image" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -990,7 +997,8 @@ test "placement_rect with transmit and display" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -1051,7 +1059,8 @@ test "placement_pixel_size with transmit and display" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -1106,7 +1115,8 @@ test "placement_grid_size with transmit and display" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -1160,7 +1170,8 @@ test "placement_viewport_pos with transmit and display" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -1205,7 +1216,8 @@ test "placement_viewport_pos fully off-screen above" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 5, .max_scrollback = 100 }, + 80, + 5, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20)); @@ -1244,7 +1256,8 @@ test "placement_viewport_pos top off-screen" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 5, .max_scrollback = 100 }, + 80, + 5, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20)); @@ -1287,7 +1300,8 @@ test "placement_viewport_pos bottom off-screen" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 5, .max_scrollback = 0 }, + 80, + 5, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20)); @@ -1327,7 +1341,8 @@ test "placement_viewport_pos top and bottom off-screen" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 5, .max_scrollback = 100 }, + 80, + 5, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20)); @@ -1378,7 +1393,8 @@ test "placement_source_rect defaults to full image" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); @@ -1417,7 +1433,8 @@ test "placement_source_rect with explicit source rect" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); @@ -1461,7 +1478,8 @@ test "placement_source_rect clamps to image bounds" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); @@ -1522,7 +1540,8 @@ test "placement_render_info returns all fields" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); @@ -1561,7 +1580,8 @@ test "placement_render_info off-screen sets viewport_visible false" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 5, .max_scrollback = 100 }, + 80, + 5, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20)); @@ -1610,7 +1630,8 @@ test "image_get_multi success" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); @@ -1664,7 +1685,8 @@ test "placement_get_multi success" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20)); @@ -1722,7 +1744,8 @@ test "storage generation via get" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -1774,7 +1797,8 @@ test "image generation detects same-sized retransmission" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -1835,7 +1859,8 @@ test "image generation via image_get_multi" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -1871,7 +1896,8 @@ test "image compression and format always report decoded data" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -1917,7 +1943,8 @@ test "generation never recurs across resets and screen switches" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); diff --git a/src/terminal/c/mouse_encode.zig b/src/terminal/c/mouse_encode.zig index 951e80295..2586a57ad 100644 --- a/src/terminal/c/mouse_encode.zig +++ b/src/terminal/c/mouse_encode.zig @@ -324,7 +324,8 @@ test "setopt_from_terminal" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); @@ -346,7 +347,8 @@ test "setopt_from_terminal null" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer terminal_c.free(t); setopt_from_terminal(null, t); diff --git a/src/terminal/c/render.zig b/src/terminal/c/render.zig index 8ca58475f..e70e7a4b7 100644 --- a/src/terminal/c/render.zig +++ b/src/terminal/c/render.zig @@ -827,11 +827,8 @@ test "render: begin/end update" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 10, - .rows = 3, - .max_scrollback = 10_000, - }, + 10, + 3, )); defer terminal_c.free(terminal); @@ -952,11 +949,8 @@ test "render: row iterator new/free" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1008,11 +1002,8 @@ test "render: row get invalid data" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1047,11 +1038,8 @@ test "render: row set before iteration" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1081,11 +1069,8 @@ test "render: row get before iteration" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1115,11 +1100,8 @@ test "render: row get/set dirty" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1173,11 +1155,8 @@ test "render: row get selection" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 10, - .rows = 3, - .max_scrollback = 10_000, - }, + 10, + 3, )); defer terminal_c.free(terminal); @@ -1227,11 +1206,8 @@ test "render: row cells get selected" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 10, - .rows = 3, - .max_scrollback = 10_000, - }, + 10, + 3, )); defer terminal_c.free(terminal); @@ -1310,11 +1286,8 @@ test "render: row cells get has_styling" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 10, - .rows = 3, - .max_scrollback = 10_000, - }, + 10, + 3, )); defer terminal_c.free(terminal); @@ -1390,7 +1363,8 @@ test "render: row cells get graphemes utf8" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 10, .rows = 3, .max_scrollback = 10_000 }, + 10, + 3, )); defer terminal_c.free(terminal); @@ -1447,7 +1421,8 @@ test "render: row cells get graphemes utf8" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 10, .rows = 3, .max_scrollback = 10_000 }, + 10, + 3, )); defer terminal_c.free(terminal); @@ -1493,11 +1468,8 @@ test "render: row iterator next" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1543,11 +1515,8 @@ test "render: update" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1576,11 +1545,8 @@ test "render: colors get" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1618,11 +1584,8 @@ test "render: row cells bg_color no background" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1668,11 +1631,8 @@ test "render: row cells bg_color from style" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1720,11 +1680,8 @@ test "render: row cells bg_color from content tag" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1774,11 +1731,8 @@ test "render: row cells fg_color no foreground" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1824,11 +1778,8 @@ test "render: row cells fg_color from style" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1876,11 +1827,8 @@ test "render: colors get supports truncated sized struct" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1911,7 +1859,8 @@ test "render: get_multi success" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1947,7 +1896,8 @@ test "render: row_get_multi success" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(terminal); @@ -1990,7 +1940,8 @@ test "render: row_cells_get_multi success" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(terminal); diff --git a/src/terminal/c/selection.zig b/src/terminal/c/selection.zig index 7f0af205f..6dcfcc070 100644 --- a/src/terminal/c/selection.zig +++ b/src/terminal/c/selection.zig @@ -397,7 +397,8 @@ test "selection_format_alloc uses active selection" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -457,7 +458,8 @@ test "selection_format_buf uses provided selection" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); @@ -513,7 +515,8 @@ test "selection_format_alloc returns no_value without active selection" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer terminal_c.free(t); diff --git a/src/terminal/c/selection_gesture.zig b/src/terminal/c/selection_gesture.zig index 20d7386b6..46253057c 100644 --- a/src/terminal/c/selection_gesture.zig +++ b/src/terminal/c/selection_gesture.zig @@ -752,7 +752,8 @@ test "selection gesture lifecycle and get" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -788,7 +789,8 @@ test "selection gesture get_multi" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -832,7 +834,8 @@ test "selection gesture get_multi returns first failing index" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -944,7 +947,8 @@ test "selection gesture event applies press" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -991,7 +995,8 @@ test "selection gesture event press requires ref" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1019,7 +1024,8 @@ test "selection gesture event null output still reports no selection" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1053,7 +1059,8 @@ test "selection gesture event applies release" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1104,7 +1111,8 @@ test "selection gesture release without ref marks dragged" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1151,7 +1159,8 @@ test "selection gesture event applies drag" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1223,7 +1232,8 @@ test "selection gesture drag requires ref and geometry" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1267,7 +1277,8 @@ test "selection gesture event applies autoscroll tick" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1350,7 +1361,8 @@ test "selection gesture autoscroll tick requires viewport and geometry" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1389,7 +1401,8 @@ test "selection gesture event applies deep press" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); @@ -1444,7 +1457,8 @@ test "selection gesture deep press without active anchor returns no value" { try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &terminal, - .{ .cols = 5, .rows = 2, .max_scrollback = 10_000 }, + 5, + 2, )); defer terminal_c.free(terminal); diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 6a680c904..c2a1ae333 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -307,13 +307,6 @@ pub fn zigTerminal(terminal_: Terminal) ?*ZigTerminal { return (terminal_ orelse return null).terminal; } -/// C: GhosttyTerminalOptions -pub const Options = extern struct { - cols: size.CellCountInt, - rows: size.CellCountInt, - max_scrollback: usize, -}; - const NewError = error{ InvalidValue, OutOfMemory, @@ -322,9 +315,10 @@ const NewError = error{ pub fn new( alloc_: ?*const CAllocator, result: *Terminal, - opts: Options, + cols: size.CellCountInt, + rows: size.CellCountInt, ) callconv(lib.calling_conv) Result { - result.* = new_(alloc_, opts) catch |err| { + result.* = new_(alloc_, cols, rows) catch |err| { result.* = null; return switch (err) { error.InvalidValue => .invalid_value, @@ -337,9 +331,10 @@ pub fn new( fn new_( alloc_: ?*const CAllocator, - opts: Options, + cols: size.CellCountInt, + rows: size.CellCountInt, ) NewError!*TerminalWrapper { - if (opts.cols == 0 or opts.rows == 0) return error.InvalidValue; + if (cols == 0 or rows == 0) return error.InvalidValue; const alloc = lib.alloc.default(alloc_); const t = alloc.create(ZigTerminal) catch @@ -363,9 +358,8 @@ fn new_( if (has_nonfailing_io) io_impl.io() else std.Io.failing, alloc, .{ - .cols = opts.cols, - .rows = opts.rows, - .max_scrollback_bytes = opts.max_scrollback, + .cols = cols, + .rows = rows, }, ); errdefer t.deinit(alloc); @@ -461,6 +455,8 @@ pub const Option = enum(c_int) { glyph_protocol = 24, pwd_changed = 25, clipboard_write = 26, + scrollback_max_bytes = 27, + scrollback_max_lines = 28, /// Input type expected for setting the option. pub fn InType(comptime self: Option) type { @@ -485,7 +481,11 @@ pub const Option = enum(c_int) { .glyph_protocol, => ?*const bool, .kitty_image_medium_temp_file => ?*const lib.String, - .apc_max_bytes, .apc_max_bytes_kitty => ?*const usize, + .apc_max_bytes, + .apc_max_bytes_kitty, + .scrollback_max_bytes, + .scrollback_max_lines, + => ?*const usize, .selection => ?*const selection_c.CSelection, .default_cursor_style => ?*const TerminalCursorStyle, .default_cursor_blink => ?*const bool, @@ -637,6 +637,12 @@ fn setTyped( const blink = if (value) |ptr| ptr.* else false; wrapper.terminal.setDefaultCursorBlink(blink); }, + .scrollback_max_bytes => wrapper.terminal.setScrollbackMaxBytes( + if (value) |ptr| ptr.* else null, + ), + .scrollback_max_lines => wrapper.terminal.setScrollbackMaxLines( + if (value) |ptr| ptr.* else null, + ), } return .success; } @@ -776,6 +782,8 @@ pub const TerminalData = enum(c_int) { selection = 31, viewport_active = 32, vt_processing_error = 33, + scrollback_max_bytes = 34, + scrollback_max_lines = 35, /// Output type expected for querying the data of the given kind. pub fn OutType(comptime self: TerminalData) type { @@ -793,7 +801,11 @@ pub const TerminalData = enum(c_int) { .scrollbar => TerminalScrollbar, .cursor_style => style_c.Style, .title, .pwd => lib.String, - .total_rows, .scrollback_rows => usize, + .total_rows, + .scrollback_rows, + .scrollback_max_bytes, + .scrollback_max_lines, + => usize, .width_px, .height_px => u32, .color_foreground, .color_background, @@ -929,6 +941,16 @@ fn getTyped( ), .viewport_active => out.* = t.screens.active.pages.viewport == .active, .vt_processing_error => out.* = wrapper.stream.handler.semantic_failure, + .scrollback_max_bytes => { + const max = t.screens.get(.primary).?.pages.limits.bytes.explicit; + if (max == std.math.maxInt(usize)) return .no_value; + out.* = max; + }, + .scrollback_max_lines => { + const max = t.screens.get(.primary).?.pages.limits.lines.explicit; + if (max == std.math.maxInt(usize)) return .no_value; + out.* = max; + }, } return .success; @@ -1027,11 +1049,8 @@ test "new/free" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); try testing.expect(t != null); @@ -1044,26 +1063,73 @@ test "new invalid value" { try testing.expectEqual(Result.invalid_value, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 0, - .rows = 24, - .max_scrollback = 10_000, - }, + 0, + 24, )); try testing.expect(t == null); try testing.expectEqual(Result.invalid_value, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 0, - .max_scrollback = 10_000, - }, + 80, + 0, )); try testing.expect(t == null); } +test "set scrollback limits" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + 80, + 3, + )); + defer free(t); + + const primary = t.?.terminal.screens.get(.primary).?; + + const max_bytes: usize = 0; + try testing.expectEqual( + Result.success, + set(t, .scrollback_max_bytes, &max_bytes), + ); + try testing.expectEqual( + @as(usize, 0), + primary.pages.limits.bytes.explicit, + ); + try testing.expect(primary.no_scrollback); + + try testing.expectEqual( + Result.success, + set(t, .scrollback_max_bytes, null), + ); + try testing.expectEqual( + std.math.maxInt(usize), + primary.pages.limits.bytes.explicit, + ); + try testing.expect(!primary.no_scrollback); + + const max_lines: usize = 12; + try testing.expectEqual( + Result.success, + set(t, .scrollback_max_lines, &max_lines), + ); + try testing.expectEqual( + max_lines, + primary.pages.limits.lines.explicit, + ); + + try testing.expectEqual( + Result.success, + set(t, .scrollback_max_lines, null), + ); + try testing.expectEqual( + std.math.maxInt(usize), + primary.pages.limits.lines.explicit, + ); +} + test "free null" { free(null); } @@ -1073,11 +1139,8 @@ test "scroll_viewport" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 5, - .rows = 2, - .max_scrollback = 10_000, - }, + 5, + 2, )); defer free(t); @@ -1135,11 +1198,8 @@ test "scroll_viewport row" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 5, - .rows = 2, - .max_scrollback = 10_000, - }, + 5, + 2, )); defer free(t); @@ -1195,11 +1255,8 @@ test "scroll_viewport row alt screen" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 5, - .rows = 2, - .max_scrollback = 10_000, - }, + 5, + 2, )); defer free(t); @@ -1248,11 +1305,8 @@ test "compression invalid arguments" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000_000, - }, + 80, + 24, )); defer free(t); @@ -1275,11 +1329,8 @@ test "compression activity and incremental scheduling" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000_000, - }, + 80, + 24, )); defer free(t); @@ -1342,11 +1393,8 @@ test "reset" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer free(t); @@ -1367,11 +1415,8 @@ test "resize" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer free(t); @@ -1389,11 +1434,8 @@ test "resize invalid value" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer free(t); @@ -1406,11 +1448,8 @@ test "resize shrinks both axes with cursor at bottom" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1430,11 +1469,8 @@ test "mode_get and mode_set" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1476,11 +1512,8 @@ test "mode_get unknown mode" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1494,11 +1527,8 @@ test "mode_set unknown mode" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1511,11 +1541,8 @@ test "vt_write" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer free(t); @@ -1531,11 +1558,8 @@ test "vt_write split escape sequence" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer free(t); @@ -1556,11 +1580,8 @@ test "vt_write split combining mark after base at right edge" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 2, - .rows = 2, - .max_scrollback = 0, - }, + 2, + 2, )); defer free(t); @@ -1579,11 +1600,8 @@ test "get cols and rows" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1600,11 +1618,8 @@ test "get cursor position" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1623,11 +1638,8 @@ test "get vt_processing_error" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1676,11 +1688,8 @@ test "get cursor_visible" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1700,11 +1709,8 @@ test "get active_screen" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1718,11 +1724,8 @@ test "get kitty_keyboard_flags" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1742,11 +1745,8 @@ test "get mouse_tracking" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1792,11 +1792,8 @@ test "get total_rows" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 10_000, - }, + 80, + 24, )); defer free(t); @@ -1810,11 +1807,8 @@ test "get scrollback_rows" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 3, - .max_scrollback = 10_000, - }, + 80, + 3, )); defer free(t); @@ -1829,16 +1823,87 @@ test "get scrollback_rows" { try testing.expectEqual(@as(usize, 2), scrollback); } +test "get configured scrollback limits" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + 80, + 3, + )); + defer free(t); + + var value: usize = undefined; + try testing.expectEqual( + Result.success, + get(t, .scrollback_max_bytes, @ptrCast(&value)), + ); + try testing.expectEqual(@as(usize, 10_000), value); + try testing.expectEqual( + Result.no_value, + get(t, .scrollback_max_lines, @ptrCast(&value)), + ); + + const max_bytes: usize = 0; + const max_lines: usize = 12; + try testing.expectEqual( + Result.success, + set(t, .scrollback_max_bytes, &max_bytes), + ); + try testing.expectEqual( + Result.success, + set(t, .scrollback_max_lines, &max_lines), + ); + try testing.expectEqual( + Result.success, + get(t, .scrollback_max_bytes, @ptrCast(&value)), + ); + try testing.expectEqual(max_bytes, value); + try testing.expectEqual( + Result.success, + get(t, .scrollback_max_lines, @ptrCast(&value)), + ); + try testing.expectEqual(max_lines, value); + + // The configured limits belong to the primary screen and remain + // readable while an alternate screen is active. + vt_write(t, "\x1b[?1049h", 8); + try testing.expectEqual( + Result.success, + get(t, .scrollback_max_bytes, @ptrCast(&value)), + ); + try testing.expectEqual(max_bytes, value); + try testing.expectEqual( + Result.success, + get(t, .scrollback_max_lines, @ptrCast(&value)), + ); + try testing.expectEqual(max_lines, value); + + try testing.expectEqual( + Result.success, + set(t, .scrollback_max_bytes, null), + ); + try testing.expectEqual( + Result.success, + set(t, .scrollback_max_lines, null), + ); + try testing.expectEqual( + Result.no_value, + get(t, .scrollback_max_bytes, @ptrCast(&value)), + ); + try testing.expectEqual( + Result.no_value, + get(t, .scrollback_max_lines, @ptrCast(&value)), + ); +} + test "get invalid" { var t: Terminal = null; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1850,11 +1915,8 @@ test "set default cursor style and blink" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1895,11 +1957,8 @@ test "set and get selection" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -1943,11 +2002,8 @@ test "selection derivation helpers" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2033,11 +2089,8 @@ test "selection_adjust mutates snapshot end" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2081,11 +2134,8 @@ test "selection_order and selection_ordered" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2134,11 +2184,8 @@ test "selection_contains" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2192,11 +2239,8 @@ test "selection_equal" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2204,11 +2248,8 @@ test "selection_equal" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &other_t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(other_t); @@ -2281,11 +2322,8 @@ test "selection_order invalid values" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2299,11 +2337,8 @@ test "grid_ref" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2337,7 +2372,8 @@ test "point_from_grid_ref roundtrip active" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer free(t); @@ -2362,7 +2398,8 @@ test "point_from_grid_ref roundtrip viewport" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + 80, + 24, )); defer free(t); @@ -2385,7 +2422,8 @@ test "point_from_grid_ref history ref to active returns no_value" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 4, .max_scrollback = 10_000 }, + 80, + 4, )); defer free(t); @@ -2420,7 +2458,8 @@ test "point_from_grid_ref null node" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer free(t); @@ -2433,11 +2472,8 @@ test "set write_pty callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2476,11 +2512,8 @@ test "write_pty receives DECRQSS response" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2511,11 +2544,8 @@ test "write_pty receives OSC color query response" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2551,11 +2581,8 @@ test "set write_pty without callback ignores queries" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2568,11 +2595,8 @@ test "set write_pty null clears callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2597,11 +2621,8 @@ test "set bell callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2637,11 +2658,8 @@ test "bell without callback is silent" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2654,11 +2672,8 @@ test "set enquiry callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2696,11 +2711,8 @@ test "enquiry without callback is silent" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2713,11 +2725,8 @@ test "set xtversion callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2756,11 +2765,8 @@ test "xtversion without callback reports default" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2792,11 +2798,8 @@ test "set title_changed callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2831,11 +2834,8 @@ test "title_changed without callback is silent" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2848,11 +2848,8 @@ test "set pwd_changed callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -2891,11 +2888,8 @@ test "set clipboard_write callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3046,11 +3040,8 @@ test "clipboard_write without callback is unsupported and silent" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3071,11 +3062,8 @@ test "pwd_changed without callback is silent" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3090,11 +3078,8 @@ test "set size callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3137,11 +3122,8 @@ test "size without callback is silent" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3154,11 +3136,8 @@ test "set device_attributes callback primary" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3208,11 +3187,8 @@ test "set device_attributes callback secondary" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3262,11 +3238,8 @@ test "set device_attributes callback tertiary" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3316,11 +3289,8 @@ test "device_attributes without callback uses default" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3352,11 +3322,8 @@ test "device_attributes callback returns false uses default" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3393,11 +3360,8 @@ test "set and get title" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3432,11 +3396,8 @@ test "set and get pwd" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3464,11 +3425,8 @@ test "get title set via vt_write" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3485,11 +3443,8 @@ test "resize updates pixel dimensions" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3506,11 +3461,8 @@ test "resize pixel overflow saturates" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3526,11 +3478,8 @@ test "resize disables synchronized output" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3547,11 +3496,8 @@ test "resize sends in-band size report" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3588,11 +3534,8 @@ test "resize no size report without mode 2048" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3616,11 +3559,8 @@ test "resize in-band report without write_pty callback" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3638,11 +3578,8 @@ test "resize zero cols" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3654,11 +3591,8 @@ test "resize zero rows" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3670,11 +3604,8 @@ test "grid_ref out of bounds" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3690,11 +3621,8 @@ test "set and get color_foreground" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3718,11 +3646,8 @@ test "set and get color_background" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3743,11 +3668,8 @@ test "set and get color_cursor" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3768,11 +3690,8 @@ test "set and get color_palette" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3799,11 +3718,8 @@ test "get color default vs effective with override" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3846,11 +3762,8 @@ test "get color default returns no_value when unset" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3865,11 +3778,8 @@ test "get color_palette_default vs current" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3898,11 +3808,8 @@ test "set color sets dirty flag" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ - .cols = 80, - .rows = 24, - .max_scrollback = 0, - }, + 80, + 24, )); defer free(t); @@ -3919,7 +3826,8 @@ test "set glyph protocol disables APC handling and clears glossary" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer free(t); @@ -3946,7 +3854,8 @@ test "get_multi success" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer free(t); @@ -3967,7 +3876,8 @@ test "get_multi error sets out_written" { try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &t, - .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + 80, + 24, )); defer free(t); diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index f160f3852..549dfff15 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -68,7 +68,6 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: { .{ "GhosttySurfacePosition", StructInfo.init(SurfacePosition) }, .{ "GhosttyStyle", StructInfo.init(style_c.Style) }, .{ "GhosttyStyleColor", StructInfo.init(style_c.Color) }, - .{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) }, .{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) }, .{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) }, }); @@ -216,7 +215,6 @@ test "json parses" { // Verify we have all expected structs try std.testing.expect(root.contains("GhosttyClipboardContent")); try std.testing.expect(root.contains("GhosttyClipboardWrite")); - try std.testing.expect(root.contains("GhosttyTerminalOptions")); try std.testing.expect(root.contains("GhosttyFormatterTerminalOptions")); const clipboard_content = root.get("GhosttyClipboardContent").?.object; @@ -231,20 +229,7 @@ test "json parses" { try std.testing.expect(clipboard_write_fields.contains("contents")); try std.testing.expect(clipboard_write_fields.contains("contents_len")); - // Verify GhosttyTerminalOptions fields - const term_opts = root.get("GhosttyTerminalOptions").?.object; - try std.testing.expect(term_opts.contains("size")); - try std.testing.expect(term_opts.contains("align")); - try std.testing.expect(term_opts.contains("fields")); - - const fields = term_opts.get("fields").?.object; - try std.testing.expect(fields.contains("cols")); - try std.testing.expect(fields.contains("rows")); - try std.testing.expect(fields.contains("max_scrollback")); - - // Verify field offsets make sense (cols should be at 0) - const cols = fields.get("cols").?.object; - try std.testing.expectEqual(0, cols.get("offset").?.integer); + try std.testing.expect(!root.contains("GhosttyTerminalOptions")); } test "struct sizes are non-zero" {