: nply/wmenu

commit 2f8c87d6079876046a718f2490a30dbfb2020a8e
parent 40397a4342d58a3cdbafac60f5da7e0355e60637
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:30:52 +0530

Reset view on search clear and add bounded location navigation
4 files changed, 125 insertions(+), 7 deletions(-)
M
HACKING
|
5
++++-
M
README
|
4
++++
M
test_stream.c
|
52
++++++++++++++++++++++++++++++++++++++++++++++++----
M
wmenu.c
|
71
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
diff --git a/HACKING b/HACKING
@@ -114,7 +114,10 @@ Known simplifications
   queued stream messages cannot starve WM_PAINT. Restore the selection and
   top row after LB_SETCOUNT (Windows can otherwise jump to the end), and
   preserve pending selection paints when validating the viewport. The bottom
-  bar shows search progress after stdin completes.
+  bar shows search progress after stdin completes. A changed query resets the
+  viewport to row zero and clears the bounded Ctrl+O/I row history; growing
+  the current result set does neither. The existing loading timer also picks
+  up partial stdin batches when the pipe stays open.
 - 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/README b/README
@@ -38,6 +38,9 @@ Useful flags:
 -h                  print usage
 ```
 
+Clearing the search returns to the first row. Changing the search resets
+Ctrl+O/I history; history is limited to the latest 128 positions.
+
 Default keys:
 
 ```

@@ -46,6 +49,7 @@ Esc         exit
 Up/Down     move list selection
 Ctrl+N/P    next/previous item
 Ctrl+D/U    page down/up
+Ctrl+O/I    back/forward through visited rows (current search only)
 Ctrl+A      move to start; press again at start to select all
 Ctrl+E      move to end
 Ctrl+K      delete to end
diff --git a/test_stream.c b/test_stream.c
@@ -34,7 +34,7 @@ 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);
-    input_hwnd = CreateWindowW(L"EDIT", L"", WS_CHILD, 0, 0, 400, 28, main_hwnd, NULL, NULL, NULL);
+    input_hwnd = CreateWindowW(L"EDIT", L"", WS_CHILD, 0, 0, 400, 28, main_hwnd, (HMENU)ID_EDIT, wc.hInstance, NULL);
     assert(list_hwnd && progress_hwnd && input_hwnd);
     ui_fiber = ConvertThreadToFiber(NULL);
     assert(ui_fiber);

@@ -62,13 +62,48 @@ int main(int argc, char **argv)
     assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60000);
     SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0);
     UpdateWindow(list_hwnd);
+    visit_location(-1); /* Empty history and both ends are no-ops. */
+    visit_location(1);
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0);
+    list_move(5);
+    list_move(5);
+    struct key_state ctrl = { true, false, false };
+    assert(handle_global_keys('O', ctrl));
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 5);
+    visit_location(-1);
+    visit_location(-1);
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0);
+    assert(handle_global_keys('I', ctrl));
+    visit_location(1);
+    visit_location(1);
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 10);
+    visit_location(-1);
+    list_move(2);
+    visit_location(1); /* A new move discards the old forward branch. */
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 7);
+    SendMessageW(list_hwnd, LB_SETCURSEL, 9, 0);
+    WndProc(main_hwnd, WM_COMMAND, MAKEWPARAM(ID_COMBO, LBN_SELCHANGE), (LPARAM)list_hwnd);
+    visit_location(-1);
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 7);
+    location_count = location_cursor = 0;
+    for (int i = 0; i < LOCATION_LIMIT + 2; ++i) record_location(i, i + 1);
+    assert(location_count == LOCATION_LIMIT && location_cursor == LOCATION_LIMIT - 1);
+    assert(locations[0] == 3); /* Oldest positions drop off at the limit. */
+    location_count = location_cursor = 0;
+    list_move(-2);
     list_move(5);
     add_item("extra-row");
-    refill_list(""); /* Count update before the selection's WM_PAINT. */
+    input_done = false;
+    WndProc(main_hwnd, WM_TIMER, TIMER_PROGRESS, 0); /* Partial batch before EOF. */
+    input_done = true;
+    update_progress();
     UpdateWindow(list_hwnd);
