: nply/wmenu

commit f0c7d6152842e3a30e95de2d8ee91888b3d4f277
parent b5a00bb375a1c1080a491000d76712486b6dae9d
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:05:06 +0530

Virtualize log list and search cooperatively with Win32 fibers
4 files changed, 253 insertions(+), 124 deletions(-)
M
HACKING
|
31
+++++++++++++++++++++----------
M
Makefile
|
3
+++
M
test_stream.c
|
98
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
M
wmenu.c
|
245
+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
diff --git a/HACKING b/HACKING
@@ -35,7 +35,7 @@ Important sections in wmenu.c:
 
 - input/output conversion: widen(), text_encoded(), current_text()
 - stdin loading: read_stdin_thread()
-- matching/list population: refill_list()
+- row mapping/search: row_item(), refill_list(), search_step(), update_count()
 - selection/output: add_selection(), finish()
 - key modes:
   - handle_global_keys()

@@ -77,12 +77,15 @@ Use -default-controls for normal Windows edit behavior.
 
 Release checklist
 -----------------
-1. Run make and make test (native ListBox append/selection regression).
+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).
 2. Check:
        .\\wmenu.exe -help
        .\\wmenu.exe -version
        dir /b | .\\wmenu.exe
-3. Check a slow pipe shows the bottom progress bar animating.
+3. Check a slow pipe shows the bottom progress bar animating; search a loaded
+   large file and check the same bar shows search progress.
 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.

@@ -97,10 +100,18 @@ 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. 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.
+- The classic Win32 ListBox uses LBS_NODATA | LBS_OWNERDRAWFIXED. LB_SETCOUNT
+  gives it the full row count; WM_DRAWITEM fetches only visible rows from items
+  or the filtered row-index array. The ListBox handles scrolling/selection,
+  but holds no copies of line text. Search runs in a Win32 fiber on the UI
+  thread, yields after each SEARCH_BATCH, and resumes through WM_SEARCH_STEP.
+  It never yields while holding items_lock. The reader remains a separate
+  thread. Matching rows are indexed in source order (fzy subsequence matching,
+  not score-ranked). No silent row cap. 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
+  (https://github.com/tstack/lnav/blob/master/src/textview_curses.hh,
+  text_line_count). We borrow the index/view separation, not lnav's log
+  parsing or on-disk line buffers: piped input is still held in memory.
diff --git a/Makefile b/Makefile
@@ -13,6 +13,9 @@ test: test_stream.c wmenu.c arg.h deps/fzy/src/match.c deps/fzy/config.h deps/fz
 	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
 
+stress: test
+	./test_stream.exe stress
+
 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
@@ -3,34 +3,92 @@
 #undef main
 #include <assert.h>
 
-int main(void)
+int main(int argc, char **argv)
 {
     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];
+    INITCOMMONCONTROLSEX icc = { sizeof icc, ICC_PROGRESS_CLASS };
+    assert(InitCommonControlsEx(&icc));
+    make_font();
+    win_w = 400;
+    win_h = 300;
+    input_done = true;
+    has_items = true;
+    WNDCLASSW wc = {0};
+    wc.lpfnWndProc = WndProc;
+    wc.hInstance = GetModuleHandleW(NULL);
+    wc.lpszClassName = L"wmenu-test";
+    assert(RegisterClassW(&wc));
+    main_hwnd = CreateWindowW(L"wmenu-test", L"", WS_OVERLAPPEDWINDOW, 0, 0, 400, 300, NULL, NULL, wc.hInstance, NULL);
+    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);
+    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) {
+        char buf[2700];
         sprintf(buf, "item-%d", i);
+        if (stress) {
+            size_t len = strlen(buf);
+            memset(buf + len, 'x', 2699 - len);
+            buf[2699] = 0; /* About 155 MiB of line data in stress mode. */
+        }
         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);
+    assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60000);
+    SendMessageW(list_hwnd, LB_SETCURSEL, 59999, 0);
+    char *last = current_text();
+    assert(last && !strncmp(last, "item-59999", 10));
+    free(last);
     refill_list("");
