diff options
| -rw-r--r-- | src/tui_app.zig | 4 | ||||
| -rw-r--r-- | src/tui_engine.zig | 40 |
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(); |
