: nply/wmenu

commit 14afa3e42b7e02c07e90a26caa38a54d9efb69ff
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 21:31:04 +0530

Release wmenu 0.7
7 files changed, 729 insertions(+), 0 deletions(-)
A
.gitignore
|
13
+++++++++++++
A
HACKING
|
100
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
LICENSE
|
62
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
Makefile
|
19
+++++++++++++++++++
A
README
|
61
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
requirements.ini
|
5
+++++
A
wmenu.c
|
469
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,13 @@
+*.*user
+x64/*
+/.vs/*
+build/*
+Debug/*
+Release/*
+/garbage
+wmenu.vcxproj.user
+*.obj
+*.exe
+mor.log
+deps/
+*.manifest
diff --git a/HACKING b/HACKING
@@ -0,0 +1,100 @@
+wmenu hacking notes
+===================
+
+Goal
+----
+wmenu is a small Win32 dmenu-style picker. Keep it boring: one C file, native
+controls, stdin in, stdout out.
+
+Build
+-----
+    make
+
+Dependencies
+------------
+External source is pulled with mor from requirements.ini:
+
+    bash C:/Users/bhara/Projects/mor/mor.sh .
+
+Currently this fetches fzy into deps/fzy. The build uses only fzy's matcher:
+
+    deps/fzy/src/match.c
+    deps/fzy/src/match.h
+
+Makefile generates two tiny compatibility files inside deps/ when needed:
+
+    deps/fzy/config.h
+    deps/fzy/src/strings.h
+
+Program shape
+-------------
+Important sections in wmenu.c:
+
+- input/output conversion: widen(), text_encoded(), current_text()
+- stdin loading: read_stdin_thread()
+- matching/list population: refill_list()
+- selection/output: add_selection(), finish()
+- key modes:
+  - handle_global_keys()
+  - handle_emacs_keys()
+  - handle_vi_keys()
+- Win32 plumbing:
+  - InputProc()
+  - ListProc()
+  - WndProc()
+  - main()
+
+Detachable modes
+----------------
+The key modes are intentionally isolated. To remove a mode:
+
+- Emacs keys: delete handle_emacs_keys() and its call from InputProc().
+- Vi keys: delete handle_vi_keys() and the Ctrl+Alt+V toggle in
+  handle_emacs_keys().
+- Default Windows controls: pass -default-controls at runtime.
+
+Keep global keys tiny. Enter, Esc, Up/Down, and multi-select belong in
+handle_global_keys(). Editing policy belongs in a mode handler.
+
+Current key policy
+------------------
+Default mode is emacs-ish, but pragmatic:
+
+- Ctrl+A: first press moves to start; second press at start selects all
+- Ctrl+E: end
+- Ctrl+K: delete to end
+- Ctrl+Backspace: delete previous word
+- Ctrl+N / Ctrl+P: next/previous list item
+- Ctrl+D / Ctrl+U: page down/up list
+- Ctrl+W: cut selection
+- Ctrl+Alt+V: toggle vi mode
+- Esc: exit
+
+Use -default-controls for normal Windows edit behavior.
+
+Release checklist
+-----------------
+1. Run make.
+2. Check:
+       .\\wmenu.exe -help
+       .\\wmenu.exe -version
+       dir /b | .\\wmenu.exe
+3. Check a slow pipe shows the progress bar.
+4. Check Alt-Tab back focuses the input.
+5. Check Esc exits from input and list focus.
+6. Update the version string in wmenu.c.
+7. Do not commit build outputs (*.obj, *.exe) unless intentionally packaging.
+
+HiDPI/layout
+------------
+Pixel constants in wmenu.c should go through `S(px)`, which scales from the
+system DPI. Do not add new raw layout numbers unless they are not pixels.
+
+Known simplifications
+---------------------
+- Default input/output encoding is UTF-8. Use `-enc ansi` or `-enc oem` for
+  legacy Windows pipes such as classic `cmd.exe` output.
+- List filtering rebuilds the whole ListBox on every edit. That is fine for
+  normal launcher-sized lists. Optimize only with measured pain.
+- Stdin is loaded on a background thread, then the list is populated once done.
+  Incremental display can be added later if huge/slow streams matter.
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,62 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, Bharatvaj Hemanth
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+arg.h (suckless, by 20h): MIT/X Consortium License
+--------------------------------------------------
+This notice is reproduced from the dmenu distribution containing arg.h.
+
+© 2006-2019 Anselm R Garbe <anselm@garbe.ca>
+© 2006-2008 Sander van Dijk <a.h.vandijk@gmail.com>
+© 2006-2007 Michał Janeczek <janeczek@gmail.com>
+© 2007 Kris Maglione <jg@suckless.org>
+© 2009 Gottox <gottox@s01.de>
+© 2009 Markus Schnalke <meillo@marmaro.de>
+© 2010-2012 Connor Lane Smith <cls@lubutu.com>
+© 2014-2022 Hiltjo Posthuma <hiltjo@codemadness.org>
+© 2015-2019 Quentin Rameau <quinq@fifth.space>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,19 @@
+
+
+
+RELEASE=/Release /D_WIN32 /DNDEBUG /D_CONSOLE
+DEBUG=/D_WIN32 /D_DEBUG /D_CONSOLE /D_CRT_SECURE_NO_WARNINGS
+
+CFLAGS = $(DEBUG)
+
+wmenu.exe: wmenu.c deps/fzy/src/match.c deps/fzy/config.h deps/fzy/src/strings.h
+	cl wmenu.c deps/fzy/src/match.c /I deps/fzy/src $(CFLAGS) user32.lib comctl32.lib gdi32.lib
+
+deps/fzy/config.h: deps/fzy/src/config.def.h
+	printf '#include "src/config.def.h"\n' > deps/fzy/config.h
+
+deps/fzy/src/strings.h:
+	printf '/* generated for MSVC: fzy match.c does not need strings.h here */\n' > deps/fzy/src/strings.h
+
+wmenu-mingw.exe: wmenu.c deps/fzy/src/match.c deps/fzy/config.h deps/fzy/src/strings.h
+	/usr/bin/x86_64-w64-mingw32-gcc -municode -I deps/fzy/src -o wmenu-mingw.exe wmenu.c deps/fzy/src/match.c -luser32 -lcomctl32 -lgdi32
diff --git a/README b/README
@@ -0,0 +1,61 @@
+wmenu
+=====
+
+A small Win32 picker for newline-separated input. Inspired by LinArcX's
+original wmenu: https://github.com/LinArcX/wmenu
+
+Build
+-----
+Use a Visual Studio Developer Command Prompt and run make.
+
+How it works
+------------
+wmenu is a GUI filter: newline-separated stdin goes in, the selected line is printed to stdout.
+
+Examples:
+
+```
+dir /b | .\wmenu.exe
+where.exe *.exe | .\wmenu.exe -p run:
+dir /b | .\wmenu.exe -c -w 800 -l 12
+```
+
+Useful flags:
+
+```
+-p text             prompt text
+-enc utf8|ansi|oem  stdin/stdout/prompt encoding; default utf8
+-x px -y px -w px   position and width
+-c / -center        center window
+-l lines            visible list lines
+-multi              select multiple entries; Ctrl+Enter adds one
+-vi                 enable vi support; Ctrl+Alt+V toggles vi mode
+-default-controls   use normal Windows edit shortcuts instead of emacs-ish keys
+-close-on-blur      exit when focus leaves the window
+-fn name -fs pt -fw weight
+                    font face, size, and weight
+-version            print version
+-help               print usage
+```
+
+Default keys:
+
+```
+Enter       print selection and exit
+Esc         exit
+Up/Down     move list selection
+Ctrl+N/P    next/previous item
+Ctrl+D/U    page down/up
+Ctrl+A      move to start; press again at start to select all
+Ctrl+E      move to end
+Ctrl+K      delete to end
+Ctrl+W      cut selection
+Ctrl+Backspace delete previous word
+Ctrl+Alt+V  toggle vi mode
+```
+
+See HACKING for code layout and release notes.
+
+License
+-------
+BSD 3-Clause; see LICENSE. arg.h is MIT/X Consortium licensed.
diff --git a/requirements.ini b/requirements.ini
@@ -0,0 +1,5 @@
+[$]
+/=deps
+
+[.]
+fzy=https://github.com/jhawthorn/fzy.git@master
diff --git a/wmenu.c b/wmenu.c
@@ -0,0 +1,469 @@
+#define UNICODE
+#define _UNICODE
+#include <windows.h>
+#include <commctrl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <io.h>
+#include <wctype.h>
+
+#include "deps/fzy/src/match.h"
+
+#ifdef _MSC_VER
+#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
+#endif
+
+#define ID_COMBO 101
+#define ID_EDIT 102
+#define ID_LABEL 103
+#define ID_PROGRESS 104
+#define WM_STDIN_CHUNK (WM_APP + 1)
+#define WM_STDIN_DONE (WM_APP + 2)
+
+static int dpi = 96;
+#define S(px) MulDiv((px), dpi, 96)
+
+static char **items, **selected;
+static int item_count, selected_count;
+static bool has_items, has_prompt, multi_select, vi_mode, normal_mode, default_controls;
+static wchar_t prompt[300];
+static HWND main_hwnd, input_hwnd, list_hwnd, progress_hwnd;
+static WNDPROC old_input_proc, old_list_proc;
+static CRITICAL_SECTION items_lock;
+static int win_x = 0, win_y = 0, win_w = 0, win_h = 0, lines = 5;
+static int font_size = 0, font_weight = FW_NORMAL;
+static UINT text_codepage = CP_UTF8;
+static wchar_t font_name[LF_FACESIZE];
+static HFONT ui_font;
+static bool custom_font;
+static bool center_window, close_on_blur, stdin_pipe, loading_items;
+
+static 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 void add_item(const char *s)
+{
+    EnterCriticalSection(&items_lock);
+    items = (char **)realloc(items, sizeof(char *) * (item_count + 1));
+    items[item_count++] = _strdup(s ? s : "");
+    LeaveCriticalSection(&items_lock);
+}
+
+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);
+}
+
+static DWORD WINAPI read_stdin_thread(void *unused)
+{
+    char buf[4096];
+    int n = 0;
+    (void)unused;
+    while (fgets(buf, sizeof buf, stdin)) {
+        buf[strcspn(buf, "\r\n")] = 0;
+        if (*buf) {
+            add_item(buf);
+            if (++n == 1 || n % 16 == 0) PostMessageW(main_hwnd, WM_STDIN_CHUNK, 0, 0);
+        }
+    }
+    loading_items = false;
+    PostMessageW(main_hwnd, WM_STDIN_DONE, 0, 0);
+    return 0;
+}
+
+struct ranked_item { int index; score_t score; };
+
+static int rank_cmp(const void *a, const void *b)
+{
+    const struct ranked_item *x = (const struct ranked_item *)a, *y = (const struct ranked_item *)b;
+    return x->score < y->score ? 1 : x->score > y->score ? -1 : x->index - y->index;
+}
+
+static void refill_list(const char *query)
+{
+    struct ranked_item *ranked;
+    int n = 0;
+    EnterCriticalSection(&items_lock);
+    ranked = (struct ranked_item *)calloc(item_count, sizeof(*ranked));
+    SendMessageW(list_hwnd, LB_RESETCONTENT, 0, 0);
+    for (int i = 0; i < item_count; ++i) {
+        if (!query || !*query || has_match(query, items[i])) {
+            ranked[n].index = i;
+            ranked[n++].score = query && *query ? match(query, items[i]) : 0;
+        }
+    }
+    qsort(ranked, n, sizeof(*ranked), rank_cmp);
+    for (int i = 0; i < n; ++i) {
+        wchar_t *w = widen(items[ranked[i].index]);
+        SendMessageW(list_hwnd, LB_ADDSTRING, 0, (LPARAM)w);
+        free(w);
+    }
+    LeaveCriticalSection(&items_lock);
+    if (n) SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0);
+    free(ranked);
+}
+
+static char *current_text(void)
+{
+    wchar_t *w;
+    int n, m;
+    char *s;
+    LRESULT sel = list_hwnd ? SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) : LB_ERR;
+    if (has_items && sel != LB_ERR) {
+        n = (int)SendMessageW(list_hwnd, LB_GETTEXTLEN, sel, 0) + 1;
+        w = (wchar_t *)calloc(n, sizeof(wchar_t));
+        if (!w) return NULL;
+        SendMessageW(list_hwnd, LB_GETTEXT, sel, (LPARAM)w);
+    } else {
+        n = GetWindowTextLengthW(input_hwnd) + 1;
+        w = (wchar_t *)calloc(n, sizeof(wchar_t));
+        if (!w) return NULL;
+        GetWindowTextW(input_hwnd, w, n);
+    }
+    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 void add_selection(void)
+{
+    char *s = current_text();
+    if (!s || !*s) { free(s); return; }
+    selected = (char **)realloc(selected, sizeof(char *) * (selected_count + 1));
+    selected[selected_count++] = s;
+    SetWindowTextW(input_hwnd, L"");
+    if (has_items) refill_list("");
+}
+
+static void finish(void)
+{
+    for (int i = 0; i < selected_count; ++i) printf("%s\n", selected[i]);
+    if (input_hwnd) {
+        char *s = current_text();
+        if (s && *s) fputs(s, stdout);
+        free(s);
+    }
+    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);
+    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;
+    SendMessageW(list_hwnd, LB_SETCURSEL, cur, 0);
+}
+
+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 == VK_RETURN && multi_select) { add_selection(); return true; }
+    if (key == VK_RETURN) { finish(); return true; }
+    if (key == VK_ESCAPE) { 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': SendMessageW(list_hwnd, LB_SETCURSEL, k.shift ? SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) - 1 : 0, 0); 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)) 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) { PostQuitMessage(0); 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:
+        if (list_hwnd && loading_items) {
+            char *q = text_encoded(input_hwnd);
+            refill_list(q);
+            free(q);
+        }
+        return 0;
+    case WM_STDIN_DONE:
+        has_items = item_count > 0;
+        if (progress_hwnd) ShowWindow(progress_hwnd, SW_HIDE);
+        if (list_hwnd) {
+            SetWindowPos(list_hwnd, NULL, 0, S(28), win_w, win_h - S(28), SWP_NOZORDER);
+            refill_list("");
+        }
+        return 0;
+    case WM_COMMAND:
+        if (LOWORD(wp) == ID_LABEL) return 0;
+        if (LOWORD(wp) == ID_EDIT && HIWORD(wp) == EN_CHANGE && has_items && !loading_items) {
+            char *q = text_encoded(input_hwnd);
+            refill_list(q);
+            free(q);
+        }
+        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 void usage(void)
+{
+    puts("usage: wmenu [-p prompt] [-enc utf8|ansi|oem] [-fn name] [-fs pt] [-fw weight] [-x px] [-y px] [-w px] [-c] [-l lines] [-multi] [-vi] [-default-controls] [-close-on-blur] < 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;
+
+    for (int i = 1; i < argc; ++i) {
+        if (!strcmp(argv[i], "-help")) { usage(); return 0; }
+        if (!strcmp(argv[i], "-version")) { puts("version: 0.7"); return 0; }
+        if ((!strcmp(argv[i], "-enc") || !strcmp(argv[i], "-encoding")) && i + 1 < argc) {
+            char *enc = argv[++i];
+            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;
+        }
+        else if ((!strcmp(argv[i], "-p") || !strcmp(argv[i], "-prompt")) && i + 1 < argc) { wchar_t *w = widen(argv[++i]); has_prompt = true; wcsncpy(prompt, w, 299); prompt[299] = 0; free(w); }
+        else if ((!strcmp(argv[i], "-fn") || !strcmp(argv[i], "-font")) && i + 1 < argc) { wchar_t *w = widen(argv[++i]); custom_font = true; wcsncpy(font_name, w, LF_FACESIZE - 1); font_name[LF_FACESIZE - 1] = 0; free(w); }
+        else if ((!strcmp(argv[i], "-fs") || !strcmp(argv[i], "-font-size")) && i + 1 < argc) { custom_font = true; font_size = atoi(argv[++i]); }
+        else if ((!strcmp(argv[i], "-fw") || !strcmp(argv[i], "-font-weight")) && i + 1 < argc) { custom_font = true; font_weight = atoi(argv[++i]); }
+        else if (!strcmp(argv[i], "-x") && i + 1 < argc) win_x = atoi(argv[++i]);
+        else if (!strcmp(argv[i], "-y") && i + 1 < argc) win_y = atoi(argv[++i]);
+        else if (!strcmp(argv[i], "-w") && i + 1 < argc) win_w = atoi(argv[++i]);
+        else if (!strcmp(argv[i], "-l") && i + 1 < argc) lines = atoi(argv[++i]);
+        else if (!strcmp(argv[i], "-c") || !strcmp(argv[i], "-center")) center_window = true;
+        else if (!strcmp(argv[i], "-multi")) multi_select = true;
+        else if (!strcmp(argv[i], "-vi")) vi_mode = true;
+        else if (!strcmp(argv[i], "-default-controls")) default_controls = true;
+        else if (!strcmp(argv[i], "-close-on-blur")) close_on_blur = true;
+        else if (!strcmp(argv[i], "-i")) ;
+    }
+
+    InitializeCriticalSection(&items_lock);
+    stdin_pipe = !_isatty(_fileno(stdin));
+    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);
+    win_h = has_items ? S(28) + (lines > 0 ? lines : 5) * 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 | WS_VSCROLL, 0, S(28), win_w, win_h - S(28) - (loading_items ? S(6) : 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) | PBS_MARQUEE, 0, win_h - S(6), win_w, S(6), main_hwnd, (HMENU)ID_PROGRESS, inst, NULL);
+        SendMessageW(progress_hwnd, PBM_SETMARQUEE, TRUE, 30);
+        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);
+
+    ShowWindow(main_hwnd, SW_SHOW);
+    UpdateWindow(main_hwnd);
+    SetFocus(input_hwnd);
+    if (loading_items) CreateThread(NULL, 0, read_stdin_thread, NULL, 0, NULL);
+    while (GetMessageW(&msg, NULL, 0, 0) > 0) {
+        TranslateMessage(&msg);
+        DispatchMessageW(&msg);
+    }
+    return 0;
+}