├── .gitignore ├── LICENSE ├── Makefile ├── MyKeymap.exe ├── bin ├── AlignComment.ahk ├── AutoHotkey64.exe ├── ChangeBrightness.ahk ├── CollectText.ahk ├── Launcher.ahk ├── MiscTools.ahk ├── MyKeymap-CommandInput.exe ├── MyKeymap.ahk ├── RunActivate.ahk ├── SoundControl.exe ├── WindowSpy.ahk ├── font │ └── font.ttf ├── html │ ├── AlignText.html │ ├── logo.ico │ └── tailwind.css ├── icons │ ├── logo.ico │ ├── logo2.ico │ └── logo3.ico ├── lib │ ├── Actions.ahk │ ├── Functions.ahk │ ├── InputTipWindow.ahk │ ├── KeymapManager.ahk │ ├── Monitor.ahk │ ├── Utils.ahk │ └── translation.ahk ├── msvcp140.dll ├── sound │ ├── cancel.wav │ ├── keydown.wav │ ├── show.wav │ └── spaceKey.wav ├── vcruntime140.dll └── vcruntime140_1.dll ├── build_tools.go ├── config-server ├── .gitignore ├── cmd │ └── settings │ │ └── main.go ├── go.mod ├── go.sum ├── internal │ ├── command │ │ └── command.go │ ├── matrix │ │ └── matrix.go │ └── script │ │ ├── abbr.go │ │ ├── action.go │ │ ├── config.go │ │ ├── options.go │ │ ├── script.go │ │ └── script_test.go └── templates │ ├── CommandInputSkin.tmpl │ ├── CustomShellMenu.ahk │ ├── help.html │ └── mykeymap.tmpl ├── config-ui ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── ProgramPathExample.html │ ├── config_doc.html │ ├── config_doc.md │ ├── exe-path.png │ ├── favicon.ico │ ├── font-compare │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ ├── img │ │ ├── example01.png │ │ ├── example02.png │ │ ├── example03.png │ │ └── example04.png │ └── uwp-shortcut.png ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── ActionCommentTable.vue │ │ ├── Code.vue │ │ ├── HelloWorld.vue │ │ ├── Key.vue │ │ ├── NavigationDrawer.vue │ │ ├── Table.vue │ │ ├── Tip.vue │ │ ├── actions │ │ │ ├── Action.vue │ │ │ ├── ActivateOrRun.vue │ │ │ ├── BuiltinFunction.vue │ │ │ ├── Mouse.vue │ │ │ ├── MyKeymap.vue │ │ │ ├── RadioGroup.vue │ │ │ ├── RemapKey.vue │ │ │ ├── SendKey.vue │ │ │ ├── System.vue │ │ │ ├── Text.vue │ │ │ └── Window.vue │ │ └── dialog │ │ │ ├── InputKeyValueDialog.vue │ │ │ ├── PathDialog.vue │ │ │ └── WindowGroupDialog.vue │ ├── main.ts │ ├── plugins │ │ ├── index.ts │ │ ├── vuetify.ts │ │ └── webfontloader.ts │ ├── router │ │ └── index.ts │ ├── store │ │ ├── app.ts │ │ ├── config.ts │ │ ├── index.ts │ │ ├── language-map.ts │ │ ├── server.ts │ │ └── shortcut.ts │ ├── styles │ │ └── settings.scss │ ├── types │ │ └── config.ts │ ├── views │ │ ├── Abbr.vue │ │ ├── CustomHotkey.vue │ │ ├── Home.vue │ │ ├── HomeSettings.vue │ │ ├── Keymap.vue │ │ └── Settings.vue │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── data ├── config.json └── custom_functions.ahk ├── doc ├── features.png ├── settings.en.png ├── settings.png └── 夏日大作战.gif ├── lanzou_client.py ├── readme.en.md ├── readme.md ├── tailwind ├── help_page.config.js ├── help_page.css ├── html-tools.config.js ├── index.css ├── package-lock.json ├── package.json └── tailwind.config.js ├── tools ├── Rexplorer_x64.exe └── 工具来源.txt ├── 变量方法差别.md └── 误报病毒时执行这个.bat /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .pnpm-debug.log 3 | MyKeymap 4 | bin/site 5 | bin/html-tools/AlignText.html 6 | bin/html-tools/tailwind.css 7 | bin/templates 8 | bin/mykeymap-settings-server 9 | bin/mykeymap-settings-server.exe 10 | bin/settings.exe 11 | *.7z 12 | todo.md 13 | .idea 14 | MyKeymap-*/ 15 | config-server/settings.exe 16 | shortcuts/ 17 | lanzou_cookie.txt 18 | CommandInputSkin.txt -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | version = 2.0-beta33 2 | ahkVersion = 2.0.19 3 | folder = MyKeymap-$(version) 4 | zip = $(folder).7z 5 | 6 | buildServer: 7 | go.exe build -C ./config-server -tags=nomsgpack -ldflags "-s -w -X settings/internal/script.MykeymapVersion=$(version)" -o ../bin/settings.exe ./cmd/settings 8 | rm -f -r bin/templates 9 | cp -r config-server/templates bin/templates 10 | 11 | buildClient: 12 | cd config-ui; npm run build 13 | rm -f config-ui/tsconfig.tsbuildinfo 14 | cd config-ui/dist/assets; rm -f *.woff *.eot *.ttf 15 | 16 | copyFiles: CopyAHK 17 | rm -f -r $(folder) 18 | mkdir $(folder) 19 | mkdir $(folder)/shortcuts 20 | 21 | rm -f -r bin/site 22 | cp -r config-ui/dist bin/site 23 | 24 | cp -r data $(folder)/ 25 | cp -r bin $(folder)/ 26 | cp -r tools $(folder)/ 27 | cp MyKeymap.exe $(folder)/ 28 | cp 误报病毒时执行这个.bat $(folder)/ 29 | 30 | # 如果直接用 wsl 的 cp 命令复制, 复制出的文件会有 read-only 属性, 比较奇怪 31 | CopyAHK: 32 | @echo '@copy /y "C:\\Program Files\\AutoHotkey\\v2\AutoHotkey64.exe" .\\bin\\' > CopyAHK.bat 33 | cmd.exe /c CopyAHK.bat 34 | rm CopyAHK.bat 35 | 36 | build: buildServer buildClient copyFiles 37 | cd bin; ./settings.exe ChangeVersion $(version) 38 | rm -f MyKeymap-*.7z 39 | 7z.exe a $(zip) $(folder) 40 | rm -f -r $(folder) 41 | @echo ------------------------- build ok ------------------------------- 42 | 43 | createRelease: 44 | curl -L \ 45 | -X POST \ 46 | -H "Accept: application/vnd.github+json" \ 47 | -H "Authorization: Bearer $$(cat ~/gh_token)" \ 48 | -H "X-GitHub-Api-Version: 2022-11-28" \ 49 | https://api.github.com/repos/xianyukang/MyKeymap/releases \ 50 | -d '{"tag_name":"v$(version)","target_commitish":"main","name":"v$(version)","body":"Description of the release"}' 2>/dev/null | jq -r '.id' > release_id 51 | curl -L \ 52 | -X POST \ 53 | -H "Accept: application/vnd.github+json" \ 54 | -H "Authorization: Bearer $$(cat ~/gh_token)" \ 55 | -H "X-GitHub-Api-Version: 2022-11-28" \ 56 | -H "Content-Type: application/octet-stream" \ 57 | "https://uploads.github.com/repos/xianyukang/MyKeymap/releases/$$(cat release_id)/assets?name=$(zip)" \ 58 | --data-binary "@$(zip)" | jq 59 | rm release_id 60 | 61 | 62 | uploadLanZou: 63 | go run build_tools.go checkForAHKUpdate $(ahkVersion) 64 | python3 lanzou_client.py $(zip) 2> share_link.json 65 | go run build_tools.go updateShareLink $(version) 66 | rm -f share_link.json 67 | 68 | upload: uploadLanZou createRelease 69 | @echo ------------------------- upload ok ------------------------------- 70 | 71 | # 下面是开发时用到的命令: 72 | 73 | server: buildServer 74 | @cd config-server; ../bin/settings.exe debug 75 | 76 | # https://github.com/facebook/create-react-app/issues/10253#issuecomment-747970009 77 | # 坑啊: WSL2 中的 Chokidar 无法监测 Windows 上的文件修改, 导致 hot reload 不起作用 78 | # 需要把项目移到 WSL2 里面, 或者使用 Polling 模式 79 | client: 80 | @cd config-ui; CHOKIDAR_USEPOLLING=true npm run dev 81 | 82 | ahk: buildServer 83 | @config-server/settings.exe GenerateAHK ./data/config.json ./config-server/templates/mykeymap.tmpl ./bin/MyKeymap.ahk 84 | 85 | .PHONY: server client ahk buildServer buildClient copyFiles upload build -------------------------------------------------------------------------------- /MyKeymap.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/MyKeymap.exe -------------------------------------------------------------------------------- /bin/AlignComment.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #NoTrayIcon 3 | #SingleInstance Force 4 | SetWorkingDir A_ScriptDir 5 | 6 | AlignText() 7 | 8 | AlignText() { 9 | text := GetSelectedText() 10 | value := URIEncode(text) 11 | Run("msedge.exe --app=" A_WorkingDir "\html\AlignText.html?text=" value) 12 | } 13 | 14 | GetSelectedText() { 15 | temp := A_Clipboard 16 | ; 清空剪贴板 17 | A_Clipboard := "" 18 | 19 | Send("^c") 20 | if not (ClipWait(0.4)) { 21 | Tip("no items selected", -1200) 22 | return 23 | } 24 | text := A_Clipboard 25 | 26 | A_Clipboard := temp 27 | return RTrim(text, "`r`n") 28 | } 29 | 30 | Tip(message, time := -1500) { 31 | ToolTip(message) 32 | SetTimer(() => ToolTip(), time) 33 | } 34 | 35 | URIEncode(uri, encoding := "UTF-8") { 36 | if !uri { 37 | return 38 | } 39 | var := Buffer(StrPut(uri, encoding), 0) 40 | StrPut(uri, var, encoding) 41 | pos := 1 42 | ; 按字节遍历 buffer 中的 utf-8 字符串, 注意字符串有 null-terminator 43 | While pos < var.Size { 44 | code := NumGet(var, pos - 1, "UChar") 45 | if (code >= 0x30 && code <= 0x39) || (code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A) 46 | res .= Chr(code) 47 | else 48 | res .= "%" . Format("{:02X}", code) 49 | pos++ 50 | } 51 | return res 52 | } -------------------------------------------------------------------------------- /bin/AutoHotkey64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/AutoHotkey64.exe -------------------------------------------------------------------------------- /bin/ChangeBrightness.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance Force 2 | #NoTrayIcon 3 | SetWorkingDir(A_ScriptDir) 4 | TraySetIcon("./icons/logo.ico") 5 | 6 | #Include ./lib/Monitor.ahk 7 | #Include ./lib/Utils.ahk 8 | 9 | class CLayout extends Gui { 10 | monitors := [] 11 | currentIndex := 1 12 | monCount := SysGet(80) 13 | monitorCol := Monitor() 14 | defaultGuiWidth := 440 15 | monitorIcoSize := 170 16 | 17 | __New() { 18 | super.__New() 19 | super.Opt("+AlwaysOnTop +Owner +LastFound +Resize -MinimizeBox -MaximizeBox") 20 | super.Title := "显示器亮度调节" 21 | this.InitMonitors() 22 | this.ChangeSelectedMonitor() 23 | 24 | ; 添加下方帮助信息 25 | super.SetFont("s12") 26 | super.Add("text", "x10 y190 w290 h20", "EDSF调节亮度、WR切换显示器、C退出") 27 | super.Add("text", "x10 y210 w" this.defaultGuiWidth " h20", "如果不起作用,可能是因为使用的显示器没有开启DDC/CI功能") 28 | } 29 | 30 | ; 初始化已使用的显示器信息 31 | InitMonitors() { 32 | isWin10 := StrCompare(A_OSVersion, "10.0.22000") < 0 33 | 34 | ; 获取已使用的显示器数量 35 | loop this.monCount { 36 | monIndex := A_Index 37 | ; 获取显示器当前亮度 38 | brightness := this.getBrightness(monIndex) 39 | 40 | ; 计算MonIcoText要显示的位置 41 | ; 当显示器图标无法沾满窗口默认大小时,让显示器图标剧中 42 | padding := 0 43 | if (this.monitorIcoSize * this.monCount < this.defaultGuiWidth) { 44 | padding := (this.defaultGuiWidth - (this.monitorIcoSize * this.monCount)) / 2 45 | } 46 | x := this.monitorIcoSize * (monIndex - 1) + padding 47 | monIco := super.Add("Text", "x" x " y0 w" this.monitorIcoSize " h" this.monitorIcoSize " 0x1", "🖥️") 48 | monIco.SetFont("s128 c000000") 49 | 50 | y := 52 51 | ; 是 Windows 10 则使用不同的 y 坐标 52 | if isWin10 { 53 | y := 66 54 | } 55 | 56 | ; 计算显示器亮度要显示的位置 57 | x := x + 52 58 | monBrightness := super.Add("Text", "x" x " y" y " w65 h40 0x1", brightness) 59 | monBrightness.SetFont("s32 cffffff") 60 | monBrightness.Opt("+BackgroundTrans") 61 | 62 | ; 将当前控件保存起来 63 | m := Map() 64 | m["ico"] := monIco 65 | m["brightness"] := monBrightness 66 | this.monitors.Push(m) 67 | } 68 | } 69 | 70 | ; 修改界面上的当前显示器 71 | ChangeSelectedMonitor(old := 1) { 72 | m := this.monitors.Get(old) 73 | m["ico"].SetFont("c000000") 74 | 75 | m := this.monitors.Get(this.currentIndex) 76 | m["ico"].SetFont("cff6688") 77 | } 78 | 79 | ; 上一个显示屏 80 | Previous() { 81 | if (this.currentIndex > 1) { 82 | this.changeSelectedMonitor(this.currentIndex--) 83 | } 84 | } 85 | 86 | ; 下一个显示屏 87 | Next() { 88 | if (this.currentIndex < this.monCount) { 89 | this.changeSelectedMonitor(this.currentIndex++) 90 | } 91 | } 92 | 93 | ; 加亮度 94 | IncBrightness(mon) { 95 | m := this.monitors.Get(this.currentIndex) 96 | val := m["brightness"].Value + mon 97 | if val > 100 { 98 | val := 100 99 | } 100 | this.SetBrightness(val, this.currentIndex) 101 | m["brightness"].Value := val 102 | } 103 | 104 | ; 减亮度 105 | DecBrightness(mon) { 106 | m := this.monitors.Get(this.currentIndex) 107 | val := m["brightness"].Value - mon 108 | if val < 0 { 109 | val := 0 110 | } 111 | this.SetBrightness(val, this.currentIndex) 112 | m["brightness"].Value := val 113 | } 114 | 115 | ; 显示GUI 116 | ShowGui() { 117 | super.Show() 118 | disableIME(super.Hwnd) 119 | } 120 | 121 | ; 获取屏幕亮度 122 | GetBrightness(monIndex) { 123 | brightness := "" 124 | try { 125 | brightness := this.monitorCol.GetBrightness(monIndex)["Current"] 126 | } catch Error as e { 127 | ; 使用wmi获取亮度 128 | For property in ComObjGet("winmgmts:\\.\root\WMI").ExecQuery("SELECT * FROM WmiMonitorBrightness") 129 | brightness := property.CurrentBrightness 130 | } 131 | 132 | return brightness 133 | } 134 | 135 | ; 设置屏幕亮度 136 | SetBrightness(brightness, monIndex, timeout := 1) { 137 | try { 138 | this.monitorCol.setBrightness(brightness, monIndex) 139 | } catch Error as e { 140 | ; 使用wmi设置亮度 141 | For property in ComObjGet("winmgmts:\\.\root\WMI").ExecQuery("SELECT * FROM WmiMonitorBrightnessMethods") 142 | property.WmiSetBrightness(timeout, brightness) 143 | } 144 | } 145 | } 146 | 147 | layout := CLayout() 148 | layout.ShowGui() 149 | 150 | SetTimer(IfLoseFocusThenExit, 100) 151 | IfLoseFocusThenExit() { 152 | if ( not WinActive("ahk_id" layout.Hwnd)) { 153 | ExitApp 154 | } 155 | } 156 | 157 | OnMessage(0x100, WM_KEYDOWN) 158 | WM_KEYDOWN(wParam, lParam, msg, hwnd) { 159 | switch (GetKeyName(Format("vk{:x}", wparam))) { 160 | case "w": layout.Previous() 161 | case "r": layout.Next() 162 | case "e": layout.IncBrightness(10) 163 | case "f": layout.IncBrightness(5) 164 | case "d": layout.DecBrightness(10) 165 | case "s": layout.DecBrightness(5) 166 | case "c": ExitApp 167 | case "Escape": ExitApp 168 | } 169 | 170 | return 0 171 | } -------------------------------------------------------------------------------- /bin/CollectText.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance Force 2 | #NoTrayIcon 3 | 4 | ListLines(false) 5 | SetWorkingDir(A_ScriptDir) 6 | TraySetIcon("./icons/logo.ico") 7 | SendMode("Input") 8 | 9 | 10 | last := "" 11 | layout := CLayout() 12 | layout.showGui() 13 | OnClipboardChange(ClipboardChangeCallbakc) 14 | 15 | class CLayout { 16 | X := 10 17 | Y := 10 18 | W := 700 19 | H := 180 20 | textList := [] 21 | 22 | __New() { 23 | this.myGui := Gui("+resize +AlwaysOnTop -SysMenu") 24 | this.hwnd := this.myGUI.hwnd 25 | this.myGUI.SetFont("s12", "Microsoft YaHei Ui") 26 | this.myGUI.Add("text", "y-20", "连续复制后,在本窗口按空格键复制下面的文本") 27 | } 28 | 29 | ShowGui(title := "连续复制后, 在本窗口内按空格或 C 键") { 30 | this.myGui.Title := title 31 | this.myGUI.Show("AutoSize NoActivate W" this.W) 32 | } 33 | 34 | AddItem(text) { 35 | this.textList.Push(text) 36 | space := "y+6" 37 | this.myGUI.Add("text", "y+6 w700 +wrap", text) 38 | } 39 | 40 | Restart() { 41 | this.myGui.Destroy() 42 | this.__New 43 | this.ShowGui("已把收集到的 " this.textList.Length " 行挪入剪切版") 44 | this.textList := [] 45 | SetTimer(() => this.ShowGui(), -5000) 46 | } 47 | 48 | } 49 | 50 | 51 | join(sep, params*) { 52 | str := "" 53 | 54 | for index, param in params 55 | str .= sep . param 56 | 57 | return SubStr(str, StrLen(sep) + 1) 58 | } 59 | 60 | OnMessage(0x100, WM_KEYDOWN) 61 | WM_KEYDOWN(wParam, lParam, msg, hwnd) { 62 | switch (GetKeyName(Format("vk{:x}", wparam))) { 63 | case "Space": 64 | JoinCopiedText() 65 | case "Escape": 66 | ExitApp 67 | case "c": 68 | JoinCopiedText() 69 | ExitApp 70 | } 71 | return 0 72 | } 73 | 74 | ClipboardChangeCallbakc(type) { 75 | global last 76 | if (type != 1) 77 | return 78 | 79 | text := RTrim(A_Clipboard, " `t`r`n") 80 | if text == last { 81 | return 82 | } 83 | last := text 84 | layout.AddItem(text) 85 | layout.ShowGui() 86 | } 87 | 88 | JoinCopiedText() 89 | { 90 | ; 临时取消监听 91 | OnClipboardChange(ClipboardChangeCallbakc, 0) 92 | res := join("`n", layout.textList*) 93 | if res { 94 | A_Clipboard := res 95 | } 96 | layout.Restart() 97 | ; 启动监听 98 | OnClipboardChange(ClipboardChangeCallbakc) 99 | } -------------------------------------------------------------------------------- /bin/Launcher.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | ; 该脚本只能编译成Exe使用,只用做启动MyKeymap.ahk和AHk v2使用 3 | #SingleInstance Force 4 | #NoTrayIcon 5 | ;@Ahk2Exe-SetMainIcon ./bin/icons/logo3.ico 6 | ;@Ahk2Exe-ExeName MyKeymap 7 | SetWorkingDir(A_ScriptDir) 8 | 9 | ; 以管理员权限运行 10 | full_command_line := DllCall("GetCommandLine", "str") 11 | if not (A_IsAdmin or RegExMatch(full_command_line, " /restart(?!\S)")) { 12 | otherArgs := "" 13 | if (A_Args.Length) { 14 | otherArgs := A_Args.Get(1) 15 | } 16 | 17 | try { 18 | if A_IsCompiled 19 | Run '*RunAs "' A_ScriptFullPath '" ' otherArgs ' /restart' 20 | else 21 | Run '*RunAs "' A_AhkPath '" /restart "' A_ScriptFullPath '"' 22 | ExitApp 23 | } catch Error as e { 24 | hasTip := true 25 | ToolTip("`n MyKeymap is running with normal privileges.`n MyKeymap will not work in a window with admin rights ( e.g., Taskmgr.exe ) `n ") 26 | } 27 | } 28 | 29 | mainAhkFilePath := "./bin/MyKeymap.ahk" 30 | 31 | if (A_Args.Length) { 32 | Run("MyKeymap.exe /script " A_Args.Get(1)) 33 | } else { 34 | ; 通过配置文件生成脚本 35 | RunWait("./bin/settings.exe GenerateScripts", "./bin", "Hide") 36 | ; 首次运行则生成快捷方式 37 | if !FileExist(A_WorkingDir "\shortcuts\*.*") { 38 | Run("MyKeymap.exe /script ./bin/MiscTools.ahk GenerateShortcuts") 39 | } 40 | ; 启动脚本 41 | Run("MyKeymap.exe /script " mainAhkFilePath) 42 | } 43 | 44 | if IsSet(hasTip) { 45 | Sleep 7000 46 | } -------------------------------------------------------------------------------- /bin/MiscTools.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #SingleInstance Force 3 | #NoTrayIcon 4 | 5 | SetWorkingDir A_ScriptDir "\.." 6 | 7 | if !A_Args.Length { 8 | return 9 | } 10 | 11 | if A_Args[1] = "GenerateShortcuts" { 12 | ; 由于 windows 系统不允许存在同名文件和文件夹,故预先删除之 13 | try FileDelete("shortcuts") 14 | try DirDelete("shortcuts", true) 15 | ; 休息 0.05 s,防止 delete 操作未完成引起的 shortcuts 目录被占用问题 16 | Sleep(50) 17 | try DirCreate("shortcuts") 18 | 19 | ; 排除特定的快捷方式 20 | useless := "i)(?:uninstall|卸载|help|iSCSI 发起程序|ODBC 数据源|Data Sources \(ODBC\)" 21 | . "|ODBC Data|Windows 内存诊断|恢复驱动器|组件服务|碎片整理和优化驱动器|Office 语言首选项" 22 | . "|手册|更新|帮助|Tools Command Prompt for|license|Website|设置向导|More Games from Microsoft" 23 | . "|细胞词库|意见反馈|输入法管理器|输入法修复器|皮肤下载|官方网站|Microsoft Office 语言设置" 24 | . "|Microsoft Office 文档关联中心|Internet Explorer \(No Add-ons\)" 25 | . "|Windows Easy Transfer Reports|Welcome Center|Microsoft Office 2007 控制中心" 26 | 27 | Loop Files A_StartupCommon . "\*.lnk*" 28 | useless .= "|" . A_LoopFileName 29 | Loop Files A_Startup . "\*.lnk*" 30 | useless .= "|" . A_LoopFileName 31 | useless .= ")" 32 | 33 | ; 把开始菜单中的快捷方式都拷贝到 shortcuts 目录 34 | copyFiles(A_ProgramsCommon "\*.lnk", "shortcuts\", useless) 35 | copyFiles(A_Programs "\*.lnk", "shortcuts\", useless) 36 | ; 然后再生成 UWP 相关的快捷方式 37 | oFolder := ComObject("Shell.Application").NameSpace("shell:AppsFolder") 38 | if Type(oFolder) != 'String' { 39 | for item in oFolder.Items { 40 | if item.Name . ".lnk" ~= useless || FileExist("shortcuts\" item.Name ".lnk") { 41 | continue 42 | } 43 | try FileCreateShortcut("shell:appsfolder\" item.Path, "shortcuts\" item.Name ".lnk") 44 | } 45 | } 46 | return 47 | } 48 | 49 | if A_Args[1] = "RunAtStartup" { 50 | linkFile := A_Startup "\MyKeymap.lnk" 51 | if A_Args[2] = "On" { 52 | FileCreateShortcut(A_WorkingDir "\MyKeymap.exe", linkFile, A_WorkingDir) 53 | } else if (A_Args[2] = "Off") { 54 | if FileExist(linkFile) { 55 | FileDelete(linkFile) 56 | } 57 | } 58 | return 59 | } 60 | 61 | copyFiles(pattern, dest, ignore := "") { 62 | Loop Files pattern, "R" { 63 | if (A_LoopFileName ~= ignore) { 64 | continue 65 | } 66 | try FileCopy(A_LoopFilePath, dest, true) 67 | } 68 | } -------------------------------------------------------------------------------- /bin/MyKeymap-CommandInput.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/MyKeymap-CommandInput.exe -------------------------------------------------------------------------------- /bin/RunActivate.ahk: -------------------------------------------------------------------------------- 1 | ; 启动后激活一下窗口 2 | target := A_Args[1] 3 | Run target, , , &pid 4 | exists := WinWait("ahk_pid " pid, , 1) 5 | if exists && !WinActive() { 6 | WinActivate 7 | } -------------------------------------------------------------------------------- /bin/SoundControl.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/SoundControl.exe -------------------------------------------------------------------------------- /bin/WindowSpy.ahk: -------------------------------------------------------------------------------- 1 | ; 2 | ; Window Spy for AHKv2 3 | ; 4 | 5 | #Requires AutoHotkey v2.0 6 | 7 | #NoTrayIcon 8 | #SingleInstance force 9 | SetWorkingDir A_ScriptDir 10 | CoordMode "Pixel", "Screen" 11 | 12 | Global oGui 13 | 14 | WinSpyGui() 15 | 16 | WinSpyGui() { 17 | Global oGui 18 | 19 | try TraySetIcon "./icons/logo.ico" 20 | DllCall("shell32\SetCurrentProcessExplicitAppUserModelID", "wstr", "AutoHotkey.WindowSpy") 21 | 22 | oGui := Gui("AlwaysOnTop Resize MinSize +DPIScale", "当前窗口的标识符") 23 | oGui.OnEvent("Close", WinSpyClose) 24 | oGui.OnEvent("Size", WinSpySize) 25 | 26 | oGui.BackColor := "FFFFFF" 27 | oGui.SetFont("s11", "Microsoft YaHei") 28 | oGui.Add("Text", , "程序的窗口标识符有下面三种:") 29 | oGui.Add("Text", , "▷ 窗口名: 无标题 - 记事本") 30 | oGui.Add("Text", , "▷ 进程名: ahk_exe notepad.exe") 31 | oGui.Add("Text", , "▷ 窗口类名: ahk_class Notepad") 32 | oGui.Add("Text", , "➤ 一般选进程名") 33 | oGui.Add("Text", , "➤ 可以只使用部分窗口名: 记事") 34 | oGui.Add("Text", , "➤ 也可以组合两个标识符: 记事 ahk_exe notepad.exe (更精确") 35 | oGui.Add("Text", , "") 36 | 37 | oGui.Add("Text", , "Window Title, Class and Process:") 38 | oGui.Add("Edit", "xm w640 r4 ReadOnly -Wrap vCtrl_Title") 39 | ; oGui.Add("Text",,"当前鼠标位置:") 40 | ; oGui.Add("Edit","w640 r4 ReadOnly vCtrl_MousePos") 41 | ; oGui.Add("Text",,"当前窗口位置:") 42 | ; oGui.Add("Edit","w640 r2 ReadOnly vCtrl_Pos") 43 | oGui.Add("Text", "w640 r1 vCtrl_Freeze", (txtNotFrozen := "")) 44 | oGui.Add("Checkbox", "yp+20 xp+400 h15 w240 Left vCtrl_FollowMouse", "跟随鼠标 (可按 Ctrl 暂停刷新)") 45 | 46 | oGui.Show("NoActivate") 47 | ;WinGetClientPos(&x_temp, &y_temp2, , , "ahk_id " oGui.hwnd) 48 | 49 | ; oGui.horzMargin := x_temp*96//A_ScreenDPI - 320 ; now using oGui.MarginX 50 | 51 | oGui.txtNotFrozen := txtNotFrozen ; create properties for futur use 52 | oGui.txtFrozen := "" 53 | 54 | SetTimer Update, 250 55 | } 56 | 57 | WinSpySize(GuiObj, MinMax, Width, Height) { 58 | Global oGui 59 | 60 | If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is 61 | return 62 | 63 | SetTimer Update, (MinMax = 0) ? 250 : 0 ; suspend updates on minimize 64 | 65 | ctrlW := Width - (oGui.MarginX * 2) ; ctrlW := Width - horzMargin 66 | list := "Title,MousePos,Ctrl,Pos,SBText,VisText,AllText,Freeze" 67 | } 68 | 69 | WinSpyClose(GuiObj) { 70 | ExitApp 71 | } 72 | 73 | Update() { ; timer, no params 74 | Try TryUpdate() ; Try 75 | } 76 | 77 | TryUpdate() { 78 | Global oGui 79 | 80 | If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is 81 | return 82 | 83 | Ctrl_FollowMouse := oGui["Ctrl_FollowMouse"].Value 84 | CoordMode "Mouse", "Screen" 85 | MouseGetPos &msX, &msY, &msWin, &msCtrl, 2 ; get ClassNN and hWindow 86 | actWin := WinExist("A") 87 | 88 | if (Ctrl_FollowMouse) { 89 | curWin := msWin, curCtrl := msCtrl 90 | WinExist("ahk_id " curWin) ; updating LastWindowFound? 91 | } else { 92 | curWin := actWin 93 | curCtrl := ControlGetFocus() ; get focused control hwnd from active win 94 | } 95 | curCtrlClassNN := "" 96 | Try curCtrlClassNN := ControlGetClassNN(curCtrl) 97 | 98 | t1 := WinGetTitle(), t2 := WinGetClass() 99 | if (curWin = oGui.hwnd || t2 = "MultitaskingViewFrame") { ; Our Gui || Alt-tab 100 | UpdateText("Ctrl_Freeze", oGui.txtFrozen) 101 | return 102 | } 103 | 104 | UpdateText("Ctrl_Freeze", oGui.txtNotFrozen) 105 | t3 := WinGetProcessName() 106 | 107 | WinDataText := t1 "`n" ; ZZZ 108 | . "ahk_class " t2 "`n" 109 | . "ahk_exe " t3 "`n" 110 | 111 | UpdateText("Ctrl_Title", WinDataText) 112 | CoordMode "Mouse", "Window" 113 | MouseGetPos &mrX, &mrY 114 | CoordMode "Mouse", "Client" 115 | MouseGetPos &mcX, &mcY 116 | mClr := PixelGetColor(msX, msY, "RGB") 117 | mClr := SubStr(mClr, 3) 118 | 119 | mpText := "Screen:`t" msX ", " msY "`n" 120 | . "Window:`t" mrX ", " mrY "`n" 121 | . "Client:`t" mcX ", " mcY " (default)`n" 122 | . "Color:`t" mClr " (Red=" SubStr(mClr, 1, 2) " Green=" SubStr(mClr, 3, 2) " Blue=" SubStr(mClr, 5) ")" 123 | 124 | UpdateText("Ctrl_MousePos", mpText) 125 | 126 | wX := "", wY := "", wW := "", wH := "" 127 | WinGetPos &wX, &wY, &wW, &wH, "ahk_id " curWin 128 | WinGetClientPos(&wcX, &wcY, &wcW, &wcH, "ahk_id " curWin) 129 | 130 | wText := "Screen:`tx: " wX "`ty: " wY "`tw: " wW "`th: " wH "`n" 131 | . "Client:`tx: " wcX "`ty: " wcY "`tw: " wcW "`th: " wcH 132 | 133 | UpdateText("Ctrl_Pos", wText) 134 | sbTxt := "" 135 | 136 | Loop { 137 | ovi := "" 138 | Try ovi := StatusBarGetText(A_Index) 139 | if (ovi = "") 140 | break 141 | sbTxt .= "(" A_Index "):`t" textMangle(ovi) "`n" 142 | } 143 | 144 | sbTxt := SubStr(sbTxt, 1, -1) ; StringTrimRight, sbTxt, sbTxt, 1 145 | UpdateText("Ctrl_SBText", sbTxt) 146 | bSlow := oGui["Ctrl_IsSlow"].Value ; GuiControlGet, bSlow,, Ctrl_IsSlow 147 | 148 | if (bSlow) { 149 | DetectHiddenText False 150 | ovVisText := WinGetText() ; WinGetText, ovVisText 151 | DetectHiddenText True 152 | ovAllText := WinGetText() ; WinGetText, ovAllText 153 | } else { 154 | ovVisText := WinGetTextFast(false) 155 | ovAllText := WinGetTextFast(true) 156 | } 157 | 158 | UpdateText("Ctrl_VisText", ovVisText) 159 | UpdateText("Ctrl_AllText", ovAllText) 160 | } 161 | 162 | ; =========================================================================================== 163 | ; WinGetText ALWAYS uses the "slow" mode - TitleMatchMode only affects 164 | ; WinText/ExcludeText parameters. In "fast" mode, GetWindowText() is used 165 | ; to retrieve the text of each control. 166 | ; =========================================================================================== 167 | WinGetTextFast(detect_hidden) { 168 | controls := WinGetControlsHwnd() 169 | 170 | static WINDOW_TEXT_SIZE := 32767 ; Defined in AutoHotkey source. 171 | 172 | buf := Buffer(WINDOW_TEXT_SIZE * 2, 0) 173 | 174 | text := "" 175 | 176 | Loop controls.Length { 177 | hCtl := controls[A_Index] 178 | if !detect_hidden && !DllCall("IsWindowVisible", "ptr", hCtl) 179 | continue 180 | if !DllCall("GetWindowText", "ptr", hCtl, "Ptr", buf.ptr, "int", WINDOW_TEXT_SIZE) 181 | continue 182 | 183 | text .= StrGet(buf) "`r`n" ; text .= buf "`r`n" 184 | } 185 | return text 186 | } 187 | 188 | ; =========================================================================================== 189 | ; Unlike using a pure GuiControl, this function causes the text of the 190 | ; controls to be updated only when the text has changed, preventing periodic 191 | ; flickering (especially on older systems). 192 | ; =========================================================================================== 193 | UpdateText(vCtl, NewText) { 194 | Global oGui 195 | static OldText := {} 196 | ctl := oGui[vCtl], hCtl := Integer(ctl.hwnd) 197 | 198 | if (!oldText.HasProp(hCtl) Or OldText.%hCtl% != NewText) { 199 | ctl.Value := NewText 200 | OldText.%hCtl% := NewText 201 | } 202 | } 203 | 204 | textMangle(x) { 205 | elli := false 206 | if (pos := InStr(x, "`n")) 207 | x := SubStr(x, 1, pos - 1), elli := true 208 | else if (StrLen(x) > 40) 209 | x := SubStr(x, 1, 40), elli := true 210 | if elli 211 | x .= " (...)" 212 | return x 213 | } 214 | 215 | suspend_timer() { 216 | Global oGui 217 | SetTimer Update, 0 218 | UpdateText("Ctrl_Freeze", oGui.txtFrozen) 219 | } 220 | 221 | ~*Shift:: 222 | ~*Ctrl:: suspend_timer() 223 | 224 | ~*Ctrl up:: 225 | ~*Shift up:: SetTimer Update, 250 -------------------------------------------------------------------------------- /bin/font/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/font/font.ttf -------------------------------------------------------------------------------- /bin/html/AlignText.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 对齐代码注释 9 | 10 | 11 | 18 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 |

