简介:基于C语言实现的Linux屏幕取词翻译工具源码包,面向中高级Linux开发者和对C语言感兴趣的读者,用于解决终端或文本环境下单词、短语的快速截取与翻译需求。压缩包共141个文件,大小16.7MB,核心代码由40个C源文件和25个头文件组成,覆盖主程序、函数库、数据结构及用户交互逻辑;配套提供makefile构建脚本、sh辅助脚本、ui界面模块、config配置文件,以及png演示截图和gif操作示例,目录划分清晰,便于定位和二次开发。项目同时集成微软必应和谷歌两套翻译接口,支持在配置文件中切换翻译引擎,并围绕屏幕取词、鼠标监听、窗口位置设置、快捷键控制、设置窗口等模块展示完整的实现细节。随附的readme说明、bugRecord排错记录和COPYING许可证文件,有助于梳理从源码编译、运行配置到排错的全过程。当前已有124人学习下载,适合希望提升C语言实践能力、了解Linux下界面交互设计及第三方翻译服务调用技巧的开发者对照研读。
1. 这可能是 Linux 上最轻量的屏幕取词翻译方案
如果你经常在 Linux 下读英文文档、看外文网页,一定会遇到一个很现实的问题:鼠标悬停在某个单词上,想立刻知道它是什么意思。Windows 生态里这种小工具很多,但 Linux 下要么依赖某大型词典软件,要么只能选中文字后手动粘贴到网页翻译。而这个项目给了另一条路:用纯 C 配合 X11/XTest 扩展,在鼠标所在位置模拟双击选中单词,再读剪贴板里的文本,交给翻译接口,最后弹窗展示释义。
整个过程不依赖浏览器插件,也不需要给桌面环境装额外驱动。它是 Linux 里常见的那种“小而硬核”的工具:一个事件循环、几组 Xlib 调用、一个翻译接口,就能在 KDE、GNOME、i3 等任何 X11 环境里跑起来。无论你是想给自己做个取词插件,还是想研究 X11 窗口系统下的自动化交互,这份源码都值得拆开看一遍。项目主体是一个 C 写的命令行守护进程,读懂它的核心链路之后,你完全可以改成划词翻译、OCR 识别,甚至接进自己的笔记软件。
2. 整体架构:一个 C 程序如何实现“鼠标悬停翻译”这整套动作
2.1 先想清楚:取词到底取的是什么
很多人拿到类似需求,第一反应是“从窗口里拿文本”。但实际操作过就知道,X11 协议里并没有提供一个“给我这个坐标下的单词”的通用方法。不同工具包(GTK、Qt、Electron)渲染文本的方式完全不同,有的把文本画在离屏缓冲区,有的直接用字体绘制,窗口树里根本拿不到字符串。
所以主流实现有三条路线。第一条是模拟鼠标选中:通过 XTest 扩展向当前鼠标位置发送双击事件,让目标程序自己选中单词并写入剪贴板,我们的程序再读取剪贴板。这种方式最接近用户手动操作,不需要知道窗口内部实现。第二条是对鼠标附近屏幕区域截图,然后用 OCR 识别文字。第三条是走底层无障碍接口(Linux 的 AT-SPI),但需要应用主动暴露控件树,很多程序不配合。
这个源码采用的是第一条路线,也是我在类似场景里最常用的一条。它的优点是兼容性好,任何一个能正常用鼠标选词的文本控件都能取词;缺点是不能用于图片、游戏画面这种不可选中区域。中文翻译场景里这基本够用,因为绝大多数阅读场景都在 PDF 阅读器、浏览器、终端里,而这些程序对剪贴板的选择都是支持的。仿照这条链路做出来的工具,核心结构非常清晰:取鼠标坐标 → 模拟双击 → 复制选中内容 → 读剪贴板 → 翻译 → 弹窗。
2.2 源码目录结构:每个文件只做一件事
打开这份源码,第一件事是看目录划分。它遵循了老派的 C 工程布局,源码放src下,按功能拆成多个文件,没有把所有逻辑塞进一个main.c里。目录结构大致是这样的:
| 文件 | 职责 |
|---|---|
src/main.c | 入口,初始化 X 连接,注册全局热键,进入事件循环 |
src/mouse.c | 获取当前鼠标坐标和目标窗口句柄 |
src/select.c | 用 XTest 模拟双击与复制按键 |
src/clipboard.c | 通过 X Selection 机制读取剪贴板文本 |
src/translator.c | 调用翻译接口并解析返回结果 |
src/popup.c | 在鼠标附近创建置顶弹窗显示翻译结果 |
src/notify.c | 简单的日志与错误处理 |
Makefile | 编译配置,链接 X11、XTest、libcurl 等库 |
这种拆分最大的好处是调试方便。屏幕取词最麻烦的问题就是不知道卡在哪一环:是没选中文字?还是没复制上?还是剪贴板没读到?拆成独立模块之后,我经常在select.c里加打印,看模拟按键有没有执行,再去clipboard.c里调查事件是否到达。对于这种系统级编程,分文件模块要远比单个文件重要,因为 X11 的错误信息往往不会直接告诉你上下文。
2.3 主程序:一个事件驱动的热键循环
整个程序的骨架是一个标准 X11 事件循环。main.c里创建与 X Server 的连接后,注册了一个全局热键(默认是 Ctrl+Shift+F4),然后进入循环等待事件。这样做的好处是不需要常驻高轮询,热键按下时才会触发取词,CPU 占用几乎为零。核心逻辑是:
// src/main.c #include <X11/Xlib.h> #include <X11/keysym.h> int main(void) { Display *display = XOpenDisplay(NULL); if (!display) return 1; Window root = DefaultRootWindow(display); KeyCode hotkey = XKeysymToKeycode(display, XK_F4); // 在根窗口上注册全局热键,Ctrl+Shift+F4 触发取词 XGrabKey(display, hotkey, ControlMask | ShiftMask, root, False, GrabModeAsync, GrabModeAsync); while (1) { XEvent event; XNextEvent(display, &event); if (event.type == KeyPress && event.xkey.keycode == hotkey) { handle_grab_and_translate(display); } } XCloseDisplay(display); return 0; }参数说明你看几个关键点:XOpenDisplay(NULL)会连接当前用户会话里的 X Server,如果是在 Wayland 下面跑,需要用 XWayland 兼容层;XKeysymToKeycode把 F4 这个键名转换成当前键盘映射下的 scancode;XGrabKey的第五个参数False表示不事件继续传给下层程序,这里可以理解为“我监听这个键但不抢走常规功能”。比较值得关注的是修饰键掩码,很多新手的坑在于只监听一个键,导致 Ctrl+Shift+F4 和 F4 都触发,后面避坑部分我会细说。
事件循环里的XNextEvent是阻塞调用,没有事件时程序挂起。这也意味着如果剪贴板读取阶段也依赖这个循环,就得处理事件嵌套的问题,源码里用了单独的窗口来接收 SelectionNotify,避免和热键事件抢同一个循环。设计上能让热键响应保持轻快,而读取剪贴板时进入短暂的子循环,这在 X11 编程里是很常见的做法。
3. 核心模块拆解:一个取词请求从鼠标到返回的完整链路
3.1 第一步:定位鼠标坐标与目标窗口
取词的第一步是拿到鼠标当前的坐标。普通程序里要光标位置并不难,很多桌面工具库都封装了现成接口。但为了让这段代码不依赖任何 GUI 工具包,源码直接调了 Xlib 的XQueryPointer,同时还能拿到鼠标落在哪个子窗口上。这部分逻辑在mouse.c里,关键实现如下:
// src/mouse.c #include <X11/Xlib.h> Window get_window_at_pointer(Display *display, int *root_x, int *root_y) { Window root = DefaultRootWindow(display); Window root_ret = 0, child = 0; int win_x = 0, win_y = 0; unsigned int mask = 0; // 返回鼠标所在位置以及对应的子窗口 if (!XQueryPointer(display, root, &root_ret, &child, root_x, root_y, &win_x, &win_y, &mask)) { return 0; } // 有子窗口就用子窗口,否则用根窗口 if (child != 0) { return child; } return root_ret; }代码里需要注意XQueryPointer的返回值是布尔值,表示鼠标是否在当前屏幕上。如果鼠标移出了屏幕窗口区,比如进入了 NoInput 区域,它会返回 False。我们这时应该中止取词流程,避免模拟点击打在空白位置。另外,拿到的child只是鼠标悬停的最底层子窗口,未必是文本框所属的窗口,但用于模拟点击已经够了。如果你后续要做针对特定应用窗口的优化,可以用XGetWindowAttributes沿着 parent 一路向上找,这个我在第六部分会讲。
3.2 第二步:模拟双击选中单词
拿到坐标之后,真正让目标程序选中单词的是select.c。这段代码用 XTest 扩展注入鼠标事件,相当于假装用户在鼠标位置连点了两次。双击在大多数文本控件里都会选中一个单词。这里有一个容易被忽略的细节:两次单击之间必须留出足够的间隔,但也不能太长,否则两个单击会变成两个独立操作。
// src/select.c #include <X11/extensions/XTest.h> #include <unistd.h> void double_click_at_current_position(Display *display) { // 第一次按下与释放 XTestFakeButtonEvent(display, 1, True, CurrentTime); XTestFakeButtonEvent(display, 1, False, CurrentTime); XFlush(display); usleep(180 * 1000); // 等待首次点击生效 // 第二次按下与释放,完成双击 XTestFakeButtonEvent(display, 1, True, CurrentTime); XTestFakeButtonEvent(display, 1, False, CurrentTime); XFlush(display); usleep(200 * 1000); } void press_ctrl_c(Display *display) { KeyCode ctrl = XKeysymToKeycode(display, XK_Control_L); KeyCode c = XKeysymToKeycode(display, XK_c); XTestFakeKeyEvent(display, ctrl, True, CurrentTime); XTestFakeKeyEvent(display, c, True, CurrentTime); XTestFakeKeyEvent(display, c, False, CurrentTime); XTestFakeKeyEvent(display, ctrl, False, CurrentTime); XFlush(display); usleep(150 * 1000); }这段代码看似简单,但有几个参数值得细抠。XTestFakeButtonEvent第二个参数是按键编号,1 表示左键,2 中键,3 右键;第三个参数True/False对应按下还是释放。usleep的间隔我习惯取 120ms 到 250ms 之间,太短程序会识别成一次长按拖拽,太长就变成两次独立单击,都不会选中单词。XFlush在这里很重要,它保证 X Server 确实处理完了注入事件,而不是留在缓冲区。这个时间窗口在不同应用上有差异,终端里稍短一点反而灵敏,浏览器里则要略长。如果你改用 200ms 固定值,多数应用都能接受。
模拟按键之后,被选中的文本就会写入目标程序的 PRIMARY 或 CLIPBOARD。这里的press_ctrl_c执行的是一个标准复制动作。需要注意并不是所有文本控件都响应 Ctrl+C 复制,个别的只响应 Ctrl+Insert,这可以做成可配置项。
3.3 第三步:读取剪贴板里的选中文本
剪贴板读取是这套链路里最复杂的一环。X11 的剪贴板不像 Windows 那样提供一个“取剪贴板文本”的同步 API,它用的是选择(Selection)机制:程序向当前拥有 CLIPBOARD 的窗口发送请求,拥有者再通过 SelectionNotify 事件把数据送回来。所以这段代码必须创建一个临时窗口,发起XConvertSelection,再阻塞等待数据到达。
// src/clipboard.c #include <X11/Xlib.h> #include <X11/Xatom.h> static int selection_done = 0; static char *selection_text = NULL; static void handle_selection_notify(Display *display, XSelectionEvent *event) { if (event->property == None) { selection_done = -1; return; } Atom type; int format; unsigned long length, remaining; unsigned char *data = NULL; XGetWindowProperty(display, event->requestor, event->property, 0, 1024 * 1024, True, AnyPropertyType, &type, &format, &length, &remaining, &data); if (data && type != None) { selection_text = strdup((char *)data); XFree(data); } selection_done = 1; } char *read_clipboard_text(Display *display) { Window requestor = XCreateSimpleWindow( display, DefaultRootWindow(display), 0, 0, 1, 1, 0, 0, 0); Atom clipboard = XInternAtom(display, "CLIPBOARD", False); Atom utf8 = XInternAtom(display, "UTF8_STRING", False); Atom property = XInternAtom(display, "TEXT_FEED", False); selection_text = NULL; selection_done = 0; XConvertSelection(display, clipboard, utf8, property, requestor, CurrentTime); int timeout = 50; while (selection_done == 0 && timeout-- > 0) { XEvent event; XNextEvent(display, &event); if (event.type == SelectionNotify) { handle_selection_notify(display, &event.xselection); } } XDestroyWindow(display, requestor); return selection_text; }这段代码里最值得琢磨的是XConvertSelection的第四个参数property,它相当于一个约定好的“收件箱”,数据到达后会写在这个属性上。我用的是自定义名字TEXT_FEED,只要不和系统属性冲突就行。XGetWindowProperty的1024 * 1024是单次最多读取 1MB,翻译取词根本用不了这么多,但防止某些程序往里面塞大段内容导致读取失败。主循环里的XNextEvent是为了等 SelectionNotify,但这里有个潜在问题:如果外部程序迟迟不响应,循环会卡住 50 次事件。更稳的方式是根据时间设置阻塞上限,而不是事件次数,我一般会在真实项目里用XPending加usleep来轮询,把这个循环改成带超时的等待。
3.4 第四步:触发翻译并展示结果
拿到字符串之后,取词链路的后半程就简单了。translator.c负责把文本发送给翻译接口,popup.c负责把结果弹到屏幕上。主流程函数handle_grab_and_translate就像一条流水线,把前面几个模块串起来:
// src/translate.c void handle_grab_and_translate(Display *display) { int root_x = 0, root_y = 0; Window target = get_window_at_pointer(display, &root_x, &root_y); if (!target) return; double_click_at_current_position(display); press_ctrl_c(display); char *word = read_clipboard_text(display); if (!word || strlen(word) == 0) { log_error("clipboard is empty"); return; } strip_whitespace(word); char *translation = translate_text(word); if (translation) { show_popup(display, root_x, root_y, word, translation); free(translation); } free(word); }这个流程函数没有任何图形界面代码,逻辑非常直白。先拿坐标,再模拟双击,再复制,再读剪贴板,最后翻译弹窗。如果中间某个环节失败,日志会指出是哪一步出了问题,这也是模块化的核心收益。strip_whitespace是我额外加的一个小函数,因为剪贴板里有时会带换行符或多余空格,若不清理,后续造请求 URL 时有概率出现非法字符。
4. 翻译引擎接入:词典服务、弹窗显示与编译配置
4.1 翻译请求的封装:拆掉网络差异这道坎
取词工具的核心价值在“翻得准”,而不是“能在哪跑”。这部分源码并没有内置词库,而是选择了在线翻译接口。我看到的实现是用 libcurl 发送 HTTP GET 请求,把取到的单词作为 query 参数拼在请求里,然后解析 JSON 返回结果。这种方式最常见也最容易替换服务商,因为只需要改动 URL 和签名逻辑。
// src/translator.c #include <curl/curl.h> #include <string.h> #include <stdlib.h> static char *extract_translation(const char *json) { // 简化的 JSON 解析,只取 "translation" 字段第一个值 const char *key = strstr(json, "\"translation\""); if (!key) return NULL; const char *value = strchr(key + strlen("\"translation\""), '\"'); if (!value) return NULL; value++; const char *end = strchr(value, '\"'); if (!end) return NULL; size_t len = end - value; char *result = malloc(len + 1); memcpy(result, value, len); result[len] = '\0'; return result; } char *translate_text(const char *word) { CURL *curl = curl_easy_init(); if (!curl) return NULL; char *escaped = curl_easy_escape(curl, word, 0); char url[512]; snprintf(url, sizeof(url), "https://api.example.com/translate?text=%s&from=en&to=zh", escaped); curl_free(escaped); char *response = NULL; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); // 设置超时,避免网络慢时卡住取词 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L); CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { curl_easy_cleanup(curl); return NULL; } curl_easy_cleanup(curl); return extract_translation(response); }这里你要注意curl_easy_escape是必须的一步,因为单词里可能有空格、单引号、括号之类的特殊字符,直接拼 URL 会导致请求格式错误。CURLOPT_WRITEFUNCTION是一个回调,把服务器返回的 JSON 往内存缓冲里写,这里没有用默认的fwritestdout,因为我们需要拿到字符串做解析。整个函数接口设计成返回char *,调用方用完释放,这样以后换本地词典或别家服务时,不需要改动上层逻辑。
4.2 结果弹窗:一个不抢焦点的置顶提示框
弹窗实现选择了 Xlib 直接创建窗口,而不是引入 GTK/Qt。这样做的好处是依赖更少,程序体积小,而且能完全控制窗口层级。关键设置是 override-redirect 窗口属性,让窗口不经过窗口管理器,直接显示在最顶层并且不抢键盘焦点。这样取词之后,用户不需要手动关闭弹窗,鼠标移走后按一下 Ctrl+Shift+F4 弹窗就会被新结果替换。
// src/popup.c void show_popup(Display *display, int root_x, int root_y, const char *word, const char *translation) { XSetWindowAttributes attrs; Window win = XCreateSimpleWindow( display, DefaultRootWindow(display), root_x, root_y, 240, 60, 0, 0, 0xffeeee); attrs.override_redirect = True; attrs.event_mask = ExposureMask; XChangeWindowAttributes(display, win, CWOverrideRedirect | CWEventMask, &attrs); XSelectInput(display, win, ExposureMask | ButtonPressMask); XMapRaised(display, win); XStoreName(display, win, "myword-translate"); // 绘制文本 const char *lines[2]; lines[0] = word; lines[1] = translation; // 这里省略了字体加载与换行布局,实际会分两行画出 draw_text(display, win, lines, 2); // 简单延时自动关闭 usleep(4 * 1000 * 1000); XDestroyWindow(display, win); }要解释一下override_redirect=True的含义:它告诉 X Server 不要把这个窗口交给窗口管理器管理,所以不会出现标题栏、边框,也不会被抢焦点。这个属性对临时提示窗口特别重要,否则每次翻译都会让当前输入窗口失焦,打字就变得断断续续。XMapRaised会让窗口出现在层叠顺序最上面,从而被用户看到。这个简单实现是在一个事件循环里直接 sleep 4 秒后销毁窗口,它不会单独起线程,所以取词过程中程序会处于非响应状态,但功能上是能用的。如果后续要做成一直悬停显示,可以把窗口管理放到独立线程或把 DestroyWindow 改成隐藏窗口。
4.3 Makefile 与依赖说明
整个项目编译用了一个非常朴素的 Makefile,依赖库主要是三块:X11、Xtst、libcurl。还有一个隐含的libm用于数学计算,不过在取词场景下用不到。Makefile 里的关键目标如下:
CC ?= cc CFLAGS ?= -O2 -Wall -Wextra LDFLAGS = -lX11 -lXtst -lcurl -lpthread SRCS = src/main.c src/mouse.c src/select.c \ src/clipboard.c src/translator.c src/popup.c src/notify.c OBJS = $(SRCS:.c=.o) myword: $(OBJS) $(CC) -o $@ $(OBJS) $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c -o $@ $< clean: rm -f $(OBJS) myword编译前需要确认系统装好了 X11 开发头和 libcurl 开发库。在 Ubuntu/Debian 上常见做法是sudo apt install libx11-dev libxtst-dev libcurl4-openssl-dev,Arch 系则是sudo pacman -S libx11 libxtst curl。这些依赖是屏幕取词这类 X11 程序的老朋友,属于基础环境,不算重型依赖。从 Makefile 里你能看出这个项目刻意避开了 GUI 框架,所以最终二进制体积很小,内存占用也比 Electron 类工具低得多。
5. 避坑记录:写屏幕取词常见的 5 个坑
5.1 模拟按键被目标程序的快捷键拦截
现象就是双击和复制的动作都执行了,但最终剪贴板里什么都没有。原因有两类:一类是目标程序把 Ctrl+C 绑定成了自定义快捷键,另一类是模拟按键速度太快,程序还没有从双击的选中状态里恢复,就收到了复制指令。排查时先手动在那个窗口试一次 Ctrl+C,如果手动能复制而程序不行,把press_ctrl_c和double_click_at_current_position之间的延时从 200ms 加大到 500ms 再试。注意 XTest 注入的按键会被当作真实键盘输入处理,无法绕过应用快捷键,所以必须在逻辑上避开冲突。
5.2 剪贴板读取在 Wayland 下永远超时
现象是程序在 GNOME 默认会话下按热键没反应,但切到 X11 会话就好了。原因很简单,Wayland 的剪贴板协议与 X11 的 Selection 机制不互通,XConvertSelection广播出去的请求不会被 Wayland 原生应用处理。解决办法是让程序运行时检测$XDG_SESSION_TYPE,如果是 wayland 则提示用户使用 XWayland 或者切换到 X11 会话。对这个项目来说,直接限定在 X11 下运行是合理的选择,因为它是为 X11 设计的,但你至少要在 README 里写明这个约束,否则用户会以为是源码坏了。
5.3 模拟双击之前没有判断鼠标是否真的落在窗口上
现象是热键一按,程序弹出一个空的结果窗口,有时候还伴随奇怪的鼠标位置跳跃。原因是XQueryPointer返回的坐标是相对根窗口的,但如果你把鼠标放在桌面空白区域,child是根窗口本身,双击根窗口不会选中任何文本,剪贴板自然为空。我处理这个坑的方法是:在handle_grab_and_translate里检查鼠标下方窗口是不是根窗口,如果是就直接返回,不再往下走。更极端的场景是鼠标落在了某个 override-redirect 窗口上,比如状态栏,那也不应该触发取词。
5.4 取词完成后弹窗抢焦点,导致正在输入的文本丢失
这是弹窗实现里最容易翻车的点。如果你没有设置override_redirect=True,新建的窗口会被窗口管理器当作普通窗口处理,然后自动获得焦点。用户正在浏览器里打字,一按取词热键,输入焦点就跳到了弹窗上,刚打一半的内容失去焦点。解决方法是严格使用XChangeWindowAttributes设置 override 属性,并且不要调用XSetInputFocus。另外,也不要在弹窗上开启KeyPressMask事件,否则按键焦点虽然没过去,但键盘事件依然可能被子窗口捕获。
5.5 热键注册时修饰键掩码设置错误导致吃快捷键
现象是程序开着的时候,系统里某个全局快捷键失效了。原因在于XGrabKey注册时用了AnyModifier或遗漏了修饰键。比如只注册了 F4,那么无论是否按 Ctrl 或 Shift,都会触发取词,原来单词里的 Ctrl+F4 快捷键反而会被程序拦截。正确做法是像前面代码里那样显式指定修饰键组合,例如ControlMask | ShiftMask,同时可以注册多个组合备用。如果还想放过某些修饰键,可以监听keycode + modifiers时加一个XAllowEvents判断来放行。
6. 二次开发:让取词器支持划词、生词本和可选 OCR
走到这一步,你已经能把这个项目跑起来了。接下来真正值得花时间的是把它改造成适合自己习惯的工具。我拿到这类项目后的第一个改造点,是给它增加划词模式:不双击选中单词,而是监听用户手动选中的文字。这个改造其实很小,把double_click_at_current_position换成直接发送 Ctrl+C,就变成了“选中词后按热键翻译”。在很多阅读场景里,用户想要翻译的是一个短语或者一整句话,双击反而选中不完整。
第二个很实用的改造是绕过在线接口,接入本地词典文件。把translator.c里的网络请求替换成一个哈希表查询,取词后先在本地词库找释义,找到就直接返回,找不到再走一次在线接口。这个方案能显著降低翻译延迟,离线时也能用。我一般会准备一个 CSV 格式的词库文件,启动时加载进哈希表,结构体里存单词和释义两个字段,排序后做二分查找,C 语言里做这件事的复杂度并不高。如果你想要一个更完整的用户体验,还可以顺手把弹窗改成可拖拽、可固定,用一个线程管理窗口生命周期。
第三个值得动手的方向是 OCR。我的建议是不去自己写图像识别,而是把截图命令和tesseract管道接起来。比如先用import -window root -crop WxH+X+Y /tmp/shot.png抓取鼠标周围区域,再调用tesseract /tmp/shot.png stdout -l eng拿到识别结果。这会引入外部程序依赖,但它能让整个取词器打破“只能选词”的限制,直接作用于图片、网页里的缩略字,实用性提升非常明显。如果你要在程序内实现,可以在select.c加一个ocr_grab分支,在创建进程时通过 fork/exec 执行系统命令,把 stdout 读回来。
我不建议一上来就改事件循环去支持多屏坐标映射,那个改动涉及物理屏幕坐标和逻辑坐标的比例问题,调试容易碰壁。先做以上三个点到为止的改造,再观察使用场景里哪里不顺。我的习惯是每次改完都手动跑一遍gdb,在handle_grab_and_translate的每个子步骤设置断点,确保障碍是真真实实出在剪贴板读取,而不是翻译接口返回了空结果。
这个项目最打动我的地方,是它把“屏幕取词”这件本来需要大型框架支撑的事,拆成了几个小 C 函数。读它就像跟一位朋友聊他踩过的坑:模拟按键的时机、剪贴板协议的流程、弹窗焦点问题,每一处都对应真实世界的摩擦。从那以后我每次拿到类似的小工具源码,都会先找它的核心链路函数,再顺藤摸瓜读外围模块,因为这种程序的价值就在于一条直截了当的主路径。如果你也想做出一个 Linux 桌面上真正好用的取词翻译器,希望这份解析能帮到你的第一行代码。
本文还有配套的精品资源,点击获取