: nply/wmenu

commit ba8693554ce96dce515859406227ddeb5d19af86
parent 14afa3e42b7e02c07e90a26caa38a54d9efb69ff
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 21:37:20 +0530

Keep huge streams responsive
2 files changed, 55 insertions(+), 26 deletions(-)
M
HACKING
|
11
++++++-----
M
wmenu.c
|
70
+++++++++++++++++++++++++++++++++++++++++++++++++---------------------
diff --git a/HACKING b/HACKING
@@ -79,7 +79,7 @@ Release checklist
        .\\wmenu.exe -help
        .\\wmenu.exe -version
        dir /b | .\\wmenu.exe
-3. Check a slow pipe shows the progress bar.
+3. Check a slow pipe shows the bottom progress bar animating.
 4. Check Alt-Tab back focuses the input.
 5. Check Esc exits from input and list focus.
 6. Update the version string in wmenu.c.

@@ -94,7 +94,8 @@ 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.
-- List filtering rebuilds the whole ListBox on every edit. That is fine for
-  normal launcher-sized lists. Optimize only with measured pain.
-- Stdin is loaded on a background thread, then the list is populated once done.
-  Incremental display can be added later if huge/slow streams matter.
+- 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.
+- Filtering stops after LIST_LIMIT matches. This intentionally favors staying
+  interactive over exhaustive ranking on massive inputs.
diff --git a/wmenu.c b/wmenu.c
@@ -19,14 +19,18 @@
 #define ID_EDIT 102
 #define ID_LABEL 103
 #define ID_PROGRESS 104
+#define TIMER_PROGRESS 201
 #define WM_STDIN_CHUNK (WM_APP + 1)
 #define WM_STDIN_DONE (WM_APP + 2)
+#define LIST_LIMIT 1000
+#define CHUNK_LINES 2048
 
 static int dpi = 96;
 #define S(px) MulDiv((px), dpi, 96)
 
 static char **items, **selected;
-static int item_count, selected_count;
+static int item_count, item_capacity, selected_count;
+static volatile LONG refresh_pending;
 static bool has_items, has_prompt, multi_select, vi_mode, normal_mode, default_controls;
 static wchar_t prompt[300];
 static HWND main_hwnd, input_hwnd, list_hwnd, progress_hwnd;

@@ -39,6 +43,7 @@ static wchar_t font_name[LF_FACESIZE];
 static HFONT ui_font;
 static bool custom_font;
 static bool center_window, close_on_blur, stdin_pipe, loading_items;
+static int progress_pos, progress_dir = 5;
 
 static wchar_t *widen(const char *s)
 {

@@ -66,7 +71,10 @@ static char *text_encoded(HWND hwnd)
 static void add_item(const char *s)
 {
     EnterCriticalSection(&items_lock);
-    items = (char **)realloc(items, sizeof(char *) * (item_count + 1));
+    if (item_count == item_capacity) {
+        item_capacity = item_capacity ? item_capacity * 2 : 1024;
+        items = (char **)realloc(items, sizeof(char *) * item_capacity);
+    }
     items[item_count++] = _strdup(s ? s : "");
     LeaveCriticalSection(&items_lock);
 }

@@ -100,7 +108,8 @@ static DWORD WINAPI read_stdin_thread(void *unused)
         buf[strcspn(buf, "\r\n")] = 0;
         if (*buf) {
             add_item(buf);
-            if (++n == 1 || n % 16 == 0) PostMessageW(main_hwnd, WM_STDIN_CHUNK, 0, 0);
+            if ((++n == 1 || n % CHUNK_LINES == 0) && !InterlockedExchange(&refresh_pending, 1))
+                PostMessageW(main_hwnd, WM_STDIN_CHUNK, 0, 0);
         }
     }
     loading_items = false;

@@ -118,26 +127,33 @@ static int rank_cmp(const void *a, const void *b)
 
 static void refill_list(const char *query)
 {
-    struct ranked_item *ranked;
+    struct ranked_item ranked[LIST_LIMIT];
     int n = 0;
-    EnterCriticalSection(&items_lock);
-    ranked = (struct ranked_item *)calloc(item_count, sizeof(*ranked));
     SendMessageW(list_hwnd, LB_RESETCONTENT, 0, 0);
-    for (int i = 0; i < item_count; ++i) {
-        if (!query || !*query || has_match(query, items[i])) {
-            ranked[n].index = i;
-            ranked[n++].score = query && *query ? match(query, items[i]) : 0;
+    EnterCriticalSection(&items_lock);
+    if (!query || !*query) {
+        for (int i = 0; i < item_count && n < LIST_LIMIT; ++i) {
+            wchar_t *w = widen(items[i]);
+            SendMessageW(list_hwnd, LB_ADDSTRING, 0, (LPARAM)w);
+            free(w);
+            ++n;
+        }
+    } else {
+        for (int i = 0; i < item_count && n < LIST_LIMIT; ++i) {
+            if (has_match(query, items[i])) {
+                ranked[n].index = i;
+                ranked[n++].score = match(query, items[i]);
+            }
+        }
+        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);
         }
-    }
-    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);
     }
     LeaveCriticalSection(&items_lock);
     if (n) SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0);
