sleepymalc / VSCode-LaTeX-Inkscape

✍️ A way to integrate LaTeX, VS Code, and Inkscape in macOS
https://pbb.wtf/posts/VSCode-LaTeX-Inkscape/
MIT License
364 stars 24 forks source link

Hammerspoon hs.eventtap #1

Closed kiryph closed 2 years ago

kiryph commented 2 years ago

@sleepymalc Did you have a look at http://www.hammerspoon.org/docs/hs.eventtap.html to replace xlib on macOS?

sleepymalc commented 2 years ago

I'll look into this doc later in detail. But a quick question is, can these APIs be restricted to a certain app? Or does it just replace the key globally?

sleepymalc commented 2 years ago

Hi there, I have looked into hammerspoon and its functionality provided, but it seems that it can't perform the functionality we want by using xlib. If I missed something welcome to re-open the issues and discuss with me, thanks!

kiryph commented 2 years ago

... can these APIs be restricted to a certain app?

Yes, it is possible to change keys only for a specific app. See

  1. http://www.hammerspoon.org/go/#winfilters
  2. https://stackoverflow.com/q/63795560
kiryph commented 2 years ago

I have tried it myself and came up with following hammerspoon init.lua using https://github.com/knu/hs-knu/blob/master/chord.lua for key chords:

~/.hammerspoon/init.lua ```lua knu = require("knu") -- Function to guard a given object from GC guard = knu.runtime.guard -- Helper function (lua is not python) 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 (lua is not python) local function has_value (tab, val) for index, value in ipairs(tab) do if value == val then return true end end return false end -- Helper function (lua is not python) function tableHasKey(table,key) return table[key] ~= nil end local function create_svg_and_paste(keys) print(hs.inspect.inspect(keys)) pt = 1.327 -- pixels w = 1 * pt thick_width = 2 * pt very_thick_width = 4 * 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 (tableHasKey(style, 'marker-end') and style['marker-end'] ~= 'none') or (tableHasKey(style, 'marker-start') 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 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 -- Keychords (48x) keychord_strings = {} keychord_strings[1] = {"f", "space"} keychord_strings[2] = {"w", "space"} keychord_strings[3] = {"b", "space"} keychord_strings[4] = {"s", "space"} keychord_strings[5] = {"d", "space"} keychord_strings[6] = {"e", "space"} keychord_strings[7] = {"s", "f"} keychord_strings[8] = {"d", "f"} keychord_strings[9] = {"e", "f"} keychord_strings[10] = {"s", "w"} keychord_strings[11] = {"d", "w"} keychord_strings[12] = {"e", "w"} keychord_strings[13] = {"s", "g"} keychord_strings[14] = {"d", "g"} keychord_strings[15] = {"e", "g"} keychord_strings[16] = {"s", "f", "g"} keychord_strings[17] = {"d", "f", "g"} keychord_strings[18] = {"e", "f", "g"} keychord_strings[19] = {"s", "w", "g"} keychord_strings[20] = {"d", "w", "g"} keychord_strings[21] = {"e", "w", "g"} keychord_strings[22] = {"s", "v"} keychord_strings[23] = {"d", "v"} keychord_strings[24] = {"e", "v"} keychord_strings[25] = {"s", "f", "v"} keychord_strings[26] = {"d", "f", "v"} keychord_strings[27] = {"e", "f", "v"} keychord_strings[28] = {"s", "w", "v"} keychord_strings[29] = {"d", "w", "v"} keychord_strings[30] = {"e", "w", "v"} keychord_strings[31] = {"s", "a"} keychord_strings[32] = {"d", "a"} keychord_strings[33] = {"e", "a"} keychord_strings[34] = {"s", "a", "v"} keychord_strings[35] = {"d", "a", "v"} keychord_strings[36] = {"e", "a", "v"} keychord_strings[37] = {"s", "a", "g"} keychord_strings[38] = {"d", "a", "g"} keychord_strings[39] = {"e", "a", "g"} keychord_strings[40] = {"s", "x"} keychord_strings[41] = {"d", "x"} keychord_strings[42] = {"e", "x"} keychord_strings[43] = {"s", "x", "v"} keychord_strings[44] = {"d", "x", "v"} keychord_strings[45] = {"e", "x", "v"} keychord_strings[46] = {"s", "x", "g"} keychord_strings[47] = {"d", "x", "g"} keychord_strings[48] = {"e", "x", "g"} keychords = {} for k,v in pairs(keychord_strings) do keychords[k] = knu.chord.new({}, v, function() create_svg_and_paste(v); end, 0.5) end -- Initialize an inkscape window filter -- https://stackoverflow.com/q/63795560 local InkscapeWF = hs.window.filter.new("Inkscape") -- Subscribe to when your Inkscape window is focused and unfocused InkscapeWF :subscribe(hs.window.filter.windowFocused, function() print("starting keychords") for k,v in pairs(keychords) do v:start() end end) :subscribe(hs.window.filter.windowUnfocused, function() print("stopping keychords") for k,v in pairs(keychords) do v:stop() end end) ```

