From 35a91767e9bbf38f0a533f7c4390a655bf90a6be Mon Sep 17 00:00:00 2001
From: Sergey Fedorov <barracuda@macos-powerpc.org>
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) <noreply@anthropic.com>
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 <chrono>
 #include <cstdlib>
 #include <cstring>
+#include <optional>
 #include <string>
 
 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<std::chrono::milliseconds> 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<int>(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);