-    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 5);
-    refill_list("item-9");
-    assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) > 0);
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 59999);
+    refill_list("item-1");
+    search_step();
+    assert(search_pos == SEARCH_BATCH);
+    refill_list("item-59999"); /* Cancel the previous search and start over. */
+    assert(progress_visible);
+    while (search_pos < item_count) search_step();
+    assert(match_count == 1 && matches[0] == 59999);
+    assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1);
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0);
+    last = current_text();
+    assert(last && !strncmp(last, "item-59999", 10));
+    free(last);
+    assert(!progress_visible);
+    HDC screen = GetDC(NULL);
+    HDC dc = CreateCompatibleDC(screen);
+    HBITMAP bitmap = CreateCompatibleBitmap(screen, 250, 30);
+    HGDIOBJ old = SelectObject(dc, bitmap);
+    DRAWITEMSTRUCT draw = {0};
+    draw.CtlID = ID_COMBO;
+    draw.itemID = 0;
+    draw.itemState = ODS_SELECTED;
+    draw.hDC = dc;
+    draw.rcItem.right = 250;
+    draw.rcItem.bottom = 30;
+    assert(WndProc(main_hwnd, WM_DRAWITEM, ID_COMBO, (LPARAM)&draw) == TRUE);
+    assert(GetPixel(dc, 1, 1) == GetSysColor(COLOR_HIGHLIGHT));
+    SelectObject(dc, old);
+    DeleteObject(bitmap);
+    DeleteDC(dc);
+    ReleaseDC(NULL, screen);
     refill_list("");
-    assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == LIST_LIMIT);
+    assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60000);
+    refill_list("tail-special");
+    search_step();
+    input_done = false;
+    add_item("tail-special");
+    while (search_pos < item_count) search_step();
+    assert(match_count == 1 && matches[0] == 60000);
+    input_done = true;
+    update_progress();
+    assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1);
+    DeleteFiber(search_fiber);
+    ConvertFiberToThread();
     DestroyWindow(main_hwnd);
     DeleteCriticalSection(&items_lock);
     return 0;
diff --git a/wmenu.c b/wmenu.c
@@ -23,8 +23,8 @@
 #define TIMER_PROGRESS 201
 #define WM_STDIN_CHUNK (WM_APP + 1)
 #define WM_STDIN_DONE (WM_APP + 2)
-#define LIST_LIMIT 1000
-#define SCAN_LIMIT 50000
+#define WM_SEARCH_STEP (WM_APP + 3)
+#define SEARCH_BATCH 1024
 #define CHUNK_LINES 2048
 
 static int dpi = 96;

@@ -32,11 +32,14 @@ static int dpi = 96;
 
 static char **items, **selected;
 static int item_count, item_capacity, selected_count;
+static int *matches, match_count, match_capacity, search_pos;
+static char *search_query;
 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;
 static WNDPROC old_input_proc, old_list_proc;
+static LPVOID ui_fiber, search_fiber;
 static CRITICAL_SECTION items_lock;
 static int win_x = 0, win_y = 0, win_w = 0, win_h = 0, lines = 5;
 static int font_size = 0, font_weight = FW_NORMAL;

@@ -44,7 +47,8 @@ static UINT text_codepage = CP_UTF8;
 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 bool center_window, close_on_blur, stdin_pipe, loading_items, input_done, progress_visible;
+static volatile LONG search_pending;
 static int progress_pos, progress_dir = 5;
 char *argv0;
 

@@ -120,104 +124,106 @@ static DWORD WINAPI read_stdin_thread(void *unused)
     return 0;
 }
 
-struct ranked_item { int index; score_t score; };
-static int displayed[LIST_LIMIT], displayed_count;
-static bool displayed_filtered;
+/* The ListBox stores only a count. Its row IDs map into items or matches. */
+static int row_item(int row)
+{
+    if (row < 0) return -1;
+    if (search_query && *search_query)
+        return row < match_count ? matches[row] : -1;
+    return row < item_count ? row : -1;
+}
 
-static int rank_cmp(const void *a, const void *b)
+static void update_count(void)
 {
-    const struct ranked_item *x = (const struct ranked_item *)a, *y = (const struct ranked_item *)b;
-    return x->score < y->score ? 1 : x->score > y->score ? -1 : x->index - y->index;
+    int count = search_query && *search_query ? match_count : item_count;
+    if (SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == count) return;
+    int selected = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0);
+    SendMessageW(list_hwnd, LB_SETCOUNT, count, 0);
+    if (count && selected < 0) SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0);
 }
 
