: nply/wmenu

commit 27505a573a4fb41b681de41d3b80dde0e3fac625
parent 767655d6eb5e7d11a499ca54f7eda36f19423a09
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:57:34 +0530

Preserve long piped lines and handle allocation failures
5 files changed, 137 insertions(+), 31 deletions(-)
M
HACKING
|
8
+++++---
M
requirements.ini
|
2
+-
M
test_pipe.ps1
|
15
++++++++++++---
M
test_stream.c
|
35
++++++++++++++++++++++++++++++-----
M
wmenu.c
|
108
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
diff --git a/HACKING b/HACKING
@@ -14,9 +14,11 @@ Dependencies
 ------------
 External source is pulled with mor from requirements.ini:
 
-    bash C:/Users/bhara/Projects/mor/mor.sh .
+    bash /path/to/mor.sh .
 
-Currently this fetches fzy into deps/fzy. The build uses only fzy's matcher:
+This fetches fzy v1.1 into deps/fzy (requirements.ini pins the tag).
+For an existing checkout, remove deps/fzy before rerunning mor: mor does not
+update directories already present. The build uses only fzy's matcher:
 
     deps/fzy/src/match.c
     deps/fzy/src/match.h

@@ -80,7 +82,7 @@ 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). Run `powershell.exe -NoProfile -ExecutionPolicy Bypass -File
-   test_pipe.ps1` to verify rows appear before pipe EOF. Run
+   test_pipe.ps1` to verify complete long rows appear before pipe EOF. Run
    `powershell.exe -NoProfile -ExecutionPolicy Bypass -File test_typing.ps1`
    to measure typing latency with 20,000 streamed rows and stdin still open.
 2. Check:
diff --git a/requirements.ini b/requirements.ini
@@ -2,4 +2,4 @@
 /=deps
 
 [.]
-fzy=https://github.com/jhawthorn/fzy.git@master
+fzy=https://github.com/jhawthorn/fzy.git@v1.1
diff --git a/test_pipe.ps1 b/test_pipe.ps1
@@ -30,15 +30,24 @@ try {
         }
     }
     if ($count -ne 1) { throw "Expected row before pipe closes; got $count, hwnd=$win, list=$list, exited=$($p.HasExited)" }
+    $p.StandardInput.WriteLine('long-' + ('x' * 8192))
     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++) {
+    for ($i = 0; $i -lt 100 -and $more -lt 3002; $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."
+    if ($more -ne 3002) { throw "Expected 3,002 rows including one long line before EOF; got $more" }
+    $edit = [NativeMenu]::FindWindowEx($win, [IntPtr]::Zero, 'Edit', $null)
+    if ($edit -eq [IntPtr]::Zero) { throw 'No edit control' }
+    [void][NativeMenu]::SendMessage($list, 0x186, [IntPtr]1, [IntPtr]::Zero) # LB_SETCURSEL
+    if ([NativeMenu]::SendMessage($list, 0x188, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() -ne 1) { throw 'Could not select the long line' }
+    $output = $p.StandardOutput.ReadToEndAsync()
+    [void][NativeMenu]::SendMessage($edit, 0x100, [IntPtr]13, [IntPtr]::Zero) # WM_KEYDOWN, Enter
+    if (-not $p.WaitForExit(5000)) { throw 'wmenu did not exit after Enter' }
+    if ($output.GetAwaiter().GetResult() -ne ('long-' + ('x' * 8192))) { throw 'Long row was not preserved on stdout' }
+    Write-Output "List showed $more complete rows before EOF; long row output intact."
 } finally {
     if (-not $p.HasExited) { $p.Kill(); $p.WaitForExit() }
     $p.Dispose()
diff --git a/test_stream.c b/test_stream.c
@@ -17,6 +17,24 @@ static LRESULT CALLBACK TestWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
 
 int main(int argc, char **argv)
 {
+    FILE *input = tmpfile();
+    assert(input);
+    char long_row[8193];
+    memset(long_row, 'x', sizeof long_row - 1);
+    long_row[sizeof long_row - 1] = 0;
+    assert(fputs(long_row, input) >= 0);
+    assert(fputs("\r\nnext\r\npartial", input) >= 0);
+    rewind(input);
+    char *line = NULL;
+    size_t capacity = 0;
+    assert(read_line(input, &line, &capacity) == 1);
+    assert(!strcmp(line, long_row));
+    assert(read_line(input, &line, &capacity) == 1 && !strcmp(line, "next"));
+    assert(read_line(input, &line, &capacity) == 1 && !strcmp(line, "partial"));
+    assert(read_line(input, &line, &capacity) == 0);
+    free(line);
+    fclose(input);
+
     InitializeCriticalSection(&items_lock);
     INITCOMMONCONTROLSEX icc = { sizeof icc, ICC_PROGRESS_CLASS };
     assert(InitCommonControlsEx(&icc));

@@ -43,7 +61,14 @@ int main(int argc, char **argv)
     bool stress = argc > 1 && !strcmp(argv[1], "stress");
     ShowWindow(main_hwnd, SW_SHOWNOACTIVATE);
     ShowWindow(list_hwnd, SW_SHOWNOACTIVATE);
-    add_item("item-0");
+    assert(add_item("item-0"));
+    int saved_capacity = item_capacity;
+    char **saved_items = items;
+    item_count = item_capacity = INT_MAX;
+    assert(!add_item("overflow"));
+    assert(item_count == INT_MAX && items == saved_items);
+    item_count = 1;
+    item_capacity = saved_capacity;
     WndProc(main_hwnd, WM_STDIN_CHUNK, 0, 0);
     UpdateWindow(list_hwnd);
     assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1);

@@ -56,7 +81,7 @@ int main(int argc, char **argv)
             memset(buf + len, 'x', 2699 - len);
             buf[2699] = 0; /* About 155 MiB of line data in stress mode. */
         }
-        add_item(buf);
+        assert(add_item(buf));
     }
     refill_list("");
     assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60000);

@@ -93,7 +118,7 @@ int main(int argc, char **argv)
     location_count = location_cursor = 0;
     list_move(-2);
     list_move(5);
-    add_item("extra-row");
+    assert(add_item("extra-row"));
     input_done = false;
     WndProc(main_hwnd, WM_TIMER, TIMER_PROGRESS, 0); /* Partial batch before EOF. */
     input_done = true;

@@ -187,7 +212,7 @@ int main(int argc, char **argv)
     assert(!GetUpdateRect(list_hwnd, NULL, FALSE));
     SendMessageW(list_hwnd, LB_SETCURSEL, 59999, 0);
     int kept_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0);
-    add_item("item-60000");
+    assert(add_item("item-60000"));
     refill_list("");
     assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60002);
     assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 59999);

@@ -198,7 +223,7 @@ int main(int argc, char **argv)
     refill_list("tail-special");
     search_step();
     input_done = false;
-    add_item("tail-special");
+    assert(add_item("tail-special"));
     while (search_pos < item_count) search_step();
     assert(match_count == 1 && matches[0] == 60002);
     input_done = true;
diff --git a/wmenu.c b/wmenu.c
@@ -7,6 +7,8 @@
 #include <stdlib.h>
 #include <string.h>
 #include <io.h>
+#include <limits.h>
+#include <stdint.h>
 #include <wctype.h>
 
 #include "arg.h"

@@ -80,15 +82,35 @@ static char *text_encoded(HWND hwnd)
     return s;
 }
 
-static void add_item(const char *s)
+static bool add_item(const char *s)
 {
+    char *copy = _strdup(s ? s : "");
+    if (!copy) return false;
     EnterCriticalSection(&items_lock);
     if (item_count == item_capacity) {
-        item_capacity = item_capacity ? item_capacity * 2 : 1024;
-        items = (char **)realloc(items, sizeof(char *) * item_capacity);
+        if (item_capacity > INT_MAX / 2) {
+            LeaveCriticalSection(&items_lock);
+            free(copy);
+            return false;
+        }
+        int next = item_capacity ? item_capacity * 2 : 1024;
+        if ((size_t)next > SIZE_MAX / sizeof(*items)) {
+            LeaveCriticalSection(&items_lock);
+            free(copy);
+            return false;
+        }
+        char **grown = (char **)realloc(items, (size_t)next * sizeof(*items));
+        if (!grown) {
+            LeaveCriticalSection(&items_lock);
+            free(copy);
+            return false;
+        }
+        items = grown;
+        item_capacity = next;
     }
-    items[item_count++] = _strdup(s ? s : "");
+    items[item_count++] = copy;
     LeaveCriticalSection(&items_lock);
+    return true;
 }
 
 static void make_font(void)

@@ -111,21 +133,49 @@ static void set_font(HWND hwnd)
     if (ui_font && hwnd) SendMessageW(hwnd, WM_SETFONT, (WPARAM)ui_font, TRUE);
 }
 
+/* Read one complete line, growing the buffer rather than splitting long rows. */
+static int read_line(FILE *stream, char **line, size_t *capacity)
+{
+    size_t len = 0;
+    for (;;) {
+        if (*capacity - len < 2) {
+            if (*capacity > INT_MAX / 2) return -1;
+            size_t next = *capacity ? *capacity * 2 : 4096;
+            char *grown = (char *)realloc(*line, next);
+            if (!grown) return -1;
+            *line = grown;
+            *capacity = next;
+        }
+        if (!fgets(*line + len, (int)(*capacity - len), stream)) {
+            if (ferror(stream)) return -1;
+            if (!len) return 0;
+            break;
+        }
+        len += strlen(*line + len);
+        if (len && (*line)[len - 1] == '\n') break;
+    }
+    if (len && (*line)[len - 1] == '\n') --len;
+    if (len && (*line)[len - 1] == '\r') --len;
+    (*line)[len] = 0;
+    return 1;
+}
+
 static DWORD WINAPI read_stdin_thread(void *unused)
 {
-    char buf[4096];
-    int n = 0;
+    char *line = NULL;
+    size_t capacity = 0;
+    int n = 0, status;
     (void)unused;
-    while (fgets(buf, sizeof buf, stdin)) {
-        buf[strcspn(buf, "\r\n")] = 0;
-        if (*buf) {
-            add_item(buf);
+    while ((status = read_line(stdin, &line, &capacity)) > 0) {
+        if (*line) {
+            if (!add_item(line)) { status = -1; break; }
             if ((++n == 1 || n % CHUNK_LINES == 0) && !InterlockedExchange(&refresh_pending, 1))
                 PostMessageW(main_hwnd, WM_STDIN_CHUNK, 0, 0);
         }
     }
+    free(line);
     loading_items = false;
-    PostMessageW(main_hwnd, WM_STDIN_DONE, 0, 0);
+    PostMessageW(main_hwnd, WM_STDIN_DONE, status < 0, 0);
     return 0;
 }
 

@@ -293,7 +343,9 @@ static VOID WINAPI search_fiber_run(void *unused)
         for (; search_pos < end; ++search_pos) {
             if (!has_match(search_query, items[search_pos])) continue;
             if (match_count == match_capacity) {
+                if (match_capacity > INT_MAX / 2) { PostQuitMessage(1); search_pos = item_count; break; }
                 int next = match_capacity ? match_capacity * 2 : 1024;
+                if ((size_t)next > SIZE_MAX / sizeof(*matches)) { PostQuitMessage(1); search_pos = item_count; break; }
                 int *grown = (int *)realloc(matches, (size_t)next * sizeof(*matches));
                 if (!grown) { PostQuitMessage(1); search_pos = item_count; break; }
                 matches = grown;

@@ -387,8 +439,22 @@ static char *current_text(void)
 static void add_selection(void)
 {
     char *s = current_text();
-    if (!s || !*s) { free(s); return; }
-    selected = (char **)realloc(selected, sizeof(char *) * (selected_count + 1));
+    if (!s) { fputs("wmenu: out of memory selecting item\n", stderr); PostQuitMessage(1); return; }
+    if (!*s) { free(s); return; }
+    if (selected_count == INT_MAX || (size_t)(selected_count + 1) > SIZE_MAX / sizeof(*selected)) {
+        free(s);
+        fputs("wmenu: too many selections\n", stderr);
+        PostQuitMessage(1);
+        return;
+    }
+    char **grown = (char **)realloc(selected, sizeof(*selected) * (size_t)(selected_count + 1));
+    if (!grown) {
+        free(s);
+        fputs("wmenu: out of memory selecting item\n", stderr);
+        PostQuitMessage(1);
+        return;
+    }
+    selected = grown;
     selected[selected_count++] = s;
     SetWindowTextW(input_hwnd, L"");
     if (has_items) refill_list("");

@@ -396,12 +462,11 @@ static void add_selection(void)
 
 static void finish(void)
 {
+    char *s = input_hwnd ? current_text() : NULL;
+    if (input_hwnd && !s) { fputs("wmenu: out of memory accepting item\n", stderr); PostQuitMessage(1); return; }
     for (int i = 0; i < selected_count; ++i) printf("%s\n", selected[i]);
-    if (input_hwnd) {
-        char *s = current_text();
-        if (s && *s) fputs(s, stdout);
-        free(s);
-    }
+    if (s && *s) fputs(s, stdout);
+    free(s);
     PostQuitMessage(0);
 }
 

@@ -582,6 +647,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
         }
         return 0;
     case WM_STDIN_DONE:
+        if (wp) { fputs("wmenu: failed to read stdin (I/O or memory error)\n", stderr); PostQuitMessage(1); return 0; }
         input_done = true;
         has_items = item_count > 0;
         KillTimer(hwnd, TIMER_PROGRESS);

@@ -705,6 +771,7 @@ int main(int argc, char **argv)
     } break;
     case 'p': {
         wchar_t *w = widen(EARGF(usage()));
+        if (!w) { fputs("wmenu: out of memory reading prompt\n", stderr); return 1; }
         has_prompt = true;
         wcsncpy(prompt, w, 299);
         prompt[299] = 0;

@@ -715,6 +782,7 @@ int main(int argc, char **argv)
         if (argv[0][1] == 'n') {
             argv[0]++;
             w = widen(EARGF(usage()));
+            if (!w) { fputs("wmenu: out of memory reading font name\n", stderr); return 1; }
             custom_font = true;
             wcsncpy(font_name, w, LF_FACESIZE - 1);
             font_name[LF_FACESIZE - 1] = 0;

@@ -803,7 +871,9 @@ int main(int argc, char **argv)
     SetFocus(input_hwnd);
     if (loading_items) {
         SetTimer(main_hwnd, TIMER_PROGRESS, 30, NULL);
-        CreateThread(NULL, 0, read_stdin_thread, NULL, 0, NULL);
+        HANDLE reader = CreateThread(NULL, 0, read_stdin_thread, NULL, 0, NULL);
+        if (!reader) { fputs("wmenu: cannot start stdin reader\n", stderr); return 1; }
+        CloseHandle(reader);
     }
     while (GetMessageW(&msg, NULL, 0, 0) > 0) {
         TranslateMessage(&msg);