-    free(ranked);
 }
 
 static char *current_text(void)

@@ -330,6 +346,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
 {
     switch (msg) {
     case WM_STDIN_CHUNK:
+        InterlockedExchange(&refresh_pending, 0);
         if (list_hwnd && loading_items) {
             char *q = text_encoded(input_hwnd);
             refill_list(q);

@@ -338,12 +355,20 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
         return 0;
     case WM_STDIN_DONE:
         has_items = item_count > 0;
+        KillTimer(hwnd, TIMER_PROGRESS);
         if (progress_hwnd) ShowWindow(progress_hwnd, SW_HIDE);
         if (list_hwnd) {
             SetWindowPos(list_hwnd, NULL, 0, S(28), win_w, win_h - S(28), SWP_NOZORDER);
             refill_list("");
         }
         return 0;
+    case WM_TIMER:
+        if (wp == TIMER_PROGRESS && progress_hwnd) {
+            progress_pos += progress_dir;
+            if (progress_pos >= 100 || progress_pos <= 0) progress_dir = -progress_dir;
+            SendMessageW(progress_hwnd, PBM_SETPOS, progress_pos, 0);
+        }
+        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) {

@@ -445,11 +470,11 @@ int main(int argc, char **argv)
     if (has_items) {
         input_hwnd = CreateWindowW(WC_EDITW, L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, prompt_w, S(2), win_w - prompt_w, S(24), main_hwnd, (HMENU)ID_EDIT, inst, NULL);
         set_font(input_hwnd);
-        list_hwnd = CreateWindowW(WC_LISTBOXW, L"", WS_CHILD | WS_VISIBLE | WS_BORDER | LBS_NOTIFY | WS_VSCROLL, 0, S(28), win_w, win_h - S(28) - (loading_items ? S(6) : 0), main_hwnd, (HMENU)ID_COMBO, inst, NULL);
+        list_hwnd = CreateWindowW(WC_LISTBOXW, L"", WS_CHILD | WS_VISIBLE | WS_BORDER | LBS_NOTIFY | WS_VSCROLL, 0, S(28), win_w, win_h - S(28) - (loading_items ? S(10) : 0), main_hwnd, (HMENU)ID_COMBO, inst, NULL);
         set_font(list_hwnd);
         old_list_proc = (WNDPROC)SetWindowLongPtrW(list_hwnd, GWLP_WNDPROC, (LONG_PTR)ListProc);
-        progress_hwnd = CreateWindowW(PROGRESS_CLASSW, L"", WS_CHILD | (loading_items ? WS_VISIBLE : 0) | PBS_MARQUEE, 0, win_h - S(6), win_w, S(6), main_hwnd, (HMENU)ID_PROGRESS, inst, NULL);
-        SendMessageW(progress_hwnd, PBM_SETMARQUEE, TRUE, 30);
+        progress_hwnd = CreateWindowW(PROGRESS_CLASSW, L"", WS_CHILD | (loading_items ? WS_VISIBLE : 0), 0, win_h - S(10), win_w, S(10), main_hwnd, (HMENU)ID_PROGRESS, inst, NULL);
+        SendMessageW(progress_hwnd, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
         if (!loading_items) refill_list("");
     } else {
         input_hwnd = CreateWindowW(WC_EDITW, L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, prompt_w, S(2), win_w - prompt_w, S(24), main_hwnd, (HMENU)ID_EDIT, inst, NULL);

@@ -460,7 +485,10 @@ int main(int argc, char **argv)
     ShowWindow(main_hwnd, SW_SHOW);
     UpdateWindow(main_hwnd);
     SetFocus(input_hwnd);
-    if (loading_items) CreateThread(NULL, 0, read_stdin_thread, NULL, 0, NULL);
+    if (loading_items) {
+        SetTimer(main_hwnd, TIMER_PROGRESS, 30, NULL);
+        CreateThread(NULL, 0, read_stdin_thread, NULL, 0, NULL);
+    }
     while (GetMessageW(&msg, NULL, 0, 0) > 0) {
         TranslateMessage(&msg);
         DispatchMessageW(&msg);