Install the used knu.chord as following:

$ git clone https://github.com/knu/hs-knu ~/.hammerspoon/knu

Main credits go to Gilles Castel and his https://github.com/gillescastel/inkscape-shortcut-manager from where most code was adapted to lua.

Reference Card for Key Chords

As reference for the key chords I add the original picture from https://castel.dev/post/lecture-notes-2/ but with the key chords included in the picture.

default-styles-names2

Missing Key Chords

I did not add the “ergonomic” rebindings x, w, f, and shift+z. This should be possible in Inkscape itself.

This setup also misses the bindings t, shift+t, a, shift+a, s, and shift+s. Since I encountered issues I did not pursue these.

Issues

I also use yabai. yabai signals could be helpful for the first issue. Possibly the second issue can be resolved with a different implementation of keychords compared to knu.chords.

Currently, I have no plans to push this further. If someone else finds/has found a stable solution for macOS, I would be happy to hear about it.

References

Versions

sleepymalc commented 2 years ago

Ok, it seems to me that this is at least a useable solution. I need to dive into it and see how can I fix those issues you mentioned. That may need some time, I'll update what I get if I have any breakthroughs.

kiryph commented 2 years ago

The author of knu.chord has suggested to use Karabiner Elements for capturing the overlapping key chords. I have come up with following complex_modifications for Karabiner Elements using a jsonnet file.

karabiner-inkscape.jsonnet ```jsonnet local arr = [ [x, y, z] // 24x for x in ['s', 'd', 'e'] // solid, dotted, dashed for y in ['f', 'w', 'a', 'x'] // gray, white, arrow, double-arrow for z in ['g', 'v'] // thick, very thick ] + [ [x, y] // 18x for x in ['s', 'd', 'e'] // solid, dotted, dashed for y in ['f', 'w', 'g', 'v', 'a', 'x'] ] + [ ['spacebar', x] // 6x for x in ['s', 'd', 'e', 'f', 'b', 'w'] // solid, dotted, dashed, gray, black, white ]; { title: 'Apply quickly essential shape or line styles in Inkscape using hammerspoon (Gilles Castel, 2019)', rules: [ { description: 'Apply quickly essential shape or line styles in Inkscape using hammerspoon (Gilles Castel, 2019)', manipulators: [ { local str = std.join('+', el), type: 'basic', from: { simultaneous: [{ key_code: i } for i in el], simultaneous_options: { key_up_when: 'all', }, }, to: [{ shell_command: "/usr/local/bin/hs -c 'hs.alert(\"" + str + '");create_svg_and_paste({"' + std.join('","', el) + "\"});'" }], conditions: [{ type: 'frontmost_application_if', bundle_identifiers: ['org.inkscape.Inkscape'] }], } for el in arr ], }, ], } ```

The jsonnet tool can be installed via $ brew install jsonnet.

Converting the .jsonnet file into the json file for Karabiner Elements can be done as follwoing

$ jsonnet karabiner-inkscape.jsonnet > ~/.config/karabiner/assets/complex_modifications/karabiner-inkscape.json

Then enable in Karabiner Elements UI the complex modifications. Screenshot 2022-01-27 at 17 31 35

Configuring Hammerspoon

  1. Open the Hammerspoon console and run hs.ipc.cliInstall() to install the cli command hs.
  2. Add to your ~/.hammerspoon/init.lua following:
~/.hammerspoon/init.lua ```lua -- Make cli command `hs` available: -- After an update of hammerspoon run following two commmands once in the hammerspoon console -- hs.ipc.cliUninstall(); hs.ipc.cliInstall() require("hs.ipc") 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 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) pt = 1.327 -- pixels w = 2 * 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, "v") 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 = [[ ]] 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" hs.pasteboard.writeDataForUTI("dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xu", svg) -- get UTI via https://github.com/sindresorhus/Pasteboard-Viewer hs.eventtap.keyStroke({"shift", "cmd"}, "v") end ```

Versions

sleepymalc commented 2 years ago

Cool, I'll certainly look into this. Are there any known bugs for this?

Btw, since I'm now going to move on to a bigger project, I apologize that I can't discuss the detailed implementation and configuration for Inkscape with you. Just a spoiler, I'm planning on integrating LaTeX with all these drawing tools with a more modern UI and faster renderer into a single IDE, so we can then all relax and not worry about these weird workaround after this.

kiryph commented 2 years ago

Are there any known bugs for this?

Not that I am aware of. However, it is not battle tested.

Just a spoiler, I'm planning on integrating LaTeX with all these drawing tools with a more modern UI and faster renderer into a single IDE, so we can then all relax and not worry about these weird workaround after this.

I am looking forward hearing about it.

Also thank you for creating this repository.

