Windows 平台的键盘 / 鼠标按键重映射工具。TOML 声明式配置,托盘常驻,改完配置自动生效。
单文件运行,无运行时依赖,release 体积约 1 MB。
| 键盘重映射 | 鼠标按键映射 | 按程序区分 | 声明式配置 | 图形配置界面 | |
|---|---|---|---|---|---|
| AutoHotkey | ✅ | ✅ | ✅ | 需写脚本 | ❌ |
| PowerToys Keyboard Manager | ✅ | ❌ | 仅快捷键重映射 | ✅ | ✅ |
| keyremap | ✅ | ✅ | ✅ | ✅ | ✅ 网页版 |
需要把鼠标侧键映射成快捷键、又不想写脚本,这正是本项目存在的理由。
其余场景各有更合适的选择:要条件判断、宏、按窗口标题区分这类没有上限的能力,用 AutoHotkey;只做键盘上的键→键映射,PowerToys Keyboard Manager 是微软官方维护、开箱即用的方案。
本项目的取舍是小而专:只做声明式的键位映射,不做脚本引擎,实现约 2800 行代码(不含测试与注释)。键盘钩子在技术上等同于键盘记录器,能自己把代码读完再决定用不用,在某些环境里是有意义的。
scoop
scoop bucket add huanfeng https://github.com/huanfeng/scoop-bucket
scoop install keyremap
装完直接运行 keyremap。配置自动生成在 %APPDATA%\keyremap\keyremap.toml,升级不会动它。
绿色版
下载 release 压缩包解压到任意目录(keyremap.exe 与 keyremap.toml 放一起),双击运行。没有控制台窗口,托盘出现图标即已生效。
同一个 exe 只能跑一个实例,但把整个目录复制一份到别处,就能带着各自的配置同时运行多份。
不确定某个键叫什么名字,用托盘菜单 → 按键监听(命令行 keyremap --listen)按一下看输出。
按顺序查找,用第一个命中的:
| 顺序 | 位置 | 用于 |
|---|---|---|
| 1 | -c <路径> 指定的文件 |
手动指定 |
| 2 | exe 同目录的 keyremap.toml |
绿色版,整个目录拷走就带走配置 |
| 3 | %APPDATA%\keyremap\keyremap.toml |
安装版,首次运行自动生成 |
不确定当前用的是哪个,跑 keyremap --dump 看第一行。
scoop 安装时第 2 条会被跳过 —— 安装目录每次升级都会被换掉,配置放那里会丢,包里自带的那份只作示例。
保存即生效:程序监听配置文件变化,保存后自动重载并弹气泡。写错也不要紧,解析失败时保留上一份配置继续运行。
[[mappings]]
name = "Pause转Insert" # 可选,显示在托盘菜单和日志里
comment = "暂停键改成 Insert" # 可选,纯注释
from = "Pause"
to = "Insert"from 和 to 都可以写成单个键或数组:
| 写法 | 含义 |
|---|---|
from = "Pause" |
单键触发 |
from = ["Ctrl", "E"] |
按住 Ctrl 再按 E(末位是触发键,见下节) |
to = "Insert" |
输出单键,保留长按连发 |
to = ["Ctrl", "W"] |
输出组合键,在触发键抬起时发一次 |
to = [] |
屏蔽:吞掉输入,什么都不发(防误触) |
组合键输出之所以在抬起时才触发,是为了避开长按连发 —— Ctrl+W 连发意味着一口气关掉一串标签页。最多 8 个键,用一次原子的 SendInput 发出。
反向映射可以同时启用,不会死循环:
[[mappings]]
from = "D"
to = "E"
[[mappings]]
from = "E"
to = "D"from 写成数组时,最后一个是触发键,前面的都是要按住的修饰键:
[[mappings]]
from = ["Ctrl", "E"]
to = "Backspace"
[[mappings]]
from = ["Ctrl", "MouseX2"] # 触发键也可以是鼠标按键
to = ["Ctrl", "Shift", "T"]修饰键写 Ctrl / Shift / Alt / Win 表示左右任一按下都算;要限定某一侧就写 LCtrl / RAlt 之类。
几条需要知道的规则:
- 触发时会先松开修饰键,发完再按回去。 不这么做的话,你按下的
Ctrl还在,应用收到的就是Ctrl+Backspace(多数编辑器会删掉一整个词)。代价是应用会看到一次修饰键的松开与重新按下,用户态方案绕不开。只有from里点名的修饰键会被动,你同时按着的其他键不受影响。 - 长按连发跟着
to走:to是单键就连发(长按Ctrl+E连续删字),是组合键就只触发一次。 - 更具体的匹配优先:同时配了
E → D和Ctrl+E → Backspace,按住 Ctrl 走后者。 - 触发键本身不能是修饰键。
["Ctrl", "Shift"]含义不明,直接报错而不去猜。
CapsLock 可以写在修饰键位置。按住期间不会切换大小写:
[[mappings]]
from = ["CapsLock", "H"]
to = "Left"单独轻点 CapsLock 的行为,取决于你有没有单独为它配映射:
| 配置 | 轻点 CapsLock |
|---|---|
只配了 CapsLock+H |
照常切换大小写,原有功能一点没丢 |
加 from = "CapsLock", to = [] |
什么都不做 —— 这才是真正的"禁用 CapsLock" |
加 from = "CapsLock", to = "Esc" |
发出 Esc,即轻点是 Esc、按住是修饰键那个经典布局 |
没有任何映射用到 CapsLock 时,它完全不受影响。
单条映射可以关掉,也可以在托盘菜单里逐条勾选(不写回文件,重载后回到文件里的状态):
[[mappings]]
enable = false # 默认 true
from = "D"
to = "E"加 group 就能分组,组不需要预先声明,托盘菜单里可以按组整体开关:
[[mappings]]
group = "浏览器"
from = "MouseX2"
to = ["Ctrl", "W"]
[[mappings]] # 不写 group 的归入"未分组"
from = "Pause"
to = "Insert"想让某组默认关着,加一段 [groups](整段可选,没列出的组默认启用):
[groups]
"文本编辑" = false一条映射真正生效需要自身启用且所属组也启用,--dump 会用 grp 标出"自身开着但组被关掉"的情况。
加 window 就只在指定的程序里生效。填进程的可执行文件名,不区分大小写:
[[mappings]]
name = "浏览器侧键后退"
from = "MouseX2"
to = ["Alt", "Left"]
window = ["chrome.exe", "msedge.exe"]| 写法 | 含义 |
|---|---|
window = "chrome.exe" |
只在 Chrome 里 |
window = ["chrome.exe", "msedge.exe"] |
这几个程序里都行 |
window = "!code.exe" |
除了 VSCode,其他都行 |
| 不写 | 所有程序 |
同一个键可以在不同程序里映射成不同的东西,互不干扰:
[[mappings]] # 浏览器里侧键是后退
from = "MouseX2"
to = ["Alt", "Left"]
window = "chrome.exe"
[[mappings]] # 编辑器里同一个键是撤销
from = "MouseX2"
to = ["Ctrl", "Z"]
window = "code.exe"
[[mappings]] # 别处一律当 Esc(不写 window 即兜底)
from = "MouseX2"
to = "Esc"匹配规则是越具体越优先:先比修饰键数量,再比有没有限定程序。所以上面这三条里,兜底那条只在前两条都不匹配时才轮到。
进程名从哪来:托盘菜单 → 按键监听(或 keyremap --listen),切换到目标程序时窗口里会直接报出它的名字。
几点需要知道的:
- 只认进程名,不认窗口标题。想按标题区分(比如同一个浏览器里的不同网站)做不到 —— 标题随时在变,每次变化都要重新判断,这个代价不该压在按键路径上。
- 判断依据是前台窗口,不是鼠标底下的窗口。
- 极少数系统进程查不到名字,这时限定型的规则(
window = "a.exe")不生效、排除型的(!a.exe)照常生效。宁可少触发一次,也不在认不出的程序里凭空发出按键。 - 按下时符合条件、按住期间切走了程序(Alt+Tab),抬起仍然会被正确处理,不会留下卡住的键。
keyremap --dump-keys 输出完整 JSON 列表。键名不区分大小写。
| 类别 | 键名 |
|---|---|
| 字母 | A ~ Z |
| 数字 | 0 ~ 9 |
| 功能键 | F1 ~ F24 |
| 修饰键 | Ctrl LCtrl RCtrl Alt LAlt RAlt Shift LShift RShift LWin RWin |
| 编辑导航 | Esc Tab CapsLock Space Enter Backspace Insert Delete Home End PageUp PageDown Left Right Up Down PrintScreen ScrollLock Pause Apps NumLock |
| 符号 | Backquote Minus Equal LeftBracket RightBracket Backslash Semicolon Quote Comma Period Slash |
| 小键盘 | Numpad0 ~ Numpad9 NumpadAdd NumpadSubtract NumpadMultiply NumpadDivide NumpadDecimal |
| 媒体键 | VolumeMute VolumeDown VolumeUp MediaNext MediaPrev MediaStop MediaPlayPause |
| 鼠标 | MouseLeft MouseRight MouseMiddle MouseX1 MouseX2 |
不带 L/R 前缀的修饰键(Ctrl Alt Shift)作为输入时匹配任意一侧,作为输出时注入左侧键。
MouseX1 / MouseX2 是鼠标侧键,通常 X1 后退、X2 前进,但不同鼠标可能相反,用 --listen 确认。鼠标按键目前只能作输入源,to 里只支持键盘按键。
托盘菜单 → Web 配置工具打开网页版界面:按键捕获、组合键勾选、分组管理、冲突提示、TOML 预览。
- Chrome / Edge:直接打开并保存
keyremap.toml,文件授权会被记住 - 其他浏览器:编辑后复制 TOML 粘贴进配置文件
主程序本身不含任何网络代码 —— 它只是把地址交给系统浏览器去开。页面也是纯静态的,配置只在你的浏览器和本机文件之间往来。
默认指向仓库自带的 GitHub Pages 部署,想换成自己部署的就在配置里写:
web_url = "https://my-keyremap.pages.dev/"页面代码在 docs/,详见 docs/README.md。
右键(或左键)托盘图标:
[✓] 启用映射 总开关,切换是即时的
─────────────
映射设置 (2/4) ▸ 映射条数会变,所以收进子菜单
按键监听... 另开一个窗口显示按下的键名
─────────────
重新加载配置
打开配置文件 用系统默认编辑器打开
打开配置目录
Web 配置工具... 用系统浏览器打开网页版界面
─────────────
[✓] 开机自启动
[✓] 以管理员身份启动 持久设置,手动启动与开机自启都生效
─────────────
退出
"映射设置"子菜单里,组标题本身就是组开关,组内映射缩进显示,组被关掉时组内各项一并置灰。没用分组时就直接是映射列表。
一级菜单的项目固定,不随映射条数变化 —— 这样"退出"这类常用项的位置不会漂移。
托盘图标会跟着启用状态(禁用时变淡)和任务栏主题(深色任务栏用浅色图标)变化。后者是必需的:图标是单色线条,纯黑的放在 Windows 11 默认的深色任务栏上几乎看不见。
Windows 的 UIPI 机制禁止低权限进程向高权限进程注入输入。所以前台窗口属于管理员权限的程序时(任务管理器、提权运行的编辑器等),普通权限的 keyremap 发出的按键会被丢弃,表现为"在某些窗口里映射突然不生效"。
勾上托盘菜单 → 以管理员身份启动即可解决。这是持久设置:勾上之后手动启动、开机自启都会以管理员身份运行。
首次勾选会请求一次 UAC 并重启程序 —— 因为它背后要创建一个计划任务,而这需要管理员权限(HKCU\Run 启动的进程永远不会提权,这是 Windows 的设计,所以"开机自启 + 管理员 + 不弹 UAC"只有计划任务这一条路)。之后每次启动都不会再打扰你。UAC 被取消时程序会以普通权限继续运行,并在日志里说明。
托盘提示里的 · 管理员 表示此刻真的以管理员在跑,菜单里的勾选表示设置 —— 刚改完还没重启时两者会不一致,这是正常的。
程序是 GUI 子系统的,双击不会有黑窗口;需要输出时才附加控制台。
| 参数 | 说明 |
|---|---|
-c, --config <PATH> |
指定配置文件,默认见配置文件在哪 |
-l, --listen |
监听模式,只打印按下的键名与切换到的程序名,不做任何映射 |
--dump |
按组打印已加载的映射后退出 |
--dump-keys |
输出全部可用键名(JSON)后退出 |
-o, --output <FILE> |
配合上面两个:把输出写入文件 |
--logfile |
把日志写入 exe 同目录的 keyremap.log |
-v, -vv |
提高日志级别,并附加控制台显示日志 |
要落盘请用 -o 而不是 shell 重定向:GUI 程序不会被 shell 等待,--dump-keys > keys.json 会因为管道提前关闭而写不进去。
一次性输出的命令若是自己新开的窗口(双击运行,或经 scoop 的 shim 启动),会等你按回车再关,不会闪一下就没了;输出重定向到文件时不等待,脚本里可以放心用。
监听窗口关掉了"快速编辑模式" —— 开着的话点一下鼠标就进入选区,而选区会阻塞控制台写入,把按键事件的输出通道堵住。要复制文字请用标题栏右键菜单里的编辑 → 标记。
--dump 和 --listen 不受单实例限制,方便在后台运行时排查。
- UAC 安全桌面与部分反作弊游戏覆盖不到。这类场景任何用户态方案都无能为力,需要内核级驱动。普通的管理员权限窗口用以管理员身份启动即可解决。
- 组合键作为输入源时,应用会看到一次修饰键的松开与重新按下,用户态方案绕不开。
- 只有
CapsLock能当"普通键改修饰键"用。任意键都这么做需要一整套"层"的机制,CapsLock 因为副作用最烦人而被单独支持。 window只认进程名,不认窗口标题,也无法按控件区分。标题随时在变,每次变化都要重新判断,这个代价不该压在按键路径上。不同window条件最多 64 种(写法相同的会合并成一种)。- Web 配置工具只给通用修饰键的勾选框,分侧的(
LCtrl等)需要手写;不过已经写在配置里的会被认出来,不会被工具改掉。 to暂不支持鼠标按键输出。- 仅支持 Windows。实现直接调用 Win32 API,没有跨平台打算。
cargo build --release
需要 Rust 1.85+(edition 2024)。产物在 target/release/keyremap.exe。
不需要资源编译器 —— assets/app.res(四个图标变体 + 文件版本信息)是预编译好提交进仓库的。只有改动图标或版本号时才要重新生成:
python assets\make-icons.py # 图标源变了:从 app_icon.ico 派生其余变体
.\assets\build-res.ps1 # 重新编译 app.res(用 rc.exe 或 windres)版本号有三处,发版前必须在本地对齐:Cargo.toml 的 version、assets/app.rc 的三个版本字段,然后跑 .\assets\build-res.ps1 重新生成 app.res 并提交。打 tag 推送即可:
git tag v0.5.2 && git push origin v0.5.2
release.yml 会校验 tag、Cargo.toml、exe 版本资源三者一致 —— 不一致就直接失败,而不是发一个自相矛盾的包(流水线改不了预编译的 app.res,所以只能校验不能自动修)。
通过之后自动:构建 → 打包 keyremap-<版本>-windows-x86_64.zip(包内不套目录,scoop 要求)→ 建 Release → 通知 huanfeng/scoop-bucket 更新清单。需要在本仓库配置 secret SCOOP_TOKEN(对 bucket 仓库有 contents:write 的 PAT)。
如果你打算读代码或做类似的东西,几个可能有用的点:
- 键盘和鼠标用
SetWindowsHookExW装WH_KEYBOARD_LL/WH_MOUSE_LL低级钩子。这是用户态下唯一能拦截并改写按键的方式,AutoHotkey 和 PowerToys 也都是这套。RegisterHotKey不能改键,Raw Input 只能观察不能拦截。 - 注入事件的
dwExtraInfo打上标记,钩子回调第一件事就是检查它并放行自己发出的事件。没有这一步,反向映射会无限回环。AutoHotkey 用的是同一手法。 - 钩子回调里不做任何耗时操作:不 sleep、不做字符串比较、不碰 IO、不堆分配。低级钩子回调超过
LowLevelHooksTimeout(默认 300 ms)会被系统静默摘掉,程序不崩但从此失效。组合键因此用单次SendInput原子发送;监听模式的打印投给另一个线程做 —— 控制台被鼠标选中时写入会阻塞,而回调阻塞意味着全系统的输入都在等它。 - 回调第一步是查一份预计算的位图:这个键上没挂映射就立刻放行,不加锁也不克隆
Arc。绝大多数按键跟配置毫无关系,让它们走完整流程是白花代价。 - 配置在加载时就"编译"成虚拟键码与数组下标,热路径上只比较整数。
- 按程序区分不在按键路径上查进程。查进程要打开内核对象再比字符串,每次按键都做代价太大。改为用
SetWinEventHook监听前台窗口切换(人手速度的事件),切换时查一次进程名、算出"此刻哪些window条件成立"存成一个u64位掩码;按键时只剩一次原子读加一次&。没有任何映射用window时连这个钩子都不装。 - 键盘钩子、托盘窗口消息、配置重载通知共用同一个消息循环,不额外开线程 —— 低级钩子本来就需要消息泵,托盘图标也需要窗口。
- 启动时就用
ImmDisableIME关掉本进程的输入法。程序里没有任何地方需要打字,但托盘菜单弹出前必须SetForegroundWindow(不抢到前台的话,点击别处菜单不会消失),而进程一旦成为前台,输入法框架就会把输入法的 TIP 连同词库、皮肤资源一起加载进来。实测装了第三方输入法的机器上,这一下多出 21 个 DLL、139 个句柄、2 个线程,提交内存翻倍、工作集翻近三倍;关掉之后一个都不进来。这也是为什么这个调用看着无用却不能删。 - 配置里没有鼠标映射时不安装鼠标钩子(
WH_MOUSE_LL会收到全部鼠标移动事件)。这个判断每次重载都会重做,新出现鼠标映射就补装、没有了就卸掉;托盘提示里的「含鼠标」反映钩子的实际状态。 - CapsLock 当修饰键时,它的按住状态只能靠自己记录 —— 按下事件被我们吞掉了,而被吞掉的事件不会进入系统按键状态,
GetAsyncKeyState查出来永远是"没按下"。
MIT