├── README.md ├── Spoons ├── AppKeyable.spoon │ ├── config.lua │ ├── docs.json │ └── init.lua ├── ReloadConfiguration.spoon │ ├── docs.json │ └── init.lua └── SpeedMenu.spoon │ ├── docs.json │ └── init.lua ├── init.example.lua ├── init.lua ├── profile.lua └── screenshot ├── app-hotkey-help.jpg └── preview.png /README.md: -------------------------------------------------------------------------------- 1 | # Hammerspoon 为常用 App 绑定快捷键 2 | 3 | ## 预览 4 | 5 | ![打开 App 的快捷键清单](screenshot/preview.png) 6 | 7 | ## 用法 8 | 9 | 1. `caps + 字符` - 打开或切换到指定的 App (`caps` 代指 `CapsLock` 这个大小写切换键) 10 | 1. `caps + /` - 显示已绑定快捷键的 App 11 | 12 | Tip: 连按同一个快捷键可以在当前 App 的各窗口间循环切换,比如连续按 `caps+c` 可以在 `Google Chrome` 的各窗口间切换。 13 | 14 | ## 依赖 15 | 16 | 1. [Hammerspoon.app](https://www.hammerspoon.org/) 17 | 1. [Karabiner-Elements.app](https://karabiner-elements.pqrs.org/) - 把 `CapsLock` 变成 `^ + ⌥ + ⌘`(仅与其它键组合按时才变) 18 | 19 | ## 安装 20 | 21 | **方法一** 22 | 23 | 推荐第一次使用的用户 24 | 25 | ``` 26 | mv ~/.hammerspoon ~/.hammerspoon_bak 27 | git clone https://github.com/git4coder/hammerspoon_config.git ~/.hammersoppn 28 | mv init.lua init.lua.bak 29 | mv init.example.lua init.lua 30 | ``` 31 | 32 | **方法二** 33 | 34 | Hammerspoon 用户可使用这个方法追加到已有配置中 35 | 36 | 下载 [Spoon/Appkeyable.spoon](https://github.com/git4coder/hammerspoon_config/releases) 后双击打开即可自动导入 37 | 38 | ## 配置 39 | 40 | 编辑 `./Spoons/AppKeyable.spoon/config.lua`,在 `applications` 中修改 app 及其快捷键(不推荐); 41 | 42 | 或在 `~/.hammerspoon/init.lua` 中添加以下内容(推荐): 43 | 44 | ```lua 45 | hs.loadSpoon("AppKeyable") 46 | spoon.AppKeyable.config.hyper = {'ctrl', 'alt', 'cmd'} -- 不要使用 shift,原因见下文 47 | spoon.AppKeyable.config.applications = { -- 注意: path 中出现 CJK 字符会导致 `caps + /` 的排版错乱(https://en.wikipedia.org/wiki/CJK_characters) 48 | {key = 'a', path = '/Applications/Affinity Photo.app'}, 49 | {key = 'b', path = '/Applications/Bear.app'}, 50 | {key = 'B', path = '/Applications/Blender.app'}, 51 | -- more applications ... 52 | } 53 | spoon.AppKeyable.config.functions = {} -- 空 table 关掉自带的 functions,详见 `./Spoons/AppKeyable.spoon/config.lua` 54 | spoon.AppKeyable:start() 55 | ``` 56 | 57 | **注意 key 是区分大小写的**,当设置为大写时快捷按需要增加一个 `shift`,例: 58 | 59 | ``` 60 | 'key' = 'A' --> CapsLock + Shift + a 61 | 'key' = 'a' --> CapsLock + a 62 | 'key' = '@' --> CapsLock + Shift + 2 63 | 'key' = '2' --> CapsLock + 2 64 | 'key' = '<' --> CapsLock + Shift + , 65 | 'key' = ',' --> CapsLock + , 66 | ``` 67 | 68 | ## 文件清单 69 | 70 | 以下功能需要在 `init.lua` 中启用: 71 | 72 | 1. `Spoons/AppKeyable.spoon` 为常用 App 绑定快捷键,核心功能,必须使用 73 | 1. `Spoons/ReloadConfiguration.spoon` 自动加载新配置,额外功能,非必须 74 | 1. `Spoons/SpeedMenu.spoon` 状态栏显示网速,额外功能,非必须 75 | 76 | ## Karabiner-Elements 里设置 hyper 键的 json 77 | 78 | * 按下 `CapsLock + {其它键}` 时相当于按下 `command + option + control + {其它键}` 79 | * 当没有按下 `{其它键}` 时还是本身的 `CapsLock` 的功能 80 | 81 | ```jsonnet 82 | // ~/.config/karabiner/assets/complex_modifications/capslock2hyper.json 83 | 84 | { 85 | "title": "capslock2hyper", 86 | "rules": [ 87 | { 88 | "description": "Hyper(⌃⌥⌘)", 89 | "manipulators": [ 90 | { 91 | "from": { 92 | "key_code": "caps_lock", 93 | "modifiers": { 94 | "optional": ["any"] 95 | } 96 | }, 97 | "to": [ 98 | { 99 | "key_code": "right_control", 100 | "modifiers": ["right_command", "right_option"] 101 | } 102 | ], 103 | "to_if_alone": { 104 | "hold_down_milliseconds": 100, 105 | "key_code": "caps_lock" 106 | }, 107 | "type": "basic" 108 | } 109 | ] 110 | }, 111 | { 112 | "description": "独按两边的 shift 改为 f17 和 f18", 113 | "manipulators": [ 114 | { 115 | "type": "basic", 116 | "from": { 117 | "key_code": "left_shift", 118 | "modifiers": { 119 | "optional": [ 120 | "any" 121 | ] 122 | } 123 | }, 124 | "to": [ 125 | { 126 | "key_code": "left_shift" 127 | } 128 | ], 129 | "to_if_alone": [ 130 | { 131 | "key_code": "f17" 132 | } 133 | ] 134 | }, 135 | { 136 | "type": "basic", 137 | "from": { 138 | "key_code": "right_shift", 139 | "modifiers": { 140 | "optional": [ 141 | "any" 142 | ] 143 | } 144 | }, 145 | "to": [ 146 | { 147 | "key_code": "right_shift" 148 | } 149 | ], 150 | "to_if_alone": [ 151 | { 152 | "key_code": "f18" 153 | } 154 | ] 155 | } 156 | ] 157 | } 158 | ] 159 | } 160 | ``` 161 | 162 | ## Other 163 | 164 | 多次点击 Dock 里的微信开发者工具后,会多出一个僵死的,以下可以找到它,但是杀不死…… 165 | ```lua 166 | for i,v in ipairs(hs.application.runningApplications()) do 167 | local title = v:title() 168 | if title == 'wechatwebdevtools' then 169 | local wins = v:allWindows() 170 | print(i, title, #wins) 171 | for a,b in ipairs(wins) do 172 | print(a, b:role(), b:title()) 173 | end 174 | v:kill() 175 | v:kill9() 176 | end 177 | end 178 | ``` 179 | 180 | Windows 用户可[使用 AutoHotKey v1 来实现类似功能](https://blog.upall.cn/3479.html)。 181 | -------------------------------------------------------------------------------- /Spoons/AppKeyable.spoon/config.lua: -------------------------------------------------------------------------------- 1 | -- DON'T EDIT !!!! 2 | -- Default configurations 3 | -- Usage: 4 | -- hs.loadSpoon("AppKeyable") 5 | -- spoon.AppKeyable.config.hyper = {'ctrl', 'alt', 'cmd'} -- NOT'T ADD SHIFT KEY 6 | -- spoon.AppKeyable.config.applications = {{key = 's', path='/Applications/Skype.app'}} 7 | -- spoon.AppKeyable.config.functions = {} -- Empty table to replace default 8 | -- spoon.AppKeyable:start() 9 | 10 | local module = {} 11 | 12 | module.hyper = {'control', 'option', 'command'} -- 不要加 shift,shift 在使用“大写字母”、“需要按Shift才能输入的符号”时会自动补上 13 | module.todoFile = '~/Documents/todo.txt' -- 这是默认值,所以此行可删除 14 | -- lnu 15 | module.applications = { 16 | {key = 'a', color = '#FFFFFF', path = '/System/Applications/App Store.app'}, 17 | {key = 'b', color = '#FFFFFF', path = '/System/Applications/Books.app'}, 18 | {key = 'c', color = '#FFFFFF', path = '/Applications/Google Chrome.app'}, 19 | {key = 'C', color = '#FFFFFF', path = '/System/Applications/Contacts.app'}, 20 | {key = 'e', color = '#FFFFFF', path = '/System/Applications/TextEdit.app'}, -- Editor 21 | {key = 'f', color = '#FFFFFF', path = '/System/Library/CoreServices/Finder.app'}, 22 | {key = 'F', color = '#FFFFFF', path = '/System/Applications/FaceTime.app'}, 23 | {key = 'k', color = '#FFFFFF', path = '/System/Applications/Utilities/Activity Monitor.app'}, 24 | {key = 'm', color = '#FFFFFF', path = '/System/Applications/Messages.app'}, 25 | {key = 's', color = '#FFFFFF', path = '/System/Applications/System Preferences.app'}, 26 | {key = 't', color = '#FFFFFF', path = '/System/Applications/Utilities/Terminal.app'}, 27 | } 28 | 29 | module.functions = { 30 | { 31 | -- 获得当前 APP 的信息 32 | name = 'AppInfo', 33 | key = '.', 34 | fun = function() 35 | local title = hs.application.frontmostApplication():title() 36 | local bundleID = hs.application.frontmostApplication():bundleID() 37 | local path = hs.application.frontmostApplication():path() 38 | local im = hs.keycodes.currentSourceID() 39 | local fillColor = function(string, color, alpha) 40 | return hs.styledtext.new(string ,{ 41 | color = hs.drawing.color.asRGB({hex = color, alpha = 1}), 42 | font = {name = 'Monaco', size = 14} 43 | }) 44 | end 45 | local content = fillColor('', '#666666') 46 | content = content .. fillColor(' Name ', '#666666') .. fillColor(title, '#FFFFFF') .. '\n' 47 | content = content .. fillColor('BundleID ', '#666666') .. fillColor(bundleID, '#FFFFFF') .. '\n' 48 | content = content .. fillColor(' Path ', '#666666') .. fillColor(path, '#FFFFFF') .. '\n' 49 | content = content .. fillColor(' IM ', '#666666') .. fillColor(im, '#FFFFFF') 50 | hs.alert.closeAll() 51 | hs.alert.show( 52 | content, 53 | alertStyle 54 | ) 55 | -- hs.pasteboard.setContents(string.format('%s\n%s\n%s\n%s\n', title, bundleID, path, im)); -- 复制到剪贴板 56 | hs.pasteboard.setContents(path); -- 复制到剪贴板 57 | print('BundleID:', bundleID); 58 | print(' Path:', path); 59 | end 60 | }, 61 | { 62 | -- todo form 63 | name = 'Todo Form', 64 | key = "'", 65 | fun = function() 66 | hs.alert.closeAll(); 67 | hs.focus() 68 | local file = nil ~= module.todoFile and module.todoFile or '~/Documents/todo.txt' 69 | local confirm, content = hs.dialog.textPrompt('请输入需要记录的内容', 'File: ' .. file, '', '保存', '取消') 70 | print(confirm, content); 71 | if ('保存' == confirm and '' ~= content) then 72 | local script = string.format([[ 73 | do shell script "echo $(date) - %s >> %s" 74 | ]], content, file) 75 | print(script) 76 | local rs = hs.osascript.applescript(script) 77 | if rs == true then 78 | hs.alert.show('已记录 ' .. content) 79 | else 80 | -- 保存失败时存入剪贴板并在控制台输出 81 | hs.pasteboard.setContents(content); 82 | hs.alert.show('记录失败,已存入剪贴板 ' .. content) 83 | print('save todo fail:', content) 84 | end 85 | end 86 | end 87 | }, 88 | { 89 | name = 'Todo List', 90 | key = '"', 91 | fun = function() 92 | hs.alert.closeAll(); 93 | hs.focus() 94 | local file = nil ~= module.todoFile and module.todoFile or '~/Documents/todo.txt' 95 | local script = string.format([[ 96 | do shell script "qlmanage -p %s" 97 | ]], file) 98 | print(script) 99 | hs.osascript.applescript(script) 100 | end 101 | }, 102 | { 103 | name = 'Lock Screen', 104 | key = 'l', 105 | fun = function() 106 | hs.caffeinate.lockScreen() 107 | end 108 | } 109 | } 110 | 111 | return module 112 | -------------------------------------------------------------------------------- /Spoons/AppKeyable.spoon/docs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Constant" : [ 4 | 5 | ], 6 | "submodules" : [ 7 | 8 | ], 9 | "Function" : [ 10 | 11 | ], 12 | "Variable" : [ 13 | { 14 | "name" : "hyper", 15 | "stripped_doc" : [ 16 | "hyper key", 17 | "", 18 | "Do not use the shift key. The shift key will be added automatically when the upper case letter is specified.", 19 | "", 20 | "Expmple:", 21 | " `{'ctrl', 'alt', 'cmd'}`" 22 | ], 23 | "doc" : "hyper key\n\nDo not use the shift key. The shift key will be added automatically when the upper case letter is specified.\n\nExpmple:\n `{'ctrl', 'alt', 'cmd'}`", 24 | "parameters" : [ 25 | 26 | ], 27 | "notes" : [ 28 | 29 | ], 30 | "signature" : "AppKeyable.hyper", 31 | "type" : "Variable", 32 | "returns" : [ 33 | 34 | ], 35 | "def" : "AppKeyable.hyper", 36 | "desc" : "hyper key" 37 | }, 38 | { 39 | "name" : "applications", 40 | "stripped_doc" : [ 41 | "applications table", 42 | "", 43 | "Expmple:", 44 | " `{{key = 'a', path = '\/Applications\/Affinity Photo.app'},{key = 'A', path = '\/Applications\/Blender.app'}}`" 45 | ], 46 | "doc" : "applications table\n\nExpmple:\n `{{key = 'a', path = '\/Applications\/Affinity Photo.app'},{key = 'A', path = '\/Applications\/Blender.app'}}`", 47 | "parameters" : [ 48 | 49 | ], 50 | "notes" : [ 51 | 52 | ], 53 | "signature" : "AppKeyable.applications", 54 | "type" : "Variable", 55 | "returns" : [ 56 | 57 | ], 58 | "def" : "AppKeyable.applications", 59 | "desc" : "applications table" 60 | }, 61 | { 62 | "name" : "functions", 63 | "stripped_doc" : [ 64 | "functions table", 65 | "", 66 | "Expmple:", 67 | " `{{key = 'c', name = 'Show Hello', fun = function() hs.alert('hello') end},{key = 'C', name = 'Show World', fun = function() hs.alert('world') end}}" 68 | ], 69 | "doc" : "functions table\n\nExpmple:\n `{{key = 'c', name = 'Show Hello', fun = function() hs.alert('hello') end},{key = 'C', name = 'Show World', fun = function() hs.alert('world') end}}", 70 | "parameters" : [ 71 | 72 | ], 73 | "notes" : [ 74 | 75 | ], 76 | "signature" : "AppKeyable.functions", 77 | "type" : "Variable", 78 | "returns" : [ 79 | 80 | ], 81 | "def" : "AppKeyable.functions", 82 | "desc" : "functions table" 83 | } 84 | ], 85 | "stripped_doc" : [ 86 | 87 | ], 88 | "desc" : "You can use the shortcut key to switch to the specified app, or switch between the windows of the current app.", 89 | "Deprecated" : [ 90 | 91 | ], 92 | "type" : "Module", 93 | "Constructor" : [ 94 | 95 | ], 96 | "Field" : [ 97 | 98 | ], 99 | "Method" : [ 100 | 101 | ], 102 | "Command" : [ 103 | 104 | ], 105 | "doc" : "You can use the shortcut key to switch to the specified app, or switch between the windows of the current app.", 106 | "items" : [ 107 | { 108 | "name" : "applications", 109 | "stripped_doc" : [ 110 | "applications table", 111 | "", 112 | "Expmple:", 113 | " `{{key = 'a', path = '\/Applications\/Affinity Photo.app'},{key = 'A', path = '\/Applications\/Blender.app'}}`" 114 | ], 115 | "doc" : "applications table\n\nExpmple:\n `{{key = 'a', path = '\/Applications\/Affinity Photo.app'},{key = 'A', path = '\/Applications\/Blender.app'}}`", 116 | "parameters" : [ 117 | 118 | ], 119 | "notes" : [ 120 | 121 | ], 122 | "signature" : "AppKeyable.applications", 123 | "type" : "Variable", 124 | "returns" : [ 125 | 126 | ], 127 | "def" : "AppKeyable.applications", 128 | "desc" : "applications table" 129 | }, 130 | { 131 | "name" : "functions", 132 | "stripped_doc" : [ 133 | "functions table", 134 | "", 135 | "Expmple:", 136 | " `{{key = 'c', name = 'Show Hello', fun = function() hs.alert('hello') end},{key = 'C', name = 'Show World', fun = function() hs.alert('world') end}}" 137 | ], 138 | "doc" : "functions table\n\nExpmple:\n `{{key = 'c', name = 'Show Hello', fun = function() hs.alert('hello') end},{key = 'C', name = 'Show World', fun = function() hs.alert('world') end}}", 139 | "parameters" : [ 140 | 141 | ], 142 | "notes" : [ 143 | 144 | ], 145 | "signature" : "AppKeyable.functions", 146 | "type" : "Variable", 147 | "returns" : [ 148 | 149 | ], 150 | "def" : "AppKeyable.functions", 151 | "desc" : "functions table" 152 | }, 153 | { 154 | "name" : "hyper", 155 | "stripped_doc" : [ 156 | "hyper key", 157 | "", 158 | "Do not use the shift key. The shift key will be added automatically when the upper case letter is specified.", 159 | "", 160 | "Expmple:", 161 | " `{'ctrl', 'alt', 'cmd'}`" 162 | ], 163 | "doc" : "hyper key\n\nDo not use the shift key. The shift key will be added automatically when the upper case letter is specified.\n\nExpmple:\n `{'ctrl', 'alt', 'cmd'}`", 164 | "parameters" : [ 165 | 166 | ], 167 | "notes" : [ 168 | 169 | ], 170 | "signature" : "AppKeyable.hyper", 171 | "type" : "Variable", 172 | "returns" : [ 173 | 174 | ], 175 | "def" : "AppKeyable.hyper", 176 | "desc" : "hyper key" 177 | } 178 | ], 179 | "name" : "AppKeyable" 180 | } 181 | ] 182 | -------------------------------------------------------------------------------- /Spoons/AppKeyable.spoon/init.lua: -------------------------------------------------------------------------------- 1 | --- === AppKeyable === 2 | --- 3 | --- You can use the shortcut key to switch to the specified app, or switch between the windows of the current app. 4 | local obj = { 5 | name = 'AppKeyable', 6 | version = '1.0', 7 | author = 'Mr.User ', 8 | homepage = 'https://github.com/git4coder/hammerspoon_config/tree/master/Spoons/AppKeyable.spoon', 9 | license = 'MIT - https://opensource.org/licenses/MIT' 10 | } 11 | 12 | local _config = {} 13 | 14 | --- AppKeyable.hyper 15 | --- Variable 16 | --- hyper key 17 | --- 18 | --- Do not use the shift key. The shift key will be added automatically when the upper case letter is specified. 19 | --- 20 | --- Expmple: 21 | --- `{'ctrl', 'option', 'cmd'}` 22 | _config.hyper = nil 23 | 24 | --- AppKeyable.applications 25 | --- Variable 26 | --- applications table 27 | --- 28 | --- Expmple: 29 | --- `{{key = 'a', path = '/Applications/Affinity Photo.app'},{key = 'A', path = '/Applications/Blender.app'}}` 30 | _config.applications = nil 31 | 32 | --- AppKeyable.functions 33 | --- Variable 34 | --- functions table 35 | --- 36 | --- Expmple: 37 | --- `{{key = 'c', name = 'Show Hello', fun = function() hs.alert('hello') end},{key = 'C', name = 'Show World', fun = function() hs.alert('world') end}} 38 | _config.functions = nil 39 | 40 | -- obj.config = {applications = nil} 41 | obj.config = dofile(hs.spoons.scriptPath() .. 'config.lua') or {} 42 | obj.__index = obj 43 | 44 | -- 帮助信息的样式 45 | local alertStyle = { 46 | textColor = hs.drawing.color.asRGB( 47 | { 48 | hex = '#FFFFFF', 49 | alpha = 1.00 50 | } 51 | ), -- 文本色 52 | fillColor = hs.drawing.color.asRGB( 53 | { 54 | hex = '#000000', 55 | alpha = 0.90 56 | } 57 | ), -- 背景色 58 | strokeColor = hs.drawing.color.asRGB( 59 | { 60 | hex = '#FFFFFF', 61 | alpha = 0.45 62 | } 63 | ), -- 边框色 64 | radius = 5, -- 边框圆角半径 65 | textFont = 'Monaco', -- 字体 66 | textSize = 12, -- 字号 67 | atScreenEdge = 0, -- 显示位置:0: screen center (default); 1: top edge; 2: bottom edge 68 | fadeInDuration = 0, -- 渐现耗时 69 | fadeOutDuration = 0.5 -- 渐隐耗时 70 | } 71 | 72 | -- 彩色化文本 73 | local fillColor = function(string, color, alpha) 74 | if nil == string then 75 | string = '' 76 | end 77 | if nil == color then 78 | color = '#FFFFFF' 79 | end 80 | if nil == alpha then 81 | alpha = 1 82 | end 83 | return hs.styledtext.new( 84 | string, 85 | { 86 | color = hs.drawing.color.asRGB( 87 | { 88 | hex = color, 89 | alpha = alpha 90 | } 91 | ), 92 | font = { 93 | name = 'Monaco', 94 | size = 14 95 | } 96 | } 97 | ) 98 | end 99 | 100 | -- 文件是否存在 101 | local fileExists = function(path) 102 | local f = io.open(path, 'r') 103 | if f then 104 | io.close(f) -- f:close() 105 | end 106 | return f ~= nil 107 | end 108 | 109 | -- 从 path 中取 app 的名字 110 | local getAppName = function(path) 111 | -- local name = 'NO_NAME'; 112 | -- print(path) 113 | -- return string.match(path, '/([^/]-)%.app$') or name 114 | local appName = string.match(path, '/([^/]-)%.app$') or 'NO_NAME' 115 | -- 不存在的文件名后添加“404” 116 | local isFileExists = fileExists(path) 117 | if false == isFileExists then 118 | appName = appName .. ' - nil' 119 | end 120 | return appName 121 | end 122 | 123 | -- 打开/切换到App(可以在当前 APP 的窗口间切换) 124 | local launchOrFocusWindowByPath = function(path) 125 | return function() 126 | -- 切换时添加提醒信息(hs.hotkey.bind 的 message 去不掉按键提示) 127 | hs.alert.closeAll() 128 | local message = getAppName(path) 129 | local key = 'SwitchTo' 130 | local alertStyle = hs.fnutils.copy(alertStyle) 131 | alertStyle['fadeInDuration'] = 0.2 -- 渐现耗时 132 | alertStyle['fadeOutDuration'] = 0.5 -- 渐隐耗时 133 | for i, v in pairs(obj.config.applications) do 134 | if v.path == path then 135 | key = 'caps-' .. v.key 136 | break 137 | end 138 | end 139 | local total = {} 140 | local bundle = hs.application.infoForBundlePath(path) 141 | if nil ~= bundle then 142 | local appBundleID = bundle['CFBundleIdentifier'] 143 | local app = hs.application.get(appBundleID) 144 | if nil ~= app then 145 | total = 146 | hs.fnutils.filter( 147 | app:allWindows(), 148 | function(item) 149 | return item:role() == 'AXWindow' 150 | end 151 | ) 152 | end 153 | else 154 | print('PathNotFound:', path) 155 | message = fillColor('NotFound: ', '#FF0000') .. fillColor(path, '#666666') 156 | hs.alert.show(message, alertStyle, hs.screen.mainScreen(), 0.5) 157 | return false 158 | end 159 | message = fillColor(key .. ': ', '#666666') .. fillColor(message, '#FFFFFF') .. fillColor(' ' .. #total, '#666666') 160 | hs.alert.show(message, alertStyle, hs.screen.mainScreen(), 0.5) 161 | 162 | -- 不存在时什么也不做 163 | if false == fileExists(path) then 164 | return false 165 | end 166 | 167 | local curApp = hs.application.frontmostApplication() 168 | -- print(getAppName(path)) .. ' <-- ' .. getAppName(curApp:path()) 169 | if curApp:path() == path then -- 当前 APP 就是要打开的 APP 时找到当前 APP 的下一个窗口 170 | -- 获取 APP 的所有窗口(不含 toast、scrollarea 等窗体) 171 | local wins = 172 | hs.fnutils.filter( 173 | curApp:allWindows(), 174 | function(item) 175 | return item:role() == 'AXWindow' 176 | end 177 | ) 178 | --[[ 179 | -- 调试用 180 | print('#wins: ' .. #wins .. ' --> ' .. getAppName(path)) 181 | for i,v in ipairs(wins) do 182 | print(i, v:id(), v:role(), v:title()) 183 | end 184 | ]] 185 | -- 只有一个窗口时直接返回 186 | if #wins == 1 then 187 | for _, v in ipairs(wins) do 188 | if true == v:isMinimized() then 189 | v:unminimize() 190 | end 191 | end 192 | return 193 | end 194 | -- 没有窗口(当前窗口全部关闭后程序并不退出) 195 | if #wins == 0 then 196 | hs.application.launchOrFocus(path) 197 | end 198 | -- 显示当前 APP 的下一个窗口 199 | local curWin = curApp:focusedWindow() 200 | table.sort( 201 | wins, 202 | function(x, y) 203 | return x:id() < y:id() 204 | end 205 | ) 206 | -- 把第一个窗口追加到末尾,用于当前窗口是最后一个窗口时可以快速找到下一个窗口 207 | wins[#wins + 1] = wins[1] 208 | for k, v in ipairs(wins) do 209 | if v:id() == curWin:id() then 210 | if true == wins[k + 1]:isMinimized() then 211 | wins[k + 1]:unminimize() 212 | end 213 | wins[k + 1]:focus() 214 | return 215 | end 216 | end 217 | wins = nil 218 | else -- 当前 APP 不是要打开的 APP 时直接切到 APP 不用切换 APP 的窗口 219 | hs.application.launchOrFocus(path) 220 | -- Finder 比较特殊,可能 focus 不了,需要再来一下,原因不详 221 | local appBundleID = hs.application.infoForBundlePath(path)['CFBundleIdentifier'] 222 | if 'com.apple.finder' == appBundleID then 223 | local app = hs.application.get(appBundleID) -- 参数不能是 Finder 得是 访达,不过支持传 bundleID 224 | local wins = 225 | hs.fnutils.filter( 226 | app:allWindows(), 227 | function(item) 228 | return item:role() == 'AXWindow' 229 | end 230 | ) 231 | if #wins == 1 then -- 有时 Finder active 不了,得全部 active 才有效果 232 | app:activate(true) 233 | else -- 当窗口多时不想全 active ,也不知道在多个 window 时下边这行能不能解决不能 active 的问题 234 | app:activate() 235 | end 236 | wins = nil 237 | end 238 | end 239 | curApp = nil 240 | alertStyle = nil 241 | end 242 | end 243 | 244 | -- 为 applications 和 functions 绑定快捷键的方法 245 | local bindHotkey = function(app) 246 | -- in case called as function 247 | if self ~= obj then 248 | self = obj 249 | end 250 | 251 | local hyper = hs.fnutils.copy(obj.config.hyper) -- clone 252 | -- 找到需要按shift才能打出的符号字母所在的键 253 | local getOriginKey = function(key) 254 | local origin = key 255 | -- 需要按 Shift 才能输出的按键 256 | local shiftKeys = { 257 | ['~'] = '`', 258 | ['!'] = '1', 259 | ['@'] = '2', 260 | ['#'] = '3', 261 | ['$'] = '4', 262 | ['%'] = '5', 263 | ['^'] = '6', 264 | ['&'] = '7', 265 | ['*'] = '8', 266 | ['('] = '9', 267 | [')'] = '0', 268 | ['_'] = '-', 269 | ['+'] = '=', 270 | [':'] = ';', 271 | ['"'] = "'", 272 | ['<'] = ',', 273 | ['>'] = '.', 274 | ['?'] = '/', 275 | ['{'] = '[', 276 | ['}'] = ']', 277 | ['|'] = '\\' 278 | } 279 | -- 检查标点符号 280 | origin = shiftKeys[key] or origin 281 | -- 检查字母 282 | origin = nil ~= string.match(key, '[A-Z]') and string.lower(key) or origin 283 | return origin 284 | end 285 | local key = getOriginKey(app.key) -- 找到大写对应的小写按键、“上档符号”所在的按键(比如“E“在键“e”上、“#”的键是“3”、“?”的键在“/”上) 286 | local message = nil 287 | -- message = nil ~= app.path and getAppName(app.path) or nil 288 | if app.key ~= key then 289 | table.insert(hyper, 'shift') 290 | end 291 | 292 | if nil ~= app.fun or nil ~= app.path then 293 | local bound = hs.hotkey.bind(hyper, key, message, app.fun or launchOrFocusWindowByPath(app.path)) 294 | table.insert(self.boundHotkey, bound) 295 | end 296 | end 297 | 298 | function obj:init() 299 | hs.application.enableSpotlightForNameSearches(true) 300 | for i, v in pairs(_config) do -- ipairs 在遇到第一个 nil 就终止了,nil 会自动跳过 301 | -- print('merge config:', i, v) 302 | self.config[i] = v 303 | end 304 | _config = nil 305 | self.boundHotkey = {} -- 已经绑好的快捷键,用于 release hotkey 306 | return self 307 | end 308 | 309 | function obj:start() 310 | local hyper = 311 | hs.fnutils.filter( 312 | self.config.hyper, 313 | function(v) 314 | return string.lower(v) ~= 'shift' 315 | end 316 | ) -- 排除 shift 317 | -- 为常用工具绑定快捷键 318 | hs.fnutils.each( 319 | self.config.applications, 320 | function(app) 321 | bindHotkey(app) 322 | end 323 | ) 324 | 325 | -- 为功能绑定快捷键 326 | hs.fnutils.each( 327 | self.config.functions, 328 | function(fun) 329 | bindHotkey(fun) 330 | end 331 | ) 332 | 333 | ---- 显示帮助 ? 334 | --local helpHTML = 335 | -- '
' 378 | --local cscreen = hs.screen.mainScreen() 379 | --local cres = cscreen:fullFrame() 380 | --local dialogWidth = 80 * 10 + 30 381 | --local dialogHeight = math.ceil(applicationCount / 10) * 100 382 | --local helpView = 383 | -- hs.webview.new( 384 | -- { 385 | -- x = cres.x + (cres.w - dialogWidth) / 2, 386 | -- y = cres.y + (cres.h - dialogHeight) / 2, 387 | -- w = dialogWidth, 388 | -- h = dialogHeight 389 | -- }, 390 | -- { 391 | -- javaScriptEnabled = false, 392 | -- javaScriptCanOpenWindowsAutomatically = false 393 | -- } 394 | --) 395 | --helpView:shadow(true) 396 | --helpView:closeOnEscape(true) 397 | --helpView:darkMode(true) 398 | --helpView:bringToFront(true) 399 | --helpView:transparent(true) 400 | --helpView:html(helpHTML) 401 | --local helpBound = 402 | -- hs.hotkey.bind( 403 | -- {'control', 'option', 'command', 'shift'}, 404 | -- '/', 405 | -- function() 406 | -- if helpView and helpView:hswindow() and helpView:hswindow():isVisible() then 407 | -- helpView:hide() 408 | -- else 409 | -- helpView:show() 410 | -- end 411 | -- end 412 | --) 413 | --table.insert(self.boundHotkey, helpBound) 414 | 415 | -- 显示帮助 / 416 | local bound = 417 | hs.hotkey.bind( 418 | hyper, 419 | '/', 420 | function() 421 | hs.alert.closeAll() 422 | local info = fillColor('', '#FFFFFF') -- 开头必须是 hs.styledText ,不能是纯文本否则 hs.alert 的颜色不生效 423 | local capsLockSymbol = 'caps' -- '⇪' 424 | 425 | -- 合并 applications 和 functions 426 | local actions = {} 427 | for _, v in ipairs(self.config.applications) do 428 | table.insert(actions, v) 429 | end 430 | for _, v in ipairs(self.config.functions) do 431 | table.insert(actions, v) 432 | end 433 | 434 | table.insert( 435 | actions, 436 | { 437 | name = 'Help', 438 | key = '/' 439 | } 440 | ) 441 | 442 | table.sort( 443 | actions, 444 | function(e1, e2) 445 | local e1Key = string.byte(string.upper(e1.key)) -- ascii 446 | local e2Key = string.byte(string.upper(e2.key)) 447 | -- 先排字母,后排符号,字母前的符号都移到后边 448 | if e1Key < 65 then 449 | e1Key = e1Key + 128 450 | end 451 | if e2Key < 65 then 452 | e2Key = e2Key + 128 453 | end 454 | return e1Key < e2Key 455 | end 456 | ) 457 | 458 | -- 多列显示 459 | local cols = 2 460 | local appNameMaxLen = 0 461 | for _, v in ipairs(actions) do 462 | local appName = nil ~= v.name and v.name or getAppName(v.path) 463 | appNameMaxLen = #appName > appNameMaxLen and #appName or appNameMaxLen 464 | end 465 | local colWidth = #(capsLockSymbol .. '-? ') + appNameMaxLen 466 | if cols <= 0 then 467 | cols = 1 468 | end 469 | 470 | -- 把单词表示的按键转为单个符号(防止显示时错位) 471 | local keyName2KeySymbol = function(name) 472 | local data = { 473 | left = '←', 474 | right = '→', 475 | up = '↑', 476 | down = '↓' 477 | } 478 | return data[name] or name 479 | end 480 | 481 | -- actions 竖排 482 | local totalRow = math.ceil(#actions / cols) -- 0-4,5-9,10-13 size*(p-1) 483 | for i = 1, totalRow, 1 do 484 | local row = fillColor('', '#FFFFFF') 485 | for j = 0, cols - 1, 1 do 486 | local v = actions[i + j * totalRow] 487 | if nil == v then 488 | break 489 | end 490 | local key = capsLockSymbol .. '-' .. keyName2KeySymbol(v.key) 491 | local val = nil ~= v.name and v.name or getAppName(v.path) 492 | local color = v.color or '#FFFFFF' 493 | local item = fillColor(key .. ' ', '#666666') .. fillColor(val, color) 494 | local itemLen = #(capsLockSymbol .. '-? ') + #val -- 不使用 #item 是因为“←”等的长度不是1,可能是2、3(取决于字符的 utf8 长度),会导致对不齐 495 | -- paddingLeft 496 | local colPrefix = fillColor(' ', '#666666') 497 | item = colPrefix .. item 498 | itemLen = #colPrefix + itemLen 499 | -- 每项宽度一样才能对齐列 500 | if (itemLen < colWidth) then 501 | item = item .. string.rep(' ', colWidth - itemLen) 502 | end 503 | -- 列加间距 504 | if j ~= cols - 1 then 505 | item = item .. ' ' 506 | end 507 | -- 合并列为行 508 | row = row .. item 509 | end 510 | info = info .. row 511 | -- 不是最后一页的话加个换行 512 | if (i ~= totalRow) then 513 | info = info .. '\n' 514 | end 515 | end 516 | 517 | local displaySeconds = 1 -- 展示时长 518 | hs.alert.show(info, alertStyle, hs.screen.mainScreen(), displaySeconds) 519 | end 520 | ) 521 | table.insert(self.boundHotkey, bound) 522 | end 523 | 524 | function obj:stop() 525 | for _, v in pairs(self.boundHotkey) do 526 | v:delete() 527 | end 528 | end 529 | 530 | return obj 531 | -------------------------------------------------------------------------------- /Spoons/ReloadConfiguration.spoon/docs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Constant" : [ 4 | 5 | ], 6 | "submodules" : [ 7 | 8 | ], 9 | "Function" : [ 10 | 11 | ], 12 | "Variable" : [ 13 | { 14 | "doc" : "List of directories to watch for changes, defaults to hs.configdir", 15 | "stripped_doc" : [ 16 | "List of directories to watch for changes, defaults to hs.configdir" 17 | ], 18 | "parameters" : [ 19 | 20 | ], 21 | "def" : "ReloadConfiguration.watch_paths", 22 | "notes" : [ 23 | 24 | ], 25 | "signature" : "ReloadConfiguration.watch_paths", 26 | "type" : "Variable", 27 | "returns" : [ 28 | 29 | ], 30 | "name" : "watch_paths", 31 | "desc" : "List of directories to watch for changes, defaults to hs.configdir" 32 | } 33 | ], 34 | "stripped_doc" : [ 35 | 36 | ], 37 | "Deprecated" : [ 38 | 39 | ], 40 | "desc" : "Adds a hotkey to reload the hammerspoon configuration, and a pathwatcher to automatically reload on changes.", 41 | "type" : "Module", 42 | "Constructor" : [ 43 | 44 | ], 45 | "doc" : "Adds a hotkey to reload the hammerspoon configuration, and a pathwatcher to automatically reload on changes.\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/ReloadConfiguration.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/ReloadConfiguration.spoon.zip)", 46 | "Field" : [ 47 | 48 | ], 49 | "Command" : [ 50 | 51 | ], 52 | "Method" : [ 53 | { 54 | "doc" : "Binds hotkeys for ReloadConfiguration\n\nParameters:\n * mapping - A table containing hotkey modifier\/key details for the following items:\n * reloadConfiguration - This will cause the configuration to be reloaded", 55 | "stripped_doc" : [ 56 | "Binds hotkeys for ReloadConfiguration", 57 | "" 58 | ], 59 | "parameters" : [ 60 | " * mapping - A table containing hotkey modifier\/key details for the following items:", 61 | " * reloadConfiguration - This will cause the configuration to be reloaded" 62 | ], 63 | "def" : "ReloadConfiguration:bindHotkeys(mapping)", 64 | "notes" : [ 65 | 66 | ], 67 | "signature" : "ReloadConfiguration:bindHotkeys(mapping)", 68 | "type" : "Method", 69 | "returns" : [ 70 | 71 | ], 72 | "name" : "bindHotkeys", 73 | "desc" : "Binds hotkeys for ReloadConfiguration" 74 | }, 75 | { 76 | "doc" : "Start ReloadConfiguration\n\nParameters:\n * None", 77 | "stripped_doc" : [ 78 | "Start ReloadConfiguration", 79 | "" 80 | ], 81 | "parameters" : [ 82 | " * None" 83 | ], 84 | "def" : "ReloadConfiguration:start()", 85 | "notes" : [ 86 | 87 | ], 88 | "signature" : "ReloadConfiguration:start()", 89 | "type" : "Method", 90 | "returns" : [ 91 | 92 | ], 93 | "name" : "start", 94 | "desc" : "Start ReloadConfiguration" 95 | } 96 | ], 97 | "items" : [ 98 | { 99 | "doc" : "List of directories to watch for changes, defaults to hs.configdir", 100 | "stripped_doc" : [ 101 | "List of directories to watch for changes, defaults to hs.configdir" 102 | ], 103 | "parameters" : [ 104 | 105 | ], 106 | "def" : "ReloadConfiguration.watch_paths", 107 | "notes" : [ 108 | 109 | ], 110 | "signature" : "ReloadConfiguration.watch_paths", 111 | "type" : "Variable", 112 | "returns" : [ 113 | 114 | ], 115 | "name" : "watch_paths", 116 | "desc" : "List of directories to watch for changes, defaults to hs.configdir" 117 | }, 118 | { 119 | "doc" : "Binds hotkeys for ReloadConfiguration\n\nParameters:\n * mapping - A table containing hotkey modifier\/key details for the following items:\n * reloadConfiguration - This will cause the configuration to be reloaded", 120 | "stripped_doc" : [ 121 | "Binds hotkeys for ReloadConfiguration", 122 | "" 123 | ], 124 | "parameters" : [ 125 | " * mapping - A table containing hotkey modifier\/key details for the following items:", 126 | " * reloadConfiguration - This will cause the configuration to be reloaded" 127 | ], 128 | "def" : "ReloadConfiguration:bindHotkeys(mapping)", 129 | "notes" : [ 130 | 131 | ], 132 | "signature" : "ReloadConfiguration:bindHotkeys(mapping)", 133 | "type" : "Method", 134 | "returns" : [ 135 | 136 | ], 137 | "name" : "bindHotkeys", 138 | "desc" : "Binds hotkeys for ReloadConfiguration" 139 | }, 140 | { 141 | "doc" : "Start ReloadConfiguration\n\nParameters:\n * None", 142 | "stripped_doc" : [ 143 | "Start ReloadConfiguration", 144 | "" 145 | ], 146 | "parameters" : [ 147 | " * None" 148 | ], 149 | "def" : "ReloadConfiguration:start()", 150 | "notes" : [ 151 | 152 | ], 153 | "signature" : "ReloadConfiguration:start()", 154 | "type" : "Method", 155 | "returns" : [ 156 | 157 | ], 158 | "name" : "start", 159 | "desc" : "Start ReloadConfiguration" 160 | } 161 | ], 162 | "name" : "ReloadConfiguration" 163 | } 164 | ] -------------------------------------------------------------------------------- /Spoons/ReloadConfiguration.spoon/init.lua: -------------------------------------------------------------------------------- 1 | --- === ReloadConfiguration === 2 | --- 3 | --- Adds a hotkey to reload the hammerspoon configuration, and a pathwatcher to automatically reload on changes. 4 | --- 5 | --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ReloadConfiguration.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ReloadConfiguration.spoon.zip) 6 | 7 | local obj = {} 8 | obj.__index = obj 9 | 10 | -- Metadata 11 | obj.name = "ReloadConfiguration" 12 | obj.version = "1.0" 13 | obj.author = "Jon Lorusso " 14 | obj.homepage = "https://github.com/Hammerspoon/Spoons" 15 | obj.license = "MIT - https://opensource.org/licenses/MIT" 16 | 17 | 18 | --- ReloadConfiguration.watch_paths 19 | --- Variable 20 | --- List of directories to watch for changes, defaults to hs.configdir 21 | obj.watch_paths = { hs.configdir } 22 | 23 | --- ReloadConfiguration:bindHotkeys(mapping) 24 | --- Method 25 | --- Binds hotkeys for ReloadConfiguration 26 | --- 27 | --- Parameters: 28 | --- * mapping - A table containing hotkey modifier/key details for the following items: 29 | --- * reloadConfiguration - This will cause the configuration to be reloaded 30 | function obj:bindHotkeys(mapping) 31 | local def = { reloadConfiguration = hs.fnutils.partial(hs.reload, self) } 32 | hs.spoons.bindHotkeysToSpec(def, mapping) 33 | end 34 | 35 | --- ReloadConfiguration:start() 36 | --- Method 37 | --- Start ReloadConfiguration 38 | --- 39 | --- Parameters: 40 | --- * None 41 | function obj:start() 42 | self.watchers = {} 43 | for _,dir in pairs(self.watch_paths) do 44 | self.watchers[dir] = hs.pathwatcher.new(dir, hs.reload):start() 45 | end 46 | return self 47 | end 48 | 49 | return obj 50 | -------------------------------------------------------------------------------- /Spoons/SpeedMenu.spoon/docs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Constant" : [ 4 | 5 | ], 6 | "submodules" : [ 7 | 8 | ], 9 | "Function" : [ 10 | 11 | ], 12 | "Variable" : [ 13 | 14 | ], 15 | "stripped_doc" : [ 16 | 17 | ], 18 | "Deprecated" : [ 19 | 20 | ], 21 | "desc" : "Menubar netspeed meter", 22 | "type" : "Module", 23 | "Constructor" : [ 24 | 25 | ], 26 | "doc" : "Menubar netspeed meter\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/SpeedMenu.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/SpeedMenu.spoon.zip)", 27 | "Method" : [ 28 | { 29 | "doc" : "Redetect the active interface, darkmode …And redraw everything.", 30 | "stripped_doc" : [ 31 | "Redetect the active interface, darkmode …And redraw everything." 32 | ], 33 | "desc" : "Redetect the active interface, darkmode …And redraw everything.", 34 | "parameters" : [ 35 | 36 | ], 37 | "notes" : [ 38 | 39 | ], 40 | "signature" : "SpeedMenu:rescan()", 41 | "type" : "Method", 42 | "returns" : [ 43 | 44 | ], 45 | "def" : "SpeedMenu:rescan()", 46 | "name" : "rescan" 47 | } 48 | ], 49 | "Field" : [ 50 | 51 | ], 52 | "Command" : [ 53 | 54 | ], 55 | "items" : [ 56 | { 57 | "doc" : "Redetect the active interface, darkmode …And redraw everything.", 58 | "stripped_doc" : [ 59 | "Redetect the active interface, darkmode …And redraw everything." 60 | ], 61 | "desc" : "Redetect the active interface, darkmode …And redraw everything.", 62 | "parameters" : [ 63 | 64 | ], 65 | "notes" : [ 66 | 67 | ], 68 | "signature" : "SpeedMenu:rescan()", 69 | "type" : "Method", 70 | "returns" : [ 71 | 72 | ], 73 | "def" : "SpeedMenu:rescan()", 74 | "name" : "rescan" 75 | } 76 | ], 77 | "name" : "SpeedMenu" 78 | } 79 | ] -------------------------------------------------------------------------------- /Spoons/SpeedMenu.spoon/init.lua: -------------------------------------------------------------------------------- 1 | --- === SpeedMenu === 2 | --- 3 | --- Menubar netspeed meter 4 | --- 5 | --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip) 6 | 7 | local obj={} 8 | obj.__index = obj 9 | 10 | -- Metadata 11 | obj.name = "SpeedMenu" 12 | obj.version = "1.0" 13 | obj.author = "ashfinal " 14 | obj.homepage = "https://github.com/Hammerspoon/Spoons" 15 | obj.license = "MIT - https://opensource.org/licenses/MIT" 16 | 17 | function obj:init() 18 | self.menubar = hs.menubar.new() 19 | obj:rescan() 20 | end 21 | 22 | local function data_diff() 23 | local in_seq = hs.execute(obj.instr) 24 | local out_seq = hs.execute(obj.outstr) 25 | local in_diff = in_seq - obj.inseq 26 | local out_diff = out_seq - obj.outseq 27 | if in_diff/1024 > 1024 then 28 | obj.kbin = string.format("%.0f", in_diff/1024/1024) .. 'M/s' 29 | else 30 | obj.kbin = string.format("%.0f", in_diff/1024) .. 'K/s' 31 | end 32 | if out_diff/1024 > 1024 then 33 | obj.kbout = string.format("%.0f", out_diff/1024/1024) .. 'M' 34 | else 35 | obj.kbout = string.format("%.0f", out_diff/1024) .. 'K' 36 | end 37 | local disp_str = obj.kbout .. ',' .. obj.kbin 38 | local text_style = { 39 | dark = { 40 | font = {name = "Monaco", size = 12.0}, 41 | color = {hex = "#FFFFFF"}, 42 | paragraphStyle = { 43 | alignment = 'right', 44 | } 45 | }, 46 | light = { 47 | font = {name = "Monaco", size = 12.0}, 48 | color = {hex = "#000000"}, 49 | paragraphStyle = { 50 | alignment = 'right', 51 | } 52 | } 53 | } 54 | if obj.darkmode then 55 | obj.disp_str = hs.styledtext.new(disp_str, text_style.dark) 56 | else 57 | obj.disp_str = hs.styledtext.new(disp_str, text_style.light) 58 | end 59 | obj.menubar:setTitle(obj.disp_str) 60 | obj.inseq = in_seq 61 | obj.outseq = out_seq 62 | end 63 | 64 | --- SpeedMenu:rescan() 65 | --- Method 66 | --- Redetect the active interface, darkmode …And redraw everything. 67 | --- 68 | 69 | function obj:rescan() 70 | let , darkmode = hs.osascript.applescript('tell application "System Events"\nreturn dark mode of appearance preferences\nend tell') 71 | obj.interface = hs.network.primaryInterfaces() 72 | obj.darkmode = darkmode 73 | local menuitems_table = {} 74 | if obj.interface then 75 | -- Inspect active interface and create menuitems 76 | local interface_detail = hs.network.interfaceDetails(obj.interface) 77 | if interface_detail.AirPort then 78 | local ssid = interface_detail.AirPort.SSID 79 | table.insert(menuitems_table, { 80 | title = "SSID: " .. ssid, 81 | tooltip = "Copy SSID to clipboard", 82 | fn = function() hs.pasteboard.setContents(ssid) end 83 | }) 84 | end 85 | if interface_detail.IPv4 then 86 | local ipv4 = interface_detail.IPv4.Addresses[1] 87 | table.insert(menuitems_table, { 88 | title = "IPv4: " .. ipv4, 89 | tooltip = "Copy IPv4 to clipboard", 90 | fn = function() hs.pasteboard.setContents(ipv4) end 91 | }) 92 | end 93 | if interface_detail.IPv6 then 94 | local ipv6 = interface_detail.IPv6.Addresses[1] 95 | table.insert(menuitems_table, { 96 | title = "IPv6: " .. ipv6, 97 | tooltip = "Copy IPv6 to clipboard", 98 | fn = function() hs.pasteboard.setContents(ipv6) end 99 | }) 100 | end 101 | local macaddr = hs.execute('ifconfig ' .. obj.interface .. ' | grep ether | awk \'{print $2}\'') 102 | table.insert(menuitems_table, { 103 | title = "MAC: " .. macaddr, 104 | tooltip = "Copy MAC Address to clipboard", 105 | fn = function() hs.pasteboard.setContents(macaddr) end 106 | }) 107 | -- Start watching the netspeed delta 108 | obj.instr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $7}\'' 109 | obj.outstr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $10}\'' 110 | 111 | obj.inseq = hs.execute(obj.instr) 112 | obj.outseq = hs.execute(obj.outstr) 113 | 114 | if obj.timer then 115 | obj.timer:stop() 116 | obj.timer = nil 117 | end 118 | obj.timer = hs.timer.doEvery(1, data_diff) 119 | end 120 | table.insert(menuitems_table, { 121 | title = "Rescan Network Interfaces", 122 | fn = function() obj:rescan() end 123 | }) 124 | obj.menubar:setTitle("⚠︎") 125 | obj.menubar:setMenu(menuitems_table) 126 | end 127 | 128 | return obj 129 | -------------------------------------------------------------------------------- /init.example.lua: -------------------------------------------------------------------------------- 1 | -- 给APP绑定独立的激活键 2 | hs.loadSpoon('AppKeyable') 3 | spoon.AppKeyable.config.applications = { 4 | {key = 'A', color = '#FFFFFF', path = '/System/Applications/App Store.app'}, 5 | {key = 'B', color = '#FFFFFF', path = '/System/Applications/Books.app'}, 6 | {key = 'c', color = '#FFFFFF', path = '/Applications/Google Chrome.app'}, 7 | {key = 'C', color = '#FFFFFF', path = '/System/Applications/Contacts.app'}, 8 | {key = 'd', color = '#FFFFFF', path = '/Applications/DBeaver.app'}, 9 | {key = 'E', color = '#FFFFFF', path = '/System/Applications/TextEdit.app'}, -- Editor 10 | {key = 'e', color = '#03a9f4', path = '/Applications/Typora.app'}, 11 | {key = 'f', color = '#FFFFFF', path = '/System/Library/CoreServices/Finder.app'}, 12 | {key = 'F', color = '#FFFFFF', path = '/System/Applications/FaceTime.app'}, 13 | {key = 'g', color = '#FFFFFF', path = '/Applications/Fork.app'}, -- Git fork 14 | {key = 'G', color = '#FFFFFF', path = '/Applications/WeWork.app'}, 15 | {key = 'j', color = '#FFFFFF', path = '/Applications/IntelliJ IDEA CE.app'}, 16 | {key = 'K', color = '#FFFFFF', path = '/System/Applications/Utilities/Activity Monitor.app'}, 17 | {key = 'm', color = '#FFFFFF', path = '/System/Applications/Messages.app'}, 18 | {key = 'q', color = '#FFFFFF', path = '/Applications/QQ.app'}, 19 | {key = 's', color = '#FFFFFF', path = '/Applications/Safari.app'}, 20 | {key = 'S', color = '#FFFFFF', path = '/System/Applications/System Settings.app'}, 21 | {key = 't', color = '#FFFFFF', path = '/System/Applications/Utilities/Terminal.app'}, 22 | {key = 'v', color = '#03a9f4', path = '/Applications/Visual Studio Code.app'}, 23 | } 24 | spoon.AppKeyable:start() 25 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | print('#######################') 2 | print('>> Hammerspoon Init ...') 3 | Profile = require('profile') 4 | hs.window.animationDuration = 0 -- 禁用动画,默认 0.2 https://github.com/Hammerspoon/hammerspoon/issues/1936 5 | 6 | hs.console.darkMode(true) 7 | if hs.console.darkMode() then 8 | hs.console.outputBackgroundColor({white = 0, alpha = 0.5}) 9 | hs.console.consoleCommandColor({white = 1, alpha = 0.7}) 10 | hs.console.alpha(1.00) 11 | end 12 | 13 | -- 全局 hs.alert 样式 14 | hs.alert.defaultStyle.textColor = hs.drawing.color.asRGB({hex = '#FFFFFF', alpha = 1.00}) -- 文本色 15 | hs.alert.defaultStyle.fillColor = hs.drawing.color.asRGB({hex = '#000000', alpha = 0.75}) -- 背景色 16 | hs.alert.defaultStyle.strokeColor = hs.drawing.color.asRGB({hex = '#FFFFFF', alpha = 0.30}) -- 边框色 17 | hs.alert.defaultStyle.radius = 5 18 | hs.alert.defaultStyle.textFont = 'Monaco' 19 | hs.alert.defaultStyle.textSize = 16 20 | hs.alert.defaultStyle.atScreenEdge = 0 21 | 22 | hs.loadSpoon('AppKeyable') -- 给APP绑定独立的激活键 23 | --hs.loadSpoon("SpeedMenu") -- 状态栏的下/下截网速 24 | hs.loadSpoon('ReloadConfiguration') -- 自动重载 Hammerspoon 配置 25 | 26 | spoon.AppKeyable.config.applications = { 27 | {key = 'A', color = '#FFFFFF', path = '/System/Applications/App Store.app'}, 28 | {key = 'b', color = '#FFFFFF', path = '/Applications/Blender.app'}, 29 | {key = 'B', color = '#FFFFFF', path = '/System/Applications/Books.app'}, 30 | {key = 'c', color = '#FFFFFF', path = '/Applications/Google Chrome.app'}, 31 | {key = 'C', color = '#FFFFFF', path = '/System/Applications/Contacts.app'}, 32 | {key = 'd', color = '#FFFFFF', path = '/Applications/DBeaver.app'}, 33 | -- {key = 'D', color = '#FFFFFF', path = '/Applications/NeteaseDictionary.app'}, 34 | {key = 'E', color = '#FFFFFF', path = '/System/Applications/TextEdit.app'}, -- Editor 35 | {key = 'e', color = '#03a9f4', path = '/Applications/Typora.app'}, 36 | {key = 'f', color = '#FFFFFF', path = '/System/Library/CoreServices/Finder.app'}, 37 | {key = 'F', color = '#FFFFFF', path = '/Applications/FileZilla.app'}, 38 | {key = 'g', color = '#FFFFFF', path = '/Applications/Fork.app'}, -- Git fork 39 | {key = 'G', color = '#FFFFFF', path = '/Applications/WeWork.app'}, 40 | {key = 'H', color = '#FFFFFF', path = '/Applications/VirtualBox.app/Contents/Resources/VirtualBoxVM.app'}, 41 | {key = 'i', color = '#FFFFFF', path = '/Applications/Postman.app'}, 42 | -- {key = 'j', color = '#03a9f4', path = '/Applications/PhpStorm.app'}, 43 | {key = 'j', color = '#FFFFFF', path = '/Applications/IntelliJ IDEA CE.app'}, 44 | {key = 'K', color = '#FFFFFF', path = '/System/Applications/Utilities/Activity Monitor.app'}, 45 | {key = 'k', color = '#FFFFFF', path = '/Applications/Sketch.app'}, 46 | {key = 'm', color = '#FFFFFF', path = '/System/Applications/Messages.app'}, 47 | {key = 'M', color = '#FFFFFF', path = '/Applications/TencentMeeting.app'}, 48 | {key = 'o', color = '#FFFFFF', path = '/Applications/wpsoffice.app'}, 49 | {key = 'p', color = '#FFFFFF', path = '/Applications/Affinity Photo.app'}, 50 | {key = 'P', color = '#FFFFFF', path = '/System/Applications/Freeform.app'}, 51 | {key = 'q', color = '#FFFFFF', path = '/Applications/QQ.app'}, 52 | {key = 'r', color = '#FFFFFF', path = '/Applications/Microsoft Remote Desktop.app'}, 53 | {key = 's', color = '#FFFFFF', path = '/Applications/Safari.app'}, 54 | {key = 'S', color = '#FFFFFF', path = '/System/Applications/System Settings.app'}, 55 | {key = 't', color = '#FFFFFF', path = '/System/Applications/Utilities/Terminal.app'}, 56 | {key = 'u', color = '#FFFFFF', path = '/Applications/Firefox.app'}, 57 | {key = 'v', color = '#03a9f4', path = '/Applications/Visual Studio Code.app'}, 58 | -- {key = 'V', color = '#FFFFFF', path = '/Applications/wechatwebdevtools.app'}, 59 | {key = 'w', color = '#FFFFFF', path = '/Applications/WeChat.app'}, 60 | {key = 'W', color = '#FFFFFF', path = '/Applications/WeWork.app'}, 61 | {key = 'x', color = '#FFFFFF', path = '/Applications/Xmind.app'}, 62 | {key = 'X', color = '#FFFFFF', path = '/Applications/Xcode.app'}, 63 | {key = 'y', color = '#FFFFFF', path = '/Applications/NeteaseMusic.app'} 64 | } 65 | 66 | spoon.ReloadConfiguration:start() 67 | spoon.AppKeyable:start() 68 | -- spoon.SpeedMenu:start() -- 不需要 start,loadSpoon() 时已经自动启动了 69 | 70 | -- 打开项目所在文件夹 71 | hs.hotkey.bind( 72 | {'cmd'}, 73 | 'e', 74 | function() 75 | hs.execute('open ~/Projects') 76 | end 77 | ) 78 | 79 | -- 切换输入法 80 | hs.hotkey.bind( 81 | {}, 82 | 'f18', 83 | function() 84 | hs.hid.capslock.set(false) 85 | local im = 'com.apple.inputmethod.SCIM.WBX' 86 | hs.keycodes.currentSourceID(im) 87 | hs.alert.closeAll() 88 | hs.alert.show('🇨🇳中文', 0.3) 89 | end 90 | ) 91 | hs.hotkey.bind( 92 | {}, 93 | 'f17', 94 | function() 95 | hs.hid.capslock.set(false) 96 | local im = 'com.apple.keylayout.ABC' 97 | hs.keycodes.currentSourceID(im) 98 | hs.alert.closeAll() 99 | hs.alert.show('🇬🇧English', 0.3) 100 | end 101 | ) 102 | 103 | -- -- https://stackoverflow.com/questions/656199/search-for-an-item-in-a-lua-list 104 | -- function Set(list) 105 | -- local set = {} 106 | -- for _, l in ipairs(list) do 107 | -- set[l] = true 108 | -- end 109 | -- return set 110 | -- end 111 | -- 112 | -- -- 不同位置的 WiFi 使用不同的网络配置 113 | -- function SSIDChanged() 114 | -- hs.location.start() 115 | -- local mac = hs.wifi.interfaceDetails().bssid 116 | -- hs.location.stop() 117 | -- local uuid = 'A2DF6E86-2F00-481C-938E-3CC160347D26' -- Automatic 118 | -- local homeMacAddresses = Set {'8c:be:be:2c:fb:77', '8c:be:be:2c:fb:78'} 119 | -- local ylgMacAddresses = Set {'a2:91:ce:b5:18:54'} 120 | -- local currentUUID = Profile.getCurrentUUID() 121 | -- if (mac ~= nil) then 122 | -- if (mac == 'd4:da:21:5a:ee:41') then -- JonieuNet 123 | -- uuid = 'E736F2F1-0DB3-47C6-A179-2779923A0021' 124 | -- elseif homeMacAddresses[mac] then -- Home 125 | -- uuid = '5AE03170-29FD-4ECE-B891-C72DCDB00712' 126 | -- elseif ylgMacAddresses[mac] then -- YaoLG 127 | -- uuid = '90C23198-478B-4032-BBA0-A7024FB19797' 128 | -- end 129 | -- end 130 | -- if (currentUUID ~= uuid) then 131 | -- Profile.setLocation(uuid) 132 | -- end 133 | -- end 134 | -- wifiWatcher = hs.wifi.watcher.new(SSIDChanged) 135 | -- wifiWatcher:start() 136 | -- 137 | -- -- 不同的网络位置使用不同的浏览器(需要先将默认浏览器设为Hammerspoon.app) 138 | -- hs.urlevent.httpCallback = function(scheme, host, params, fullURL, senderPID) 139 | -- local p = Profile.getCurrentProfile() 140 | -- local bundleID = nil 141 | -- hs.urlevent.openURLWithBundle(fullURL, p.browser) 142 | -- if (senderPID ~= -1) then 143 | -- local app = hs.application.applicationForPID(senderPID) 144 | -- if (app ~= nil) then 145 | -- bundleID = app:bundleID(); 146 | -- end 147 | -- end 148 | -- print(' OpenURL: ' .. fullURL) 149 | -- print(' Browser: ' .. p.browser) 150 | -- print('Location: ' .. p.name) 151 | -- print(' App: ' .. (bundleID or '')) 152 | -- end 153 | 154 | -- 定时锁屏 155 | lockScreenTimes = { "11:45", "17:30", "18:45" } 156 | LST = nil -- 在 hammerspoon 的控制台中输入 LST:stop() 可终止 timer 157 | for _, time in pairs(lockScreenTimes) 158 | do 159 | hs.timer.doAt(time, "1d", function() 160 | print('hs.timer.doAt: LockScreen: ' .. time) 161 | timer = hs.timer.delayed.new(300, function() 162 | notify:withdraw() 163 | hs.alert("Locking screen …") 164 | hs.caffeinate.lockScreen() 165 | end):start() 166 | LST = timer 167 | notify = hs.notify.new( 168 | function(notify) 169 | timer:stop() 170 | hs.alert("Screen lock terminated.") 171 | print('LockScreenTime: Cancelled ' .. time) 172 | end, 173 | { 174 | title = "即将锁屏", 175 | informativeText = "已计划于5分钟后锁定屏幕", 176 | actionButtonTitle = "终止计划", 177 | hasActionButton = true, 178 | } 179 | ) 180 | notify:send() 181 | end, true) 182 | print('LockScreenTime: ' .. time) 183 | end 184 | 185 | -------------------------------------------------------------------------------- /profile.lua: -------------------------------------------------------------------------------- 1 | -- usage: 2 | -- require('profile') 3 | -- profile.getCurrentLocation() 4 | -- usage: 5 | -- local p = require('profile') 6 | -- p.getCurrentLocation() 7 | 8 | module = {} 9 | 10 | module.locations = { 11 | ['A2DF6E86-2F00-481C-938E-3CC160347D26'] = { 12 | name= 'Automatic', 13 | -- browser = 'com.apple.Safari', 14 | browser = 'com.google.Chrome', 15 | }, 16 | ['5AE03170-29FD-4ECE-B891-C72DCDB00712'] = { 17 | name= 'Home', 18 | browser = 'com.google.Chrome', 19 | }, 20 | ['90C23198-478B-4032-BBA0-A7024FB19797'] = { 21 | name= 'YaoLG', 22 | browser = 'com.google.Chrome', 23 | }, 24 | ['AE128892-4C47-400D-B784-7C5EB9A60442'] = { 25 | name= 'Company', 26 | browser = 'org.mozilla.firefox', 27 | }, 28 | ['E736F2F1-0DB3-47C6-A179-2779923A0021'] = { 29 | name= 'JonieuNET', 30 | browser = 'org.mozilla.firefox', 31 | }, 32 | } 33 | 34 | function module.getCurrentUUID() 35 | local uuid = (hs.network.configuration.open():location():gsub('/Sets/', '')) 36 | return uuid 37 | end 38 | 39 | function module.getCurrentProfile() 40 | local uuid = (hs.network.configuration.open():location():gsub('/Sets/', '')) 41 | return module.locations[uuid] or module.locations['A2DF6E86-2F00-481C-938E-3CC160347D26'] 42 | end 43 | 44 | function module.setLocation(uuid) 45 | local profile = module.locations[uuid] 46 | hs.notify.new({title = '位置', informativeText = '已切换至「' .. profile.name .. '」'}):send() 47 | print('WiFi位置:' .. profile.name) 48 | return hs.network.configuration.open():setLocation(uuid) 49 | end 50 | 51 | -- -- https://igregory.ca/2020/hammerspoon-browser-picker/ 52 | -- function module.chooseBrowser() 53 | -- local appBundle 54 | -- local networkAddress = (hs.network.configuration.open():location():gsub('/Sets/', '')) -- scselect UUID 55 | -- if networkAddress == 'E736F2F1-0DB3-47C6-A179-2779923A0021' then -- Jonieu.NET 56 | -- appBundle = 'org.mozilla.firefox' 57 | -- elseif networkAddress == '5AE03170-29FD-4ECE-B891-C72DCDB00712' then -- Home 58 | -- appBundle = 'com.google.Chrome' 59 | -- else 60 | -- if host == nil then 61 | -- host = 'file' 62 | -- end 63 | -- local script = 'choose from list {"Chrome", "Safari", "Firefox"} with prompt "Open ' .. fullURL .. ' with…"' 64 | -- local success, _, res = hs.osascript.applescript(script) 65 | -- local appBundle 66 | -- if success and res:match('Chrome') ~= nil then 67 | -- appBundle = 'com.google.Chrome' 68 | -- elseif success and res:match('Safari') ~= nil then 69 | -- appBundle = 'com.apple.Safari' 70 | -- elseif success and res:match('Firefox') ~= nil then 71 | -- appBundle = 'org.mozilla.firefox' 72 | -- else 73 | -- return 74 | -- end 75 | -- end 76 | -- end 77 | 78 | return module 79 | -------------------------------------------------------------------------------- /screenshot/app-hotkey-help.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git4coder/hammerspoon_config/c4d52c98c9bf76a5d0bc2d1dd2ffc8ea46f941a0/screenshot/app-hotkey-help.jpg -------------------------------------------------------------------------------- /screenshot/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git4coder/hammerspoon_config/c4d52c98c9bf76a5d0bc2d1dd2ffc8ea46f941a0/screenshot/preview.png --------------------------------------------------------------------------------