-static void refill_list(const char *query)
+static void update_progress(void)
 {
-    struct ranked_item ranked[LIST_LIMIT];
-    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 (!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);
-        }
-        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;
-        for (int i = 0; i < scan && 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);
-        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);
+    bool searching = input_done && search_query && *search_query && search_pos < item_count;
+    bool visible = !input_done || searching;
+    if (visible != progress_visible) {
+        ShowWindow(progress_hwnd, visible ? SW_SHOW : SW_HIDE);
+        SetWindowPos(list_hwnd, NULL, 0, S(28), win_w,
+                     win_h - S(28) - (visible ? S(10) : 0), SWP_NOZORDER);
+        progress_visible = visible;
+    }
+    if (searching)
+        SendMessageW(progress_hwnd, PBM_SETPOS,
+                     item_count ? (int)(100LL * search_pos / item_count) : 100, 0);
+}
+
+static void queue_search(void)
+{
+    if (!InterlockedExchange(&search_pending, 1))
+        PostMessageW(main_hwnd, WM_SEARCH_STEP, 0, 0);
+}
+
+static VOID WINAPI search_fiber_run(void *unused)
+{
+    (void)unused;
+    for (;;) {
+        EnterCriticalSection(&items_lock);
+        int end = item_count < search_pos + SEARCH_BATCH ? item_count : search_pos + SEARCH_BATCH;
+        for (; search_pos < end; ++search_pos) {
+            if (!has_match(search_query, items[search_pos])) continue;
+            if (match_count == match_capacity) {
+                int next = match_capacity ? match_capacity * 2 : 1024;
+                int *grown = (int *)realloc(matches, (size_t)next * sizeof(*matches));
+                if (!grown) { PostQuitMessage(1); search_pos = item_count; break; }
+                matches = grown;
+                match_capacity = next;
             }
-            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);
+            matches[match_count++] = search_pos;
         }
+        LeaveCriticalSection(&items_lock);
+        SwitchToFiber(ui_fiber);  /* Never yield while holding items_lock. */
     }
-    LeaveCriticalSection(&items_lock);
+}
+
+static void search_step(void)
+{
+    if (!search_query || !*search_query || search_pos >= item_count) return;
+    SwitchToFiber(search_fiber);
+    update_count();
+    update_progress();
+    if (search_pos < item_count) queue_search();
+}
+
+static void refill_list(const char *query)
+{
+    if (!query) return;
+    if (!search_query || strcmp(query, search_query)) {
+        char *copy = _strdup(query);
+        if (!copy) return;
+        free(search_query);
+        search_query = copy;
+        match_count = 0;
+        search_pos = 0;
+        if (*search_query) SendMessageW(list_hwnd, LB_SETCOUNT, 0, 0);
+        else update_count();
+    }
+    if (*search_query && search_pos < item_count) queue_search();
+    else if (!*search_query) update_count();
+    if (progress_hwnd) update_progress();
 }
 
 static char *current_text(void)
 {
-    wchar_t *w;
-    int n, m;
-    char *s;
-    LRESULT sel = list_hwnd ? SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) : LB_ERR;
-    if (has_items && sel != LB_ERR) {
-        n = (int)SendMessageW(list_hwnd, LB_GETTEXTLEN, sel, 0) + 1;
-        w = (wchar_t *)calloc(n, sizeof(wchar_t));
-        if (!w) return NULL;
-        SendMessageW(list_hwnd, LB_GETTEXT, sel, (LPARAM)w);
-    } else {
-        n = GetWindowTextLengthW(input_hwnd) + 1;
-        w = (wchar_t *)calloc(n, sizeof(wchar_t));
-        if (!w) return NULL;
-        GetWindowTextW(input_hwnd, w, n);
+    int sel = list_hwnd ? (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) : -1;
+    if (has_items && sel >= 0) {
+        char *s = NULL;
+        EnterCriticalSection(&items_lock);
+        int index = row_item(sel);
+        if (index >= 0 && index < item_count) s = _strdup(items[index]);
+        LeaveCriticalSection(&items_lock);
+        return s;
     }
-    m = WideCharToMultiByte(text_codepage, 0, w, -1, NULL, 0, NULL, NULL);
-    s = (char *)calloc(m ? m : 1, 1);
-    if (s) WideCharToMultiByte(text_codepage, 0, w, -1, s, m, NULL, NULL);
-    free(w);
-    return s;
+    return text_encoded(input_hwnd);
 }
 
 static void add_selection(void)

@@ -387,30 +393,69 @@ 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) {
+        if (list_hwnd) {
             char *q = text_encoded(input_hwnd);
             if (q) refill_list(q);
             free(q);
         }
         return 0;
     case WM_STDIN_DONE:
+        input_done = true;
         has_items = item_count > 0;
         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);
             if (q) refill_list(q);
             free(q);
