├── Spoons
├── VimMode.spoon
│ ├── docs.json
│ ├── .gitignore
│ ├── .rspec
│ ├── lib
│ │ ├── utils
│ │ │ ├── visible_range.lua
│ │ │ ├── set.lua
│ │ │ ├── times.lua
│ │ │ ├── find_first.lua
│ │ │ ├── number_utils.lua
│ │ │ ├── prequire.lua
│ │ │ ├── benchmark.lua
│ │ │ ├── table.lua
│ │ │ ├── version.lua
│ │ │ ├── browser.lua
│ │ │ ├── keys.lua
│ │ │ ├── visual.lua
│ │ │ ├── ax.lua
│ │ │ ├── log.lua
│ │ │ └── string_utils.lua
│ │ ├── motions
│ │ │ ├── noop.lua
│ │ │ ├── current_selection.lua
│ │ │ ├── left.lua
│ │ │ ├── right.lua
│ │ │ ├── line_beginning.lua
│ │ │ ├── first_line.lua
│ │ │ ├── up.lua
│ │ │ ├── down.lua
│ │ │ ├── till_before_search.lua
│ │ │ ├── till_after_search.lua
│ │ │ ├── last_line.lua
│ │ │ ├── forward_search.lua
│ │ │ ├── backward_search.lua
│ │ │ ├── entire_line.lua
│ │ │ ├── first_non_blank.lua
│ │ │ ├── line_end.lua
│ │ │ ├── in_word.lua
│ │ │ ├── between_chars.lua
│ │ │ ├── big_word.lua
│ │ │ ├── word.lua
│ │ │ ├── back_word.lua
│ │ │ └── end_of_word.lua
│ │ ├── strategy.lua
│ │ ├── operators
│ │ │ ├── change.lua
│ │ │ ├── delete.lua
│ │ │ ├── yank.lua
│ │ │ └── replace.lua
│ │ ├── hot_patcher.lua
│ │ ├── operator.lua
│ │ ├── axuielement.lua
│ │ ├── motion.lua
│ │ ├── screen_dimmer.lua
│ │ ├── selection.lua
│ │ ├── wait_for_char.lua
│ │ ├── config.lua
│ │ ├── focus_watcher.lua
│ │ ├── command_state.lua
│ │ ├── strategies
│ │ │ └── keyboard_strategy.lua
│ │ ├── contextual_modal
│ │ │ └── registry.lua
│ │ ├── block_cursor.lua
│ │ ├── app_watcher.lua
│ │ ├── state.lua
│ │ ├── key_sequence.lua
│ │ └── contextual_modal.lua
│ ├── vendor
│ │ ├── luautf8
│ │ │ ├── .gitattributes
│ │ │ ├── lutf8lib.o
│ │ │ ├── .gitignore
│ │ │ ├── rockspecs
│ │ │ │ ├── luautf8-scm-0.rockspec
│ │ │ │ ├── luautf8-0.1.1-1.rockspec
│ │ │ │ ├── luautf8-0.1.2-2.rockspec
│ │ │ │ ├── luautf8-0.1.3-1.rockspec
│ │ │ │ └── luautf8-0.1.0-1.rockspec
│ │ │ ├── LICENSE
│ │ │ └── .travis.yml
│ │ ├── hs
│ │ │ └── _asm
│ │ │ │ └── axuielement
│ │ │ │ ├── internal.so
│ │ │ │ └── internal.so.dSYM
│ │ │ │ └── Contents
│ │ │ │ ├── Resources
│ │ │ │ └── DWARF
│ │ │ │ │ └── internal.so
│ │ │ │ └── Info.plist
│ │ └── luautf8.lua
│ ├── .busted
│ ├── .luacheckrc
│ ├── images
│ │ └── vim-mode.gif
│ ├── Rakefile
│ ├── Gemfile
│ ├── init.lua
│ ├── spec
│ │ ├── support
│ │ │ ├── chrome_kill.rb
│ │ │ ├── hammerspoon.rb
│ │ │ └── capybara.rb
│ │ ├── spec_helper.lua
│ │ ├── features
│ │ │ ├── big_word_spec.rb
│ │ │ ├── delete_word_spec.rb
│ │ │ ├── visual_mode_spec.rb
│ │ │ ├── delete_line_spec.rb
│ │ │ └── linewise_spec.rb
│ │ ├── fixtures
│ │ │ └── textarea.html
│ │ ├── config_spec.lua
│ │ ├── utils
│ │ │ ├── number_utils_spec.lua
│ │ │ ├── table_spec.lua
│ │ │ ├── string_utils_spec.lua
│ │ │ └── visual_spec.lua
│ │ ├── selection_spec.lua
│ │ ├── operators
│ │ │ ├── delete_spec.lua
│ │ │ └── replace_spec.lua
│ │ ├── motions
│ │ │ ├── big_word_spec.lua
│ │ │ ├── line_beginning_spec.lua
│ │ │ ├── end_of_word_spec.lua
│ │ │ ├── between_chars_spec.lua
│ │ │ ├── line_end_spec.lua
│ │ │ ├── in_word_spec.lua
│ │ │ ├── back_word_spec.lua
│ │ │ └── word_spec.lua
│ │ ├── buffer_spec.lua
│ │ └── spec_helper.rb
│ ├── .travis.yml
│ ├── bin
│ │ └── dev-setup
│ ├── docs
│ │ └── Integration_Tests.md
│ ├── CHANGELOG.md
│ └── Gemfile.lock
├── HSearch.spoon
│ ├── resources
│ │ ├── tabs.png
│ │ ├── time.png
│ │ ├── v2ex.png
│ │ ├── chrome.png
│ │ ├── emoji.png
│ │ ├── menus.png
│ │ ├── safari.png
│ │ ├── youdao.png
│ │ ├── justnote.png
│ │ ├── taskkill.png
│ │ └── thesaurus.png
│ ├── hs_v2ex.lua
│ ├── hs_datamuse.lua
│ ├── docs.json
│ ├── hs_btabs.lua
│ ├── hs_note.lua
│ ├── hs_time.lua
│ ├── hs_emoji.lua
│ └── hs_yddict.lua
├── SpeedMenu.spoon
│ ├── docs.json
│ └── init.lua
├── AClock.spoon
│ ├── init.lua
│ └── docs.json
├── KSheet.spoon
│ └── docs.json
├── VolumeScroll.spoon
│ └── init.lua
└── CountDown.spoon
│ └── init.lua
├── .DS_Store
├── images
├── appm.png
├── help.png
├── cipshow.png
├── ksheet.png
├── winwin.png
└── countdown.png
├── README.md
├── config-example.lua
└── private
└── config.lua
/Spoons/VimMode.spoon/docs.json:
--------------------------------------------------------------------------------
1 | [
2 |
3 | ]
--------------------------------------------------------------------------------
/Spoons/VimMode.spoon/.gitignore:
--------------------------------------------------------------------------------
1 | luacov.stats.out
2 |
--------------------------------------------------------------------------------
/Spoons/VimMode.spoon/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 | --color
3 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/.DS_Store
--------------------------------------------------------------------------------
/Spoons/VimMode.spoon/lib/utils/visible_range.lua:
--------------------------------------------------------------------------------
1 | local VisibleRange = {}
2 |
--------------------------------------------------------------------------------
/Spoons/VimMode.spoon/vendor/luautf8/.gitattributes:
--------------------------------------------------------------------------------
1 | *.h linguist-language=C
2 |
3 |
--------------------------------------------------------------------------------
/images/appm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/images/appm.png
--------------------------------------------------------------------------------
/images/help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/images/help.png
--------------------------------------------------------------------------------
/images/cipshow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/images/cipshow.png
--------------------------------------------------------------------------------
/images/ksheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/images/ksheet.png
--------------------------------------------------------------------------------
/images/winwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/images/winwin.png
--------------------------------------------------------------------------------
/images/countdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/images/countdown.png
--------------------------------------------------------------------------------
/Spoons/VimMode.spoon/.busted:
--------------------------------------------------------------------------------
1 | return {
2 | _all = {
3 | helper = "spec/spec_helper.lua"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/Spoons/VimMode.spoon/.luacheckrc:
--------------------------------------------------------------------------------
1 | std = {
2 | globals = {"vimModeScriptPath"}
3 | read_globals = {}
4 | }
5 |
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/tabs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/tabs.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/time.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/v2ex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/v2ex.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/chrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/chrome.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/emoji.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/menus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/menus.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/safari.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/safari.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/youdao.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/youdao.png
--------------------------------------------------------------------------------
/Spoons/VimMode.spoon/images/vim-mode.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/VimMode.spoon/images/vim-mode.gif
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/justnote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/justnote.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/taskkill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/taskkill.png
--------------------------------------------------------------------------------
/Spoons/HSearch.spoon/resources/thesaurus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/HSearch.spoon/resources/thesaurus.png
--------------------------------------------------------------------------------
/Spoons/VimMode.spoon/vendor/luautf8/lutf8lib.o:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/VimMode.spoon/vendor/luautf8/lutf8lib.o
--------------------------------------------------------------------------------
/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/vendor/hs/_asm/axuielement/internal.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/VimMode.spoon/vendor/hs/_asm/axuielement/internal.so
--------------------------------------------------------------------------------
/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/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/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/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/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/hs/_asm/axuielement/internal.so.dSYM/Contents/Resources/DWARF/internal.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zuorn/hammerspoon_config/HEAD/Spoons/VimMode.spoon/vendor/hs/_asm/axuielement/internal.so.dSYM/Contents/Resources/DWARF/internal.so
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/spec/fixtures/textarea.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test fixture with textarea
4 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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/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/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/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/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/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/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/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/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/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/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/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/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/.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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | 
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 | 
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 | 
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 | 
63 |
64 | 注:如设置程序坞自动隐藏请修改 `/Users/zuorn/.hammerspoon/Spoons/WinWin.spoon/init.lua.bak` 为`init.lua`
65 |
66 | ### 应用快速切换
67 |
68 | 按下前缀键 `Option` + `tab` 显示窗口提示,按下对应应用显示的字母快速切换。
69 | 
70 |
71 |
72 |
73 |
74 | ### KSheet - 展示应用快捷键
75 |
76 | 按下快捷键 `Option` + `s` 展示当前应用快捷键,按 `q` 或者 `esc` 退出。
77 |
78 | 
79 |
80 |
81 | ### 快速启动
82 |
83 | 按下快捷键 `Option` + `a` 打开快速启动,按下对应字母快速打开应用。
84 |
85 | 
86 |
87 |
88 | ### AClock - 显示当前时间
89 |
90 | 按下 `Option` + `t` 显示当前时间。
91 |
92 | 
93 |
94 |
95 | ### 倒计时(番茄钟)
96 |
97 | 按下 `Option` + `i` 打开倒计时面板,按下对应数字开始计时。
98 |
99 | * 使用 `空格` 可暂停/恢复倒计时。
100 |
101 | 
102 |
103 |
104 | ### clipshowM 剪切板
105 |
106 | 按下 `Option` + `c` 打开剪切板面板。
107 |
108 | 
109 |
110 | 功能:
111 |
112 | * 保存会话
113 | * 恢复上一个会话
114 | * 在浏览器中打开
115 | * 使用百度搜索
116 | * 使用谷歌搜索
117 | * 保存到桌面
118 | * 使用 github 搜索
119 | * 在 Sublime Text 打开
120 |
121 | ### 顶部实时显示网速
122 |
123 | 没有模式快捷键,默认开启。
124 |
125 | 
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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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_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/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/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/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/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 |
--------------------------------------------------------------------------------