summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/tui_app.zig4
-rw-r--r--src/tui_engine.zig40
2 files changed, 44 insertions, 0 deletions
diff --git a/src/tui_app.zig b/src/tui_app.zig
index f2a205b..c99985b 100644
--- a/src/tui_app.zig
+++ b/src/tui_app.zig
@@ -1040,6 +1040,10 @@ pub fn runLoop(app: *App, term: *Terminal, opts: RunOptions) !void {
// iff the terminal turns out not to support Kitty.
term.writeAll(input_mod.negotiate_query);
defer {
+ app.engine.finalizeCursor() catch {};
+ app.flushSink();
+ }
+ defer {
input_mod.setKittyActive(false);
if (hs.mok_enabled) term.writeAll(input_mod.disable_modify_other_keys);
term.writeAll(input_mod.negotiate_teardown);
diff --git a/src/tui_engine.zig b/src/tui_engine.zig
index c6ddf49..05b0b89 100644
--- a/src/tui_engine.zig
+++ b/src/tui_engine.zig
@@ -812,6 +812,25 @@ pub const Engine = struct {
self.viewport_top = 0;
}
}
+
+ /// On teardown, move the hardware cursor to the first row AFTER the
+ /// rendered content so the shell prompt appears below the footer, not at
+ /// the focused input cursor position we used for IME anchoring.
+ pub fn finalizeCursor(self: *Engine) Error!void {
+ if (self.total_lines == 0) {
+ try self.writer.writeAll(terminal.seq.carriage_return);
+ self.hw_cursor_row = 0;
+ return;
+ }
+
+ const past_content_row = self.total_lines;
+ const current_row = self.hw_cursor_row;
+ if (past_content_row > current_row) {
+ try self.cursorDown(past_content_row - current_row);
+ }
+ try self.writer.writeAll(terminal.seq.carriage_return);
+ self.hw_cursor_row = past_content_row;
+ }
};
// ===========================================================================
@@ -1048,6 +1067,27 @@ test "first paint preserves the cursor-resting invariant (no trailing newline)"
try testing.expectEqual(@as(usize, 2), std.mem.count(u8, out, "\r\n"));
}
+test "finalizeCursor moves below the footer before exit" {
+ var buf = std.Io.Writer.Allocating.init(testing.allocator);
+ defer buf.deinit();
+ var eng = makeEngine(&buf, 80, 24);
+ defer eng.deinit();
+
+ var body = FakeComponent{ .scripts = &.{&.{ "welcome", "prompt", "footer" }}, .first_changed = &.{0} };
+ try eng.addComponent(body.comp());
+ try eng.render();
+
+ // Simulate the session parking the hardware cursor on the input row while
+ // footer content still exists below it.
+ eng.hw_cursor_row = 1;
+
+ buf.clearRetainingCapacity();
+ try eng.finalizeCursor();
+
+ try testing.expectEqualStrings("\x1b[2B\r", buf.written());
+ try testing.expectEqual(@as(usize, 3), eng.hw_cursor_row);
+}
+
test "cut is the min across ALL components (ticking footer below clean body)" {
var buf = std.Io.Writer.Allocating.init(testing.allocator);
defer buf.deinit();