knu / Knu.spoon

A Spoon package of my Hammerspoon modules, mainly for keyboard customization
BSD 2-Clause "Simplified" License
51 stars 2 forks source link

Overlapping set of keychords #2

Open kiryph opened 2 years ago

kiryph commented 2 years ago

I try to adapt Key chords to apply commonly used styles to Inkscape objects on macOS.

The list of key chords are shown in this graphic

default-styles-names2

The author has published his key chord setup on github https://github.com/gillescastel/inkscape-shortcut-manager for linux.

I have already managed to adapt the creation of svg snippets for pasting. The remaining issue is that defining a large overlapping set of key chords using https://github.com/knu/hs-knu/blob/master/chord.lua does not work as expected. Only a subset works.

knu = require("knu")

-- Function to guard a given object from GC
guard = knu.runtime.guard

-- Keychords (48x)
keychords = {}
-- work
keychords[1]  = {"f", "space"}
keychords[2]  = {"w", "space"}
keychords[3]  = {"b", "space"}
keychords[4]  = {"s", "space"}
keychords[5]  = {"d", "space"}
keychords[6]  = {"e", "space"}
-- do not work
keychords[7]  = {"s", "f"}
keychords[8]  = {"d", "f"}
keychords[9]  = {"e", "f"}
keychords[10] = {"s", "w"}
keychords[11] = {"d", "w"}
keychords[12] = {"e", "w"}
keychords[13] = {"s", "g"}
keychords[14] = {"d", "g"}
keychords[15] = {"e", "g"}
keychords[16] = {"s", "f", "g"}
keychords[17] = {"d", "f", "g"}
keychords[18] = {"e", "f", "g"}
keychords[19] = {"s", "w", "g"}
keychords[20] = {"d", "w", "g"}
keychords[21] = {"e", "w", "g"}
keychords[22] = {"s", "v"}
keychords[23] = {"d", "v"}
keychords[24] = {"e", "v"}
keychords[25] = {"s", "f", "v"}
keychords[26] = {"d", "f", "v"}
keychords[27] = {"e", "f", "v"}
keychords[28] = {"s", "w", "v"}
keychords[29] = {"d", "w", "v"}
keychords[30] = {"e", "w", "v"}
keychords[31] = {"s", "a"}
keychords[32] = {"d", "a"}
keychords[33] = {"e", "a"}
keychords[34] = {"s", "a", "v"}
keychords[35] = {"d", "a", "v"}
keychords[36] = {"e", "a", "v"}
keychords[37] = {"s", "a", "g"}
keychords[38] = {"d", "a", "g"}
keychords[39] = {"e", "a", "g"}
keychords[40] = {"s", "x"}
keychords[41] = {"d", "x"}
keychords[42] = {"e", "x"}
keychords[43] = {"s", "x", "v"}
keychords[44] = {"d", "x", "v"}
keychords[45] = {"e", "x", "v"}
keychords[46] = {"s", "x", "g"}
keychords[47] = {"d", "x", "g"}
keychords[48] = {"e", "x", "g"}

knuchords = {}
for k, v in pairs(keychords) do
    knuchords[k] = knu.chord.new({}, v, function() print(hs.inspect.inspect(v)); end, 0.5)
end

local InkscapeWF = hs.window.filter.new("Inkscape")

InkscapeWF
    :subscribe(hs.window.filter.windowFocused, function()
        print("starting keychords")
        for k,v in pairs(knuchords) do
            guard(v:start())
        end
    end)
    :subscribe(hs.window.filter.windowUnfocused, function()
        print("stopping keychords")
        for k,v in pairs(knuchords) do
            guard(v:stop())
        end
    end)

@knu Do you know where the issue is located and how one could resolve it?

knu commented 2 years ago

That's a massive use case of key chords! I can imagine too many active event taps could lead to an unreliable state because the event loop of the operating system has strict time constraints. Under the hood knu.chord creates one event tap for each chord, so it may not scale well.

Instead of using knu.chord, I'd suggest you use Karabiner-Elements for the chord part, mapping simultaneous non-modifier key combinations to modifiers + single keys, and then define them as hotkeys on Hammerspoon.

knu commented 2 years ago