-    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 5);
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 10);
     assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) == 0);
-    assert(!painted_selected[0] && painted_selected[5]);
+    assert(!painted_selected[5] && painted_selected[10]);
+    list_move(-5);
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 5);
+    assert(painted_selected[5] && !painted_selected[10]);
     list_move(-5);
     assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0);
     assert(painted_selected[0] && !painted_selected[5]);

@@ -81,6 +116,15 @@ int main(int argc, char **argv)
     refill_list("item-1");
     search_step();
     assert(search_pos == SEARCH_BATCH);
+    assert(location_count == 0); /* A new query starts a new history. */
+    SendMessageW(list_hwnd, LB_SETCURSEL, match_count - 1, 0);
+    assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) > 0);
+    SetWindowTextW(input_hwnd, L"item-1");
+    SetWindowTextW(input_hwnd, L""); /* EN_CHANGE: clearing returns to the top. */
+    assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0);
+    assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) == 0);
+    refill_list("item-1");
+    search_step();
     refill_list("item-59999"); /* Cancel the previous search and start over. */
     assert(progress_visible);
     while (search_pos < item_count) search_step();
diff --git a/wmenu.c b/wmenu.c
@@ -26,6 +26,7 @@
 #define WM_SEARCH_STEP (WM_APP + 3)
 #define SEARCH_BATCH 1024
 #define CHUNK_LINES 2048
+#define LOCATION_LIMIT 128
 
 static int dpi = 96;
 #define S(px) MulDiv((px), dpi, 96)

@@ -34,6 +35,7 @@ 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 int locations[LOCATION_LIMIT], location_count, location_cursor, last_row = -1;
 static volatile LONG refresh_pending;
 static bool has_items, has_prompt, multi_select, vi_mode, normal_mode, default_controls;
 static wchar_t prompt[300];

@@ -133,6 +135,42 @@ static int row_item(int row)
     return row < item_count ? row : -1;
 }
 
+static void record_location(int from, int to)
+{
+    if (from < 0 || to < 0 || from == to) return;
+    if (!location_count) {
+        locations[0] = from;
+        location_count = 1;
+        location_cursor = 0;
+    }
+    location_count = location_cursor + 1; /* A new move discards forward history. */
+    if (location_count == LOCATION_LIMIT) {
+        memmove(locations, locations + 1, (LOCATION_LIMIT - 1) * sizeof(*locations));
+        --location_count;
+        --location_cursor;
+    }
+    locations[location_count++] = to;
+    location_cursor = location_count - 1;
+}
+
+static void visit_location(int delta)
+{
+    if (!list_hwnd || !location_count) return;
+    int next = location_cursor + delta;
+    int count = (int)SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0);
+    if (next < 0 || next >= location_count || locations[next] < 0 || locations[next] >= count) return;
+    int previous = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0);
+    location_cursor = next;
+    SendMessageW(list_hwnd, LB_SETCURSEL, locations[next], 0);
+    last_row = locations[next];
+    RECT rect;
+    if (previous >= 0 && SendMessageW(list_hwnd, LB_GETITEMRECT, previous, (LPARAM)&rect) != LB_ERR)
+        InvalidateRect(list_hwnd, &rect, FALSE);
+    if (SendMessageW(list_hwnd, LB_GETITEMRECT, locations[next], (LPARAM)&rect) != LB_ERR)
+        InvalidateRect(list_hwnd, &rect, FALSE);
+    UpdateWindow(list_hwnd);
+}
+
 static void update_count(void)
 {
     int count = search_query && *search_query ? match_count : item_count;

@@ -148,6 +186,7 @@ static void update_count(void)
     SendMessageW(list_hwnd, LB_SETCOUNT, count, 0);
     if (count) {
         SendMessageW(list_hwnd, LB_SETCURSEL, selected >= 0 && selected < count ? selected : 0, 0);
+        last_row = selected >= 0 && selected < count ? selected : 0;
         if (top > 0 || SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) != 0)
             SendMessageW(list_hwnd, LB_SETTOPINDEX, top < count ? top : count - 1, 0);
     }

