From 35a91767e9bbf38f0a533f7c4390a655bf90a6be Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Sun, 19 Jul 2026 15:01:57 +0000 Subject: [PATCH 34/71] =?UTF-8?q?fix(macos):=20settle=20the=20cursor=20?= =?UTF-8?q?=E2=80=94=20no=20motion=20animation=20+=20follow-up=20redraws?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs (cursor drawn one cell off, as an opaque block hiding the char under it, and arrow/backspace appearing to move the wrong way first) share one cause: the cursor-motion animation. The frontend redraws on demand, not continuously, so after a keystroke it renders once mid-animation — the cursor drawn at the interpolated position as a solid block (cell-inversion is disabled during animation, so the char under it vanishes) and then frozen there until the next keystroke. - Set cursorMotionAnimationDuration = 0 so the cursor is effectively settled (animationProgress == 1): it then renders via cell-color inversion at the correct position, and the character under it stays visible. - bridge_render_frame now returns Terminal::nextRender() (ms until the next needed frame for blink/animation, or -1); the view schedules a one-shot redraw after that delay so any remaining time-based effect completes instead of freezing. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AeG2jwYMX5gdvvtUnx3PJa --- src/contour_macos/SessionBridge.cpp | 8 +++++++- src/contour_macos/SessionBridge.h | 5 ++++- src/contour_macos/TerminalSession.cpp | 6 ++++++ src/contour_macos/TerminalView.mm | 14 +++++++++++++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/contour_macos/SessionBridge.cpp b/src/contour_macos/SessionBridge.cpp index d6a51846..af229ed0 100644 --- a/src/contour_macos/SessionBridge.cpp +++ b/src/contour_macos/SessionBridge.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace contour_macos @@ -125,9 +126,14 @@ void bridge_start(TerminalSession* session) session->start(); } -void bridge_render_frame(TerminalSession* session) +int bridge_render_frame(TerminalSession* session) { session->renderFrame(); + std::optional const next = session->terminal().nextRender(); + if (!next.has_value() || *next == std::chrono::milliseconds::max()) + return -1; + auto const ms = next->count(); + return ms < 0 ? 0 : static_cast(ms); } uint8_t const* bridge_output_buffer(TerminalSession* session, int* outWidth, int* outHeight) diff --git a/src/contour_macos/SessionBridge.h b/src/contour_macos/SessionBridge.h index 7c8ac9e1..c937e0d7 100644 --- a/src/contour_macos/SessionBridge.h +++ b/src/contour_macos/SessionBridge.h @@ -63,7 +63,10 @@ void bridge_destroy(TerminalSession* session); void bridge_start(TerminalSession* session); /// Renders one frame into the internal buffer. Call on the main thread (from -drawRect:). -void bridge_render_frame(TerminalSession* session); +/// Returns the number of milliseconds until another frame is needed (cursor/cell blink, +/// cursor-motion animation), or -1 if no further frame is currently scheduled. The caller +/// should schedule one more redraw after that delay so time-based effects complete. +int bridge_render_frame(TerminalSession* session); /// Returns a pointer to the current RGBA8, top-left-origin frame buffer, and its dimensions. /// The pointer is valid until the next bridge_render_frame()/bridge_resize() call. diff --git a/src/contour_macos/TerminalSession.cpp b/src/contour_macos/TerminalSession.cpp index bfc7e8af..1f87a85e 100644 --- a/src/contour_macos/TerminalSession.cpp +++ b/src/contour_macos/TerminalSession.cpp @@ -28,6 +28,12 @@ namespace settings.ptyReadBufferSize = 16384; settings.goodImageProtocol = true; settings.allowClipboardRead = true; + // No cursor motion animation: we redraw on demand (not continuously), so an animated + // cursor would be stuck mid-flight — drawn at the interpolated position as an opaque + // block that hides the character under it. With a zero duration the cursor is always + // "settled" (animationProgress == 1), so it renders via cell-color inversion at the + // correct position and the character under it stays visible. + settings.cursorMotionAnimationDuration = std::chrono::milliseconds { 0 }; return settings; } diff --git a/src/contour_macos/TerminalView.mm b/src/contour_macos/TerminalView.mm index 6640891e..c863422a 100644 --- a/src/contour_macos/TerminalView.mm +++ b/src/contour_macos/TerminalView.mm @@ -216,7 +216,19 @@ void cbOnClosed(void* userData) if (!session) return; - contour_macos::bridge_render_frame(session); + int const nextMs = contour_macos::bridge_render_frame(session); + if (nextMs >= 0) + { + // A time-based effect (cursor/cell blink, cursor-motion animation) needs another + // frame; schedule one so it completes instead of freezing mid-flight. + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(mainThreadRedraw) + object:nil]; + [self performSelector:@selector(mainThreadRedraw) + withObject:nil + afterDelay:(double) nextMs / 1000.0]; + } + int w = 0; int h = 0; uint8_t const* bytes = contour_macos::bridge_output_buffer(session, &w, &h);