commit c4d751181102f2d6ca8243a376d2add3821159ab
parent de4d213a66b669e4e4e3a58f407af6d33096fc1b
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:17:37 +0530
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(-)
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. */ } }
: nply/wmenu