: nply/wmenu

commit c4d751181102f2d6ca8243a376d2add3821159ab
parent de4d213a66b669e4e4e3a58f407af6d33096fc1b
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:17:37 +0530

Paint new streamed rows before queued updates starve the UI
4 files changed, 70 insertions(+), 6 deletions(-)
M
HACKING
|
8
+++++---
A
test_pipe.ps1
|
45
+++++++++++++++++++++++++++++++++++++++++++++
M
test_stream.c
|
21
++++++++++++++++++---
M
wmenu.c
|
2
++
diff --git a/HACKING b/HACKING
@@ -79,7 +79,8 @@ Release checklist
 -----------------
 1. Run make, make test, and optionally make stress (about 155 MiB of synthetic
    rows; validates no-data ListBox navigation, full-file fiber search, and
-   progress state).
+   progress state). Run `powershell.exe -NoProfile -ExecutionPolicy Bypass -File
+   test_pipe.ps1` to verify rows appear before pipe EOF.
 2. Check:
        .\\wmenu.exe -help
        .\\wmenu.exe -version

@@ -109,8 +110,9 @@ Known simplifications
   thread. Matching rows are indexed in source order (fzy subsequence matching,
   not score-ranked). No silent row cap. LB_SETCOUNT is wrapped with redraw
   suppression: unchanged visible rows are not invalidated as the row count
-  grows; only newly visible rows are. The bottom bar shows search progress
-  after stdin completes.
+  grows; only newly visible rows are, and those are painted immediately so
+  queued stream messages cannot starve WM_PAINT. The bottom bar shows search
+  progress after stdin completes.
 - lnav inspiration: its logfile index tracks lines separately from the view
   (https://github.com/tstack/lnav/blob/master/src/logfile.hh, lf_index and
   read_line); its text view requests counts and rows from a text sub-source
diff --git a/test_pipe.ps1 b/test_pipe.ps1
@@ -0,0 +1,45 @@
+$ErrorActionPreference = 'Stop'
+Add-Type @'
+using System;
+using System.Runtime.InteropServices;
+public static class NativeMenu {
+    [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string cls, string title);
+    [DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wp, IntPtr lp);
+}
+'@
+$p = New-Object System.Diagnostics.Process
+$p.StartInfo.FileName = (Join-Path $PSScriptRoot 'wmenu.exe')
+$p.StartInfo.UseShellExecute = $false
+$p.StartInfo.RedirectStandardInput = $true
+$p.StartInfo.RedirectStandardOutput = $true
+try {
+    if (-not $p.Start()) { throw 'Could not start wmenu' }
+    $p.StandardInput.WriteLine('first-streamed-line')
+    $p.StandardInput.Flush()
+    $count = -1
+    for ($i = 0; $i -lt 100; $i++) {
+        Start-Sleep -Milliseconds 50
+        $p.Refresh()
+        $win = $p.MainWindowHandle
+        if ($win -ne [IntPtr]::Zero) {
+            $list = [NativeMenu]::FindWindowEx($win, [IntPtr]::Zero, 'ListBox', $null)
+            if ($list -ne [IntPtr]::Zero) {
+                $count = [NativeMenu]::SendMessage($list, 0x18B, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32()
+                if ($count -gt 0) { break }
+            }
+        }
+    }
+    if ($count -ne 1) { throw "Expected row before pipe closes; got $count, hwnd=$win, list=$list, exited=$($p.HasExited)" }
+    for ($j = 0; $j -lt 3000; $j++) { $p.StandardInput.WriteLine("streamed-$j") }
+    $p.StandardInput.Flush()
+    $more = 0
+    for ($i = 0; $i -lt 100 -and $more -lt 2049; $i++) {
+        Start-Sleep -Milliseconds 50
+        $more = [NativeMenu]::SendMessage($list, 0x18B, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32()
+    }
+    if ($more -lt 2049) { throw "Expected streamed list to grow while stdin open; got $more" }
+    Write-Output "List reached $more rows before EOF."
+} finally {
+    if (-not $p.HasExited) { $p.Kill(); $p.WaitForExit() }
+    $p.Dispose()
+}
diff --git a/test_stream.c b/test_stream.c
@@ -3,6 +3,13 @@
 #undef main
 #include <assert.h>
 
+static int draw_calls;
+static LRESULT CALLBACK TestWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
+{
+    if (msg == WM_DRAWITEM) ++draw_calls;
+    return WndProc(hwnd, msg, wp, lp);
+}
+
 int main(int argc, char **argv)
 {
     InitializeCriticalSection(&items_lock);

@@ -14,7 +21,7 @@ int main(int argc, char **argv)
     input_done = true;
     has_items = true;
     WNDCLASSW wc = {0};
-    wc.lpfnWndProc = WndProc;
+    wc.lpfnWndProc = TestWndProc;
     wc.hInstance = GetModuleHandleW(NULL);
     wc.lpszClassName = L"wmenu-test";
     assert(RegisterClassW(&wc));

@@ -22,13 +29,21 @@ int main(int argc, char **argv)
     list_hwnd = CreateWindowW(L"LISTBOX", L"", WS_CHILD | WS_VSCROLL | LBS_NODATA | LBS_OWNERDRAWFIXED,
                               0, 0, 400, 300, main_hwnd, (HMENU)ID_COMBO, wc.hInstance, NULL);
     progress_hwnd = CreateWindowW(PROGRESS_CLASSW, L"", WS_CHILD, 0, 290, 400, 10, main_hwnd, NULL, NULL, NULL);
-    assert(list_hwnd && progress_hwnd);
+    input_hwnd = CreateWindowW(L"EDIT", L"", WS_CHILD, 0, 0, 400, 28, main_hwnd, NULL, NULL, NULL);
+    assert(list_hwnd && progress_hwnd && input_hwnd);
     ui_fiber = ConvertThreadToFiber(NULL);
     assert(ui_fiber);
     search_fiber = CreateFiber(0, search_fiber_run, NULL);
     assert(search_fiber);
     bool stress = argc > 1 && !strcmp(argv[1], "stress");
-    for (int i = 0; i < 60000; ++i) {
+    ShowWindow(main_hwnd, SW_SHOWNOACTIVATE);
+    ShowWindow(list_hwnd, SW_SHOWNOACTIVATE);
+    add_item("item-0");
+    WndProc(main_hwnd, WM_STDIN_CHUNK, 0, 0);
+    UpdateWindow(list_hwnd);
+    assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1);
+    assert(draw_calls > 0); /* First streamed row must actually paint. */
+    for (int i = 1; i < 60000; ++i) {
         char buf[2700];
         sprintf(buf, "item-%d", i);
         if (stress) {
diff --git a/wmenu.c b/wmenu.c
@@ -152,10 +152,12 @@ static void update_count(void)
     int new_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0);
     if (top != new_top || count < old_count || height <= 0) {
         InvalidateRect(list_hwnd, NULL, FALSE);
+        UpdateWindow(list_hwnd);
     } else if (old_count < new_top + (client.bottom - client.top + height - 1) / height) {
         client.top += (old_count - new_top) * height;
         if (client.top < 0) client.top = 0;
         InvalidateRect(list_hwnd, &client, FALSE);
+        UpdateWindow(list_hwnd); /* Posted stream messages must not starve WM_PAINT. */
     }
 }