@@ -228,8 +267,16 @@ static void refill_list(const char *query)
         search_query = copy;
         match_count = 0;
         search_pos = 0;
+        location_count = location_cursor = 0;
+        last_row = -1;
         if (*search_query) SendMessageW(list_hwnd, LB_SETCOUNT, 0, 0);
         else update_count();
+        SendMessageW(list_hwnd, LB_SETTOPINDEX, 0, 0);
+        if (SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) > 0) {
+            SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0);
+            last_row = 0;
+        }
+        UpdateWindow(list_hwnd);
     }
     if (*search_query && search_pos < item_count) queue_search();
     else if (!*search_query) update_count();

@@ -284,6 +331,8 @@ static void list_move(int delta)
     if (cur >= count) cur = count - 1;
     if (cur == previous) return;
     SendMessageW(list_hwnd, LB_SETCURSEL, cur, 0);
+    record_location((int)previous, (int)cur);
+    last_row = (int)cur;
     RECT rect;
     if (previous >= 0 && SendMessageW(list_hwnd, LB_GETITEMRECT, previous, (LPARAM)&rect) != LB_ERR)
         InvalidateRect(list_hwnd, &rect, FALSE);

@@ -349,6 +398,8 @@ static struct key_state key_state(void)
 
 static bool handle_global_keys(WORD key, struct key_state k)
 {
+    if (k.ctrl && key == 'O') { visit_location(-1); return true; }
+    if (k.ctrl && key == 'I') { visit_location(1); return true; }
     if (k.ctrl && key == VK_RETURN && multi_select) { add_selection(); return true; }
     if (key == VK_RETURN) { finish(); return true; }
     if (key == VK_ESCAPE) { PostQuitMessage(0); return true; }

@@ -389,7 +440,16 @@ static bool handle_vi_keys(WORD key, struct key_state k)
     case 'D': if (k.shift) { delete_to_eol(); return true; } break;
     case 'J': list_move(1); return true;
     case 'K': list_move(-1); return true;
-    case 'G': SendMessageW(list_hwnd, LB_SETCURSEL, k.shift ? SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) - 1 : 0, 0); return true;
+    case 'G': {
+        int target = k.shift ? (int)SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) - 1 : 0;
+        if (target >= 0 && target < SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0)) {
+            SendMessageW(list_hwnd, LB_SETCURSEL, target, 0);
+            record_location(last_row, target);
+            last_row = target;
+            UpdateWindow(list_hwnd);
+        }
+        return true;
+    }
     case 'W': edit_key(VK_RIGHT); return true;
     case 'B': edit_key(VK_LEFT); return true;
     }

@@ -398,7 +458,8 @@ static bool handle_vi_keys(WORD key, struct key_state k)
 
 static LRESULT CALLBACK InputProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
 {
-    if (msg == WM_CHAR && ((vi_mode && normal_mode) || wp == 0x7f)) return 0;
+    if (msg == WM_CHAR && ((vi_mode && normal_mode) || wp == 0x7f ||
+        ((GetKeyState(VK_CONTROL) & 0x8000) && (wp == 9 || wp == 15)))) return 0;
     if (msg == WM_KEYDOWN) {
         struct key_state k = key_state();
         if (handle_global_keys((WORD)wp, k)) return 0;

@@ -448,6 +509,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
         return 0;
     case WM_TIMER:
         if (wp == TIMER_PROGRESS && progress_hwnd && !input_done) {
+            if (list_hwnd) refill_list(search_query ? search_query : "");
             progress_pos += progress_dir;
             if (progress_pos >= 100 || progress_pos <= 0) progress_dir = -progress_dir;
             SendMessageW(progress_hwnd, PBM_SETPOS, progress_pos, 0);

@@ -495,6 +557,11 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
             refill_list(q);
             free(q);
         }
+        if (LOWORD(wp) == ID_COMBO && HIWORD(wp) == LBN_SELCHANGE) {
+            int row = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0);
+            record_location(last_row, row);
+            last_row = row;
+        }
         if (LOWORD(wp) == ID_COMBO && HIWORD(wp) == LBN_DBLCLK) {
             if (multi_select) add_selection(); else finish();
         }