adds ticked modifier and normal key handling
This commit is contained in:
parent
9c8afbbf69
commit
aaa533161a
|
@ -85,6 +85,7 @@ vscgo.COLOR_CLOSEPANEL = vscgo.COLOR_CLOSEPANEL or vscgo.RED
|
|||
|
||||
-- AND NOW FOR SOMETHING DIFFERENT: THE REAL MEAT --
|
||||
|
||||
-- luacov: disable
|
||||
function vscgo.command(cmd)
|
||||
mb.tap("P", keybow.LEFT_SHIFT, keybow.LEFT_CTRL)
|
||||
keybow.sleep(100)
|
||||
|
@ -95,6 +96,7 @@ end
|
|||
function vscgo.go_test_package(_)
|
||||
vscgo.command("go test package")
|
||||
end
|
||||
-- luacov: enable
|
||||
|
||||
-- luacheck: ignore 631
|
||||
vscgo.keymap = {
|
||||
|
|
|
@ -58,3 +58,128 @@ function mb.tap_times(key, times, ...)
|
|||
if modifier then keybow.set_modifier(modifier, keybow.KEY_UP) end
|
||||
end
|
||||
end
|
||||
|
||||
-- Tick mappers execute a series of tick'ered function calls, passing in each
|
||||
-- one of a sequence of things with each tick until done. So, this is like
|
||||
-- array mappers, but this time they're broken into discrete ticks.
|
||||
local TickMapper = {}
|
||||
TickMapper.__index = TickMapper
|
||||
mb.TickMapper = TickMapper
|
||||
|
||||
-- Creates a new tick mapper that calls a function on every element of an
|
||||
-- array, but only one call per tick.
|
||||
--
|
||||
-- luacheck: ignore 212/self
|
||||
function TickMapper:new(func, ...)
|
||||
local slf = {
|
||||
func=func, -- the function to call for each element.
|
||||
elements={...}, -- the elements to map onto function calls.
|
||||
idx=1,
|
||||
len=#{...}
|
||||
}
|
||||
return setmetatable(slf, TickMapper)
|
||||
end
|
||||
|
||||
-- For each tick, process only the next element in our list until all elements
|
||||
-- have been processed. Then indicate finish.
|
||||
function TickMapper:process()
|
||||
-- Don't wonder why we shield this, but this way we also correctly handle
|
||||
-- the border case of an empty list of elements to map without crashing.
|
||||
if self.idx <= self.len then
|
||||
self.func(self.elements[self.idx])
|
||||
end
|
||||
-- Update index to next element and indicate back whether we'll need a
|
||||
-- further round in the future.
|
||||
self.idx = self.idx + 1
|
||||
return self.idx <= self.len
|
||||
end
|
||||
|
||||
-- Adds a set of modifiers to be either pressed or released to the queue of
|
||||
-- tick'ed key operations, so in each tick only one modifier will be pressed
|
||||
-- or released. The state parameter should be either keybow.KEY_DOWN or
|
||||
-- keybow.KEY_UP. The final variable args is/are the modifier(s) to be pressed
|
||||
-- or released.
|
||||
function mb.addmodifiers(after, state, ...)
|
||||
mb.addkeyticker(
|
||||
TickMapper:new(
|
||||
function(mod) keybow.set_modifier(mod, state) end,
|
||||
...
|
||||
),
|
||||
after)
|
||||
end
|
||||
|
||||
-- Adds a sequence of key presses to the queue of tick'ed key operations,
|
||||
-- optionally enclosed by modifier presses and releases.
|
||||
function mb.addkeys(after, keys, ...)
|
||||
local modsno = #{...}
|
||||
-- First queue ticked modifier press(es) if necessary.
|
||||
if modsno > 0 then
|
||||
mb.addmodifiers(after, keybow.KEY_DOWN, ...)
|
||||
after = 0
|
||||
end
|
||||
-- For convenience, explode a single keys string parameter into its
|
||||
-- individual characters as an array.
|
||||
if type(keys) == "string" then
|
||||
local keysarr = {}
|
||||
for idx = 1, #keys do
|
||||
keysarr[idx] = keys:sub(idx, idx)
|
||||
end
|
||||
keys = keysarr
|
||||
end
|
||||
-- Queue the keys to tap in a sequence of ticks.
|
||||
mb.addkeyticker(
|
||||
TickMapper:new(
|
||||
function(key) keybow.tap_key(key) end,
|
||||
table.unpack(keys)
|
||||
),
|
||||
after)
|
||||
-- And finally queue to release the modifier keys if necessary.
|
||||
if modsno > 0 then
|
||||
mb.addmodifiers(0, keybow.KEY_UP, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- All key tickers are handled in a (singly linked) list, as only the first
|
||||
-- key ticker can be active and getting processed. Only after the first one
|
||||
-- has finished, it is removed, and the next (now new) key ticker gets
|
||||
-- processed. And so on...
|
||||
local firstkey = nil
|
||||
local lastkey = nil
|
||||
-- Key tickers can be initially delayed as necessary.
|
||||
local keyafter = 0
|
||||
|
||||
-- Adds another asynchronous USB HID key operation at the end of the key
|
||||
-- queue, waiting to be processed piecemeal-wise tick by tick.
|
||||
function mb.addkeyticker(keyop, afterms)
|
||||
keyop.afterms = afterms or 0
|
||||
if lastkey then
|
||||
lastkey.next = keyop
|
||||
end
|
||||
keyop.next = nil
|
||||
lastkey = keyop
|
||||
if not firstkey then
|
||||
-- Queue was empty before, so we need to kick it off...
|
||||
firstkey = keyop
|
||||
keyafter = mb.now + keyop.afterms
|
||||
end
|
||||
end
|
||||
|
||||
-- Key tick(ing) handler responsible to work on the current key operation as
|
||||
-- well as on the asynchronous key operations queue tick by tick. Since key
|
||||
-- operations are always in sequence as added, there is no point in using a
|
||||
-- (min) priority queue here. Instead, we roll our own very basic and
|
||||
-- low-profile single-linked list.
|
||||
function mb.tickkey(t)
|
||||
-- something in the queue (still) to be processed?
|
||||
if firstkey and t >= keyafter then
|
||||
if not firstkey:process() then
|
||||
-- as the current key operation has finished, so prepare the next
|
||||
-- key operation for the next round, optionally delaying it the
|
||||
-- span requested.
|
||||
firstkey = firstkey.next
|
||||
if firstkey then
|
||||
keyafter = mb.now + firstkey.afterms
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
-- Multibow internal "module" handling Keybow's tick events.
|
||||
|
||||
--[[
|
||||
Copyright 2019 Harald Albrecht
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]--
|
||||
|
||||
-- luacheck: globals mb tick
|
||||
|
||||
|
||||
-- Tick gets called by the Keybow "firmware" every 1ms (or so). If any timers
|
||||
-- have gone off by now, then call their timer user functions. Keybow's tick
|
||||
-- ms counter has its epoch set when the Keybow Lua scripting started (so it's
|
||||
-- not a *nix timestamp or such). In addition to timers, we also handle queued
|
||||
-- ticking objects here: these are always processed in sequence, and each such
|
||||
-- ticking object may consume multiple ticks until it has finished its job.
|
||||
function tick(t)
|
||||
mb.now = t
|
||||
while true do
|
||||
local next, tim = mb.timers:peek()
|
||||
if next == nil or t < next then
|
||||
break
|
||||
end
|
||||
mb.timers:remove()
|
||||
tim:trigger()
|
||||
end
|
||||
mb.tickkey(t)
|
||||
end
|
|
@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
]]--
|
||||
|
||||
-- luacheck: globals mb tick
|
||||
-- luacheck: globals mb
|
||||
|
||||
-- Our timer queue ... is nothing more than a priority queue.
|
||||
mb.timers = mb.pq:new()
|
||||
|
@ -100,19 +100,3 @@ function mb.every(everyms, timerf, ...)
|
|||
shim.tim = tim
|
||||
return tim
|
||||
end
|
||||
|
||||
-- Tick gets called by the Keybow "firmware" every 1ms (or so). If any timers
|
||||
-- have gone off by now, then call their timer user functions. Keybow's tick
|
||||
-- ms counter has its epoch set when the Keybow Lua scripting started (so it's
|
||||
-- not a *nix timestamp or such).
|
||||
function tick(t)
|
||||
mb.now = t
|
||||
while true do
|
||||
local next, tim = mb.timers:peek()
|
||||
if next == nil or t < next then
|
||||
break
|
||||
end
|
||||
mb.timers:remove()
|
||||
tim:trigger()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,6 +32,7 @@ require "keybow"
|
|||
mb.path = (...):match("^(.-)[^%/]+$")
|
||||
|
||||
mb.pq = require(mb.path .. "mb/prioqueue")
|
||||
require(mb.path .. "mb/ticker")
|
||||
require(mb.path .. "mb/timer")
|
||||
require(mb.path .. "mb/morekeys")
|
||||
require(mb.path .. "mb/keymaps")
|
||||
|
|
|
@ -67,3 +67,69 @@ describe("multibow keys", function()
|
|||
end)
|
||||
|
||||
end)
|
||||
|
||||
describe("asynchronous keys", function()
|
||||
|
||||
local tt = require("spec/snippets/ticktock")
|
||||
|
||||
it("map a function on a ticking element sequence", function()
|
||||
local s = stub.new()
|
||||
local tm1 = mb.TickMapper:new(s, 1, 2, 3)
|
||||
local t = stub.new()
|
||||
local tm2 = mb.TickMapper:new(t, 42)
|
||||
|
||||
mb.addkeyticker(tm1, 20)
|
||||
mb.addkeyticker(tm2, 100)
|
||||
|
||||
-- "empty tick", as the tick mapper is yet delayed...
|
||||
tt.ticktock(10)
|
||||
assert.stub(s).was.Not.called()
|
||||
|
||||
-- should process all elements of tm1, but none of tm2...
|
||||
tt.ticktock(30)
|
||||
assert.stub(s).was.called(3)
|
||||
assert.stub(s).was.called.With(1)
|
||||
assert.stub(s).was.called.With(2)
|
||||
assert.stub(s).was.called.With(3)
|
||||
s:clear()
|
||||
tt.ticktock(20)
|
||||
assert.stub(s).was.called(0)
|
||||
assert.stub(t).was.Not.called()
|
||||
|
||||
-- should now process all elements of tm2, too.
|
||||
tt.ticktock(100)
|
||||
assert.stub(s).was.Not.called()
|
||||
assert.stub(t).was.called(1)
|
||||
assert.stub(t).was.called.With(42)
|
||||
end)
|
||||
|
||||
it("tick modifiers", function()
|
||||
local s = spy.on(keybow, "set_modifier")
|
||||
mb.addmodifiers(0, keybow.KEY_DOWN, keybow.LEFT_CTRL, keybow.LEFT_SHIFT)
|
||||
|
||||
tt.ticktock(50)
|
||||
assert.spy(s).was.called(2)
|
||||
assert.spy(s).was.called.With(keybow.LEFT_CTRL, keybow.KEY_DOWN)
|
||||
assert.spy(s).was.called.With(keybow.LEFT_SHIFT, keybow.KEY_DOWN)
|
||||
end)
|
||||
|
||||
it("ticks keys", function()
|
||||
local sm = spy.on(keybow, "set_modifier")
|
||||
local sk = spy.on(keybow, "tap_key")
|
||||
mb.addkeys(0, "abc", keybow.LEFT_CTRL, keybow.LEFT_SHIFT)
|
||||
|
||||
tt.ticktock(100)
|
||||
-- note that the modifiers were pressed AND released by now...
|
||||
assert.spy(sm).was.called(4)
|
||||
assert.spy(sm).was.called.With(keybow.LEFT_CTRL, keybow.KEY_DOWN)
|
||||
assert.spy(sm).was.called.With(keybow.LEFT_SHIFT, keybow.KEY_DOWN)
|
||||
assert.spy(sm).was.called.With(keybow.LEFT_CTRL, keybow.KEY_UP)
|
||||
assert.spy(sm).was.called.With(keybow.LEFT_SHIFT, keybow.KEY_UP)
|
||||
|
||||
assert.spy(sk).was.called(3)
|
||||
assert.spy(sk).was.called.With("a")
|
||||
assert.spy(sk).was.called.With("b")
|
||||
assert.spy(sk).was.called.With("c")
|
||||
end)
|
||||
|
||||
end)
|
||||
|
|
Loading…
Reference in New Issue