Repository navigation
Architecture.zh CN
语言: English · 简体中文
带你过一遍源码是怎么组织的、数据怎么流动。看不懂源码就先看这个。
seekey 在一个后台线程里从 Linux 内核读原始键盘事件(/dev/input/event*,经 libevdev),把每次按键送到 GTK 主线程,画成 CSS 样式的气泡,显示在锚定于屏幕底部的点击穿透浮层窗口里。
seekey/
├── LICENSE MIT
├── README.md / README.zh-CN.md
├── Makefile
├── install.sh 用户级安装器 + 依赖检测
├── seekey.ini.example 带注释的示例配置
├── src/
│ ├── seekey.h 共享类型 + input/keyname 公开 API
│ ├── main.c GTK 应用、事件循环、气泡逻辑
│ ├── config.c / .h 配置加载/保存/解析、主题、matugen
│ ├── gui.c / .h fuzzel 风格图形配置菜单
│ ├── style.c / .h 浮层 CSS 生成
│ ├── tui.c / .h TUI 编辑器(ncurses)
│ ├── preview_session.c / .h GUI/TUI 共用的独立实时浮层
│ ├── window_state.c / .h 显示器记忆(XDG_STATE_HOME)
│ ├── input.c evdev 键盘/鼠标捕获(后台线程)
│ ├── keynames.c 按键名 + 图标查找表
│ └── layer_shell.c gtk4-layer-shell 集成(dlopen)
├── data/dev.seekey.desktop desktop 启动入口及快捷操作
├── tests/
│ ├── test_main.c 测试运行器入口
│ ├── test_config.c
│ ├── test_tui.c
│ ├── test_keynames.c
│ ├── test_window_state.c
│ ├── test_helpers.c / .h
│ └── vendor/unity/ 内置 Unity 测试框架(MIT)
├── po/ gettext 翻译
│ ├── POTFILES.in LINGUAS seekey.pot zh_CN.po
└── locale/ 编译出的 .mo 文件(编译产物)
中心头文件。定义 SeekeyConfig(每个设置就是一个普通结构体字段)、KeyEventMessage(从 input 递交给 UI 的一次按键)、修饰键位掩码、滚动伪键码,以及 input.c / keynames.c / layer_shell.c 的公开 API。所有文件都包含它。
-
seekey_config_set_defaults—— 用硬编码默认值填充SeekeyConfig。 - 主题预设表(
default、light、nord、dracula、catppuccin、monokai、matugen)+seekey_config_apply_theme。 -
seekey_config_resolve_path—— 选配置文件(--config→--xdg→<cwd>/seekey.ini→ 无)。 -
seekey_config_load—— 用GKeyFile读 INI,拒绝非法值,应用主题后再应用逐键颜色覆盖,读[icons],并兼容 0.2.0 的窗口尺寸位置。 -
seekey_config_save/seekey_config_init/seekey_config_print/seekey_config_validate。保存时保留稀疏主题继承,确保 Matugen 引用在调色板变化和另存为后仍保持动态,清除已删除的图标覆盖,并规范旧版窗口尺寸键。 -
seekey_parse_args—— CLI 参数覆盖配置字段。 - Matugen 辅助:
seekey_matugen_resolve_path、seekey_matugen_load(把colors.json解析成GHashTable)、seekey_matugen_resolve_value(把@matugen:role@0.86变成alpha(#hex, 0.86))。
输入侧。seekey_input_new 先取得用户级 flock(2) 运行时锁,保证同时只有一个 Seekey 进程采集输入;随后用 libevdev 打开所有 /dev/input/event* 设备,保留看起来像键盘的(有一组有用按键)设备,若 show-mouse=true 还保留鼠标类设备。它起一个后台线程跑跨所有 fd 的 poll(2) 循环。按键事件到来时构造 KeyEventMessage,跟踪修饰键状态和 shifted/非 shift 修饰键标志,通过 g_main_context_invoke(dispatch_key_event → 回调)推到 GTK 主线程。遇到 SYN_DROPPED 时会排空同步事件并重建按下状态,避免修饰键卡住。/dev/input 上的 inotify 监视配合低频兜底扫描,会移除已断开的设备并自动打开新建的事件节点。只有键盘集合确实变化时才重建按下状态,因此周期扫描不会破坏持续按住的修饰键。即使启动时没有可读键盘,轮询线程也会保持运行,让后续权限变化或热插拔自动恢复。这就是 seekey 能在任何合成器上工作的原因:它不碰 Wayland 键盘协议。
静态表把 evdev 键码(linux/input-event-codes.h 里的 KEY_*)映射到人类可读标签(Backspace、Enter、Up、Volume Up、…)。提供 seekey_key_name、seekey_key_text(某键打出的字符,区分 shifted)、seekey_key_icon(应用 [icons] 覆盖)、seekey_is_modifier、seekey_modifier_order(Ctrl/Shift/Alt/Super 在组合里的规范显示顺序)。
用运行时 dlopen 加载(非硬链接)的 gtk4-layer-shell,依次探测 libgtk4-layer-shell.so.0 和 libgtk4-layer-shell.so。seekey_layer_shell_try_init 为窗口初始化 layer-shell,设 layer(top)、锚定底边、应用边距、设显示器、键盘模式设为 NONE(这样 seekey 永不抢键盘)、设命名空间。库缺失或 layer-shell=off 时返回错误,main.c 降级成普通透明窗口。
为什么用
dlopen,Makefile 又把它链接在 GTK4 之前?gtk4-layer-shell 注册的静态构造函数必须在 libwayland-client 初始化前运行。链接时 Makefile 把它放在 GTK4 之前;不链接时,运行时dlopen路径对那些有库但没.pc文件的发行版仍可用。
把浮层上次在哪台显示器持久化到 $XDG_STATE_HOME/seekey/window.ini(回退 ~/.local/state/seekey/...)。seekey_window_state_load(缺失/坏文件不是错误——返回零值)、seekey_window_state_save(写连接器名,如 DP-1)、seekey_window_state_clear、seekey_find_monitor_by_name(按连接器名匹配 GdkDisplay 的显示器)。
--config-tui 编辑器(ncurses)。构造一个 TuiField 数组(35 个字段)描述每个设置——类型(TUI_UINT / TUI_STRING / TUI_CHOICE / TUI_BOOL / TUI_COLOR)、目标指针、min/max/step、默认值。纯辅助函数(tui_field_value、tui_adjust_field、tui_reset_field、tui_nearest_color_index、tui_current_choice_index)无需 ncurses 即可单测。ncurses 渲染代码用 #ifdef SEEKEY_TEST 包裹,测试构建会跳过。
两个配置编辑器都通过 preview_session.c 把内存中的有效配置写入私有临时 ini。
启动样例前会查询 dev.seekey 应用 ID 和浮层运行锁;真实输入浮层存在时保留它并跳过样例,
否则才启动非唯一的浮层子进程。配置变化会重启样例;子进程通过普通 GTK/CSS
路径渲染示例气泡,但不会打开 evdev。用户级预览锁避免重复预览窗口,
普通浮层也会持有预览锁,避免外部启动与编辑器样例叠加。崩溃的样例会被回收并
重启;有时限的停止流程、PR_SET_PDEATHSIG 和临时文件清理共同避免卡死或残留预览进程。像
--matugen <path> 这类只存在于运行时的选项也会继续传给子进程。
最大的文件。职责:
-
main():setlocale、绑 gettext、设默认、解析+加载配置、解析 CLI、处理--init-config/--print-config/--validate-config/--config-tui(然后退出),否则创建GtkApplication。 -
activate():已有窗口时只重新 present,保证普通启动始终只有一个浮层;否则建窗口 + 一个水平GtkBox放气泡、设空输入区域(点击穿透)、试 layer-shell(否则降级窗口)、加载保存的显示器状态、启动input.c。 -
气泡逻辑:
on_key_event(来自input.c的回调)决定是合并进上一个气泡(merge-repeats、merge-modifiers)、开/扩展打字分组、按typing-display遮盖/忽略文字,还是新建气泡。每个气泡安排一个duration-ms超时;若disappear=fade,两阶段移除先加fadingCSS 类,fade-ms后再移除 widget。trim_bubbles强制max-items。 -
shutdown():把当前显示器连接器名持久化到window_state,释放输入线程。
/dev/input/event* ──libevdev──▶ input.c poll 线程
│ 构造 KeyEventMessage
│ g_main_context_invoke(dispatch_key_event)
▼
main.c on_key_event (GTK 主线程)
│ 合并?打字分组?新气泡?
▼
GtkBox ──CSS──▶ 屏幕气泡
│ duration-ms(+ fade-ms)后
▼
移除 / 淡出
-
与合成器无关的输入。 直接读 evdev 意味着 seekey 在任何 Wayland 合成器上都能工作,无需逐合成器协议代码。代价:需要读
/dev/input/event*(见 Troubleshooting.zh-CN)。 -
运行时
dlopenlayer-shell。 让一个二进制同时在 layer-shell 和非 layer-shell 桌面上工作;库是可选的。 -
线程 → 主线程递交。 evdev 轮询会阻塞,所以放线程里;所有 GTK 调用通过
g_main_context_invoke在主线程进行。 -
每个会话一个浮层。
dev.seekey应用 ID 用于正常 D-Bus 会话;浮层运行锁在会话总线缺失时继续保证互斥,输入锁则单独保证只有一个进程读取 evdev。普通浮层和编辑器样例还共享预览锁,因此不会同时出现。 -
通过空输入区域实现点击穿透(在
GdkSurface上),加KEYBOARD_MODE_NONE保证永不抢键盘焦点。 -
配置是普通结构体。 没有 getter/setter;
SeekeyConfig字段直接读。简单,TUI 的TuiField目标指针也直接指向它们。
纯逻辑如何在无显示器下测试见 Testing.zh-CN。
Getting started
Behaviour & compatibility
Looks
For contributors
Reference