Skip to content

Latest commit

 

History

32 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

keyremap

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.exekeyremap.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"

fromto 都可以写成单个键或数组:

写法 含义
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 里点名的修饰键会被动,你同时按着的其他键不受影响。
  • 长按连发跟着 toto 是单键就连发(长按 Ctrl+E 连续删字),是组合键就只触发一次。
  • 更具体的匹配优先:同时配了 E → DCtrl+E → Backspace,按住 Ctrl 走后者。
  • 触发键本身不能是修饰键。["Ctrl", "Shift"] 含义不明,直接报错而不去猜。

CapsLock 当修饰键

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 配置工具

托盘菜单 → 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.tomlversionassets/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)。

实现说明

如果你打算读代码或做类似的东西,几个可能有用的点:

  • 键盘和鼠标用 SetWindowsHookExWWH_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 查出来永远是"没按下"。

License

MIT

About

一个Windwos平台的按键映射工具

Topics

Resources

Stars

29 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages