commit c49e49340dbfb751f6f1a306c0f1f7d55fe635b9
parent f627466663050e59e7372751497d4a520fd0c164
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 23:33:52 +0530
parent f627466663050e59e7372751497d4a520fd0c164
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 23:33:52 +0530
Apply suckless-inspired C style with AStyle config
4 files changed, 1434 insertions(+), 1129 deletions(-)
diff --git a/.astylerc b/.astylerc @@ -0,0 +1,13 @@ +# Closest AStyle approximation of https://suckless.org/coding_style/. +# Manual conventions (declarations at block start, 79 columns, comments) +# are not reliably enforceable by AStyle. +style=kr +indent=tab=8 +pad-oper +pad-header +pad-comma +align-pointer=name +break-return-type +max-code-length=79 +max-code-length-mode=code +suffix=none
diff --git a/HACKING b/HACKING @@ -10,6 +10,17 @@ Build ----- make +Style +----- +Follow https://suckless.org/coding_style/ where practical for Win32 C. +Format the tracked C sources with: + + astyle --options=none --project=.astylerc wmenu.c test_stream.c arg.h + +AStyle covers indentation, braces, function layout, and most line breaks; +review comments and long Win32 directives manually. Keep the existing C99 +bool usage rather than changing program logic just to satisfy a style rule. + Dependencies ------------ No external source dependencies. The byte-safe subsequence matcher lives in
diff --git a/test_stream.c b/test_stream.c @@ -1,270 +1,285 @@ +/* See LICENSE for license details. */ +#include <assert.h> + #define main wmenu_main #include "wmenu.c" #undef main -#include <assert.h> static int draw_calls; static bool painted_selected[16]; -static LRESULT CALLBACK TestWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) +static LRESULT CALLBACK +TestWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { - if (msg == WM_DRAWITEM) { - DRAWITEMSTRUCT *draw = (DRAWITEMSTRUCT *)lp; - ++draw_calls; - if (draw->itemID < 16) painted_selected[draw->itemID] = (draw->itemState & ODS_SELECTED) != 0; - } - return WndProc(hwnd, msg, wp, lp); + if (msg == WM_DRAWITEM) { + DRAWITEMSTRUCT *draw = (DRAWITEMSTRUCT *)lp; + ++draw_calls; + if (draw->itemID < 16) painted_selected[draw->itemID] = + (draw->itemState & ODS_SELECTED) != 0; + } + return WndProc(hwnd, msg, wp, lp); } -int main(int argc, char **argv) +int +main(int argc, char **argv) { - assert(fit_lines(INT_MAX, 300) == 12); /* Oversized values are still safe. */ - assert(fit_lines(-1, 300) == 12); /* -l-1 fills the screen. */ - assert(fit_lines(0, 300) == 5); - assert(fit_lines(7, 35) == 1); - assert(has_match("aC", "ABC")); - assert(!has_match("Ab", "abc")); /* Default smart case. */ - ignore_case = true; - assert(has_match("Ab", "abc")); /* -i also folds uppercase query bytes. */ - assert(has_match("AB", "alphabet")); - ignore_case = false; - assert(has_match("\xc3\xa9", "caf\xc3\xa9")); - assert(!has_match("\xc3\xa9", "caf\xc3\x89")); - 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); + assert(fit_lines(INT_MAX, 300) == 12); /* Oversized values are still safe. */ + assert(fit_lines(-1, 300) == 12); /* -l-1 fills the screen. */ + assert(fit_lines(0, 300) == 5); + assert(fit_lines(7, 35) == 1); + assert(has_match("aC", "ABC")); + assert(!has_match("Ab", "abc")); /* Default smart case. */ + ignore_case = true; + assert(has_match("Ab", "abc")); /* -i also folds uppercase query bytes. */ + assert(has_match("AB", "alphabet")); + ignore_case = false; + assert(has_match("\xc3\xa9", "caf\xc3\xa9")); + assert(!has_match("\xc3\xa9", "caf\xc3\x89")); + 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)); - make_font(); - win_w = 400; - win_h = 300; - input_done = true; - has_items = true; - WNDCLASSW wc = {0}; - wc.lpfnWndProc = TestWndProc; - 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); - 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); - search_fiber = CreateFiber(0, search_fiber_run, NULL); - assert(search_fiber); - bool stress = argc > 1 && !strcmp(argv[1], "stress"); - ShowWindow(main_hwnd, SW_SHOWNOACTIVATE); - ShowWindow(list_hwnd, SW_SHOWNOACTIVATE); - 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); - assert(draw_calls > 0); /* First streamed row must actually paint. */ - vi_mode = true; - normal_mode = false; - assert(InputProc(input_hwnd, WM_KEYDOWN, VK_ESCAPE, 0) == 0 && normal_mode); - MSG quit; - assert(!PeekMessageW(&quit, NULL, WM_QUIT, WM_QUIT, PM_NOREMOVE)); - normal_mode = false; - assert(ListProc(list_hwnd, WM_KEYDOWN, VK_ESCAPE, 0) == 0 && normal_mode); - assert(!PeekMessageW(&quit, NULL, WM_QUIT, WM_QUIT, PM_NOREMOVE)); - vi_mode = normal_mode = false; - for (int i = 1; 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. */ - } - assert(add_item(buf)); - } - refill_list(""); - 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].item == 3); /* Oldest positions drop off at the limit. */ - for (int i = 0; i < location_count; ++i) free(locations[i].query); - location_count = location_cursor = 0; - list_move(-2); - list_move(5); - assert(add_item("extra-row")); - 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) == 10); - assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) == 0); - 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]); - 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) == 59999); - refill_list("item-1"); - search_step(); - assert(search_pos == SEARCH_BATCH); - assert(location_count > 0); /* Previous search is retained. */ - int searches = location_count; - refill_list("item-12"); - refill_list("item-123"); - refill_list("item-1"); - assert(location_count == searches); /* Editing one query is not four jumps. */ - while (search_pos < 1024) search_step(); - int old_item = matches[match_count - 1]; - 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); - visit_location(-1); - assert(!strcmp(search_query, "item-1")); - search_step(); - assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == LB_ERR); - char *pending_text = current_text(); /* Enter must use the saved line, not row zero. */ - assert(pending_text && !strcmp(pending_text, items[old_item])); - free(pending_text); - while (search_pos <= old_item) search_step(); - assert(row_item((int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0)) == old_item); - visit_location(1); - assert(!strcmp(search_query, "")); - assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0); - visit_location(-1); - search_step(); - list_move(1); /* Manual navigation cancels a pending history restore. */ - assert(pending_item == -1 && SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0); - visit_location(-1); - assert(pending_item == old_item && SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == LB_ERR); - refill_list(""); - 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(); - 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) == 60001); - ShowWindow(main_hwnd, SW_SHOWNOACTIVATE); - ShowWindow(list_hwnd, SW_SHOWNOACTIVATE); - UpdateWindow(list_hwnd); - assert(!GetUpdateRect(list_hwnd, NULL, FALSE)); - SendMessageW(list_hwnd, LB_SETCURSEL, 59999, 0); - int kept_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); - 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); - assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) == kept_top); - RECT dirty; - assert(!GetUpdateRect(list_hwnd, &dirty, FALSE) || IsRectEmpty(&dirty)); - /* Count/scrollbar may update, but the existing client rows stay untouched. */ - refill_list("tail-special"); - search_step(); - input_done = false; - assert(add_item("tail-special")); - while (search_pos < item_count) search_step(); - assert(match_count == 1 && matches[0] == 60002); - input_done = true; - update_progress(); - assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1); - assert(add_item("caf\xc3\xa9")); - SetWindowTextW(input_hwnd, L"\x00e9"); /* UTF-8 edit text goes through the search path. */ - while (search_pos < loaded_count()) search_step(); - assert(match_count == 1 && matches[0] == 60003); - add_selection(); - assert(selected_count == 1 && !strcmp(selected[0], "caf\xc3\xa9")); - assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == LB_ERR); - char *after_add = current_text(); - assert(after_add && !*after_add); /* Enter would output only added rows. */ - free(after_add); - list_move(1); - assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0); - DeleteFiber(search_fiber); - ConvertFiberToThread(); - DestroyWindow(main_hwnd); - DeleteCriticalSection(&items_lock); - return 0; + InitializeCriticalSection(&items_lock); + 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 = TestWndProc; + 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); + 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); + search_fiber = CreateFiber(0, search_fiber_run, NULL); + assert(search_fiber); + bool stress = argc > 1 && !strcmp(argv[1], "stress"); + ShowWindow(main_hwnd, SW_SHOWNOACTIVATE); + ShowWindow(list_hwnd, SW_SHOWNOACTIVATE); + 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); + assert(draw_calls > 0); /* First streamed row must actually paint. */ + vi_mode = true; + normal_mode = false; + assert(InputProc(input_hwnd, WM_KEYDOWN, VK_ESCAPE, 0) == 0 && normal_mode); + MSG quit; + assert(!PeekMessageW(&quit, NULL, WM_QUIT, WM_QUIT, PM_NOREMOVE)); + normal_mode = false; + assert(ListProc(list_hwnd, WM_KEYDOWN, VK_ESCAPE, 0) == 0 && normal_mode); + assert(!PeekMessageW(&quit, NULL, WM_QUIT, WM_QUIT, PM_NOREMOVE)); + vi_mode = normal_mode = false; + for (int i = 1; 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. */ + } + assert(add_item(buf)); + } + refill_list(""); + 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].item == 3); /* Oldest positions drop off at the limit. */ + for (int i = 0; i < location_count; ++i) free(locations[i].query); + location_count = location_cursor = 0; + list_move(-2); + list_move(5); + assert(add_item("extra-row")); + 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) == 10); + assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) == 0); + 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]); + 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) == 59999); + refill_list("item-1"); + search_step(); + assert(search_pos == SEARCH_BATCH); + assert(location_count > 0); /* Previous search is retained. */ + int searches = location_count; + refill_list("item-12"); + refill_list("item-123"); + refill_list("item-1"); + assert(location_count == searches); /* Editing one query is not four jumps. */ + while (search_pos < 1024) search_step(); + int old_item = matches[match_count - 1]; + 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); + visit_location(-1); + assert(!strcmp(search_query, "item-1")); + search_step(); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == LB_ERR); + char *pending_text = + current_text(); /* Enter must use the saved line, not row zero. */ + assert(pending_text && !strcmp(pending_text, items[old_item])); + free(pending_text); + while (search_pos <= old_item) search_step(); + assert(row_item((int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0)) == old_item); + visit_location(1); + assert(!strcmp(search_query, "")); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0); + visit_location(-1); + search_step(); + list_move(1); /* Manual navigation cancels a pending history restore. */ + assert(pending_item == -1 && SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0); + visit_location(-1); + assert(pending_item == old_item + && SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == LB_ERR); + refill_list(""); + 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(); + 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) == 60001); + ShowWindow(main_hwnd, SW_SHOWNOACTIVATE); + ShowWindow(list_hwnd, SW_SHOWNOACTIVATE); + UpdateWindow(list_hwnd); + assert(!GetUpdateRect(list_hwnd, NULL, FALSE)); + SendMessageW(list_hwnd, LB_SETCURSEL, 59999, 0); + int kept_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); + 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); + assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) == kept_top); + RECT dirty; + assert(!GetUpdateRect(list_hwnd, &dirty, FALSE) || IsRectEmpty(&dirty)); + /* Count/scrollbar may update, but the existing client rows stay untouched. */ + refill_list("tail-special"); + search_step(); + input_done = false; + assert(add_item("tail-special")); + while (search_pos < item_count) search_step(); + assert(match_count == 1 && matches[0] == 60002); + input_done = true; + update_progress(); + assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1); + assert(add_item("caf\xc3\xa9")); + SetWindowTextW(input_hwnd, + L"\x00e9"); /* UTF-8 edit text goes through the search path. */ + while (search_pos < loaded_count()) search_step(); + assert(match_count == 1 && matches[0] == 60003); + add_selection(); + assert(selected_count == 1 && !strcmp(selected[0], "caf\xc3\xa9")); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == LB_ERR); + char *after_add = current_text(); + assert(after_add && !*after_add); /* Enter would output only added rows. */ + free(after_add); + list_move(1); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0); + DeleteFiber(search_fiber); + ConvertFiberToThread(); + DestroyWindow(main_hwnd); + DeleteCriticalSection(&items_lock); + return 0; }
diff --git a/wmenu.c b/wmenu.c @@ -1,15 +1,17 @@ +/* See LICENSE for license details. */ #define UNICODE #define _UNICODE -#include <windows.h> -#include <commctrl.h> +#include <io.h> +#include <limits.h> #include <stdbool.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <io.h> -#include <limits.h> -#include <stdint.h> #include <wctype.h> +#include <windows.h> +/* commctrl.h requires windows.h first. */ +#include <commctrl.h> #include "arg.h" #ifdef _MSC_VER @@ -37,12 +39,17 @@ static volatile LONG item_count; /* Published after items[] is populated. */ static int item_capacity, selected_count; static int *matches, match_count, match_capacity, search_pos; static char *search_query; -struct location { char *query; int item; }; /* Source item IDs survive filtering. */ +/* Source item IDs survive filtering. */ +struct location { + char *query; + int item; +}; static struct location locations[LOCATION_LIMIT]; static int location_count, location_cursor, last_row = -1, pending_item = -1; static bool restoring_location, search_posted; static volatile LONG refresh_pending; -static bool has_items, has_prompt, multi_select, vi_mode, normal_mode, default_controls, ignore_case; +static bool has_items, has_prompt, multi_select, vi_mode, normal_mode, + default_controls, ignore_case; static wchar_t prompt[300]; static HWND main_hwnd, input_hwnd, list_hwnd, progress_hwnd; static WNDPROC old_input_proc, old_list_proc; @@ -54,874 +61,1133 @@ 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, input_done, progress_visible; +static bool center_window, close_on_blur, stdin_pipe, loading_items, + input_done, progress_visible; static int progress_pos, progress_dir = 5; char *argv0; -static wchar_t *widen(const char *s) -{ - int n = MultiByteToWideChar(text_codepage, 0, s ? s : "", -1, NULL, 0); - wchar_t *w = (wchar_t *)calloc(n ? n : 1, sizeof(wchar_t)); - if (w) MultiByteToWideChar(text_codepage, 0, s ? s : "", -1, w, n); - return w; -} - -static char *text_encoded(HWND hwnd) -{ - int n = GetWindowTextLengthW(hwnd) + 1; - wchar_t *w = (wchar_t *)calloc(n, sizeof(wchar_t)); - char *s = NULL; - int m; - if (!w) return NULL; - GetWindowTextW(hwnd, w, n); - 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; -} - -static int loaded_count(void) -{ - return (int)InterlockedCompareExchange(&item_count, 0, 0); -} - -static bool add_item(const char *s) -{ - char *copy = _strdup(s ? s : ""); - if (!copy) return false; - EnterCriticalSection(&items_lock); - if (item_count == 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] = copy; - InterlockedIncrement(&item_count); - LeaveCriticalSection(&items_lock); - return true; -} - -static void make_font(void) -{ - ui_font = (HFONT)GetStockObject(DEFAULT_GUI_FONT); - if (custom_font) { - LOGFONTW lf; - HDC dc = GetDC(NULL); - GetObjectW(ui_font, sizeof lf, &lf); - if (font_name[0]) { wcsncpy(lf.lfFaceName, font_name, LF_FACESIZE - 1); lf.lfFaceName[LF_FACESIZE - 1] = 0; } - if (font_size > 0) lf.lfHeight = -MulDiv(font_size, GetDeviceCaps(dc, LOGPIXELSY), 72); - lf.lfWeight = font_weight; - ReleaseDC(NULL, dc); - ui_font = CreateFontIndirectW(&lf); - } -} - -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 *line = NULL; - size_t capacity = 0; - int n = 0, status; - (void)unused; - 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); - PostMessageW(main_hwnd, WM_STDIN_DONE, status < 0, 0); - return 0; -} - -/* fzy-style subsequence matching, with non-ASCII UTF-8 bytes compared literally. */ -static bool has_match(const char *query, const char *line) -{ - while (*query) { - unsigned char ch = (unsigned char)*query++; - if (ignore_case && ch >= 'A' && ch <= 'Z') ch = ch - 'A' + 'a'; - unsigned char upper = ch >= 'a' && ch <= 'z' ? ch - 'a' + 'A' : ch; - while (*line && (unsigned char)*line != ch && (unsigned char)*line != upper) ++line; - if (!*line) return false; - ++line; - } - return true; +static wchar_t * +widen(const char *s) +{ + int n = MultiByteToWideChar(text_codepage, 0, s ? s : "", -1, NULL, 0); + wchar_t *w = (wchar_t *)calloc(n ? n : 1, sizeof(wchar_t)); + if (w) MultiByteToWideChar(text_codepage, 0, s ? s : "", -1, w, n); + return w; +} + +static char * +text_encoded(HWND hwnd) +{ + int n = GetWindowTextLengthW(hwnd) + 1; + wchar_t *w = (wchar_t *)calloc(n, sizeof(wchar_t)); + char *s = NULL; + int m; + if (!w) return NULL; + GetWindowTextW(hwnd, w, n); + 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; +} + +static int +loaded_count(void) +{ + return (int)InterlockedCompareExchange(&item_count, 0, 0); +} + +static bool +add_item(const char *s) +{ + char *copy = _strdup(s ? s : ""); + if (!copy) return false; + EnterCriticalSection(&items_lock); + if (item_count == 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] = copy; + InterlockedIncrement(&item_count); + LeaveCriticalSection(&items_lock); + return true; +} + +static void +make_font(void) +{ + ui_font = (HFONT)GetStockObject(DEFAULT_GUI_FONT); + if (custom_font) { + LOGFONTW lf; + HDC dc = GetDC(NULL); + GetObjectW(ui_font, sizeof lf, &lf); + if (font_name[0]) { + wcsncpy(lf.lfFaceName, font_name, LF_FACESIZE - 1); + lf.lfFaceName[LF_FACESIZE - 1] = 0; + } + if (font_size > 0) lf.lfHeight = -MulDiv(font_size, GetDeviceCaps(dc, + LOGPIXELSY), 72); + lf.lfWeight = font_weight; + ReleaseDC(NULL, dc); + ui_font = CreateFontIndirectW(&lf); + } +} + +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 instead of splitting 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 *line = NULL; + size_t capacity = 0; + int n = 0, status; + (void)unused; + 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); + PostMessageW(main_hwnd, WM_STDIN_DONE, status < 0, 0); + return 0; +} + +/* fzy-style subsequence matching; compare non-ASCII bytes literally. */ +static bool +has_match(const char *query, const char *line) +{ + while (*query) { + unsigned char ch = (unsigned char) * query++; + if (ignore_case && ch >= 'A' && ch <= 'Z') ch = ch - 'A' + 'a'; + unsigned char upper = ch >= 'a' && ch <= 'z' ? ch - 'a' + 'A' : ch; + while (*line && (unsigned char) * line != ch + && (unsigned char) * line != upper) ++line; + if (!*line) return false; + ++line; + } + return true; } /* 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 < loaded_count() ? row : -1; -} - -static void truncate_locations(void) -{ - for (int i = location_cursor + 1; i < location_count; ++i) free(locations[i].query); - if (location_count) location_count = location_cursor + 1; -} - -static void push_location(const char *query, int item) -{ - char *copy = _strdup(query ? query : ""); - if (!copy) return; - truncate_locations(); - if (location_count == LOCATION_LIMIT) { - free(locations[0].query); - memmove(locations, locations + 1, (LOCATION_LIMIT - 1) * sizeof(*locations)); - --location_count; - } - locations[location_count++] = (struct location){ copy, item }; - location_cursor = location_count - 1; -} - -static void record_location(int from, int to) -{ - if (to < 0 || from == to) return; - int new_item = row_item(to); - if (from < 0) { - if (!location_count || pending_item >= 0 || - strcmp(locations[location_cursor].query, search_query ? search_query : "")) - push_location(search_query, new_item); - else locations[location_cursor].item = new_item; - return; - } - int old_item = row_item(from); - if (!location_count) push_location(search_query, old_item); - else if (!strcmp(locations[location_cursor].query, search_query ? search_query : "") && - locations[location_cursor].item < 0) - locations[location_cursor].item = old_item; - else if (strcmp(locations[location_cursor].query, search_query ? search_query : "") || - locations[location_cursor].item != old_item) - push_location(search_query, old_item); - push_location(search_query, new_item); -} - -static void restore_location(void) -{ - if (pending_item < 0) return; - int row = pending_item; - if (search_query && *search_query) { - if (search_pos <= pending_item) return; /* Search has not reached this item. */ - int lo = 0, hi = match_count; - while (lo < hi) { - int mid = lo + (hi - lo) / 2; - if (matches[mid] < pending_item) lo = mid + 1; - else hi = mid; - } - row = lo < match_count && matches[lo] == pending_item ? lo : -1; - } - pending_item = -1; - if (row >= 0 && row < SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0)) { - int previous = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); - SendMessageW(list_hwnd, LB_SETCURSEL, row, 0); - last_row = row; - 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, row, (LPARAM)&rect) != LB_ERR) - InvalidateRect(list_hwnd, &rect, FALSE); - UpdateWindow(list_hwnd); - } -} - -static void visit_location(int delta) -{ - int next = location_cursor + delta; - if (!list_hwnd || !location_count || next < 0 || next >= location_count) return; - wchar_t *query = widen(locations[next].query); - if (!query) return; - location_cursor = next; - pending_item = locations[next].item; - restoring_location = true; - SetWindowTextW(input_hwnd, query); /* EN_CHANGE starts the saved search. */ - restoring_location = false; - free(query); - if (pending_item >= 0 && search_query && *search_query && search_pos <= pending_item) { - SendMessageW(list_hwnd, LB_SETCURSEL, -1, 0); - last_row = -1; - InvalidateRect(list_hwnd, NULL, FALSE); - UpdateWindow(list_hwnd); - } - restore_location(); -} - -static void update_count(void) -{ - int count = search_query && *search_query ? match_count : loaded_count(); - int old_count = (int)SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0); - if (old_count == count) return; - int selected = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); - int top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); - int height = (int)SendMessageW(list_hwnd, LB_GETITEMHEIGHT, 0, 0); - RECT client, pending; - bool had_pending = GetUpdateRect(list_hwnd, &pending, FALSE) && !IsRectEmpty(&pending); - GetClientRect(list_hwnd, &client); - SendMessageW(list_hwnd, WM_SETREDRAW, FALSE, 0); - SendMessageW(list_hwnd, LB_SETCOUNT, count, 0); - if (count) { - int wanted = pending_item >= 0 ? -1 : selected >= 0 && selected < count ? selected : 0; - SendMessageW(list_hwnd, LB_SETCURSEL, wanted, 0); - last_row = wanted; - if (top > 0 || SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) != 0) - SendMessageW(list_hwnd, LB_SETTOPINDEX, top < count ? top : count - 1, 0); - } - SendMessageW(list_hwnd, WM_SETREDRAW, TRUE, 0); - /* LB_SETCOUNT may invalidate everything. Only newly visible rows need paint. */ - RedrawWindow(list_hwnd, NULL, NULL, RDW_VALIDATE | RDW_NOCHILDREN); - int new_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); - if (top != new_top || count < old_count || height <= 0) { - InvalidateRect(list_hwnd, NULL, FALSE); - } else if (old_count < new_top + (client.bottom - client.top + height - 1) / height) { - client.top += (old_count - new_top) * height; - if (client.top < 0) client.top = 0; - InvalidateRect(list_hwnd, &client, FALSE); - } - if (had_pending) InvalidateRect(list_hwnd, &pending, FALSE); - UpdateWindow(list_hwnd); /* Do not starve new rows or selection paints. */ -} - -static void update_progress(void) -{ - int count = loaded_count(); - bool searching = input_done && search_query && *search_query && search_pos < 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, - count ? (int)(100LL * search_pos / count) : 100, 0); -} - -static void queue_search(void) -{ - if (!search_posted) search_posted = PostMessageW(main_hwnd, WM_SEARCH_STEP, 0, 0) != 0; -} - -static VOID WINAPI search_fiber_run(void *unused) -{ - (void)unused; - for (;;) { - EnterCriticalSection(&items_lock); - int remaining = item_count - search_pos; - int end = search_pos + (remaining < SEARCH_BATCH ? remaining : SEARCH_BATCH); - 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; - match_capacity = next; - } - matches[match_count++] = search_pos; - } - LeaveCriticalSection(&items_lock); - SwitchToFiber(ui_fiber); /* Never yield while holding items_lock. */ - } -} - -static void search_step(void) -{ - if (!search_query || !*search_query || search_pos >= loaded_count()) { - KillTimer(main_hwnd, TIMER_SEARCH); - return; - } - SwitchToFiber(search_fiber); - update_count(); - restore_location(); - update_progress(); - if (search_pos >= loaded_count()) KillTimer(main_hwnd, TIMER_SEARCH); - else if (HIWORD(GetQueueStatus(QS_INPUT))) - SetTimer(main_hwnd, TIMER_SEARCH, 15, NULL); /* Let keyboard/mouse go first. */ - else 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; - if (!restoring_location) { - pending_item = -1; - if (search_query) { - int item = row_item((int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0)); - if (!location_count) push_location(search_query, item); - else if (!strcmp(locations[location_cursor].query, search_query) && item >= 0) - locations[location_cursor].item = item; - bool related = *query && *search_query && last_row <= 0 && - (!strncmp(query, search_query, strlen(query)) || - !strncmp(query, search_query, strlen(search_query))); - bool intermediate_empty = !*search_query && *query && last_row <= 0 && - location_count > 1 && !*locations[location_cursor].query; - if (related || intermediate_empty) { - char *history_copy = _strdup(query); - if (!history_copy) { free(copy); return; } - truncate_locations(); - free(locations[location_cursor].query); - locations[location_cursor] = (struct location){ history_copy, -1 }; - } else push_location(query, -1); - } - } - free(search_query); - search_query = copy; - match_count = 0; - search_pos = 0; - KillTimer(main_hwnd, TIMER_SEARCH); - 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); - restore_location(); - } - if (*search_query && search_pos < loaded_count()) queue_search(); - else if (!*search_query) update_count(); - if (progress_hwnd) update_progress(); -} - -static char *current_text(void) -{ - int sel = list_hwnd ? (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) : -1; - if (has_items && (sel >= 0 || pending_item >= 0)) { - char *s = NULL; - EnterCriticalSection(&items_lock); - int index = pending_item >= 0 ? pending_item : row_item(sel); - if (index >= 0 && index < item_count) s = _strdup(items[index]); - LeaveCriticalSection(&items_lock); - return s; - } - return text_encoded(input_hwnd); -} - -static void add_selection(void) -{ - char *s = current_text(); - 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(""); - SendMessageW(list_hwnd, LB_SETCURSEL, -1, 0); - last_row = -1; - UpdateWindow(list_hwnd); - } -} - -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 (s && *s) fputs(s, stdout); - free(s); - PostQuitMessage(0); -} - -static void list_move(int delta) -{ - if (!has_items) return; - LRESULT count = SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0); - LRESULT cur = SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); - LRESULT previous = cur; - if (count <= 0) return; - if (cur < 0) cur = delta > 0 ? -1 : count; - cur += delta; - if (cur < 0) cur = 0; - if (cur >= count) cur = count - 1; - if (cur == previous) return; - SendMessageW(list_hwnd, LB_SETCURSEL, cur, 0); - record_location((int)previous, (int)cur); - pending_item = -1; - last_row = (int)cur; - 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, cur, (LPARAM)&rect) != LB_ERR) - InvalidateRect(list_hwnd, &rect, FALSE); - UpdateWindow(list_hwnd); -} - -static void edit_key(WORD key) { SendMessageW(input_hwnd, WM_KEYDOWN, key, 0); } - -static void move_caret(int pos) -{ - SendMessageW(input_hwnd, EM_SETSEL, pos, pos); -} - -static bool at_start_unselected(HWND hwnd) -{ - DWORD start = 0, end = 0; - SendMessageW(hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end); - return start == 0 && end == 0; -} - -static void delete_to_eol(void) -{ - DWORD start = 0, endsel = 0; - int end = GetWindowTextLengthW(input_hwnd); - SendMessageW(input_hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&endsel); - SendMessageW(input_hwnd, EM_SETSEL, start, end); - edit_key(VK_DELETE); -} - -static void delete_prev_word(void) -{ - DWORD start = 0, end = 0; - int len = GetWindowTextLengthW(input_hwnd); - wchar_t *s = (wchar_t *)calloc(len + 1, sizeof(wchar_t)); - if (!s) return; - GetWindowTextW(input_hwnd, s, len + 1); - SendMessageW(input_hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end); - if (start != end) { - SendMessageW(input_hwnd, EM_REPLACESEL, TRUE, (LPARAM)L""); - } else if (start) { - int i = (int)start; - while (i > 0 && iswspace(s[i - 1])) --i; - while (i > 0 && !iswspace(s[i - 1])) --i; - SendMessageW(input_hwnd, EM_SETSEL, i, start); - SendMessageW(input_hwnd, EM_REPLACESEL, TRUE, (LPARAM)L""); - } - free(s); -} - -struct key_state { bool ctrl, alt, shift; }; - -static struct key_state key_state(void) -{ - struct key_state k = { - (GetKeyState(VK_CONTROL) & 0x8000) != 0, - (GetKeyState(VK_MENU) & 0x8000) != 0, - (GetKeyState(VK_SHIFT) & 0x8000) != 0, - }; - return k; -} - -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) { - if (vi_mode && !normal_mode) normal_mode = true; - else PostQuitMessage(0); - return true; - } - if (key == VK_DOWN) { list_move(1); return true; } - if (key == VK_UP) { list_move(-1); return true; } - return false; -} - -/* Detachable mode: remove this function and its call for plain Windows controls. */ -static bool handle_emacs_keys(HWND hwnd, WORD key, struct key_state k) -{ - if (default_controls) return false; - if (k.ctrl && key == VK_BACK) { delete_prev_word(); return true; } - if (k.ctrl && key == 'A') { if (at_start_unselected(hwnd)) SendMessageW(hwnd, EM_SETSEL, 0, -1); else move_caret(0); return true; } - if (k.ctrl && key == 'E') { move_caret(GetWindowTextLengthW(hwnd)); return true; } - if (k.ctrl && key == 'K') { delete_to_eol(); return true; } - if (k.ctrl && key == 'W') { SendMessageW(hwnd, WM_CUT, 0, 0); return true; } - if (k.ctrl && k.alt && key == 'V') { vi_mode = !vi_mode; normal_mode = vi_mode; return true; } - if (k.ctrl && key == 'N') { list_move(1); return true; } - if (k.ctrl && key == 'P') { list_move(-1); return true; } - if (k.ctrl && key == 'D') { list_move(lines > 0 ? lines : 5); return true; } - if (k.ctrl && key == 'U') { list_move(-(lines > 0 ? lines : 5)); return true; } - return false; -} - -/* Detachable mode: remove this function and Ctrl+Alt+V toggle for no vi support. */ -static bool handle_vi_keys(WORD key, struct key_state k) -{ - if (!vi_mode || !normal_mode) return false; - switch (key) { - case 'I': if (k.shift) edit_key(VK_HOME); normal_mode = false; return true; - case 'A': if (k.shift) edit_key(VK_END); else edit_key(VK_RIGHT); normal_mode = false; return true; - case 'H': edit_key(VK_LEFT); return true; - case 'L': edit_key(VK_RIGHT); return true; - case '0': edit_key(VK_HOME); return true; - case '4': if (k.shift) { edit_key(VK_END); return true; } break; - case 'X': if (k.shift) return true; edit_key(VK_DELETE); return true; - 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': { - 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); - pending_item = -1; - last_row = target; - UpdateWindow(list_hwnd); - } - return true; - } - case 'W': edit_key(VK_RIGHT); return true; - case 'B': edit_key(VK_LEFT); return true; - } - return false; -} - -static LRESULT CALLBACK InputProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -{ - 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; - if (handle_emacs_keys(hwnd, (WORD)wp, k)) return 0; - if (handle_vi_keys((WORD)wp, k)) return 0; - } - return CallWindowProcW(old_input_proc, hwnd, msg, wp, lp); -} - -static LRESULT CALLBACK ListProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -{ - if (msg == WM_KEYDOWN) { - if (wp == VK_ESCAPE) { handle_global_keys(VK_ESCAPE, key_state()); return 0; } - if (wp == VK_RETURN) { finish(); return 0; } - } - LRESULT r = CallWindowProcW(old_list_proc, hwnd, msg, wp, lp); - if (msg == WM_SETFOCUS || msg == WM_LBUTTONDOWN || msg == WM_MOUSEWHEEL) - SetFocus(input_hwnd); - return r; -} - -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) { - char *q = text_encoded(input_hwnd); - if (q) refill_list(q); - free(q); - } - 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 = loaded_count() > 0; - KillTimer(hwnd, TIMER_PROGRESS); - if (list_hwnd) { - char *q = text_encoded(input_hwnd); - if (q) refill_list(q); - free(q); - update_progress(); - } - return 0; - case WM_SEARCH_STEP: - search_posted = false; - search_step(); - return 0; - case WM_TIMER: - if (wp == TIMER_SEARCH) { - KillTimer(hwnd, TIMER_SEARCH); - if (!search_posted) search_step(); - return 0; - } - 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); - } - 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 = (int)draw->itemID == (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 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) { - char *q = text_encoded(input_hwnd); - 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); - pending_item = -1; - last_row = row; - } - if (LOWORD(wp) == ID_COMBO && HIWORD(wp) == LBN_DBLCLK) { - if (multi_select) add_selection(); else finish(); - } - return 0; - case WM_ACTIVATE: - if (close_on_blur && LOWORD(wp) == WA_INACTIVE) PostQuitMessage(0); - else if (LOWORD(wp) != WA_INACTIVE && input_hwnd) SetFocus(input_hwnd); - return 0; - case WM_ACTIVATEAPP: - if (close_on_blur && !wp) PostQuitMessage(0); - else if (wp && input_hwnd) SetFocus(input_hwnd); - return 0; - case WM_CLOSE: - case WM_DESTROY: - PostQuitMessage(0); - return 0; - } - return DefWindowProcW(hwnd, msg, wp, lp); -} - -static int fit_lines(int requested, int screen_height) -{ - int row_height = S(22); - int fit = row_height > 0 && screen_height > S(28) ? (screen_height - S(28)) / row_height : 1; - if (fit < 1) fit = 1; - if (requested == -1) return fit; - if (requested < 1) requested = 5; - return requested < fit ? requested : fit; -} - -static void usage(void) -{ - puts("usage: wmenu [-chimbdVv] [-e utf8|ansi|oem] [-p prompt] [-x px] [-y px] [-w px] [-l lines] [-fn name] [-fs pt] [-fw weight] < items"); -} - -int main(int argc, char **argv) -{ - HINSTANCE inst = GetModuleHandleW(NULL); - INITCOMMONCONTROLSEX icc = { sizeof icc, ICC_PROGRESS_CLASS }; - WNDCLASSW wc = {0}; - MSG msg; - int prompt_w = 0; - - ARGBEGIN { - case 'h': - usage(); - return 0; - case 'v': - puts("version: 0.8"); - return 0; - case 'e': { - char *enc = EARGF(usage()); - if (!strcmp(enc, "ansi")) text_codepage = CP_ACP; - else if (!strcmp(enc, "oem")) text_codepage = CP_OEMCP; - else if (!strcmp(enc, "utf8") || !strcmp(enc, "utf-8")) text_codepage = CP_UTF8; - } 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; - free(w); - } break; - case 'f': { - wchar_t *w; - 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; - free(w); - } else if (argv[0][1] == 's') { - argv[0]++; - custom_font = true; - font_size = atoi(EARGF(usage())); - } else if (argv[0][1] == 'w') { - argv[0]++; - custom_font = true; - font_weight = atoi(EARGF(usage())); - } - } break; - case 'x': win_x = atoi(EARGF(usage())); break; - case 'y': win_y = atoi(EARGF(usage())); break; - case 'w': win_w = atoi(EARGF(usage())); break; - case 'l': lines = atoi(EARGF(usage())); break; - case 'c': center_window = true; break; - case 'm': multi_select = true; break; - case 'V': vi_mode = true; break; - case 'd': default_controls = true; break; - case 'b': close_on_blur = true; break; - case 'i': ignore_case = true; break; - default: usage(); return 1; - } ARGEND; - - InitializeCriticalSection(&items_lock); - stdin_pipe = !_isatty(_fileno(stdin)); - input_done = !stdin_pipe; - if (stdin_pipe) { has_items = true; loading_items = true; } - - SetProcessDPIAware(); - HDC screen = GetDC(NULL); - dpi = GetDeviceCaps(screen, LOGPIXELSX); - ReleaseDC(NULL, screen); - InitCommonControlsEx(&icc); - make_font(); - if (!win_w) win_w = GetSystemMetrics(SM_CXSCREEN); - lines = fit_lines(lines, GetSystemMetrics(SM_CYSCREEN)); - win_h = has_items ? S(28) + lines * S(22) : S(28); - if (center_window) { - win_x = (GetSystemMetrics(SM_CXSCREEN) - win_w) / 2; - win_y = (GetSystemMetrics(SM_CYSCREEN) - win_h) / 2; - } - - wc.lpfnWndProc = WndProc; - wc.hInstance = inst; - wc.hCursor = LoadCursor(NULL, IDC_ARROW); - wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - wc.lpszClassName = L"wmenu"; - if (!RegisterClassW(&wc)) return 1; - - main_hwnd = CreateWindowExW(WS_EX_TOPMOST | WS_EX_APPWINDOW, L"wmenu", L"wmenu", WS_POPUP | WS_BORDER, win_x, win_y, win_w, win_h, NULL, NULL, inst, NULL); - if (!main_hwnd) return 1; - - if (has_prompt) { - prompt_w = (int)wcslen(prompt) * S(8) + S(12); - HWND label = CreateWindowW(L"STATIC", prompt, WS_CHILD | WS_VISIBLE, S(4), S(5), prompt_w, S(22), main_hwnd, (HMENU)ID_LABEL, inst, NULL); - set_font(label); - } - - 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 | 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 { - 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); - } - 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); - SetFocus(input_hwnd); - if (loading_items) { - SetTimer(main_hwnd, TIMER_PROGRESS, 30, 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); - DispatchMessageW(&msg); - } - if (search_fiber) { - DeleteFiber(search_fiber); - ConvertFiberToThread(); - } - return (int)msg.wParam; +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 < loaded_count() ? row : -1; +} + +static void +truncate_locations(void) +{ + for (int i = location_cursor + 1; i < location_count; + ++i) free(locations[i].query); + if (location_count) location_count = location_cursor + 1; +} + +static void +push_location(const char *query, int item) +{ + char *copy = _strdup(query ? query : ""); + if (!copy) return; + truncate_locations(); + if (location_count == LOCATION_LIMIT) { + free(locations[0].query); + memmove(locations, locations + 1, (LOCATION_LIMIT - 1) * sizeof(*locations)); + --location_count; + } + locations[location_count++] = (struct location) { + copy, item + }; + location_cursor = location_count - 1; +} + +static void +record_location(int from, int to) +{ + if (to < 0 || from == to) return; + int new_item = row_item(to); + if (from < 0) { + if (!location_count || pending_item >= 0 || + strcmp(locations[location_cursor].query, search_query ? search_query : "")) + push_location(search_query, new_item); + else locations[location_cursor].item = new_item; + return; + } + int old_item = row_item(from); + if (!location_count) push_location(search_query, old_item); + else if (!strcmp(locations[location_cursor].query, + search_query ? search_query : "") && + locations[location_cursor].item < 0) + locations[location_cursor].item = old_item; + else if (strcmp(locations[location_cursor].query, + search_query ? search_query : "") || + locations[location_cursor].item != old_item) + push_location(search_query, old_item); + push_location(search_query, new_item); +} + +static void +restore_location(void) +{ + if (pending_item < 0) return; + int row = pending_item; + if (search_query && *search_query) { + if (search_pos <= pending_item) return; /* Search has not reached this item. */ + int lo = 0, hi = match_count; + while (lo < hi) { + int mid = lo + (hi - lo) / 2; + if (matches[mid] < pending_item) lo = mid + 1; + else hi = mid; + } + row = lo < match_count && matches[lo] == pending_item ? lo : -1; + } + pending_item = -1; + if (row >= 0 && row < SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0)) { + int previous = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); + SendMessageW(list_hwnd, LB_SETCURSEL, row, 0); + last_row = row; + 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, row, (LPARAM)&rect) != LB_ERR) + InvalidateRect(list_hwnd, &rect, FALSE); + UpdateWindow(list_hwnd); + } +} + +static void +visit_location(int delta) +{ + int next = location_cursor + delta; + if (!list_hwnd || !location_count || next < 0 + || next >= location_count) return; + wchar_t *query = widen(locations[next].query); + if (!query) return; + location_cursor = next; + pending_item = locations[next].item; + restoring_location = true; + SetWindowTextW(input_hwnd, query); /* EN_CHANGE starts the saved search. */ + restoring_location = false; + free(query); + if (pending_item >= 0 && search_query && *search_query + && search_pos <= pending_item) { + SendMessageW(list_hwnd, LB_SETCURSEL, -1, 0); + last_row = -1; + InvalidateRect(list_hwnd, NULL, FALSE); + UpdateWindow(list_hwnd); + } + restore_location(); +} + +static void +update_count(void) +{ + int count = search_query && *search_query ? match_count : loaded_count(); + int old_count = (int)SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0); + if (old_count == count) return; + int selected = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); + int top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); + int height = (int)SendMessageW(list_hwnd, LB_GETITEMHEIGHT, 0, 0); + RECT client, pending; + bool had_pending = GetUpdateRect(list_hwnd, &pending, FALSE) + && !IsRectEmpty(&pending); + GetClientRect(list_hwnd, &client); + SendMessageW(list_hwnd, WM_SETREDRAW, FALSE, 0); + SendMessageW(list_hwnd, LB_SETCOUNT, count, 0); + if (count) { + int wanted = pending_item >= 0 ? -1 : selected >= 0 + && selected < count ? selected : 0; + SendMessageW(list_hwnd, LB_SETCURSEL, wanted, 0); + last_row = wanted; + if (top > 0 || SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) != 0) + SendMessageW(list_hwnd, LB_SETTOPINDEX, top < count ? top : count - 1, 0); + } + SendMessageW(list_hwnd, WM_SETREDRAW, TRUE, 0); + /* LB_SETCOUNT may invalidate everything; paint only new visible rows. */ + RedrawWindow(list_hwnd, NULL, NULL, RDW_VALIDATE | RDW_NOCHILDREN); + int new_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); + if (top != new_top || count < old_count || height <= 0) { + InvalidateRect(list_hwnd, NULL, FALSE); + } else if (old_count < new_top + (client.bottom - client.top + height - 1) / + height) { + client.top += (old_count - new_top) * height; + if (client.top < 0) client.top = 0; + InvalidateRect(list_hwnd, &client, FALSE); + } + if (had_pending) InvalidateRect(list_hwnd, &pending, FALSE); + UpdateWindow(list_hwnd); /* Do not starve new rows or selection paints. */ +} + +static void +update_progress(void) +{ + int count = loaded_count(); + bool searching = input_done && search_query && *search_query + && search_pos < 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, + count ? (int)(100LL * search_pos / count) : 100, 0); +} + +static void +queue_search(void) +{ + if (!search_posted) search_posted = PostMessageW(main_hwnd, WM_SEARCH_STEP, 0, + 0) != 0; +} + +static VOID WINAPI +search_fiber_run(void *unused) +{ + (void)unused; + for (;;) { + EnterCriticalSection(&items_lock); + int remaining = item_count - search_pos; + int end = search_pos + (remaining < SEARCH_BATCH ? remaining : SEARCH_BATCH); + 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; + match_capacity = next; + } + matches[match_count++] = search_pos; + } + LeaveCriticalSection(&items_lock); + SwitchToFiber(ui_fiber); /* Never yield while holding items_lock. */ + } +} + +static void +search_step(void) +{ + if (!search_query || !*search_query || search_pos >= loaded_count()) { + KillTimer(main_hwnd, TIMER_SEARCH); + return; + } + SwitchToFiber(search_fiber); + update_count(); + restore_location(); + update_progress(); + if (search_pos >= loaded_count()) KillTimer(main_hwnd, TIMER_SEARCH); + else if (HIWORD(GetQueueStatus(QS_INPUT))) + SetTimer(main_hwnd, TIMER_SEARCH, 15, NULL); /* Let keyboard/mouse go first. */ + else 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; + if (!restoring_location) { + pending_item = -1; + if (search_query) { + int item = row_item((int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0)); + if (!location_count) push_location(search_query, item); + else if (!strcmp(locations[location_cursor].query, search_query) && item >= 0) + locations[location_cursor].item = item; + bool related = *query && *search_query && last_row <= 0 && + (!strncmp(query, search_query, strlen(query)) || + !strncmp(query, search_query, strlen(search_query))); + bool intermediate_empty = !*search_query && *query && last_row <= 0 && + location_count > 1 && !*locations[location_cursor].query; + if (related || intermediate_empty) { + char *history_copy = _strdup(query); + if (!history_copy) { + free(copy); + return; + } + truncate_locations(); + free(locations[location_cursor].query); + locations[location_cursor] = (struct location) { + history_copy, -1 + }; + } else push_location(query, -1); + } + } + free(search_query); + search_query = copy; + match_count = 0; + search_pos = 0; + KillTimer(main_hwnd, TIMER_SEARCH); + 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); + restore_location(); + } + if (*search_query && search_pos < loaded_count()) queue_search(); + else if (!*search_query) update_count(); + if (progress_hwnd) update_progress(); +} + +static char * +current_text(void) +{ + int sel = list_hwnd ? (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) : -1; + if (has_items && (sel >= 0 || pending_item >= 0)) { + char *s = NULL; + EnterCriticalSection(&items_lock); + int index = pending_item >= 0 ? pending_item : row_item(sel); + if (index >= 0 && index < item_count) s = _strdup(items[index]); + LeaveCriticalSection(&items_lock); + return s; + } + return text_encoded(input_hwnd); +} + +static void +add_selection(void) +{ + char *s = current_text(); + 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(""); + SendMessageW(list_hwnd, LB_SETCURSEL, -1, 0); + last_row = -1; + UpdateWindow(list_hwnd); + } +} + +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 (s && *s) fputs(s, stdout); + free(s); + PostQuitMessage(0); +} + +static void +list_move(int delta) +{ + if (!has_items) return; + LRESULT count = SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0); + LRESULT cur = SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); + LRESULT previous = cur; + if (count <= 0) return; + if (cur < 0) cur = delta > 0 ? -1 : count; + cur += delta; + if (cur < 0) cur = 0; + if (cur >= count) cur = count - 1; + if (cur == previous) return; + SendMessageW(list_hwnd, LB_SETCURSEL, cur, 0); + record_location((int)previous, (int)cur); + pending_item = -1; + last_row = (int)cur; + 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, cur, (LPARAM)&rect) != LB_ERR) + InvalidateRect(list_hwnd, &rect, FALSE); + UpdateWindow(list_hwnd); +} + +static void +edit_key(WORD key) +{ + SendMessageW(input_hwnd, WM_KEYDOWN, key, 0); +} + +static void +move_caret(int pos) +{ + SendMessageW(input_hwnd, EM_SETSEL, pos, pos); +} + +static bool +at_start_unselected(HWND hwnd) +{ + DWORD start = 0, end = 0; + SendMessageW(hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end); + return start == 0 && end == 0; +} + +static void +delete_to_eol(void) +{ + DWORD start = 0, endsel = 0; + int end = GetWindowTextLengthW(input_hwnd); + SendMessageW(input_hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&endsel); + SendMessageW(input_hwnd, EM_SETSEL, start, end); + edit_key(VK_DELETE); +} + +static void +delete_prev_word(void) +{ + DWORD start = 0, end = 0; + int len = GetWindowTextLengthW(input_hwnd); + wchar_t *s = (wchar_t *)calloc(len + 1, sizeof(wchar_t)); + if (!s) return; + GetWindowTextW(input_hwnd, s, len + 1); + SendMessageW(input_hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end); + if (start != end) { + SendMessageW(input_hwnd, EM_REPLACESEL, TRUE, (LPARAM)L""); + } else if (start) { + int i = (int)start; + while (i > 0 && iswspace(s[i - 1])) --i; + while (i > 0 && !iswspace(s[i - 1])) --i; + SendMessageW(input_hwnd, EM_SETSEL, i, start); + SendMessageW(input_hwnd, EM_REPLACESEL, TRUE, (LPARAM)L""); + } + free(s); +} + +struct key_state { + bool ctrl, alt, shift; +}; + +static struct key_state +key_state(void) +{ + struct key_state k = { + (GetKeyState(VK_CONTROL) & 0x8000) != 0, + (GetKeyState(VK_MENU) & 0x8000) != 0, + (GetKeyState(VK_SHIFT) & 0x8000) != 0, + }; + return k; +} + +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) { + if (vi_mode && !normal_mode) normal_mode = true; + else PostQuitMessage(0); + return true; + } + if (key == VK_DOWN) { + list_move(1); + return true; + } + if (key == VK_UP) { + list_move(-1); + return true; + } + return false; +} + +/* Detachable mode: remove this and its call for plain Windows controls. */ +static bool +handle_emacs_keys(HWND hwnd, WORD key, struct key_state k) +{ + if (default_controls) return false; + if (k.ctrl && key == VK_BACK) { + delete_prev_word(); + return true; + } + if (k.ctrl && key == 'A') { + if (at_start_unselected(hwnd)) SendMessageW(hwnd, EM_SETSEL, 0, -1); + else move_caret(0); + return true; + } + if (k.ctrl && key == 'E') { + move_caret(GetWindowTextLengthW(hwnd)); + return true; + } + if (k.ctrl && key == 'K') { + delete_to_eol(); + return true; + } + if (k.ctrl && key == 'W') { + SendMessageW(hwnd, WM_CUT, 0, 0); + return true; + } + if (k.ctrl && k.alt && key == 'V') { + vi_mode = !vi_mode; + normal_mode = vi_mode; + return true; + } + if (k.ctrl && key == 'N') { + list_move(1); + return true; + } + if (k.ctrl && key == 'P') { + list_move(-1); + return true; + } + if (k.ctrl && key == 'D') { + list_move(lines > 0 ? lines : 5); + return true; + } + if (k.ctrl && key == 'U') { + list_move(-(lines > 0 ? lines : 5)); + return true; + } + return false; +} + +/* Detachable mode: remove this and Ctrl+Alt+V for no vi support. */ +static bool +handle_vi_keys(WORD key, struct key_state k) +{ + if (!vi_mode || !normal_mode) return false; + switch (key) { + case 'I': + if (k.shift) edit_key(VK_HOME); + normal_mode = false; + return true; + case 'A': + if (k.shift) edit_key(VK_END); + else edit_key(VK_RIGHT); + normal_mode = false; + return true; + case 'H': + edit_key(VK_LEFT); + return true; + case 'L': + edit_key(VK_RIGHT); + return true; + case '0': + edit_key(VK_HOME); + return true; + case '4': + if (k.shift) { + edit_key(VK_END); + return true; + } + break; + case 'X': + if (k.shift) return true; + edit_key(VK_DELETE); + return true; + 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': { + 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); + pending_item = -1; + last_row = target; + UpdateWindow(list_hwnd); + } + return true; + } + case 'W': + edit_key(VK_RIGHT); + return true; + case 'B': + edit_key(VK_LEFT); + return true; + } + return false; +} + +static LRESULT CALLBACK +InputProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) +{ + 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; + if (handle_emacs_keys(hwnd, (WORD)wp, k)) return 0; + if (handle_vi_keys((WORD)wp, k)) return 0; + } + return CallWindowProcW(old_input_proc, hwnd, msg, wp, lp); +} + +static LRESULT CALLBACK +ListProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) +{ + if (msg == WM_KEYDOWN) { + if (wp == VK_ESCAPE) { + handle_global_keys(VK_ESCAPE, key_state()); + return 0; + } + if (wp == VK_RETURN) { + finish(); + return 0; + } + } + LRESULT r = CallWindowProcW(old_list_proc, hwnd, msg, wp, lp); + if (msg == WM_SETFOCUS || msg == WM_LBUTTONDOWN || msg == WM_MOUSEWHEEL) + SetFocus(input_hwnd); + return r; +} + +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) { + char *q = text_encoded(input_hwnd); + if (q) refill_list(q); + free(q); + } + 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 = loaded_count() > 0; + KillTimer(hwnd, TIMER_PROGRESS); + if (list_hwnd) { + char *q = text_encoded(input_hwnd); + if (q) refill_list(q); + free(q); + update_progress(); + } + return 0; + case WM_SEARCH_STEP: + search_posted = false; + search_step(); + return 0; + case WM_TIMER: + if (wp == TIMER_SEARCH) { + KillTimer(hwnd, TIMER_SEARCH); + if (!search_posted) search_step(); + return 0; + } + 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); + } + 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 = (int)draw->itemID == (int)SendMessageW(list_hwnd, + LB_GETCURSEL, 0, 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) { + char *q = text_encoded(input_hwnd); + 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); + pending_item = -1; + last_row = row; + } + if (LOWORD(wp) == ID_COMBO && HIWORD(wp) == LBN_DBLCLK) { + if (multi_select) add_selection(); + else finish(); + } + return 0; + case WM_ACTIVATE: + if (close_on_blur && LOWORD(wp) == WA_INACTIVE) PostQuitMessage(0); + else if (LOWORD(wp) != WA_INACTIVE && input_hwnd) SetFocus(input_hwnd); + return 0; + case WM_ACTIVATEAPP: + if (close_on_blur && !wp) PostQuitMessage(0); + else if (wp && input_hwnd) SetFocus(input_hwnd); + return 0; + case WM_CLOSE: + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + return DefWindowProcW(hwnd, msg, wp, lp); +} + +static int +fit_lines(int requested, int screen_height) +{ + int row_height = S(22); + int fit = row_height > 0 + && screen_height > S(28) ? (screen_height - S(28)) / row_height : 1; + if (fit < 1) fit = 1; + if (requested == -1) return fit; + if (requested < 1) requested = 5; + return requested < fit ? requested : fit; +} + +static void +usage(void) +{ + puts("usage: wmenu [-chimbdVv] [-e utf8|ansi|oem] [-p prompt] " + "[-x px] [-y px] [-w px] [-l lines] " + "[-fn name] [-fs pt] [-fw weight] < items"); +} + +int +main(int argc, char **argv) +{ + HINSTANCE inst = GetModuleHandleW(NULL); + INITCOMMONCONTROLSEX icc = { sizeof icc, ICC_PROGRESS_CLASS }; + WNDCLASSW wc = {0}; + MSG msg; + int prompt_w = 0; + + ARGBEGIN { + case 'h': + usage(); + return 0; + case 'v': + puts("version: 0.8"); + return 0; + case 'e': + { + char *enc = EARGF(usage()); + if (!strcmp(enc, "ansi")) text_codepage = CP_ACP; + else if (!strcmp(enc, "oem")) text_codepage = CP_OEMCP; + else if (!strcmp(enc, "utf8") + || !strcmp(enc, "utf-8")) text_codepage = CP_UTF8; + } + 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; + free(w); + } + break; + case 'f': + { + wchar_t *w; + 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; + free(w); + } else if (argv[0][1] == 's') { + argv[0]++; + custom_font = true; + font_size = atoi(EARGF(usage())); + } else if (argv[0][1] == 'w') { + argv[0]++; + custom_font = true; + font_weight = atoi(EARGF(usage())); + } + } + break; + case 'x': + win_x = atoi(EARGF(usage())); + break; + case 'y': + win_y = atoi(EARGF(usage())); + break; + case 'w': + win_w = atoi(EARGF(usage())); + break; + case 'l': + lines = atoi(EARGF(usage())); + break; + case 'c': + center_window = true; + break; + case 'm': + multi_select = true; + break; + case 'V': + vi_mode = true; + break; + case 'd': + default_controls = true; + break; + case 'b': + close_on_blur = true; + break; + case 'i': + ignore_case = true; + break; + default: + usage(); + return 1; + } ARGEND; + + InitializeCriticalSection(&items_lock); + stdin_pipe = !_isatty(_fileno(stdin)); + input_done = !stdin_pipe; + if (stdin_pipe) { + has_items = true; + loading_items = true; + } + + SetProcessDPIAware(); + HDC screen = GetDC(NULL); + dpi = GetDeviceCaps(screen, LOGPIXELSX); + ReleaseDC(NULL, screen); + InitCommonControlsEx(&icc); + make_font(); + if (!win_w) win_w = GetSystemMetrics(SM_CXSCREEN); + lines = fit_lines(lines, GetSystemMetrics(SM_CYSCREEN)); + win_h = has_items ? S(28) + lines * S(22) : S(28); + if (center_window) { + win_x = (GetSystemMetrics(SM_CXSCREEN) - win_w) / 2; + win_y = (GetSystemMetrics(SM_CYSCREEN) - win_h) / 2; + } + + wc.lpfnWndProc = WndProc; + wc.hInstance = inst; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszClassName = L"wmenu"; + if (!RegisterClassW(&wc)) return 1; + + main_hwnd = CreateWindowExW(WS_EX_TOPMOST | WS_EX_APPWINDOW, L"wmenu", + L"wmenu", WS_POPUP | WS_BORDER, win_x, win_y, win_w, win_h, NULL, NULL, inst, + NULL); + if (!main_hwnd) return 1; + + if (has_prompt) { + prompt_w = (int)wcslen(prompt) * S(8) + S(12); + HWND label = CreateWindowW(L"STATIC", prompt, WS_CHILD | WS_VISIBLE, S(4), + S(5), prompt_w, S(22), main_hwnd, (HMENU)ID_LABEL, inst, NULL); + set_font(label); + } + + 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 | 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 { + 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); + } + 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); + SetFocus(input_hwnd); + if (loading_items) { + SetTimer(main_hwnd, TIMER_PROGRESS, 30, 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); + DispatchMessageW(&msg); + } + if (search_fiber) { + DeleteFiber(search_fiber); + ConvertFiberToThread(); + } + return (int)msg.wParam; }
: nply/wmenu