+            update_progress();
         }
         return 0;
+    case WM_SEARCH_STEP:
+        InterlockedExchange(&search_pending, 0);
+        search_step();
+        return 0;
     case WM_TIMER:
-        if (wp == TIMER_PROGRESS && progress_hwnd) {
+        if (wp == TIMER_PROGRESS && progress_hwnd && !input_done) {
             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_MEASUREITEM: {
+        MEASUREITEMSTRUCT *measure = (MEASUREITEMSTRUCT *)lp;
+        if (measure->CtlID != ID_COMBO) break;
+        HDC dc = GetDC(hwnd);
+        HFONT old = (HFONT)SelectObject(dc, ui_font);
+        TEXTMETRICW tm;
+        GetTextMetricsW(dc, &tm);
+        SelectObject(dc, old);
+        ReleaseDC(hwnd, dc);
+        measure->itemHeight = tm.tmHeight + S(4);
+        return TRUE;
+    }
+    case WM_DRAWITEM: {
+        DRAWITEMSTRUCT *draw = (DRAWITEMSTRUCT *)lp;
+        if (draw->CtlID != ID_COMBO || draw->itemID == (UINT)-1) break;
+        bool selected_row = (draw->itemState & ODS_SELECTED) != 0;
+        FillRect(draw->hDC, &draw->rcItem,
+                 GetSysColorBrush(selected_row ? COLOR_HIGHLIGHT : COLOR_WINDOW));
+        EnterCriticalSection(&items_lock);
+        int index = row_item((int)draw->itemID);
+        wchar_t *w = index >= 0 && index < item_count ? widen(items[index]) : NULL;
+        LeaveCriticalSection(&items_lock);
+        if (w) {
+            HFONT old = (HFONT)SelectObject(draw->hDC, ui_font);
+            SetBkMode(draw->hDC, TRANSPARENT);
+            SetTextColor(draw->hDC, GetSysColor(selected_row ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT));
+            RECT rect = draw->rcItem;
+            rect.left += S(4);
+            DrawTextW(draw->hDC, w, -1, &rect, DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS | DT_NOPREFIX);
+            SelectObject(draw->hDC, old);
+            free(w);
+        }
+        if (draw->itemState & ODS_FOCUS) DrawFocusRect(draw->hDC, &draw->rcItem);
+        return TRUE;
+    }
     case WM_COMMAND:
         if (LOWORD(wp) == ID_LABEL) return 0;
         if (LOWORD(wp) == ID_EDIT && HIWORD(wp) == EN_CHANGE && has_items) {

@@ -505,6 +550,7 @@ int main(int argc, char **argv)
 
     InitializeCriticalSection(&items_lock);
     stdin_pipe = !_isatty(_fileno(stdin));
+    input_done = !stdin_pipe;
     if (stdin_pipe) { has_items = true; loading_items = true; }
 
     SetProcessDPIAware();

@@ -539,10 +585,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(10) : 0), main_hwnd, (HMENU)ID_COMBO, inst, NULL);
+        list_hwnd = CreateWindowW(WC_LISTBOXW, L"", WS_CHILD | WS_VISIBLE | WS_BORDER | LBS_NOTIFY | LBS_NODATA | LBS_OWNERDRAWFIXED | 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), 0, win_h - S(10), win_w, S(10), main_hwnd, (HMENU)ID_PROGRESS, inst, NULL);
+        progress_visible = loading_items;
         SendMessageW(progress_hwnd, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
         if (!loading_items) refill_list("");
     } else {

@@ -550,6 +597,12 @@ int main(int argc, char **argv)
         set_font(input_hwnd);
     }
     old_input_proc = (WNDPROC)SetWindowLongPtrW(input_hwnd, GWLP_WNDPROC, (LONG_PTR)InputProc);
+    if (has_items) {
+        ui_fiber = ConvertThreadToFiber(NULL);
+        if (!ui_fiber) return 1;
+        search_fiber = CreateFiber(0, search_fiber_run, NULL);
+        if (!search_fiber) { ConvertFiberToThread(); return 1; }
+    }
 
     ShowWindow(main_hwnd, SW_SHOW);
     UpdateWindow(main_hwnd);

@@ -562,5 +615,9 @@ int main(int argc, char **argv)
         TranslateMessage(&msg);
         DispatchMessageW(&msg);
     }
-    return 0;
+    if (search_fiber) {
+        DeleteFiber(search_fiber);
+        ConvertFiberToThread();
+    }
+    return (int)msg.wParam;
 }