I will close this issue even though a solution with hs.eventtap could still be done. E.g. this would reduce the number of required tools: I have now three background programs with overlapping feature set:

  1. skhd for Yabai
  2. Hammerspoon for stackline and Inkscape style pasting and
  3. Karabiner Elements for the Inkscape key chords.
sleepymalc commented 2 years ago

Just out of curiosity, since I'm thinking about trying your implementation for usability (and also for my own convenience), I'm here for some detailed implementation.

It seems that after following all the configurations you mentioned, the keybinding doesn't work as expected. For now, I have installed

  1. Hammerspoon
  2. Karabiner(which I'm using for quite a long time for re-mapping already)
  3. pull the knu repo

Seems to me that you're using Yabai for defining keybinding? Since I'm using Amethyst, if this is the case, I can try Yabai instead.

kiryph commented 2 years ago

Most likely the NSPasteboard UTI is different. Since Inkscape does not define them, macOS chooses dynamical UTI strings "dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xu". INCORRECT (see following comments)

Can you create a simple rectangle in Inkscape and copy it to the clipboard: Screenshot 2022-02-17 at 10 07 08

Without putting anything on the clipboard (i.e. copy following snippet to the terminal before you copy the rectangle), run on the command line (requires $ brew install jq)

hs -c "hs.json.encode(hs.pasteboard.readAllData(), true)" | jq 'to_entries[] | select( .value | contains("<inkscape:clipboard") ) | .key'

which returns for me

"dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xw425trz2he3pxsrw0k"
"dyn.ah62d4rv4gu81k3p2su11a5dbrf1a"
"dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xu"
"dyn.ah62d4rv4gu80c6durvy0g2pyrf106p52fz7gw6a"

I suspect the dynamic UTIs are all different for you. INCORRECT (see following comments)

If you want to the see the content in the different pasteboards, run

hs -c "hs.json.encode(hs.pasteboard.readAllData(), true)" | jq 'to_entries[] | select( .value | contains("<inkscape:clipboard") ) | [.key, .value]'

So you have to change the following line in the lua init: INCORRECT (see following comments)

hs.pasteboard.writeDataForUTI("dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xu", svg)
sleepymalc commented 2 years ago

Yup, what I have is the following:

"dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xu"
"dyn.ah62d4rv4gu81k3p2su11a5dbrf1a"
"dyn.ah62d4rv4gu80c6durvy0g2pyrf106p52fz7gw6a"
"dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xw425trz2he3pxsrw0k"

So basically, I just change that line with the third (corresponds to what you put in that line) dynamic UTIs?

Updates I can run it successfully. I'll test it for several days and try to put them in this repo. Or if you think you can create a pull request and make your own contribution (as I think it's more appropriate since you create this by yourself and I don't like to steal others' works), feel free to do it. If you think putting them nicely is quite time-consuming, you can simply put all the necessary files into the inkscape folder, and I'll try to write some nice documentation about this.

kiryph commented 2 years ago

I can run it successfully.

I am glad to hear this.

I compared the dynamic UTIs from your and mine post. They are actually identical only the order is different:

❯ cat a.txt
"dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xw425trz2he3pxsrw0k"
"dyn.ah62d4rv4gu81k3p2su11a5dbrf1a"
"dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xu"
"dyn.ah62d4rv4gu80c6durvy0g2pyrf106p52fz7gw6a"

❯ cat b.txt
"dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xu"
"dyn.ah62d4rv4gu81k3p2su11a5dbrf1a"
"dyn.ah62d4rv4gu80c6durvy0g2pyrf106p52fz7gw6a"
"dyn.ah62d4rv4gu80w5pbq7ww88brrf1g065dqf2gnppxs3xw425trz2he3pxsrw0k"

❯ diff -s <(sort a.txt) <(sort b.txt)
Files /dev/fd/11 and /dev/fd/18 are identical

So my assumption that the dynamic UTIs are actually arbitrary values set by macOS, when Inkscape is run or a copy is done for the first time, is incorrect. This is actually good news: the script should work fine on different systems without having to change the value.


I'll test it for several days and try to put them in this repo. Or if you think you can create a pull request and make your own contribution.

Right now I do not have the time to create a PR with a well written explanation with figures. So I do not mind when you put it into this repo (if it successfully works for you).

Currently, I tend to fallback to create tikz pictures which are time consuming as you know due to habit and the desire to make it perfect. Hopefully with a more productive Inkscape setup I will stop investing too much time in tikz pictures.

(as I think it's more appropriate since you create this by yourself and I don't like to steal others' works)

Do not worry. I was happy to find your repo and was interested in a macOS solution myself. If there would have been a solution, I have to admit that I probably would have used it right away.

sleepymalc commented 2 years ago

Ok, then I'll try to doc this as detailed as I can, and I'm glad that this works well without bugs. The issue is reopened as an enhancement now, I'll close it after I did all the documentation.

Best.

sleepymalc commented 2 years ago

I finally have time to document this setup, and I think it's good to have you as one of the collaborators since this is basically from you!