commit b5a00bb375a1c1080a491000d76712486b6dae9d
parent 92508a34be083841836724ac26566f76f70f894e
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 21:46:44 +0530
parent 92508a34be083841836724ac26566f76f70f894e
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 21:46:44 +0530
Append streamed list rows without resetting scroll
4 files changed, 97 insertions(+), 17 deletions(-)
diff --git a/HACKING b/HACKING @@ -77,7 +77,7 @@ Use -default-controls for normal Windows edit behavior. Release checklist ----------------- -1. Run make. +1. Run make and make test (native ListBox append/selection regression). 2. Check: .\\wmenu.exe -help .\\wmenu.exe -version @@ -97,9 +97,10 @@ Known simplifications --------------------- - Default input/output encoding is UTF-8. Use `-enc ansi` or `-enc oem` for legacy Windows pipes such as classic `cmd.exe` output. -- The ListBox displays at most LIST_LIMIT rows. This keeps huge streams (for - example 100MB+ logs) responsive instead of trying to shove every line into a - Win32 list control. +- The ListBox displays at most LIST_LIMIT rows. Stream updates append new + unfiltered rows rather than resetting the control; filtered results rebuild + only if the displayed matches change. The native ListBox owns scrolling. + This keeps huge streams (for example 100MB+ logs) responsive. - Filtering scans at most SCAN_LIMIT rows and stops after LIST_LIMIT matches. This intentionally favors staying interactive over exhaustive ranking on massive inputs.
diff --git a/Makefile b/Makefile @@ -9,6 +9,10 @@ CFLAGS = $(DEBUG) wmenu.exe: wmenu.c arg.h deps/fzy/src/match.c deps/fzy/config.h deps/fzy/src/strings.h cl wmenu.c deps/fzy/src/match.c /I deps/fzy/src $(CFLAGS) user32.lib comctl32.lib gdi32.lib +test: test_stream.c wmenu.c arg.h deps/fzy/src/match.c deps/fzy/config.h deps/fzy/src/strings.h + cl test_stream.c deps/fzy/src/match.c /I deps/fzy/src $(CFLAGS) user32.lib comctl32.lib gdi32.lib /Fe:test_stream.exe + ./test_stream.exe + deps/fzy/config.h: deps/fzy/src/config.def.h printf '#include "src/config.def.h"\n' > deps/fzy/config.h
diff --git a/test_stream.c b/test_stream.c @@ -0,0 +1,37 @@ +#define main wmenu_main +#include "wmenu.c" +#undef main +#include <assert.h> + +int main(void) +{ + InitializeCriticalSection(&items_lock); + main_hwnd = CreateWindowW(L"STATIC", L"", WS_OVERLAPPEDWINDOW, 0, 0, 400, 300, NULL, NULL, NULL, NULL); + list_hwnd = CreateWindowW(L"LISTBOX", L"", WS_CHILD | WS_VSCROLL, 0, 0, 400, 300, main_hwnd, NULL, NULL, NULL); + assert(list_hwnd); + for (int i = 0; i < 10; ++i) { + char buf[32]; + sprintf(buf, "item-%d", i); + add_item(buf); + } + refill_list(""); + assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 10); + SendMessageW(list_hwnd, LB_SETCURSEL, 5, 0); + for (int i = 10; i < LIST_LIMIT + 10; ++i) { + char buf[32]; + sprintf(buf, "item-%d", i); + add_item(buf); + } + refill_list(""); + assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == LIST_LIMIT); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 5); + refill_list(""); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 5); + refill_list("item-9"); + assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) > 0); + refill_list(""); + assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == LIST_LIMIT); + DestroyWindow(main_hwnd); + DeleteCriticalSection(&items_lock); + return 0; +}
diff --git a/wmenu.c b/wmenu.c @@ -121,6 +121,8 @@ static DWORD WINAPI read_stdin_thread(void *unused) } struct ranked_item { int index; score_t score; }; +static int displayed[LIST_LIMIT], displayed_count; +static bool displayed_filtered; static int rank_cmp(const void *a, const void *b) { @@ -131,15 +133,33 @@ static int rank_cmp(const void *a, const void *b) static void refill_list(const char *query) { struct ranked_item ranked[LIST_LIMIT]; - int n = 0; - SendMessageW(list_hwnd, LB_RESETCONTENT, 0, 0); + int n = 0, old_selection = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); + int old_item = old_selection >= 0 && old_selection < displayed_count ? displayed[old_selection] : -1; + int old_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); + bool filtered = query && *query; + bool was_filtered = displayed_filtered; EnterCriticalSection(&items_lock); - if (!query || !*query) { - for (int i = 0; i < item_count && n < LIST_LIMIT; ++i) { + if (!filtered) { + if (was_filtered) { + SendMessageW(list_hwnd, WM_SETREDRAW, FALSE, 0); + SendMessageW(list_hwnd, LB_RESETCONTENT, 0, 0); + displayed_count = 0; + displayed_filtered = false; + } + /* Append only new rows. LB_ADDSTRING repaints affected rows itself. */ + for (int i = displayed_count; i < item_count && i < LIST_LIMIT; ++i) { wchar_t *w = widen(items[i]); SendMessageW(list_hwnd, LB_ADDSTRING, 0, (LPARAM)w); + displayed[displayed_count++] = i; free(w); - ++n; + } + if (was_filtered) { + SendMessageW(list_hwnd, LB_SETCURSEL, + old_item >= 0 && old_item < displayed_count ? old_item : 0, 0); + SendMessageW(list_hwnd, WM_SETREDRAW, TRUE, 0); + InvalidateRect(list_hwnd, NULL, TRUE); + } else if (displayed_count && old_selection < 0) { + SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0); } } else { int scan = item_count < SCAN_LIMIT ? item_count : SCAN_LIMIT; @@ -150,14 +170,30 @@ static void refill_list(const char *query) } } qsort(ranked, n, sizeof(*ranked), rank_cmp); - for (int i = 0; i < n; ++i) { - wchar_t *w = widen(items[ranked[i].index]); - SendMessageW(list_hwnd, LB_ADDSTRING, 0, (LPARAM)w); - free(w); + bool changed = !displayed_filtered || n != displayed_count; + for (int i = 0; !changed && i < n; ++i) + if (ranked[i].index != displayed[i]) changed = true; + if (changed) { + int selection = n ? 0 : -1; + SendMessageW(list_hwnd, WM_SETREDRAW, FALSE, 0); + SendMessageW(list_hwnd, LB_RESETCONTENT, 0, 0); + for (int i = 0; i < n; ++i) { + wchar_t *w = widen(items[ranked[i].index]); + displayed[i] = ranked[i].index; + SendMessageW(list_hwnd, LB_ADDSTRING, 0, (LPARAM)w); + if (displayed[i] == old_item) selection = i; + free(w); + } + displayed_count = n; + displayed_filtered = true; + if (selection >= 0) SendMessageW(list_hwnd, LB_SETCURSEL, selection, 0); + if (old_top > 0 && selection >= 0 && old_item >= 0 && displayed[selection] == old_item) + SendMessageW(list_hwnd, LB_SETTOPINDEX, old_top, 0); + SendMessageW(list_hwnd, WM_SETREDRAW, TRUE, 0); + InvalidateRect(list_hwnd, NULL, TRUE); } } LeaveCriticalSection(&items_lock); - if (n) SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0); } static char *current_text(void) @@ -353,7 +389,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) InterlockedExchange(&refresh_pending, 0); if (list_hwnd && loading_items) { char *q = text_encoded(input_hwnd); - refill_list(q); + if (q) refill_list(q); free(q); } return 0; @@ -362,8 +398,10 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) KillTimer(hwnd, TIMER_PROGRESS); if (progress_hwnd) ShowWindow(progress_hwnd, SW_HIDE); if (list_hwnd) { + char *q = text_encoded(input_hwnd); SetWindowPos(list_hwnd, NULL, 0, S(28), win_w, win_h - S(28), SWP_NOZORDER); - refill_list(""); + if (q) refill_list(q); + free(q); } return 0; case WM_TIMER: @@ -375,7 +413,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) return 0; case WM_COMMAND: if (LOWORD(wp) == ID_LABEL) return 0; - if (LOWORD(wp) == ID_EDIT && HIWORD(wp) == EN_CHANGE && has_items && !loading_items) { + if (LOWORD(wp) == ID_EDIT && HIWORD(wp) == EN_CHANGE && has_items) { char *q = text_encoded(input_hwnd); refill_list(q); free(q);
: nply/wmenu