Apart from that, I cannot deny an overlapping set of key chords could not work well, depending on the order of definitions. Each event tap created for a key chord holds key events for a certain amount of time, and when those are accumulated to be more than a timeout of the later chord, it will always fail to form.

This could be fixed by making a giant event tap that would check all defined chords, but that's going to be a lot of work.

kiryph commented 2 years ago

Thanks for your comments.

I can imagine too many active event taps could lead to an unreliable state because the event loop of the operating system has strict time constraints.

I also suspect this.

Karabiner Elements instead of knu.chord

Instead of using knu.chord, I'd suggest you use Karabiner-Elements for the chord part, mapping simultaneous non-modifier key combinations to modifiers + single keys, and then define them as hotkeys on Hammerspoon.

I have tried Karabiner Elements. Karabiner Elements actually offers also to.shell_command so I do not have to remap to modifier + single key. Since I have written the function create_svg_and_paste(keys) for hammerspoon:

~/.hammerspoon/init.lua ```lua -- Helper function local function intersect(m,n) local r={} for i,v1 in ipairs(m) do for k,v2 in pairs(n) do if (v1==v2) then return true end end end return false end -- Helper function local function has_value (tab, val) for index, value in ipairs(tab) do if value == val then return true end end return false end function create_svg_and_paste(keys) print(hs.inspect.inspect(keys)) pt = 1.327 -- pixels w = 1 * pt thick_width = 4 * pt very_thick_width = 8 * pt style = {} style["stroke-opacity"] = 1 if intersect({"s", "a", "d", "g", "h", "x", "e"}, keys) then style["stroke"] = "black" style["stroke-width"] = w style["marker-end"] = "none" style["marker-start"] = "none" style["stroke-dasharray"] = "none" else style["stroke"] = "none" end if has_value(keys, "g") then w = thick_width style["stroke-width"] = w end if has_value(keys, "h") then w = very_thick_width style["stroke-width"] = w end if has_value(keys, "a") then style['marker-end'] = 'url(#marker-arrow-' .. tostring(w) .. ')' end if has_value(keys, "x") then style['marker-start'] = 'url(#marker-arrow-' .. tostring(w) .. ')' style['marker-end'] = 'url(#marker-arrow-' .. tostring(w) .. ')' end if has_value(keys, "d") then style['stroke-dasharray'] = tostring(w) .. ',' .. tostring(2*pt) end if has_value(keys, "e") then style['stroke-dasharray'] = tostring(3*pt) .. ',' .. tostring(3*pt) end if has_value(keys, "f") then style['fill'] = 'black' style['fill-opacity'] = 0.12 end if has_value(keys, "b") then style['fill'] = 'black' style['fill-opacity'] = 1 end if has_value(keys, "w") then style['fill'] = 'white' style['fill-opacity'] = 1 end if intersect(keys, {"f", "b", "w"}) then style['marker-end'] = 'none' style['marker-start'] = 'none' end if not intersect(keys, {"f", "b", "w"}) then style['fill'] = 'none' style['fill-opacity'] = 1 end svg = [[ ]] print(hs.inspect.inspect(style)) -- ENABLE ONLY FOR DEBUGGING if (style['marker-end'] ~= nil and style['marker-end'] ~= 'none') or (style['marker-start'] ~= nil and style['marker-start'] ~= 'none') then svgtemp = [[ ' .. "\n" svgtemp = svgtemp .. ' ' .. "\n" svg = svg .. svgtemp svgtemp = [[ ]] svg = svg .. svgtemp end style_string = '' for key, value in pairs(style) do -- skips nil? style_string = style_string .. key .. ":" .. " " .. value .. ";" end svg = svg .. '' .. "\n" print(svg) -- ENABLE ONLY FOR DEBUGGING hs.pasteboard.writeDataForUTI("dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xu", svg) -- get UTI via https://github.com/sindresorhus/Pasteboard-Viewer hs.eventtap.keyStroke({"shift", "cmd"}, "v") end ```

I will simply call the lua-hammerspoon function from the cli

"shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+␣\");create_svg_and_paste({\"e\",\"space\"});'"

