commit 2f8c87d6079876046a718f2490a30dbfb2020a8e
parent 40397a4342d58a3cdbafac60f5da7e0355e60637
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:30:52 +0530
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(-)
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(); }
: nply/wmenu