├── .DS_Store ├── README.md ├── Spoons ├── AClock.spoon │ ├── docs.json │ └── init.lua ├── ClipShow.spoon │ ├── docs.json │ └── init.lua ├── CountDown.spoon │ ├── docs.json │ └── init.lua ├── HSearch.spoon │ ├── docs.json │ ├── hs_btabs.lua │ ├── hs_datamuse.lua │ ├── hs_emoji.lua │ ├── hs_note.lua │ ├── hs_time.lua │ ├── hs_v2ex.lua │ ├── hs_yddict.lua │ ├── init.lua │ └── resources │ │ ├── chrome.png │ │ ├── emoji.png │ │ ├── justnote.png │ │ ├── menus.png │ │ ├── safari.png │ │ ├── tabs.png │ │ ├── taskkill.png │ │ ├── thesaurus.png │ │ ├── time.png │ │ ├── v2ex.png │ │ └── youdao.png ├── HeadphoneAutoPause.spoon │ ├── docs.json │ └── init.lua ├── KSheet.spoon │ ├── docs.json │ └── init.lua ├── ModalMgr.spoon │ ├── docs.json │ └── init.lua ├── MountedVolumes.spoon │ ├── docs.json │ └── init.lua ├── PopupTranslateSelection.spoon │ ├── docs.json │ └── init.lua ├── SpeedMenu.spoon │ ├── docs.json │ └── init.lua ├── SpoonInstall.spoon │ ├── docs.json │ └── init.lua ├── VimMode.spoon │ ├── .busted │ ├── .gitignore │ ├── .luacheckrc │ ├── .rspec │ ├── .travis.yml │ ├── CHANGELOG.md │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.md │ ├── Rakefile │ ├── bin │ │ ├── dev-setup │ │ └── installer │ ├── docs.json │ ├── docs │ │ └── Integration_Tests.md │ ├── images │ │ ├── vim-mode.gif │ │ └── vim-state-diagram.svg │ ├── init.lua │ ├── lib │ │ ├── accessibility_buffer.lua │ │ ├── app_watcher.lua │ │ ├── axuielement.lua │ │ ├── block_cursor.lua │ │ ├── buffer.lua │ │ ├── command_state.lua │ │ ├── config.lua │ │ ├── contextual_modal.lua │ │ ├── contextual_modal │ │ │ └── registry.lua │ │ ├── focus_watcher.lua │ │ ├── hot_patcher.lua │ │ ├── key_sequence.lua │ │ ├── modal.lua │ │ ├── motion.lua │ │ ├── motions │ │ │ ├── back_word.lua │ │ │ ├── backward_search.lua │ │ │ ├── between_chars.lua │ │ │ ├── big_word.lua │ │ │ ├── current_selection.lua │ │ │ ├── down.lua │ │ │ ├── end_of_word.lua │ │ │ ├── entire_line.lua │ │ │ ├── first_line.lua │ │ │ ├── first_non_blank.lua │ │ │ ├── forward_search.lua │ │ │ ├── in_word.lua │ │ │ ├── last_line.lua │ │ │ ├── left.lua │ │ │ ├── line_beginning.lua │ │ │ ├── line_end.lua │ │ │ ├── noop.lua │ │ │ ├── right.lua │ │ │ ├── till_after_search.lua │ │ │ ├── till_before_search.lua │ │ │ ├── up.lua │ │ │ └── word.lua │ │ ├── operator.lua │ │ ├── operators │ │ │ ├── change.lua │ │ │ ├── delete.lua │ │ │ ├── replace.lua │ │ │ └── yank.lua │ │ ├── screen_dimmer.lua │ │ ├── selection.lua │ │ ├── state.lua │ │ ├── state_indicator.lua │ │ ├── strategies │ │ │ ├── accessibility_strategy.lua │ │ │ └── keyboard_strategy.lua │ │ ├── strategy.lua │ │ ├── utils │ │ │ ├── ax.lua │ │ │ ├── benchmark.lua │ │ │ ├── browser.lua │ │ │ ├── find_first.lua │ │ │ ├── inspect.lua │ │ │ ├── keys.lua │ │ │ ├── log.lua │ │ │ ├── number_utils.lua │ │ │ ├── prequire.lua │ │ │ ├── set.lua │ │ │ ├── statemachine.lua │ │ │ ├── string_utils.lua │ │ │ ├── table.lua │ │ │ ├── times.lua │ │ │ ├── version.lua │ │ │ ├── visible_range.lua │ │ │ └── visual.lua │ │ ├── vim.lua │ │ └── wait_for_char.lua │ ├── spec │ │ ├── buffer_spec.lua │ │ ├── config_spec.lua │ │ ├── features │ │ │ ├── big_word_spec.rb │ │ │ ├── delete_line_spec.rb │ │ │ ├── delete_word_spec.rb │ │ │ ├── linewise_spec.rb │ │ │ └── visual_mode_spec.rb │ │ ├── fixtures │ │ │ └── textarea.html │ │ ├── motions │ │ │ ├── back_word_spec.lua │ │ │ ├── between_chars_spec.lua │ │ │ ├── big_word_spec.lua │ │ │ ├── end_of_word_spec.lua │ │ │ ├── in_word_spec.lua │ │ │ ├── line_beginning_spec.lua │ │ │ ├── line_end_spec.lua │ │ │ └── word_spec.lua │ │ ├── operators │ │ │ ├── delete_spec.lua │ │ │ └── replace_spec.lua │ │ ├── selection_spec.lua │ │ ├── spec_helper.lua │ │ ├── spec_helper.rb │ │ ├── support │ │ │ ├── capybara.rb │ │ │ ├── chrome_kill.rb │ │ │ ├── hammerspoon.rb │ │ │ └── text_helpers.rb │ │ └── utils │ │ │ ├── number_utils_spec.lua │ │ │ ├── string_utils_spec.lua │ │ │ ├── table_spec.lua │ │ │ └── visual_spec.lua │ └── vendor │ │ ├── hs │ │ └── _asm │ │ │ └── axuielement │ │ │ ├── docs.json │ │ │ ├── init.lua │ │ │ ├── internal.so │ │ │ └── internal.so.dSYM │ │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ └── DWARF │ │ │ └── internal.so │ │ ├── luautf8.lua │ │ └── luautf8 │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── README.md │ │ ├── lutf8lib.c │ │ ├── lutf8lib.o │ │ ├── parseucd.lua │ │ ├── rockspecs │ │ ├── luautf8-0.1.0-1.rockspec │ │ ├── luautf8-0.1.1-1.rockspec │ │ ├── luautf8-0.1.2-2.rockspec │ │ ├── luautf8-0.1.3-1.rockspec │ │ └── luautf8-scm-0.rockspec │ │ ├── test.lua │ │ └── unidata.h ├── VolumeScroll.spoon │ ├── docs.json │ └── init.lua └── WinWin.spoon │ ├── docs.json │ ├── init-dock.lua │ └── init.lua ├── config-example.lua ├── images ├── appm.png ├── cipshow.png ├── countdown.png ├── help.png ├── ksheet.png └── winwin.png ├── init.lua └── private └── config.lua /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [hammerspoon-config](hammerspoon-config) 2 | 3 | 4 | > 本配置基于 vim 风格,实现了窗口管理,剪切板,倒计时,快速启动等功能。所有模式按照指定快捷键进入,所有模式都可以用 `esc` 或 `q`退出。在进入对应模式之前只有模式快捷键生效,进入对应模式之后此模式的操作快捷键生效。 5 | 6 | ## 安装配置与升级: 7 | 8 | 安装 hammerspoon 9 | ``` 10 | brew cask install hammerspoon 11 | ``` 12 | 13 | 将配置文件克隆到本地根目录。 14 | ``` 15 | git clone https://github.com/zuorn/hammerspoon_config ~/.hammerspoon 16 | ``` 17 | **重新加载配置文件即可生效**。 18 | 19 | 如果提示:already exists and is not an empty directory. 20 | 先删除目录 21 | 22 | ``` 23 | rm -rf ~/.hammerspoon 24 | ``` 25 | 26 | 升级: 27 | 28 | ``` 29 | cd ~/.hammerspoon && git pull 30 | ``` 31 | 32 | ## 功能实现: 33 | 34 | **注:所有模式按 `esc` 和 `q` 退出。** 35 | 36 | ### 帮助面板 37 | 按下快捷键 `shift` + `option` + `/` 显示帮助面板查看各个模式快捷键。再按下对应快捷键切换模式。 38 | 39 | ![](http://ww1.sinaimg.cn/large/006tNc79ly1g4pzrve6gsj31c00u0k0p.jpg) 40 | 41 | 42 | ### 窗口管理模式 43 | 按下前缀键 `Option` + `R` 进入窗口管理模式: 44 | 45 | * 使用 `h、j、k、l` 移动为上下左右的半屏 46 | * 使用 ` y、u、i、o`(即 hjkl 上方按键)移动为左上/左下/右上/右下的四分之一窗口 47 | * 使用 `c` 居中,按下 `=、-` 进行窗口大小缩放 48 | * 使用 `w、s、a、d` 向上下左右移动窗口 49 | * 使用 `H、J、K、L` 向左/下增减窗口大小 50 | * 使用方向键 `上、下、左、右` 移动到相应方向上的显示器(多块显示器的话) 51 | * 使用 `[,]` 左三分之二屏和右三分之二屏 52 | * 使用 `space` 将窗口投送到另外一块屏幕(假如有两块以上显示器的话) 53 | * 使用 `t` 光标移动到所在窗口的中间位置 54 | * 使用 `tab` 显示帮助面板,查看键位图 55 | * 使用 `G` 左三分之二居中分屏 56 | * 使用 `Z` 展示显示 57 | * 使用 `V` 编程显示 58 | * 使用 `t` 将光标移至所在窗口的中心位置 59 | * 使用 `X` 三分之一居中分屏 60 | 61 | 62 | ![](http://ww4.sinaimg.cn/large/006tNc79ly1g4pz9dhogwj31c00u04aw.jpg) 63 | 64 | 注:如设置程序坞自动隐藏请修改 `/Users/zuorn/.hammerspoon/Spoons/WinWin.spoon/init.lua.bak` 为`init.lua` 65 | 66 | ### 应用快速切换 67 | 68 | 按下前缀键 `Option` + `tab` 显示窗口提示,按下对应应用显示的字母快速切换。 69 | ![快速切换](https://i.loli.net/2019/07/06/5d20193818dd473100.png) 70 | 71 | 72 | 73 | 74 | ### KSheet - 展示应用快捷键 75 | 76 | 按下快捷键 `Option` + `s` 展示当前应用快捷键,按 `q` 或者 `esc` 退出。 77 | 78 | ![应用快捷键](https://i.loli.net/2019/07/06/5d2019381760e52911.png) 79 | 80 | 81 | ### 快速启动 82 | 83 | 按下快捷键 `Option` + `a` 打开快速启动,按下对应字母快速打开应用。 84 | 85 | ![启动器](https://i.loli.net/2019/07/06/5d2019368b6dc67355.png) 86 | 87 | 88 | ### AClock - 显示当前时间 89 | 90 | 按下 `Option` + `t` 显示当前时间。 91 | 92 | ![时钟](https://i.loli.net/2019/07/06/5d201936dbfdf69558.png) 93 | 94 | 95 | ### 倒计时(番茄钟) 96 | 97 | 按下 `Option` + `i` 打开倒计时面板,按下对应数字开始计时。 98 | 99 | * 使用 `空格` 可暂停/恢复倒计时。 100 | 101 | ![倒计时](https://i.loli.net/2019/07/06/5d2019372da4545679.png) 102 | 103 | 104 | ### clipshowM 剪切板 105 | 106 | 按下 `Option` + `c` 打开剪切板面板。 107 | 108 | ![剪切板](https://i.loli.net/2019/07/06/5d201937266fe84053.png) 109 | 110 | 功能: 111 | 112 | * 保存会话 113 | * 恢复上一个会话 114 | * 在浏览器中打开 115 | * 使用百度搜索 116 | * 使用谷歌搜索 117 | * 保存到桌面 118 | * 使用 github 搜索 119 | * 在 Sublime Text 打开 120 | 121 | ### 顶部实时显示网速 122 | 123 | 没有模式快捷键,默认开启。 124 | 125 | ![网速显示](https://i.loli.net/2019/07/06/5d2019336a0b441738.jpg) 126 | 127 | ## 自定义配置 128 | 129 | 拷贝私有配置文件 130 | 131 | ``` 132 | cp ~/.hammerspoon/config-example.lua ~/.hammerspoon/private/config.lua 133 | ``` 134 | 135 | 按照注释编辑私有配置文件 `~/.hammerspoon/private/config.lua` 即可。 136 | 137 | #### 可自定义范围: 138 | 139 | * 指定要启用模块 140 | * 找到配置文件启用模块,注释对应模块可禁止用对应功能。 141 | * 绑定快速启动 app 及快捷键 142 | * 自定义模式快捷键 143 | * 自定义 hammerspoon 快捷键绑定 144 | 145 | 146 | ## 参考: 147 | 148 | * [Hammerspoon Spoons](https://www.hammerspoon.org/Spoons/) 149 | * [awesome-hammerspoon](https://github.com/ashfinal/awesome-hammerspoon) 150 | -------------------------------------------------------------------------------- /Spoons/AClock.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 | "type" : "Module", 22 | "desc" : "Just another clock, floating above all", 23 | "Constructor" : [ 24 | 25 | ], 26 | "doc" : "Just another clock, floating above all\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/AClock.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/AClock.spoon.zip)", 27 | "Field" : [ 28 | 29 | ], 30 | "items" : [ 31 | { 32 | "doc" : "Show AClock, if already showing, just hide it.", 33 | "stripped_doc" : [ 34 | "Show AClock, if already showing, just hide it." 35 | ], 36 | "def" : "AClock:toggleShow()", 37 | "parameters" : [ 38 | 39 | ], 40 | "notes" : [ 41 | 42 | ], 43 | "signature" : "AClock:toggleShow()", 44 | "type" : "Method", 45 | "returns" : [ 46 | 47 | ], 48 | "name" : "toggleShow", 49 | "desc" : "Show AClock, if already showing, just hide it." 50 | } 51 | ], 52 | "Method" : [ 53 | { 54 | "doc" : "Show AClock, if already showing, just hide it.", 55 | "stripped_doc" : [ 56 | "Show AClock, if already showing, just hide it." 57 | ], 58 | "def" : "AClock:toggleShow()", 59 | "parameters" : [ 60 | 61 | ], 62 | "notes" : [ 63 | 64 | ], 65 | "signature" : "AClock:toggleShow()", 66 | "type" : "Method", 67 | "returns" : [ 68 | 69 | ], 70 | "name" : "toggleShow", 71 | "desc" : "Show AClock, if already showing, just hide it." 72 | } 73 | ], 74 | "Command" : [ 75 | 76 | ], 77 | "name" : "AClock" 78 | } 79 | ] -------------------------------------------------------------------------------- /Spoons/AClock.spoon/init.lua: -------------------------------------------------------------------------------- 1 | --- === AClock === 2 | --- 3 | --- Just another clock, floating above all 4 | --- 5 | --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/AClock.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/AClock.spoon.zip) 6 | 7 | local obj={} 8 | obj.__index = obj 9 | 10 | -- Metadata 11 | obj.name = "AClock" 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.canvas = hs.canvas.new({x=0, y=0, w=0, h=0}):show() 19 | self.canvas[1] = { 20 | type = "text", 21 | text = "", 22 | textFont = "Impact", 23 | textSize = 130, 24 | textColor = {hex="#1891C3"}, 25 | textAlignment = "center", 26 | } 27 | end 28 | 29 | --- AClock:toggleShow() 30 | --- Method 31 | --- Show AClock, if already showing, just hide it. 32 | --- 33 | 34 | function obj:toggleShow() 35 | if self.timer then 36 | self.timer:stop() 37 | self.timer = nil 38 | self.canvas:hide() 39 | else 40 | local mainScreen = hs.screen.mainScreen() 41 | local mainRes = mainScreen:fullFrame() 42 | self.canvas:frame({ 43 | x = (mainRes.w-300)/2, 44 | y = (mainRes.h-230)/2, 45 | w = 300, 46 | h = 230 47 | }) 48 | self.canvas[1].text = os.date("%H:%M") 49 | self.canvas:show() 50 | self.timer = hs.timer.doAfter(4, function() 51 | self.canvas:hide() 52 | self.timer = nil 53 | end) 54 | end 55 | end 56 | 57 | return obj 58 | -------------------------------------------------------------------------------- /Spoons/CountDown.spoon/init.lua: -------------------------------------------------------------------------------- 1 | --- === CountDown === 2 | --- 3 | --- Tiny countdown with visual indicator 4 | --- 5 | --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/CountDown.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/CountDown.spoon.zip) 6 | 7 | local obj = {} 8 | obj.__index = obj 9 | 10 | -- Metadata 11 | obj.name = "CountDown" 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 | obj.canvas = nil 18 | obj.timer = nil 19 | 20 | function obj:init() 21 | self.canvas = hs.canvas.new({x=0, y=0, w=0, h=0}):show() 22 | self.canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) 23 | self.canvas:level(hs.canvas.windowLevels.status) 24 | self.canvas:alpha(0.35) 25 | self.canvas[1] = { 26 | type = "rectangle", 27 | action = "fill", 28 | fillColor = hs.drawing.color.osx_red, 29 | frame = {x="0%", y="0%", w="0%", h="100%"} 30 | } 31 | self.canvas[2] = { 32 | type = "rectangle", 33 | action = "fill", 34 | fillColor = hs.drawing.color.osx_green, 35 | frame = {x="0%", y="0%", w="0%", h="100%"} 36 | } 37 | end 38 | 39 | --- CountDown:startFor(minutes) 40 | --- Method 41 | --- Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance. 42 | --- 43 | --- Parameters: 44 | --- * minutes - How many minutes 45 | 46 | local function canvasCleanup() 47 | if obj.timer then 48 | obj.timer:stop() 49 | obj.timer = nil 50 | end 51 | obj.canvas[1].frame.w = "0%" 52 | obj.canvas[2].frame.x = "0%" 53 | obj.canvas[2].frame.w = "0%" 54 | obj.canvas:frame({x=0, y=0, w=0, h=0}) 55 | end 56 | 57 | function obj:startFor(minutes) 58 | if obj.timer then 59 | canvasCleanup() 60 | else 61 | local mainScreen = hs.screen.mainScreen() 62 | local mainRes = mainScreen:fullFrame() 63 | obj.canvas:frame({x=mainRes.x, y=mainRes.y+mainRes.h-5, w=mainRes.w, h=5}) 64 | -- Set minimum visual step to 2px (i.e. Make sure every trigger updates 2px on screen at least.) 65 | local minimumStep = 2 66 | local secCount = math.ceil(60*minutes) 67 | obj.loopCount = 0 68 | if mainRes.w/secCount >= 2 then 69 | obj.timer = hs.timer.doEvery(1, function() 70 | obj.loopCount = obj.loopCount+1/secCount 71 | obj:setProgress(obj.loopCount, minutes) 72 | end) 73 | else 74 | local interval = 2/(mainRes.w/secCount) 75 | obj.timer = hs.timer.doEvery(interval, function() 76 | obj.loopCount = obj.loopCount+1/mainRes.w*2 77 | obj:setProgress(obj.loopCount, minutes) 78 | end) 79 | end 80 | end 81 | 82 | return self 83 | end 84 | 85 | --- CountDown:pauseOrResume() 86 | --- Method 87 | --- Pause or resume the existing countdown. 88 | --- 89 | 90 | function obj:pauseOrResume() 91 | if obj.timer then 92 | if obj.timer:running() then 93 | obj.timer:stop() 94 | else 95 | obj.timer:start() 96 | end 97 | end 98 | end 99 | 100 | --- CountDown:setProgress(progress) 101 | --- Method 102 | --- Set the progress of visual indicator to `progress`. 103 | --- 104 | --- Parameters: 105 | --- * progress - an number specifying the value of progress (0.0 - 1.0) 106 | 107 | function obj:setProgress(progress, notifystr) 108 | if obj.canvas:frame().h == 0 then 109 | -- Make the canvas actully visible 110 | local mainScreen = hs.screen.mainScreen() 111 | local mainRes = mainScreen:fullFrame() 112 | obj.canvas:frame({x=mainRes.x, y=mainRes.y+mainRes.h-5, w=mainRes.w, h=5}) 113 | end 114 | if progress >= 1 then 115 | canvasCleanup() 116 | if notifystr then 117 | hs.notify.new({ 118 | title = "倒计时(" .. notifystr .. " 分钟) 已经结束!!", 119 | informativeText = " 现在的时间是: " .. os.date("%X") 120 | }):send() 121 | end 122 | -- 倒计时结束后锁屏 123 | hs.caffeinate.lockScreen() 124 | 125 | else 126 | obj.canvas[1].frame.w = tostring(progress) 127 | obj.canvas[2].frame.x = tostring(progress) 128 | obj.canvas[2].frame.w = tostring(1-progress) 129 | end 130 | end 131 | 132 | return obj 133 | -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/docs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Command": [], 4 | "Constant": [], 5 | "Constructor": [], 6 | "Deprecated": [], 7 | "Field": [], 8 | "Function": [], 9 | "Method": [ 10 | { 11 | "def": "HSearch:loadSources()", 12 | "desc": "Load new sources from `HSearch.search_path`, the search_path defaults to `~/.hammerspoon/private/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual.", 13 | "doc": "Load new sources from `HSearch.search_path`, the search_path defaults to `~/.hammerspoon/private/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual.\n", 14 | "name": "loadSources", 15 | "signature": "HSearch:loadSources()", 16 | "stripped_doc": "", 17 | "type": "Method" 18 | }, 19 | { 20 | "def": "HSearch:switchSource()", 21 | "desc": "Tigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual.", 22 | "doc": "Tigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual.\n", 23 | "name": "switchSource", 24 | "signature": "HSearch:switchSource()", 25 | "stripped_doc": "", 26 | "type": "Method" 27 | }, 28 | { 29 | "def": "HSearch:toggleShow()", 30 | "desc": "Toggle the display of HSearch", 31 | "doc": "Toggle the display of HSearch\n", 32 | "name": "toggleShow", 33 | "signature": "HSearch:toggleShow()", 34 | "stripped_doc": "", 35 | "type": "Method" 36 | } 37 | ], 38 | "Variable": [], 39 | "desc": "Hammerspoon Search", 40 | "doc": "Hammerspoon Search\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HSearch.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HSearch.spoon.zip)", 41 | "items": [ 42 | { 43 | "def": "HSearch:loadSources()", 44 | "desc": "Load new sources from `HSearch.search_path`, the search_path defaults to `~/.hammerspoon/private/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual.", 45 | "doc": "Load new sources from `HSearch.search_path`, the search_path defaults to `~/.hammerspoon/private/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual.\n", 46 | "name": "loadSources", 47 | "signature": "HSearch:loadSources()", 48 | "stripped_doc": "", 49 | "type": "Method" 50 | }, 51 | { 52 | "def": "HSearch:switchSource()", 53 | "desc": "Tigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual.", 54 | "doc": "Tigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual.\n", 55 | "name": "switchSource", 56 | "signature": "HSearch:switchSource()", 57 | "stripped_doc": "", 58 | "type": "Method" 59 | }, 60 | { 61 | "def": "HSearch:toggleShow()", 62 | "desc": "Toggle the display of HSearch", 63 | "doc": "Toggle the display of HSearch\n", 64 | "name": "toggleShow", 65 | "signature": "HSearch:toggleShow()", 66 | "stripped_doc": "", 67 | "type": "Method" 68 | } 69 | ], 70 | "name": "HSearch", 71 | "stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HSearch.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HSearch.spoon.zip)", 72 | "submodules": [], 73 | "type": "Module" 74 | } 75 | ] -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/hs_btabs.lua: -------------------------------------------------------------------------------- 1 | local obj={} 2 | obj.__index = obj 3 | 4 | obj.name = "browserTabs" 5 | obj.version = "1.0" 6 | obj.author = "ashfinal " 7 | 8 | -- Internal function used to find our location, so we know where to load files from 9 | local function script_path() 10 | local str = debug.getinfo(2, "S").source:sub(2) 11 | return str:match("(.*/)") 12 | end 13 | 14 | obj.spoonPath = script_path() 15 | 16 | -- Define the source's overview. A unique `keyword` key should exist, so this source can be found. 17 | obj.overview = {text="Type t ⇥ to search safari/chrome Tabs.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/tabs.png"), keyword="t"} 18 | -- Define the notice when a long-time request is being executed. It could be `nil`. 19 | obj.notice = {text="Requesting data, please wait a while …"} 20 | 21 | local function browserTabsRequest() 22 | local safari_running = hs.application.applicationsForBundleID("com.apple.Safari") 23 | local chooser_data = {} 24 | if #safari_running > 0 then 25 | local stat, data= hs.osascript.applescript('tell application "Safari"\nset winlist to tabs of windows\nset tablist to {}\nrepeat with i in winlist\nif (count of i) > 0 then\nrepeat with currenttab in i\nset tabinfo to {name of currenttab as unicode text, URL of currenttab}\ncopy tabinfo to the end of tablist\nend repeat\nend if\nend repeat\nreturn tablist\nend tell') 26 | -- Notice `output` key and its `arg`. The built-in output contains `browser`, `safari`, `chrome`, `firefon`, `clipboard`, `keystrokes`. You can define new output type if you like. 27 | if stat then 28 | chooser_data = hs.fnutils.imap(data, function(item) 29 | return {text=item[1], subText=item[2], image=hs.image.imageFromPath(obj.spoonPath .. "/resources/safari.png"), output="safari", arg=item[2]} 30 | end) 31 | end 32 | end 33 | local chrome_running = hs.application.applicationsForBundleID("com.google.Chrome") 34 | if #chrome_running > 0 then 35 | local stat, data= hs.osascript.applescript('tell application "Google Chrome"\nset winlist to tabs of windows\nset tablist to {}\nrepeat with i in winlist\nif (count of i) > 0 then\nrepeat with currenttab in i\nset tabinfo to {name of currenttab as unicode text, URL of currenttab}\ncopy tabinfo to the end of tablist\nend repeat\nend if\nend repeat\nreturn tablist\nend tell') 36 | if stat then 37 | for idx,val in pairs(data) do 38 | -- Usually we want to open chrome tabs in Google Chrome. 39 | table.insert(chooser_data, {text=val[1], subText=val[2], image=hs.image.imageFromPath(obj.spoonPath .. "/resources/chrome.png"), output="chrome", arg=val[2]}) 40 | end 41 | end 42 | end 43 | -- Return specific table as hs.chooser's data, other keys except for `text` could be optional. 44 | return chooser_data 45 | end 46 | 47 | -- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices 48 | obj.init_func = browserTabsRequest 49 | -- Insert a friendly tip at the head so users know what to do next. 50 | obj.description = {text="Browser Tabs Search", subText="Search and select one item to open in corresponding browser.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/tabs.png")} 51 | 52 | -- As the user is typing, the callback function will be called for every keypress. The returned value is a table. 53 | obj.callback = nil 54 | 55 | return obj 56 | -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/hs_datamuse.lua: -------------------------------------------------------------------------------- 1 | local obj={} 2 | obj.__index = obj 3 | 4 | obj.name = "thesaurusDM" 5 | obj.version = "1.0" 6 | obj.author = "ashfinal " 7 | 8 | -- Internal function used to find our location, so we know where to load files from 9 | local function script_path() 10 | local str = debug.getinfo(2, "S").source:sub(2) 11 | return str:match("(.*/)") 12 | end 13 | 14 | obj.spoonPath = script_path() 15 | 16 | -- Define the source's overview. A unique `keyword` key should exist, so this source can be found. 17 | obj.overview = {text="Type s ⇥ to request English Thesaurus.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/thesaurus.png"), keyword="s"} 18 | -- Define the notice when a long-time request is being executed. It could be `nil`. 19 | obj.notice = nil 20 | 21 | local function dmTips() 22 | local chooser_data = { 23 | {text="Datamuse Thesaurus", subText="Type something to get more words like it …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/thesaurus.png")} 24 | } 25 | return chooser_data 26 | end 27 | 28 | -- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices 29 | obj.init_func = dmTips 30 | -- Insert a friendly tip at the head so users know what to do next. 31 | -- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here 32 | obj.description = nil 33 | 34 | -- As the user is typing, the callback function will be called for every keypress. The returned value is a table. 35 | 36 | local function thesaurusRequest(querystr) 37 | local datamuse_baseurl = 'http://api.datamuse.com' 38 | if string.len(querystr) > 0 then 39 | local encoded_query = hs.http.encodeForQuery(querystr) 40 | local query_url = datamuse_baseurl .. '/words?ml=' .. encoded_query .. '&max=20' 41 | 42 | hs.http.asyncGet(query_url, nil, function(status, data) 43 | if status == 200 then 44 | if pcall(function() hs.json.decode(data) end) then 45 | local decoded_data = hs.json.decode(data) 46 | if #decoded_data > 0 then 47 | local chooser_data = hs.fnutils.imap(decoded_data, function(item) 48 | return {text = item.word, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/thesaurus.png"), output="keystrokes", arg=item.word} 49 | end) 50 | -- Because we don't know when asyncGet will return data, we have to refresh hs.chooser choices in this callback. 51 | if spoon.HSearch then 52 | -- Make sure HSearch spoon is running now 53 | spoon.HSearch.chooser:choices(chooser_data) 54 | spoon.HSearch.chooser:refreshChoicesCallback() 55 | end 56 | end 57 | end 58 | end 59 | end) 60 | else 61 | local chooser_data = { 62 | {text="Datamuse Thesaurus", subText="Type something to get more words like it …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/thesaurus.png")} 63 | } 64 | if spoon.HSearch then 65 | spoon.HSearch.chooser:choices(chooser_data) 66 | spoon.HSearch.chooser:refreshChoicesCallback() 67 | end 68 | end 69 | end 70 | 71 | obj.callback = thesaurusRequest 72 | 73 | return obj 74 | -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/hs_emoji.lua: -------------------------------------------------------------------------------- 1 | local obj={} 2 | obj.__index = obj 3 | 4 | obj.name = "MLemoji" 5 | obj.version = "1.0" 6 | obj.author = "ashfinal " 7 | 8 | -- Internal function used to find our location, so we know where to load files from 9 | local function script_path() 10 | local str = debug.getinfo(2, "S").source:sub(2) 11 | return str:match("(.*/)") 12 | end 13 | 14 | obj.spoonPath = script_path() 15 | 16 | -- Define the source's overview. A unique `keyword` key should exist, so this source can be found. 17 | obj.overview = {text="Type e ⇥ to find relevant Emoji.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/emoji.png"), keyword="e"} 18 | -- Define the notice when a long-time request is being executed. It could be `nil`. 19 | obj.notice = nil 20 | 21 | local function emojiTips() 22 | local chooser_data = { 23 | {text="Relevant Emoji", subText="Type something to find relevant emoji from text …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/emoji.png")} 24 | } 25 | return chooser_data 26 | end 27 | -- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices 28 | obj.init_func = emojiTips 29 | -- Insert a friendly tip at the head so users know what to do next. 30 | -- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here 31 | obj.description = nil 32 | 33 | -- As the user is typing, the callback function will be called for every keypress. The returned value is a table. 34 | 35 | -- Some global objects 36 | local emoji_database_path = "/System/Library/Input Methods/CharacterPalette.app/Contents/Resources/CharacterDB.sqlite3" 37 | obj.database = hs.sqlite3.open(emoji_database_path) 38 | obj.canvas = hs.canvas.new({x=0, y=0, w=96, h=96}) 39 | 40 | local function getEmojiDesc(arg) 41 | for w in obj.database:rows("SELECT info FROM unihan_dict WHERE uchr=\'" .. arg .. "\'") do 42 | return w[1] 43 | end 44 | end 45 | 46 | local function emojiRequest(querystr) 47 | local emoji_baseurl = 'https://emoji.getdango.com' 48 | if string.len(querystr) > 0 then 49 | local encoded_query = hs.http.encodeForQuery(querystr) 50 | local query_url = emoji_baseurl .. '/api/emoji?q=' .. encoded_query 51 | 52 | hs.http.asyncGet(query_url, nil, function(status, data) 53 | if status == 200 then 54 | if pcall(function() hs.json.decode(data) end) then 55 | local decoded_data = hs.json.decode(data) 56 | if decoded_data.results and #decoded_data.results > 0 then 57 | local chooser_data = hs.fnutils.imap(decoded_data.results, function(item) 58 | obj.canvas[1] = {type="text", text=item.text, textSize=64, frame={x="15%", y="10%", w="100%", h="100%"}} 59 | local hexcode = string.format("%#X", utf8.codepoint(item.text)) 60 | local emoji_description = getEmojiDesc(item.text) 61 | local formatted_desc = string.gsub(emoji_description, "|||||||||||||||", "") 62 | return {text = formatted_desc, image=obj.canvas:imageFromCanvas(), subText="Hex Code: " .. hexcode, outputType="keystrokes", arg=item.text} 63 | end) 64 | -- Because we don't know when asyncGet will return data, we have to refresh hs.chooser choices in this callback. 65 | if spoon.HSearch then 66 | -- Make sure HSearch spoon is running now 67 | spoon.HSearch.chooser:choices(chooser_data) 68 | spoon.HSearch.chooser:refreshChoicesCallback() 69 | end 70 | end 71 | end 72 | end 73 | end) 74 | else 75 | local chooser_data = { 76 | {text="Relevant Emoji", subText="Type something to find relevant emoji from text …", image=hs.image.imageFromPath(hs.configdir.."/resources/emoji.png")} 77 | } 78 | if spoon.HSearch then 79 | spoon.HSearch.chooser:choices(chooser_data) 80 | spoon.HSearch.chooser:refreshChoicesCallback() 81 | end 82 | end 83 | end 84 | 85 | obj.callback = emojiRequest 86 | 87 | return obj 88 | -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/hs_note.lua: -------------------------------------------------------------------------------- 1 | local obj={} 2 | obj.__index = obj 3 | 4 | obj.name = "justNote" 5 | obj.version = "1.0" 6 | obj.author = "ashfinal " 7 | 8 | -- Internal function used to find our location, so we know where to load files from 9 | local function script_path() 10 | local str = debug.getinfo(2, "S").source:sub(2) 11 | return str:match("(.*/)") 12 | end 13 | 14 | obj.spoonPath = script_path() 15 | 16 | -- Define the source's overview. A unique `keyword` key should exist, so this source can be found. 17 | obj.overview = {text="Type n ⇥ to Note something.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/justnote.png"), keyword="n"} 18 | -- Define the notice when a long-time request is being executed. It could be `nil`. 19 | obj.notice = nil 20 | -- Define the hotkeys, which will be enabled/disabled automatically. You need to add your keybindings into this table manually. 21 | obj.hotkeys = {} 22 | 23 | local function justNoteRequest() 24 | local note_history = hs.settings.get("just.another.note") or {} 25 | if #note_history == 0 then 26 | local chooser_data = {{text="Write something and press Enter.", subText="Your notes is automatically saved, selected item will be erased.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/justnote.png")}} 27 | return chooser_data 28 | else 29 | local chooser_data = hs.fnutils.imap(note_history, function(item) 30 | return {uuid=item.uuid, text=item.content, subText=item.ctime, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/justnote.png"), output="noteremove", arg=item.uuid} 31 | end) 32 | return chooser_data 33 | end 34 | end 35 | -- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices 36 | obj.init_func = justNoteRequest 37 | -- Insert a friendly tip at the head so users know what to do next. 38 | obj.description = nil 39 | -- As the user is typing, the callback function will be called for every keypress. The returned value is a table. 40 | 41 | local function isInNoteHistory(value, tbl) 42 | for idx,val in pairs(tbl) do 43 | if val.uuid == value then 44 | return true 45 | end 46 | end 47 | return false 48 | end 49 | 50 | local function justNoteStore() 51 | if spoon.HSearch then 52 | local querystr = string.gsub(spoon.HSearch.chooser:query(), "%s+$", "") 53 | if string.len(querystr) > 0 then 54 | local query_hash = hs.hash.SHA1(querystr) 55 | local note_history = hs.settings.get("just.another.note") or {} 56 | if not isInNoteHistory(query_hash, note_history) then 57 | table.insert(note_history, {uuid=query_hash, ctime="Created at "..os.date(), content=querystr}) 58 | hs.settings.set("just.another.note", note_history) 59 | end 60 | end 61 | end 62 | end 63 | 64 | local store_trigger = hs.hotkey.new("", "return", nil, function() 65 | justNoteStore() 66 | if spoon.HSearch then 67 | local chooser_data = justNoteRequest() 68 | spoon.HSearch.chooser:choices(chooser_data) 69 | spoon.HSearch.chooser:query("") 70 | end 71 | end) 72 | table.insert(obj.hotkeys, store_trigger) 73 | 74 | obj.callback = nil 75 | 76 | -- Define a new output type 77 | local function removeNote(arg) 78 | local note_history = hs.settings.get("just.another.note") or {} 79 | for idx,val in pairs(note_history) do 80 | if val.uuid == arg then 81 | table.remove(note_history, idx) 82 | hs.settings.set("just.another.note", note_history) 83 | end 84 | local chooser_data = justNoteRequest() 85 | if spoon.HSearch then 86 | spoon.HSearch.chooser:choices(chooser_data) 87 | end 88 | end 89 | end 90 | obj.new_output = { 91 | name = "noteremove", 92 | func = removeNote 93 | } 94 | 95 | 96 | return obj 97 | -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/hs_time.lua: -------------------------------------------------------------------------------- 1 | local obj={} 2 | obj.__index = obj 3 | 4 | obj.name = "timeDelta" 5 | obj.version = "1.0" 6 | obj.author = "ashfinal " 7 | 8 | -- Internal function used to find our location, so we know where to load files from 9 | local function script_path() 10 | local str = debug.getinfo(2, "S").source:sub(2) 11 | return str:match("(.*/)") 12 | end 13 | 14 | obj.spoonPath = script_path() 15 | 16 | -- Define the source's overview. A unique `keyword` key should exist, so this source can be found. 17 | obj.overview = {text="Type d ⇥ to format/query Date.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/time.png"), keyword="d"} 18 | -- Define the notice when a long-time request is being executed. It could be `nil`. 19 | obj.notice = nil 20 | 21 | -- Some global objects 22 | obj.exec_args = { 23 | '+"%Y-%m-%d"', 24 | '+"%H:%M:%S %p"', 25 | '+"%A, %B %d, %Y"', 26 | '+"%Y-%m-%d %H:%M:%S %p"', 27 | '+"%a, %b %d, %y"', 28 | '+"%m/%d/%y %H:%M %p"', 29 | '', 30 | '-u', 31 | } 32 | 33 | local function timeRequest() 34 | local chooser_data = hs.fnutils.imap(obj.exec_args, function(item) 35 | local exec_result = hs.execute("date " .. item) 36 | return {text=exec_result, subText="date " .. item, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/time.png"), output="keystrokes", arg=exec_result} 37 | end) 38 | return chooser_data 39 | end 40 | -- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices 41 | obj.init_func = timeRequest 42 | -- Insert a friendly tip at the head so users know what to do next. 43 | -- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here 44 | obj.description = {text="Date Query", subText="Type +/-1d (or y, m, w, H, M, S) to query date forward or backward.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/time.png")} 45 | 46 | -- As the user is typing, the callback function will be called for every keypress. The returned value is a table. 47 | 48 | local function splitBySpace(str) 49 | local tmptbl = {} 50 | for w in string.gmatch(str,"[+-]?%d+[ymdwHMS]") do table.insert(tmptbl,w) end 51 | return tmptbl 52 | end 53 | 54 | local function timeDeltaRequest(querystr) 55 | if string.len(querystr) > 0 then 56 | local valid_inputs = splitBySpace(querystr) 57 | if #valid_inputs > 0 then 58 | local addv_before = hs.fnutils.imap(valid_inputs, function(item) 59 | return "-v" .. item 60 | end) 61 | local vv_var = table.concat(addv_before, " ") 62 | local chooser_data = hs.fnutils.imap(obj.exec_args, function(item) 63 | local new_exec_command = "date " .. vv_var .. " " .. item 64 | local new_exec_result = hs.execute(new_exec_command) 65 | return {text=new_exec_result, subText=new_exec_command, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/time.png"), output="keystrokes", arg=new_exec_result} 66 | end) 67 | local source_desc = {text="Date Query", subText="Type +/-1d (or y, m, w, H, M, S) to query date forward or backward.", image=hs.image.imageFromPath(hs.configdir.."/resources/time.png")} 68 | table.insert(chooser_data, 1, source_desc) 69 | if spoon.HSearch then 70 | -- Make sure HSearch spoon is running now 71 | spoon.HSearch.chooser:choices(chooser_data) 72 | end 73 | end 74 | else 75 | local chooser_data = timeRequest() 76 | local source_desc = {text="Date Query", subText="Type +/-1d (or y, m, w, H, M, S) to query date forward or backward.", image=hs.image.imageFromPath(hs.configdir.."/resources/time.png")} 77 | table.insert(chooser_data, 1, source_desc) 78 | if spoon.HSearch then 79 | -- Make sure HSearch spoon is running now 80 | spoon.HSearch.chooser:choices(chooser_data) 81 | end 82 | end 83 | end 84 | 85 | obj.callback = timeDeltaRequest 86 | 87 | return obj 88 | -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/hs_v2ex.lua: -------------------------------------------------------------------------------- 1 | local obj={} 2 | obj.__index = obj 3 | 4 | obj.name = "v2exPosts" 5 | obj.version = "1.0" 6 | obj.author = "ashfinal " 7 | 8 | -- Internal function used to find our location, so we know where to load files from 9 | local function script_path() 10 | local str = debug.getinfo(2, "S").source:sub(2) 11 | return str:match("(.*/)") 12 | end 13 | 14 | obj.spoonPath = script_path() 15 | 16 | -- Define the source's overview. A unique `keyword` key should exist, so this source can be found. 17 | obj.overview = {text="Type v ⇥ to fetch v2ex posts.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/v2ex.png"), keyword="v"} 18 | -- Define the notice when a long-time request is being executed. It could be `nil`. 19 | obj.notice = {text="Requesting data, please wait a while …"} 20 | 21 | local function v2exRequest() 22 | local query_url = 'https://www.v2ex.com/api/topics/latest.json' 23 | local stat, body = hs.http.asyncGet(query_url, nil, function(status, data) 24 | if status == 200 then 25 | if pcall(function() hs.json.decode(data) end) then 26 | local decoded_data = hs.json.decode(data) 27 | if #decoded_data > 0 then 28 | local chooser_data = hs.fnutils.imap(decoded_data, function(item) 29 | local sub_content = string.gsub(item.content, "\r\n", " ") 30 | local function trim_content() 31 | if utf8.len(sub_content) > 40 then 32 | return string.sub(sub_content, 1, utf8.offset(sub_content, 40)-1) 33 | else 34 | return sub_content 35 | end 36 | end 37 | local final_content = trim_content() 38 | return {text=item.title, subText=final_content, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/v2ex.png"), outputType="browser", arg=item.url} 39 | end) 40 | local source_desc = {text="v2ex Posts", subText="Select some item to get it opened in default browser …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/v2ex.png")} 41 | table.insert(chooser_data, 1, source_desc) 42 | -- Because we don't know when asyncGet will return data, we have to refresh hs.chooser choices in this callback. 43 | if spoon.HSearch then 44 | -- Make sure HSearch spoon is running now 45 | spoon.HSearch.chooser:choices(chooser_data) 46 | spoon.HSearch.chooser:refreshChoicesCallback() 47 | end 48 | end 49 | end 50 | end 51 | end) 52 | end 53 | -- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices 54 | obj.init_func = v2exRequest 55 | -- Insert a friendly tip at the head so users know what to do next. 56 | -- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here 57 | obj.description = nil 58 | 59 | -- As the user is typing, the callback function will be called for every keypress. The returned value is a table. 60 | 61 | obj.callback = nil 62 | 63 | return obj 64 | -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/hs_yddict.lua: -------------------------------------------------------------------------------- 1 | local obj={} 2 | obj.__index = obj 3 | 4 | obj.name = "youdaoDict" 5 | obj.version = "1.0" 6 | obj.author = "ashfinal " 7 | 8 | -- Internal function used to find our location, so we know where to load files from 9 | local function script_path() 10 | local str = debug.getinfo(2, "S").source:sub(2) 11 | return str:match("(.*/)") 12 | end 13 | 14 | obj.spoonPath = script_path() 15 | 16 | -- Define the source's overview. A unique `keyword` key should exist, so this source can be found. 17 | obj.overview = {text="Type y ⇥ to use Yaodao dictionary.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/youdao.png"), keyword="y"} 18 | -- Define the notice when a long-time request is being executed. It could be `nil`. 19 | obj.notice = nil 20 | 21 | local function youdaoTips() 22 | local chooser_data = { 23 | {text="Youdao Dictionary", subText="Type something to get it translated …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/youdao.png")} 24 | } 25 | return chooser_data 26 | end 27 | 28 | -- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices 29 | obj.init_func = youdaoTips 30 | -- Insert a friendly tip at the head so users know what to do next. 31 | -- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here 32 | obj.description = nil 33 | 34 | -- As the user is typing, the callback function will be called for every keypress. The returned value is a table. 35 | 36 | local function basic_extract(arg) 37 | if arg then return arg.explains else return {} end 38 | end 39 | local function web_extract(arg) 40 | if arg then 41 | local value = hs.fnutils.imap(arg, function(item) 42 | return item.key .. table.concat(item.value, ",") 43 | end) 44 | return value 45 | else 46 | return {} 47 | end 48 | end 49 | 50 | local function youdaoInstantTrans(querystr) 51 | local youdao_keyfrom = 'hsearch' 52 | local youdao_apikey = '1199732752' 53 | local youdao_baseurl = 'http://fanyi.youdao.com/openapi.do?keyfrom=' .. youdao_keyfrom .. '&key=' .. youdao_apikey .. '&type=data&doctype=json&version=1.1&q=' 54 | if string.len(querystr) > 0 then 55 | local encoded_query = hs.http.encodeForQuery(querystr) 56 | local query_url = youdao_baseurl .. encoded_query 57 | 58 | hs.http.asyncGet(query_url, nil, function(status, data) 59 | if status == 200 then 60 | if pcall(function() hs.json.decode(data) end) then 61 | local decoded_data = hs.json.decode(data) 62 | if decoded_data.errorCode == 0 then 63 | local basictrans = basic_extract(decoded_data.basic) 64 | local webtrans = web_extract(decoded_data.web) 65 | local dictpool = hs.fnutils.concat(basictrans, webtrans) 66 | if #dictpool > 0 then 67 | local chooser_data = hs.fnutils.imap(dictpool, function(item) 68 | return {text=item, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/youdao.png"), output="clipboard", arg=item} 69 | end) 70 | -- Because we don't know when asyncGet will return data, we have to refresh hs.chooser choices in this callback. 71 | if spoon.HSearch then 72 | -- Make sure HSearch spoon is running now 73 | spoon.HSearch.chooser:choices(chooser_data) 74 | spoon.HSearch.chooser:refreshChoicesCallback() 75 | end 76 | end 77 | end 78 | end 79 | end 80 | end) 81 | else 82 | local chooser_data = { 83 | {text="Youdao Dictionary", subText="Type something to get it translated …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/youdao.png")} 84 | } 85 | if spoon.HSearch then 86 | spoon.HSearch.chooser:choices(chooser_data) 87 | spoon.HSearch.chooser:refreshChoicesCallback() 88 | end 89 | end 90 | end 91 | 92 | obj.callback = youdaoInstantTrans 93 | 94 | return obj 95 | -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/chrome.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/emoji.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/justnote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/justnote.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/menus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/menus.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/safari.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/tabs.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/taskkill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/taskkill.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/thesaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/thesaurus.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/time.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/v2ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/v2ex.png -------------------------------------------------------------------------------- /Spoons/HSearch.spoon/resources/youdao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/HSearch.spoon/resources/youdao.png -------------------------------------------------------------------------------- /Spoons/KSheet.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" : "Keybindings cheatsheet for current application", 22 | "type" : "Module", 23 | "Constructor" : [ 24 | 25 | ], 26 | "doc" : "Keybindings cheatsheet for current application\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/KSheet.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/KSheet.spoon.zip)", 27 | "Method" : [ 28 | { 29 | "doc" : "Show current application's keybindings in a webview", 30 | "desc" : "Show current application's keybindings in a webview", 31 | "parameters" : [ 32 | 33 | ], 34 | "stripped_doc" : [ 35 | "Show current application's keybindings in a webview" 36 | ], 37 | "notes" : [ 38 | 39 | ], 40 | "signature" : "KSheet:show()", 41 | "type" : "Method", 42 | "returns" : [ 43 | 44 | ], 45 | "name" : "show", 46 | "def" : "KSheet:show()" 47 | }, 48 | { 49 | "doc" : "Hide the cheatsheet webview", 50 | "desc" : "Hide the cheatsheet webview", 51 | "parameters" : [ 52 | 53 | ], 54 | "stripped_doc" : [ 55 | "Hide the cheatsheet webview" 56 | ], 57 | "notes" : [ 58 | 59 | ], 60 | "signature" : "KSheet:hide()", 61 | "type" : "Method", 62 | "returns" : [ 63 | 64 | ], 65 | "name" : "hide", 66 | "def" : "KSheet:hide()" 67 | } 68 | ], 69 | "Command" : [ 70 | 71 | ], 72 | "Field" : [ 73 | 74 | ], 75 | "items" : [ 76 | { 77 | "doc" : "Hide the cheatsheet webview", 78 | "desc" : "Hide the cheatsheet webview", 79 | "parameters" : [ 80 | 81 | ], 82 | "stripped_doc" : [ 83 | "Hide the cheatsheet webview" 84 | ], 85 | "notes" : [ 86 | 87 | ], 88 | "signature" : "KSheet:hide()", 89 | "type" : "Method", 90 | "returns" : [ 91 | 92 | ], 93 | "name" : "hide", 94 | "def" : "KSheet:hide()" 95 | }, 96 | { 97 | "doc" : "Show current application's keybindings in a webview", 98 | "desc" : "Show current application's keybindings in a webview", 99 | "parameters" : [ 100 | 101 | ], 102 | "stripped_doc" : [ 103 | "Show current application's keybindings in a webview" 104 | ], 105 | "notes" : [ 106 | 107 | ], 108 | "signature" : "KSheet:show()", 109 | "type" : "Method", 110 | "returns" : [ 111 | 112 | ], 113 | "name" : "show", 114 | "def" : "KSheet:show()" 115 | } 116 | ], 117 | "name" : "KSheet" 118 | } 119 | ] -------------------------------------------------------------------------------- /Spoons/SpeedMenu.spoon/docs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Command": [], 4 | "Constant": [], 5 | "Constructor": [], 6 | "Deprecated": [], 7 | "Field": [], 8 | "Function": [], 9 | "Method": [ 10 | { 11 | "def": "SpeedMenu:rescan()", 12 | "desc": "Redetect the active interface, darkmode …And redraw everything.", 13 | "doc": "Redetect the active interface, darkmode …And redraw everything.\n", 14 | "name": "rescan", 15 | "signature": "SpeedMenu:rescan()", 16 | "stripped_doc": "", 17 | "type": "Method" 18 | } 19 | ], 20 | "Variable": [], 21 | "desc": "Menubar netspeed meter", 22 | "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)", 23 | "items": [ 24 | { 25 | "def": "SpeedMenu:rescan()", 26 | "desc": "Redetect the active interface, darkmode …And redraw everything.", 27 | "doc": "Redetect the active interface, darkmode …And redraw everything.\n", 28 | "name": "rescan", 29 | "signature": "SpeedMenu:rescan()", 30 | "stripped_doc": "", 31 | "type": "Method" 32 | } 33 | ], 34 | "name": "SpeedMenu", 35 | "stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip)", 36 | "submodules": [], 37 | "type": "Module" 38 | } 39 | ] -------------------------------------------------------------------------------- /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("%6.2f", in_diff/1024/1024) .. ' mb/s' 29 | else 30 | obj.kbin = string.format("%6.2f", in_diff/1024) .. ' kb/s' 31 | end 32 | if out_diff/1024 > 1024 then 33 | obj.kbout = string.format("%6.2f", out_diff/1024/1024) .. ' mb/s' 34 | else 35 | obj.kbout = string.format("%6.2f", out_diff/1024) .. ' kb/s' 36 | end 37 | local disp_str = '⥄ ' .. obj.kbout .. '\n⥂ ' .. obj.kbin 38 | if obj.darkmode then 39 | obj.disp_str = hs.styledtext.new(disp_str, {font={size=9.0, color={hex="#FFFFFF"}}}) 40 | else 41 | obj.disp_str = hs.styledtext.new(disp_str, {font={size=9.0, color={hex="#000000"}}}) 42 | end 43 | obj.menubar:setTitle(obj.disp_str) 44 | obj.inseq = in_seq 45 | obj.outseq = out_seq 46 | end 47 | 48 | --- SpeedMenu:rescan() 49 | --- Method 50 | --- Redetect the active interface, darkmode …And redraw everything. 51 | --- 52 | 53 | function obj:rescan() 54 | obj.interface = hs.network.primaryInterfaces() 55 | obj.darkmode = hs.osascript.applescript('tell application "System Events"\nreturn dark mode of appearance preferences\nend tell') 56 | local menuitems_table = {} 57 | if obj.interface then 58 | -- Inspect active interface and create menuitems 59 | local interface_detail = hs.network.interfaceDetails(obj.interface) 60 | if interface_detail.AirPort then 61 | local ssid = interface_detail.AirPort.SSID 62 | table.insert(menuitems_table, { 63 | title = "SSID: " .. ssid, 64 | tooltip = "Copy SSID to clipboard", 65 | fn = function() hs.pasteboard.setContents(ssid) end 66 | }) 67 | end 68 | if interface_detail.IPv4 then 69 | local ipv4 = interface_detail.IPv4.Addresses[1] 70 | table.insert(menuitems_table, { 71 | title = "IPv4: " .. ipv4, 72 | tooltip = "Copy IPv4 to clipboard", 73 | fn = function() hs.pasteboard.setContents(ipv4) end 74 | }) 75 | end 76 | if interface_detail.IPv6 then 77 | local ipv6 = interface_detail.IPv6.Addresses[1] 78 | table.insert(menuitems_table, { 79 | title = "IPv6: " .. ipv6, 80 | tooltip = "Copy IPv6 to clipboard", 81 | fn = function() hs.pasteboard.setContents(ipv6) end 82 | }) 83 | end 84 | local macaddr = hs.execute('ifconfig ' .. obj.interface .. ' | grep ether | awk \'{print $2}\'') 85 | table.insert(menuitems_table, { 86 | title = "MAC Addr: " .. macaddr, 87 | tooltip = "Copy MAC Address to clipboard", 88 | fn = function() hs.pasteboard.setContents(macaddr) end 89 | }) 90 | -- Start watching the netspeed delta 91 | obj.instr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $7}\'' 92 | obj.outstr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $10}\'' 93 | 94 | obj.inseq = hs.execute(obj.instr) 95 | obj.outseq = hs.execute(obj.outstr) 96 | 97 | if obj.timer then 98 | obj.timer:stop() 99 | obj.timer = nil 100 | end 101 | obj.timer = hs.timer.doEvery(1, data_diff) 102 | end 103 | table.insert(menuitems_table, { 104 | title = "Rescan Network Interfaces", 105 | fn = function() obj:rescan() end 106 | }) 107 | obj.menubar:setTitle("⚠︎") 108 | obj.menubar:setMenu(menuitems_table) 109 | end 110 | 111 | return obj 112 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | helper = "spec/spec_helper.lua" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/.gitignore: -------------------------------------------------------------------------------- 1 | luacov.stats.out 2 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/.luacheckrc: -------------------------------------------------------------------------------- 1 | std = { 2 | globals = {"vimModeScriptPath"} 3 | read_globals = {} 4 | } 5 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | env: 6 | matrix: 7 | - LUA="lua 5.4" 8 | 9 | before_install: 10 | - pip install hererocks 11 | - hererocks here -r^ --$LUA # Install latest LuaRocks version 12 | # plus the Lua version for this build job 13 | # into 'here' subdirectory 14 | - export PATH=$PATH:$PWD/here/bin # Add directory with all installed binaries to PATH 15 | - eval `luarocks path --bin` 16 | - bin/dev-setup 17 | 18 | install: 19 | - echo "Nothing to install" 20 | 21 | script: 22 | - busted -c 23 | 24 | after_success: 25 | - luacov-coveralls -v 26 | 27 | notifications: 28 | email: 29 | on_success: change 30 | on_failure: always 31 | 32 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2020-12-28 2 | 3 | * Added a beta feature for enforcing fallback mode on certain URL patterns in Chrome and Safari (see README) 4 | * Made hitting `escape` when tapping `g` cancel and reset to normal mode [closes #56] 5 | 6 | # 2020-11-30 7 | 8 | * Rollback the keypress disallowing - it conflicts with the keys we send in fallback mode. 9 | 10 | # 2020-11-29 11 | 12 | * Added the `iw` "in word" text object motion 13 | * Added the `i[`, `i<`, `i{`, `i'`, `i"`, and `i`` motions. 14 | * Added `ctrl-u` and `ctrl-d` to page up/down half a visible screen 15 | * Update the modal to disallow pressing keys that aren't registered with the current active mode 16 | 17 | # 2020-11-04 18 | 19 | * Fix offset calculations with UTF-8 characters like smart quotes. 20 | * Add a beta feature for enabling a block cursor overlay in fields that support it in #65. Turn this on with `vim:enableBetaFeature('block_cursor_overlay')` 21 | 22 | # 2020-10-15 23 | 24 | * Fix the library to work on the new Lua 5.4 version of Hammerspoon. Previous releases before Hammerspoon 0.9.79 will not work anymore. 25 | 26 | # 2020-09-06 27 | 28 | * Fix #54 where the overlay doesn't sit above the Safari location bar 29 | 30 | # 2020-08-30 31 | 32 | * Allow advanced mode to work in `AXComboBox` fields 33 | 34 | # 2020-08-29 35 | 36 | * Passthru the main Vim normal mode keys when focused in a disabled app 37 | * Update the key sequence to have a default timeout of 140ms to accommodate `jj` users 38 | * Make key sequence timeout optionally configurable 39 | 40 | There is no changelog prior to this date :( 41 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'capybara' 6 | gem 'pry' 7 | gem 'rack' 8 | gem 'rspec' 9 | gem 'webdrivers' 10 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.7.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | capybara (3.29.0) 7 | addressable 8 | mini_mime (>= 0.1.3) 9 | nokogiri (~> 1.8) 10 | rack (>= 1.6.0) 11 | rack-test (>= 0.6.3) 12 | regexp_parser (~> 1.5) 13 | xpath (~> 3.2) 14 | childprocess (3.0.0) 15 | coderay (1.1.2) 16 | diff-lcs (1.3) 17 | method_source (0.9.2) 18 | mini_mime (1.0.2) 19 | mini_portile2 (2.4.0) 20 | nokogiri (1.10.10) 21 | mini_portile2 (~> 2.4.0) 22 | pry (0.12.2) 23 | coderay (~> 1.1.0) 24 | method_source (~> 0.9.0) 25 | public_suffix (4.0.1) 26 | rack (2.1.4) 27 | rack-test (1.1.0) 28 | rack (>= 1.0, < 3) 29 | regexp_parser (1.6.0) 30 | rspec (3.9.0) 31 | rspec-core (~> 3.9.0) 32 | rspec-expectations (~> 3.9.0) 33 | rspec-mocks (~> 3.9.0) 34 | rspec-core (3.9.0) 35 | rspec-support (~> 3.9.0) 36 | rspec-expectations (3.9.0) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.9.0) 39 | rspec-mocks (3.9.0) 40 | diff-lcs (>= 1.2.0, < 2.0) 41 | rspec-support (~> 3.9.0) 42 | rspec-support (3.9.0) 43 | rubyzip (2.0.0) 44 | selenium-webdriver (3.142.6) 45 | childprocess (>= 0.5, < 4.0) 46 | rubyzip (>= 1.2.2) 47 | webdrivers (4.1.3) 48 | nokogiri (~> 1.6) 49 | rubyzip (>= 1.3.0) 50 | selenium-webdriver (>= 3.0, < 4.0) 51 | xpath (3.2.0) 52 | nokogiri (~> 1.8) 53 | 54 | PLATFORMS 55 | ruby 56 | 57 | DEPENDENCIES 58 | capybara 59 | pry 60 | rack 61 | rspec 62 | webdrivers 63 | 64 | BUNDLED WITH 65 | 2.0.2 66 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/Rakefile: -------------------------------------------------------------------------------- 1 | task :spec do 2 | system("bundle exec rspec -t advanced spec") 3 | system("bundle exec rspec -t fallback spec") 4 | end 5 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/bin/dev-setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function install_lua_package() { 4 | local package_name=$1 5 | 6 | if ! luarocks show "$package_name" 2>&1 >/dev/null; then 7 | echo "===> $ luarocks install $@" 8 | luarocks install $@ 9 | echo 10 | else 11 | echo "===> $package_name already installed" 12 | fi 13 | } 14 | 15 | function brew_install() { 16 | local package=$1 17 | 18 | if brew list "$package" > /dev/null 2>&1; then 19 | echo "+ $package already installed... skipping." 20 | else 21 | brew install $@ 22 | fi 23 | } 24 | 25 | function brew_cask_install() { 26 | local package=$1 27 | 28 | if brew cask list "$package" > /dev/null 2>&1; then 29 | echo "+ $package already installed... skipping." 30 | else 31 | brew cask install $@ 32 | fi 33 | } 34 | 35 | brew_install selenium-server-standalone 36 | brew_cask_install chromedriver 37 | 38 | install_lua_package penlight 39 | install_lua_package lua_cliargs 40 | install_lua_package luasystem 41 | install_lua_package mediator_lua 42 | install_lua_package lua-term 43 | install_lua_package luacov 44 | install_lua_package luacov-coveralls 45 | install_lua_package busted 46 | install_lua_package luacheck 47 | install_lua_package debugger 48 | install_lua_package luaselenium 49 | install_lua_package luautf8 50 | install_lua_package luassert 51 | install_lua_package say 52 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/docs.json: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | ] -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/docs/Integration_Tests.md: -------------------------------------------------------------------------------- 1 | # Setting up to run integration tests 2 | 3 | The integration tests require a few things up front to run: 4 | 5 | ``` 6 | brew install chromedriver 7 | ``` 8 | 9 | Next, you'll have to deal with some first-run crap, in order to give 10 | `osascript` permissions. This is the only way I could figure out how to give 11 | RSpec the ability to send native OS X keys. If there is a way to do this 12 | without needing permissions, please open a PR/issue! 13 | 14 | ``` 15 | bundle install 16 | bundle exec rspec spec 17 | ``` 18 | 19 | It will ask for permissions for `System Events.app`, you'll need to give them: 20 | 21 | ![image](https://user-images.githubusercontent.com/59429/70395829-c00cdf00-19b7-11ea-91c2-50cefe25b329.png) 22 | 23 | Run `bundle exec rspec spec` again. 24 | 25 | It will also ask for permissions for `osascript` (via iTerm), you'll need to enable iTerm under accessibility here: 26 | 27 | ![image](https://user-images.githubusercontent.com/59429/70395855-0a8e5b80-19b8-11ea-9343-5da0496cdf9b.png) 28 | 29 | You should now be able to run: 30 | 31 | ``` 32 | bundle exec rspec spec 33 | ``` 34 | 35 | You can't have another instance of Chrome running while you run the tests, or 36 | else the wrong Vim modes get entered, unfortunately. The rspec runner will kill 37 | Chrome for you. You'll can get your tabs back when you open Chrome back up. 38 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/images/vim-mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/VimMode.spoon/images/vim-mode.gif -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/init.lua: -------------------------------------------------------------------------------- 1 | inspect = hs.inspect.inspect 2 | 3 | local function scriptPath() 4 | local str = debug.getinfo(2, "S").source:sub(2) 5 | return str:match("(.*/)") 6 | end 7 | 8 | vimModeScriptPath = scriptPath() 9 | 10 | local Vim = dofile(vimModeScriptPath .. "lib/vim.lua") 11 | 12 | return Vim 13 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/app_watcher.lua: -------------------------------------------------------------------------------- 1 | local AppWatcher = {} 2 | 3 | local function debugEventType(eventType) 4 | if eventType == hs.application.watcher.activated then 5 | return "activated" 6 | elseif eventType == hs.application.watcher.deactivated then 7 | return "deactivated" 8 | elseif eventType == hs.application.watcher.hidden then 9 | return "hidden" 10 | elseif eventType == hs.application.watcher.launched then 11 | return "launched" 12 | elseif eventType == hs.application.watcher.launching then 13 | return "launching" 14 | elseif eventType == hs.application.watcher.terminated then 15 | return "terminated" 16 | elseif eventType == hs.application.watcher.unhidden then 17 | return "unhidden" 18 | else 19 | return "unknown event: " .. eventType 20 | end 21 | end 22 | 23 | function AppWatcher:new(vim) 24 | local watcher = { 25 | -- These are the default apps that we automatically turn off Vim mode 26 | -- in when they become focused in the OS. 27 | disabled = { 28 | MacVim = true, 29 | iTerm = true, 30 | iTerm2 = true, 31 | Terminal = true 32 | }, 33 | vim = vim, 34 | watcher = nil 35 | } 36 | 37 | setmetatable(watcher, self) 38 | self.__index = self 39 | 40 | watcher:createWatcher() 41 | 42 | return watcher 43 | end 44 | 45 | function AppWatcher:disableVim() 46 | self.vim:exit() 47 | self.vim:disable() 48 | end 49 | 50 | function AppWatcher:enableVim() 51 | self.vim:enable() 52 | end 53 | 54 | function AppWatcher:start() 55 | self.watcher:start() 56 | 57 | return self 58 | end 59 | 60 | function AppWatcher:stop() 61 | self.watcher:stop() 62 | 63 | return self 64 | end 65 | 66 | function AppWatcher:disableApp(name) 67 | self.disabled[name] = true 68 | 69 | -- disable it proactively if needed 70 | local currentApplication = hs.application.frontmostApplication() 71 | 72 | if currentApplication and currentApplication:name() == name then 73 | self:disableVim() 74 | end 75 | 76 | return self 77 | end 78 | 79 | function AppWatcher:createWatcher() 80 | -- build the watcher 81 | self.watcher = 82 | hs.application.watcher.new(function(applicationName, eventType, application) 83 | local disabled = self.disabled[applicationName] 84 | 85 | if eventType == hs.application.watcher.activated then 86 | if disabled then 87 | self:disableVim() 88 | else 89 | self:enableVim() 90 | end 91 | end 92 | end) 93 | 94 | -- If we are currently in this disabled application, exit vim mode 95 | -- and disable 96 | local currentApplication = hs.application.frontmostApplication() 97 | 98 | if self.disabled[currentApplication:name()] then 99 | self:disableVim() 100 | end 101 | end 102 | 103 | return AppWatcher 104 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/axuielement.lua: -------------------------------------------------------------------------------- 1 | local versionUtils = dofile(vimModeScriptPath .. "lib/utils/version.lua") 2 | 3 | -- make this global so it only runs once 4 | vimModeAxLibrary = nil 5 | 6 | local function loadAxUiElement() 7 | if vimModeAxLibrary then return vimModeAxLibrary end 8 | 9 | -- support old versions of Hammerspoon that didn't have axuielement packaged. 10 | if versionUtils.hammerspoonVersionLessThan("0.9.79") then 11 | vimModeAxLibrary = require("hs._asm.axuielement") 12 | else 13 | -- use the built-in 14 | vimModeAxLibrary = require("hs.axuielement") 15 | end 16 | 17 | return vimModeAxLibrary 18 | end 19 | 20 | return loadAxUiElement() 21 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/block_cursor.lua: -------------------------------------------------------------------------------- 1 | local AccessibilityBuffer = dofile(vimModeScriptPath .. "lib/accessibility_buffer.lua") 2 | 3 | local BlockCursor = {} 4 | 5 | function BlockCursor:new(vim) 6 | local canvas = hs.canvas.new({ x = 0, y = 0, h = 1, w = 1 }) 7 | local rectangleElementIndex = 1 8 | 9 | canvas:level('overlay') 10 | canvas:insertElement( 11 | { 12 | type = 'rectangle', 13 | action = 'fill', 14 | fillColor = { red = 0, green = 0, blue = 0, alpha = 0.2 }, 15 | frame = { x = "0%", y = "0%", h = "100%", w = "100%", }, 16 | withShadow = false 17 | }, 18 | rectangleElementIndex 19 | ) 20 | 21 | local cursor = { 22 | canvas = canvas, 23 | vim = vim, 24 | } 25 | 26 | setmetatable(cursor, self) 27 | self.__index = self 28 | 29 | cursor.redrawTimer = hs.timer.new(1 / 60, function() 30 | local result = cursor:_renderFrame() 31 | 32 | if not result then 33 | cursor:hide() 34 | end 35 | end) 36 | 37 | return cursor 38 | end 39 | 40 | function BlockCursor:show() 41 | if self.canvas:isShowing() then return nil end 42 | 43 | self.redrawTimer:start() 44 | self.canvas:show() 45 | end 46 | 47 | function BlockCursor:hide() 48 | if not self.canvas:isShowing() then return nil end 49 | 50 | self.canvas:hide() 51 | self.redrawTimer:stop() 52 | end 53 | 54 | -- Renders a single frame. Returns `true` if successful. 55 | function BlockCursor:_renderFrame() 56 | local buffer = AccessibilityBuffer:new(self.vim) 57 | if not buffer:isValid() then return false end 58 | 59 | local currentElement = buffer:getCurrentElement() 60 | 61 | -- We don't want to draw the cursor if we're at the end of the textbox (or 62 | -- past the end!) 63 | if buffer:isAtLastVisibleCharacter() then return false end 64 | 65 | -- Get the range for the next character after the blinking cursor 66 | local range = buffer:getSelectionRange() 67 | local caretRange = { 68 | location = range.location, 69 | length = 1, 70 | } 71 | 72 | -- Get the { h, w, x, y } bounding box for the next character's range so we 73 | -- can draw over it. 74 | local bounds = currentElement:parameterizedAttributeValue( 75 | "AXBoundsForRange", 76 | caretRange 77 | ) 78 | 79 | -- chrome doesn't have good support for AXBoundsForRange and returns a 0-sized 80 | -- bounds: 81 | -- 82 | -- https://groups.google.com/a/chromium.org/g/chromium-accessibility/c/eB34iqVFAu8 83 | if bounds.h == 0 or bounds.w == 0 then return false end 84 | 85 | -- move the position and resize 86 | self.canvas:topLeft({ x = bounds.x, y = bounds.y }) 87 | self.canvas:size({ h = bounds.h, w = bounds.w }) 88 | 89 | return true 90 | end 91 | 92 | return BlockCursor 93 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/command_state.lua: -------------------------------------------------------------------------------- 1 | local numberUtils = dofile(vimModeScriptPath .. "lib/utils/number_utils.lua") 2 | 3 | local CommandState = {} 4 | 5 | function CommandState:new() 6 | local state = { 7 | charsEntered = "", 8 | motion = nil, 9 | motionTimes = nil, 10 | operator = nil, 11 | operatorTimes = nil, 12 | pendingInput = nil 13 | } 14 | 15 | setmetatable(state, self) 16 | self.__index = self 17 | 18 | return state 19 | end 20 | 21 | function CommandState:getCharsEntered() 22 | return self.charsEntered 23 | end 24 | 25 | function CommandState:resetCharsEntered() 26 | self.charsEntered = "" 27 | return self 28 | end 29 | 30 | function CommandState:pushChar(char) 31 | if char then 32 | self.charsEntered = self.charsEntered .. char 33 | end 34 | 35 | return self 36 | end 37 | 38 | function CommandState:getRepeatTimes() 39 | local operatorTimes = self:getCount('operator') or 1 40 | local motionTimes = self:getCount('motion') or 1 41 | 42 | return operatorTimes * motionTimes 43 | end 44 | 45 | function CommandState:getCount(type) 46 | return self[type .. "Times"] 47 | end 48 | 49 | function CommandState:getPendingInput() 50 | return self.pendingInput 51 | end 52 | 53 | function CommandState:setPendingInput(value) 54 | self.pendingInput = value 55 | 56 | return self 57 | end 58 | 59 | function CommandState:pushCountDigit(type, digit) 60 | local key = type .. "Times" 61 | self[key] = numberUtils.pushDigit(self[key], digit) 62 | end 63 | 64 | return CommandState 65 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/config.lua: -------------------------------------------------------------------------------- 1 | local Config = {} 2 | 3 | function Config:new(options) 4 | options = options or {} 5 | 6 | -- defaults 7 | local config = { 8 | alert = { 9 | font = "Courier New" 10 | }, 11 | betaFeatures = {}, 12 | fallbackOnlyUrlPatterns = {}, 13 | shouldShowAlertInNormalMode = true, 14 | shouldDimScreenInNormalMode = true, 15 | } 16 | 17 | setmetatable(config, self) 18 | self.__index = self 19 | 20 | config:setOptions(options) 21 | 22 | return config 23 | end 24 | 25 | function Config:setOptions(options) 26 | for key, value in pairs(options) do 27 | self[key] = value 28 | end 29 | end 30 | 31 | function Config:isBetaFeatureEnabled(feature) 32 | return not not self.betaFeatures[feature] 33 | end 34 | 35 | function Config:enableBetaFeature(feature) 36 | self.betaFeatures[feature] = true 37 | end 38 | 39 | function Config:disableBetaFeature(feature) 40 | self.betaFeatures[feature] = false 41 | end 42 | 43 | return Config 44 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/contextual_modal.lua: -------------------------------------------------------------------------------- 1 | local Registry = dofile(vimModeScriptPath .. "lib/contextual_modal/registry.lua") 2 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 3 | local tableUtils = dofile(vimModeScriptPath .. "lib/utils/table.lua") 4 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 5 | 6 | local ContextualModal = {} 7 | 8 | local function mapToList(map) 9 | local list = {} 10 | 11 | for key, value in pairs(map) do 12 | table.insert(list, key) 13 | end 14 | 15 | return list 16 | end 17 | 18 | -- Wraps a modal and provides different key layers depending on which 19 | -- context you happen to be in. 20 | -- 21 | -- Swapping between multiple modals is too slow, so having a single modal 22 | -- that has context layers helps with key latency and lets us buffer keystrokes 23 | -- correctly. 24 | -- 25 | -- To bind keys to a new context layer, use the withContext helper to change 26 | -- the binding context: 27 | -- 28 | -- local modal = ContextualModal:new() 29 | -- 30 | -- modal 31 | -- :withContext("foo") 32 | -- :bind({}, 'e', function() print("foo e") end) 33 | -- 34 | -- modal 35 | -- :withContext("bar") 36 | -- :bind({}, 'e', function() print("bar e") end) 37 | -- 38 | -- modal:enterContext("foo") -- pressing 'e' prints 'foo e' 39 | -- modal:enterContext("bar") -- pressing 'e' prints 'bar e' 40 | function ContextualModal:new() 41 | local registry = Registry:new() 42 | local wrapper = { 43 | activeContext = nil, 44 | bindingContext = nil, 45 | bindings = {}, 46 | entered = false, 47 | modal = hs.hotkey.modal.new(), 48 | onBeforePress = function() end, 49 | registry = registry, 50 | } 51 | 52 | setmetatable(wrapper, self) 53 | self.__index = self 54 | 55 | return wrapper 56 | end 57 | 58 | function ContextualModal:handlePress(mods, key, eventType) 59 | return function() 60 | local handler = self.registry:getHandler( 61 | self.activeContext, 62 | mods, 63 | key, 64 | eventType 65 | ) 66 | 67 | if handler then 68 | self.onBeforePress(mods, key) 69 | handler() 70 | end 71 | end 72 | end 73 | 74 | function ContextualModal:setOnBeforePress(fn) 75 | self.onBeforePress = fn 76 | return self 77 | end 78 | 79 | function ContextualModal:hasBinding(mods, key) 80 | if not self.bindings[key] then return false end 81 | 82 | for _, boundMods in pairs(self.bindings[key]) do 83 | if tableUtils.matches(boundMods, mods) then 84 | return true 85 | end 86 | end 87 | 88 | return false 89 | end 90 | 91 | function ContextualModal:registerBinding(mods, key) 92 | if not self.bindings[key] then self.bindings[key] = {} end 93 | 94 | table.insert(self.bindings[key], mods) 95 | 96 | return self 97 | end 98 | 99 | function ContextualModal:bind(mods, key, pressedfn, releasedfn, repeatfn) 100 | self.registry:registerHandler( 101 | self.bindingContext, 102 | mods, 103 | key, 104 | pressedfn, 105 | releasedfn, 106 | repeatfn 107 | ) 108 | 109 | -- only bind once for this modal 110 | if not self:hasBinding(mods, key) then 111 | self:registerBinding(mods, key) 112 | 113 | self.modal:bind( 114 | mods, 115 | key, 116 | self:handlePress(mods, key, 'onPressed'), 117 | self:handlePress(mods, key, 'onReleased'), 118 | self:handlePress(mods, key, 'onRepeat') 119 | ) 120 | end 121 | 122 | return self 123 | end 124 | 125 | function ContextualModal:bindWithRepeat(mods, key, fn) 126 | return self:bind(mods, key, fn, nil, fn) 127 | end 128 | 129 | function ContextualModal:withContext(contextKey) 130 | self.bindingContext = contextKey 131 | 132 | return self 133 | end 134 | 135 | function ContextualModal:enterContext(contextKey) 136 | self.activeContext = contextKey 137 | 138 | if not self.entered then 139 | self.entered = true 140 | self.modal:enter() 141 | end 142 | 143 | return self 144 | end 145 | 146 | function ContextualModal:exit() 147 | self.activeContext = nil 148 | 149 | if self.entered then 150 | self.entered = false 151 | self.modal:exit() 152 | end 153 | 154 | return self 155 | end 156 | 157 | return ContextualModal 158 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/contextual_modal/registry.lua: -------------------------------------------------------------------------------- 1 | local tableUtils = dofile(vimModeScriptPath .. "lib/utils/table.lua") 2 | 3 | local Registry = {} 4 | 5 | function Registry:new() 6 | local registry = { 7 | fns = {} 8 | } 9 | 10 | setmetatable(registry, self) 11 | self.__index = self 12 | 13 | return registry 14 | end 15 | 16 | function Registry:registerHandler(contextKey, mods, key, pressedfn, releasedfn, repeatfn) 17 | if not self.fns[contextKey] then self.fns[contextKey] = {} end 18 | local context = self.fns[contextKey] 19 | 20 | if not context[key] then context[key] = {} end 21 | local keyHandlers = context[key] 22 | 23 | table.insert(keyHandlers, { 24 | mods = mods, 25 | handlers = { 26 | onPressed = pressedfn, 27 | onReleased = releasedfn, 28 | onRepeat = repeatfn 29 | } 30 | }) 31 | 32 | return self 33 | end 34 | 35 | function Registry:hasAnyHandler(contextKey, mods, key) 36 | local context = self.fns[contextKey] 37 | if not context then return false end 38 | 39 | local keyHandlers = context[key] 40 | if not keyHandlers then return false end 41 | 42 | for _, entry in pairs(keyHandlers) do 43 | if tableUtils.matches(entry.mods, mods) then 44 | local handlers = entry.handlers 45 | 46 | return not not (handlers.onPressed or handlers.onRepeat or handlers.onReleased) 47 | end 48 | end 49 | 50 | return false 51 | end 52 | 53 | function Registry:getHandler(contextKey, mods, key, eventType) 54 | local context = self.fns[contextKey] 55 | if not context then return nil end 56 | 57 | local keyHandlers = context[key] 58 | if not keyHandlers then return nil end 59 | 60 | for _, entry in pairs(keyHandlers) do 61 | if tableUtils.matches(entry.mods, mods) then 62 | return entry.handlers[eventType] 63 | end 64 | end 65 | 66 | return nil 67 | end 68 | 69 | return Registry 70 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/focus_watcher.lua: -------------------------------------------------------------------------------- 1 | local ax = dofile(vimModeScriptPath .. "lib/axuielement.lua") 2 | 3 | local registeredPids = {} 4 | 5 | local function createApplicationWatcher(application, vim) 6 | local pid = application:pid() 7 | local observer 8 | 9 | local creator = function () 10 | if registeredPids[pid] then return end 11 | 12 | observer = ax.observer.new(application:pid()) 13 | 14 | observer 15 | :callback(function() vim:exit() end) 16 | :addWatcher( 17 | ax.applicationElement(application), 18 | "AXFocusedUIElementChanged" 19 | ) 20 | :start() 21 | 22 | registeredPids[pid] = observer 23 | end 24 | 25 | if not pcall(creator) then 26 | registeredPids[pid] = nil 27 | 28 | vimLogger.d( 29 | "Could not start watcher for PID: " .. pid .. 30 | " and name: " .. application:name() 31 | ) 32 | end 33 | 34 | return observer 35 | end 36 | 37 | -- When someone focuses out of a field, we want to exit Vim mode. 38 | local function createFocusWatcher(vim) 39 | createApplicationWatcher(hs.application.frontmostApplication(), vim) 40 | 41 | local watcher = hs.application.watcher.new(function(_, eventType, application) 42 | if eventType == hs.application.watcher.activated then 43 | createApplicationWatcher(application, vim) 44 | end 45 | end) 46 | 47 | watcher:start() 48 | 49 | return watcher 50 | end 51 | 52 | return createFocusWatcher 53 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/hot_patcher.lua: -------------------------------------------------------------------------------- 1 | local axUtils = dofile(vimModeScriptPath .. "lib/utils/ax.lua") 2 | 3 | local function createHotPatcher() 4 | -- Always patch the currently open application. 5 | axUtils.patchCurrentApplication() 6 | 7 | return hs.application.watcher.new(function(_, eventType) 8 | if eventType == hs.application.watcher.activated then 9 | axUtils.patchCurrentApplication() 10 | end 11 | end) 12 | end 13 | 14 | return createHotPatcher 15 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/key_sequence.lua: -------------------------------------------------------------------------------- 1 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 2 | local KeySequence = {} 3 | 4 | function KeySequence:new(keys, maxDelayBetweenKeysMilliseconds, onSequencePressed) 5 | local sequence = {} 6 | 7 | setmetatable(sequence, self) 8 | self.__index = self 9 | 10 | sequence.keys = stringUtils.toChars(keys) 11 | sequence.maxDelayBetweenKeysMilliseconds = maxDelayBetweenKeysMilliseconds or 140 12 | sequence.onSequencePressed = onSequencePressed 13 | sequence.enabled = false 14 | sequence.timer = nil 15 | sequence.sequencePosition = 1 16 | sequence.typedEvents = {} 17 | sequence.alreadyTyped = "" 18 | sequence:resetTap() 19 | 20 | return sequence 21 | end 22 | 23 | function KeySequence:enable() 24 | if self.enabled then return end 25 | 26 | self.enabled = true 27 | self:reset() 28 | self.tap:start() 29 | 30 | return self 31 | end 32 | 33 | function KeySequence:disable() 34 | if not self.enabled then return end 35 | 36 | self.enabled = false 37 | self:reset() 38 | self.tap:stop() 39 | 40 | return self 41 | end 42 | 43 | function KeySequence:resetTap() 44 | self.tap = hs.eventtap.new( 45 | { hs.eventtap.event.types.keyDown }, 46 | self:buildEventHandler() 47 | ) 48 | end 49 | 50 | function KeySequence:reset() 51 | self:cancelTimer() 52 | self:resetEvents() 53 | self.sequencePosition = 1 54 | self.alreadyTyped = "" 55 | end 56 | 57 | function KeySequence:resetEvents() 58 | self.typedEvents = {} 59 | return self 60 | end 61 | 62 | function KeySequence:cancelTimer() 63 | if self.timer then self.timer:stop() end 64 | end 65 | 66 | function KeySequence:startTimer(fn) 67 | self.timer = hs.timer.doAfter(self.maxDelayBetweenKeysMilliseconds / 1000, fn) 68 | end 69 | 70 | function KeySequence:recordEvent(event) 71 | local currentModifiers = event:getFlags() 72 | local currentKey = event:getKeyCode() 73 | 74 | table.insert( 75 | self.typedEvents, 76 | hs.eventtap.event.newKeyEvent(currentModifiers, currentKey, true) 77 | ) 78 | 79 | table.insert( 80 | self.typedEvents, 81 | hs.eventtap.event.newKeyEvent(currentModifiers, currentKey, false) 82 | ) 83 | end 84 | 85 | function KeySequence:recordKey(key) 86 | self.alreadyTyped = self.alreadyTyped .. key 87 | end 88 | 89 | local function getTableSize(t) 90 | local count = 0 91 | for _, __ in pairs(t) do count = count + 1 end 92 | 93 | return count 94 | end 95 | 96 | function KeySequence:buildEventHandler() 97 | return function(event) 98 | if not self.enabled then return end 99 | 100 | -- got another key, kill the abort timer 101 | self:cancelTimer() 102 | 103 | local position = self.sequencePosition 104 | local keyPressed = hs.keycodes.map[event:getKeyCode()] 105 | local keyToCompare = self.keys[position] 106 | 107 | if keyPressed == keyToCompare and getTableSize(event:getFlags()) == 0 then 108 | local typedFinalChar = position == #self.keys 109 | 110 | if typedFinalChar then 111 | self:disable() 112 | self.onSequencePressed() 113 | else 114 | self.sequencePosition = position + 1 115 | self:recordEvent(event) 116 | self:recordKey(keyPressed) 117 | 118 | self:startTimer(function() 119 | self.tap:stop() 120 | hs.eventtap.keyStrokes(self.alreadyTyped) 121 | self.tap:start() 122 | 123 | self:reset() 124 | end) 125 | end 126 | 127 | return true 128 | elseif self.sequencePosition > 1 then 129 | -- Abort the sequence and pass through any keys we already typed 130 | self:recordEvent(event) 131 | local events = self.typedEvents 132 | 133 | self:reset() 134 | 135 | return true, events 136 | end 137 | 138 | return false 139 | end 140 | end 141 | 142 | return KeySequence 143 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motion.lua: -------------------------------------------------------------------------------- 1 | local Motion = {} 2 | 3 | function Motion:new(fields) 4 | local motion = fields or {} 5 | 6 | motion.extraChar = nil 7 | 8 | setmetatable(motion, self) 9 | self.__index = self 10 | 11 | return motion 12 | end 13 | 14 | function Motion:setExtraChar(char) 15 | self.extraChar = char 16 | 17 | return self 18 | end 19 | 20 | function Motion:getExtraChar() 21 | return self.extraChar 22 | end 23 | 24 | function Motion:getMovements() 25 | error("Please implement getMovements()") 26 | end 27 | 28 | function Motion:getRange(buffer) 29 | error("Please implement getRange()") 30 | end 31 | 32 | function Motion.getModeForTransition() 33 | return "normal" 34 | end 35 | 36 | return Motion 37 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/back_word.lua: -------------------------------------------------------------------------------- 1 | local machine = dofile(vimModeScriptPath .. 'lib/utils/statemachine.lua') 2 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 3 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 4 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 5 | 6 | local isPunctuation = stringUtils.isPunctuation 7 | local isWhitespace = stringUtils.isWhitespace 8 | local isPrintableChar = stringUtils.isPrintableChar 9 | 10 | local BackWord = Motion:new{ name = 'back_word' } 11 | 12 | local parser = machine.create({ 13 | initial = 'started', 14 | events = { 15 | { name = 'seenPrintable', from = 'started', to = 'first-printable' }, 16 | { name = 'seenPunctuation', from = 'started', to = 'first-punctuation' }, 17 | { name = 'seenWhitespace', from = 'started', to = 'ignore-whitespace' }, 18 | 19 | { name = 'seenPrintable', from = 'first-printable', to = 'printable-sequence' }, 20 | { name = 'seenPunctuation', from = 'first-printable', to = 'punctuation-sequence' }, 21 | { name = 'seenWhitespace', from = 'first-printable', to = 'ignore-whitespace' }, 22 | { name = 'reset', from = 'first-printable', to = 'started' }, 23 | 24 | { name = 'seenPrintable', from = 'ignore-whitespace', to = 'first-printable' }, 25 | { name = 'seenPunctuation', from = 'ignore-whitespace', to = 'first-punctuation' }, 26 | { name = 'seenWhitespace', from = 'ignore-whitespace', to = 'ignore-whitespace' }, 27 | { name = 'reset', from = 'ignore-whitespace', to = 'started' }, 28 | 29 | { name = 'seenPrintable', from = 'printable-sequence', to = 'printable-sequence' }, 30 | { name = 'seenPunctuation', from = 'printable-sequence', to = 'finished' }, 31 | { name = 'seenWhitespace', from = 'printable-sequence', to = 'finished' }, 32 | { name = 'reset', from = 'printable-sequence', to = 'started' }, 33 | 34 | { name = 'seenPrintable', from = 'first-punctuation', to = 'first-printable' }, 35 | { name = 'seenPunctuation', from = 'first-punctuation', to = 'punctuation-sequence' }, 36 | { name = 'seenWhitespace', from = 'first-punctuation', to = 'ignore-whitespace' }, 37 | { name = 'reset', from = 'first-punctuation', to = 'started' }, 38 | 39 | { name = 'seenPrintable', from = 'punctuation-sequence', to = 'finished' }, 40 | { name = 'seenPunctuation', from = 'punctuation-sequence', to = 'punctuation-sequence' }, 41 | { name = 'seenWhitespace', from = 'punctuation-sequence', to = 'finished' }, 42 | { name = 'reset', from = 'punctuation-sequence', to = 'started' }, 43 | 44 | { name = 'reset', from = 'finished', to = 'started' }, 45 | }, 46 | callbacks = { 47 | -- onstatechange = function(_, event, from, to, char) 48 | -- char = char or "" 49 | 50 | -- vimLogger.i( 51 | -- "Firing: " .. event .. " from: " .. from .. "to: " .. to .. 52 | -- " | for char: " .. char 53 | -- ) 54 | -- end 55 | } 56 | }) 57 | 58 | function BackWord.getRange(_, buffer) 59 | local start = buffer:getCaretPosition() 60 | 61 | local range = { 62 | start = start, 63 | finish = start, 64 | mode = 'exclusive', 65 | direction = 'characterwise' 66 | } 67 | 68 | local bufferLength = buffer:getLength() 69 | local contents = buffer:getValue() 70 | 71 | while range.start >= 0 do 72 | local charIndex = range.start + 1 -- lua strings are 1-indexed :( 73 | local char = utf8.sub(contents, charIndex, charIndex) 74 | 75 | if char == "\n" then parser:seenWhitespace(char) end 76 | if isPunctuation(char) then parser:seenPunctuation(char) end 77 | if isWhitespace(char) then parser:seenWhitespace(char) end 78 | if isPrintableChar(char) then parser:seenPrintable(char) end 79 | 80 | if parser.current == "finished" then 81 | range.start = range.start + 1 82 | break 83 | end 84 | 85 | if range.start == 0 then 86 | break 87 | else 88 | range.start = range.start - 1 89 | end 90 | end 91 | 92 | parser:reset() 93 | 94 | return range 95 | end 96 | 97 | function BackWord.getMovements() 98 | return { 99 | { 100 | modifiers = { ' alt' }, 101 | key = 'left', 102 | selection = true 103 | } 104 | } 105 | end 106 | 107 | return BackWord 108 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/backward_search.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 3 | 4 | local BackwardSearch = Motion:new{ name = 'backward_search' } 5 | 6 | function BackwardSearch:getRange(buffer) 7 | local finish = buffer:getCaretPosition() 8 | local stringFinish = finish + 1 9 | local searchChar = self:getExtraChar() 10 | 11 | local prevOccurringIndex = stringUtils.findPrevIndex( 12 | buffer:getValue(), 13 | searchChar, 14 | stringFinish - 1 -- start from the prev char 15 | ) 16 | 17 | if not prevOccurringIndex then return nil end 18 | 19 | return { 20 | start = prevOccurringIndex - 1, 21 | finish = finish, 22 | mode = 'exclusive', 23 | direction = 'characterwise' 24 | } 25 | end 26 | 27 | function BackwardSearch.getMovements() 28 | return nil 29 | end 30 | 31 | return BackwardSearch 32 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/between_chars.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local BackwardSearch = dofile(vimModeScriptPath .. "lib/motions/backward_search.lua") 4 | local ForwardSearch = dofile(vimModeScriptPath .. "lib/motions/forward_search.lua") 5 | 6 | local BetweenChars = Motion:new{ name = 'between_chars' } 7 | 8 | function BetweenChars:setSearchChars(beginningChar, endingChar) 9 | self.beginningChar = beginningChar 10 | self.endingChar = endingChar 11 | end 12 | 13 | function BetweenChars:getRange(buffer) 14 | if not self.beginningChar or not self.endingChar then 15 | error("Please setSearchChars(..., ...)") 16 | end 17 | 18 | local currentChar = buffer:currentChar() 19 | 20 | local start = nil 21 | 22 | if currentChar == self.beginningChar then 23 | start = buffer:getCaretPosition() 24 | end 25 | 26 | if not start then 27 | local backwardResult = BackwardSearch 28 | :new() 29 | :setExtraChar(self.beginningChar) 30 | :getRange(buffer) 31 | 32 | start = backwardResult and backwardResult.start 33 | end 34 | 35 | if not start then return nil end 36 | 37 | -- Find the finish position. 38 | local finish = nil 39 | 40 | if currentChar == self.endingChar then 41 | finish = buffer:getCaretPosition() 42 | end 43 | 44 | if not finish then 45 | local forwardResult = ForwardSearch 46 | :new() 47 | :setExtraChar(self.endingChar) 48 | :getRange(buffer) 49 | 50 | finish = forwardResult and forwardResult.finish 51 | end 52 | 53 | if not finish then return nil end 54 | 55 | return { 56 | start = start + 1, 57 | finish = finish - 1, 58 | mode = "inclusive", 59 | direction = "characterwise", 60 | } 61 | end 62 | 63 | function BetweenChars:getMovements() 64 | return nil 65 | end 66 | 67 | return BetweenChars 68 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/big_word.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 3 | local isWhitespace = stringUtils.isWhitespace 4 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 5 | 6 | local BigWord = Motion:new{ name = 'big_word' } 7 | 8 | -- or ** *W* 9 | -- W [count] WORDS forward. |exclusive| motion. 10 | -- 11 | -- 12 | -- bigword 13 | 14 | -- In the POSIX locale, vi shall recognize four kinds of bigwords: 15 | -- 1. A maximal sequence of non- characters preceded and followed by 16 | -- characters or the beginning or end of a line or the edit buffer 17 | 18 | -- 2. One or more sequential blank lines 19 | 20 | -- 3. The first character in the edit buffer 21 | 22 | -- 4. The last non- in the edit buffer 23 | 24 | function BigWord.getRange(_, buffer) 25 | local start = buffer:getCaretPosition() 26 | 27 | local range = { 28 | start = start, 29 | mode = 'exclusive', 30 | direction = 'characterwise' 31 | } 32 | 33 | local seenWhitespace = false 34 | local bufferLength = buffer:getLength() 35 | local contents = buffer:getValue() 36 | 37 | range.finish = start 38 | 39 | while range.finish < bufferLength do 40 | local charIndex = range.finish + 1 -- lua strings are 1-indexed :( 41 | local char = utf8.sub(contents, charIndex, charIndex) 42 | 43 | if seenWhitespace and not isWhitespace(char) then break end 44 | if not seenWhitespace and isWhitespace(char) then seenWhitespace = true end 45 | 46 | range.finish = range.finish + 1 47 | 48 | if char == "\n" then break end 49 | end 50 | 51 | if range.finish == bufferLength then 52 | -- don't go off the right edge of the buffer 53 | range.mode = 'inclusive' 54 | end 55 | 56 | return range 57 | end 58 | 59 | function BigWord.getMovements() 60 | return { 61 | { 62 | modifiers = { 'alt' }, 63 | key = 'right', 64 | selection = true 65 | } 66 | } 67 | end 68 | 69 | return BigWord 70 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/current_selection.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local CurrentSelection = Motion:new{ name = 'current_selection' } 4 | 5 | function CurrentSelection.getRange(_, buffer) 6 | local selection = buffer:getSelectionRange() 7 | 8 | return { 9 | start = selection.location, 10 | finish = selection:positionEnd(), 11 | mode = 'inclusive', 12 | direction = 'characterwise' 13 | } 14 | end 15 | 16 | function CurrentSelection.getMovements() 17 | return {} 18 | end 19 | 20 | return CurrentSelection 21 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/down.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local Down = Motion:new{ name = 'down' } 4 | 5 | function Down.getRange(_, buffer) 6 | if buffer:isOnLastLine() then return nil end 7 | 8 | local lineNum = buffer:getCurrentLineNumber() 9 | local column = buffer:getCurrentColumn() 10 | local finish = buffer:getPositionForLineAndColumn(lineNum + 1, column) 11 | 12 | return { 13 | start = buffer:getCaretPosition(), 14 | finish = finish, 15 | mode = 'exclusive', 16 | direction = 'linewise' 17 | } 18 | end 19 | 20 | function Down.getMovements() 21 | return { 22 | { 23 | modifiers = {}, 24 | key = 'down', 25 | selection = true 26 | } 27 | } 28 | end 29 | 30 | return Down 31 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/end_of_word.lua: -------------------------------------------------------------------------------- 1 | local machine = dofile(vimModeScriptPath .. 'lib/utils/statemachine.lua') 2 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 3 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 4 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 5 | 6 | local isPunctuation = stringUtils.isPunctuation 7 | local isWhitespace = stringUtils.isWhitespace 8 | local isPrintableChar = stringUtils.isPrintableChar 9 | 10 | local EndOfWord = Motion:new{ name = 'end_of_word' } 11 | 12 | local parser = machine.create({ 13 | initial = 'started', 14 | events = { 15 | { name = 'seenPrintable', from = 'started', to = 'first-printable' }, 16 | { name = 'seenPunctuation', from = 'started', to = 'first-punctuation' }, 17 | { name = 'seenWhitespace', from = 'started', to = 'ignore-whitespace' }, 18 | { name = 'seenNewLine', from = 'started', to = 'finished' }, 19 | 20 | { name = 'seenPrintable', from = 'first-printable', to = 'printable-sequence' }, 21 | { name = 'seenPunctuation', from = 'first-printable', to = 'punctuation-sequence' }, 22 | { name = 'seenWhitespace', from = 'first-printable', to = 'ignore-whitespace' }, 23 | { name = 'seenNewLine', from = 'first-printable', to = 'finished' }, 24 | { name = 'reset', from = 'first-printable', to = 'started' }, 25 | 26 | { name = 'seenPrintable', from = 'ignore-whitespace', to = 'first-printable' }, 27 | { name = 'seenPunctuation', from = 'ignore-whitespace', to = 'first-punctuation' }, 28 | { name = 'seenWhitespace', from = 'ignore-whitespace', to = 'ignore-whitespace' }, 29 | { name = 'seenNewLine', from = 'ignore-whitespace', to = 'finished' }, 30 | { name = 'reset', from = 'ignore-whitespace', to = 'started' }, 31 | 32 | { name = 'seenPrintable', from = 'first-punctuation', to = 'first-printable' }, 33 | { name = 'seenPunctuation', from = 'first-punctuation', to = 'punctuation-sequence' }, 34 | { name = 'seenWhitespace', from = 'first-punctuation', to = 'ignore-whitespace' }, 35 | { name = 'seenNewLine', from = 'first-punctuation', to = 'finished' }, 36 | { name = 'reset', from = 'first-punctuation', to = 'started' }, 37 | 38 | { name = 'seenPrintable', from = 'punctuation-sequence', to = 'finished' }, 39 | { name = 'seenPunctuation', from = 'punctuation-sequence', to = 'punctuation-sequence' }, 40 | { name = 'seenWhitespace', from = 'punctuation-sequence', to = 'finished' }, 41 | { name = 'seenNewLine', from = 'punctuation-sequence', to = 'finished' }, 42 | { name = 'reset', from = 'punctuation-sequence', to = 'started' }, 43 | 44 | { name = 'seenPrintable', from = 'printable-sequence', to = 'printable-sequence' }, 45 | { name = 'seenPunctuation', from = 'printable-sequence', to = 'finished' }, 46 | { name = 'seenWhitespace', from = 'printable-sequence', to = 'finished' }, 47 | { name = 'seenNewLine', from = 'printable-sequence', to = 'finished' }, 48 | { name = 'reset', from = 'printable-sequence', to = 'started' }, 49 | 50 | { name = 'reset', from = 'finished', to = 'started' }, 51 | }, 52 | callbacks = { 53 | -- onstatechange = function(_, event, from, to) 54 | -- vimLogger.i("Firing: " .. event .. " from: " .. from .. "to: " .. to) 55 | -- end 56 | } 57 | }) 58 | 59 | function EndOfWord.getRange(_, buffer) 60 | local start = buffer:getCaretPosition() 61 | 62 | local range = { 63 | start = start, 64 | finish = start, 65 | mode = 'inclusive', 66 | direction = 'characterwise' 67 | } 68 | 69 | local bufferLength = buffer:getLength() 70 | local contents = buffer:getValue() 71 | 72 | while range.finish < bufferLength do 73 | local charIndex = range.finish + 1 -- lua strings are 1-indexed :( 74 | local char = utf8.sub(contents, charIndex, charIndex) 75 | 76 | if char == "\n" then parser:seenNewLine(char) end 77 | if isPunctuation(char) then parser:seenPunctuation(char) end 78 | if isWhitespace(char) then parser:seenWhitespace(char) end 79 | if isPrintableChar(char) then parser:seenPrintable(char) end 80 | 81 | if parser.current == "finished" then 82 | range.finish = range.finish - 1 83 | break 84 | end 85 | 86 | range.finish = range.finish + 1 87 | end 88 | 89 | parser:reset() 90 | 91 | return range 92 | end 93 | 94 | function EndOfWord.getMovements() 95 | return { 96 | { 97 | modifiers = { 'alt' }, 98 | key = 'right', 99 | selection = true 100 | } 101 | } 102 | end 103 | 104 | return EndOfWord 105 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/entire_line.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local EntireLine = Motion:new{ name = 'entire_line' } 4 | 5 | function EntireLine.getRange(_, buffer) 6 | local lineRange = buffer:getCurrentLineRange() 7 | local start = lineRange.location 8 | 9 | if buffer:isOnLastLine() and buffer:charAt(start - 1) == "\n" then 10 | -- delete upwards from the last line and remove the trailing \n 11 | start = start - 1 12 | end 13 | 14 | return { 15 | start = math.max(start, 0), 16 | finish = lineRange:positionEnd(), 17 | mode = 'exclusive', 18 | direction = 'linewise' 19 | } 20 | end 21 | 22 | function EntireLine.getMovements() 23 | return { 24 | { 25 | modifiers = { 'cmd' }, 26 | key = 'left' 27 | }, 28 | { 29 | modifiers = { 'cmd' }, 30 | key = 'right', 31 | selection = true 32 | } 33 | } 34 | end 35 | 36 | return EntireLine 37 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/first_line.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local FirstLine = Motion:new{ name = 'first_line' } 4 | 5 | function FirstLine.getRange(_, buffer) 6 | local finish = buffer:getCurrentLineRange():positionEnd() 7 | 8 | return { 9 | start = 0, 10 | finish = finish, 11 | mode = 'exclusive', 12 | direction = 'linewise' 13 | } 14 | end 15 | 16 | function FirstLine.getMovements() 17 | return { 18 | { 19 | modifiers = {'cmd'}, 20 | key = 'up', 21 | selection = true 22 | }, 23 | { 24 | modifiers = {'ctrl'}, 25 | key = 'a', 26 | selection = true 27 | } 28 | } 29 | end 30 | 31 | return FirstLine 32 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/first_non_blank.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 3 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 4 | 5 | local FirstNonBlank = Motion:new{ name = 'first_non_blank' } 6 | 7 | function FirstNonBlank.getRange(_, buffer) 8 | local start = buffer:getCaretPosition() 9 | local bufferLength = buffer:getLength() 10 | local contents = buffer:getValue() 11 | 12 | local range = { 13 | start = start, 14 | mode = 'exclusive', 15 | direction = 'characterwise' 16 | } 17 | 18 | range.finish = start 19 | 20 | while range.finish < bufferLength do 21 | local charIndex = range.finish + 1 -- lua strings are 1-indexed :( 22 | local char = utf8.sub(contents, charIndex, charIndex) 23 | 24 | if char == "\n" then break end 25 | if not stringUtils.isWhitespace(char) then break end 26 | 27 | range.finish = range.finish + 1 28 | end 29 | 30 | return range 31 | end 32 | 33 | function FirstNonBlank.getMovements() 34 | return nil 35 | end 36 | 37 | return FirstNonBlank 38 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/forward_search.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 3 | 4 | local ForwardSearch = Motion:new{ name = 'forward_search' } 5 | 6 | function ForwardSearch:getRange(buffer) 7 | local start = buffer:getCaretPosition() 8 | local stringStart = start + 1 9 | local searchChar = self:getExtraChar() 10 | 11 | local nextOccurringIndex = stringUtils.findNextIndex( 12 | buffer:getValue(), 13 | searchChar, 14 | stringStart + 1 -- start from the next char 15 | ) 16 | 17 | if not nextOccurringIndex then return nil end 18 | 19 | return { 20 | start = start, 21 | finish = nextOccurringIndex - 1, 22 | mode = 'inclusive', 23 | direction = 'characterwise' 24 | } 25 | end 26 | 27 | function ForwardSearch.getMovements() 28 | return nil 29 | end 30 | 31 | return ForwardSearch 32 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/in_word.lua: -------------------------------------------------------------------------------- 1 | local BackWord = dofile(vimModeScriptPath .. "lib/motions/back_word.lua") 2 | local EndOfWord = dofile(vimModeScriptPath .. "lib/motions/end_of_word.lua") 3 | 4 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 5 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 6 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 7 | 8 | local InWord = Motion:new{ name = 'in_word' } 9 | 10 | function InWord:getRange(buffer) 11 | local start = buffer:getCaretPosition() 12 | local finish = start 13 | 14 | local atBeginning = stringUtils.isWordBoundary(buffer:prevChar()) 15 | 16 | if not atBeginning then 17 | local beginningOfWord = BackWord:new() 18 | local startRange = beginningOfWord:getRange(buffer) 19 | 20 | start = startRange.start 21 | end 22 | 23 | local atEnd = stringUtils.isWordBoundary(buffer:nextChar()) 24 | 25 | if not atEnd then 26 | local endOfWord = EndOfWord:new() 27 | local endRange = endOfWord:getRange(buffer) 28 | 29 | finish = endRange.finish 30 | end 31 | 32 | return { 33 | start = start, 34 | finish = finish, 35 | mode = 'inclusive', 36 | direction = 'characterwise', 37 | } 38 | end 39 | 40 | function InWord.getMovements() 41 | return nil 42 | end 43 | 44 | return InWord 45 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/last_line.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local LastLine = Motion:new{ name = 'last_line' } 4 | 5 | function LastLine.getRange(_, buffer) 6 | local range = buffer:getCurrentLineRange() 7 | local start = range.location 8 | 9 | return { 10 | start = start, 11 | finish = buffer:getLastIndex(), 12 | mode = 'exclusive', 13 | direction = 'linewise' 14 | } 15 | end 16 | 17 | function LastLine.getMovements() 18 | return { 19 | { 20 | modifiers = {'cmd'}, 21 | key = 'down', 22 | selection = true 23 | }, 24 | -- end of line 25 | { 26 | modifiers = {'ctrl'}, 27 | key = 'e', 28 | selection = true 29 | }, 30 | -- reset it to beginning of line 31 | { 32 | modifiers = {'ctrl'}, 33 | key = 'a', 34 | selection = true 35 | } 36 | } 37 | end 38 | 39 | return LastLine 40 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/left.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local Left = Motion:new{ name = 'left' } 4 | 5 | function Left.getRange(_, buffer) 6 | local start = buffer:getCaretPosition() 7 | 8 | return { 9 | start = start - 1, 10 | finish = start, 11 | mode = 'exclusive', 12 | direction = 'characterwise' 13 | } 14 | end 15 | 16 | function Left.getMovements() 17 | return { 18 | { 19 | modifiers = {}, 20 | key = 'left', 21 | selection = true 22 | } 23 | } 24 | end 25 | 26 | return Left 27 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/line_beginning.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local LineBeginning = Motion:new{ name = 'line_beginning' } 4 | 5 | function LineBeginning.getRange(_, buffer) 6 | local lineRange = buffer:getCurrentLineRange() 7 | 8 | return { 9 | start = lineRange.location, 10 | finish = buffer:getCaretPosition(), 11 | mode = 'exclusive', 12 | direction = 'characterwise' 13 | } 14 | end 15 | 16 | function LineBeginning.getMovements() 17 | return { 18 | { 19 | modifiers = { 'ctrl' }, 20 | key = 'a', 21 | selection = true 22 | } 23 | } 24 | end 25 | 26 | return LineBeginning 27 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/line_end.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 3 | 4 | local LineEnd = Motion:new{ name = 'line_end' } 5 | 6 | function LineEnd.getRange(_, buffer) 7 | local lineRange = buffer:getCurrentLineRange() 8 | local line = buffer:getCurrentLine() 9 | local finish = lineRange:positionEnd() 10 | 11 | if stringUtils.lastChar(line) == "\n" then 12 | finish = finish - 1 13 | end 14 | 15 | local range = { 16 | start = buffer:getCaretPosition(), 17 | finish = finish, 18 | -- the vim manual says this is an inclusive motion, but I swear 19 | -- it *behaves* like an exclusive motion, so I'm keeping it this way 20 | -- for now as it feels more correct. I might be missing some key things 21 | -- here though. 22 | mode = 'exclusive', 23 | direction = 'characterwise' 24 | } 25 | 26 | return range 27 | end 28 | 29 | function LineEnd.getMovements() 30 | return { 31 | { 32 | modifiers = { 'ctrl' }, 33 | key = 'e', 34 | selection = true 35 | } 36 | } 37 | end 38 | 39 | return LineEnd 40 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/noop.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local Noop = Motion:new{ name = 'noop' } 4 | 5 | function Noop.getRange(_, _) 6 | return nil 7 | end 8 | 9 | function Noop.getMovements() 10 | return nil 11 | end 12 | 13 | return Noop 14 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/right.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local Right = Motion:new{ name = 'right' } 4 | 5 | function Right.getRange(_, buffer) 6 | local start = buffer:getCaretPosition() 7 | 8 | return { 9 | start = start, 10 | finish = start + 1, 11 | mode = 'exclusive', 12 | direction = 'characterwise' 13 | } 14 | end 15 | 16 | function Right.getMovements() 17 | return { 18 | { 19 | modifiers = {}, 20 | key = 'right', 21 | selection = true 22 | } 23 | } 24 | end 25 | 26 | return Right 27 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/till_after_search.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | local BackwardSearch = dofile(vimModeScriptPath .. "lib/motions/backward_search.lua") 3 | 4 | local TillAfterSearch = Motion:new{ name = 'till_after_search' } 5 | 6 | function TillAfterSearch:getRange(buffer, ...) 7 | local motion = BackwardSearch:new():setExtraChar(self:getExtraChar()) 8 | local range = motion:getRange(buffer, ...) 9 | 10 | if not range then return nil end 11 | 12 | -- go right after the search result 13 | range.start = range.start + 1 14 | 15 | -- don't overflow off the start 16 | range.start = math.min(buffer:getLength() - 1, range.start) 17 | 18 | return range 19 | end 20 | 21 | function TillAfterSearch.getMovements() 22 | return nil 23 | end 24 | 25 | return TillAfterSearch 26 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/till_before_search.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | local ForwardSearch = dofile(vimModeScriptPath .. "lib/motions/forward_search.lua") 3 | 4 | local TillBeforeSearch = Motion:new{ name = 'till_before_search' } 5 | 6 | function TillBeforeSearch:getRange(buffer, ...) 7 | local motion = ForwardSearch:new():setExtraChar(self:getExtraChar()) 8 | local range = motion:getRange(buffer, ...) 9 | 10 | if not range then return nil end 11 | 12 | -- go right before the search result 13 | range.finish = range.finish - 1 14 | 15 | -- don't overflow 16 | range.finish = math.max(0, range.finish) 17 | 18 | return range 19 | end 20 | 21 | function TillBeforeSearch.getMovements() 22 | return nil 23 | end 24 | 25 | return TillBeforeSearch 26 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/up.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | 3 | local Up = Motion:new{ name = 'up' } 4 | 5 | function Up.getRange(_, buffer) 6 | local lineNum = buffer:getCurrentLineNumber() 7 | if lineNum == 1 then return nil end 8 | 9 | local column = buffer:getCurrentColumn() 10 | local start = buffer:getPositionForLineAndColumn(lineNum - 1, column) 11 | 12 | return { 13 | start = start, 14 | finish = buffer:getCaretPosition(), 15 | mode = 'exclusive', 16 | direction = 'linewise' 17 | } 18 | end 19 | 20 | function Up.getMovements() 21 | return { 22 | { 23 | modifiers = {}, 24 | key = 'up', 25 | selection = true 26 | } 27 | } 28 | end 29 | 30 | return Up 31 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/motions/word.lua: -------------------------------------------------------------------------------- 1 | local Motion = dofile(vimModeScriptPath .. "lib/motion.lua") 2 | local EndOfWord = dofile(vimModeScriptPath .. "lib/motions/end_of_word.lua") 3 | local stringUtils = dofile(vimModeScriptPath .. "lib/utils/string_utils.lua") 4 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 5 | 6 | local Word = Motion:new{ name = 'word' } 7 | 8 | local isPunctuation = stringUtils.isPunctuation 9 | local isWhitespace = stringUtils.isWhitespace 10 | local isPrintableChar = stringUtils.isPrintableChar 11 | 12 | -- word motion, exclusive 13 | -- 14 | -- from :help motions.txt 15 | -- 16 | -- or ** *w* 17 | -- w [count] words forward. |exclusive| motion. 18 | -- 19 | -- 20 | -- https://pubs.opengroup.org/onlinepubs/9699919799/utilities/vi.html 21 | -- 22 | 23 | -- TODO handle more edge cases for :help word 24 | function Word.getRange(_, buffer, operator) 25 | local start = buffer:getCaretPosition() 26 | 27 | local range = { 28 | start = start, 29 | mode = 'exclusive', 30 | direction = 'characterwise' 31 | } 32 | 33 | range.finish = start 34 | 35 | local seenWhitespace = false 36 | local bufferLength = buffer:getLength() 37 | local contents = buffer:getValue() 38 | 39 | local startingChar = utf8.sub( 40 | contents, 41 | range.finish + 1, 42 | range.finish + 1 43 | ) 44 | 45 | -- From :h word 46 | -- 47 | -- Special case: "cw" and "cW" are treated like "ce" and "cE" if the 48 | -- cursor is on a non-blank. This is because "cw" is interpreted as 49 | -- change-word, and a word does not include the following white space. 50 | if not isWhitespace(startingChar) and operator and operator.name == 'change' then 51 | return EndOfWord:new():getRange(buffer, operator) 52 | end 53 | 54 | local startedOnPunctuation = isPunctuation(startingChar) 55 | 56 | while range.finish < bufferLength do 57 | local charIndex = range.finish + 1 -- lua strings are 1-indexed :( 58 | local char = utf8.sub(contents, charIndex, charIndex) 59 | 60 | if char == "\n" then 61 | if start == range.finish then range.finish = range.finish + 1 end 62 | 63 | break 64 | end 65 | 66 | if startedOnPunctuation then 67 | if isPrintableChar(char) then break end 68 | else 69 | if seenWhitespace and not isWhitespace(char) then break end 70 | if isPunctuation(char) then break end 71 | 72 | if not seenWhitespace and isWhitespace(char) then 73 | seenWhitespace = true 74 | end 75 | end 76 | 77 | range.finish = range.finish + 1 78 | end 79 | 80 | if range.finish == bufferLength then 81 | -- don't go off the right edge of the buffer 82 | range.mode = 'inclusive' 83 | end 84 | 85 | return range 86 | end 87 | 88 | function Word.getMovements() 89 | return { 90 | { 91 | modifiers = { 'alt' }, 92 | key = 'right', 93 | selection = true 94 | } 95 | } 96 | end 97 | 98 | return Word 99 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/operator.lua: -------------------------------------------------------------------------------- 1 | local Operator = {} 2 | 3 | function Operator:new(fields) 4 | local operator = fields or {} 5 | 6 | operator.extraChar = nil 7 | 8 | setmetatable(operator, self) 9 | self.__index = self 10 | 11 | return operator 12 | end 13 | 14 | function Operator.getModeForTransition() 15 | return "normal" 16 | end 17 | 18 | function Operator:setExtraChar(char) 19 | self.extraChar = char 20 | 21 | return self 22 | end 23 | 24 | function Operator:getExtraChar() 25 | return self.extraChar 26 | end 27 | 28 | function Operator.getKeys() 29 | error("Please implement getKeys()") 30 | end 31 | 32 | return Operator 33 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/operators/change.lua: -------------------------------------------------------------------------------- 1 | local Delete = dofile(vimModeScriptPath .. "lib/operators/delete.lua") 2 | 3 | local Change = Delete:new{ name = 'change' } 4 | 5 | -- Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is 6 | -- on a non-blank. This is Vi-compatible, see |cpo-_| to change the behavior. 7 | function Change.getModeForTransition() 8 | return "insert" 9 | end 10 | 11 | return Change 12 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/operators/delete.lua: -------------------------------------------------------------------------------- 1 | local Operator = dofile(vimModeScriptPath .. "lib/operator.lua") 2 | local Delete = Operator:new{name = 'delete'} 3 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 4 | 5 | function Delete.getModifiedBuffer(_, buffer, rangeStart, rangeFinish) 6 | local value = buffer:getValue() 7 | local length = rangeFinish - rangeStart + 1 8 | 9 | local contents = "" 10 | local stringStart, stringFinish = rangeStart + 1, rangeFinish + 1 11 | 12 | if stringStart > 1 then 13 | contents = utf8.sub(value, 1, stringStart - 1) 14 | end 15 | 16 | contents = contents .. utf8.sub(value, stringFinish + 1, -1) 17 | 18 | return buffer:createNew(contents, rangeStart, 0) 19 | end 20 | 21 | function Delete.modifySelection() 22 | hs.eventtap.keyStroke({}, 'delete', 0) 23 | end 24 | 25 | function Delete.getKeys() 26 | return { 27 | { 28 | modifiers = {}, 29 | key = 'delete' 30 | } 31 | } 32 | end 33 | 34 | return Delete 35 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/operators/replace.lua: -------------------------------------------------------------------------------- 1 | local Operator = dofile(vimModeScriptPath .. "lib/operator.lua") 2 | local times = dofile(vimModeScriptPath .. "lib/utils/times.lua") 3 | local Replace = Operator:new{name = 'replace'} 4 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 5 | 6 | function Replace:modifySelection(_, rangeStart, rangeFinish) 7 | local numChars = rangeFinish - rangeStart + 1 8 | local replaceChar = self:getExtraChar() 9 | local replacement = "" 10 | 11 | times(numChars, function() 12 | replacement = replacement .. replaceChar 13 | end) 14 | 15 | hs.eventtap.keyStroke({}, 'delete', 50) 16 | hs.eventtap.keyStrokes(replacement) 17 | 18 | times(numChars, function() 19 | hs.eventtap.keyStroke({}, 'left', 0) 20 | end) 21 | end 22 | 23 | function Replace:getModifiedBuffer(buffer, rangeStart, rangeFinish) 24 | local value = buffer:getValue() 25 | local replaceChar = self:getExtraChar() 26 | 27 | local length = rangeFinish - rangeStart + 1 28 | 29 | local contents = "" 30 | local stringStart, stringFinish = rangeStart + 1, rangeFinish + 1 31 | 32 | if stringStart > 1 then 33 | contents = utf8.sub(value, 1, stringStart - 1) 34 | end 35 | 36 | local numChars = rangeFinish - rangeStart + 1 37 | 38 | times(numChars, function() 39 | contents = contents .. replaceChar 40 | end) 41 | 42 | contents = contents .. utf8.sub(value, stringFinish + 1, -1) 43 | 44 | return buffer:createNew(contents, rangeStart, 0) 45 | end 46 | 47 | function Replace:getKeys() 48 | -- TODO support in bootleg mode 49 | return nil 50 | end 51 | 52 | return Replace 53 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/operators/yank.lua: -------------------------------------------------------------------------------- 1 | local Operator = dofile(vimModeScriptPath .. "lib/operator.lua") 2 | local Yank = Operator:new{name = 'yank'} 3 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 4 | 5 | function Yank:modifySelection(buffer, rangeStart, rangeFinish) 6 | if hs then 7 | local stringStart, stringFinish = rangeStart + 1, rangeFinish + 1 8 | local toCopy = utf8.sub(buffer:getValue(), stringStart, stringFinish) 9 | 10 | hs.pasteboard.setContents(toCopy) 11 | end 12 | end 13 | 14 | function Yank.getModifiedBuffer(_, buffer, rangeStart, rangeFinish) 15 | -- we just want to set it in the pasteboard 16 | if hs then 17 | local stringStart, stringFinish = rangeStart + 1, rangeFinish + 1 18 | local toCopy = utf8.sub(buffer:getValue(), stringStart, stringFinish) 19 | 20 | hs.pasteboard.setContents(toCopy) 21 | end 22 | 23 | -- we actually don't need to modify the buffer 24 | return buffer 25 | end 26 | 27 | function Yank.getKeys() 28 | return { 29 | { 30 | modifiers = {'cmd'}, 31 | key = 'c' 32 | } 33 | } 34 | end 35 | 36 | return Yank 37 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/screen_dimmer.lua: -------------------------------------------------------------------------------- 1 | -- Dims the screen a la Flux to indicate we've shifted modes 2 | 3 | local ScreenDimmer = {} 4 | 5 | function ScreenDimmer.dimScreen() 6 | -- Stole these shifts from flux-like plugin 7 | -- https://github.com/calvinwyoung/.dotfiles/blob/master/darwin/hammerspoon/flux.lua 8 | local whiteShift = { 9 | alpha = 1.0, 10 | red = 1.0, 11 | green = 0.95201559, 12 | blue = 0.90658983, 13 | } 14 | 15 | local blackShift = { 16 | alpha = 1.0, 17 | red = 0, 18 | green = 0, 19 | blue = 0, 20 | } 21 | 22 | hs.screen.primaryScreen():setGamma(whiteShift, blackShift) 23 | end 24 | 25 | function ScreenDimmer.restoreScreen() 26 | hs.screen.restoreGamma() 27 | end 28 | 29 | return ScreenDimmer 30 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/selection.lua: -------------------------------------------------------------------------------- 1 | local Selection = {} 2 | 3 | function Selection:new(location, length) 4 | local selection = { 5 | location = location, 6 | length = length 7 | } 8 | 9 | setmetatable(selection, self) 10 | self.__index = self 11 | 12 | return selection 13 | end 14 | 15 | function Selection.fromRange(axRange) 16 | -- Older versions of axuielement return `loc/len` 17 | local location = axRange.location or axRange.loc 18 | local length = axRange.length or axRange.len 19 | 20 | return Selection:new(location, length) 21 | end 22 | 23 | function Selection:isSelected() 24 | return self.length > 0 25 | end 26 | 27 | function Selection:positionEnd() 28 | return self.location + self.length 29 | end 30 | 31 | function Selection:getCharRange() 32 | return { 33 | start = self.location, 34 | finish = self:positionEnd() 35 | } 36 | end 37 | 38 | return Selection 39 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/state.lua: -------------------------------------------------------------------------------- 1 | local machine = dofile(vimModeScriptPath .. 'lib/utils/statemachine.lua') 2 | 3 | local function createStateMachine(vim) 4 | return machine.create({ 5 | initial = 'insert-mode', 6 | events = { 7 | { name = 'enterNormal', from = 'insert-mode', to = 'normal-mode' }, 8 | { name = 'enterNormal', from = 'visual-mode', to = 'normal-mode' }, 9 | { name = 'enterNormal', from = 'firing', to = 'normal-mode' }, 10 | { name = 'enterNormal', from = 'operator-pending', to = 'normal-mode' }, 11 | { name = 'enterNormal', from = 'normal-mode', to = 'normal-mode' }, 12 | 13 | { name = 'enterMotion', from = 'normal-mode', to = 'entered-motion' }, 14 | { name = 'enterMotion', from = 'operator-pending', to = 'entered-motion' }, 15 | { name = 'enterMotion', from = 'visual-mode', to = 'entered-motion' }, 16 | 17 | { name = 'enterOperator', from = 'normal-mode', to = 'operator-pending' }, 18 | { name = 'enterOperator', from = 'visual-mode', to = 'operator-pending' }, 19 | 20 | { name = 'enterVisual', from = 'normal-mode', to = 'visual-mode' }, 21 | { name = 'enterVisual', from = 'firing', to = 'visual-mode' }, 22 | 23 | { name = 'fire', from = 'entered-motion', to = 'firing' }, 24 | { name = 'fire', from = 'visual-mode', to = 'firing' }, 25 | 26 | { name = 'enterInsert', from = 'firing', to = 'insert-mode' }, 27 | { name = 'enterInsert', from = 'normal-mode', to = 'insert-mode' }, 28 | { name = 'enterInsert', from = 'operator-pending', to = 'insert-mode' }, 29 | { name = 'enterInsert', from = 'visual-mode', to = 'insert-mode' }, 30 | }, 31 | callbacks = { 32 | onenterNormal = function() 33 | vim:enableBlockCursor() 34 | vim:disableSequence() 35 | vim:resetCommandState() 36 | vim:setNormalMode() 37 | vim:enterModal('normal') 38 | end, 39 | onenterInsert = function() 40 | vim.visualCaretPosition = nil 41 | vim:disableBlockCursor() 42 | vim:exitAllModals() 43 | vim:setInsertMode() 44 | vim:resetCommandState() 45 | vim:enableSequence() 46 | end, 47 | onenterVisual = function() 48 | vim:setVisualMode() 49 | vim:enterModal('visual') 50 | end, 51 | onenterOperator = function(_, _, _, _, operator) 52 | vim:enterModal('operatorPending') 53 | vim.commandState.operator = operator 54 | end, 55 | onenterMotion = function(self, _, _, _, motion) 56 | vim.commandState.motion = motion 57 | self:fire() 58 | end, 59 | onfire = function(self) 60 | local result = vim:fireCommandState() 61 | 62 | if result.mode == "visual" then 63 | if result.hadOperator then 64 | self:enterNormal() 65 | else 66 | self:enterVisual() 67 | end 68 | else 69 | if result.transition == "normal" then self:enterNormal() 70 | else vim:exitAsync() end 71 | end 72 | end, 73 | onstatechange = function() 74 | vim:updateStateIndicator() 75 | end 76 | } 77 | }) 78 | end 79 | 80 | return createStateMachine 81 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/strategies/keyboard_strategy.lua: -------------------------------------------------------------------------------- 1 | local Strategy = dofile(vimModeScriptPath .. "lib/strategy.lua") 2 | 3 | local KeyboardStrategy = Strategy:new() 4 | 5 | function KeyboardStrategy:new(vim) 6 | local strategy = { 7 | vim = vim 8 | } 9 | 10 | setmetatable(strategy, self) 11 | self.__index = self 12 | 13 | return strategy 14 | end 15 | 16 | function KeyboardStrategy:fire() 17 | local result = self:fireMovement() 18 | 19 | -- If the movement is canceled or impossible with the KB strategy, don't do 20 | -- the operator. 21 | if result then self:fireOperator() end 22 | end 23 | 24 | function KeyboardStrategy:fireMovement() 25 | -- select the movement 26 | local motion = self.vim.commandState.motion 27 | local operator = self.vim.commandState.operator 28 | local visualMode = self.vim:isMode('visual') 29 | 30 | if not motion then return true end 31 | 32 | local movements = motion.getMovements() 33 | if not movements then return false end 34 | 35 | for _, movement in ipairs(movements) do 36 | local modifiers = movement.modifiers 37 | 38 | local isSelection = visualMode or (operator and movement.selection) 39 | 40 | if isSelection then 41 | modifiers = { "shift", table.unpack(modifiers) } 42 | end 43 | 44 | hs.eventtap.keyStroke(modifiers, movement.key, 0) 45 | end 46 | 47 | return true 48 | end 49 | 50 | function KeyboardStrategy:fireOperator() 51 | local operator = self.vim.commandState.operator 52 | 53 | if operator then 54 | -- fire the operator 55 | for _, movement in pairs(operator:getKeys()) do 56 | hs.eventtap.keyStroke(movement.modifiers, movement.key, 0) 57 | end 58 | end 59 | end 60 | 61 | return KeyboardStrategy 62 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/strategy.lua: -------------------------------------------------------------------------------- 1 | local Strategy = {} 2 | 3 | function Strategy:new(vim) 4 | local strategy = { 5 | vim = vim, 6 | } 7 | 8 | setmetatable(strategy, self) 9 | self.__index = self 10 | 11 | return strategy 12 | end 13 | 14 | function Strategy.fire(_) 15 | error("Implement fire()") 16 | end 17 | 18 | function Strategy.isValid(_) 19 | return true 20 | end 21 | 22 | 23 | return Strategy 24 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/ax.lua: -------------------------------------------------------------------------------- 1 | local ax = dofile(vimModeScriptPath .. "lib/axuielement.lua") 2 | 3 | local axUtils = {} 4 | 5 | axUtils.isTextField = function(element) 6 | if not element then return false end 7 | 8 | local role = element:attributeValue("AXRole") 9 | 10 | return role == "AXTextField" or role == "AXTextArea" or role == "AXComboBox" 11 | end 12 | 13 | axUtils.isRichTextField = function(element) 14 | -- If the current element has any children typically it means there 15 | -- are fancy-ass things in the input element like images, complex HTML, 16 | -- etc. 17 | -- 18 | -- from observation, plain text inputs/textareas do not seem to have 19 | -- children. 20 | if not element then return false end 21 | 22 | local children = element:attributeValue("AXChildren") 23 | 24 | if not children then return false end 25 | 26 | return #children > 0 27 | end 28 | 29 | ------------------------------------------------- 30 | -- patching Accessibility APIs on a per-app basis 31 | ------------------------------------------------- 32 | local function patchChromiumWithAccessibilityFlag(axApp) 33 | -- Google Chrome needs this flag to turn on accessibility in the browser 34 | axApp:setAttributeValue('AXEnhancedUserInterface', true) 35 | end 36 | 37 | local function patchElectronAppsWithAccessibilityFlag(axApp) 38 | -- Electron apps require this attribute to be set or else you cannot 39 | -- read the accessibility tree 40 | axApp:setAttributeValue('AXManualAccessibility', true) 41 | end 42 | 43 | local alreadyPatchedApps = {} 44 | 45 | axUtils.patchCurrentApplication = function() 46 | local currentApp = hs.application.frontmostApplication() 47 | 48 | -- cache whether we patched it already by app name and pid 49 | -- pray for no collisions hahahahahhahaha 50 | local patchKey = currentApp:name() .. currentApp:pid() 51 | if alreadyPatchedApps[patchKey] then return end 52 | 53 | alreadyPatchedApps[patchKey] = true 54 | local axApp = ax.applicationElement(currentApp) 55 | 56 | if axApp then 57 | patchChromiumWithAccessibilityFlag(axApp) 58 | patchElectronAppsWithAccessibilityFlag(axApp) 59 | end 60 | end 61 | 62 | return axUtils 63 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/benchmark.lua: -------------------------------------------------------------------------------- 1 | function vimBenchmark(name, fn) 2 | local start = hs.timer.absoluteTime() 3 | result = fn() 4 | local finish = hs.timer.absoluteTime() 5 | 6 | time = (finish - start) / 100000 7 | 8 | vimLogger.i(name .. " took " .. time .. "ms") 9 | 10 | return result 11 | end 12 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/browser.lua: -------------------------------------------------------------------------------- 1 | local browserUtils = {} 2 | 3 | browserUtils.isFrontmostChrome = function() 4 | local name = hs.application.frontmostApplication():name() 5 | 6 | return name == "Google Chrome" or name == "Chromium" 7 | end 8 | 9 | browserUtils.isFrontmostSafari = function() 10 | local name = hs.application.frontmostApplication():name() 11 | 12 | return name == "Safari" 13 | end 14 | 15 | browserUtils.frontmostCurrentUrl = function() 16 | if browserUtils.isFrontmostChrome() then 17 | result, url = hs.osascript.applescript( 18 | 'tell application "Google Chrome" to return URL of active tab of front window' 19 | ) 20 | 21 | if result and url then 22 | return url 23 | end 24 | elseif browserUtils.isFrontmostSafari() then 25 | result, url = hs.osascript.applescript( 26 | 'tell application "Safari" to return URL of current tab of window 1' 27 | ) 28 | 29 | if result and url then 30 | return url 31 | end 32 | end 33 | 34 | return nil 35 | end 36 | 37 | return browserUtils 38 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/find_first.lua: -------------------------------------------------------------------------------- 1 | local function findFirst(list, fn) 2 | for _, item in ipairs(list) do 3 | if fn(item) then return item end 4 | end 5 | 6 | return nil 7 | end 8 | 9 | return findFirst 10 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/keys.lua: -------------------------------------------------------------------------------- 1 | local fnutils = require("hs.fnutils") 2 | local keyUtils = {} 3 | 4 | local shiftMaps = { 5 | ["1"] = "!", 6 | ["2"] = "@", 7 | ["3"] = "#", 8 | ["4"] = "$", 9 | ["5"] = "%", 10 | ["6"] = "^", 11 | ["7"] = "&", 12 | ["8"] = "*", 13 | ["9"] = "(", 14 | ["0"] = ")", 15 | a = "A", 16 | b = "B", 17 | c = "C", 18 | d = "D", 19 | e = "E", 20 | f = "F", 21 | g = "G", 22 | h = "H", 23 | i = "I", 24 | j = "J", 25 | k = "K", 26 | l = "L", 27 | m = "M", 28 | n = "N", 29 | o = "O", 30 | p = "P", 31 | q = "Q", 32 | r = "R", 33 | s = "S", 34 | t = "T", 35 | u = "U", 36 | v = "V", 37 | w = "W", 38 | x = "X", 39 | y = "Y", 40 | z = "Z", 41 | } 42 | 43 | -- Taken from https://wincent.com/wiki/Unicode_representations_of_modifier_keys 44 | local normalMaps = { 45 | escape = "⎋", 46 | ["return"] = "⏎", 47 | left = "←", 48 | right = "→", 49 | up = "⇡", 50 | down = "↓", 51 | cmd = "⌘", 52 | alt = "⌥", 53 | ctrl = "⌃", 54 | shift = "⇧", 55 | } 56 | 57 | -- Given a table of mods and a key pressed, convert it to a readable version 58 | -- 59 | -- Examples: 60 | -- 61 | -- getRealChar({'shift'}, '4') => "$" 62 | -- getRealChar({'shift'}, 'h') => "H" 63 | -- getRealChar({}, 'h') => "h" 64 | keyUtils.getRealChar = function(mods, key) 65 | local hasShift = fnutils.contains(mods, 'shift') 66 | 67 | if hasShift then 68 | return shiftMaps[key] or key 69 | else 70 | return normalMaps[key] or key 71 | end 72 | end 73 | 74 | return keyUtils 75 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/log.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- log.lua 3 | -- 4 | -- Copyright (c) 2016 rxi 5 | -- 6 | -- This library is free software; you can redistribute it and/or modify it 7 | -- under the terms of the MIT license. See LICENSE for details. 8 | -- 9 | 10 | local log = { _version = "0.1.0" } 11 | 12 | log.usecolor = true 13 | log.outfile = nil 14 | log.level = "trace" 15 | 16 | 17 | local modes = { 18 | { name = "trace", color = "\27[34m", }, 19 | { name = "debug", color = "\27[36m", }, 20 | { name = "info", color = "\27[32m", }, 21 | { name = "warn", color = "\27[33m", }, 22 | { name = "error", color = "\27[31m", }, 23 | { name = "fatal", color = "\27[35m", }, 24 | } 25 | 26 | 27 | local levels = {} 28 | for i, v in ipairs(modes) do 29 | levels[v.name] = i 30 | end 31 | 32 | 33 | local round = function(x, increment) 34 | increment = increment or 1 35 | x = x / increment 36 | return (x > 0 and math.floor(x + .5) or math.ceil(x - .5)) * increment 37 | end 38 | 39 | 40 | local _tostring = tostring 41 | 42 | local tostring = function(...) 43 | local t = {} 44 | for i = 1, select('#', ...) do 45 | local x = select(i, ...) 46 | if type(x) == "number" then 47 | x = round(x, .01) 48 | end 49 | t[#t + 1] = _tostring(x) 50 | end 51 | return table.concat(t, " ") 52 | end 53 | 54 | 55 | for i, x in ipairs(modes) do 56 | local nameupper = x.name:upper() 57 | log[x.name] = function(...) 58 | 59 | -- Return early if we're below the log level 60 | if i < levels[log.level] then 61 | return 62 | end 63 | 64 | local msg = tostring(...) 65 | local info = debug.getinfo(2, "Sl") 66 | local lineinfo = info.short_src .. ":" .. info.currentline 67 | 68 | -- Output to console 69 | print(string.format("%s[%-6s%s]%s %s: %s", 70 | log.usecolor and x.color or "", 71 | nameupper, 72 | os.date("%H:%M:%S"), 73 | log.usecolor and "\27[0m" or "", 74 | lineinfo, 75 | msg)) 76 | 77 | -- Output to log file 78 | if log.outfile then 79 | local fp = io.open(log.outfile, "a") 80 | local str = string.format("[%-6s%s] %s: %s\n", 81 | nameupper, os.date(), lineinfo, msg) 82 | fp:write(str) 83 | fp:close() 84 | end 85 | 86 | end 87 | end 88 | 89 | 90 | return log 91 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/number_utils.lua: -------------------------------------------------------------------------------- 1 | local numberUtils = {} 2 | 3 | function numberUtils.pushDigit(number, digit) 4 | number = number or 0 5 | if not digit then return number end 6 | 7 | return number * 10 + digit 8 | end 9 | 10 | return numberUtils 11 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/prequire.lua: -------------------------------------------------------------------------------- 1 | local function prequire(...) 2 | local status, lib = pcall(require, ...) 3 | if status then return lib end 4 | 5 | -- Library failed to load, so perhaps return `nil` or something? 6 | return nil 7 | end 8 | 9 | return prequire 10 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/set.lua: -------------------------------------------------------------------------------- 1 | local function Set(list) 2 | local set = {} 3 | for _, l in ipairs(list) do set[l] = true end 4 | 5 | return set 6 | end 7 | 8 | return Set 9 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/string_utils.lua: -------------------------------------------------------------------------------- 1 | local Set = dofile(vimModeScriptPath .. "lib/utils/set.lua") 2 | local utf8 = dofile(vimModeScriptPath .. "vendor/luautf8.lua") 3 | 4 | local stringUtils = {} 5 | 6 | local punctuation = Set{ 7 | "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "=", "+", "[", "{", 8 | "}", "]", "|", " '", "\"", ":", ";", ",", ".", "/", "?", "`" 9 | } 10 | 11 | function stringUtils.isPunctuation(char) 12 | return not not punctuation[char] 13 | end 14 | 15 | function stringUtils.isWhitespace(char) 16 | return char == " " 17 | end 18 | 19 | function stringUtils.isNonAlphanumeric(str) 20 | return not not utf8.match(str, "%W") 21 | end 22 | 23 | function stringUtils.isPrintableChar(char) 24 | return not stringUtils.isPunctuation(char) and 25 | not stringUtils.isWhitespace(char) 26 | end 27 | 28 | function stringUtils.isWordBoundary(char) 29 | if char == nil then return true end 30 | 31 | return stringUtils.isNonAlphanumeric(char) 32 | end 33 | 34 | function stringUtils.toChars(str) 35 | local chars = {} 36 | local current = 1 37 | 38 | while current <= #str do 39 | table.insert(chars, utf8.sub(str, current, current)) 40 | current = current + 1 41 | end 42 | 43 | return chars 44 | end 45 | 46 | function stringUtils.findPrevIndex(str, searchChar, startPos) 47 | local length = utf8.len(str) 48 | local position = math.min(startPos or length, length) 49 | 50 | while position > 0 do 51 | if utf8.sub(str, position, position) == searchChar then 52 | return position 53 | end 54 | 55 | position = position - 1 56 | end 57 | 58 | return nil 59 | end 60 | 61 | function stringUtils.findNextIndex(str, searchChar, startPos) 62 | local length = utf8.len(str) 63 | local position = math.max(startPos or 1, 1) 64 | 65 | while position <= length do 66 | if utf8.sub(str, position, position) == searchChar then 67 | return position 68 | end 69 | 70 | position = position + 1 71 | end 72 | 73 | return nil 74 | end 75 | 76 | function stringUtils.split(delimiter, text, includeDelimiter) 77 | local includeDelimiter = includeDelimiter or false 78 | local list = {} 79 | local pos = 1 80 | 81 | if utf8.find("", delimiter, 1) then -- this would result in endless loops 82 | error("delimiter matches empty string!") 83 | end 84 | 85 | while 1 do 86 | local first, last = utf8.find(text, delimiter, pos) 87 | 88 | if first then -- found? 89 | local part = utf8.sub(text, pos, first - 1) 90 | if includeDelimiter then part = part .. delimiter end 91 | 92 | table.insert(list, part) 93 | pos = last + 1 94 | else 95 | table.insert(list, utf8.sub(text, pos)) 96 | break 97 | end 98 | end 99 | 100 | return list 101 | end 102 | 103 | function stringUtils.lastChar(text) 104 | return utf8.sub(text, #text, #text) 105 | end 106 | 107 | return stringUtils 108 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/table.lua: -------------------------------------------------------------------------------- 1 | local Set = dofile(vimModeScriptPath .. 'lib/utils/set.lua') 2 | 3 | local tableUtils = {} 4 | 5 | tableUtils.matches = function(table1, table2) 6 | if #table1 ~= #table2 then return false end 7 | 8 | local set = Set(table1) 9 | 10 | for _, value in pairs(table2) do 11 | if not set[value] then return false end 12 | end 13 | 14 | return true 15 | end 16 | 17 | return tableUtils 18 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/times.lua: -------------------------------------------------------------------------------- 1 | local function times(n, fn) 2 | local i = 0 3 | 4 | while i < n do 5 | fn() 6 | i = i + 1 7 | end 8 | end 9 | 10 | return times 11 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/version.lua: -------------------------------------------------------------------------------- 1 | local fnutils = require("hs.fnutils") 2 | 3 | local versionUtils = {} 4 | 5 | function versionUtils.hammerspoonVersionLessThan(compareVersion) 6 | local compare = fnutils.split(compareVersion, ".", nil, true) 7 | local current = fnutils.split(hs.processInfo.version, ".", nil, true) 8 | 9 | local maxLength = math.max(#compare, #current) 10 | 11 | for i = 1, maxLength do 12 | local compareVal = tonumber(compare[i]) or 0 13 | local currentVal = tonumber(current[i]) or 0 14 | 15 | if currentVal < compareVal then 16 | return true 17 | elseif currentVal > compareVal then 18 | return false 19 | end 20 | end 21 | 22 | return false 23 | end 24 | 25 | return versionUtils 26 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/visible_range.lua: -------------------------------------------------------------------------------- 1 | local VisibleRange = {} 2 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/utils/visual.lua: -------------------------------------------------------------------------------- 1 | local visualUtils = {} 2 | 3 | local function isRangeEqual(range1, range2) 4 | return range1.start == range2.start and range1.finish == range2.finish 5 | end 6 | 7 | -- Given a `currentRange` selected, and a new `motionRange` to add to the 8 | -- selection, and a `caretPosition` tracked separate from the selection, 9 | -- calculate the new caret position and a new range that merges the 2 10 | -- together 11 | visualUtils.getNewRange = function (currentRange, motionRange, caretPosition) 12 | local noSelection = currentRange.start == currentRange.finish 13 | 14 | local caretOn = (caretPosition < currentRange.finish and "left") or "right" 15 | local motionDirection = "right" 16 | 17 | if currentRange.finish == motionRange.finish or 18 | currentRange.start == motionRange.finish then 19 | motionDirection = "left" 20 | end 21 | 22 | if isRangeEqual(currentRange, motionRange) then 23 | local newPosition = motionRange.finish 24 | 25 | if caretPosition == motionRange.finish then 26 | newPosition = motionRange.start 27 | end 28 | 29 | return { 30 | range = { start = newPosition, finish = newPosition }, 31 | caretPosition = newPosition 32 | } 33 | end 34 | 35 | if noSelection then 36 | return { 37 | caretPosition = 38 | (motionDirection == "left" and motionRange.start) or motionRange.finish, 39 | range = motionRange 40 | } 41 | end 42 | 43 | local newRange = { 44 | start = currentRange.start, 45 | finish = currentRange.finish, 46 | } 47 | 48 | local key = (caretOn == "left" and "start") or "finish" 49 | local newValue = 50 | (motionDirection == "left" and motionRange.start) or motionRange.finish 51 | 52 | newRange[key] = newValue 53 | 54 | return { 55 | caretPosition = newRange[key], 56 | range = newRange 57 | } 58 | end 59 | 60 | return visualUtils 61 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/lib/wait_for_char.lua: -------------------------------------------------------------------------------- 1 | local WaitForChar = {} 2 | 3 | function WaitForChar:new(options) 4 | options = options or {} 5 | 6 | local waiter = { 7 | onCancel = options.onCancel or function() end, 8 | onChar = options.onChar or function() end, 9 | tap = nil 10 | } 11 | 12 | setmetatable(waiter, self) 13 | self.__index = self 14 | 15 | return waiter 16 | end 17 | 18 | function WaitForChar:start() 19 | self.tap = hs.eventtap.new( 20 | { hs.eventtap.event.types.keyDown }, 21 | function(event) 22 | local character = event:getCharacters() 23 | local escChar = "" 24 | 25 | if character == "" or character == escChar then 26 | self.onCancel() 27 | else 28 | self.onChar(character) 29 | end 30 | 31 | self.tap:stop() 32 | 33 | -- prevent any char passthru 34 | return true 35 | end 36 | ) 37 | 38 | self.tap:start() 39 | end 40 | 41 | return WaitForChar 42 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/buffer_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require('lib/buffer') 2 | local Selection = require('lib/selection') 3 | 4 | describe("Buffer", function() 5 | local text = "fdsafdsa" 6 | 7 | describe("#getContentsBeforeSelection()", function() 8 | it("returns the text in before the cursor", function() 9 | local buffer = Buffer:new() 10 | 11 | buffer:setValue(text) 12 | buffer:setSelectionRange(1, 0) 13 | 14 | assert.are.equals( 15 | "f", 16 | buffer:getContentsBeforeSelection() 17 | ) 18 | end) 19 | 20 | it("returns nil if we're at the start", function() 21 | local buffer = Buffer:new() 22 | 23 | buffer:setValue(text) 24 | buffer:setSelectionRange(0, 0) 25 | 26 | assert.are.equals( 27 | nil, 28 | buffer:getContentsBeforeSelection() 29 | ) 30 | end) 31 | end) 32 | 33 | describe("#getCurrentLineRange()", function() 34 | it("gets the range for line 1", function() 35 | local buffer = Buffer:new() 36 | buffer:setValue("haha\nwhat yeah\nwhatever") 37 | buffer:setSelectionRange(0, 0) 38 | 39 | assert.are.same( 40 | Selection:new(0, 5), 41 | buffer:getCurrentLineRange() 42 | ) 43 | end) 44 | 45 | it("gets the range for line 2", function() 46 | local buffer = Buffer:new() 47 | buffer:setValue("haha\nwhat yeah\nwhatever") 48 | buffer:setSelectionRange(6, 0) 49 | 50 | assert.are.same( 51 | Selection:new(5, 10), 52 | buffer:getCurrentLineRange() 53 | ) 54 | end) 55 | 56 | it("gets the range for line 3", function() 57 | local buffer = Buffer:new() 58 | buffer:setValue("haha\nwhat yeah\nwhatever") 59 | buffer:setSelectionRange(15, 0) 60 | 61 | assert.are.same( 62 | Selection:new(15, 8), 63 | buffer:getCurrentLineRange() 64 | ) 65 | end) 66 | end) 67 | 68 | describe("#getCurrentLineNumber", function() 69 | it("works at the start", function() 70 | local buffer = Buffer:new() 71 | buffer:setValue("haha\nwhat yeah\nwhatever") 72 | buffer:setSelectionRange(0, 0) -- "(h)aha\nwhat..." 73 | 74 | assert.are.equals(1, buffer:getCurrentLineNumber()) 75 | end) 76 | 77 | it("works in the middle of the buffer", function() 78 | local buffer = Buffer:new() 79 | buffer:setValue("haha\nwhat yeah\nwhatever") 80 | buffer:setSelectionRange(6, 0) -- "haha\nw(h)at..." 81 | 82 | assert.are.equals(2, buffer:getCurrentLineNumber()) 83 | end) 84 | 85 | it("works at the end", function() 86 | local buffer = Buffer:new() 87 | buffer:setValue("haha\nwhat yeah\nwhatever") 88 | buffer:setSelectionRange(22, 0) -- "haha\nwhat yeah\nwhateve(r)" 89 | 90 | assert.are.equals(3, buffer:getCurrentLineNumber()) 91 | end) 92 | end) 93 | 94 | describe("#getContentsAfterSelection()", function() 95 | it("returns the text in front of the cursor", function() 96 | local buffer = Buffer:new() 97 | 98 | buffer:setValue(text) 99 | buffer:setSelectionRange(1, 0) 100 | 101 | assert.are.equals( 102 | "dsafdsa", 103 | buffer:getContentsAfterSelection() 104 | ) 105 | end) 106 | 107 | it("returns nil if we're at the end", function() 108 | local buffer = Buffer:new() 109 | 110 | buffer:setValue(text) 111 | buffer:setSelectionRange(8, 0) 112 | 113 | assert.are.equals( 114 | nil, 115 | buffer:getContentsAfterSelection() 116 | ) 117 | end) 118 | end) 119 | end) 120 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/config_spec.lua: -------------------------------------------------------------------------------- 1 | local Config = require('lib/config') 2 | 3 | describe("Config", function() 4 | describe("default values", function() 5 | it("has them", function() 6 | local config = Config:new() 7 | 8 | assert.are.equals(true, config.shouldShowAlertInNormalMode) 9 | assert.are.equals("Courier New", config.alert.font) 10 | end) 11 | end) 12 | 13 | describe("#setOptions", function() 14 | it("sets options", function() 15 | local config = Config:new() 16 | config:setOptions({ shouldDimScreenInNormalMode = false }) 17 | 18 | assert.are.same(false, config.shouldDimScreenInNormalMode) 19 | end) 20 | end) 21 | end) 22 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/features/big_word_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'big word motion', js: true do 2 | context 'W' do 3 | advanced_mode do 4 | it 'moves to the end of a single line when utf8 chars' do 5 | value = 'First “line |here.' 6 | expected = 'First “line here.|' 7 | 8 | expect_textarea_change_in_normal_mode(from: value, to: expected) do 9 | fire 'W' 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/features/delete_line_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'delete word', js: true do 6 | context 'dd' do 7 | fallback_mode do 8 | it "deletes a line and stays on the line :(" do 9 | value = <<~EOF.strip 10 | Line 1 11 | Lin|e 2 12 | Line 3 13 | EOF 14 | 15 | expected = <<~EOF.strip 16 | Line 1 17 | | 18 | Line 3 19 | EOF 20 | 21 | expect_textarea_change_in_normal_mode(from: value, to: expected) do 22 | fire 'dd' 23 | end 24 | end 25 | 26 | it "deletes the last line and puts the cursor on the line above" do 27 | value = <<~EOF.strip 28 | Line 1 29 | Lin|e 2 30 | EOF 31 | 32 | expected = "Line 1\n|" 33 | 34 | expect_textarea_change_in_normal_mode(from: value, to: expected) do 35 | fire 'dd' 36 | end 37 | end 38 | end 39 | 40 | advanced_mode do 41 | it "deletes a line and goes down one line" do 42 | value = <<~EOF.strip 43 | Line 1 44 | Lin|e 2 45 | Line 3 46 | EOF 47 | 48 | expected = <<~EOF.strip 49 | Line 1 50 | |Line 3 51 | EOF 52 | 53 | expect_textarea_change_in_normal_mode(from: value, to: expected) do 54 | fire 'dd' 55 | end 56 | end 57 | 58 | it "deletes the last line and puts the cursor on the line above" do 59 | value = <<~EOF.strip 60 | Line 1 61 | Lin|e 2 62 | EOF 63 | 64 | expected = "|Line 1" 65 | 66 | expect_textarea_change_in_normal_mode(from: value, to: expected) do 67 | fire 'dd' 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/features/delete_word_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'delete word', js: true do 6 | context 'dw' do 7 | fallback_mode do 8 | it 'deletes a single word value' do 9 | expect_textarea_change_in_normal_mode(from: '|Word', to: "|") do 10 | fire('dw') 11 | end 12 | end 13 | 14 | it 'deletes from the middle of a word to the end' do 15 | expect_textarea_change_in_normal_mode(from: "W|ord a", to: "W| a") do 16 | fire('dw') 17 | end 18 | end 19 | end 20 | 21 | advanced_mode do 22 | it 'deletes a single word value' do 23 | expect_textarea_change_in_normal_mode(from: '|Word', to: '|') do 24 | fire('dw') 25 | end 26 | end 27 | 28 | it 'deletes from the middle of a word' do 29 | expect_textarea_change_in_normal_mode(from: 'W|ord', to: 'W|') do 30 | fire('dw') 31 | end 32 | end 33 | 34 | it 'handles multiple words' do 35 | expect_textarea_change_in_normal_mode( 36 | from: '|Word another', 37 | to: '|another' 38 | ) do 39 | fire('dw') 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/features/linewise_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'linewise movements', js: true do 6 | context 'moving up' do 7 | fallback_mode do 8 | it 'goes up and maintains column position' do 9 | set_textarea_value_and_selection <<~EOF 10 | My line 1 11 | My |line 2 12 | EOF 13 | 14 | normal_mode do 15 | fire 'k' 16 | 17 | expect_textarea_to_have_value_and_selection <<~EOF 18 | My |line 1 19 | My line 2 20 | EOF 21 | end 22 | end 23 | end 24 | 25 | advanced_mode do 26 | it 'goes up and maintains column position' do 27 | set_textarea_value_and_selection <<~EOF 28 | My line 1 29 | My |line 2 30 | EOF 31 | 32 | normal_mode do 33 | fire 'k' 34 | 35 | expect_textarea_to_have_value_and_selection <<~EOF 36 | My |line 1 37 | My line 2 38 | EOF 39 | end 40 | end 41 | end 42 | end 43 | 44 | context 'moving down' do 45 | fallback_mode do 46 | it 'goes down and maintains column position' do 47 | set_textarea_value_and_selection <<~EOF 48 | My |line 1 49 | My line 2 50 | EOF 51 | 52 | normal_mode do 53 | fire 'j' 54 | 55 | expect_textarea_to_have_value_and_selection <<~EOF 56 | My line 1 57 | My |line 2 58 | EOF 59 | end 60 | end 61 | end 62 | 63 | advanced_mode do 64 | it 'goes down and maintains column position' do 65 | set_textarea_value_and_selection <<~EOF 66 | My |line 1 67 | My line 2 68 | EOF 69 | 70 | normal_mode do 71 | fire 'j' 72 | 73 | expect_textarea_to_have_value_and_selection <<~EOF 74 | My line 1 75 | My |line 2 76 | EOF 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/features/visual_mode_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'visual mode', js: true do 6 | before { open_and_focus_page! } 7 | 8 | context 'forward word' do 9 | it 'goes forward one word' do 10 | set_textarea_value_and_selection('|Thing word yeah') 11 | 12 | visual_mode do 13 | fire 'w' 14 | 15 | expect_textarea_to_have_value_and_selection('|Thing w|ord yeah') 16 | end 17 | end 18 | end 19 | 20 | context 't-search' do 21 | it 'should search all the way to before the char inclusive' do 22 | set_textarea_value_and_selection('|Thing word yeah') 23 | 24 | visual_mode do 25 | fire 't' 26 | fire 'g' 27 | 28 | expect_textarea_to_have_value_and_selection('|Thin|g word yeah') 29 | end 30 | end 31 | end 32 | 33 | context 'forward search' do 34 | it 'should search all the way to the char inclusive' do 35 | set_textarea_value_and_selection('|Thing word yeah') 36 | 37 | visual_mode do 38 | fire 'f' 39 | fire 'g' 40 | 41 | expect_textarea_to_have_value_and_selection('|Thing| word yeah') 42 | end 43 | end 44 | end 45 | 46 | def visual_mode 47 | send_os_keys('jk') 48 | send_os_keys('v') 49 | yield 50 | ensure 51 | send_os_keys(:escape) 52 | send_os_keys('i') 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/fixtures/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test fixture with textarea 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/motions/back_word_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local BackWord = require("lib/motions/back_word") 3 | 4 | describe("BackWord", function() 5 | it("has a name", function() 6 | assert.are.equals("back_word", BackWord:new().name) 7 | end) 8 | 9 | describe("#getRange", function() 10 | it("handles simple words", function() 11 | local buffer = Buffer:new() 12 | buffer:setValue("cat dog mouse") 13 | buffer:setSelectionRange(3, 0) 14 | 15 | local backWord = BackWord:new() 16 | 17 | assert.are.same( 18 | { 19 | start = 0, 20 | finish = 3, 21 | mode = "exclusive", 22 | direction = "characterwise" 23 | }, 24 | backWord:getRange(buffer) 25 | ) 26 | end) 27 | 28 | it("handles starting on the next word", function() 29 | local buffer = Buffer:new() 30 | buffer:setValue("cat dog mouse") -- cat dog (m)ouse 31 | buffer:setSelectionRange(8, 0) 32 | 33 | local backWord = BackWord:new() 34 | local result = backWord:getRange(buffer) 35 | 36 | assert.are.same( 37 | { 38 | start = 4, -- cat (d)og mouse 39 | finish = 8, 40 | mode = "exclusive", 41 | direction = "characterwise" 42 | }, 43 | result 44 | ) 45 | end) 46 | 47 | it("crosses the new line boundary", function() 48 | local buffer = Buffer:new() 49 | buffer:setValue("ab cd\n ef") 50 | buffer:setSelectionRange(8, 0) -- (e)f 51 | 52 | local backWord = BackWord:new() 53 | 54 | assert.are.same( 55 | { 56 | start = 3, 57 | finish = 8, 58 | mode = "exclusive", 59 | direction = "characterwise" 60 | }, 61 | backWord:getRange(buffer) 62 | ) 63 | end) 64 | 65 | it("handles punctuation stops", function() 66 | local buffer = Buffer:new() 67 | buffer:setValue("www.test.com") 68 | buffer:setSelectionRange(11, 0) -- .co(m) 69 | 70 | local backWord = BackWord:new() 71 | 72 | assert.are.same( 73 | { 74 | start = 9, 75 | finish = 11, 76 | mode = "exclusive", 77 | direction = "characterwise" 78 | }, 79 | backWord:getRange(buffer) 80 | ) 81 | end) 82 | 83 | it("handles jumping across punctuation sequences", function() 84 | local buffer = Buffer:new() 85 | buffer:setValue("www.test..com") 86 | buffer:setSelectionRange(10, 0) -- ..(c)om 87 | 88 | local backWord = BackWord:new() 89 | 90 | assert.are.same( 91 | { 92 | start = 8, 93 | finish = 10, 94 | mode = "exclusive", 95 | direction = "characterwise" 96 | }, 97 | backWord:getRange(buffer) 98 | ) 99 | end) 100 | 101 | it("handles jumping from punctuation thru words", function() 102 | local buffer = Buffer:new() 103 | buffer:setValue("www.test.com") 104 | buffer:setSelectionRange(8, 0) -- www.test(.)com 105 | 106 | local backWord = BackWord:new() 107 | 108 | assert.are.same( 109 | { 110 | start = 4, 111 | finish = 8, 112 | mode = "exclusive", 113 | direction = "characterwise" 114 | }, 115 | backWord:getRange(buffer) 116 | ) 117 | end) 118 | end) 119 | end) 120 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/motions/between_chars_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local BetweenChars = require("lib/motions/between_chars") 3 | 4 | describe("BetweenChars", function() 5 | it("has a name", function() 6 | assert.are.equals("between_chars", BetweenChars:new().name) 7 | end) 8 | 9 | describe("#getRange", function() 10 | it("returns nil if there is no between chars", function() 11 | local buffer = Buffer:new() 12 | buffer:setValue("html") 13 | buffer:setSelectionRange(0, 0) 14 | 15 | local betweenChars = BetweenChars:new() 16 | betweenChars:setSearchChars('<', '>') 17 | 18 | assert.are.same(nil, betweenChars:getRange(buffer)) 19 | end) 20 | 21 | it("handles the left edge", function() 22 | local buffer = Buffer:new() 23 | buffer:setValue("") 24 | buffer:setSelectionRange(0, 0) 25 | 26 | local betweenChars = BetweenChars:new() 27 | betweenChars:setSearchChars('<', '>') 28 | 29 | assert.are.same( 30 | { 31 | start = 1, 32 | finish = 4, 33 | mode = "inclusive", 34 | direction = "characterwise" 35 | }, 36 | betweenChars:getRange(buffer) 37 | ) 38 | end) 39 | 40 | it("handles the right edge", function() 41 | local buffer = Buffer:new() 42 | buffer:setValue("") 43 | buffer:setSelectionRange(5, 0) 44 | 45 | local betweenChars = BetweenChars:new() 46 | betweenChars:setSearchChars('<', '>') 47 | 48 | assert.are.same( 49 | { 50 | start = 1, 51 | finish = 4, 52 | mode = "inclusive", 53 | direction = "characterwise" 54 | }, 55 | betweenChars:getRange(buffer) 56 | ) 57 | end) 58 | 59 | it("handles between the chars", function() 60 | local buffer = Buffer:new() 61 | buffer:setValue("") 62 | buffer:setSelectionRange(3, 0) 63 | 64 | local betweenChars = BetweenChars:new() 65 | betweenChars:setSearchChars('<', '>') 66 | 67 | assert.are.same( 68 | { 69 | start = 1, 70 | finish = 4, 71 | mode = "inclusive", 72 | direction = "characterwise" 73 | }, 74 | betweenChars:getRange(buffer) 75 | ) 76 | end) 77 | end) 78 | end) 79 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/motions/big_word_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local Selection = require("lib/selection") 3 | local BigWord = require("lib/motions/big_word") 4 | 5 | describe("BigWord", function() 6 | it("has a name", function() 7 | assert.are.equals("big_word", BigWord:new().name) 8 | end) 9 | 10 | describe("#getRange", function() 11 | it("handles simple words", function() 12 | local buffer = Buffer:new() 13 | buffer:setValue("cat dog mouse") 14 | buffer:setSelectionRange(0, 0) 15 | 16 | local bigWord = BigWord:new() 17 | 18 | assert.are.same( 19 | { 20 | start = 0, 21 | finish = 4, 22 | mode = "exclusive", 23 | direction = "characterwise" 24 | }, 25 | bigWord:getRange(buffer) 26 | ) 27 | end) 28 | 29 | it("handles punctuation boundaries", function() 30 | local buffer = Buffer:new() 31 | buffer:setValue("www.site.com ok") 32 | buffer:setSelectionRange(0, 0) 33 | 34 | local bigWord = BigWord:new() 35 | 36 | assert.are.same( 37 | { 38 | start = 0, 39 | finish = 13, 40 | mode = "exclusive", 41 | direction = "characterwise" 42 | }, 43 | bigWord:getRange(buffer) 44 | ) 45 | end) 46 | end) 47 | 48 | describe("#getMovements", function() 49 | it("returns the key sequence to move by word", function() 50 | local bigWord = BigWord:new() 51 | 52 | assert.are.same( 53 | { 54 | { 55 | modifiers = { 'alt' }, 56 | key = 'right', 57 | selection = true 58 | } 59 | }, 60 | bigWord:getMovements() 61 | ) 62 | end) 63 | end) 64 | end) 65 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/motions/end_of_word_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local EndOfWord = require("lib/motions/end_of_word") 3 | 4 | describe("EndOfWord", function() 5 | it("has a name", function() 6 | assert.are.equals("end_of_word", EndOfWord:new().name) 7 | end) 8 | 9 | describe("#getRange", function() 10 | it("handles simple words", function() 11 | local buffer = Buffer:new() 12 | buffer:setValue("cat dog mouse") 13 | buffer:setSelectionRange(0, 0) 14 | 15 | local endOfWord = EndOfWord:new() 16 | 17 | assert.are.same( 18 | { 19 | start = 0, 20 | finish = 2, 21 | mode = "inclusive", 22 | direction = "characterwise" 23 | }, 24 | endOfWord:getRange(buffer) 25 | ) 26 | end) 27 | 28 | it("goes to the next word end", function() 29 | local buffer = Buffer:new() 30 | buffer:setValue("cat dog mouse") 31 | buffer:setSelectionRange(2, 0) 32 | 33 | local endOfWord = EndOfWord:new() 34 | 35 | assert.are.same( 36 | { 37 | start = 2, 38 | finish = 6, 39 | mode = "inclusive", 40 | direction = "characterwise" 41 | }, 42 | endOfWord:getRange(buffer) 43 | ) 44 | end) 45 | 46 | it("stops on new lines", function() 47 | local buffer = Buffer:new() 48 | buffer:setValue("cat\nmouse") 49 | buffer:setSelectionRange(0, 0) 50 | 51 | local endOfWord = EndOfWord:new() 52 | 53 | assert.are.same( 54 | { 55 | start = 0, 56 | finish = 2, 57 | mode = "inclusive", 58 | direction = "characterwise" 59 | }, 60 | endOfWord:getRange(buffer) 61 | ) 62 | end) 63 | end) 64 | 65 | describe("#getMovements", function() 66 | it("returns the key sequence to move by word", function() 67 | local endOfWord = EndOfWord:new() 68 | 69 | assert.are.same( 70 | { 71 | { 72 | modifiers = { 'alt' }, 73 | key = 'right', 74 | selection = true 75 | } 76 | }, 77 | endOfWord:getMovements() 78 | ) 79 | end) 80 | end) 81 | end) 82 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/motions/in_word_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local InWord = require("lib/motions/in_word") 3 | 4 | describe("InWord", function() 5 | it("has a name", function() 6 | assert.are.equals("in_word", InWord:new().name) 7 | end) 8 | 9 | describe("#getRange", function() 10 | it("handles the middle of the word", function() 11 | local buffer = Buffer:new() 12 | buffer:setValue("cat dog mouse") 13 | buffer:setSelectionRange(5, 0) 14 | 15 | local inWord = InWord:new() 16 | 17 | assert.are.same( 18 | { 19 | start = 4, 20 | finish = 6, 21 | mode = "inclusive", 22 | direction = "characterwise" 23 | }, 24 | inWord:getRange(buffer) 25 | ) 26 | end) 27 | 28 | it("handles the beginning of the buffer", function() 29 | local buffer = Buffer:new() 30 | buffer:setValue("cat dog mouse") 31 | buffer:setSelectionRange(0, 0) 32 | 33 | local inWord = InWord:new() 34 | 35 | assert.are.same( 36 | { 37 | start = 0, 38 | finish = 2, 39 | mode = "inclusive", 40 | direction = "characterwise" 41 | }, 42 | inWord:getRange(buffer) 43 | ) 44 | end) 45 | 46 | it("handles the beginning of the word", function() 47 | local buffer = Buffer:new() 48 | buffer:setValue("cat dog mouse") 49 | buffer:setSelectionRange(4, 0) 50 | 51 | local inWord = InWord:new() 52 | 53 | assert.are.same( 54 | { 55 | start = 4, 56 | finish = 6, 57 | mode = "inclusive", 58 | direction = "characterwise" 59 | }, 60 | inWord:getRange(buffer) 61 | ) 62 | end) 63 | 64 | it("handles the end of the word", function() 65 | local buffer = Buffer:new() 66 | buffer:setValue("cat dog mouse") 67 | buffer:setSelectionRange(6, 0) 68 | 69 | local inWord = InWord:new() 70 | 71 | assert.are.same( 72 | { 73 | start = 4, 74 | finish = 6, 75 | mode = "inclusive", 76 | direction = "characterwise" 77 | }, 78 | inWord:getRange(buffer) 79 | ) 80 | end) 81 | 82 | it("handles the end of the buffer", function() 83 | local buffer = Buffer:new() 84 | buffer:setValue("cat dog mouse") 85 | buffer:setSelectionRange(12, 0) 86 | 87 | local inWord = InWord:new() 88 | 89 | assert.are.same( 90 | { 91 | start = 8, 92 | finish = 13, 93 | mode = "inclusive", 94 | direction = "characterwise" 95 | }, 96 | inWord:getRange(buffer) 97 | ) 98 | end) 99 | end) 100 | end) 101 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/motions/line_beginning_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local Selection = require("lib/selection") 3 | local LineBeginning = require("lib/motions/line_beginning") 4 | 5 | describe("LineBeginning", function() 6 | it("has a name", function() 7 | assert.are.equals("line_beginning", LineBeginning:new().name) 8 | end) 9 | 10 | describe("#getRange", function() 11 | it("handles first lines", function() 12 | local buffer = Buffer:new() 13 | buffer:setValue("cat dog mouse") 14 | buffer:setSelectionRange(4, 0) -- cat (d)og mouse 15 | 16 | local lineBeginning = LineBeginning:new() 17 | 18 | assert.are.same( 19 | { 20 | start = 0, 21 | finish = 4, 22 | mode = "exclusive", 23 | direction = "characterwise" 24 | }, 25 | lineBeginning:getRange(buffer) 26 | ) 27 | end) 28 | 29 | it("handles 2 lines", function() 30 | local buffer = Buffer:new() 31 | buffer:setValue("cat\ndog mouse") 32 | buffer:setSelectionRange(6, 0) -- cat\ndo(g) mouse 33 | 34 | local lineBeginning = LineBeginning:new() 35 | 36 | assert.are.same( 37 | { 38 | start = 4, 39 | finish = 6, 40 | mode = "exclusive", 41 | direction = "characterwise" 42 | }, 43 | lineBeginning:getRange(buffer) 44 | ) 45 | end) 46 | 47 | it("handles 3 lines", function() 48 | local buffer = Buffer:new() 49 | buffer:setValue("cat\ndog\nmouse") 50 | buffer:setSelectionRange(10, 0) -- cat\ndog\nmo(u)se 51 | 52 | local lineBeginning = LineBeginning:new() 53 | 54 | assert.are.same( 55 | { 56 | start = 8, 57 | finish = 10, 58 | mode = "exclusive", 59 | direction = "characterwise" 60 | }, 61 | lineBeginning:getRange(buffer) 62 | ) 63 | end) 64 | end) 65 | end) 66 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/motions/line_end_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local Selection = require("lib/selection") 3 | local LineEnd = require("lib/motions/line_end") 4 | 5 | describe("LineEnd", function() 6 | it("has a name", function() 7 | assert.are.equals("line_end", LineEnd:new().name) 8 | end) 9 | 10 | describe("#getRange", function() 11 | it("handles simple line deletes", function() 12 | local buffer = Buffer:new() 13 | buffer:setValue("ab\ncd") 14 | buffer:setSelectionRange(0, 0) -- cat (d)og mouse 15 | 16 | local lineEnd = LineEnd:new() 17 | 18 | assert.are.same( 19 | { 20 | start = 0, 21 | finish = 2, 22 | mode = "exclusive", 23 | direction = "characterwise" 24 | }, 25 | lineEnd:getRange(buffer) 26 | ) 27 | end) 28 | 29 | it("handles first lines", function() 30 | local buffer = Buffer:new() 31 | buffer:setValue("cat dog mouse") 32 | buffer:setSelectionRange(4, 0) -- cat (d)og mouse 33 | 34 | local lineEnd = LineEnd:new() 35 | 36 | assert.are.same( 37 | { 38 | start = 4, 39 | finish = 13, 40 | mode = "exclusive", 41 | direction = "characterwise" 42 | }, 43 | lineEnd:getRange(buffer) 44 | ) 45 | end) 46 | 47 | it("handles 2 lines", function() 48 | local buffer = Buffer:new() 49 | buffer:setValue("cat\ndog mouse") 50 | buffer:setSelectionRange(6, 0) -- cat\ndo(g) mouse 51 | 52 | local lineEnd = LineEnd:new() 53 | 54 | assert.are.same( 55 | { 56 | start = 6, 57 | finish = 13, 58 | mode = "exclusive", 59 | direction = "characterwise" 60 | }, 61 | lineEnd:getRange(buffer) 62 | ) 63 | end) 64 | 65 | it("handles 3 lines", function() 66 | local buffer = Buffer:new() 67 | buffer:setValue("cat\ndog\nmouse") 68 | buffer:setSelectionRange(10, 0) -- cat\ndog\nmo(u)se 69 | 70 | local lineEnd = LineEnd:new() 71 | 72 | assert.are.same( 73 | { 74 | start = 10, 75 | finish = 13, 76 | mode = "exclusive", 77 | direction = "characterwise" 78 | }, 79 | lineEnd:getRange(buffer) 80 | ) 81 | end) 82 | end) 83 | end) 84 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/motions/word_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local Word = require("lib/motions/word") 3 | 4 | describe("Word", function() 5 | it("has a name", function() 6 | assert.are.equals("word", Word:new().name) 7 | end) 8 | 9 | describe("#getRange", function() 10 | it("handles simple words", function() 11 | local buffer = Buffer:new() 12 | buffer:setValue("cat dog mouse") 13 | buffer:setSelectionRange(0, 0) 14 | 15 | local word = Word:new() 16 | 17 | assert.are.same( 18 | { 19 | start = 0, 20 | finish = 4, 21 | mode = "exclusive", 22 | direction = "characterwise" 23 | }, 24 | word:getRange(buffer) 25 | ) 26 | end) 27 | 28 | it("deals with being on a new line", function() 29 | local buffer = Buffer:new() 30 | buffer:setValue("ok\n\nfish") 31 | buffer:setSelectionRange(2, 0) 32 | 33 | local word = Word:new() 34 | 35 | assert.are.same( 36 | { 37 | start = 2, 38 | finish = 3, 39 | mode = "exclusive", 40 | direction = "characterwise" 41 | }, 42 | word:getRange(buffer) 43 | ) 44 | end) 45 | 46 | it("continues from punctuation to the next word", function() 47 | local buffer = Buffer:new() 48 | buffer:setValue("ab- cd") 49 | buffer:setSelectionRange(2, 0) 50 | 51 | local word = Word:new() 52 | 53 | assert.are.same( 54 | { 55 | start = 2, 56 | finish = 4, 57 | mode = "exclusive", 58 | direction = "characterwise" 59 | }, 60 | word:getRange(buffer) 61 | ) 62 | end) 63 | 64 | it("stops on punctuation", function() 65 | local buffer = Buffer:new() 66 | buffer:setValue("ab-cd-ef") 67 | buffer:setSelectionRange(0, 0) 68 | 69 | local word = Word:new() 70 | 71 | assert.are.same( 72 | { 73 | start = 0, 74 | finish = 2, 75 | mode = "exclusive", 76 | direction = "characterwise" 77 | }, 78 | word:getRange(buffer) 79 | ) 80 | end) 81 | 82 | it("moves from punctuation", function() 83 | local buffer = Buffer:new() 84 | buffer:setValue("ab-cd-ef") 85 | buffer:setSelectionRange(2, 0) 86 | 87 | local word = Word:new() 88 | 89 | assert.are.same( 90 | { 91 | start = 2, 92 | finish = 3, 93 | mode = "exclusive", 94 | direction = "characterwise" 95 | }, 96 | word:getRange(buffer) 97 | ) 98 | end) 99 | 100 | it("stops on new lines", function() 101 | local buffer = Buffer:new() 102 | buffer:setValue("cat dog\nfish") 103 | buffer:setSelectionRange(4, 0) 104 | 105 | local word = Word:new() 106 | 107 | assert.are.same( 108 | { 109 | start = 4, 110 | finish = 7, 111 | mode = "exclusive", 112 | direction = "characterwise" 113 | }, 114 | word:getRange(buffer) 115 | ) 116 | end) 117 | 118 | it("flips to an inclusive motion if last word in buffer #focus", function() 119 | local buffer = Buffer:new() 120 | buffer:setValue("cat") 121 | buffer:setSelectionRange(0, 0) 122 | 123 | local word = Word:new() 124 | 125 | assert.are.same( 126 | { 127 | start = 0, 128 | finish = 3, 129 | mode = "inclusive", 130 | direction = "characterwise" 131 | }, 132 | word:getRange(buffer) 133 | ) 134 | end) 135 | end) 136 | 137 | describe("#getMovements", function() 138 | it("returns the key sequence to move by word", function() 139 | local word = Word:new() 140 | 141 | assert.are.same( 142 | { 143 | { 144 | modifiers = { 'alt' }, 145 | key = 'right', 146 | selection = true 147 | } 148 | }, 149 | word:getMovements() 150 | ) 151 | end) 152 | end) 153 | end) 154 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/operators/delete_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local Selection = require("lib/selection") 3 | local Delete = require("lib/operators/delete") 4 | 5 | describe("Delete", function() 6 | it("has a name", function() 7 | assert.are.equals("delete", Delete:new().name) 8 | end) 9 | 10 | describe("#getModifiedBuffer", function() 11 | it("deletes the range of text starting from the beginning", function() 12 | local buffer = Buffer:new() 13 | buffer:setValue("word one two") 14 | buffer:setSelectionRange(0, 0) 15 | 16 | local delete = Delete:new() 17 | 18 | local newBuffer = delete:getModifiedBuffer(buffer, 0, 4) 19 | 20 | assert.are.equals("one two", newBuffer:getValue()) 21 | assert.are.same( 22 | Selection:new(0, 0), 23 | newBuffer:getSelectionRange() 24 | ) 25 | end) 26 | 27 | it("deletes the range of text in the middle", function() 28 | local buffer = Buffer:new() 29 | buffer:setValue("word one two") 30 | buffer:setSelectionRange(5, 0) 31 | 32 | local delete = Delete:new() 33 | 34 | local newBuffer = delete:getModifiedBuffer(buffer, 5, 8) 35 | 36 | assert.are.equals("word two", newBuffer:getValue()) 37 | assert.are.same( 38 | Selection:new(5, 0), 39 | newBuffer:getSelectionRange() 40 | ) 41 | end) 42 | end) 43 | end) 44 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/operators/replace_spec.lua: -------------------------------------------------------------------------------- 1 | local Buffer = require("lib/buffer") 2 | local Selection = require("lib/selection") 3 | local Replace = require("lib/operators/replace") 4 | 5 | describe("Replace", function() 6 | it("has a name", function() 7 | assert.are.equals("replace", Replace:new().name) 8 | end) 9 | 10 | describe("#getModifiedBuffer", function() 11 | it("deletes the range of text starting from the beginning", function() 12 | local buffer = Buffer:new() 13 | buffer:setValue("abc") 14 | buffer:setSelectionRange(0, 0) 15 | 16 | local replace = Replace:new() 17 | replace:setExtraChar("1") 18 | 19 | local newBuffer = replace:getModifiedBuffer(buffer, 0, 0) 20 | 21 | assert.are.equals("1bc", newBuffer:getValue()) 22 | assert.are.same( 23 | Selection:new(0, 0), 24 | newBuffer:getSelectionRange() 25 | ) 26 | end) 27 | 28 | it("replaces the range in the middle", function() 29 | local buffer = Buffer:new() 30 | buffer:setValue("abc") 31 | buffer:setSelectionRange(1, 0) 32 | 33 | local replace = Replace:new() 34 | replace:setExtraChar("d") 35 | 36 | local newBuffer = replace:getModifiedBuffer(buffer, 1, 1) 37 | 38 | assert.are.equals("adc", newBuffer:getValue()) 39 | assert.are.same( 40 | Selection:new(1, 0), 41 | newBuffer:getSelectionRange() 42 | ) 43 | end) 44 | 45 | it("replaces with multiple chars if it's a range > 1", function() 46 | local buffer = Buffer:new() 47 | buffer:setValue("abc def") 48 | buffer:setSelectionRange(0, 0) 49 | 50 | local replace = Replace:new() 51 | replace:setExtraChar("*") 52 | 53 | local newBuffer = replace:getModifiedBuffer(buffer, 0, 2) 54 | 55 | assert.are.equals("*** def", newBuffer:getValue()) 56 | assert.are.same( 57 | Selection:new(0, 0), 58 | newBuffer:getSelectionRange() 59 | ) 60 | end) 61 | end) 62 | end) 63 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/selection_spec.lua: -------------------------------------------------------------------------------- 1 | local Selection = require('lib/selection') 2 | 3 | describe("Selection", function() 4 | describe("data accessors", function() 5 | it("has location", function() 6 | local selection = Selection:new(10, 1) 7 | 8 | assert.are.equals(selection.location, 10) 9 | end) 10 | 11 | it("has selection length", function() 12 | local selection = Selection:new(10, 50) 13 | 14 | assert.are.equals(selection.length, 50) 15 | end) 16 | end) 17 | 18 | describe("#positionEnd", function() 19 | it("should return the location plus length", function() 20 | local selection = Selection:new(10, 50) 21 | 22 | assert.are.equals(selection:positionEnd(), 60) 23 | end) 24 | end) 25 | 26 | describe("#isSelected", function() 27 | it("is selected if length > 0", function() 28 | local selection = Selection:new(10, 1) 29 | 30 | assert.True(selection:isSelected()) 31 | end) 32 | 33 | it("is not selected if length == 0", function() 34 | local selection = Selection:new(10, 0) 35 | 36 | assert.False(selection:isSelected()) 37 | end) 38 | end) 39 | end) 40 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/spec_helper.lua: -------------------------------------------------------------------------------- 1 | inspect = dofile("lib/utils/inspect.lua") 2 | 3 | vimModeScriptPath = "" 4 | 5 | vimLogger = dofile("lib/utils/log.lua") 6 | -- conform to the Hammerspoon logging API as well 7 | vimLogger.i = vimLogger.info 8 | vimLogger.d = vimLogger.debug 9 | vimLogger.e = vimLogger.error 10 | vimLogger.v = vimLogger.trace 11 | 12 | debugger = require("debugger") 13 | debugger.auto_where = 2 14 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pry' 4 | 5 | Dir.glob('spec/support/**/*.rb') do |file| 6 | require(File.dirname(__FILE__) + "/../#{file}") 7 | end 8 | 9 | RSpec.configure do |config| 10 | config.include Capybara::DSL 11 | 12 | # rspec-expectations config goes here. You can use an alternate 13 | # assertion/expectation library such as wrong or the stdlib/minitest 14 | # assertions if you prefer. 15 | config.expect_with :rspec do |expectations| 16 | # This option will default to `true` in RSpec 4. It makes the `description` 17 | # and `failure_message` of custom matchers include text for helper methods 18 | # defined using `chain`, e.g.: 19 | # be_bigger_than(2).and_smaller_than(4).description 20 | # # => "be bigger than 2 and smaller than 4" 21 | # ...rather than: 22 | # # => "be bigger than 2" 23 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 24 | end 25 | 26 | # rspec-mocks config goes here. You can use an alternate test double 27 | # library (such as bogus or mocha) by changing the `mock_with` option here. 28 | config.mock_with :rspec do |mocks| 29 | # Prevents you from mocking or stubbing a method that does not exist on 30 | # a real object. This is generally recommended, and will default to 31 | # `true` in RSpec 4. 32 | mocks.verify_partial_doubles = true 33 | end 34 | 35 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 36 | # have no way to turn it off -- the option exists only for backwards 37 | # compatibility in RSpec 3). It causes shared context metadata to be 38 | # inherited by the metadata hash of host groups and examples, rather than 39 | # triggering implicit auto-inclusion in groups with matching metadata. 40 | config.shared_context_metadata_behavior = :apply_to_host_groups 41 | 42 | # The settings below are suggested to provide a good initial experience 43 | # with RSpec, but feel free to customize to your heart's content. 44 | # # This allows you to limit a spec run to individual examples or groups 45 | # # you care about by tagging them with `:focus` metadata. When nothing 46 | # # is tagged with `:focus`, all examples get run. RSpec also provides 47 | # # aliases for `it`, `describe`, and `context` that include `:focus` 48 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 49 | # config.filter_run_when_matching :focus 50 | # 51 | # # Allows RSpec to persist some state between runs in order to support 52 | # # the `--only-failures` and `--next-failure` CLI options. We recommend 53 | # # you configure your source control system to ignore this file. 54 | # config.example_status_persistence_file_path = "spec/examples.txt" 55 | # 56 | # Limits the available syntax to the non-monkey patched syntax that is 57 | # recommended. For more details, see: 58 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 59 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 60 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 61 | config.disable_monkey_patching! 62 | # 63 | # # This setting enables warnings. It's recommended, but in some cases may 64 | # # be too noisy due to issues in dependencies. 65 | # config.warnings = true 66 | # 67 | # # Many RSpec users commonly either run the entire suite or an individual 68 | # # file, and it's useful to allow more verbose output when running an 69 | # # individual spec file. 70 | # if config.files_to_run.one? 71 | # # Use the documentation formatter for detailed output, 72 | # # unless a formatter has already been configured 73 | # # (e.g. via a command-line flag). 74 | # config.default_formatter = "doc" 75 | # end 76 | # 77 | # # Print the 10 slowest examples and example groups at the 78 | # # end of the spec run, to help surface which specs are running 79 | # # particularly slow. 80 | # config.profile_examples = 10 81 | # 82 | # Run specs in random order to surface order dependencies. If you find an 83 | # order dependency and want to debug it, you can fix the order by providing 84 | # the seed, which is printed after each run. 85 | # --seed 1234 86 | config.order = :random 87 | 88 | config.filter_gems_from_backtrace *%w[ 89 | selenium-webdriver 90 | ] 91 | 92 | # Seed global randomization in this process using the `--seed` CLI option. 93 | # Setting this allows you to use `--seed` to deterministically reproduce 94 | # test failures related to randomization by passing the same `--seed` value 95 | # as the one that triggered the failure. 96 | Kernel.srand config.seed 97 | end 98 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/support/capybara.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/rspec' 4 | require 'webdrivers/chromedriver' 5 | 6 | Capybara.default_max_wait_time = 2 7 | 8 | Capybara.register_driver :chrome do |app| 9 | capabilities = Selenium::WebDriver::Remote::Capabilities.new( 10 | chromeOptions: { 11 | args: %w[ 12 | no-sandbox 13 | force-renderer-accessibility 14 | ] 15 | } 16 | ) 17 | 18 | Capybara::Selenium::Driver.new( 19 | app, 20 | browser: :chrome, 21 | desired_capabilities: capabilities 22 | ) 23 | end 24 | 25 | Capybara.javascript_driver = :chrome 26 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/support/chrome_kill.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before :suite do 3 | result = `ps aux | grep 'Google Chrome' | grep -v grep`.strip 4 | 5 | unless result.empty? 6 | puts "==> Killing running instance of Chrome, we can't have both running" 7 | system("killall 'Google Chrome'") 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/support/hammerspoon.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.before(:suite) do 5 | # Refresh Hammerspoon 6 | system('killall Hammerspoon 2>/dev/null') 7 | system('open -a Hammerspoon') 8 | 9 | puts 10 | puts '==> Restarted Hammerspoon, sleeping 1 second...' 11 | puts 12 | 13 | sleep 1 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/utils/number_utils_spec.lua: -------------------------------------------------------------------------------- 1 | local numberUtils = require("lib/utils/number_utils") 2 | 3 | describe("numberUtils", function() 4 | describe("#pushDigit", function() 5 | local pushDigit = numberUtils.pushDigit 6 | 7 | it("concats a digit onto 0", function() 8 | assert.are.equals(1, pushDigit(0, 1)) 9 | end) 10 | 11 | it("does nothing when pushing 0 onto 0", function() 12 | assert.are.equals(0, pushDigit(0, 0)) 13 | end) 14 | 15 | it("pushes a digit onto another", function() 16 | assert.are.equals(10, pushDigit(1, 0)) 17 | assert.are.equals(21, pushDigit(2, 1)) 18 | assert.are.equals(105, pushDigit(10, 5)) 19 | assert.are.equals(1239, pushDigit(123, 9)) 20 | end) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/utils/string_utils_spec.lua: -------------------------------------------------------------------------------- 1 | local stringUtils = require("lib/utils/string_utils") 2 | 3 | describe("stringUtils", function() 4 | describe("#findPrevIndex", function() 5 | it("should find the prev occurrence of a character", function() 6 | local str = "12345" 7 | local index = stringUtils.findPrevIndex(str, "2", 5) 8 | 9 | assert.are.equals(2, index) 10 | end) 11 | 12 | it("should return nil if it doesn't find it", function() 13 | local str = "12345" 14 | local index = stringUtils.findPrevIndex(str, "a") 15 | 16 | assert.are.equals(nil, index) 17 | end) 18 | end) 19 | 20 | describe("#findNextIndex", function() 21 | it("should find the next occurrence of a character", function() 22 | local str = "12345" 23 | local index = stringUtils.findNextIndex(str, "5") 24 | 25 | assert.are.equals(5, index) 26 | end) 27 | 28 | it("should return nil if it doesn't find it", function() 29 | local str = "12345" 30 | local index = stringUtils.findNextIndex(str, "6") 31 | 32 | assert.are.equals(nil, index) 33 | end) 34 | end) 35 | end) 36 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/utils/table_spec.lua: -------------------------------------------------------------------------------- 1 | local tableUtils = require("lib/utils/table") 2 | 3 | describe("tableUtils", function() 4 | describe("#matches", function() 5 | local matches = tableUtils.matches 6 | 7 | it("returns true when two tables match", function() 8 | assert.are.equals( 9 | true, 10 | matches({'cmd'}, {'cmd'}) 11 | ) 12 | end) 13 | 14 | it("returns true when two tables are empty", function() 15 | assert.are.equals( 16 | true, 17 | matches({}, {}) 18 | ) 19 | end) 20 | 21 | it("returns true when two tables equal but out of order", function() 22 | assert.are.equals( 23 | true, 24 | matches({'shift', 'cmd'}, {'cmd', 'shift'}) 25 | ) 26 | end) 27 | 28 | it("returns false when two tables are not equal", function() 29 | assert.are.equals( 30 | false, 31 | matches({'cmd'}, {'cmd', 'shift'}) 32 | ) 33 | end) 34 | end) 35 | end) 36 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/spec/utils/visual_spec.lua: -------------------------------------------------------------------------------- 1 | local visualUtils = require("lib/utils/visual") 2 | 3 | local getNewRange = visualUtils.getNewRange 4 | 5 | describe("visual utils", function() 6 | describe("#getNewRange", function() 7 | it("can handle going to the left from a left cursor position", function() 8 | local currentRange = { start = 3, finish = 8 } 9 | local motionRange = { start = 0, finish = 3 } 10 | local caretPosition = 3 11 | 12 | assert.are.same( 13 | { 14 | caretPosition = 0, 15 | range = { start = 0, finish = 8 } 16 | }, 17 | getNewRange(currentRange, motionRange, caretPosition, "pause") 18 | ) 19 | end) 20 | 21 | it("can handle going to the right from a left cursor position", function() 22 | local currentRange = { start = 0, finish = 5 } 23 | local motionRange = { start = 0, finish = 1 } 24 | local caretPosition = 0 25 | 26 | assert.are.same( 27 | { 28 | caretPosition = 1, 29 | range = { start = 1, finish = 5 } 30 | }, 31 | getNewRange(currentRange, motionRange, caretPosition) 32 | ) 33 | end) 34 | 35 | it("can handle going to the left from a right cursor position", function() 36 | local currentRange = { start = 0, finish = 5 } 37 | local motionRange = { start = 3, finish = 5 } 38 | local caretPosition = 5 39 | 40 | assert.are.same( 41 | { 42 | caretPosition = 3, 43 | range = { start = 0, finish = 3 } 44 | }, 45 | getNewRange(currentRange, motionRange, caretPosition) 46 | ) 47 | end) 48 | 49 | it("can handle going to the right from a right cursor position", function() 50 | local currentRange = { start = 0, finish = 5 } 51 | local motionRange = { start = 5, finish = 8 } 52 | local caretPosition = 5 53 | 54 | assert.are.same( 55 | { 56 | caretPosition = 8, 57 | range = { start = 0, finish = 8 } 58 | }, 59 | getNewRange(currentRange, motionRange, caretPosition) 60 | ) 61 | end) 62 | 63 | it("can handle the start of the buffer", function() 64 | local currentRange = { start = 0, finish = 0 } 65 | local motionRange = { start = 0, finish = 5 } 66 | local caretPosition = 0 67 | 68 | assert.are.same( 69 | { 70 | caretPosition = 5, 71 | range = { start = 0, finish = 5 } 72 | }, 73 | getNewRange(currentRange, motionRange, caretPosition) 74 | ) 75 | end) 76 | 77 | it("can handle the end of the buffer", function() 78 | local currentRange = { start = 33, finish = 33 } 79 | local motionRange = { start = 16, finish = 33 } 80 | local caretPosition = 33 81 | 82 | assert.are.same( 83 | { 84 | caretPosition = 16, 85 | range = { start = 16, finish = 33 } 86 | }, 87 | getNewRange(currentRange, motionRange, caretPosition) 88 | ) 89 | end) 90 | 91 | it("can handle a beginning of line movement", function() 92 | local currentRange = { start = 16, finish = 33 } 93 | local motionRange = { start = 0, finish = 33 } 94 | local caretPosition = 16 95 | 96 | assert.are.same( 97 | { 98 | caretPosition = 0, 99 | range = { start = 0, finish = 33 } 100 | }, 101 | getNewRange(currentRange, motionRange, caretPosition) 102 | ) 103 | end) 104 | 105 | it("can cancel out a linewise movement", function() 106 | local currentRange = { start = 28, finish = 62 } 107 | local motionRange = { start = 28, finish = 62 } 108 | local caretPosition = 28 109 | 110 | assert.are.same( 111 | { 112 | caretPosition = 62, 113 | range = { start = 62, finish = 62 } 114 | }, 115 | getNewRange(currentRange, motionRange, caretPosition) 116 | ) 117 | end) 118 | end) 119 | end) 120 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/hs/_asm/axuielement/internal.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/VimMode.spoon/vendor/hs/_asm/axuielement/internal.so -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/hs/_asm/axuielement/internal.so.dSYM/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleIdentifier 8 | com.apple.xcode.dsym.internal.so 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundlePackageType 12 | dSYM 13 | CFBundleSignature 14 | ???? 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1 19 | 20 | 21 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/hs/_asm/axuielement/internal.so.dSYM/Contents/Resources/DWARF/internal.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/VimMode.spoon/vendor/hs/_asm/axuielement/internal.so.dSYM/Contents/Resources/DWARF/internal.so -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8.lua: -------------------------------------------------------------------------------- 1 | local prequire = dofile(vimModeScriptPath .. "lib/utils/prequire.lua") 2 | 3 | -- Try to load it from luarocks, otherwise require the vendored version. 4 | local luautf8 = prequire("lua-utf8") or require("luautf8.lua-utf8") 5 | 6 | return luautf8 7 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/.gitattributes: -------------------------------------------------------------------------------- 1 | *.h linguist-language=C 2 | 3 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/.gitignore: -------------------------------------------------------------------------------- 1 | UCD/ 2 | UCD.*/ 3 | ucd/ 4 | ucd.*/ 5 | *.dll 6 | 7 | lua-utf8.so 8 | lua-utf8.so.* 9 | luautf8-*.zip 10 | luautf8-*.rock 11 | 12 | *.gcov 13 | *.gcda 14 | *.gcno 15 | 16 | test_*.lua 17 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: false 3 | 4 | env: 5 | global: 6 | - LUAROCKS=2.4.3 7 | - ROCKSPEC=rockspecs/luautf8-scm-0.rockspec 8 | matrix: 9 | - LUA="lua 5.1" 10 | - LUA="lua 5.2" 11 | - LUA="lua 5.3" 12 | - LUA="luajit 2.0" 13 | - LUA="luajit 2.1" 14 | 15 | branches: 16 | only: 17 | - master 18 | - develop 19 | 20 | before_install: 21 | - pip install --user hererocks urllib3[secure] cpp-coveralls 22 | - hererocks env --$LUA -rlatest # Use latest LuaRocks, install into 'env' directory. 23 | - source env/bin/activate # Add directory with all installed binaries to PATH. 24 | 25 | install: 26 | # - sudo luarocks make $ROCKSPEC CFLAGS="-O2 -fPIC -ftest-coverage -fprofile-arcs" LIBFLAG="-shared --coverage" 27 | - luarocks make $ROCKSPEC CFLAGS="-O3 -fPIC -Wall -Wextra --coverage" LIBFLAG="-shared --coverage" 28 | 29 | script: 30 | - lua test.lua 31 | - lua test_pm.lua 32 | - lua test_compat.lua 33 | # - lunit.sh test.lua 34 | 35 | after_success: 36 | - coveralls 37 | # - coveralls -b .. -r .. --dump c.report.json 38 | # - luacov-coveralls -j c.report.json -v 39 | 40 | notifications: 41 | email: 42 | on_success: change 43 | on_failure: always 44 | 45 | # vim: ft=yaml nu et sw=2 fdc=2 fdm=syntax 46 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Xavier Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/lutf8lib.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/Spoons/VimMode.spoon/vendor/luautf8/lutf8lib.o -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/rockspecs/luautf8-0.1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "luautf8" 2 | version = "0.1.0-1" 3 | 4 | source = { 5 | url = "https://github.com/starwing/luautf8/archive/0.1.0.tar.gz", 6 | dir = "luautf8-0.1.0" 7 | } 8 | 9 | description = { 10 | summary = "A UTF-8 support module for Lua", 11 | detailed = [[ 12 | This module adds UTF-8 support to Lua. It's compatible with Lua "string" module. 13 | ]], 14 | homepage = "http://github.com/starwing/luautf8", 15 | license = "MIT", 16 | } 17 | 18 | dependencies = { 19 | "lua >= 5.1" 20 | } 21 | 22 | build = { 23 | type = "builtin", 24 | modules = { 25 | ["lua-utf8"]= "lutf8lib.c" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/rockspecs/luautf8-0.1.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "luautf8" 2 | version = "0.1.1-1" 3 | source = { 4 | url = "https://github.com/starwing/luautf8/archive/0.1.1.tar.gz", 5 | dir = "luautf8-0.1.1" 6 | } 7 | description = { 8 | summary = "A UTF-8 support module for Lua", 9 | detailed = [[ 10 | This module adds UTF-8 support to Lua. It's compatible with Lua "string" module. 11 | ]], 12 | homepage = "http://github.com/starwing/luautf8", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua >= 5.1" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["lua-utf8"] = "lutf8lib.c" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/rockspecs/luautf8-0.1.2-2.rockspec: -------------------------------------------------------------------------------- 1 | package = "luautf8" 2 | version = "0.1.2-2" 3 | source = { 4 | url = "https://github.com/starwing/luautf8/archive/0.1.2.tar.gz", 5 | dir = "luautf8-0.1.2" 6 | } 7 | description = { 8 | summary = "A UTF-8 support module for Lua", 9 | detailed = [[ 10 | This module adds UTF-8 support to Lua. It's compatible with Lua "string" module. 11 | ]], 12 | homepage = "http://github.com/starwing/luautf8", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua >= 5.1" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["lua-utf8"] = "lutf8lib.c" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/rockspecs/luautf8-0.1.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "luautf8" 2 | version = "0.1.3-1" 3 | source = { 4 | url = "https://github.com/starwing/luautf8/archive/0.1.3.tar.gz", 5 | dir = "luautf8-0.1.3" 6 | } 7 | description = { 8 | summary = "A UTF-8 support module for Lua", 9 | detailed = [[ 10 | This module adds UTF-8 support to Lua. It's compatible with Lua "string" module. 11 | ]], 12 | homepage = "http://github.com/starwing/luautf8", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua >= 5.1" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["lua-utf8"] = "lutf8lib.c" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Spoons/VimMode.spoon/vendor/luautf8/rockspecs/luautf8-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "luautf8" 2 | version = "scm-0" 3 | 4 | source = { 5 | url = "git://github.com/starwing/luautf8" 6 | } 7 | 8 | description = { 9 | summary = "A UTF-8 support module for Lua", 10 | detailed = [[ 11 | This module adds UTF-8 support to Lua. It's compatible with Lua "string" module. 12 | ]], 13 | homepage = "http://github.com/starwing/luautf8", 14 | license = "MIT", 15 | } 16 | 17 | dependencies = { 18 | "lua >= 5.1" 19 | } 20 | 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | ["lua-utf8"]= "lutf8lib.c" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Spoons/VolumeScroll.spoon/init.lua: -------------------------------------------------------------------------------- 1 | --- === VolumeScroll === 2 | --- 3 | --- Use mouse scroll wheel and modifiers to adjust volume. 4 | --- 5 | --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/VolumeScroll.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/VolumeScroll.spoon.zip) 6 | 7 | local obj={} 8 | obj.__index = obj 9 | 10 | -- Metadata 11 | obj.name = "VolumeScroll" 12 | obj.version = "1.0" 13 | obj.author = "Garth Mortensen (voldemortensen)" 14 | obj.twitter = "@voldemortensen" 15 | obj.github = "@voldemortensen" 16 | obj.homepage = "https://github.com/Hammerspoon/Spoons" 17 | obj.license = "MIT - https://opensource.org/licenses/MIT" 18 | 19 | --- VolumeScroll:init() 20 | --- Method 21 | --- Initialize spoon 22 | --- 23 | --- Parameters: 24 | --- 25 | --- Returns: 26 | --- * void 27 | function obj:init() 28 | self.modifiers = hs.eventtap.event.newScrollEvent({0,0}, {'alt'}) 29 | self.flags = self.modifiers:getFlags() 30 | end 31 | 32 | --- VolumeScroll:start() 33 | --- Method 34 | --- Start event watcher. 35 | --- 36 | --- Parameters: 37 | --- * mods - a table containing the modifiers to bind in scrolling 38 | --- 39 | --- Returns: 40 | --- * void 41 | function obj:start(mods) 42 | if mods ~= nil and type(mods) == 'table' then 43 | self.modifiers = hs.eventtap.event.newScrollEvent({0,0}, mods) 44 | self.flags = self.modifiers:getFlags() 45 | end 46 | 47 | self.scrollWatcher = hs.eventtap.new({hs.eventtap.event.types.scrollWheel}, function(event) 48 | local currentMods = event:getFlags() 49 | if self:sameMods(currentMods) then 50 | local direction = event:getProperty(hs.eventtap.event.properties.scrollWheelEventFixedPtDeltaAxis1) 51 | local device = hs.audiodevice.current() 52 | if direction > 0 then 53 | if device.volume < 100 then 54 | device.device:setOutputVolume(device.volume + 1) 55 | end 56 | elseif direction < 0 then 57 | if device.volume > 0 then 58 | device.device:setOutputVolume(device.volume - 1) 59 | end 60 | end 61 | return true 62 | end 63 | return false 64 | end) 65 | 66 | self.scrollWatcher:start() 67 | end 68 | 69 | --- VolumeScroll:stop() 70 | --- Method 71 | --- Stop the scroll watcher 72 | --- 73 | --- Parameters: 74 | --- 75 | --- Returns: 76 | --- * void 77 | function obj:stop() 78 | self.scrollWatcher:stop() 79 | end 80 | 81 | --- VolumeScroll:sameMods() 82 | --- Method 83 | --- Determine if a table of modifiers are the same modifiers passed into :start() 84 | --- 85 | --- Parameters: 86 | --- * mods - a table of modifiers 87 | --- 88 | --- Returns: 89 | --- * boolean - true if mods are same, false otherwise 90 | function obj:sameMods(mods) 91 | if type(mods) ~= type(self.flags) then return false end 92 | if type(mods) ~= 'table' then return false end 93 | if self:tableLength(mods) ~= self:tableLength(self.flags) then return false end 94 | 95 | for key1, value1 in pairs(mods) do 96 | local value2 = self.flags[key1] 97 | if value1 ~= value2 then 98 | return false 99 | end 100 | end 101 | 102 | return true 103 | end 104 | 105 | --- VolumeScroll:tableLength(T) 106 | --- Method 107 | --- Determine the number of items in a table 108 | --- 109 | --- Parameters: 110 | --- * T - a table 111 | --- 112 | --- Returns: 113 | --- * number or boolean - the number of items in the table, false if T is not a table 114 | function obj:tableLength(T) 115 | if type(T) == 'table' then 116 | local count = 0 117 | for _ in pairs(T) do count = count + 1 end 118 | return count 119 | else 120 | return false 121 | end 122 | end 123 | 124 | return obj 125 | -------------------------------------------------------------------------------- /config-example.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------------------------------- 2 | 3 | -- author: zuorn 4 | -- mail: zuorn@qq.com 5 | -- github: https://github.com/zuorn/hammerspoon_config 6 | 7 | ---------------------------------------------------------------------------------------------------- 8 | 9 | ----------------------------------------- 配 置 文 件 ----------------------------------------------- 10 | 11 | ---------------------------------------------------------------------------------------------------- 12 | 13 | --指定要启用的模块 14 | hspoon_list = { 15 | "AClock", 16 | "ClipShow", 17 | "CountDown", 18 | "KSheet", 19 | "WinWin", 20 | "VolumeScroll", 21 | "PopupTranslateSelection", 22 | "DeepLTranslate" 23 | -- "HSaria2" 24 | -- "HSearch" 25 | --"SpeedMenu", 26 | -- "MountedVolumes", 27 | -- "HeadphoneAutoPause", 28 | } 29 | 30 | ---------------------------------------------------------------------------------------------------- 31 | ----------------------------------------- 快速启动配置 ---------------------------------------------- 32 | 33 | -- 绑定 启动 app 快捷键 34 | 35 | hsapp_list = { 36 | {key = 'a', name = 'Atom'}, 37 | {key = 'c', id = 'com.google.Chrome'}, 38 | {key = 'e', name = '印象笔记'}, 39 | {key = 'f', name = 'Finder'}, 40 | {key = 'i', name = 'iTerm'}, 41 | {key = 'j', name = 'Typora'}, 42 | {key = 'o', name = 'Obsidian'}, 43 | {key = 'k', name = 'Keynote'}, 44 | {key = 's', name = 'Sublime Text'}, 45 | {key = 'p', name = 'Podcasts'}, 46 | {key = 't', name = 'Terminal'}, 47 | -- {key = 'v', id = 'com.apple.ActivityMonitor'}, 48 | {key = 'v', id = 'vsCode.app'}, 49 | {key = 'm', name = 'Mweb'}, 50 | {key = 'w', name = 'WeChat'}, 51 | {key = 'x', name = '迅雷 2'}, 52 | {key = 'y', id = 'com.apple.systempreferences'}, 53 | } 54 | 55 | 56 | ---------------------------------------------------------------------------------------------------- 57 | ---------------------------------------- 模式快捷键绑定 ---------------------------------------------- 58 | 59 | -- 窗口提示键绑定,快速切换到你想要的窗口上 60 | hswhints_keys = {"alt", "tab"} 61 | 62 | -- 快速启动面板快捷键绑定 63 | hsappM_keys = {"alt", "A"} 64 | 65 | -- 系统剪切板快捷键绑定 66 | hsclipsM_keys = {"alt", "C"} 67 | 68 | 69 | -- 在默认浏览器中打开 Hammerspoon 和 Spoons API 手册 70 | --hsman_keys = {"alt", "H"} 71 | 72 | -- 倒计时快捷键绑定 73 | hscountdM_keys = {"alt", "I"} 74 | 75 | -- 锁定电脑快捷键绑定 76 | --hslock_keys = {"alt", "L"} 77 | 78 | -- 窗口自定义大小及位置快捷键绑定 79 | hsresizeM_keys = {"alt", "R"} 80 | 81 | -- 定义应用程序快捷键面板快捷键 82 | hscheats_keys = {"alt", "S"} 83 | 84 | -- 显示时钟快捷键绑定 85 | hsaclock_keys = {"alt", "T"} 86 | 87 | -- 粘贴 chrome 或 safari 打开最前置的网址 88 | hstype_keys = {"alt", "V"} 89 | 90 | -- 显示 Hammerspoon 控制台 91 | hsconsole_keys = {"alt", "Z"} 92 | 93 | -- 显示 MountedVolumes 94 | hstype_keys = {"alt", "M"} 95 | 96 | -- 显示搜索 97 | hsearch_keys = {"alt", "G"} 98 | 99 | ---------------------------------------------------------------------------------------------------- 100 | --------------------------------- hammerspoon 快捷键绑定配置 ----------------------------------------- 101 | 102 | -- 临时禁用所有快捷键(注意:只能手动接禁。) 103 | hsupervisor_keys = {{"cmd", "shift", "ctrl"}, "Q"} 104 | 105 | -- 重新加载配置文件 106 | hsreload_keys = {{"cmd", "shift", "ctrl"}, "R"} 107 | 108 | -- 显示各种模式绑定快捷键 109 | hshelp_keys = {{"alt", "shift"}, "/"} 110 | 111 | 112 | ---------------------------------------------------------------------------------------------------- 113 | ---------------------------------------------- end ------------------------------------------------ 114 | ---------------------------------------------------------------------------------------------------- 115 | -------------------------------------------------------------------------------- /images/appm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/images/appm.png -------------------------------------------------------------------------------- /images/cipshow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/images/cipshow.png -------------------------------------------------------------------------------- /images/countdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/images/countdown.png -------------------------------------------------------------------------------- /images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/images/help.png -------------------------------------------------------------------------------- /images/ksheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/images/ksheet.png -------------------------------------------------------------------------------- /images/winwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuorn/hammerspoon_config/460ba080ca47b838c2b3552ab77f2591bb98d01f/images/winwin.png -------------------------------------------------------------------------------- /private/config.lua: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------------------------------- 2 | 3 | -- author: zuorn 4 | -- mail: zuorn@qq.com 5 | -- github: https://github.com/zuorn/hammerspoon_config 6 | 7 | ---------------------------------------------------------------------------------------------------- 8 | 9 | ----------------------------------------- 配 置 文 件 ----------------------------------------------- 10 | 11 | ---------------------------------------------------------------------------------------------------- 12 | 13 | --指定要启用的模块 14 | hspoon_list = { 15 | "AClock", 16 | "ClipShow", 17 | "CountDown", 18 | "KSheet", 19 | "WinWin", 20 | "VolumeScroll", 21 | "PopupTranslateSelection", 22 | -- "DeepLTranslate" 23 | -- "HSaria2" 24 | -- "HSearch" 25 | --"SpeedMenu", 26 | -- "MountedVolumes", 27 | -- "HeadphoneAutoPause", 28 | } 29 | 30 | ---------------------------------------------------------------------------------------------------- 31 | ----------------------------------------- 快速启动配置 ---------------------------------------------- 32 | 33 | -- 绑定 启动 app 快捷键 34 | 35 | hsapp_list = { 36 | {key = 'a', name = 'Alacritty'}, 37 | {key = 'c', id = 'com.google.Chrome'}, 38 | {key = 'e', name = '印象笔记'}, 39 | {key = 'f', name = 'Finder'}, 40 | {key = 'i', name = 'kitty'}, 41 | {key = 'j', name = 'Typora'}, 42 | {key = 'o', name = 'Obsidian'}, 43 | {key = 'k', name = 'Keynote'}, 44 | {key = 's', name = 'Sublime Text'}, 45 | {key = 'p', name = 'Podcasts'}, 46 | {key = 't', name = 'Terminal'}, 47 | {key = 'v', id = 'com.apple.ActivityMonitor'}, 48 | {key = 'b', id = 'vsCode.app'}, 49 | {key = 'm', name = 'Mweb'}, 50 | {key = 'w', name = 'WeChat'}, 51 | {key = 'x', name = 'Thunder'}, 52 | {key = 'y', id = 'com.apple.systempreferences'}, 53 | } 54 | 55 | 56 | ---------------------------------------------------------------------------------------------------- 57 | ---------------------------------------- 模式快捷键绑定 ---------------------------------------------- 58 | 59 | -- 窗口提示键绑定,快速切换到你想要的窗口上 60 | hswhints_keys = {"alt", "tab"} 61 | 62 | -- 快速启动面板快捷键绑定 63 | hsappM_keys = {"alt", "A"} 64 | 65 | -- 系统剪切板快捷键绑定 66 | hsclipsM_keys = {"alt", "C"} 67 | 68 | 69 | -- 在默认浏览器中打开 Hammerspoon 和 Spoons API 手册 70 | --hsman_keys = {"alt", "H"} 71 | 72 | -- 倒计时快捷键绑定 73 | hscountdM_keys = {"alt", "I"} 74 | 75 | -- 锁定电脑快捷键绑定 76 | --hslock_keys = {"alt", "L"} 77 | 78 | -- 窗口自定义大小及位置快捷键绑定 79 | hsresizeM_keys = {"alt", "R"} 80 | 81 | -- 定义应用程序快捷键面板快捷键 82 | hscheats_keys = {"alt", "S"} 83 | 84 | -- 显示时钟快捷键绑定 85 | hsaclock_keys = {"alt", "w"} 86 | 87 | -- 粘贴 chrome 或 safari 打开最前置的网址 88 | hstype_keys = {"alt", "V"} 89 | 90 | -- 显示 Hammerspoon 控制台 91 | hsconsole_keys = {"alt", "Z"} 92 | 93 | -- 显示 MountedVolumes 94 | hstype_keys = {"alt", "M"} 95 | 96 | -- 显示搜索 97 | hsearch_keys = {"alt", "G"} 98 | 99 | ---------------------------------------------------------------------------------------------------- 100 | --------------------------------- hammerspoon 快捷键绑定配置 ----------------------------------------- 101 | 102 | -- 临时禁用所有快捷键(注意:只能手动接禁。) 103 | hsupervisor_keys = {{"cmd", "shift", "ctrl"}, "Q"} 104 | 105 | -- 重新加载配置文件 106 | hsreload_keys = {{"cmd", "shift", "ctrl"}, "R"} 107 | 108 | -- 显示各种模式绑定快捷键 109 | hshelp_keys = {{"alt", "shift"}, "/"} 110 | 111 | 112 | ---------------------------------------------------------------------------------------------------- 113 | ---------------------------------------------- end ------------------------------------------------ 114 | ---------------------------------------------------------------------------------------------------- 115 | --------------------------------------------------------------------------------