~/.config/karabiner/karabiner.json for the 30 shape key chords ```json "complex_modifications": { "parameters": { "basic.simultaneous_threshold_milliseconds": 200, "basic.to_delayed_action_delay_milliseconds": 500, "basic.to_if_alone_timeout_milliseconds": 1000, "basic.to_if_held_down_threshold_milliseconds": 500, "mouse_motion_to_scroll.speed": 100 }, "rules": [ { "description": "s+space", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "s" }, { "key_code": "spacebar" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"s+␣\");create_svg_and_paste({\"s\",\"space\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+space", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "spacebar" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+␣\");create_svg_and_paste({\"d\",\"space\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+space", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "spacebar" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+␣\");create_svg_and_paste({\"e\",\"space\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "f+space", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "f" }, { "key_code": "spacebar" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"f+␣\");create_svg_and_paste({\"f\",\"space\"});'" } ], "conditions": [ { "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] } ] } ] }, { "description": "w+space", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "w" }, { "key_code": "spacebar" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"w+␣\");create_svg_and_paste({\"w\",\"space\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "b+space", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "b" }, { "key_code": "spacebar" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"b+␣\");create_svg_and_paste({\"b\",\"space\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "s+f", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "s" }, { "key_code": "f" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"s+f\");create_svg_and_paste({\"s\",\"f\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+f", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "f" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+f\");create_svg_and_paste({\"d\",\"f\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+f", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "f" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+f\");create_svg_and_paste({\"e\",\"f\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "s+w", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "w" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+w\");create_svg_and_paste({\"d\",\"w\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+w", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "b" }, { "key_code": "space" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"b+␣\");create_svg_and_paste({\"b\",\"space\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+w", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "w" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+w\");create_svg_and_paste({\"e\",\"w\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "s+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "s" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"s+g\");create_svg_and_paste({\"s\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+g\");create_svg_and_paste({\"d\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+g\");create_svg_and_paste({\"e\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "s+f+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "s" }, { "key_code": "f" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"s+f+g\");create_svg_and_paste({\"s\",\"f\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+f+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "f" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+f+g\");create_svg_and_paste({\"d\",\"f\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+f+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "f" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+f+g\");create_svg_and_paste({\"e\",\"f\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "s+w+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "s" }, { "key_code": "w" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"s+w+g\");create_svg_and_paste({\"s\",\"w\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+w+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "w" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+w+g\");create_svg_and_paste({\"d\",\"w\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+w+g", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "w" }, { "key_code": "g" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+w+g\");create_svg_and_paste({\"e\",\"w\",\"g\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "s+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "s" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"s+h\");create_svg_and_paste({\"s\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+h\");create_svg_and_paste({\"d\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+h\");create_svg_and_paste({\"e\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "s+f+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "s" }, { "key_code": "f" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"s+f+h\");create_svg_and_paste({\"s\",\"f\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+f+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "f" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+f+h\");create_svg_and_paste({\"d\",\"f\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+f+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "f" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+f+h\");create_svg_and_paste({\"e\",\"f\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "s+w+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "s" }, { "key_code": "w" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"s+w+h\");create_svg_and_paste({\"s\",\"w\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "d+w+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "d" }, { "key_code": "w" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"d+w+h\");create_svg_and_paste({\"d\",\"w\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] }, { "description": "e+w+h", "manipulators": [ { "type": "basic", "from": { "simultaneous": [ { "key_code": "e" }, { "key_code": "w" }, { "key_code": "h" } ] }, "to_if_alone": [ { "shell_command": "/usr/local/bin/hs -c 'hs.alert(\"e+w+h\");create_svg_and_paste({\"e\",\"w\",\"h\"});'" } ], "conditions": [{ "type": "frontmost_application_if", "bundle_identifiers": [ "org.inkscape.Inkscape" ] }] } ] } ] }, ... ```

This uses

This is an improvement. So all two-letter combinations work. However, the three-letter combinations do not yet. There are a few options I have not tested comprehensively

          "basic.simultaneous_threshold_milliseconds": 200,
          "basic.to_if_alone_timeout_milliseconds": 1000,

and the simultaneous_options detect_key_down_uninterruptedly.

Single hs.eventtap for all key chords

This could be fixed by making a giant event tap that would check all defined chords, but that's going to be a lot of work.

I think this is what the original author did. And I guess this could also be possible with hammerspoon eventtap.