Fix vterm flicker for full-screen apps (top, htop) and improve theme contrast

ober

cf0a077f4e94e348b4f69fdc86360ecce1949b52

diff --git a/src/jerboa-emacs/qt/app.ss b/src/jerboa-emacs/qt/app.ss
index b336909..30930f1 100644
--- a/src/jerboa-emacs/qt/app.ss
+++ b/src/jerboa-emacs/qt/app.ss
@@ -169,8 +169,10 @@
 
 (def (vterm-row-diff-render! ed vt ts)
   "Render vtscreen rows into the QScintilla document.
-   When many rows changed, uses full set-text! for efficiency.
-   When few rows changed, uses per-line replacement."
+   In normal mode with many dirty rows, uses full set-text! for efficiency.
+   On alt screen (top, htop, etc.), always uses per-line replacement to avoid
+   the full document reset that causes visible bouncing/flicker.
+   Tracks which rows actually changed in *vterm-dirty-rows* for targeted recoloring."
   (let ((rows (vtscreen-rows vt))
         (line-offset (hash-ref *vterm-line-offset* ts 0))
         (cache (hash-ref *vterm-row-cache* ts #f)))
@@ -190,40 +192,45 @@
            ((= n 0)
             ;; No dirty rows
             (vtscreen-clear-damage! vt)
+            (hash-put! *vterm-dirty-rows* ts (make-hash-table))
             #f)
-           ((> n 4)
-            ;; Many dirty rows — full set-text! is faster than N line replacements
+           ((and (> n 4) (not (vtscreen-alt-screen? vt)))
+            ;; Many dirty rows in normal mode — full set-text! is faster than N line replacements.
+            ;; Never do this on alt screen (top, htop, etc.) — set-text! replaces the entire
+            ;; document, causing QScintilla to reset scroll/layout and visibly bounce.
             (let* ((rendered (vtscreen-render vt))
-                   (pre-text (or (terminal-state-pre-pty-text ts) ""))
-                   (full (if (vtscreen-alt-screen? vt)
-                           rendered
-                           (string-append pre-text rendered))))
+                   (pre-text (or (terminal-state-pre-pty-text ts) "")))
               ;; Update cache for all rows
               (let update ((r2 0))
                 (when (< r2 rows)
                   (vector-set! cache r2 (vtscreen-get-row-text vt r2))
                   (update (+ r2 1))))
               (vtscreen-clear-damage! vt)
-              (qt-plain-text-edit-set-text! ed full)
+              ;; Full render — mark all rows dirty for coloring
+              (hash-put! *vterm-dirty-rows* ts #f)
+              (qt-plain-text-edit-set-text! ed (string-append pre-text rendered))
               #t))
            (else
-            ;; Few dirty rows — per-line replacement
+            ;; Per-line replacement — only update rows whose text actually changed
             (vterm-ensure-lines! ed (+ line-offset rows))
-            (let loop ((r2 0) (any-changed? #f))
-              (if (>= r2 rows)
-                (begin
-                  (vtscreen-clear-damage! vt)
-                  any-changed?)
-                (if (vtscreen-row-dirty? vt r2)
-                  (let ((new-text (vtscreen-get-row-text vt r2))
-                        (old-text (vector-ref cache r2)))
-                    (if (string=? new-text old-text)
-                      (loop (+ r2 1) any-changed?)
-                      (begin
-                        (vector-set! cache r2 new-text)
-                        (vterm-replace-line! ed (+ line-offset r2) new-text)
-                        (loop (+ r2 1) #t))))
-                  (loop (+ r2 1) any-changed?)))))))
+            (let ((dirty-set (make-hash-table)))
+              (let loop ((r2 0) (any-changed? #f))
+                (if (>= r2 rows)
+                  (begin
+                    (vtscreen-clear-damage! vt)
+                    (hash-put! *vterm-dirty-rows* ts dirty-set)
+                    any-changed?)
+                  (if (vtscreen-row-dirty? vt r2)
+                    (let ((new-text (vtscreen-get-row-text vt r2))
+                          (old-text (vector-ref cache r2)))
+                      (if (string=? new-text old-text)
+                        (loop (+ r2 1) any-changed?)
+                        (begin
+                          (vector-set! cache r2 new-text)
+                          (hash-put! dirty-set r2 #t)
+                          (vterm-replace-line! ed (+ line-offset r2) new-text)
+                          (loop (+ r2 1) #t))))
+                    (loop (+ r2 1) any-changed?))))))))
         ((vtscreen-row-dirty? vt r)
          (count-dirty (+ r 1) (+ n 1)))
         (else
@@ -291,20 +298,24 @@
                   (sci-send ed SCI_SETSTYLING (- c run-start) run-style))
                 (loop (+ c 1) c style)))))))))
 
+;; Per-terminal-state: set of row indices updated in the last row-diff-render
+(def *vterm-dirty-rows* (make-hash-table-eq))
+
 (def (vterm-apply-colors! ed vt ts)
-  "Apply colors to all dirty rows."
+  "Apply colors to rows updated in the last render pass."
   (let ((rows (vtscreen-rows vt))
-        (line-offset (hash-ref *vterm-line-offset* ts 0)))
+        (line-offset (hash-ref *vterm-line-offset* ts 0))
+        (dirty-set (hash-ref *vterm-dirty-rows* ts #f)))
     (let loop ((r 0))
       (when (< r rows)
-        ;; Only re-color rows that had their text updated
-        ;; (The damage was already cleared in row-diff-render, but we apply colors
-        ;;  to all rows whose text was just replaced in the cache)
-        (let ((cache (hash-ref *vterm-row-cache* ts #f)))
-          (when (and cache
-                     (< r (vector-length cache))
-                     (> (string-length (vector-ref cache r)) 0))
-            (vterm-apply-row-colors! ed vt r (+ line-offset r))))
+        ;; Only re-color rows that were actually replaced in the last render
+        (when (or (not dirty-set)   ;; full render — color everything
+                  (hash-ref dirty-set r #f))
+          (let ((cache (hash-ref *vterm-row-cache* ts #f)))
+            (when (and cache
+                       (< r (vector-length cache))
+                       (> (string-length (vector-ref cache r)) 0))
+              (vterm-apply-row-colors! ed vt r (+ line-offset r)))))
         (loop (+ r 1))))))
 
 ;;;============================================================================
@@ -589,12 +600,31 @@
                         (hash-put! *vterm-line-offset* ts 0)
                         ;; Normal mode: keep existing offset
                         (void))
+                      ;; Suppress intermediate repaints: disable widget updates,
+                      ;; do all line replacements + styling, then re-enable for
+                      ;; a single
+                      (qt-widget-set-updates-enabled! ed #f)
                       (let ((changed? (vterm-row-diff-render! ed vt ts)))
                         (when changed?
                           ;; Apply colors to changed rows
                           (vterm-apply-colors! ed vt ts)
-                          (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
-                          (qt-plain-text-edit-ensure-cursor-visible! ed)))))
+                          (if (vtscreen-alt-screen? vt)
+                            ;; Alt screen (top, htop, etc.): place cursor at terminal's
+                            ;; actual cursor position. Do NOT move to end or call
+                            ;; ensure-cursor-visible — that forces a scroll/relayout
+                            ;; on every refresh, causing the visible "bounce".
+                            (let* ((crow (vtscreen-cursor-row vt))
+                                   (ccol (vtscreen-cursor-col vt))
+                                   (offset (hash-ref *vterm-line-offset* ts 0))
+                                   (line-start (sci-send ed SCI_POSITIONFROMLINE (+ offset crow)))
+                                   (pos (+ line-start ccol)))
+                              (when (>= line-start 0)
+                                (sci-send ed SCI_GOTOPOS pos)))
+                            ;; Normal mode: scroll to bottom to follow output
+                            (begin
+                              (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
+                              (qt-plain-text-edit-ensure-cursor-visible! ed))))
+                        (qt-widget-set-updates-enabled! ed #t))))
                   (vterm-mark-rendered! ts)))
               (begin
                 (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
diff --git a/src/jerboa-emacs/themes.ss b/src/jerboa-emacs/themes.ss
index a4b28b3..03bf175 100644
--- a/src/jerboa-emacs/themes.ss
+++ b/src/jerboa-emacs/themes.ss
@@ -597,7 +597,7 @@
     ;; UI elements
     (modeline . (fg: "#f8f8f2" bg: "#44475a"))
     (modeline-inactive . (fg: "#6272a4" bg: "#44475a"))
-    (line-number . (fg: "#6272a4" bg: "#282a36"))
+    (line-number . (fg: "#a4b0d4" bg: "#282a36"))
     (cursor-line . (bg: "#44475a"))
 
     ;; Search and matching
@@ -678,7 +678,7 @@
     ;; UI elements
     (modeline . (fg: "#d8dee9" bg: "#3b4252"))
     (modeline-inactive . (fg: "#616e88" bg: "#3b4252"))
-    (line-number . (fg: "#616e88" bg: "#2e3440"))
+    (line-number . (fg: "#8690a8" bg: "#2e3440"))
     (cursor-line . (bg: "#3b4252"))
 
     ;; Search and matching