From f730ee0557917258024e18a45489918de2ce9fa7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 May 2026 15:23:48 -0700 Subject: [PATCH] libghostty: expose viewport active state Expose whether the terminal viewport is currently pinned to the active area through the libghostty-vt terminal data API. Previously embedders could only infer this from scrollbar geometry, which was indirect and could require the more expensive scrollbar calculation. The new GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE value returns the exact PageList viewport state as a bool. The scroll viewport test now verifies the value while moving between the active area and scrollback. --- include/ghostty/vt/terminal.h | 10 ++++++++++ src/terminal/c/terminal.zig | 14 +++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 19b27b7a6..ddfcb9c0d 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -901,6 +901,16 @@ typedef enum GHOSTTY_ENUM_TYPED { * Output type: GhosttySelection * */ GHOSTTY_TERMINAL_DATA_SELECTION = 31, + + /** + * Whether the viewport is currently pinned to the active area. + * + * This is true when the viewport is following the active terminal area, + * and false when the user has scrolled into history. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE = 32, GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalData; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 8deb4c95c..302fb77a6 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -593,13 +593,14 @@ pub const TerminalData = enum(c_int) { kitty_image_medium_shared_mem = 29, kitty_graphics = 30, selection = 31, + viewport_active = 32, /// Output type expected for querying the data of the given kind. pub fn OutType(comptime self: TerminalData) type { return switch (self) { .invalid => void, .cols, .rows, .cursor_x, .cursor_y => size.CellCountInt, - .cursor_pending_wrap, .cursor_visible, .mouse_tracking => bool, + .cursor_pending_wrap, .cursor_visible, .mouse_tracking, .viewport_active => bool, .active_screen => TerminalScreen, .kitty_keyboard_flags => u8, .scrollbar => TerminalScrollbar, @@ -734,6 +735,7 @@ fn getTyped( .selection => out.* = selection_c.CSelection.fromZig( t.screens.active.selection orelse return .no_value, ), + .viewport_active => out.* = t.screens.active.pages.viewport == .active, } return .success; @@ -883,6 +885,10 @@ test "scroll_viewport" { const zt = t.?.terminal; + var viewport_active: bool = false; + try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active))); + try testing.expect(viewport_active); + // Write "hello" on the first line vt_write(t, "hello", 5); @@ -897,6 +903,8 @@ test "scroll_viewport" { // Scroll to top: "hello" should be visible again scroll_viewport(t, .{ .tag = .top, .value = undefined }); + try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active))); + try testing.expect(!viewport_active); { const str = try zt.plainString(testing.allocator); defer testing.allocator.free(str); @@ -905,6 +913,8 @@ test "scroll_viewport" { // Scroll to bottom: viewport should be empty again scroll_viewport(t, .{ .tag = .bottom, .value = undefined }); + try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active))); + try testing.expect(viewport_active); { const str = try zt.plainString(testing.allocator); defer testing.allocator.free(str); @@ -913,6 +923,8 @@ test "scroll_viewport" { // Scroll up by delta to bring "hello" back into view scroll_viewport(t, .{ .tag = .delta, .value = .{ .delta = -3 } }); + try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active))); + try testing.expect(!viewport_active); { const str = try zt.plainString(testing.allocator); defer testing.allocator.free(str);