diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | LICENSE | 15 | ||||
| -rw-r--r-- | Makefile | 34 | ||||
| -rw-r--r-- | README | 60 | ||||
| -rw-r--r-- | config.def.h | 22 | ||||
| -rw-r--r-- | config.mk | 19 | ||||
| -rw-r--r-- | selc.c | 548 |
7 files changed, 701 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..189fb68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config.h +selc.exe +*.o @@ -0,0 +1,15 @@ +ISC License + +© 2026 selc authors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..feab946 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +.POSIX: + +include config.mk + +HDR = config.h +SRC = selc.c + +all: selc$(EXE) + +config.h: + copy /Y config.def.h config.h + +selc$(EXE): $(SRC) $(HDR) + $(CC) $(CFLAGS) -o $@ $(SRC) $(LDFLAGS) + +clean: + -$(RM) selc$(EXE) + +install: all + -mkdir "$(subst /,\,$(DESTDIR)$(BINDIR))" + copy /Y selc$(EXE) "$(subst /,\,$(DESTDIR)$(BINDIR)/selc$(EXE))" + @echo installed to $(DESTDIR)$(BINDIR)/selc$(EXE) + +uninstall: + -$(RM) "$(subst /,\,$(DESTDIR)$(BINDIR)/selc$(EXE))" + +boot: install + "$(subst /,\,$(DESTDIR)$(BINDIR))\selc$(EXE)" -i + +unboot: + -"$(subst /,\,$(DESTDIR)$(BINDIR))\selc$(EXE)" -u + -$(RM) "$(subst /,\,$(DESTDIR)$(BINDIR)/selc$(EXE))" + +.PHONY: all clean install uninstall boot unboot @@ -0,0 +1,60 @@ +selc - select-copy + + selc [-i] [-k] [-u] [-v] + +Windows X11-PRIMARY analogue: selecting text copies it. A drag, +double-click, shift+click, or shift+arrow selection is copied to +the clipboard. No tray, no window, one process, a mouse hook that +only snapshots points. + + selc run (no-op if already running) + selc -i write HKCU Run (start at logon) and run + selc -u remove HKCU Run and stop + selc -k stop the running instance + selc -v run with a console; print copy method + + +Build + + make + make install + make boot + +boot is install plus selc -i. The binary is a Windows subsystem +image; launching it from Explorer or Run does not open a console. +as(1) and ld(1) must be the MinGW ones; config.mk prepends +MINGWBIN to PATH. + +Default install path: %USERPROFILE%\.local\bin +Override: make PREFIX=... install + + +How + +A WH_MOUSE_LL hook (and optionally WH_KEYBOARD_LL) records the +gesture on the hook thread and posts to a message-only window. +The window waits COPYWAIT ms so the target app can finish the +selection, then: + + 1. ignore non-client hits (caption, scrollbars, resize) + 2. ignore password Edit controls + 3. WM_COPY to the focused child (SendMessageTimeout) + 4. if the clipboard sequence did not change, inject Ctrl+C + except into console classes listed in config.h + +Logon autostart is HKCU\...\Run\selc, so it starts with the +interactive session. That is the right time; a clipboard helper +cannot run in session 0. + + +Remarks + +Injected Ctrl+C is skipped for ConsoleWindowClass, Windows +Terminal, and mintty so a false gesture cannot interrupt a +job. Those hosts often already copy on select. + +UIPI: an unelevated selc cannot copy from an elevated window. +Run it at the same integrity level as the programs you use. + +Edit config.h (copied from config.def.h) for thresholds and +the no-Ctrl+C class list. Recompile. diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..357bcba --- /dev/null +++ b/config.def.h @@ -0,0 +1,22 @@ +/* Copy to config.h and edit. Recompile after changing knobs. */ + +enum { + MINPIX = 4, /* min drag distance in pixels */ + COPYWAIT = 25, /* ms after the gesture before we copy */ + COPYTO = 50, /* SendMessageTimeout budget for WM_COPY */ + HITTO = 50, /* same for WM_NCHITTEST */ + DBLCOPY = 1, /* copy on double/triple click */ + SHIFTCLICK = 1, /* copy on shift+click (extend selection) */ + KEYSEL = 1, /* copy on shift+arrows/home/end/pgup/pgdn */ + CTRLA = 0 /* copy after Ctrl+A; 0: Explorer would copy files */ +}; + +/* + * Root window classes that must not receive injected Ctrl+C. + * WM_COPY is still tried. Console hosts treat Ctrl+C as interrupt. + */ +static const char *nocctrlc[] = { + "ConsoleWindowClass", + "CASCADIA_HOSTING_WINDOW_CLASS", + "mintty", +}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..1da3239 --- /dev/null +++ b/config.mk @@ -0,0 +1,19 @@ +# selc — site/toolchain knobs. Edit this, not the Makefile. +# +# Windows only. Install: make install +# default: %USERPROFILE%\.local\bin +# Boot: make boot (install + HKCU Run, then starts selc) +# Override: make PREFIX=... install + +PREFIX ?= $(USERPROFILE)/.local +BINDIR ?= $(PREFIX)/bin + +# MinGW-w64 gcc. LLVM clang in PATH targets MSVC and needs the VS +# developer prompt; MinGW does not. as/ld live next to gcc. +MINGWBIN = C:/msys64/mingw64/bin +CC = $(MINGWBIN)/gcc +CFLAGS = -std=c99 -Wall -Wextra -pedantic -Os +LDFLAGS = -mwindows -luser32 -ladvapi32 +EXE = .exe +RM = cmd /c del /q +export PATH := $(MINGWBIN);$(PATH) @@ -0,0 +1,548 @@ +/* See LICENSE file for copyright and license details. */ +#define WINVER 0x0601 +#define _WIN32_WINNT 0x0601 +#define WIN32_LEAN_AND_MEAN + +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <wchar.h> +#include <windows.h> + +#include "config.h" + +#define LEN(x) (sizeof(x) / sizeof((x)[0])) + +enum { + WM_SEL = WM_APP + 1 +}; + +struct Sel { + POINT down; + POINT up; + int dragged; + int clicks; + int shift; + int kbd; +}; + +static char *argv0; +static int verbose; +static HWND hwnd; +static HHOOK mousehook; +static HHOOK kbdhook; +static HANDLE quit; +static struct Sel sel; +static struct Sel pending; + +static void die(const char *fmt, ...); +static void usage(void); +static void attachio(int force); +static int samewin(HWND a, HWND b); +static int isclient(HWND hw, POINT pt); +static int ispass(HWND hw); +static int skipctrlc(HWND hw); +static int goodsel(const struct Sel *s); +static void sendctrlc(void); +static void docopy(void); +static void request(int kbd); +static LRESULT CALLBACK mouseproc(int code, WPARAM wp, LPARAM lp); +static LRESULT CALLBACK kbdproc(int code, WPARAM wp, LPARAM lp); +static LRESULT CALLBACK wndproc(HWND hw, UINT msg, WPARAM wp, + LPARAM lp); +static int runkey(int add); +static void signalquit(void); +static int loop(HINSTANCE inst); + +static void +die(const char *fmt, ...) +{ + va_list ap; + size_t n; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + n = strlen(fmt); + if (n && fmt[n - 1] == ':') + fprintf(stderr, " %lu\n", GetLastError()); + else + fputc('\n', stderr); + exit(1); +} + +static void +usage(void) +{ + fprintf(stderr, "usage: %s [-i] [-k] [-u] [-v]\n", + argv0 ? argv0 : "selc"); + exit(2); +} + +static void +attachio(int force) +{ + if (!AttachConsole(ATTACH_PARENT_PROCESS)) { + if (!force) + return; + AllocConsole(); + } + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); +} + +static int +samewin(HWND a, HWND b) +{ + if (!a || !b) + return 0; + if (a == b) + return 1; + return GetAncestor(a, GA_ROOT) == GetAncestor(b, GA_ROOT); +} + +static int +isclient(HWND hw, POINT pt) +{ + DWORD_PTR ht; + LPARAM lp; + + lp = MAKELPARAM((short)pt.x, (short)pt.y); + if (!SendMessageTimeoutA(hw, WM_NCHITTEST, 0, lp, + SMTO_ABORTIFHUNG | SMTO_NORMAL, HITTO, &ht)) + return 0; + return ht == HTCLIENT; +} + +static int +ispass(HWND hw) +{ + char cls[32]; + LONG style; + + if (!hw) + return 0; + if (!GetClassNameA(hw, cls, (int)sizeof(cls))) + return 0; + if (strcmp(cls, "Edit") != 0) + return 0; + style = GetWindowLongA(hw, GWL_STYLE); + return (style & ES_PASSWORD) != 0; +} + +static int +skipctrlc(HWND hw) +{ + char cls[64]; + HWND root; + int i; + + root = GetAncestor(hw, GA_ROOT); + if (!root) + root = hw; + if (!GetClassNameA(root, cls, (int)sizeof(cls))) + return 0; + for (i = 0; i < (int)LEN(nocctrlc); i++) + if (strcmp(cls, nocctrlc[i]) == 0) + return 1; + return 0; +} + +static int +goodsel(const struct Sel *s) +{ + HWND a, b; + + a = WindowFromPoint(s->down); + b = WindowFromPoint(s->up); + if (!a || !isclient(a, s->down)) + return 0; + if (!samewin(a, b)) + return 0; + return 1; +} + +static void +sendctrlc(void) +{ + INPUT in[4]; + int n, ctrl; + + ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + memset(in, 0, sizeof(in)); + n = 0; + if (!ctrl) { + in[n].type = INPUT_KEYBOARD; + in[n].ki.wVk = VK_CONTROL; + n++; + } + in[n].type = INPUT_KEYBOARD; + in[n].ki.wVk = 'C'; + n++; + in[n].type = INPUT_KEYBOARD; + in[n].ki.wVk = 'C'; + in[n].ki.dwFlags = KEYEVENTF_KEYUP; + n++; + if (!ctrl) { + in[n].type = INPUT_KEYBOARD; + in[n].ki.wVk = VK_CONTROL; + in[n].ki.dwFlags = KEYEVENTF_KEYUP; + n++; + } + SendInput((UINT)n, in, sizeof(INPUT)); +} + +static void +docopy(void) +{ + HWND fg, foc; + DWORD tid, mine, seq; + DWORD_PTR dummy; + int attached; + + if (GetAsyncKeyState(VK_MENU) & 0x8000) + return; + fg = GetForegroundWindow(); + if (!fg) + return; + mine = GetCurrentThreadId(); + tid = GetWindowThreadProcessId(fg, NULL); + attached = 0; + if (tid && tid != mine) + attached = AttachThreadInput(mine, tid, TRUE); + foc = GetFocus(); + if (!foc) + foc = fg; + if (ispass(foc)) + goto done; + seq = GetClipboardSequenceNumber(); + SendMessageTimeoutA(foc, WM_COPY, 0, 0, + SMTO_ABORTIFHUNG | SMTO_NORMAL, COPYTO, &dummy); + if (GetClipboardSequenceNumber() != seq) { + if (verbose) + fprintf(stderr, "selc: WM_COPY\n"); + goto done; + } + if (skipctrlc(foc) || skipctrlc(fg)) { + if (verbose) + fprintf(stderr, "selc: skip Ctrl+C\n"); + goto done; + } + sendctrlc(); + if (verbose) + fprintf(stderr, "selc: Ctrl+C\n"); +done: + if (attached) + AttachThreadInput(mine, tid, FALSE); +} + +static void +request(int kbd) +{ + sel.kbd = kbd; + pending = sel; + if (hwnd) + PostMessageA(hwnd, WM_SEL, 0, 0); +} + +static LRESULT CALLBACK +mouseproc(int code, WPARAM wp, LPARAM lp) +{ + MSLLHOOKSTRUCT *ms; + static POINT down, lastpt; + static int dragged, clicks, armed; + static DWORD lastup; + long dx, dy; + int dt, cx, cy; + + if (code != HC_ACTION) + return CallNextHookEx(mousehook, code, wp, lp); + ms = (MSLLHOOKSTRUCT *)lp; + if (wp == WM_LBUTTONDOWN) { + down = ms->pt; + dragged = 0; + armed = 1; + dt = (int)(ms->time - lastup); + cx = GetSystemMetrics(SM_CXDOUBLECLK); + cy = GetSystemMetrics(SM_CYDOUBLECLK); + if (DBLCOPY && dt >= 0 && + dt <= (int)GetDoubleClickTime() && + abs(ms->pt.x - lastpt.x) <= cx && + abs(ms->pt.y - lastpt.y) <= cy) + clicks++; + else + clicks = 1; + lastpt = ms->pt; + } else if (wp == WM_MOUSEMOVE) { + if (armed && !dragged) { + dx = (long)ms->pt.x - down.x; + dy = (long)ms->pt.y - down.y; + if (dx * dx + dy * dy >= + (long)MINPIX * MINPIX) + dragged = 1; + } + } else if (wp == WM_LBUTTONUP) { + lastup = ms->time; + lastpt = ms->pt; + if (armed) { + sel.down = down; + sel.up = ms->pt; + sel.dragged = dragged; + sel.clicks = clicks; + sel.shift = (GetAsyncKeyState(VK_SHIFT) & + 0x8000) != 0; + if (sel.dragged || + (DBLCOPY && sel.clicks >= 2) || + (SHIFTCLICK && sel.shift && + sel.clicks == 1)) + request(0); + } + armed = 0; + dragged = 0; + } else if (wp == WM_RBUTTONDOWN || wp == WM_MBUTTONDOWN) { + armed = 0; + dragged = 0; + } + return CallNextHookEx(mousehook, code, wp, lp); +} + +static LRESULT CALLBACK +kbdproc(int code, WPARAM wp, LPARAM lp) +{ + KBDLLHOOKSTRUCT *kb; + int shift, ctrl, alt, nav, akey; + + if (code != HC_ACTION) + return CallNextHookEx(kbdhook, code, wp, lp); + if (wp != WM_KEYUP && wp != WM_SYSKEYUP) + return CallNextHookEx(kbdhook, code, wp, lp); + kb = (KBDLLHOOKSTRUCT *)lp; + shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; + ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + alt = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; + nav = 0; + switch (kb->vkCode) { + case VK_LEFT: + case VK_RIGHT: + case VK_UP: + case VK_DOWN: + case VK_HOME: + case VK_END: + case VK_PRIOR: + case VK_NEXT: + nav = 1; + break; + } + akey = CTRLA && kb->vkCode == 'A' && ctrl && !shift && !alt; + if ((KEYSEL && nav && shift && !alt) || akey) + request(1); + return CallNextHookEx(kbdhook, code, wp, lp); +} + +static LRESULT CALLBACK +wndproc(HWND hw, UINT msg, WPARAM wp, LPARAM lp) +{ + switch (msg) { + case WM_SEL: + if (COPYWAIT > 0) + SetTimer(hw, 1, COPYWAIT, NULL); + else if (pending.kbd || goodsel(&pending)) + docopy(); + return 0; + case WM_TIMER: + if (wp != 1) + break; + KillTimer(hw, 1); + if (pending.kbd || goodsel(&pending)) + docopy(); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + return DefWindowProcA(hw, msg, wp, lp); +} + +static int +runkey(int add) +{ + HKEY k; + LONG e; + wchar_t path[MAX_PATH], val[MAX_PATH + 3]; + DWORD n; + const wchar_t *sub = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + + e = RegCreateKeyExW(HKEY_CURRENT_USER, sub, 0, NULL, 0, + KEY_SET_VALUE | KEY_QUERY_VALUE, NULL, &k, NULL); + if (e != ERROR_SUCCESS) { + SetLastError((DWORD)e); + die("RegCreateKeyEx:"); + } + if (!add) { + RegDeleteValueW(k, L"selc"); + RegCloseKey(k); + if (verbose) + fprintf(stderr, "selc: uninstalled Run key\n"); + return 0; + } + n = GetModuleFileNameW(NULL, path, (DWORD)LEN(path)); + if (n == 0 || n >= LEN(path)) { + RegCloseKey(k); + die("GetModuleFileName:"); + } + if (wcschr(path, L' ')) { + val[0] = L'"'; + wcsncpy(val + 1, path, LEN(val) - 3); + val[LEN(val) - 2] = L'\0'; + n = (DWORD)wcslen(val); + if (n < LEN(val) - 1) { + val[n] = L'"'; + val[n + 1] = L'\0'; + } + } else { + wcsncpy(val, path, LEN(val)); + val[LEN(val) - 1] = L'\0'; + } + n = (DWORD)((wcslen(val) + 1) * sizeof(wchar_t)); + e = RegSetValueExW(k, L"selc", 0, REG_SZ, (BYTE *)val, n); + RegCloseKey(k); + if (e != ERROR_SUCCESS) { + SetLastError((DWORD)e); + die("RegSetValueEx:"); + } + if (verbose) + fprintf(stderr, "selc: installed Run key\n"); + return 0; +} + +static void +signalquit(void) +{ + HANDLE e; + + e = OpenEventA(EVENT_MODIFY_STATE, FALSE, "Local\\selc-quit"); + if (!e) + return; + SetEvent(e); + CloseHandle(e); +} + +static int +loop(HINSTANCE inst) +{ + WNDCLASSA wc; + MSG msg; + DWORD r; + + memset(&wc, 0, sizeof(wc)); + wc.lpfnWndProc = wndproc; + wc.hInstance = inst; + wc.lpszClassName = "selc"; + if (!RegisterClassA(&wc)) + die("RegisterClass:"); + hwnd = CreateWindowExA(0, "selc", "selc", WS_OVERLAPPED, + 0, 0, 0, 0, HWND_MESSAGE, NULL, inst, NULL); + if (!hwnd) + die("CreateWindowEx:"); + mousehook = SetWindowsHookExA(WH_MOUSE_LL, mouseproc, inst, 0); + if (!mousehook) + die("SetWindowsHookEx mouse:"); + if (KEYSEL || CTRLA) { + kbdhook = SetWindowsHookExA(WH_KEYBOARD_LL, kbdproc, + inst, 0); + if (!kbdhook) + die("SetWindowsHookEx kbd:"); + } + if (verbose) + fprintf(stderr, "selc: running\n"); + for (;;) { + r = MsgWaitForMultipleObjects(1, &quit, FALSE, + INFINITE, QS_ALLINPUT); + if (r == WAIT_OBJECT_0) + break; + if (r != WAIT_OBJECT_0 + 1) + die("MsgWaitForMultipleObjects:"); + while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) + goto done; + TranslateMessage(&msg); + DispatchMessageA(&msg); + } + } +done: + if (kbdhook) + UnhookWindowsHookEx(kbdhook); + if (mousehook) + UnhookWindowsHookEx(mousehook); + return 0; +} + +int +main(int argc, char *argv[]) +{ + HINSTANCE inst; + HANDLE mtx; + char *a, *p; + int i, iflag, kflag, uflag; + + argv0 = argv[0] ? argv[0] : "selc"; + iflag = kflag = uflag = 0; + attachio(0); + for (i = 1; i < argc; i++) { + a = argv[i]; + if (a[0] != '-' || a[1] == '\0') + usage(); + if (a[1] == '-' && a[2] == '\0') { + i++; + break; + } + for (p = a + 1; *p; p++) + switch (*p) { + case 'i': + iflag = 1; + break; + case 'k': + kflag = 1; + break; + case 'u': + uflag = 1; + break; + case 'v': + verbose = 1; + break; + case 'h': + usage(); + break; /* NOTREACHED */ + default: + usage(); + } + } + if (i != argc) + usage(); + if (verbose) + attachio(1); + if (kflag) { + signalquit(); + return 0; + } + if (uflag) { + runkey(0); + signalquit(); + return 0; + } + if (iflag) + runkey(1); + + inst = GetModuleHandleA(NULL); + quit = CreateEventA(NULL, TRUE, FALSE, "Local\\selc-quit"); + if (!quit) + die("CreateEvent:"); + mtx = CreateMutexA(NULL, TRUE, "Local\\selc-inst"); + if (!mtx) + die("CreateMutex:"); + if (GetLastError() == ERROR_ALREADY_EXISTS) + return 0; + return loop(inst); +} |