44 | Before 45 |

46 | 48 |
49 |
50 |
51 |
52 |

53 | After 54 |

55 | 57 |
58 |
59 |
60 |
61 |
62 |

63 | 分隔符 64 |

65 |
66 | 68 |
69 |
70 |
71 |
72 | 73 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /bin/html/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/html/logo.ico -------------------------------------------------------------------------------- /bin/icons/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/icons/logo.ico -------------------------------------------------------------------------------- /bin/icons/logo2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/icons/logo2.ico -------------------------------------------------------------------------------- /bin/icons/logo3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/icons/logo3.ico -------------------------------------------------------------------------------- /bin/lib/InputTipWindow.ahk: -------------------------------------------------------------------------------- 1 | Class InputTipWindow { 2 | 3 | __New(text := " ", fontSize := 12, marginX := 2, marginY := 2, offsetX := 9, offsetY := 7) { 4 | ; 字体颜色 5 | FontColor := "000000" 6 | ; 背景颜色 7 | BackColor := "ffffe0" 8 | 9 | this.gui := Gui("+Owner +ToolWindow +Disabled -SysMenu -Caption +E0x1E +AlwaysOnTop +Border") 10 | this.gui.BackColor := BackColor 11 | 12 | this.gui.MarginX := marginX 13 | this.gui.MarginY := marginY 14 | this.gui.SetFont("c" FontColor " s" fontSize, "Microsoft YaHei UI") 15 | 16 | this.data := text 17 | this.textCon := this.gui.Add("text", "Center", this.data) 18 | this.offsetX := offsetX 19 | this.offsetY := offsetY 20 | FrameShadow(this.gui.Hwnd) 21 | } 22 | 23 | Show(text := "", addition := false) { 24 | ; 注意 ahk 中 "0" == 0 所以 if ("0") 比较危险, 推荐和空字符串 "" 作比较 25 | if (text != "") { 26 | if (addition) { 27 | this.data := this.data text 28 | } else { 29 | this.data := text 30 | } 31 | } 32 | 33 | this.ShowWindow() 34 | } 35 | 36 | ShowWindow() { 37 | this.textCon.Value := this.data 38 | GetPosRelativeScreen(&xpos, &ypos, "Mouse") 39 | xpos += this.offsetX 40 | ypos += this.offsetY 41 | this.gui.Show("AutoSize Center NoActivate x" xpos " y" ypos) 42 | WinSetAlwaysOnTop(true, "ahk_id " this.gui.Hwnd) 43 | } 44 | 45 | Backspace() { 46 | ; 空字符串也不会报错, SubStr 很强 47 | this.data := SubStr(this.textCon.Value, 1, -1) 48 | this.ShowWindow() 49 | } 50 | 51 | Hide() { 52 | this.gui.Hide() 53 | } 54 | } 55 | 56 | ; https://www.autohotkey.com/boards/viewtopic.php?t=29117 57 | FrameShadow(handle) { 58 | DllCall("dwmapi\DwmIsCompositionEnabled", "Int*", &enabled := false) ; Get if DWM Manager is Enabled 59 | if !enabled { 60 | return 61 | } 62 | margin := Buffer(16) 63 | NumPut("UInt", 1 64 | , "UInt", 1 65 | , "UInt", 1 66 | , "UInt", 1 67 | , margin) 68 | DllCall("dwmapi\DwmSetWindowAttribute", "Ptr", handle, "UInt", 2, "Int*", 2, "UInt", 4) 69 | DllCall("dwmapi\DwmExtendFrameIntoClientArea", "Ptr", handle, "Ptr", margin) 70 | } -------------------------------------------------------------------------------- /bin/lib/Utils.ahk: -------------------------------------------------------------------------------- 1 | /** 2 | * 自动关闭的提示窗口 3 | * @param message 要提示的文本 4 | * @param {number} time 超时后关闭 5 | */ 6 | Tip(message, time := -1500) { 7 | ToolTip(message) 8 | SetTimer(() => ToolTip(), time) 9 | } 10 | 11 | /** 12 | * 禁用输入法 13 | * @param hwnd 14 | */ 15 | DisableIME(hwnd) { 16 | controlName := ControlGetFocus(hwnd) 17 | controlHwnd := ControlGetHwnd(controlName) 18 | DllCall("Imm32\ImmAssociateContext", "ptr", controlHwnd, "ptr", 0, "ptr") 19 | } 20 | 21 | /** 22 | * 获取光标的位置 23 | * 来源:https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/e5e1666d016c219dc46e7fc97f2bcbf40a9c0da5/AHK_V2/Misc.ahk#L328 GetCaretPos 方法 24 | * @param X 光标相对于屏幕X轴的位置 25 | * @param Y 光标相对于屏幕Y轴的位置 26 | * @param W 光标的宽度 27 | * @param H 光标的高度 28 | * @returns {void} 29 | */ 30 | GetCaretPos(&X?, &Y?, &W?, &H?) { 31 | ; UIA2 caret 32 | static IUIA := ComObject("{e22ad333-b25f-460c-83d0-0581107395c9}", "{34723aff-0c9d-49d0-9896-7ab52df8cd8a}") 33 | try { 34 | ComCall(8, IUIA, "ptr*", &FocusedEl := 0) ; GetFocusedElement 35 | rect := Buffer(16) 36 | ComCall(43, FocusedEl, "ptr", rect) ; CurrentBoundingRectangle 37 | left := NumGet(rect, 0, "int") 38 | top := NumGet(rect, 4, "int") 39 | right := NumGet(rect, 8, "int") 40 | bottom := NumGet(rect, 12, "int") 41 | ComCall(16, FocusedEl, "int", 10024, "ptr*", &patternObject := 0), ObjRelease(FocusedEl) ; GetCurrentPattern. TextPatternElement2 = 10024 42 | if patternObject { 43 | ComCall(10, patternObject, "int*", &IsActive := 1, "ptr*", &caretRange := 0), ObjRelease(patternObject) ; GetCaretRange 44 | ComCall(10, caretRange, "ptr*", &boundingRects := 0), ObjRelease(caretRange) ; GetBoundingRectangles 45 | if (Rect := ComValue(0x2005, boundingRects)).MaxIndex() = 3 { ; VT_ARRAY | VT_R8 46 | X := Round(Rect[0]), Y := Round(Rect[1]), W := Round(Rect[2]), H := Round(Rect[3]) 47 | return "uia" 48 | } 49 | } 50 | } 51 | 52 | ; Acc caret 53 | static _ := DllCall("LoadLibrary", "Str", "oleacc", "Ptr") 54 | try { 55 | idObject := 0xFFFFFFF8 ; OBJID_CARET 56 | if DllCall("oleacc\AccessibleObjectFromWindow", "ptr", WinExist("A"), "uint", idObject &= 0xFFFFFFFF 57 | , "ptr", -16 + NumPut("int64", idObject == 0xFFFFFFF0 ? 0x46000000000000C0 : 0x719B3800AA000C81, NumPut("int64", idObject == 0xFFFFFFF0 ? 0x0000000000020400 : 0x11CF3C3D618736E0, IID := Buffer(16))) 58 | , "ptr*", oAcc := ComValue(9, 0)) = 0 { 59 | x := Buffer(4), y := Buffer(4), w := Buffer(4), h := Buffer(4) 60 | oAcc.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), 0) 61 | X := NumGet(x, 0, "int"), Y := NumGet(y, 0, "int"), W := NumGet(w, 0, "int"), H := NumGet(h, 0, "int") 62 | if (X | Y) != 0 63 | return "acc" 64 | } 65 | } 66 | 67 | ; ahk caret 68 | savedCaret := A_CoordModeCaret 69 | CoordMode "Caret", "Screen" 70 | ok := CaretGetPos(&X, &Y) 71 | CoordMode "Caret", savedCaret 72 | if ok { 73 | return "ahk" 74 | } 75 | 76 | ; uia focused element 77 | X := left + (right - left) / 2 78 | Y := top + (bottom - top) / 2 79 | return "uia focused element" 80 | } 81 | 82 | /** 83 | * 获取指定目标相对于屏幕的位置 84 | * @param X 光标相对于屏幕X轴的位置 85 | * @param Y 光标相对于屏幕Y轴的位置 86 | * @param type Caret | Mouse 87 | */ 88 | GetPosRelativeScreen(&X, &Y, type) { 89 | saved := type == "Caret" ? A_CoordModeCaret : A_CoordModeMouse 90 | CoordMode type, "Screen" 91 | type == "Caret" ? CaretGetPos(&X, &Y) : MouseGetPos(&X, &Y) 92 | CoordMode type, saved 93 | } -------------------------------------------------------------------------------- /bin/lib/translation.ahk: -------------------------------------------------------------------------------- 1 | class DefaultTranslation { 2 | mykeymap_on := "🚀 MyKeymap: On " 3 | mykeymap_off := "⏸️ MyKeymap: Off " 4 | 5 | menu_pause := "Pause" 6 | menu_exit := "Exit" 7 | menu_reload := "Reload" 8 | menu_settings := "Settings" 9 | menu_window_spy := "Window Spy" 10 | 11 | no_items_selected := "no items selected" 12 | always_on_top_on := "Always-on-top: On" 13 | always_on_top_off := "Always-on-top: Off" 14 | copy_failed := " Copy: fail " 15 | copy_ok := " Copy: ok " 16 | mute_on := "Mute: On" 17 | mute_off := "Mute: Off" 18 | mute_falied := "Cannot mute this app" 19 | } 20 | 21 | class ChineseTranslation extends DefaultTranslation { 22 | mykeymap_on := "🚀 恢复 MyKeymap " 23 | mykeymap_off := "⏸️ 暂停 MyKeymap " 24 | 25 | menu_pause := "暂停" 26 | menu_exit := "退出" 27 | menu_reload := "重启程序" 28 | menu_settings := "打开设置" 29 | menu_window_spy := "查看窗口标识符" 30 | 31 | no_items_selected := "没有选中的文本或文件" 32 | always_on_top_on := "置顶当前窗口" 33 | always_on_top_off := "取消置顶" 34 | copy_failed := "复制失败" 35 | copy_ok := "复制成功" 36 | mute_on := "静音当前应用" 37 | mute_off := "取消静音" 38 | mute_falied := "无法静音此应用" 39 | } 40 | 41 | 42 | Translation() { 43 | static t := SysLangIsChinese() ? ChineseTranslation() : DefaultTranslation() 44 | return t 45 | } 46 | 47 | SysLangIsChinese() 48 | { 49 | ; https://www.autohotkey.com/docs/v2/misc/Languages.htm 50 | m := Map( 51 | "7804", "Chinese", ; zh 52 | "0004", "Chinese (Simplified)", ; zh-Hans 53 | "0804", "Chinese (Simplified, China)", ; zh-CN 54 | "1004", "Chinese (Simplified, Singapore)", ; zh-SG 55 | "7C04", "Chinese (Traditional)", ; zh-Hant 56 | "0C04", "Chinese (Traditional, Hong Kong SAR)", ; zh-HK 57 | "1404", "Chinese (Traditional, Macao SAR)", ; zh-MO 58 | "0404", "Chinese (Traditional, Taiwan)", ; zh-TW 59 | ) 60 | return m.Get(A_Language, false) 61 | } -------------------------------------------------------------------------------- /bin/msvcp140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/msvcp140.dll -------------------------------------------------------------------------------- /bin/sound/cancel.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/sound/cancel.wav -------------------------------------------------------------------------------- /bin/sound/keydown.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/sound/keydown.wav -------------------------------------------------------------------------------- /bin/sound/show.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/sound/show.wav -------------------------------------------------------------------------------- /bin/sound/spaceKey.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/sound/spaceKey.wav -------------------------------------------------------------------------------- /bin/vcruntime140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/vcruntime140.dll -------------------------------------------------------------------------------- /bin/vcruntime140_1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/bin/vcruntime140_1.dll -------------------------------------------------------------------------------- /build_tools.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | func main() { 19 | m := map[string]func(args []string){ 20 | "checkForAHKUpdate": checkForAHKUpdate, 21 | "updateShareLink": updateShareLink, 22 | } 23 | if f, ok := m[os.Args[1]]; ok { 24 | f(os.Args[2:]) 25 | } 26 | } 27 | 28 | func checkForAHKUpdate(args []string) { 29 | stop := StartTimer("check for AHK update:") 30 | defer stop() 31 | currentVersion := args[0] 32 | version, err := getURL("https://www.autohotkey.com/download/2.0/version.txt") 33 | if err != nil { 34 | panic(err) 35 | } 36 | if version != currentVersion { 37 | fmt.Println("error: outdated ahk version") 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | func updateShareLink(args []string) { 43 | f, err := os.Open("share_link.json") 44 | if err != nil { 45 | panic(err) 46 | } 47 | defer f.Close() 48 | 49 | var sl ShareLink 50 | if err = Decode(f, &sl); err != nil { 51 | panic(err) 52 | } 53 | 54 | var format string 55 | replacer := func(line string) string { 56 | if strings.Index(line, "提取码") != -1 { 57 | return fmt.Sprintf(format, args[0], sl.Url, sl.Password) 58 | } 59 | return line 60 | } 61 | 62 | format = "- [MyKeymap %s](%s) ( 提取码 %s )" 63 | if err = ReplaceInFile("./readme.md", replacer); err != nil { 64 | panic(err) 65 | } 66 | 67 | format = "- 下载地址: [MyKeymap %s](%s) ( 提取码 %s )" 68 | _ = ReplaceInFile("/mnt/d/project/my_site/docs/MyKeymap.md", replacer) 69 | } 70 | 71 | func execCmd(exe string, args ...string) { 72 | var c = exec.Command(exe, args...) 73 | c.Stdout = os.Stdout 74 | c.Stderr = os.Stderr 75 | fmt.Printf("\n%s %s\n", exe, strings.Join(args, " ")) 76 | err := c.Start() 77 | if err != nil { 78 | panic(err) 79 | } 80 | err = c.Wait() 81 | if err != nil { 82 | panic(err) 83 | } 84 | } 85 | 86 | func getURL(url string) (string, error) { 87 | client := &http.Client{ 88 | Timeout: 5 * time.Second, 89 | } 90 | 91 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 92 | if err != nil { 93 | return "", err 94 | } 95 | 96 | resp, err := client.Do(req) 97 | if err != nil { 98 | return "", err 99 | } 100 | 101 | defer resp.Body.Close() 102 | body, err := io.ReadAll(resp.Body) 103 | return string(body), err 104 | } 105 | 106 | func CreateTempFile() (*os.File, func(), error) { 107 | f, err := os.CreateTemp(".", "tmp.*.txt") 108 | 109 | cleanup := func() { 110 | _ = os.Remove(f.Name()) 111 | } 112 | 113 | return f, cleanup, err 114 | } 115 | 116 | type File struct { 117 | closed bool 118 | *os.File 119 | } 120 | 121 | func (m *File) Close(err *error) { 122 | // 确保只调用一次 Close 123 | if m.closed { 124 | return 125 | } 126 | closeErr := m.File.Close() 127 | m.closed = true 128 | 129 | // 如果之前没有发生错误, 并且 Close() 时发生了错误, 那么修改返回的错误值 130 | if *err == nil { 131 | *err = closeErr 132 | } 133 | } 134 | 135 | func ReplaceInFile(filename string, handler func(string) string) (err error) { 136 | // 打开目标文件 137 | f, err := os.Open(filename) 138 | if err != nil { 139 | return err 140 | } 141 | mf := File{File: f} 142 | defer mf.Close(&err) 143 | 144 | // 创建临时文件, 用于写入 145 | f, cleanup, err := CreateTempFile() 146 | if err != nil { 147 | return err 148 | } 149 | defer cleanup() 150 | wf := File{File: f} 151 | defer wf.Close(&err) 152 | 153 | // 按行读取时, 默认 buffer 大小为 64K, 如果某行会超出 64K, 需要设置更大的 buffer 154 | // 参考此处: https://stackoverflow.com/a/16615559 155 | scanner := bufio.NewScanner(mf) 156 | writer := bufio.NewWriter(wf) 157 | for scanner.Scan() { 158 | line := scanner.Text() 159 | if _, err = writer.WriteString(handler(line) + "\n"); err != nil { 160 | return err 161 | } 162 | } 163 | 164 | // Scan 方法会在遇到错误时返回 false, 所以别忘了检查错误 165 | if err = scanner.Err(); err != nil { 166 | return err 167 | } 168 | 169 | if err = writer.Flush(); err != nil { 170 | return err 171 | } 172 | 173 | // 提前关闭两个文件 ( 因为之后要 Rename ) 174 | mf.Close(&err) 175 | if err != nil { 176 | return err 177 | } 178 | wf.Close(&err) 179 | if err != nil { 180 | return err 181 | } 182 | return os.Rename(wf.Name(), mf.Name()) 183 | } 184 | 185 | func StartTimer(name string) func() { 186 | t := time.Now() 187 | log.Println(name, "started") 188 | return func() { 189 | d := time.Now().Sub(t) 190 | log.Println(name, "took", d) 191 | } 192 | } 193 | 194 | type Valid interface { 195 | OK() error 196 | } 197 | 198 | type ShareLink struct { 199 | Url string `json:"url"` 200 | Password string `json:"password"` 201 | } 202 | 203 | // OK 实现了 Valid 接口, 这样在 Decode 时顺便校验数据是否合法 204 | func (s ShareLink) OK() error { 205 | if s.Url == "" { 206 | return errors.New("url required") 207 | } 208 | if s.Password == "" { 209 | return errors.New("password required") 210 | } 211 | return nil 212 | } 213 | 214 | func Decode[T any](r io.Reader, v *T) error { 215 | err := json.NewDecoder(r).Decode(v) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | obj, ok := any(v).(Valid) // 注意 *T 是一个具体类型 ( 比如 *int ) 所以不能用 type assertion 221 | if !ok { 222 | return nil // 类型没有 OK 方法, 所以直接返回, 不执行 OK 方法 223 | } 224 | return obj.OK() 225 | } 226 | -------------------------------------------------------------------------------- /config-server/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ -------------------------------------------------------------------------------- /config-server/cmd/settings/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/gin-contrib/cors" 7 | "github.com/gin-contrib/static" 8 | "github.com/gin-gonic/gin" 9 | "io" 10 | "log" 11 | "net" 12 | "net/http" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "settings/internal/command" 17 | "settings/internal/matrix" 18 | "settings/internal/script" 19 | "text/template" 20 | "time" 21 | ) 22 | 23 | func main() { 24 | 25 | if len(os.Args) >= 2 { 26 | if handler, ok := command.Map[os.Args[1]]; ok { 27 | handler(os.Args[2:]...) 28 | return 29 | } 30 | } 31 | 32 | hasError := make(chan struct{}) 33 | rainDone := make(chan struct{}) 34 | debug := len(os.Args) == 2 && os.Args[1] == "debug" 35 | 36 | if !debug { 37 | go matrix.DigitalRain(hasError, rainDone) 38 | } 39 | if debug { 40 | hasError = nil 41 | } 42 | 43 | execCmd("./MyKeymap.exe", "/script", "./bin/MiscTools.ahk", "GenerateShortcuts") 44 | server(hasError, rainDone, debug) 45 | } 46 | 47 | func server(hasError chan<- struct{}, rainDone <-chan struct{}, debug bool) { 48 | if !debug { 49 | gin.SetMode(gin.ReleaseMode) 50 | gin.DefaultWriter = io.Discard 51 | } 52 | 53 | router := gin.Default() 54 | router.Use(PanicHandler(hasError, rainDone)) 55 | if debug { 56 | router.Use(cors.Default()) 57 | } 58 | router.NoRoute(static.Serve("/", static.LocalFile("./site", false)), indexHandler) 59 | 60 | router.GET("/", indexHandler) 61 | router.GET("/config", GetConfigHandler) 62 | router.PUT("/config", SaveConfigHandler(debug)) 63 | router.POST("/server/command/:id", ServerCommandHandler) 64 | router.GET("/shortcuts", GetShortcutsHandler) 65 | 66 | // 先尝试 12333 端口, 失败了则用随机端口. 因为 12333 端口可能已被占用, 或者被禁: 67 | // An attempt was made to access a socket in a way forbidden by its access permissions. 68 | ln, err := net.Listen("tcp", "localhost:12333") 69 | if err != nil { 70 | ln, err = net.Listen("tcp", "localhost:0") 71 | if err != nil { 72 | close(hasError) 73 | <-rainDone 74 | fmt.Println("Error:", err.Error()) 75 | _, _ = fmt.Scanln() 76 | os.Exit(1) 77 | } 78 | } 79 | 80 | if !debug { 81 | go func() { 82 | err := openBrowser(ln.Addr()) 83 | if err != nil { 84 | hasError <- struct{}{} 85 | <-rainDone 86 | fmt.Println("Error:", err.Error()) 87 | } 88 | }() 89 | } 90 | 91 | err = router.RunListener(ln) 92 | if err != nil { 93 | close(hasError) 94 | <-rainDone 95 | log.Fatal(err) 96 | } 97 | } 98 | 99 | //goland:noinspection HttpUrlsUsage 100 | func openBrowser(addr net.Addr) error { 101 | time.Sleep(600 * time.Millisecond) 102 | if addr, ok := addr.(*net.TCPAddr); ok { 103 | // _ = exec.Command("cmd", "/c", "start", fmt.Sprintf("http://localhost:%d", addr.Port)).Start() 104 | return exec.Command("C:\\Windows\\System32\\rundll32.exe", "url.dll,FileProtocolHandler", fmt.Sprintf("http://localhost:%d", addr.Port)).Start() 105 | } 106 | return errors.New("addr is not tcp") 107 | } 108 | 109 | func indexHandler(c *gin.Context) { 110 | data, err := os.ReadFile("./site/index.html") 111 | if err != nil { 112 | panic(err) 113 | } 114 | // 设置 Cache-Control: no-store 禁用缓存 115 | c.Header("Cache-Control", "no-store") 116 | c.Data(http.StatusOK, "text/html; charset=utf-8", data) 117 | } 118 | 119 | func GetConfigHandler(c *gin.Context) { 120 | config, err := script.ParseConfig("../data/config.json") 121 | if err != nil { 122 | panic(err) 123 | } 124 | c.JSON(http.StatusOK, config) 125 | } 126 | 127 | func GetShortcutsHandler(c *gin.Context) { 128 | type shortcut struct { 129 | Path string `json:"path"` 130 | } 131 | exe, err := os.Executable() 132 | if err != nil { 133 | panic(err) 134 | } 135 | root := filepath.Dir(filepath.Dir(exe)) 136 | pattern := filepath.Join(root, "shortcuts", "*.lnk") 137 | 138 | files, err := filepath.Glob(pattern) 139 | if err != nil { 140 | panic(err) 141 | } 142 | var data []shortcut 143 | for _, f := range files { 144 | data = append(data, shortcut{ 145 | Path: f[len(root)+1:], 146 | }) 147 | } 148 | c.JSON(http.StatusOK, data) 149 | } 150 | 151 | func PanicHandler(hasError chan<- struct{}, rainDone <-chan struct{}) gin.HandlerFunc { 152 | return func(c *gin.Context) { 153 | defer func() { 154 | if err := recover(); err != nil { 155 | // 不允许重复 close channel 156 | if hasError != nil { 157 | close(hasError) 158 | <-rainDone 159 | hasError = nil 160 | } 161 | panic(err) 162 | } 163 | }() 164 | 165 | c.Next() 166 | } 167 | } 168 | 169 | func ServerCommandHandler(c *gin.Context) { 170 | m := map[string]struct { 171 | exe string 172 | args []string 173 | }{ 174 | "2": { 175 | exe: "./MyKeymap.exe", 176 | args: []string{"/script", "bin/WindowSpy.ahk"}, 177 | }, 178 | "3": { 179 | exe: "./MyKeymap.exe", 180 | args: []string{"/script", "./bin/MiscTools.ahk", "RunAtStartup", "On"}, 181 | }, 182 | "4": { 183 | exe: "./MyKeymap.exe", 184 | args: []string{"/script", "./bin/MiscTools.ahk", "RunAtStartup", "Off"}, 185 | }, 186 | } 187 | if c, ok := m[c.Param("id")]; ok { 188 | execCmd(c.exe, c.args...) 189 | } 190 | 191 | c.JSON(http.StatusOK, gin.H{}) 192 | } 193 | 194 | func execCmd(exe string, args ...string) { 195 | // 切换到 parent 文件夹, 执行完 command 后再回来 196 | // 程序工作目录算全局共享状态, 所以会影响到其他 goroutine 197 | wd, err := os.Getwd() 198 | if err == nil { 199 | base := filepath.Base(wd) 200 | err := os.Chdir("../") 201 | if err == nil { 202 | defer func() { 203 | _ = os.Chdir(base) 204 | }() 205 | } 206 | } 207 | 208 | var c = exec.Command(exe, args...) 209 | err = c.Start() 210 | if err != nil { 211 | } 212 | } 213 | 214 | func SaveConfigHandler(debug bool) gin.HandlerFunc { 215 | return func(c *gin.Context) { 216 | var config script.Config 217 | if err := c.ShouldBindJSON(&config); err != nil { 218 | panic(err) 219 | } 220 | 221 | // 生成帮助文件 222 | // helpPageHtml := config["helpPageHtml"].(string) 223 | // delete(config, "helpPageHtml") 224 | // saveHelpPageHtml(helpPageHtml) 225 | 226 | script.SaveConfigFile(&config) // 保存配置文件 227 | 228 | if debug { 229 | script.GenerateScripts(&config) // 生成脚本文件 230 | execCmd("./MyKeymap.exe") // 重启程序, 此时 launcher 会重新生成脚本 231 | // execCmd("./MyKeymap.exe", "./bin/MyKeymap.ahk") // 重启程序且跳过 ahk 脚本生成 232 | } else { 233 | execCmd("./MyKeymap.exe") // 重启程序, 此时 launcher 会重新生成脚本 234 | } 235 | 236 | c.JSON(http.StatusOK, gin.H{"message": "ok"}) 237 | } 238 | } 239 | 240 | func saveHelpPageHtml(html string) { 241 | 242 | f, err := os.Create("../bin/site/help.html") 243 | if err != nil { 244 | panic(err) 245 | } 246 | defer func(f *os.File) { 247 | _ = f.Close() 248 | }(f) 249 | 250 | files := []string{ 251 | "./templates/help.html", 252 | } 253 | 254 | ts, err := template.ParseFiles(files...) 255 | if err != nil { 256 | panic(err) 257 | } 258 | err = ts.Execute(f, map[string]string{"helpPageHtml": html}) 259 | if err != nil { 260 | panic(err) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /config-server/go.mod: -------------------------------------------------------------------------------- 1 | module settings 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/gdamore/tcell/v2 v2.8.1 7 | github.com/gin-contrib/cors v1.7.3 8 | github.com/gin-contrib/static v1.1.3 9 | github.com/gin-gonic/gin v1.10.0 10 | ) 11 | 12 | require ( 13 | github.com/bytedance/sonic v1.12.8 // indirect 14 | github.com/bytedance/sonic/loader v0.2.3 // indirect 15 | github.com/cloudwego/base64x v0.1.5 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 17 | github.com/gdamore/encoding v1.0.1 // indirect 18 | github.com/gin-contrib/sse v1.0.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-playground/validator/v10 v10.24.0 // indirect 22 | github.com/goccy/go-json v0.10.5 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 25 | github.com/kr/text v0.2.0 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/mattn/go-runewidth v0.0.16 // indirect 30 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 31 | github.com/modern-go/reflect2 v1.0.2 // indirect 32 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 33 | github.com/rivo/uniseg v0.4.7 // indirect 34 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 35 | github.com/ugorji/go/codec v1.2.12 // indirect 36 | golang.org/x/arch v0.14.0 // indirect 37 | golang.org/x/crypto v0.33.0 // indirect 38 | golang.org/x/net v0.34.0 // indirect 39 | golang.org/x/sys v0.30.0 // indirect 40 | golang.org/x/term v0.29.0 // indirect 41 | golang.org/x/text v0.22.0 // indirect 42 | google.golang.org/protobuf v1.36.5 // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /config-server/internal/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "settings/internal/script" 12 | "strings" 13 | "text/template" 14 | ) 15 | 16 | var Map = map[string]func(args ...string){ 17 | "AlignText": AlignText, 18 | "GenerateAHK": GenerateAHK, 19 | "ChangeVersion": ChangeVersion, 20 | "GenerateScripts": GenerateScripts, 21 | "UseOriginalAHK": UseOriginalAHK, 22 | } 23 | 24 | var logger = log.New(os.Stderr, "", 0) 25 | 26 | func GenerateAHK(args ...string) { 27 | if len(os.Args) < 5 { 28 | logger.Fatal("GenerateAHK requires 3 arguments, for example: GenerateAHK ./config.json ./templates/script.ahk ./output.ahk") 29 | } 30 | configFile := os.Args[2] 31 | templateFile := os.Args[3] 32 | outputFile := os.Args[4] 33 | 34 | config, err := script.ParseConfig(configFile) 35 | if err != nil { 36 | logger.Fatal(err) 37 | } 38 | 39 | if err := script.SaveAHK(config, templateFile, outputFile); err != nil { 40 | logger.Fatal(err) 41 | } 42 | } 43 | 44 | func ChangeVersion(args ...string) { 45 | config, err := script.ParseConfig("../data/config.json") 46 | if err != nil { 47 | panic(err) 48 | } 49 | config.Options.MykeymapVersion = args[0] 50 | config.Options.Language = "" // 重置语言 51 | script.SaveConfigFile(config) 52 | script.GenerateScripts(config) 53 | } 54 | 55 | func GenerateScripts(args ...string) { 56 | config, err := script.ParseConfig("../data/config.json") 57 | if err != nil { 58 | panic(err) 59 | } 60 | script.GenerateScripts(config) 61 | } 62 | 63 | func UseOriginalAHK(args ...string) { 64 | defer func() { 65 | fmt.Println() 66 | }() 67 | 68 | exe := "bin\\AutoHotkey64.exe" 69 | if _, err := os.Stat(exe); errors.Is(err, os.ErrNotExist) { 70 | fmt.Println("Error: file", exe, "does not exist") 71 | return 72 | } 73 | if err := execCmd("cmd.exe", "/c", "copy /y "+exe+" MyKeymap.exe"); err != nil { 74 | fmt.Println("\nPlease close MyKeymap and retry") 75 | return 76 | } 77 | if err := execCmd("cmd.exe", "/c", "copy /y bin\\Launcher.ahk MyKeymap.ahk"); err != nil { 78 | panic(err) 79 | return 80 | } 81 | fmt.Println("\ndone!") 82 | } 83 | 84 | func execCmd(exe string, args ...string) error { 85 | cmd := exec.Command(exe, args...) 86 | cmd.Stdout = os.Stdout 87 | cmd.Stderr = os.Stderr 88 | if err := cmd.Start(); err != nil { 89 | return err 90 | } 91 | if err := cmd.Wait(); err != nil { 92 | return err 93 | } 94 | return nil 95 | } 96 | 97 | func AlignText(args ...string) { 98 | text, err := readStdin() 99 | if err != nil { 100 | fmt.Println(err) 101 | return 102 | } 103 | r := strings.NewReplacer("`", "\\`", "\\", "\\\\") 104 | data := map[string]interface{}{ 105 | "Text": r.Replace(text), 106 | } 107 | executeTemplate(data, "./html-tools/AlignText.tmpl.html", "./html-tools/AlignText.html") 108 | } 109 | 110 | func readStdin() (string, error) { 111 | fi, _ := os.Stdin.Stat() 112 | if (fi.Mode() & os.ModeCharDevice) == 0 { 113 | // 这个分支表示 stdin 的数据来自管道 114 | } else { 115 | return "", errors.New("需要从管道读取数据") 116 | } 117 | 118 | bytes, err := ioutil.ReadAll(os.Stdin) 119 | if err != nil { 120 | return "", err 121 | } 122 | // CRLF 转换 123 | r := string(bytes) 124 | if strings.Index(r, "\r\n") != -1 { 125 | r = strings.ReplaceAll(r, "\r\n", "\n") 126 | } 127 | return r, nil 128 | } 129 | 130 | func executeTemplate(data map[string]interface{}, templateFile, outputFile string) { 131 | 132 | f, err := os.Create(outputFile) 133 | if err != nil { 134 | panic(err) 135 | } 136 | defer func(f *os.File) { 137 | _ = f.Close() 138 | }(f) 139 | 140 | files := []string{ 141 | templateFile, 142 | } 143 | 144 | ts, err := template.New(filepath.Base(templateFile)). 145 | Funcs(script.TemplateFuncMap).Delims("{%", "%}").ParseFiles(files...) 146 | if err != nil { 147 | panic(err) 148 | } 149 | 150 | err = ts.Execute(f, data) 151 | if err != nil { 152 | panic(err) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /config-server/internal/matrix/matrix.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "math/rand" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func DigitalRain(hasError <-chan struct{}, rainDone chan<- struct{}) { 12 | defer close(rainDone) 13 | 14 | // init screen 15 | s, _ := tcell.NewScreen() 16 | _ = s.Init() 17 | defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorGreen) 18 | s.SetStyle(defStyle) 19 | s.Clear() 20 | 21 | quit := func() { 22 | s.Fini() 23 | os.Exit(0) 24 | } 25 | 26 | nodes := make([]node, 0, 100) 27 | width, height := s.Size() 28 | columns := initColumns(width, height) 29 | startTime := time.Now() 30 | 31 | outer: 32 | for { 33 | select { 34 | case <-hasError: 35 | s.Fini() 36 | break outer 37 | default: 38 | // 刷新率 39 | time.Sleep(34 * time.Millisecond) 40 | } 41 | 42 | for _, col := range columns { 43 | // 让计时器减一, 计时器为 0 时产生新节点, 并重置计时器 44 | if col.Timer == 0 { 45 | n := col.spawnNode(col.X, height) 46 | nodes = append(nodes, n) 47 | } 48 | col.Timer -= 1 49 | } 50 | 51 | // 绘制节点, 并把节点下移一行 52 | for i := range nodes { 53 | drawNode(&nodes[i], s, defStyle) 54 | nodes[i].Y++ 55 | } 56 | 57 | nodes = filter(nodes, func(n node) bool { 58 | return n.Y < height+1 59 | }) 60 | 61 | // 打印横幅 62 | banner := []string{ 63 | " ------------------------------------------------------------------ ", 64 | " ", 65 | " MyKeymap config server is running... ", 66 | " ", 67 | " ------------------------------------------------------------------ ", 68 | } 69 | style := defStyle.Foreground(tcell.ColorWhite) 70 | for index, line := range banner { 71 | // 按一定速度打印第三行文字 72 | if index == 2 { 73 | text := strings.TrimLeft(line, " ") 74 | prefix := line[:len(line)-len(text)] 75 | distance := time.Since(startTime).Milliseconds() / 50 76 | text2 := text[:distance%int64(len(text))] 77 | suffix := strings.Repeat(" ", len(line)-len(prefix)-len(text2)) 78 | line = prefix + text2 + suffix 79 | } 80 | drawText(s, 24, 5+index, 100, 100, style, line) 81 | } 82 | 83 | // Update screen 84 | s.Show() 85 | 86 | // Poll event 87 | if s.HasPendingEvent() { 88 | ev := s.PollEvent() 89 | 90 | // Process event 91 | switch ev := ev.(type) { 92 | case *tcell.EventResize: 93 | s.Sync() 94 | width, height = s.Size() 95 | columns = initColumns(width, height) 96 | case *tcell.EventKey: 97 | if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC { 98 | quit() 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) { 106 | // To draw text more easily, define a render function. 107 | row := y1 108 | col := x1 109 | for _, r := range []rune(text) { 110 | s.SetContent(col, row, r, nil, style) 111 | col++ 112 | // col 在 x2 处换行 113 | if col >= x2 { 114 | row++ 115 | col = x1 116 | } 117 | // 超出的文本不展示 118 | if row > y2 { 119 | break 120 | } 121 | } 122 | } 123 | 124 | func filter(nodes []node, f func(node) bool) []node { 125 | var result []node 126 | for i := range nodes { 127 | if f(nodes[i]) { 128 | result = append(result, nodes[i]) 129 | } 130 | } 131 | return result 132 | } 133 | 134 | func drawNode(n *node, s tcell.Screen, style tcell.Style) { 135 | 136 | character := ' ' 137 | color := tcell.ColorGreen 138 | 139 | // 1/2 节点是纯绿色, 1/2 节点带彩色, 140 | if n.Type == writer { 141 | character = getChar() 142 | if n.Colored { 143 | color = tcell.ColorYellow 144 | } 145 | } 146 | 147 | s.SetContent(n.X, n.Y, character, nil, style.Foreground(color)) 148 | 149 | if n.Colored { 150 | if n.LastChar != 0 { 151 | // 三分之一的概率是亮绿色 152 | if rand.Intn(100) < 33 { 153 | color = tcell.Color46 154 | } else { 155 | color = tcell.ColorGreen 156 | } 157 | // 彩色节点的上一个格子需要从彩色改回绿色 158 | s.SetContent(n.X, n.Y-1, character, nil, style.Foreground(color)) 159 | } 160 | n.LastChar = character 161 | } 162 | } 163 | 164 | func getChar() rune { 165 | const a = `ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン1234567890` 166 | const b = `1234567890-=*_+|:<>"-=*_+|:<>"-=*_+|:<>"-=*_+|:<>"` 167 | var charSet = []rune(a + b) 168 | return charSet[rand.Intn(len(charSet))] 169 | } 170 | 171 | func initColumns(width, height int) []*column { 172 | columns := make([]*column, 0, 100) 173 | for i := 0; i < width; i += 5 { 174 | col := column{ 175 | Timer: 1 + rand.Intn(height-1), 176 | X: i, 177 | } 178 | columns = append(columns, &col) 179 | } 180 | return columns 181 | } 182 | 183 | func (c *column) spawnNode(x, rowCount int) node { 184 | 185 | nt := eraser 186 | colored := false 187 | c.Timer = 1 + rand.Intn(rowCount-1) 188 | 189 | if c.NextNodeType == writer { 190 | nt = writer 191 | if rand.Intn(2) == 0 { 192 | colored = true 193 | } 194 | c.Timer = 4 + rand.Intn(rowCount-7) 195 | c.NextNodeType = eraser 196 | } else { 197 | c.NextNodeType = writer 198 | } 199 | 200 | return node{ 201 | X: x, 202 | Type: nt, 203 | Colored: colored, 204 | } 205 | } 206 | 207 | type nodeType int 208 | 209 | const ( 210 | writer nodeType = iota 211 | eraser 212 | ) 213 | 214 | type node struct { 215 | X int 216 | Y int 217 | Type nodeType 218 | Colored bool 219 | LastChar rune 220 | } 221 | 222 | type column struct { 223 | X int 224 | Timer int 225 | NextNodeType nodeType 226 | } 227 | -------------------------------------------------------------------------------- /config-server/internal/script/abbr.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | func (c *Config) CapslockAbbr() map[string][]Action { 9 | for _, km := range c.Keymaps { 10 | if km.Hotkey == "capslockAbbr" { 11 | return km.Hotkeys 12 | } 13 | } 14 | return make(map[string][]Action) 15 | } 16 | 17 | func (c *Config) SemicolonAbbr() map[string][]Action { 18 | for _, km := range c.Keymaps { 19 | if km.Hotkey == "semicolonAbbr" { 20 | return km.Hotkeys 21 | } 22 | } 23 | return make(map[string][]Action) 24 | } 25 | 26 | func (c *Config) CapslockAbbrEnabled() bool { 27 | for _, km := range c.Keymaps { 28 | if !km.Enable { 29 | continue 30 | } 31 | for _, actions := range km.Hotkeys { 32 | for _, a := range actions { 33 | if a.TypeID == 9 && a.ValueID == 6 { 34 | return true 35 | } 36 | } 37 | } 38 | } 39 | return false 40 | } 41 | 42 | func (c *Config) CapslockAbbrKeys() string { 43 | var keys []string 44 | for key := range c.CapslockAbbr() { 45 | keys = append(keys, strings.ReplaceAll(key, ",", ",,")) 46 | } 47 | sort.Slice(keys, func(i, j int) bool { 48 | return keys[i] < keys[j] 49 | }) 50 | return strings.Join(keys, ",") 51 | } 52 | 53 | func (c *Config) SemicolonAbbrEnabled() bool { 54 | for _, km := range c.Keymaps { 55 | if !km.Enable { 56 | continue 57 | } 58 | for _, actions := range km.Hotkeys { 59 | for _, a := range actions { 60 | if a.TypeID == 9 && a.ValueID == 5 { 61 | return true 62 | } 63 | } 64 | } 65 | } 66 | return false 67 | } 68 | 69 | func (c *Config) SemicolonAbbrKeys() string { 70 | var keys []string 71 | for key := range c.SemicolonAbbr() { 72 | keys = append(keys, strings.ReplaceAll(key, ",", ",,")) 73 | } 74 | sort.Slice(keys, func(i, j int) bool { 75 | return keys[i] < keys[j] 76 | }) 77 | return strings.Join(keys, ",") 78 | } 79 | -------------------------------------------------------------------------------- /config-server/internal/script/action.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | // Cfg 在 SaveAHK 中初始化, 会被下面的转换函数用到 10 | var Cfg *Config 11 | 12 | const remapKey = 5 13 | 14 | var actionMap = map[int]func(Action, bool) string{ 15 | 1: activateOrRun1, 16 | 2: systemActions2, 17 | 3: windowActions3, 18 | 4: mouseActions4, 19 | remapKey: remapKey5, 20 | 6: sendKeys6, 21 | 7: textFeatures7, 22 | 8: builtinFunctions8, 23 | 9: mykeymapActions9, 24 | } 25 | 26 | func actionToHotkey(action Action) string { 27 | if f, ok := actionMap[action.TypeID]; ok { 28 | return f(action, false) 29 | } 30 | return "" 31 | } 32 | 33 | // TODO 缩写功能中不支持的操作: 34 | // 重映射按键, 锁定当前模式, 暂停重启锁定, 鼠标操作, 窗口操作, .... 35 | // 基本上只支持发送按键, 启动程序, 执行内置函数, 系统控制 36 | func abbrToCode(abbrMap map[string][]Action) string { 37 | // map 结构转成 list, 然后进行排序 38 | type Abbr struct { 39 | abbr string 40 | actions []Action 41 | } 42 | var abbrList []Abbr 43 | for abbr, actions := range abbrMap { 44 | actions = sortActions(actions) 45 | abbrList = append(abbrList, Abbr{abbr, actions}) 46 | } 47 | sort.Slice(abbrList, func(i, j int) bool { 48 | return abbrList[i].abbr < abbrList[j].abbr 49 | }) 50 | 51 | var s strings.Builder 52 | for _, abbr := range abbrList { 53 | s.WriteString(fmt.Sprintf(" case \"%s\":\n", abbr.abbr)) 54 | for _, a := range abbr.actions { 55 | if _, ok := actionMap[a.TypeID]; !ok { 56 | continue 57 | } 58 | 59 | call := actionMap[a.TypeID](a, true) 60 | winTitle, conditionType := Cfg.GetWinTitle(a) 61 | if conditionType == 5 { 62 | winTitle = strings.Trim(winTitle, "'") 63 | } 64 | if a.WindowGroupID == 0 { 65 | s.WriteString(fmt.Sprintf(" %s\n", call)) 66 | continue 67 | } 68 | s.WriteString(fmt.Sprintf(" if matchWinTitleCondition(%s, %d) {\n", winTitle, conditionType)) 69 | s.WriteString(fmt.Sprintf(" %s\n", call)) 70 | s.WriteString(fmt.Sprintf(" return\n")) 71 | s.WriteString(fmt.Sprintf(" }\n")) 72 | } 73 | } 74 | return s.String() 75 | } 76 | 77 | func sortActions(actions []Action) []Action { 78 | // 把 a.WindowGroupID 为 0 的放在最后面, 因为这是默认生效的动作, 优先级最低 79 | res := make([]Action, 0, len(actions)) 80 | var suffix []Action 81 | for _, a := range actions { 82 | if a.WindowGroupID == 0 { 83 | suffix = append(suffix, a) 84 | } else { 85 | res = append(res, a) 86 | } 87 | } 88 | res = append(res, suffix...) 89 | return res 90 | } 91 | 92 | func toAHKFuncArg(val string) string { 93 | prefix := "ahk-expression:" 94 | if strings.HasPrefix(val, prefix) { 95 | val = val[len(prefix):] 96 | return strings.TrimSpace(val) 97 | } 98 | return ahkString(val) 99 | } 100 | 101 | func remapKey5(a Action, inAbbrContext bool) string { 102 | if strings.ToLower(a.Hotkey) == "singlepress" { 103 | a.KeysToSend = "{blind}{" + a.RemapToKey + "}" 104 | return sendKeys6(a, inAbbrContext) 105 | } 106 | key := strings.TrimLeft(a.Hotkey, "*") 107 | ctx := Cfg.GetHotkeyContext(a) 108 | if ctx != "" { 109 | ctx = ctx[2:] 110 | } 111 | f := "RemapKey" 112 | if a.RemapInHotIf { 113 | f = "RemapInHotIf" 114 | } 115 | return fmt.Sprintf(`km.%s("%s", %s%s)`, f, key, toAHKFuncArg(a.RemapToKey), ctx) 116 | } 117 | 118 | func sendKeys6(a Action, inAbbrContext bool) string { 119 | // 有多行, 每行转成一个 send 调用, 然后用逗号 , 连起来 120 | var res []string 121 | lines := strings.Split(a.KeysToSend, "\n") 122 | for _, line := range lines { 123 | if len(strings.TrimSpace(line)) == 0 { 124 | continue 125 | } 126 | if strings.HasPrefix(line, "ahk:") { 127 | res = append(res, strings.TrimSpace(line[4:])) 128 | continue 129 | } 130 | if strings.HasPrefix(line, "sleep ") || strings.HasPrefix(line, "Sleep ") { 131 | res = append(res, fmt.Sprintf(`Sleep(%s)`, line[6:])) 132 | continue 133 | } 134 | line = toAHKFuncArg(line) 135 | res = append(res, fmt.Sprintf(`Send(%s)`, line)) 136 | } 137 | if len(res) == 0 { 138 | return "" 139 | } 140 | 141 | call := strings.Join(res, ", ") 142 | 143 | if inAbbrContext { 144 | return call 145 | } 146 | return fmt.Sprintf(`km.Map("%[1]s", _ => (%s)%s)`, a.Hotkey, call, Cfg.GetHotkeyContext(a)) 147 | } 148 | 149 | func mykeymapActions9(a Action, inAbbrContext bool) string { 150 | ctx := Cfg.GetHotkeyContext(a) 151 | callMap := map[int]string{ 152 | 1: `MyKeymapToggleSuspend()`, 153 | 2: `MyKeymapReload()`, 154 | 3: `MyKeymapExit()`, 155 | 4: `MyKeymapOpenSettings()`, 156 | 5: `EnterSemicolonAbbr(semiHook, semiHookAbbrWindow)`, 157 | 6: `EnterCapslockAbbr(capsHook)`, 158 | 7: `ToggleCapslock()`, 159 | 8: `km.ToggleLock`, 160 | } 161 | 162 | call, ok := callMap[a.ValueID] 163 | if !ok { 164 | return "" 165 | } 166 | 167 | if inAbbrContext { 168 | return call 169 | } 170 | 171 | if a.ValueID == 1 || a.ValueID == 2 { 172 | if ctx == "" { 173 | ctx += ", , , " 174 | } 175 | return fmt.Sprintf(`km.Map("%[1]s", _ => %s%s, "S")`, a.Hotkey, call, ctx) 176 | } 177 | if a.ValueID == 8 { 178 | return fmt.Sprintf(`km.Map("%[1]s", %s%s)`, a.Hotkey, call, ctx) 179 | } 180 | 181 | return fmt.Sprintf(`km.Map("%[1]s", _ => %s%s)`, a.Hotkey, call, ctx) 182 | } 183 | 184 | func mouseActions4(a Action, inAbbrContext bool) string { 185 | winTitle, conditionType := Cfg.GetWinTitle(a) 186 | ctx := Cfg.GetHotkeyContext(a) 187 | valueMap := map[int]string{ 188 | 1: `km.Map("%[1]s", fast.MoveMouseUp, slow%[2]s), slow.Map("%[1]s", slow.MoveMouseUp%[3]s)`, 189 | 2: `km.Map("%[1]s", fast.MoveMouseDown, slow%[2]s), slow.Map("%[1]s", slow.MoveMouseDown%[3]s)`, 190 | 3: `km.Map("%[1]s", fast.MoveMouseLeft, slow%[2]s), slow.Map("%[1]s", slow.MoveMouseLeft%[3]s)`, 191 | 4: `km.Map("%[1]s", fast.MoveMouseRight, slow%[2]s), slow.Map("%[1]s", slow.MoveMouseRight%[3]s)`, 192 | 193 | 5: `km.Map("%[1]s", fast.ScrollWheelUp%s), slow.Map("%[1]s", slow.ScrollWheelUp%s)`, 194 | 6: `km.Map("%[1]s", fast.ScrollWheelDown%s), slow.Map("%[1]s", slow.ScrollWheelDown%s)`, 195 | 7: `km.Map("%[1]s", fast.ScrollWheelLeft%s), slow.Map("%[1]s", slow.ScrollWheelLeft%s)`, 196 | 8: `km.Map("%[1]s", fast.ScrollWheelRight%s), slow.Map("%[1]s", slow.ScrollWheelRight%s)`, 197 | 198 | 9: `km.Map("%[1]s", fast.LButton()%s), slow.Map("%[1]s", slow.LButton()%s)`, 199 | 10: `km.Map("%[1]s", fast.RButton()%s), slow.Map("%[1]s", slow.RButton()%s)`, 200 | 11: `km.Map("%[1]s", fast.MButton()%s), slow.Map("%[1]s", slow.MButton()%s)`, 201 | 12: `km.Map("%[1]s", fast.LButtonDown()%s), slow.Map("%[1]s", slow.LButtonDown()%s)`, 202 | 13: `km.Map("%[1]s", _ => MoveMouseToCaret()%s), slow.Map("%[1]s", _ => MoveMouseToCaret()%s)`, 203 | } 204 | if format, ok := valueMap[a.ValueID]; ok { 205 | if a.ValueID >= 5 { 206 | return fmt.Sprintf(format, a.Hotkey, ctx) 207 | } 208 | fastSuffix := fmt.Sprintf(", %s, %d", winTitle, conditionType) 209 | slowSuffix := fmt.Sprintf(", , %s, %d", winTitle, conditionType) 210 | if winTitle == `""` && conditionType == 0 { 211 | fastSuffix = "" 212 | slowSuffix = "" 213 | } 214 | return fmt.Sprintf(format, a.Hotkey, fastSuffix, slowSuffix) 215 | } 216 | return "" 217 | } 218 | 219 | func windowActions3(a Action, inAbbrContext bool) string { 220 | if a.ValueID == 4 { 221 | return fmt.Sprintf(`km.Map("%[1]s", _ => Send("^!{tab}"), taskSwitch%s)`, a.Hotkey, Cfg.GetHotkeyContext(a)) 222 | } 223 | if a.ValueID == 14 { 224 | return fmt.Sprintf(`km.Map("%[1]s", BindWindow()%s)`, a.Hotkey, Cfg.GetHotkeyContext(a)) 225 | } 226 | callMap := map[int]string{ 227 | 1: `SmartCloseWindow()`, 228 | 2: `GoToLastWindow()`, 229 | 3: `LoopRelatedWindows()`, 230 | 5: `GoToPreviousVirtualDesktop()`, 231 | 6: `GoToNextVirtualDesktop()`, 232 | 7: `MoveWindowToNextMonitor()`, 233 | 8: `MinimizeWindow()`, 234 | 9: `MaximizeWindow()`, 235 | 10: `CenterAndResizeWindow(1200, 800)`, 236 | 11: `CenterAndResizeWindow(1370, 930)`, 237 | 12: `ToggleWindowTopMost()`, 238 | 13: `MakeWindowDraggable()`, 239 | 15: `CloseWindowProcesses()`, 240 | 16: `CloseSameClassWindows()`, 241 | } 242 | if call, ok := callMap[a.ValueID]; ok { 243 | if inAbbrContext { 244 | return call 245 | } 246 | return fmt.Sprintf(`km.Map("%[1]s", _ => %s%s)`, a.Hotkey, call, Cfg.GetHotkeyContext(a)) 247 | } 248 | return "" 249 | } 250 | 251 | func systemActions2(a Action, inAbbrContext bool) string { 252 | callMap := map[int]string{ 253 | 1: `SystemLockScreen()`, 254 | 2: `SystemSleep()`, 255 | 3: `SystemShutdown()`, 256 | 4: `SystemReboot()`, 257 | 5: `SoundControl()`, 258 | 6: `BrightnessControl()`, 259 | 7: `SystemRestartExplorer()`, 260 | 8: `CopySelectedAsPlainText()`, 261 | 9: `MuteActiveApp()`, 262 | 10: `ShowActiveProcessInFolder()`, 263 | } 264 | if call, ok := callMap[a.ValueID]; ok { 265 | if inAbbrContext { 266 | return call 267 | } 268 | return fmt.Sprintf(`km.Map("%[1]s", _ => %s%s)`, a.Hotkey, call, Cfg.GetHotkeyContext(a)) 269 | } 270 | return "" 271 | } 272 | 273 | func activateOrRun1(a Action, inAbbrContext bool) string { 274 | ctx := Cfg.GetHotkeyContext(a) 275 | winTitle := toAHKFuncArg(a.WinTitle) 276 | target := toAHKFuncArg(a.Target) 277 | args := toAHKFuncArg(a.Args) 278 | workingDir := toAHKFuncArg(a.WorkingDir) 279 | 280 | call := fmt.Sprintf(`ActivateOrRun(%s, %s, %s, %s, %t, %t, %t)`, winTitle, target, args, workingDir, a.RunAsAdmin, a.DetectHiddenWindow, a.RunInBackground) 281 | if args == `""` && workingDir == `""` && !a.RunAsAdmin && !a.DetectHiddenWindow && !a.RunInBackground { 282 | call = fmt.Sprintf(`ActivateOrRun(%s, %s)`, winTitle, target) 283 | } 284 | 285 | if inAbbrContext { 286 | return call 287 | } 288 | 289 | return fmt.Sprintf(`km.Map("%[1]s", _ => %s%s)`, a.Hotkey, call, ctx) 290 | } 291 | 292 | func builtinFunctions8(a Action, inAbbrContext bool) string { 293 | if inAbbrContext { 294 | return a.AHKCode 295 | } 296 | return fmt.Sprintf(`km.Map("%[1]s", _ => %s%s)`, a.Hotkey, a.AHKCode, Cfg.GetHotkeyContext(a)) 297 | } 298 | 299 | func textFeatures7(a Action, inAbbrContext bool) string { 300 | 301 | m := map[int]struct { 302 | Type string 303 | Value string 304 | }{ 305 | 1: {"remap", "up"}, 306 | 2: {"remap", "down"}, 307 | 3: {"remap", "left"}, 308 | 4: {"remap", "right"}, 309 | 5: {"remap", "home"}, 310 | 6: {"remap", "end"}, 311 | 17: {"remap", "appskey"}, 312 | 20: {"remap", "esc"}, 313 | 21: {"remap", "backspace"}, 314 | 23: {"remap", "delete"}, 315 | 24: {"remap", "insert"}, 316 | 25: {"remap", "tab"}, 317 | 318 | 7: {"send", "{blind}^{left}"}, 319 | 8: {"send", "{blind}^{right}"}, 320 | 9: {"send", "{blind}+{up}"}, 321 | 10: {"send", "{blind}+{down}"}, 322 | 11: {"send", "{blind}+{left}"}, 323 | 12: {"send", "{blind}+{right}"}, 324 | 13: {"send", "{blind}+{home}"}, 325 | 14: {"send", "{blind}+{end}"}, 326 | 15: {"send", "^+{left}"}, 327 | 16: {"send", "^+{right}"}, 328 | 18: {"send", "^{backspace}"}, 329 | 33: {"send", "{home}+{end}{backspace}"}, 330 | 22: {"send", "{blind}{enter}"}, 331 | 26: {"send", "^{tab}"}, 332 | 27: {"send", "{blind}+{tab}"}, 333 | 28: {"send", "^+{tab}"}, 334 | } 335 | 336 | if item, ok := m[a.ValueID]; ok { 337 | if item.Type == "remap" { 338 | a.RemapToKey = item.Value 339 | return remapKey5(a, inAbbrContext) 340 | } 341 | if item.Type == "send" { 342 | a.KeysToSend = item.Value 343 | return sendKeys6(a, inAbbrContext) 344 | } 345 | } 346 | 347 | callMap := map[int]string{ 348 | 19: `HoldDownModifierKey("LShift")`, 349 | 29: `InsertSpaceBetweenZHAndEn()`, 350 | 30: `HoldDownModifierKey("LCtrl")`, 351 | 31: `HoldDownModifierKey("LAlt")`, 352 | 32: `HoldDownModifierKey("LWin")`, 353 | } 354 | if call, ok := callMap[a.ValueID]; ok { 355 | if inAbbrContext { 356 | return call 357 | } 358 | return fmt.Sprintf(`km.Map("%[1]s", _ => %s%s)`, a.Hotkey, call, Cfg.GetHotkeyContext(a)) 359 | } 360 | return "" 361 | } 362 | -------------------------------------------------------------------------------- /config-server/internal/script/config.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | // 刚刚发现 GoLand 可以直接把 JSON 字符串粘贴为「 结构体定义 」 一下省掉了好多工作 13 | 14 | var MykeymapVersion string 15 | 16 | type Config struct { 17 | Keymaps []Keymap `json:"keymaps,omitempty"` 18 | Options Options `json:"options,omitempty"` 19 | KeyMapping string `json:"-"` 20 | } 21 | 22 | type Keymap struct { 23 | ID int `json:"id"` 24 | Name string `json:"name"` 25 | Enable bool `json:"enable"` 26 | Hotkey string `json:"hotkey"` 27 | ParentID int `json:"parentID"` 28 | Delay int `json:"delay"` 29 | DisableAt string `json:"disableAt"` 30 | Hotkeys map[string][]Action `json:"hotkeys"` 31 | } 32 | 33 | type Action struct { 34 | WindowGroupID int `json:"windowGroupID"` 35 | TypeID int `json:"actionTypeID"` 36 | Comment string `json:"comment,omitempty"` 37 | Hotkey string `json:"hotkey,omitempty"` 38 | // 下面的字段因动作类型而异 39 | KeysToSend string `json:"keysToSend,omitempty"` 40 | RemapToKey string `json:"remapToKey,omitempty"` 41 | ValueID int `json:"actionValueID,omitempty"` 42 | WinTitle string `json:"winTitle,omitempty"` 43 | Target string `json:"target,omitempty"` 44 | Args string `json:"args,omitempty"` 45 | WorkingDir string `json:"workingDir,omitempty"` 46 | RunAsAdmin bool `json:"runAsAdmin,omitempty"` 47 | RunInBackground bool `json:"runInBackground,omitempty"` 48 | DetectHiddenWindow bool `json:"detectHiddenWindow,omitempty"` 49 | AHKCode string `json:"ahkCode,omitempty"` 50 | 51 | RemapInHotIf bool `json:"-"` 52 | } 53 | 54 | func ParseConfig(file string) (*Config, error) { 55 | data, err := os.ReadFile(file) 56 | if err != nil { 57 | return nil, fmt.Errorf("cannot read file %s: %v", file, err) 58 | } 59 | 60 | var config Config 61 | err = json.Unmarshal(data, &config) 62 | if err != nil { 63 | return nil, fmt.Errorf("cannot parse config: %v", err) 64 | } 65 | 66 | config.Options.MykeymapVersion = MykeymapVersion 67 | if config.Options.Mouse.TipSymbol == "" { 68 | config.Options.Mouse.TipSymbol = "🐶" 69 | } 70 | if config.Options.CommandInputSkin == (CommandInputSkin{}) { 71 | config.Options.CommandInputSkin = CommandInputSkin{ 72 | BackgroundColor: "#FFFFFF", 73 | BackgroundOpacity: "0.9", 74 | BorderWidth: "3", 75 | BorderColor: "#FFFFFF", 76 | BorderOpacity: "1.0", 77 | BorderRadius: "10", 78 | CornerColor: "#000000", 79 | CornerOpacity: "0.0", 80 | GridlineColor: "#2843AD", 81 | GridlineOpacity: "0.04", 82 | KeyColor: "#000000", 83 | KeyOpacity: "1.0", 84 | HideAnimationDuration: "0.34", 85 | WindowYPos: "0.25", 86 | WindowWidth: "700", 87 | WindowShadowColor: "#000000", 88 | WindowShadowOpacity: "0.5", 89 | WindowShadowSize: "3.0", 90 | } 91 | } 92 | 93 | return &config, nil 94 | } 95 | 96 | func SaveConfigFile(config *Config) { 97 | // 先写到缓冲区, 如果直接写文件的话, 当编码过程遇到错误时, 会导致文件损坏 98 | buf := new(bytes.Buffer) 99 | encoder := json.NewEncoder(buf) 100 | encoder.SetIndent("", " ") 101 | encoder.SetEscapeHTML(false) 102 | if err := encoder.Encode(config); err != nil { 103 | panic(err) 104 | } 105 | 106 | if err := os.WriteFile("../data/config.json", buf.Bytes(), 0644); err != nil { 107 | panic(err) 108 | } 109 | } 110 | 111 | func groupName(id int, prefix ...string) string { 112 | p := "MY_WINDOW_GROUP_" 113 | if len(prefix) > 0 { 114 | p = prefix[0] 115 | } 116 | if id < 0 { 117 | return fmt.Sprintf(p+"_%d", -id) 118 | } 119 | return fmt.Sprintf(p+"%d", id) 120 | } 121 | 122 | func groupToWinTile(g WindowGroup) string { 123 | if lines := notBlankLines(g.Value); len(lines) > 1 { 124 | return fmt.Sprintf(`"ahk_group %s"`, groupName(g.ID)) 125 | } else { 126 | return fmt.Sprintf(`"%s"`, strings.TrimSpace(g.Value)) 127 | } 128 | } 129 | 130 | func (c *Config) GetWinTitle(a Action) (winTitle string, conditionType int) { 131 | if a.WindowGroupID == 0 { 132 | return `""`, 0 133 | } 134 | 135 | for _, g := range c.Options.WindowGroups { 136 | if g.ID == a.WindowGroupID { 137 | // 5 表示自定义的 HotIf 表达式 138 | if g.ConditionType == 5 { 139 | return fmt.Sprintf(`'%s'`, g.Value), 5 140 | } 141 | 142 | return groupToWinTile(g), g.ConditionType 143 | } 144 | } 145 | return `""`, 0 146 | } 147 | 148 | func (c *Config) GetHotkeyContext(a Action) string { 149 | winTitle, conditionType := c.GetWinTitle(a) 150 | if winTitle == `""` && conditionType == 0 { 151 | return "" 152 | } 153 | return fmt.Sprintf(", , %s, %d", winTitle, conditionType) 154 | } 155 | 156 | func (c *Config) EnabledKeymaps() []Keymap { 157 | var enabled []Keymap 158 | for _, km := range c.Keymaps { 159 | if km.ID == 1 && km.Enable { 160 | c.handleKeyRemapping(km) 161 | enabled = append(enabled, km) 162 | } 163 | if km.ID >= 5 && km.Enable { 164 | enabled = append(enabled, km) 165 | } 166 | } 167 | 168 | // 对模式进行分组和排序 169 | groups := make(map[int][]Keymap) 170 | for _, km := range enabled { 171 | if km.ParentID != 0 { 172 | groups[km.ParentID] = append(groups[km.ParentID], km) 173 | } 174 | } 175 | var res []Keymap 176 | for _, km := range enabled { 177 | if km.ParentID == 0 { 178 | res = append(res, km) 179 | if subKeymaps, ok := groups[km.ID]; ok { 180 | res = append(res, subKeymaps...) 181 | } 182 | } 183 | } 184 | return res 185 | } 186 | 187 | func (c *Config) handleKeyRemapping(custom Keymap) { 188 | // 把自定义热键中的按键重映射取出来, 这部分需要单独渲染 189 | var list []Action 190 | for hk, actions := range custom.Hotkeys { 191 | for i, a := range actions { 192 | if a.TypeID == remapKey { 193 | a.Hotkey = hk 194 | list = append(list, a) 195 | a.RemapInHotIf = true 196 | actions[i] = a 197 | } 198 | } 199 | } 200 | 201 | // 按照 windowGroupID 进行排序 202 | sort.SliceStable(list, func(i, j int) bool { 203 | return list[i].WindowGroupID < list[j].WindowGroupID 204 | }) 205 | 206 | var s strings.Builder 207 | lastGroup := -2233 208 | for _, a := range list { 209 | if lastGroup != a.WindowGroupID { 210 | s.WriteString("\n") 211 | s.WriteString(hotifHeader(c, a)) 212 | s.WriteString("\n") 213 | lastGroup = a.WindowGroupID 214 | } 215 | s.WriteString(fmt.Sprintf("%s::%s\n", strings.TrimLeft(a.Hotkey, "*"), a.RemapToKey)) 216 | } 217 | s.WriteString("\n#HotIf") 218 | c.KeyMapping = s.String() 219 | } 220 | 221 | func hotifHeader(c *Config, a Action) string { 222 | winTitle, conditionType := c.GetWinTitle(a) 223 | if winTitle == `""` && conditionType == 0 { 224 | return "#HotIf" 225 | } 226 | switch conditionType { 227 | case 1: 228 | return fmt.Sprintf("#HotIf WinActive(%s)", winTitle) 229 | case 2: 230 | return fmt.Sprintf("#HotIf WinExist(%s)", winTitle) 231 | case 3: 232 | return fmt.Sprintf("#HotIf !WinActive(%s)", winTitle) 233 | case 4: 234 | return fmt.Sprintf("#HotIf !WinExist(%s)", winTitle) 235 | case 5: 236 | return fmt.Sprintf("#HotIf %s ", winTitle) 237 | } 238 | return "" 239 | } 240 | -------------------------------------------------------------------------------- /config-server/internal/script/options.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Options struct { 9 | MykeymapVersion string `json:"mykeymapVersion"` 10 | WindowGroups []WindowGroup `json:"windowGroups"` 11 | Mouse Mouse `json:"mouse"` 12 | Scroll Scroll `json:"scroll"` 13 | CommandInputSkin CommandInputSkin `json:"commandInputSkin"` 14 | PathVariables []PathVariable `json:"pathVariables"` 15 | Startup bool `json:"startup"` 16 | Language string `json:"language"` 17 | KeyMapping string `json:"keyMapping"` 18 | KeyboardLayout string `json:"keyboardLayout"` 19 | } 20 | 21 | type WindowGroup struct { 22 | ID int `json:"id"` 23 | Name string `json:"name"` 24 | Value string `json:"value,omitempty"` 25 | ConditionType int `json:"conditionType,omitempty"` 26 | } 27 | 28 | type Mouse struct { 29 | KeepMouseMode bool `json:"keepMouseMode"` 30 | ShowTip bool `json:"showTip"` 31 | TipSymbol string `json:"tipSymbol"` 32 | Delay1 string `json:"delay1"` 33 | Delay2 string `json:"delay2"` 34 | FastSingle string `json:"fastSingle"` 35 | FastRepeat string `json:"fastRepeat"` 36 | SlowSingle string `json:"slowSingle"` 37 | SlowRepeat string `json:"slowRepeat"` 38 | } 39 | type Scroll struct { 40 | Delay1 string `json:"delay1"` 41 | Delay2 string `json:"delay2"` 42 | OnceLineCount string `json:"onceLineCount"` 43 | } 44 | type PathVariable struct { 45 | Name string `json:"name"` 46 | Value string `json:"value"` 47 | } 48 | 49 | type CommandInputSkin struct { 50 | BackgroundColor string `json:"backgroundColor"` 51 | BackgroundOpacity string `json:"backgroundOpacity"` 52 | BorderWidth string `json:"borderWidth"` 53 | BorderColor string `json:"borderColor"` 54 | BorderOpacity string `json:"borderOpacity"` 55 | BorderRadius string `json:"borderRadius"` 56 | CornerColor string `json:"cornerColor"` 57 | CornerOpacity string `json:"cornerOpacity"` 58 | GridlineColor string `json:"gridlineColor"` 59 | GridlineOpacity string `json:"gridlineOpacity"` 60 | KeyColor string `json:"keyColor"` 61 | KeyOpacity string `json:"keyOpacity"` 62 | HideAnimationDuration string `json:"hideAnimationDuration"` 63 | WindowYPos string `json:"windowYPos"` 64 | WindowWidth string `json:"windowWidth"` 65 | WindowShadowColor string `json:"windowShadowColor"` 66 | WindowShadowOpacity string `json:"windowShadowOpacity"` 67 | WindowShadowSize string `json:"windowShadowSize"` 68 | } 69 | 70 | func (c *Config) PathVariables() string { 71 | var s strings.Builder 72 | for _, v := range c.Options.PathVariables { 73 | if strings.TrimSpace(v.Name) == "" { 74 | continue 75 | } 76 | s.WriteString(" ") 77 | s.WriteString(v.Name) 78 | s.WriteString(" := ") 79 | s.WriteString(toAHKFuncArg(v.Value)) 80 | s.WriteString("\n") 81 | } 82 | return s.String() 83 | } 84 | 85 | func notBlankLines(str string) []string { 86 | var res []string 87 | for _, line := range strings.Split(str, "\n") { 88 | line = strings.TrimSpace(line) 89 | if line != "" { 90 | res = append(res, line) 91 | } 92 | } 93 | return res 94 | } 95 | 96 | func (c *Config) WindowGroups() string { 97 | var s strings.Builder 98 | for _, g := range c.Options.WindowGroups { 99 | addGroup(&s, g.Value, g.ID) 100 | } 101 | for _, km := range c.Keymaps { 102 | addGroup(&s, km.DisableAt, km.ID, "GROUP_DISABLE_KEYMAP_") 103 | } 104 | return s.String() 105 | } 106 | 107 | func (c *Config) getKeymapDisableAt(kmID int) string { 108 | for _, km := range c.Keymaps { 109 | if km.ID == kmID { 110 | lines := notBlankLines(km.DisableAt) 111 | switch { 112 | case len(lines) == 0: 113 | return "" 114 | case len(lines) == 1: 115 | return lines[0] 116 | case len(lines) > 1: 117 | return fmt.Sprintf("ahk_group GROUP_DISABLE_KEYMAP_%d", kmID) 118 | } 119 | } 120 | } 121 | return "" 122 | } 123 | 124 | func addGroup(s *strings.Builder, value string, id int, prefix ...string) { 125 | if lines := notBlankLines(value); len(lines) > 1 { 126 | for _, v := range lines { 127 | arg := toAHKFuncArg(v) 128 | if arg != `""` { 129 | s.WriteString(fmt.Sprintf(" GroupAdd(\"%s\", %s)\n", groupName(id, prefix...), arg)) 130 | } 131 | } 132 | 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /config-server/internal/script/script.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "sort" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | func GenerateScripts(config *Config) { 13 | preprocess(config) 14 | 15 | if err := SaveAHK(config, "./templates/MyKeymap.tmpl", "../bin/MyKeymap.ahk"); err != nil { 16 | panic(err) 17 | } 18 | if err := SaveAHK(config, "./templates/CommandInputSkin.tmpl", "../bin/CommandInputSkin.txt"); err != nil { 19 | panic(err) 20 | } 21 | // if err := SaveAHK(config, "./templates/CustomShellMenu.ahk", "../bin/CustomShellMenu.ahk"); err != nil { 22 | // panic(err) 23 | // } 24 | } 25 | 26 | func preprocess(cfg *Config) { 27 | // 添加一个隐藏的全局热键, 且免疫 suspend, 否则 ahk 的 suspend 会把键盘钩子临时移除 28 | for _, km := range cfg.Keymaps { 29 | if km.ID == 1 { 30 | km.Hotkeys["!f17"] = []Action{{TypeID: 9, ValueID: 2}} 31 | return 32 | } 33 | } 34 | } 35 | 36 | func SaveAHK(data *Config, templateFile, outputFile string) error { 37 | Cfg = data 38 | files := []string{ 39 | templateFile, 40 | } 41 | ts, err := template.New(filepath.Base(templateFile)).Funcs(TemplateFuncMap).ParseFiles(files...) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | // 用 Go 代码生成 AHK 脚本时会使用 \n 导致换行符不统一 47 | // 先输出到一个字符串, 然后对换行符进行统一, 把 \n 改成 \r\n 48 | builder := new(strings.Builder) 49 | err = ts.Execute(builder, data) 50 | if err != nil { 51 | return err 52 | } 53 | res := builder.String() 54 | res = strings.ReplaceAll(res, "\r\n", "\n") 55 | res = strings.ReplaceAll(res, "\n", "\r\n") 56 | 57 | f, err := os.Create(outputFile) 58 | if err != nil { 59 | return err 60 | } 61 | //goland:noinspection GoUnhandledErrorResult 62 | defer f.Close() 63 | 64 | // 因为模板文件就是 UTF-8 with BOM, 所以输出文件也是 UTF-8 with BOM 65 | // _, _ = f.Write([]byte{0xef, 0xbb, 0xbf}) // 写入 utf-8 的 BOM (0xefbbbf) 66 | _, err = f.Write([]byte(res)) 67 | return err 68 | } 69 | 70 | var TemplateFuncMap = template.FuncMap{ 71 | "contains": strings.Contains, 72 | "concat": concat, 73 | "join": join, 74 | "ahkString": ahkString, 75 | "escapeAhkHotkey": escapeAhkHotkey, 76 | "actionToHotkey": actionToHotkey, 77 | "abbrToCode": abbrToCode, 78 | "sortHotkeys": sortHotkeys, 79 | "divide": divide, 80 | "renderKeymap": renderKeymap, 81 | "GroupDisableMyKeymap": GroupDisableMyKeymap, 82 | } 83 | 84 | func divide(a, b int) string { 85 | res := float64(a) / float64(b) 86 | if res <= 0 { 87 | return "" 88 | } 89 | return fmt.Sprintf("%.3f", res) 90 | } 91 | 92 | func join(sep string, elems []interface{}) string { 93 | elems2 := make([]string, 0) 94 | for _, val := range elems { 95 | elems2 = append(elems2, val.(string)) 96 | } 97 | return strings.Join(elems2, sep) 98 | } 99 | 100 | func ahkString(s string) string { 101 | s = strings.ReplaceAll(s, "`", "``") 102 | s = strings.ReplaceAll(s, "\"", "`\"") 103 | s = strings.ReplaceAll(s, " ;", " `;") // 空格后的分号会被 ahk 解释为注释 104 | return `"` + s + `"` 105 | } 106 | 107 | func escapeAhkHotkey(key string) string { 108 | if key == ";" { 109 | return "`;" 110 | } 111 | return key 112 | } 113 | 114 | func concat(a, b string) string { 115 | return a + b 116 | } 117 | 118 | func sortHotkeys(hotkeyMap map[string][]Action) []Action { 119 | res := make([]Action, 0, len(hotkeyMap)) 120 | for hotkey, actions := range hotkeyMap { 121 | for _, action := range actions { 122 | // 去掉首末的 " 字符, 虽然此处按字节取子切片也行, 但写法不够通用 123 | action.Hotkey = substr(toAHKFuncArg(hotkey), 1, -1) 124 | res = append(res, action) 125 | } 126 | } 127 | 128 | sort.Slice(res, func(i, j int) bool { 129 | // 先根据 action type 排, 然后根据 hotkey 的长度和字典序来排序 130 | if res[i].TypeID != res[j].TypeID { 131 | return res[i].TypeID < res[j].TypeID 132 | } 133 | 134 | if len(res[i].Hotkey) != len(res[j].Hotkey) { 135 | return len(res[i].Hotkey) < len(res[j].Hotkey) 136 | } 137 | 138 | return res[i].Hotkey < res[j].Hotkey 139 | }) 140 | 141 | return res 142 | } 143 | 144 | // 取子字符串,并不想看起来那么简单: https://stackoverflow.com/a/56129336 145 | // NOTE: this isn't multi-Unicode-codepoint aware, like specifying skintone or 146 | // gender of an emoji: https://unicode.org/emoji/charts/full-emoji-modifiers.html 147 | func substr(input string, start int, length int) string { 148 | asRunes := []rune(input) 149 | 150 | if start >= len(asRunes) { 151 | return "" 152 | } 153 | 154 | if start+length > len(asRunes) { 155 | length = len(asRunes) - start 156 | } else if length < 0 { 157 | length = len(asRunes) - start + length 158 | } 159 | 160 | return string(asRunes[start : start+length]) 161 | } 162 | 163 | func renderKeymap(km Keymap) string { 164 | if "" == strings.TrimSpace(km.Hotkey) { 165 | return "" 166 | } 167 | var buf strings.Builder 168 | 169 | // ; Capslock + F 170 | buf.WriteString(fmt.Sprintf("\n ; %s\n", km.Name)) 171 | 172 | // km6 := KeymapManager.AddSubKeymap(km5, "*f", "Capslock + F") 173 | line := fmt.Sprintf(" km%d := KeymapManager.", km.ID) 174 | hotkey := km.Hotkey 175 | if containsOnlyModifier(km.Hotkey) { 176 | hotkey = "customHotkeys" 177 | } 178 | s := ahkString 179 | if km.ParentID == 0 { 180 | line += fmt.Sprintf("NewKeymap(%s, %s, %s, %s)\n", s(hotkey), s(km.Name), s(divide(km.Delay, 1000)), s(Cfg.getKeymapDisableAt(km.ID))) 181 | } else { 182 | line += fmt.Sprintf("AddSubKeymap(km%d, %s, %s, %s)\n", km.ParentID, s(hotkey), s(km.Name), s(divide(km.Delay, 1000))) 183 | } 184 | buf.WriteString(line) 185 | 186 | // km := km6 187 | buf.WriteString(fmt.Sprintf(" km := km%d\n", km.ID)) 188 | 189 | for _, action := range sortHotkeys(km.Hotkeys) { 190 | if containsOnlyModifier(km.Hotkey) { 191 | if action.Hotkey == "singlePress" { 192 | continue 193 | } 194 | action.Hotkey = km.Hotkey + action.Hotkey // 把触发键 # 和热键 *q 拼起来 195 | } 196 | buf.WriteString(fmt.Sprintf(" %s\n", actionToHotkey(action))) 197 | } 198 | 199 | // 替换换行符为 \r\n 200 | res := buf.String() 201 | res = strings.ReplaceAll(res, "\r\n", "\n") 202 | res = strings.ReplaceAll(res, "\n", "\r\n") 203 | return res 204 | } 205 | 206 | func containsOnlyModifier(hotkey string) bool { 207 | hotkey = strings.TrimSpace(hotkey) 208 | return hotkey != "" && strings.Trim(hotkey, "#!^+<>*~$") == "" 209 | } 210 | 211 | func GroupDisableMyKeymap(groups []WindowGroup) string { 212 | for _, g := range groups { 213 | if g.ID == -1 { 214 | return groupToWinTile(g) 215 | } 216 | } 217 | return ahkString("") 218 | } 219 | -------------------------------------------------------------------------------- /config-server/internal/script/script_test.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import "testing" 4 | 5 | func TestSortActions(t *testing.T) { 6 | a := Action{WindowGroupID: 0, Comment: "item1"} 7 | b := Action{WindowGroupID: 1, Comment: "item2"} 8 | c := Action{WindowGroupID: 0, Comment: "item3"} 9 | s := []Action{a, b, c} 10 | ss := sortActions(s) 11 | assertEqual(t, ss[0], b) 12 | assertEqual(t, ss[1], a) 13 | assertEqual(t, ss[2], c) 14 | } 15 | 16 | func assertEqual[T comparable](t *testing.T, a, b T) { 17 | t.Helper() 18 | if a != b { 19 | t.Errorf("%v != %v", a, b) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config-server/templates/CommandInputSkin.tmpl: -------------------------------------------------------------------------------- 1 | backgroundColor = {{ if .Options.CommandInputSkin.BackgroundColor }}{{ .Options.CommandInputSkin.BackgroundColor }}{{ else }}#FFFFFF{{ end }} 2 | backgroundOpacity = {{ if .Options.CommandInputSkin.BackgroundOpacity }}{{ .Options.CommandInputSkin.BackgroundOpacity }}{{ else }}0.9{{ end }} 3 | borderWidth = {{ if .Options.CommandInputSkin.BorderWidth }}{{ .Options.CommandInputSkin.BorderWidth }}{{ else }}3{{ end }} 4 | borderColor = {{ if .Options.CommandInputSkin.BorderColor }}{{ .Options.CommandInputSkin.BorderColor }}{{ else }}#FFFFFF{{ end }} 5 | borderOpacity = {{ if .Options.CommandInputSkin.BorderOpacity }}{{ .Options.CommandInputSkin.BorderOpacity }}{{ else }}1.0{{ end }} 6 | borderRadius = {{ if .Options.CommandInputSkin.BorderRadius }}{{ .Options.CommandInputSkin.BorderRadius }}{{ else }}10{{ end }} 7 | cornerColor = {{ if .Options.CommandInputSkin.CornerColor }}{{ .Options.CommandInputSkin.CornerColor }}{{ else }}#000000{{ end }} 8 | cornerOpacity = {{ if .Options.CommandInputSkin.CornerOpacity }}{{ .Options.CommandInputSkin.CornerOpacity }}{{ else }}0.0{{ end }} 9 | gridlineColor = {{ if .Options.CommandInputSkin.GridlineColor }}{{ .Options.CommandInputSkin.GridlineColor }}{{ else }}#2843AD{{ end }} 10 | gridlineOpacity = {{ if .Options.CommandInputSkin.GridlineOpacity }}{{ .Options.CommandInputSkin.GridlineOpacity }}{{ else }}0.04{{ end }} 11 | keyColor = {{ if .Options.CommandInputSkin.KeyColor }}{{ .Options.CommandInputSkin.KeyColor }}{{ else }}#000000{{ end }} 12 | keyOpacity = {{ if .Options.CommandInputSkin.KeyOpacity }}{{ .Options.CommandInputSkin.KeyOpacity }}{{ else }}1.0{{ end }} 13 | hideAnimationDuration = {{ if .Options.CommandInputSkin.HideAnimationDuration }}{{ .Options.CommandInputSkin.HideAnimationDuration }}{{ else }}0.34{{ end }} 14 | windowYPos = {{ if .Options.CommandInputSkin.WindowYPos }}{{ .Options.CommandInputSkin.WindowYPos }}{{ else }}0.25{{ end }} 15 | windowWidth = {{ if .Options.CommandInputSkin.WindowWidth }}{{ .Options.CommandInputSkin.WindowWidth }}{{ else }}700{{ end }} 16 | windowShadowColor = {{ if .Options.CommandInputSkin.WindowShadowColor }}{{ .Options.CommandInputSkin.WindowShadowColor }}{{ else }}#000000{{ end }} 17 | windowShadowOpacity = {{ if .Options.CommandInputSkin.WindowShadowOpacity }}{{ .Options.CommandInputSkin.WindowShadowOpacity }}{{ else }}0.5{{ end }} 18 | windowShadowSize = {{ if .Options.CommandInputSkin.WindowShadowSize }}{{ .Options.CommandInputSkin.WindowShadowSize }}{{ else }}3.0{{ end }} -------------------------------------------------------------------------------- /config-server/templates/CustomShellMenu.ahk: -------------------------------------------------------------------------------- 1 | #NoEnv 2 | #SingleInstance, force 3 | #NoTrayIcon 4 | SetWorkingDir %A_ScriptDir% 5 | CoordMode, Mouse, Screen 6 | SendMode Input 7 | 8 | menuItems := [] 9 | filePath := getSelectedFilePath() 10 | SplitPath, filePath , filename, fileDir, fileExt, filenameNoExt 11 | 12 | ; 固定的 13 | firstItemName := filename 14 | if InStr(filePath, "`n") { 15 | firstItemName := "选中了多个文件" 16 | } 17 | Menu, MyMenu, Add, %firstItemName%, MenuHandler 18 | Menu, MyMenu, Disable, %firstItemName% 19 | 20 | name := "(&F) 复制路径" 21 | Menu, MyMenu, Add, %name%, MenuHandler 22 | Menu, MyMenu, Icon, %name%, Shell32.dll, 135 23 | 24 | ; 自定义的 25 | {{ .Settings.CustomShellMenu }} 26 | 27 | 28 | WinGetPos, , , Width, Height, A 29 | offsetX := Width/2 30 | offsetY := Height/2 31 | Menu, MyMenu, Show, %offsetX%, %offsetY% 32 | return 33 | 34 | MenuHandler: 35 | ; MsgBox file: %filePath%`nitem: %A_ThisMenuItem%. `n x: %x% `n y: %y% 36 | if (A_ThisMenuItem == "(&F) 复制路径") { 37 | Clipboard := filePath 38 | ToolTip, 复制了 %filePath% 39 | sleep 600 40 | return 41 | } 42 | for index, item in menuItems { 43 | if (A_ThisMenuItem == item.name) { 44 | exe := item.exe 45 | args := item.args 46 | args := StrReplace(args, """{file}""", """" filePath """") 47 | args := StrReplace(args, "{file}", """" filePath """") 48 | args := StrReplace(args, """{dir}""", """" fileDir """") 49 | args := StrReplace(args, "{dir}", """" fileDir """") 50 | args := StrReplace(args, """{filename}""", """" filename """") 51 | args := StrReplace(args, "{filename}", """" filename """") 52 | args := StrReplace(args, """{filenameNoExt}""", """" filenameNoExt """") 53 | args := StrReplace(args, "{filenameNoExt}", """" filenameNoExt """") 54 | 55 | if (item.args == "smartExtractZipFile") { 56 | smartExtractZipFile(item.exe, filePath) 57 | break 58 | } 59 | if (item.args == "smartArchiveZipFile") { 60 | smartArchiveZipFile(item.exe, filePath) 61 | break 62 | } 63 | 64 | if (item.workingDir) { 65 | wr := item.workingDir 66 | wr := StrReplace(wr, "{file}", filePath) 67 | wr := StrReplace(wr, "{dir}", fileDir) 68 | run, %exe% %args%, %wr% 69 | break 70 | } 71 | 72 | run, %exe% %args% 73 | break 74 | } 75 | } 76 | return 77 | 78 | add_menu_item(key, name, icon := "", exe := "", args := "", workingDir := "") 79 | { 80 | global menuItems 81 | 82 | name := "(&" key ") " name 83 | Menu, MyMenu, Add, %name%, MenuHandler 84 | if (icon && icon != "NoIcon") { 85 | ; 如果是 .lnk 并且它指向 exe 文件, 那么用这个 exe 作为 icon 86 | if (SubStr(icon, -3) == ".lnk") { 87 | FileGetShortcut, %exe%, lnkTarget 88 | if (SubStr(lnkTarget, -3) == ".exe") { 89 | icon := lnkTarget 90 | } 91 | } 92 | Menu, MyMenu, Icon, %name%, %icon% 93 | } 94 | 95 | obj := {} 96 | obj.exe := exe 97 | obj.args := args 98 | obj.workingDir := workingDir 99 | obj.name := name 100 | menuItems.Push(obj) 101 | } 102 | 103 | getSelectedFilePath() 104 | { 105 | tmp := Clipboardall 106 | Clipboard := "" 107 | Send, ^c 108 | ClipWait, 0.6 109 | path := Clipboard 110 | Clipboard := tmp 111 | if ErrorLevel { 112 | tooltip, 没有获取到路径 113 | sleep, 1000 114 | ExitApp 115 | } 116 | return path 117 | } 118 | 119 | 120 | smartExtractZipFile(exe, filepath) 121 | { 122 | info := GetZipInfo(exe, filepath) 123 | showDialog := info.encrypted ? "-ad" : "" 124 | 125 | SplitPath, exe, OutFileName, OutDir, OutExtension, OutNameNoExt 126 | exe := OutDir . "\7zg.exe" 127 | 128 | SplitPath, filepath, OutFileName, OutDir, OutExtension, OutNameNoExt 129 | if (info.containsOnlyOneFile) { 130 | run, %exe% x %showDialog% "%filepath%" -o"%OutDir%" 131 | } else { 132 | run, %exe% x %showDialog% "%filepath%" -o"%OutDir%\%OutNameNoExt%" 133 | } 134 | } 135 | 136 | GetZipInfo(exe,filepath) 137 | { 138 | cmdLine := """" exe """ l -p123 -ba -x!*\* " """" filepath """" 139 | stdout := RunCMD(cmdLine) 140 | lines := StrSplit(stdout, "`n") 141 | zipInfo := {} 142 | zipInfo.containsOnlyOneFile := lines.Length() == 2 143 | zipInfo.encrypted := InStr(stdout, "Cannot open encrypted archive") 144 | return zipInfo 145 | } 146 | 147 | 148 | smartArchiveZipFile(exe, filepath) 149 | { 150 | SplitPath, exe, OutFileName, OutDir, OutExtension, OutNameNoExt 151 | exe := OutDir . "\7zg.exe" 152 | 153 | ; 选中一个文件/文件夹 154 | if !InStr(filepath, "`n") { 155 | SplitPath, filepath, OutFileName, OutDir, OutExtension, OutNameNoExt 156 | if InStr(FileExist(filepath), "D") { 157 | run, %exe% a "%filepath%.7z" "%filepath%" 158 | } else { 159 | run, %exe% a "%OutDir%\%OutNameNoExt%.7z" "%filepath%" 160 | } 161 | return 162 | } 163 | 164 | ; 选中多个文件, 注意换行符是 `r`n 165 | files := StrSplit(filepath, "`r`n") 166 | 167 | ; 最后会多出一个空格, 但在命令行中无所谓 168 | result := "" 169 | for i,f in files { 170 | result := result . """" f """ " 171 | } 172 | 173 | firstFile := files[1] 174 | SplitPath, firstFile, , OutDir 175 | SplitPath, OutDir, OutFileName 176 | run, %exe% a "%OutDir%\%OutFileName%.7z" %result% 177 | } 178 | 179 | 180 | RunCMD(CmdLine, WorkingDir:="", Codepage:="CP0", Fn:="RunCMD_Output") { ; RunCMD v0.94 181 | Local ; RunCMD v0.94 by SKAN on D34E/D37C @ autohotkey.com/boards/viewtopic.php?t=74647 182 | Global A_Args ; Based on StdOutToVar.ahk by Sean @ autohotkey.com/board/topic/15455-stdouttovar 183 | 184 | Fn := IsFunc(Fn) ? Func(Fn) : 0 185 | , DllCall("CreatePipe", "PtrP",hPipeR:=0, "PtrP",hPipeW:=0, "Ptr",0, "Int",0) 186 | , DllCall("SetHandleInformation", "Ptr",hPipeW, "Int",1, "Int",1) 187 | , DllCall("SetNamedPipeHandleState","Ptr",hPipeR, "UIntP",PIPE_NOWAIT:=1, "Ptr",0, "Ptr",0) 188 | 189 | , P8 := (A_PtrSize=8) 190 | , VarSetCapacity(SI, P8 ? 104 : 68, 0) ; STARTUPINFO structure 191 | , NumPut(P8 ? 104 : 68, SI) ; size of STARTUPINFO 192 | , NumPut(STARTF_USESTDHANDLES:=0x100, SI, P8 ? 60 : 44,"UInt") ; dwFlags 193 | , NumPut(hPipeW, SI, P8 ? 88 : 60) ; hStdOutput 194 | , NumPut(hPipeW, SI, P8 ? 96 : 64) ; hStdError 195 | , VarSetCapacity(PI, P8 ? 24 : 16) ; PROCESS_INFORMATION structure 196 | 197 | If not DllCall("CreateProcess", "Ptr",0, "Str",CmdLine, "Ptr",0, "Int",0, "Int",True 198 | ,"Int",0x08000000 | DllCall("GetPriorityClass", "Ptr",-1, "UInt"), "Int",0 199 | ,"Ptr",WorkingDir ? &WorkingDir : 0, "Ptr",&SI, "Ptr",&PI) 200 | Return Format("{1:}", "", ErrorLevel := -1 201 | ,DllCall("CloseHandle", "Ptr",hPipeW), DllCall("CloseHandle", "Ptr",hPipeR)) 202 | 203 | DllCall("CloseHandle", "Ptr",hPipeW) 204 | , A_Args.RunCMD := { "PID": NumGet(PI, P8? 16 : 8, "UInt") } 205 | , File := FileOpen(hPipeR, "h", Codepage) 206 | 207 | , LineNum := 1, sOutput := "" 208 | While (A_Args.RunCMD.PID + DllCall("Sleep", "Int",0)) 209 | and DllCall("PeekNamedPipe", "Ptr",hPipeR, "Ptr",0, "Int",0, "Ptr",0, "Ptr",0, "Ptr",0) 210 | While A_Args.RunCMD.PID and (Line := File.ReadLine()) 211 | sOutput .= Fn ? Fn.Call(Line, LineNum++) : Line 212 | 213 | A_Args.RunCMD.PID := 0 214 | , hProcess := NumGet(PI, 0) 215 | , hThread := NumGet(PI, A_PtrSize) 216 | 217 | , DllCall("GetExitCodeProcess", "Ptr",hProcess, "PtrP",ExitCode:=0) 218 | , DllCall("CloseHandle", "Ptr",hProcess) 219 | , DllCall("CloseHandle", "Ptr",hThread) 220 | , DllCall("CloseHandle", "Ptr",hPipeR) 221 | 222 | , ErrorLevel := ExitCode 223 | 224 | Return sOutput 225 | } -------------------------------------------------------------------------------- /config-server/templates/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 所有键位映射 7 | 8 | 9 | 14 | 15 | 16 | {{ .helpPageHtml }} 17 | 18 | -------------------------------------------------------------------------------- /config-server/templates/mykeymap.tmpl: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #SingleInstance Force 3 | #UseHook true 4 | 5 | #include lib/translation.ahk 6 | #Include lib/Functions.ahk 7 | #Include lib/Actions.ahk 8 | #Include lib/KeymapManager.ahk 9 | #Include lib/InputTipWindow.ahk 10 | #Include lib/Utils.ahk 11 | 12 | ; #WinActivateForce ; 先关了遇到相关问题再打开试试 13 | ; InstallKeybdHook ; 这个可以重装 keyboard hook, 提高自己的 hook 优先级, 以后可能会用到 14 | ; ListLines False ; 也许能提升一点点性能 ( 别抱期待 ), 当有这个需求时再打开试试 15 | ; #Warn All, Off ; 也许能提升一点点性能 ( 别抱期待 ), 当有这个需求时再打开试试 16 | 17 | DllCall("SetThreadDpiAwarenessContext", "ptr", -3, "ptr") ; 多显示器不同缩放比例会导致问题: https://www.autohotkey.com/boards/viewtopic.php?f=14&t=13810 18 | SetMouseDelay 0 ; SendInput 可能会降级为 SendEvent, 此时会有 10ms 的默认 delay 19 | SetWinDelay 0 ; 默认会在 activate, maximize, move 等窗口操作后睡眠 100ms 20 | A_MaxHotkeysPerInterval := 256 ; 默认 70 可能有点低, 即使没有热键死循环也触发警告 21 | SendMode "Event" ; 执行 SendInput 的期间会短暂卸载 Hook, 这时候松开引导键会丢失 up 事件, 所以 Event 模式更适合 MyKeymap 22 | SetKeyDelay 0 ; 默认 10 太慢了, https://www.reddit.com/r/AutoHotkey/comments/gd3z4o/possible_unreliable_detection_of_the_keyup_event/ 23 | ProcessSetPriority "High" 24 | SetWorkingDir("../") 25 | InitTrayMenu() 26 | InitKeymap() 27 | OnExit(MyKeymapExit) 28 | #include ../data/custom_functions.ahk 29 | 30 | InitKeymap() 31 | { 32 | taskSwitch := TaskSwitchKeymap("e", "d", "s", "f", "c", "space") 33 | mouseTip := {{ if .Options.Mouse.ShowTip }}InputTipWindow("{{ .Options.Mouse.TipSymbol }}",,,, 20, 16){{ else }}false{{ end }} 34 | slow := MouseKeymap("slow mouse", {{ .Options.Mouse.KeepMouseMode }}, mouseTip, {{ .Options.Mouse.SlowSingle }}, {{ .Options.Mouse.SlowRepeat }}, "T{{ .Options.Mouse.Delay1 }}", "T{{ .Options.Mouse.Delay2 }}", {{ .Options.Scroll.OnceLineCount }}, "T{{ .Options.Scroll.Delay1 }}", "T{{ .Options.Scroll.Delay2 }}") 35 | fast := MouseKeymap("fast mouse", {{ .Options.Mouse.KeepMouseMode }}, mouseTip, {{ .Options.Mouse.FastSingle }}, {{ .Options.Mouse.FastRepeat }}, "T{{ .Options.Mouse.Delay1 }}", "T{{ .Options.Mouse.Delay2 }}", {{ .Options.Scroll.OnceLineCount }}, "T{{ .Options.Scroll.Delay1 }}", "T{{ .Options.Scroll.Delay2 }}", slow) 36 | slow.Map("*space", slow.LButtonUp()) 37 | {{ if .CapslockAbbrEnabled }} 38 | capsHook := InputHook("", "{CapsLock}{Esc}", "{{ .CapslockAbbrKeys }}") 39 | capsHook.KeyOpt("{CapsLock}", "S") 40 | capsHook.KeyOpt("{Backspace}", "N") 41 | capsHook.OnChar := PostCharToCaspAbbr 42 | capsHook.OnKeyDown := PostBackspaceToCaspAbbr 43 | Run("bin\MyKeymap-CommandInput.exe") 44 | {{ end }} 45 | {{- if .SemicolonAbbrEnabled }} 46 | semiHook := InputHook("", "{CapsLock}{Esc}{;}", "{{ .SemicolonAbbrKeys }}") 47 | semiHook.KeyOpt("{CapsLock}", "S") 48 | semiHook.KeyOpt("{Backspace}", "N") 49 | semiHook.OnChar := (ih, char) => semiHookAbbrWindow.Show(char, true) 50 | semiHook.OnKeyDown := (ih, vk, sc) => semiHookAbbrWindow.Backspace() 51 | semiHookAbbrWindow := InputTipWindow() 52 | {{ end }} 53 | 54 | ; 路径变量 55 | {{ .PathVariables }} 56 | ; 窗口组 57 | {{ .WindowGroups }} 58 | KeymapManager.GlobalKeymap.DisabledAt := {{ GroupDisableMyKeymap .Options.WindowGroups }} 59 | {{range .EnabledKeymaps}}{{renderKeymap .}}{{end}} 60 | 61 | KeymapManager.GlobalKeymap.Enable() 62 | } 63 | 64 | {{ if .CapslockAbbrEnabled -}} 65 | ExecCapslockAbbr(command) { 66 | ; 路径变量 67 | {{ .PathVariables }} 68 | switch command { 69 | {{ abbrToCode .CapslockAbbr }} } 70 | } 71 | {{- else -}} 72 | ExecCapslockAbbr(command) { 73 | } 74 | {{- end }} 75 | 76 | {{ if .SemicolonAbbrEnabled -}} 77 | ExecSemicolonAbbr(command) { 78 | ; 路径变量 79 | {{ .PathVariables }} 80 | switch command { 81 | {{ abbrToCode .SemicolonAbbr }} } 82 | } 83 | {{- else -}} 84 | ExecSemicolonAbbr(command) { 85 | } 86 | {{- end }} 87 | 88 | InitTrayMenu() { 89 | A_TrayMenu.Delete() 90 | A_TrayMenu.Add(Translation().menu_pause, TrayMenuHandler) 91 | A_TrayMenu.Add(Translation().menu_exit, TrayMenuHandler) 92 | A_TrayMenu.Add(Translation().menu_reload, TrayMenuHandler) 93 | A_TrayMenu.Add(Translation().menu_settings, TrayMenuHandler) 94 | A_TrayMenu.Add(Translation().menu_window_spy, TrayMenuHandler) 95 | A_TrayMenu.Default := Translation().menu_pause 96 | A_TrayMenu.ClickCount := 1 97 | 98 | A_IconTip := "MyKeymap {{ .Options.MykeymapVersion }} created by 咸鱼阿康" 99 | TraySetIcon("./bin/icons/logo.ico", , true) 100 | } 101 | 102 | {{ .KeyMapping }} -------------------------------------------------------------------------------- /config-ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /config-ui/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | end_of_line = lf -------------------------------------------------------------------------------- /config-ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | ], 11 | rules: { 12 | 'vue/multi-word-component-names': 'off', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /config-ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /config-ui/README.md: -------------------------------------------------------------------------------- 1 | # essentials 2 | 3 | ## Project setup 4 | 5 | ``` 6 | # yarn 7 | yarn 8 | 9 | # npm 10 | npm install 11 | 12 | # pnpm 13 | pnpm install 14 | ``` 15 | 16 | ### Compiles and hot-reloads for development 17 | 18 | ``` 19 | # yarn 20 | yarn dev 21 | 22 | # npm 23 | npm run dev 24 | 25 | # pnpm 26 | pnpm dev 27 | ``` 28 | 29 | ### Compiles and minifies for production 30 | 31 | ``` 32 | # yarn 33 | yarn build 34 | 35 | # npm 36 | npm run build 37 | 38 | # pnpm 39 | pnpm build 40 | ``` 41 | 42 | ### Lints and fixes files 43 | 44 | ``` 45 | # yarn 46 | yarn lint 47 | 48 | # npm 49 | npm run lint 50 | 51 | # pnpm 52 | pnpm lint 53 | ``` 54 | 55 | ### Customize configuration 56 | 57 | See [Configuration Reference](https://vitejs.dev/config/). 58 | -------------------------------------------------------------------------------- /config-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MyKeymap Setting 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /config-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config-ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "lint": "eslint . --fix --ignore-path .gitignore" 10 | }, 11 | "dependencies": { 12 | "@mdi/font": "7.0.96", 13 | "@vueuse/core": "^10.3.0", 14 | "core-js": "^3.29.0", 15 | "lodash-es": "^4.17.21", 16 | "pinia": "^2.0.0", 17 | "roboto-fontface": "*", 18 | "vue": "^3.2.0", 19 | "vue-router": "^4.0.0", 20 | "vuetify": "^3.0.0", 21 | "webfontloader": "^1.0.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/types": "^7.21.4", 25 | "@types/lodash-es": "^4.17.8", 26 | "@types/node": "^18.15.0", 27 | "@types/webfontloader": "^1.6.35", 28 | "@vitejs/plugin-vue": "^4.0.0", 29 | "@vue/eslint-config-typescript": "^11.0.0", 30 | "eslint": "^8.37.0", 31 | "eslint-plugin-vue": "^9.3.0", 32 | "sass": "^1.60.0", 33 | "typescript": "^5.0.0", 34 | "vite": "^4.2.0", 35 | "vite-plugin-vuetify": "^1.0.0", 36 | "vue-tsc": "^1.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config-ui/public/config_doc.md: -------------------------------------------------------------------------------- 1 | ## 😀 欢迎 2 | 3 | 1. [项目 GitHub](https://github.com/xianyukang/MyKeymap) 4 | 2. [视频介绍](https://www.bilibili.com/video/BV1Sf4y1c7p8/) 5 | 3. [快速入门](https://xianyukang.com/MyKeymap.html#mykeymap-%E7%AE%80%E4%BB%8B) ( 推荐看看,能了解 MyKeymap 的功能,和注意事项 ) 6 | 7 | 8 | 9 | ## ✨ 浏览器字体 10 | 11 | 1. 可以美化浏览器字体,因为字体文件占几十 M 所以不打包在 MyKeymap 里面 12 | 2. 效果对比: 「 [修改前](/font-compare/1.png) / [修改后](/font-compare/2.png) 」, 「 [修改前](/font-compare/3.png) / [修改后](/font-compare/4.png) 」, 「 [修改前](/font-compare/5.png) / [修改后](/font-compare/6.png) 」 13 | 3. 如何修改: ①[去这下载工具](https://www.bilibili.com/video/BV1pP4y187bR/) ②把其中的常用字体全部装上 ③[从这跟着视频](https://www.bilibili.com/video/BV1pP4y187bR?t=190.0)一步一步操作 14 | 15 | 16 | 17 | 18 | 19 | ## 🚀 启动程序或激活窗口 20 | 21 | ### 概述 22 | 23 | - 此功能用来启动程序或激活窗口,其中「 激活窗口 」是亮点,因为程序往往只启动一次,却要激活它的窗口几十上百次 24 | - 基于模糊搜索的启动器 ( 比如开始菜单 ) 确实很方便,但在面对「 常用软件 」时,它们还不够方便: 25 | 1. 模糊搜索具有模糊性,需要担心和处理不匹配的情况,而快捷键是精准确定的,无脑按就行 26 | 2. 模糊搜索消耗的按键次数更多,至少要按 5 个键才能启动,而快捷键只需 1/2/3 个键 27 | 3. 缺乏激活窗口的能力,而激活窗口的频率要比启动频率高得多 28 | 29 | ### 如何配置 30 | 31 | 1. 先填第 2 个输入框,可以填「 程序路径 / 文件夹路径 / 网页链接 / PATH 中的命令 / 魔法链接 (参见后文) 」 32 | - 在文件管理器中,可用 `Capslock + Z` 复制选中文件的路径 33 | 2. 填完第 2 个输入框点左下角的「 保存 」,然后按一下当前热键,试试看能否成功启动 34 | 3. 程序启动后点一下「 查看窗口标识符 」,然后把程序的窗口标识符 ( 比如 `ahk_exe msedge.exe` ) 填到第 1 个输入框 35 | 36 | ![image-20230911094609620](img/example01.png) 37 | 38 | ### 用官方热键进行激活 39 | 40 | - 像 QQ 和微信这些挂在后台的程序,需要用它们的官方热键进行激活 41 | - 比如 QQ 要按 Ctrl + Alt + Z 进行激活,微信要按 Ctrl + Alt + W 激活 42 | - 可以使用图中的函数: 43 | - ProcessExistSendKeyOrRun("WeChat.exe", "^!w", "shortcuts\微信.lnk") 44 | - 它的含义是: 如果 `WeChat.exe` 进程存在,那么输入 `^!w` (表示 Ctrl+Alt+W 热键) 激活微信,否则启动微信 45 | 46 | ![image-20230911153600961](img/example02.png) 47 | 48 | ### 魔法链接 49 | 50 | - 在上上图的第 2 个输入框中填 https://example.com/ 能让浏览器打开指定的网页 51 | - 这个 `https:` 是一种 URI Scheme,可以让相关程序执行特定动作,除了 `https:` 还有其他格式的链接,比如: 52 | - 用 `shell:Downloads` 让文件资源管理器打开「 下载 」文件夹 53 | - 用 `shell:RecycleBinFolder` 让文件资源管理器打开「 回收站 」文件夹 54 | - 用 `ms-settings:bluetooth` 让 Windows 设置打开「 蓝牙 」页面 55 | - 用 `ms-settings:startupapps` 让 Windows 设置打开「 自启 」页面 56 | - 用 `steam://rungameid/1687950` 让 Steam 启动「 女神异闻录5 」游戏 57 | - 其中打开「 特殊文件夹 」和「 特定设置页 」比较常用,可以参考: 58 | - [文件资源管理器 ( shell: )](https://www.elevenforum.com/t/list-of-windows-11-shell-commands-for-shell-folder-shortcuts.1080/) 59 | - [Windows 设置 ( ms-settings: )](https://learn.microsoft.com/en-us/windows/uwp/launch-resume/launch-settings-app#ms-settings-uri-scheme-reference) 60 | 61 | 62 | 63 | 64 | 65 | ## 💎 输入文本或按键 66 | 67 | ### 概述 68 | 69 | - 此功能用来输入一串按键或文本,比较好用建议掌握👍,常用于: 70 | 1. 重映射不好按的快捷键: 比如用 Capslock+X 来触发 Alt+F4,能让快捷键变得更好按 71 | 2. 通过输入一串按键实现各种各样的目的: 72 | - 比如用 Capslock+D 来输入 Home、Shift+End、Backspace,能删除一行文本 73 | - 比如用 Capslock+R 来输入 Win+X、U 、R,能重启电脑 74 | 75 | ### 如何配置 76 | 77 | - 首先要把输入法切换到英文状态,因为接下来用到的标点符号,全都必须是「 英文标点符号 」! 78 | - 下图取自 Capslock 模式的 0 键,逐行解释如下: 79 | 80 | ![image-20230911153628551](img/example03.png) 81 | 82 | #### ➤ 第一行 83 | 84 | - 含义为: 先用 home 键把光标移动到行首,然后用 shift + end 选中一整行,最后按 backspace 删除 85 | 86 | - `{home}` 表示 home 键,因为键名超出 1 个字母,所以要用大括号 `{}` 括起来 ( 特殊符号也一样 ) 87 | - `+{end}` 表示 shift + end,请记住 `^ ! + #` 这四个特殊符号,它们常用于输入组合键,举个例子: 88 | - 用 `^a` 表示 ctrl + a 89 | - 用 `!f` 表示 alt + f 90 | - 用 `#s` 表示 win + s 91 | - 用 `!{f4}` 表示 alt + f4 92 | - 用 `^+{esc}` 表示 ctrl + shift + esc,能启动任务管理器 93 | - 用 `^!{tab}` 表示 ctrl + alt + tab,能打开窗口切换器 94 | 95 | #### ➤ 第二行 96 | 97 | - `{text}i love homura` 中的 `{text}` 表示以文本模式输入后面的内容,区别如下: 98 | - 使用 `love` 时,会依次输入 l、o、v、e 等 4 个按键 99 | - 使用 `{text}love` 时,会输入 `love` 这样一段文本,不受输入法中英文状态的影响 100 | 101 | #### ➤ 第三行 102 | 103 | - `sleep 1000` 表示等待 1000 毫秒,有时候要等程序处理完一组按键,再输入下一组按键,比如: 104 | 105 | ![image-20230911154509135](img/example04.png) 106 | 107 | #### ➤ 补充 108 | 109 | 1. 这里有完整的「 [键名列表](https://wyagd001.github.io/v2/docs/lib/Send.htm#keynames) 」 110 | 2. 假设用 Capslock + X 输入 `F4` 键: 111 | - 如果想让 Capslock + Alt + X 变成 `Alt + F4`,那么得把 Capslock 模式的 X 键配成 `{blind}{f4}` 112 | - 通过 `{blind}` 盲从模式输入的键,会被已经按下的修饰键 ( Ctrl / Shift / Alt ) 影响 113 | - 比如已经按下了 Alt 键,那么输入的 `{blind}{f4}` 会变成 `Alt + F4` 114 | 115 | 116 | 117 | 118 | 119 | ## ⚙️ 添加自定义模式 120 | 121 | ### 星号的作用 122 | 123 | 点开 Settings 页会发现有的触发键以星号开头 ( 例如 3 模式的的 `*3` ),而有的没有星号 ( 例如 `Tab` ) 124 | 125 | `*3` 中星号的作用是,在按住 Ctrl, Alt, Win, Shift 等键时,再按 3 也能触发 3 模式,所以 `Win+3+K` = `Win+2` 126 | 127 | 如果不加星号,那么按下 `Win+3` 就执行它原本的功能,仿佛 3 模式不存在 128 | 129 | 然后 Tab 模式的触发键不加星号是为了不影响 `Alt+Tab` 热键 130 | -------------------------------------------------------------------------------- /config-ui/public/exe-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/exe-path.png -------------------------------------------------------------------------------- /config-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/favicon.ico -------------------------------------------------------------------------------- /config-ui/public/font-compare/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/font-compare/1.png -------------------------------------------------------------------------------- /config-ui/public/font-compare/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/font-compare/2.png -------------------------------------------------------------------------------- /config-ui/public/font-compare/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/font-compare/3.png -------------------------------------------------------------------------------- /config-ui/public/font-compare/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/font-compare/4.png -------------------------------------------------------------------------------- /config-ui/public/font-compare/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/font-compare/5.png -------------------------------------------------------------------------------- /config-ui/public/font-compare/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/font-compare/6.png -------------------------------------------------------------------------------- /config-ui/public/img/example01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/img/example01.png -------------------------------------------------------------------------------- /config-ui/public/img/example02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/img/example02.png -------------------------------------------------------------------------------- /config-ui/public/img/example03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/img/example03.png -------------------------------------------------------------------------------- /config-ui/public/img/example04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/img/example04.png -------------------------------------------------------------------------------- /config-ui/public/uwp-shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/public/uwp-shortcut.png -------------------------------------------------------------------------------- /config-ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /config-ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/config-ui/src/assets/logo.png -------------------------------------------------------------------------------- /config-ui/src/components/ActionCommentTable.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 60 | 61 | 71 | -------------------------------------------------------------------------------- /config-ui/src/components/Code.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /config-ui/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /config-ui/src/components/Key.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 50 | 51 | 70 | -------------------------------------------------------------------------------- /config-ui/src/components/NavigationDrawer.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 118 | 119 | 124 | -------------------------------------------------------------------------------- /config-ui/src/components/Table.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 65 | -------------------------------------------------------------------------------- /config-ui/src/components/Tip.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/Action.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 105 | 106 | 123 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/ActivateOrRun.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | 40 | 55 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/BuiltinFunction.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/Mouse.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/MyKeymap.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/RadioGroup.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 55 | 56 | 71 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/RemapKey.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 42 | 43 | 48 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/SendKey.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/System.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/Text.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /config-ui/src/components/actions/Window.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /config-ui/src/components/dialog/InputKeyValueDialog.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 62 | 63 | 68 | -------------------------------------------------------------------------------- /config-ui/src/components/dialog/PathDialog.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 53 | 54 | 60 | -------------------------------------------------------------------------------- /config-ui/src/components/dialog/WindowGroupDialog.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /config-ui/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * main.ts 3 | * 4 | * Bootstraps Vuetify and other plugins then mounts the App` 5 | */ 6 | 7 | // Components 8 | import App from './App.vue' 9 | 10 | // Composables 11 | import { createApp } from 'vue' 12 | 13 | // Plugins 14 | import { registerPlugins } from '@/plugins' 15 | 16 | const app = createApp(App) 17 | 18 | registerPlugins(app) 19 | 20 | app.mount('#app') 21 | -------------------------------------------------------------------------------- /config-ui/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/index.ts 3 | * 4 | * Automatically included in `./src/main.ts` 5 | */ 6 | 7 | // Plugins 8 | import { loadFonts } from './webfontloader' 9 | import vuetify from './vuetify' 10 | import pinia from '../store' 11 | import router from '../router' 12 | 13 | // Types 14 | import type { App } from 'vue' 15 | 16 | export function registerPlugins (app: App) { 17 | loadFonts() 18 | app 19 | .use(vuetify) 20 | .use(router) 21 | .use(pinia) 22 | } 23 | -------------------------------------------------------------------------------- /config-ui/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/vuetify.ts 3 | * 4 | * Framework documentation: https://vuetifyjs.com` 5 | */ 6 | 7 | // Styles 8 | import '@mdi/font/css/materialdesignicons.css' 9 | import 'vuetify/styles' 10 | 11 | // Composables 12 | import { createVuetify } from 'vuetify' 13 | 14 | // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides 15 | export default createVuetify({ 16 | theme: { 17 | themes: { 18 | light: { 19 | colors: { 20 | primary: '#1867C0', 21 | secondary: '#5CBBF6', 22 | }, 23 | }, 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /config-ui/src/plugins/webfontloader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/webfontloader.ts 3 | * 4 | * webfontloader documentation: https://github.com/typekit/webfontloader 5 | */ 6 | 7 | export async function loadFonts () { 8 | const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader') 9 | 10 | webFontLoader.load({ 11 | // google: { 12 | // families: ['Roboto:100,300,400,500,700,900&display=swap'], 13 | // }, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /config-ui/src/router/index.ts: -------------------------------------------------------------------------------- 1 | // Composables 2 | import { createRouter, createWebHistory } from 'vue-router' 3 | import CustomHotkey from "@/views/CustomHotkey.vue"; 4 | import Abbr from "@/views/Abbr.vue"; 5 | import HomeSettings from '@/views/HomeSettings.vue'; 6 | import Keymap from "@/views/Keymap.vue"; 7 | 8 | const routes = [ 9 | { 10 | path: "/", 11 | component: HomeSettings 12 | }, 13 | { 14 | path: "/settings", 15 | component: HomeSettings 16 | }, 17 | { 18 | path: "/keymap", 19 | children: [ 20 | { 21 | path: ':id(1)', 22 | component: CustomHotkey 23 | }, 24 | { 25 | path: ':id(2|3)', 26 | component: Abbr 27 | }, 28 | { 29 | path: ':id', 30 | component: Keymap 31 | }, 32 | ] 33 | }, 34 | ] 35 | 36 | const router = createRouter({ 37 | history: createWebHistory(process.env.BASE_URL), 38 | routes, 39 | }) 40 | 41 | export default router 42 | -------------------------------------------------------------------------------- /config-ui/src/store/app.ts: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import { defineStore } from 'pinia' 3 | 4 | export const useAppStore = defineStore('app', { 5 | state: () => ({ 6 | // 7 | }), 8 | }) 9 | -------------------------------------------------------------------------------- /config-ui/src/store/config.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia" 2 | import { computed, ref, watch } from "vue" 3 | import { useRoute } from "vue-router" 4 | import { Action, Config, Keymap } from "@/types/config"; 5 | import { useMyFetch } from "./server"; 6 | import trimStart from "lodash-es/trimStart"; 7 | import { languageMap } from "./language-map"; 8 | import { useThrottleFn } from "@vueuse/core"; 9 | 10 | 11 | const defaultKeyboardLayout = "1 2 3 4 5 6 7 8 9 0\nq w e r t y u i o p\na s d f g h j k l ;\nz x c v b n m , . /\nspace enter backspace - [ ' singlePress" 12 | const keyboardLayout74 = "esc f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12\n` 1 2 3 4 5 6 7 8 9 0 - = backspace\ntab q w e r t y u i o p [ ] \\\ncapslock a s d f g h j k l ; ' enter\nLShift z x c v b n m , . / RShift\nLCtrl LWin LAlt space RAlt RWin RCtrl singlePress" 13 | const keyboardLayout104 = "esc f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12\n` 1 2 3 4 5 6 7 8 9 0 - = backspace\ntab q w e r t y u i o p [ ] \\\ncapslock a s d f g h j k l ; ' enter\nLShift z x c v b n m , . / RShift\nLCtrl LWin LAlt space RAlt RWin RCtrl singlePress\nPrintScreen ScrollLock Pause insert home pgup delete end pgdn up down left right\nnumpad0 numpad1 numpad2 numpad3 numpad4 numpad5 numpad6 numpad7 numpad8 numpad9\nNumpadDot NumpadEnter NumpadAdd NumpadSub NumpadMult NumpadDiv NumLock" 14 | const mouseButtons = "LButton RButton MButton XButton1 XButton2 WheelUp WheelDown WheelLeft WheelRight" 15 | 16 | export const useConfigStore = defineStore('config', () => { 17 | // 根据 url 返回对应的 keymap 18 | const config = fetchConfig() 19 | const route = useRoute() 20 | const keymap = ref() 21 | watch( 22 | () => config.value?.keymaps.find(x => x.id + '' === route.params.id), 23 | (newValue) => { 24 | keymap.value = newValue 25 | hotkey.value = "" // 防止串键, 会导致当前选择的键带到缩写模式中 26 | } 27 | ) 28 | 29 | // 根据选中的 hotkey 和 windowGroupID, 返回对应的 action 30 | const hotkey = ref("") 31 | const windowGroupID = ref(0) 32 | const action = ref({ ...emptyAction }) 33 | watch( 34 | () => _getAction(keymap.value, hotkey.value, windowGroupID.value), 35 | (newValue) => action.value = newValue 36 | ) 37 | 38 | watch(() => [action.value.actionTypeID, action.value.actionValueID], 39 | ([newTypeId, newValueId], [oldTypeid, oldValueId]) => { 40 | if ((newTypeId == 9 || oldTypeid == 9) && (newValueId == 5 || oldValueId == 5 || newValueId == 6 || oldValueId == 6)) { 41 | changeAbbrEnable() 42 | } 43 | } 44 | ) 45 | 46 | function changeAbbrEnable() { 47 | // 获取缩写Keymap 48 | const capsAbbr = config.value!.keymaps[config.value!.keymaps.length - 3] 49 | const seemAbbr = config.value!.keymaps[config.value!.keymaps.length - 2] 50 | // 默认为关闭状态 51 | let capsAbbrEnable = false 52 | let seemAbbrEnable = false 53 | 54 | for (let km: Keymap of enabledKeymaps.value) { 55 | // 不遍历缩写、设置 56 | if (km.id >= 2 && km.id <= 4) { 57 | console.log(km.id) 58 | continue; 59 | } 60 | 61 | // 当两个缩写状态都为开启时不再遍历 62 | if (capsAbbrEnable && seemAbbrEnable) { 63 | break 64 | } 65 | 66 | for (let key: string in km.hotkeys) { 67 | for (let act: Action of km.hotkeys[key]) { 68 | // 有选择caps命令将命令状态设置为开启 69 | if (act.actionTypeID == 9 && act.actionValueID == 6) { 70 | capsAbbrEnable = true 71 | continue 72 | } 73 | 74 | // 有选择缩写将缩写状态设置为开启 75 | if (act.actionTypeID == 9 && act.actionValueID == 5) { 76 | seemAbbrEnable = true 77 | } 78 | } 79 | } 80 | } 81 | 82 | // 设置缩写的状态 83 | capsAbbr.enable = capsAbbrEnable 84 | seemAbbr.enable = seemAbbrEnable 85 | } 86 | 87 | const changeActionComment = (label: string) => { 88 | action.value.comment = label 89 | } 90 | 91 | const keymaps = computed(() => config.value!.keymaps) 92 | const options = computed(() => config.value!.options) 93 | 94 | const enabledKeymaps = computed(() => keymaps.value.filter(x => x.enable)) 95 | const customKeymaps = computed(() => keymaps.value.filter(x => x.id > 4)) 96 | const customSonKeymaps = computed(() => customKeymaps.value.filter(x => x.parentID != 0)) 97 | const customParentKeymaps = computed(() => { 98 | const arr = customKeymaps.value.filter(x => x.parentID == 0) 99 | arr.unshift({ id: 0, name: "-", hotkey: "", parentID: 0, enable: true, hotkeys: {}, delay: 0 }) 100 | return arr; 101 | }) 102 | 103 | 104 | const hotkeys = computed(() => keymap.value?.hotkeys!) 105 | 106 | function changeHotkey(oldHotkey: string, newHotkey: string) { 107 | // 如果热键已存在且当前键的action.actionTypeID 为0删除当前热键,不为0删除之前的热键 108 | if (newHotkey in hotkeys.value) { 109 | if (hotkeys.value[oldHotkey][0].actionTypeID == 0) { 110 | removeHotkey(oldHotkey) 111 | return newHotkey 112 | } else { 113 | removeHotkey(newHotkey) 114 | } 115 | } 116 | 117 | // 如果直接替换会导致hotkey的位置在最下方 118 | keymap.value!.hotkeys = Object.keys(hotkeys.value).reduce((result: { [key: string]: Array }, key) => { 119 | if (key == oldHotkey) { 120 | result[newHotkey] = hotkeys.value[key]; 121 | } else { 122 | result[key] = hotkeys.value[key]; 123 | } 124 | return result; 125 | }, {}) 126 | } 127 | 128 | function removeHotkey(hotkey: string) { 129 | delete hotkeys.value[hotkey] 130 | } 131 | 132 | function addHotKey(key: string = "") { 133 | keymap.value!.hotkeys[key] = [{ ...emptyAction }] 134 | } 135 | 136 | function resetKeyboardLayout(num: number) { 137 | if (num == 0) { 138 | options.value.keyboardLayout = defaultKeyboardLayout 139 | } else if (num == 74) { 140 | options.value.keyboardLayout = keyboardLayout74 141 | } else if (num == 104) { 142 | options.value.keyboardLayout = keyboardLayout104 143 | } else if (num == 1) { 144 | options.value.keyboardLayout += '\n' + mouseButtons 145 | } 146 | } 147 | 148 | function translate(comment: string) { 149 | const m = languageMap as any 150 | if (comment && comment.startsWith('label:')) { 151 | const id = comment.substring(6) 152 | if (m[id]) { 153 | return m[id][config.value!.options.language] ?? m[id]['en'] 154 | } 155 | } 156 | return comment 157 | } 158 | 159 | return { 160 | config, keymap, hotkey, windowGroupID, action, enabledKeymaps, customKeymaps, options, hotkeys, 161 | customParentKeymaps, customSonKeymaps, keymaps, changeActionComment, changeAbbrEnable, resetKeyboardLayout, 162 | changeHotkey, removeHotkey, addHotKey, translate, 163 | disabledKeys: computed(() => _disabledKeys(enabledKeymaps.value)), 164 | getAction: (hotkey: string) => _getAction(keymap.value, hotkey, windowGroupID.value), 165 | saveConfig: useThrottleFn(() => _saveConfig(config.value), 1000), 166 | } 167 | }) 168 | 169 | const emptyAction: Action = { 170 | windowGroupID: 0, 171 | actionTypeID: 0, 172 | isEmpty: true, 173 | } 174 | 175 | function _getAction(keymap: Keymap | undefined, hotkey: string, windowGroupID: number): Action { 176 | if (!keymap || !hotkey) { 177 | return { ...emptyAction } 178 | } 179 | 180 | // keymap 中此热键还不存在, 那么初始化一下 181 | let actions = keymap.hotkeys[hotkey] 182 | if (!actions) { 183 | actions = [] 184 | keymap.hotkeys[hotkey] = actions 185 | // 比如新增了 2 模式, 让它的 singlePress 默认为输入 2 键 186 | if (keymap.isNew && hotkey == 'singlePress') { 187 | console.log('singlePress') 188 | const key = trimStart(keymap.hotkey, ' #!^+<>*~$') 189 | actions.push({ 190 | windowGroupID: 0, 191 | actionTypeID: 6, 192 | isEmpty: false, 193 | keysToSend: '{blind}{' + key + '}', 194 | comment: '输入 ' + key + ' 键' 195 | }) 196 | } 197 | } 198 | 199 | // 选择的 windowGroupID 还没有对应的 action, 那么初始化一下 200 | let found = actions.find(x => x.windowGroupID === windowGroupID) 201 | if (!found) { 202 | found = { ...emptyAction, windowGroupID } 203 | actions.push(found) 204 | actions.sort((a, b) => a.windowGroupID - b.windowGroupID) 205 | } 206 | return found 207 | } 208 | 209 | function _saveConfig(config: Config | undefined) { 210 | if (!config) { 211 | return 212 | } 213 | 214 | // 克隆一下, 然后删掉空的 action 215 | config = JSON.parse(JSON.stringify(config)) 216 | for (const km of config!.keymaps) { 217 | const keySet = new Set(parseKeyboardLayout(config!.options.keyboardLayout, km.hotkey).flatMap(x => x)) 218 | for (const [hk, actions] of Object.entries(km.hotkeys)) { 219 | const filterd = actions.filter(x => !x.isEmpty) 220 | const normalKeymap = km.id > 4 221 | if (filterd.length > 0 && (!normalKeymap || (normalKeymap && keySet.has(hk)))) { 222 | km.hotkeys[hk] = filterd 223 | } else { 224 | delete km.hotkeys[hk] 225 | } 226 | } 227 | } 228 | useMyFetch("/config", { timeout: 1500, }) 229 | .put(config) 230 | .onFetchError(err => { 231 | console.error(err) 232 | alert(`保存失败,可能设置程序被关了, ${err.name}:${err.code}`) 233 | } 234 | ) 235 | } 236 | 237 | function _disabledKeys(keymaps: Keymap[]) { 238 | // 返回值为各个 keymap 该禁用的热键, 比如 {1: {"*3": true} } 239 | const m = {} as any 240 | for (const km of keymaps) { 241 | if (!m[km.id]) { 242 | m[km.id] = {} 243 | } 244 | m[km.id][km.hotkey.toLowerCase()] = true 245 | m[km.id]['*' + km.hotkey.toLowerCase()] = true 246 | 247 | if (!m[km.parentID]) { 248 | m[km.parentID] = {} 249 | } 250 | m[km.parentID][km.hotkey.toLowerCase()] = true 251 | m[km.parentID]['*' + km.hotkey.toLowerCase()] = true 252 | } 253 | return m 254 | } 255 | 256 | function fetchConfig() { 257 | const config = ref() 258 | const { data, error } = useMyFetch("/config").json() 259 | watch(data, (val) => { 260 | val = val! 261 | // 这个字段是后加的, 旧的 config.json 肯定没有此字段, 所以要初始化 262 | if (!val.options.keyboardLayout) { 263 | val.options.keyboardLayout = defaultKeyboardLayout 264 | } 265 | // 初始化语言 266 | if (!val.options.language) { 267 | if (navigator.language.includes('zh-')) { 268 | val.options.language = 'zh' 269 | } else { 270 | val.options.language = 'en' 271 | } 272 | } 273 | // 初始化排除列表 274 | if (val.options.windowGroups[0].id != -1) { 275 | val.options.windowGroups.unshift({ 276 | id: -1, 277 | name: "🚫 Exclude", 278 | value: "", 279 | }) 280 | } 281 | config.value = val 282 | }) 283 | return config 284 | } 285 | 286 | export const parseKeyboardLayout = (layout: string, keymapHotkey: string) => { 287 | const rows = layout 288 | .split('\n') 289 | .filter(x => x.trim()) 290 | .map( 291 | line => { 292 | const keys = line.split(/\s+/).filter(x => x.trim()) 293 | return keys.map(key => { 294 | if (key == 'singlePress') { 295 | return key 296 | } 297 | return '*' + key 298 | }) 299 | } 300 | ) 301 | 302 | if (keymapHotkey.toLowerCase().includes("button")) { 303 | const list = rows.flatMap(x => x) 304 | const res = [] 305 | if (!list.includes("*LButton")) { res.push("*LButton") } 306 | if (!list.includes("*MButton")) { res.push("*MButton") } 307 | if (!list.includes("*RButton")) { res.push("*RButton") } 308 | if (!list.includes("*WheelUp")) { res.push("*WheelUp") } 309 | if (!list.includes("*WheelDown")) { res.push("*WheelDown") } 310 | if (!list.includes("*XButton1")) { res.push("*XButton1") } 311 | if (!list.includes("*XButton2")) { res.push("*XButton2") } 312 | 313 | if (res.length > 0) { 314 | rows.push(res) 315 | } 316 | } 317 | return rows 318 | } 319 | -------------------------------------------------------------------------------- /config-ui/src/store/index.ts: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import { createPinia } from 'pinia' 3 | 4 | export default createPinia() 5 | -------------------------------------------------------------------------------- /config-ui/src/store/language-map.ts: -------------------------------------------------------------------------------- 1 | export const languageList = [ 2 | { title: '简体中文', value: 'zh' }, 3 | { title: 'English', value: 'en' }, 4 | ] 5 | 6 | export const languageMap = { 7 | // window 8 | 1: { zh: "关闭窗口", en: "Close", }, 9 | 2: { zh: "切换到上一个窗口", en: "Previous window", }, 10 | 3: { zh: "在当前程序的窗口间轮换", en: "Cycle through app windows", }, 11 | 4: { zh: "窗口管理 (EDSF切换X关闭,空格选择)", en: "Task switcher (EDSF=↑↓←→,X=Delete", }, 12 | 5: { zh: "上一个虚拟桌面", en: "Previous virtual desktop", }, 13 | 6: { zh: "下一个虚拟桌面", en: "Next virtual desktop", }, 14 | 7: { zh: "移动窗口到下一个显示器", en: "Move window to next monitor", }, 15 | 8: { zh: "关闭窗口 (杀进程)", en: "Kill the active process", }, 16 | 9: { zh: "关闭同类窗口", en: "Close all app windows", }, 17 | 10: { zh: "窗口最小化", en: "Minimize", }, 18 | 11: { zh: "窗口最大化或还原", en: "Maximize", }, 19 | 12: { zh: "窗口居中 (1200x800)", en: "Center (1200x800)", }, 20 | 13: { zh: "窗口居中 (1370x930)", en: "Center (1370x930)", }, 21 | 14: { zh: "切换窗口置顶状态", en: "Always on top", }, 22 | 15: { zh: "让窗口随鼠标拖动", en: "Drag window", }, 23 | 16: { zh: "绑定当前窗口 (长按绑定,短按激活)", en: "Bind window by long press", }, 24 | 25 | // system 26 | 17: { zh: "锁屏", en: "Lock the screen", }, 27 | 18: { zh: "睡眠", en: "Sleep", }, 28 | 19: { zh: "关机", en: "Shutdown", }, 29 | 20: { zh: "重启", en: "Reboot", }, 30 | 21: { zh: "音量调节", en: "Volume control", }, 31 | 22: { zh: "显示器亮度调节", en: "Monitor brightness control", }, 32 | 23: { zh: "重启文件资源管理器", en: "Restart explorer.exe", }, 33 | 24: { zh: "复制文件路径或纯文本", en: "Copy full path of a file ", }, 34 | 2401: { zh: "静音当前应用", en: "Mute active app", }, 35 | 2402: { zh: "打开当前程序所在目录", en: "Show active process in folder", }, 36 | 37 | // mouse 38 | 25: { zh: "鼠标上移", en: "Mouse up" }, 39 | 26: { zh: "鼠标下移", en: "Mouse down" }, 40 | 27: { zh: "鼠标左移", en: "Mouse left" }, 41 | 28: { zh: "鼠标右移", en: "Mouse right" }, 42 | 29: { zh: "滚轮上滑", en: "Wheel up" }, 43 | 30: { zh: "滚轮下滑", en: "Wheel down" }, 44 | 31: { zh: "滚轮左滑", en: "Wheel left" }, 45 | 32: { zh: "滚轮右滑", en: "Wheel right" }, 46 | 33: { zh: "鼠标左键", en: "Left button" }, 47 | 34: { zh: "鼠标右键", en: "Right button" }, 48 | 35: { zh: "鼠标中键", en: "Middle button" }, 49 | 36: { zh: "鼠标左键按下 (之后按空格松开)", en: "Left button down ( Space=Release )", }, 50 | 37: { zh: "移动鼠标到活动窗口", en: "Move mouse to active window", }, 51 | 52 | // text 53 | 38: { zh: "光标 - 上移", en: "Up" }, 54 | 39: { zh: "光标 - 下移", en: "Down" }, 55 | 40: { zh: "光标 - 左移", en: "Left" }, 56 | 41: { zh: "光标 - 右移", en: "Right" }, 57 | 42: { zh: "光标 - 跳到行首", en: "Home" }, 58 | 43: { zh: "光标 - 跳到行尾", en: "End" }, 59 | 44: { zh: "光标 - 上一单词", en: "Ctrl + Left" }, 60 | 45: { zh: "光标 - 下一单词", en: "Ctrl + Right" }, 61 | 62 | 46: { zh: "选择 - 往上", en: "Shift + Up" }, 63 | 47: { zh: "选择 - 往下", en: "Shift + Down" }, 64 | 48: { zh: "选择 - 往左", en: "Shift + Left" }, 65 | 49: { zh: "选择 - 往右", en: "Shift + Right" }, 66 | 50: { zh: "选择 - 选到行首", en: "Shift + Home" }, 67 | 51: { zh: "选择 - 选到行尾", en: "Shift + End" }, 68 | 52: { zh: "选择 - 上一单词", en: "Ctrl+Shift+Left" }, 69 | 53: { zh: "选择 - 下一单词", en: "Ctrl+Shift+Right" }, 70 | 71 | 54: { zh: "右键菜单", en: "Menu key" }, 72 | 55: { zh: "删除一行文本", en: "Delete a line" }, 73 | 56: { zh: "删除一个单词", en: "Delete a word" }, 74 | 57: { zh: "Shift 键", en: "Shift" }, 75 | 58: { zh: "Ctrl 键", en: "Ctrl" }, 76 | 59: { zh: "Alt 键", en: "Alt" }, 77 | 60: { zh: "Win 键", en: "Win" }, 78 | 61: { zh: "中英文之间加空格", en: "中英文间加空格" }, 79 | 80 | 62: { en: "Esc" }, 81 | 63: { en: "Backspace" }, 82 | 64: { en: "Enter" }, 83 | 65: { en: "Delete" }, 84 | 66: { en: "Insert" }, 85 | 67: { en: "Tab" }, 86 | 68: { en: "Ctrl + Tab" }, 87 | 69: { en: "Shift + Tab" }, 88 | 70: { en: "Ctrl+Shift+Tab" }, 89 | 90 | // MyKeymap 91 | 71: { zh: "暂停 MyKeymap", en: "Pause" }, 92 | 72: { zh: "重启 MyKeymap", en: "Reload" }, 93 | 73: { zh: "退出 MyKeymap", en: "Exit" }, 94 | 74: { zh: "打开 MyKeymap 设置", en: "Settings" }, 95 | 96 | 75: { zh: "触发 Abbreviation", en: "Abbreviation" }, 97 | 76: { zh: "触发 Command", en: "Command" }, 98 | 77: { zh: "切换 CapsLock 大小写", en: "CapsLock" }, 99 | 78: { zh: "锁定当前模式 (免去按住触发键)", en: "Hold down/release the modifier key" }, 100 | 101 | // Action types 102 | 200: { zh: "⛔ 未配置", en: "⛔ Undefined" }, 103 | 201: { zh: "🚀 启动程序或激活窗口", en: "🚀 App Launcher" }, 104 | 202: { zh: "🖥️ 系统控制", en: "🖥️ System" }, 105 | 203: { zh: "🏠 窗口操作", en: "🏠 Window" }, 106 | 204: { zh: "🖱️ 鼠标操作", en: "🖱️ Mouse" }, 107 | 205: { zh: "🅰️ 重映射按键", en: "🅰️ Remap Keys" }, 108 | 206: { zh: "🅰️ 输入按键或文本", en: "🅰️ Send Keys" }, 109 | 207: { zh: "📚 文字编辑相关", en: "📚 Edit" }, 110 | 208: { zh: "⚛️ 自定义函数", en: "⚛️ Custom Functions" }, 111 | 209: { zh: "⚙️ MyKeymap 相关", en: "⚙️ MyKeymap" }, 112 | 113 | // App Launcher 114 | 301: { zh: "要激活的窗口 (窗口标识符)", en: "The window to activate" }, 115 | 302: { zh: "当窗口不存在时要启动的: 程序 / 文件夹 / URL", en: "Target" }, 116 | 303: { zh: "命令行参数", en: "Arguments" }, 117 | 304: { zh: "工作目录", en: "Working directory" }, 118 | 305: { zh: "备注", en: "Comment" }, 119 | 306: { zh: "以管理员运行", en: "Admin" }, 120 | 307: { zh: "后台运行", en: "Hide" }, 121 | 308: { zh: "检测隐藏窗口", en: "Detect hidden windows" }, 122 | 309: { zh: "🔍 查看窗口标识符", en: "🔍 Window Spy" }, 123 | 124 | // Other 125 | 401: { zh: "重映射为", en: "Remap to" }, 126 | 402: { zh: "要输入的按键或文本", en: "Keys" }, 127 | 403: { zh: "代码", en: "Code" }, 128 | 404: { zh: "热键", en: "Hotkey" }, 129 | 405: { zh: "新增一个", en: "Add" }, 130 | 406: { zh: "输入ab按回车添加/切换到ab, del ab删除ab, rn cd重命名当前为cd", en: "Input 'ab' and Enter to add a new item. Delete => del ab. Rename => rn cd."}, 131 | 132 | // Settings 133 | 501: { zh: "名称", en: "Name" }, 134 | 502: { zh: "触发键", en: "Modifer" }, 135 | 503: { zh: "上层", en: "Parent" }, 136 | 504: { zh: "开关", en: "" }, 137 | 505: { zh: "其他设置", en: "Other" }, 138 | 506: { zh: "开机自启", en: "Run on system startup" }, 139 | 507: { zh: "保存配置(CTRL+S)", en: "Save(CTRL+S)" }, 140 | 141 | 142 | // Window Groups 143 | 601: { zh: "😺 编辑程序分组", en: "😺 Window Groups" }, 144 | 602: { zh: "组名", en: "Group Name" }, 145 | 603: { zh: "窗口标识符", en: "Window List" }, 146 | 604: { zh: "条件", en: "Condition" }, 147 | 605: { zh: "是前台窗口", en: "is active" }, 148 | 606: { zh: "这些窗口存在", en: "exist" }, 149 | 607: { zh: "不是前台窗口", en: "not active" }, 150 | 608: { zh: "这些窗口不存在", en: "not exist" }, 151 | 609: { zh: "添加一行", en: "Add" }, 152 | 610: { zh: "保存", en: "Save" }, 153 | 611: { zh: "取消", en: "Cancel" }, 154 | 612: { zh: "注意: MyKeyamp 的所有热键在 Exclude 组中会被禁用", en: "Note: MyKeymap will be disabled in the \"Exclude\" group." }, 155 | 156 | // Mouse Options 157 | 701: { zh: "🖱️ 修改鼠标参数", en: "🖱️ Mouse Options" }, 158 | 702: { zh: "鼠标移动相关参数", en: "Mouse Options" }, 159 | 703: { zh: "进入连续移动前的延时(秒)", en: "Delay 1" }, 160 | 704: { zh: "两次移动的间隔时间(秒)", en: "Delay 2" }, 161 | 705: { zh: "快速模式步长(像素)", en: "Length 1" }, 162 | 706: { zh: "快速模式首步长(像素)", en: "Length 2" }, 163 | 707: { zh: "慢速模式步长(像素)", en: "Length 3" }, 164 | 708: { zh: "慢速模式首步长(像素)", en: "Length 4" }, 165 | 709: { zh: "鼠标模式的提示符", en: "Prompt" }, 166 | 710: { zh: "提示进入了鼠标模式", en: "Show prompt" }, 167 | 711: { zh: "点击鼠标后不退出鼠标模式", en: "Use only the space key to exit" }, 168 | 712: { zh: "滚轮相关参数", en: "Scroll Wheel Options" }, 169 | 713: { zh: "进入连续滚动前的延时 (秒)", en: "Delay 1" }, 170 | 714: { zh: "两次滚动的间隔时间 (越小滚动速度越快)", en: "Delay 2" }, 171 | 715: { zh: "一次滚动的行数", en: "Lines to scroll at a time" }, 172 | 173 | // Keyboard Layout 174 | 721: { zh: "⌨️ 修改键盘布局", en: "⌨️ Keyboard Layout" }, 175 | 722: { zh: "键盘布局", en: "Keyboard Layout" }, 176 | 723: { zh: "重置为默认值", en: "Default" }, 177 | 724: { zh: "重置为 74 键", en: "74 Key" }, 178 | 725: { zh: "重置为 104 键", en: "104 Key" }, 179 | 726: { zh: "添加鼠标按钮", en: "Add Mouse Buttons" }, 180 | 181 | // Command Window 182 | 741: { zh: "✨ 命令框皮肤", en: "✨ Command Window" }, 183 | 742: { zh: "命令框皮肤", en: "Command Window" }, 184 | 743: { zh: "窗口宽度", en: "Width" }, 185 | 744: { zh: "窗口 Y 轴位置 (百分比)", en: "Y pos" }, 186 | 745: { zh: "窗口圆角大小", en: "Border radius" }, 187 | 746: { zh: "窗口动画持续时间", en: "Animation duration" }, 188 | 747: { zh: "窗口背景色", en: "Background color" }, 189 | 748: { zh: "透明度", en: "Opacity" }, 190 | 749: { zh: "网格线颜色", en: "Gridline color" }, 191 | 750: { zh: "边框宽度", en: "Border width" }, 192 | 751: { zh: "边框颜色", en: "Border color" }, 193 | 752: { zh: "按键颜色", en: "Key color" }, 194 | 753: { zh: "四角颜色", en: "Corner color" }, 195 | 754: { zh: "窗口阴影大小", en: "Drop shadow size" }, 196 | 755: { zh: "窗口阴影颜色", en: "Drop shadow color" }, 197 | 198 | 199 | 761: { zh: "🕗 设置触发延时", en: "🕗 Modifer Delay" }, 200 | 762: { zh: "触发延时 (毫秒)", en: "Modifer Delay (ms)" }, 201 | 763: { zh: "一般推荐设为 0,让模式立刻生效。", en: "If delay > 0, you need long press to use the keymap." }, 202 | 764: { zh: "如果设置大于零的值,即通过长按触发模式,也许能减少打字误触。", en: "" }, 203 | 765: { zh: "但会有另一种形式的误触,比如想输入热键,但长按时间不够,所以触发热键失败。", en: "" }, 204 | 205 | 781: { zh: "🌐 切换语言", en: "🌐 Change Language" }, 206 | 207 | 208 | 209 | }; 210 | -------------------------------------------------------------------------------- /config-ui/src/store/server.ts: -------------------------------------------------------------------------------- 1 | import { createFetch } from "@vueuse/core" 2 | 3 | export const useMyFetch = createFetch({ 4 | baseUrl: import.meta.env.MODE == 'development' ? 'http://localhost:12333' : '', 5 | options: { 6 | } 7 | }) 8 | 9 | 10 | export const server = { 11 | runWindowSpy: () => useMyFetch('/server/command/2').post(), 12 | enableRunAtStartup: () => useMyFetch('/server/command/3').post(), 13 | disableRunAtStartup: () => useMyFetch('/server/command/4').post(), 14 | } 15 | -------------------------------------------------------------------------------- /config-ui/src/store/shortcut.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, watch } from 'vue' 3 | import { useMyFetch } from './server' 4 | 5 | interface Shortcut { 6 | path: string 7 | } 8 | 9 | export const useShortcutStore = defineStore('shortcut', () => { 10 | const shortcuts = fetchShortcuts() 11 | return { shortcuts } 12 | }) 13 | 14 | const fetchShortcuts = () => { 15 | const shortcuts = ref() 16 | const { data, error } = useMyFetch('/shortcuts').json() 17 | watch(data, (newValue) => shortcuts.value = newValue?.map(x => x.path)) 18 | return shortcuts 19 | } 20 | -------------------------------------------------------------------------------- /config-ui/src/styles/settings.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * src/styles/settings.scss 3 | * 4 | * Configures SASS variables and Vuetify overwrites 5 | */ 6 | 7 | // https://vuetifyjs.com/features/sass-variables/` 8 | // @use 'vuetify/settings' with ( 9 | // $color-pack: false 10 | // ); 11 | -------------------------------------------------------------------------------- /config-ui/src/types/config.ts: -------------------------------------------------------------------------------- 1 | export interface Action { 2 | isEmpty: boolean 3 | windowGroupID: number 4 | actionTypeID: number 5 | comment?: string 6 | 7 | actionValueID?: number 8 | 9 | remapToKey?: string 10 | keysToSend?: string 11 | ahkCode?: string 12 | 13 | winTitle?: string 14 | target?: string 15 | args?: string 16 | workingDir?: string 17 | runAsAdmin?: boolean 18 | runInBackground?: boolean 19 | detectHiddenWindow?: boolean 20 | 21 | } 22 | export interface Keymap { 23 | id: number 24 | name: string 25 | enable: boolean 26 | hotkey: string 27 | parentID: number 28 | delay: number 29 | isNew?: boolean 30 | hotkeys: { 31 | [key: string]: Array 32 | } 33 | } 34 | 35 | export interface Scroll { 36 | delay1: string 37 | delay2: string 38 | onceLineCount: string; 39 | } 40 | 41 | export interface Mouse { 42 | delay1: string 43 | delay2: string 44 | fastSingle: string 45 | fastRepeat: string 46 | slowSingle: string 47 | slowRepeat: string 48 | keepMouseMode: boolean 49 | showTip: boolean 50 | tipSymbol: string 51 | } 52 | 53 | export interface WindowGroup { 54 | id: number 55 | name: string 56 | value: string 57 | conditionType?: number 58 | } 59 | 60 | export type PathVariable = { 61 | name: string 62 | value: string 63 | } 64 | 65 | export interface Options { 66 | mykeymapVersion: string 67 | scroll: Scroll 68 | mouse: Mouse 69 | windowGroups: Array 70 | pathVariables: Array 71 | customShellMenu: string 72 | startup: boolean 73 | language: string 74 | keyMapping: string 75 | keyboardLayout: string 76 | commandInputSkin: any 77 | } 78 | 79 | export interface Config { 80 | keymaps: Array 81 | options: Options 82 | } 83 | -------------------------------------------------------------------------------- /config-ui/src/views/Abbr.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 70 | 71 | 73 | -------------------------------------------------------------------------------- /config-ui/src/views/CustomHotkey.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 117 | 118 | 141 | -------------------------------------------------------------------------------- /config-ui/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 34 | -------------------------------------------------------------------------------- /config-ui/src/views/HomeSettings.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /config-ui/src/views/Keymap.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | 42 | 51 | -------------------------------------------------------------------------------- /config-ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /config-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "allowSyntheticDefaultImports": true, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "jsx": "preserve", 11 | "sourceMap": false, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "lib": ["esnext", "dom"], 15 | "types": [ 16 | "node", 17 | "vuetify", 18 | // 不添加这个运行tsc --noEmit会抛出Cannot find name 'BluetoothLEScanFilter' 异常 19 | "web-bluetooth" 20 | ], 21 | "paths": { 22 | "@/*": ["src/*"] 23 | }, 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.d.ts", 28 | "src/**/*.tsx", 29 | "src/**/*.vue", 30 | "vite.config.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /config-ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /config-ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Plugins 2 | import vue from '@vitejs/plugin-vue' 3 | import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' 4 | 5 | // Utilities 6 | import { defineConfig } from 'vite' 7 | import { fileURLToPath, URL } from 'node:url' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | vue({ 13 | template: { transformAssetUrls } 14 | }), 15 | // https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin 16 | vuetify({ 17 | autoImport: true, 18 | styles: { 19 | configFile: 'src/styles/settings.scss', 20 | }, 21 | }), 22 | ], 23 | define: { 'process.env': {} }, 24 | resolve: { 25 | alias: { 26 | '@': fileURLToPath(new URL('./src', import.meta.url)) 27 | }, 28 | extensions: [ 29 | '.js', 30 | '.json', 31 | '.jsx', 32 | '.mjs', 33 | '.ts', 34 | '.tsx', 35 | '.vue', 36 | ], 37 | }, 38 | server: { 39 | port: 3000, 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /data/custom_functions.ahk: -------------------------------------------------------------------------------- 1 | ; 自定义的函数写在这个文件里, 然后能在 MyKeymap 中调用 2 | 3 | ; 使用如下写法,来加载当前目录下的其他 AutoHotKey v2 脚本 4 | ; #Include ../data/test.ahk 5 | 6 | sendSomeChinese() { 7 | Send("{text}你好中文!") 8 | } -------------------------------------------------------------------------------- /doc/features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/doc/features.png -------------------------------------------------------------------------------- /doc/settings.en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/doc/settings.en.png -------------------------------------------------------------------------------- /doc/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/doc/settings.png -------------------------------------------------------------------------------- /doc/夏日大作战.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/doc/夏日大作战.gif -------------------------------------------------------------------------------- /lanzou_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | try: 4 | from lanzou.api import LanZouCloud 5 | except: 6 | print('missing package: pip3 install lanzou-api') 7 | exit(1) 8 | 9 | def show_progress(file_name, total_size, now_size): 10 | """显示进度的回调函数""" 11 | percent = now_size / total_size 12 | bar_len = 40 # 进度条长总度 13 | bar_str = '>' * round(bar_len * percent) + '=' * round(bar_len * (1 - percent)) 14 | print('\r{:.2f}%\t[{}] {:.1f}/{:.1f}MB | {} '.format( 15 | percent * 100, bar_str, now_size / 1048576, total_size / 1048576, file_name), end='') 16 | if total_size == now_size: 17 | print('') # 下载完成换行 18 | 19 | 20 | def handler(fid, is_file): 21 | if is_file: 22 | # lzy.set_desc(fid, '设置文件的描述', is_file=True) 23 | lzy.set_passwd(fid, '1234', is_file) 24 | info = lzy.get_share_info(fid, is_file) 25 | json.dump({'url': info.url, 'password': info.pwd}, sys.stderr) 26 | print('', file=sys.stderr) 27 | 28 | def assertSuccess(code): 29 | if code != LanZouCloud.SUCCESS: 30 | raise RuntimeError("error code: " + str(code)) 31 | 32 | def login(): 33 | with open('lanzou_cookie.txt', 'r') as f: 34 | lines = f.read().splitlines() 35 | cookie = {'ylogin': lines[0], 'phpdisk_info': lines[1]} 36 | code = lzy.login_by_cookie(cookie) 37 | assertSuccess(code) 38 | 39 | if __name__ == '__main__': 40 | lzy = LanZouCloud() 41 | # 突然出现的 bug, 似乎换 ua 能解决: https://github.com/zaxtyson/LanZouCloud-API/issues/101 42 | lzy._headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" 43 | file = sys.argv[1] 44 | login() 45 | code = lzy.upload_file(file, -1, callback=show_progress, uploaded_handler=handler) 46 | assertSuccess(code) -------------------------------------------------------------------------------- /readme.en.md: -------------------------------------------------------------------------------- 1 | # MyKeymap 2 | 3 | A program helps you improve the efficiency of using the keyboard. 4 | 5 | 6 | ## Features 7 | 8 | - Quickly start and switch any application 9 | - ... 10 | 11 | ## Usage 12 | 13 | - Enter `CapsLock`, `S`, `E` to open settings page. 14 | 15 | ## Screenshots 16 | ![settings](./doc/settings.en.png) 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![en](https://img.shields.io/badge/lang-en-red.svg)](https://github.com/xianyukang/MyKeymap/blob/master/readme.en.md) 2 | 3 | # MyKeymap 4 | 5 | MyKeymap 是一款基于 [AutoHotkey](https://www.autohotkey.com/) 的键盘映射工具,用于增强 Windows 的键盘输入体验和窗口操作效率。 6 | 7 | ## Features 8 | 9 | - **程序启动切换**: 用快捷键启动程序和切换窗口,对比搜索型的启动器,效率更高 10 | - **键盘控制鼠标**: 用键盘控制鼠标,能减少键鼠切换,不必为了点一下而大幅移动手掌 11 | - **按键重新映射**: 12 | - 把常用按键重映射到主键区,能大大提升输入速度、编辑文字的效率 13 | - 内置了几套键位负责:「 光标控制 」、「 数字输入 」、「 符号输入 」 14 | 15 | ## Usage 16 | 17 | - [快速入门](https://xianyukang.com/MyKeymap.html#mykeymap-%E7%AE%80%E4%BB%8B) & [视频介绍](https://www.bilibili.com/video/BV1Sf4y1c7p8) 18 | - [MyKeymap 2.0-beta33](https://wwqw.lanzouu.com/irujX2nesore) ( 提取码 1234 ) 19 | 20 | | ![features](./doc/features.png) | ![夏日大作战](./doc/夏日大作战.gif) | 21 | | ------------------------------- | ----------------------------------- | 22 | 23 | ## Screenshots 24 | ![settings](./doc/settings.png) 25 | -------------------------------------------------------------------------------- /tailwind/help_page.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['../config-ui/src/components/HelpPage.vue'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /tailwind/help_page.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind/html-tools.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | '../bin/html-tools/**/*.html', 4 | ], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } 10 | -------------------------------------------------------------------------------- /tailwind/index.css: -------------------------------------------------------------------------------- 1 | /* 因为关掉了 tailwind 的 preflight, 所以少了某些依赖样式, 这里补上 */ 2 | .tailwind-scope * { 3 | border-width: 0; 4 | border-style: solid; 5 | } 6 | 7 | .tailwind-scope table { 8 | border-collapse: collapse; 9 | } 10 | 11 | @tailwind base; 12 | @tailwind components; 13 | @tailwind utilities; 14 | -------------------------------------------------------------------------------- /tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dumb-package", 3 | "version": "1.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tailwind -i index.css -o ../config-ui/src/style/tailwind.css", 8 | "watch": "tailwind --watch -i index.css -o ../config-ui/src/style/tailwind.css", 9 | "build-help-page-css": "tailwind -c help_page.config.js -i help_page.css -o ../bin/site/css/HelpPage.css", 10 | "watch-help-page-css": "tailwind -c help_page.config.js -i help_page.css -o ../config-ui/src/style/HelpPage.css --watch", 11 | "build-html-tools-css": "tailwind -c html-tools.config.js -i help_page.css -o ../bin/site/css/tailwind.css", 12 | "watch-html-tools-css": "tailwind -c html-tools.config.js -i help_page.css -o ../bin/html-tools/tailwind.css --watch" 13 | }, 14 | "devDependencies": { 15 | "tailwindcss": "^3.0.23" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tailwind/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | important: '.tailwind-scope', 3 | corePlugins: { 4 | preflight: false, 5 | }, 6 | content: ['../config-ui/src/**/*.vue'], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /tools/Rexplorer_x64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xianyukang/MyKeymap/968394b4bfceace5c911dee0753254cfa1160b94/tools/Rexplorer_x64.exe -------------------------------------------------------------------------------- /tools/工具来源.txt: -------------------------------------------------------------------------------- 1 | Rexplorer_x64 用于重启文件资源管理器 2 | https://www.sordum.org/9192/restart-explorer-v1-7/ -------------------------------------------------------------------------------- /变量方法差别.md: -------------------------------------------------------------------------------- 1 | 以下是配置中的变量名、方法名变量的变化 2 | 左边为V1 右边为V2 3 | 4 | 5 | 6 | ### 方法 7 | centerMouse => MouseToActiveWindowCenter 8 | 9 | close_window_processes =>CloseWindowProcesses 10 | setColor => changeTextStyle 11 | close_same_class_window => CloseSameClassWindows 12 | set_window_position_and_size => SetWindowPositionAndSize 13 | launch_multiple => LaunchMultiple 14 | activate_it_by_hotkey_or_run => ProcessExistSendKeyOrRun 15 | wrap_selected_text => WrapSelectedText 16 | 17 | 18 | 19 | 20 | ### 删除方法 -------------------------------------------------------------------------------- /误报病毒时执行这个.bat: -------------------------------------------------------------------------------- 1 | @bin\settings.exe UseOriginalAHK 2 | @pause --------------------------------------------------------------------------------