Repository navigation
Architecture
Language: English · 简体中文
A tour of how the source is laid out and how data flows through it. Read this first if the code confuses you.
Seekey reads raw keyboard events from the Linux kernel (/dev/input/event*
via libevdev) in a background thread, marshals each keypress to the GTK
main thread, and draws the last few keys as CSS-styled bubbles in a
click-through overlay window anchored to the bottom of the screen.
seekey/
├── LICENSE MIT
├── README.md / README.zh-CN.md
├── Makefile
├── install.sh user-level installer + deps detection
├── seekey.ini.example sample configuration with comments
├── src/
│ ├── seekey.h shared types + public input/keyname API
│ ├── main.c GTK app, event loop, bubble logic
│ ├── config.c / .h config load/save/parse, themes, matugen
│ ├── gui.c / .h fuzzel-style graphical config menu
│ ├── style.c / .h overlay CSS generation
│ ├── tui.c / .h TUI editor (ncurses)
│ ├── preview_session.c / .h isolated live overlay for GUI/TUI editors
│ ├── window_state.c / .h monitor persistence (XDG_STATE_HOME)
│ ├── input.c evdev key/mouse capture (background thread)
│ ├── keynames.c key name + icon lookup tables
│ └── layer_shell.c gtk4-layer-shell integration (dlopen'd)
├── data/dev.seekey.desktop desktop launcher + actions
├── tests/
│ ├── test_main.c test runner entry
│ ├── test_config.c
│ ├── test_tui.c
│ ├── test_keynames.c
│ ├── test_window_state.c
│ ├── test_helpers.c / .h
│ └── vendor/unity/ vendored Unity test framework (MIT)
├── po/ gettext translations
│ ├── POTFILES.in LINGUAS seekey.pot zh_CN.po
└── locale/ compiled .mo files (build output)
The central header. Defines SeekeyConfig (every setting as a plain struct
field), KeyEventMessage (one keypress handed from input to UI), modifier
bitmask flags, scroll pseudo key-codes, and the public API for
input.c / keynames.c / layer_shell.c. Included by everything.
-
seekey_config_set_defaults— fillsSeekeyConfigwith hardcoded defaults. - Theme presets table (
default,light,nord,dracula,catppuccin,monokai,matugen) +seekey_config_apply_theme. -
seekey_config_resolve_path— picks the config file (--config→--xdg→<cwd>/seekey.ini→ none). -
seekey_config_load— reads the INI withGKeyFile, rejects invalid values, applies theme then per-key color overrides, reads[icons], and accepts the legacy 0.2.0 window-size locations. -
seekey_config_save/seekey_config_init/seekey_config_print/seekey_config_validate. Saving preserves sparse theme inheritance, removes deleted icon overrides, keeps Matugen references dynamic across palette changes and Save As, and normalizes legacy window-size keys. -
seekey_parse_args— CLI flags override config fields. - Matugen helpers:
seekey_matugen_resolve_path,seekey_matugen_load(parsescolors.jsoninto aGHashTable),seekey_matugen_resolve_value(turns@matugen:role@0.86intoalpha(#hex, 0.86)).
The input side. seekey_input_new opens all /dev/input/event* devices
with libevdev, keeping the ones that look like keyboards (have a useful set
of keys) and, if show-mouse=true, mouse-like devices. It spawns a
background thread running a poll(2) loop across all fds. When a key
event arrives it builds a KeyEventMessage, tracks modifier state and
shifted/non-shift-modifier flags, and pushes it to the GTK main thread via
g_main_context_invoke (dispatch_key_event → the callback). It drains
SYN_DROPPED synchronization events and rebuilds pressed-key state instead of
leaving modifiers stuck. An inotify watch on /dev/input, backed by a
low-frequency scan, removes disconnected devices and opens newly created event
nodes without restarting the overlay. Pressed-key state is rebuilt only when
the keyboard set actually changes, so periodic scans do not break a held
modifier. The polling thread remains alive even when no keyboard was readable
at launch, allowing later permission changes or hotplug to recover. This is why
seekey works on any compositor: it never touches the Wayland keyboard protocol.
Before opening devices it takes a user-scoped flock(2) runtime lock, so only
one Seekey process can capture input at a time.
Static tables mapping evdev key codes (KEY_* from
linux/input-event-codes.h) to human labels (Backspace, Enter, Up,
Volume Up, …). Provides seekey_key_name, seekey_key_text (the typed
character for a key, shifted or not), seekey_key_icon (honoring [icons]
overrides), seekey_is_modifier, and seekey_modifier_order (the canonical
display order of Ctrl/Shift/Alt/Super in a combo).
Uses gtk4-layer-shell loaded at runtime with dlopen (not linked
hard), probing libgtk4-layer-shell.so.0 then libgtk4-layer-shell.so.
seekey_layer_shell_try_init initializes the window for layer-shell, sets
the layer (top), anchors it to the bottom edge, applies margins, sets the
monitor, sets keyboard mode to NONE (so seekey never grabs the keyboard),
and a namespace. If the library is missing or layer-shell=off, it returns
an error and main.c falls back to a normal transparent window.
Why
dlopenand why the Makefile links it before GTK4? gtk4-layer-shell registers a static constructor that must run before libwayland-client is initialized. When linked, the Makefile puts it before GTK4; when not linked, the runtimedlopenpath still works for distros that ship the library but not a.pcfile.
Persists which monitor the overlay was on, in
$XDG_STATE_HOME/seekey/window.ini (fallback ~/.local/state/seekey/...).
seekey_window_state_load (missing/malformed files are not errors — returns
zeroed state), seekey_window_state_save (writes the connector name, e.g.
DP-1), seekey_window_state_clear, and seekey_find_monitor_by_name
(matches a connector against GdkDisplay's monitors).
The --config-tui editor (ncurses). Builds a TuiField array (35 fields)
describing each setting — type (TUI_UINT / TUI_STRING / TUI_CHOICE /
TUI_BOOL / TUI_COLOR), target pointer, min/max/step, default. The
pure helper functions (tui_field_value, tui_adjust_field,
tui_reset_field, tui_nearest_color_index, tui_current_choice_index)
are unit-tested without ncurses. The ncurses rendering code is gated by
#ifdef SEEKEY_TEST so the test build skips it.
The biggest file. Responsibilities:
-
main(): setlocale, bind gettext, set defaults, resolve+load config, parse CLI args, handle--init-config/--print-config/--validate-config/--config-tui(then exit), else createGtkApplication. -
activate(): build the window + a horizontalGtkBoxfor bubbles, set the empty input region (click-through), try layer-shell (else fallback window), load saved monitor state, startinput.c. -
Bubble logic:
on_key_event(the callback frominput.c) decides whether to merge into the last bubble (merge-repeats,merge-modifiers), start/extend a typing group, mask/suppress typed text according totyping-display, or create a new bubble. Each bubble schedules aduration-mstimeout; ifdisappear=fade, a two-phase removal adds thefadingCSS class then removes the widget afterfade-ms.trim_bubblesenforcesmax-items. -
shutdown(): persist the current monitor connector towindow_state, free the input thread.
/dev/input/event* ──libevdev──▶ input.c poll thread
│ builds KeyEventMessage
│ g_main_context_invoke(dispatch_key_event)
▼
main.c on_key_event (GTK main thread)
│ merge? typing group? new bubble?
▼
GtkBox ──CSS──▶ screen bubbles
│ after duration-ms (+ fade-ms)
▼
remove / fade out
-
Compositor-independent input. Reading evdev directly means seekey works
on every Wayland compositor with no per-compositor protocol code. The
trade-off: it needs read access to
/dev/input/event*(see Troubleshooting). -
One overlay per user session. The normal
GtkApplicationowns thedev.seekeyapplication ID. An overlay runtime lock remains authoritative when the session bus is unavailable, while the input lock separately ensures only one process reads evdev. A normal overlay also holds the preview lock, so an editor preview and real overlay cannot coexist. -
Runtime
dlopenfor layer-shell. Lets one binary work on both layer-shell and non-layer-shell desktops; the library is optional. -
Thread → main-thread marshaling. evdev polling blocks, so it lives in
a thread; all GTK calls happen on the main thread via
g_main_context_invoke. -
Click-through via empty input region on the
GdkSurface, plusKEYBOARD_MODE_NONEso keyboard focus is never stolen. -
Config as a plain struct. No accessors/getters;
SeekeyConfigfields are read directly. Simple, and the TUI'sTuiFieldtargets point right at them. -
Isolated live editor preview.
preview_session.cserializes the effective in-memory configuration to a private temporary ini and runs a non-unique Seekey overlay child. The child renders representative bubbles through the normal GTK/CSS path but never opens evdev. Before starting it, the editor queries both thedev.seekeyapplication ID and overlay lock; an existing real overlay remains the only visible rendering surface. A user-scoped preview lock prevents duplicate preview windows and blocks an external normal launch until the editor preview closes. Crashed preview children are reaped and restarted; bounded shutdown,PR_SET_PDEATHSIG, and temporary-file cleanup prevent hangs and orphans. Runtime-only options such as--matugen <path>are forwarded to the child.
See Testing for how the pure logic is tested without a display.
Getting started
Behaviour & compatibility
Looks
For contributors
Reference