diff --git a/src/Surface.zig b/src/Surface.zig index d84e786f3..7028ce2bf 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1059,6 +1059,12 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .scrollbar => |scrollbar| self.updateScrollbar(scrollbar), + .scroll_to_bottom => { + self.queueIo(.{ + .scroll_viewport = .{ .bottom = {} }, + }, .unlocked); + }, + .report_color_scheme => |force| self.reportColorScheme(force), .present_surface => try self.presentSurface(), diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 45a847493..5507ae712 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -113,6 +113,10 @@ pub const Message = union(enum) { /// Selected search index change search_selected: ?usize, + /// Scroll the viewport to the bottom. This is triggered by the renderer + /// when new output is detected and scroll-to-bottom on output is enabled. + scroll_to_bottom, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 39eec7b43..daa32e4c7 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -122,6 +122,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { scrollbar: terminal.Scrollbar, scrollbar_dirty: bool, + /// Tracks the last bottom-right pin of the screen to detect new output. + /// When the final line changes (node or y differs), new content was added. + /// Used for scroll-to-bottom on output feature. + last_bottom_node: ?*terminal.PageList.List.Node, + last_bottom_y: terminal.size.CellCountInt, + /// The most recent viewport matches so that we can render search /// matches in the visible frame. This is provided asynchronously /// from the search thread so we have the dirty flag to also note @@ -562,6 +568,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { colorspace: configpkg.Config.WindowColorspace, blending: configpkg.Config.AlphaBlending, background_blur: configpkg.Config.BackgroundBlur, + scroll_to_bottom_on_output: bool, pub fn init( alloc_gpa: Allocator, @@ -635,6 +642,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .colorspace = config.@"window-colorspace", .blending = config.@"alpha-blending", .background_blur = config.@"background-blur", + .scroll_to_bottom_on_output = config.@"scroll-to-bottom".output, .arena = arena, }; } @@ -698,6 +706,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .focused = true, .scrollbar = .zero, .scrollbar_dirty = false, + .last_bottom_node = null, + .last_bottom_y = 0, .search_matches = null, .search_selected_match = null, .search_matches_dirty = false, @@ -1160,6 +1170,25 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // cross-thread mailbox message within the IO path. const scrollbar = state.terminal.screens.active.pages.scrollbar(); + // If scroll-to-bottom on output is enabled, check if the final line + // changed by comparing the bottom-right pin. If the node pointer or + // y offset changed, new content was added to the screen. + if (self.config.scroll_to_bottom_on_output) { + const bottom_right = state.terminal.screens.active.pages.getBottomRight(.screen); + if (bottom_right) |br| { + const pin_changed = (self.last_bottom_node != br.node) or + (self.last_bottom_y != br.y); + + if (pin_changed and !state.terminal.screens.active.viewportIsBottom()) { + _ = self.surface_mailbox.push(.scroll_to_bottom, .instant); + } + + // Update tracked pin state for next frame + self.last_bottom_node = br.node; + self.last_bottom_y = br.y; + } + } + // Get our preedit state const preedit: ?renderer.State.Preedit = preedit: { const p = state.preedit orelse break :preedit null;