New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Creating Text Expander using Hammerspoon #1042
Comments
|
What do you mean by You might get close by using Something along the lines of using a hotkey to begin an eventtap watcher for any key press until another hotkey is pressed, which disables the watcher, then using all of the key strokes entered to look up replacement text or a function to execute... that should be possible. |
|
You should also look at |
|
@asmagill : I believe going with eventtap watcher will be a good idea compared to hs.chooser. Yet I am not sure of how to set up an eventtap watcher in hammerspoon, (since I couldn't see the same in http://www.hammerspoon.org/docs/hs.eventtap.html if am not wrong) . Could you please help me on this? |
|
This is a first pass at something along the lines of what I think your wanting... I've not tested it myself yet, so use this as a starting point only. At a minimum it probably needs the following tests and/or additions to make it "resiliant" to unexpected behaviors:
I may revisit this myself if I get a chance in the next week or so. It's an interesting idea that I hadn't thought to try and I'm not aware of anyone else who has either... I'm curious to see where you go with it, so if you get a chance to post a followup with your version or if you have a github repository of your own that you stick it in eventually, I'd love to see what you do with it. keywords = {
["date"] = function() return os.date("%B %d, %Y") end,
["name"] = "my name is MISTER",
}
expander = hs.hotkey.bind({"alt"}, "d", nil, function() -- don't start watching until the keyUp -- don't want to capture an "extra" key at the begining
local what = ""
local keyMap = require"hs.keycodes".map -- shorthand... in a formal implementation, I'd do the same for all `hs.XXX` references, but that's me
local keyWatcher
keyWatcher = hs.eventtap.new({ hs.eventtap.event.types.keyUp, hs.eventtap.event.types.keyDown }, function(ev)
local keyCode = ev:getKeyCode()
local eventType = ev:getType()
if ev:getFlags().cmd then -- it's part of an application hotkey -- abort!
keyWatcher:stop()
return false
end
if eventType == hs.eventtap.event.types.keyDown then
-- these might end capturing, so handle them on the key down since it comes first
if keyCode == keyMap["return"] then
keyWatcher:stop()
local output = keywords[what]
if type(output) == "function" then
local _, o = pcall(output)
if not _ then
print("~~ expansion for '" .. what .. "' gave an error of " .. o)
-- could also set o to nil here so that the expansion doesn't occur below, but I think
-- seeing the error as the replacement will be a little more obvious that a print to the
-- console which I may or may not have open at the time...
-- maybe show an alert with hs.alert instead?
end
output = o
end
if output then
-- based on the value in `what`, delete over what was typed in and replace it with whatever we want
for i = 1, utf8.len(what), 1 do hs.eventtap.keyStroke({}, "delete") end
hs.eventtap.keyStrokes(output)
end
return true -- don't pass the "return" keystroke on
elseif keyCode == keyMap["escape"] then
keyWatcher:stop()
return true -- don't pass the "escape" keystroke on
elseif keyCode == keyMap["left"] or keyCode == keyMap["up"] or keyCode == keyMap["down"] or keyCode == keyMap["right"] then -- should others be in here?
keyWatcher:stop()
return false -- pass these on
elseif keyCode == keyMap["delete"] and #what == 0 then -- if what is empty then delete will exit the capture
keyWatcher:stop()
return true -- don't pass the "delete" keystroke on in this case
end
elseif eventType == hs.eventtap.event.types.keyUp then
if keyCode == keyMap["delete"] then
if #what > 0 then
-- while `what = what:sub(1, #what)` is simpler, it would choke on utf8 characters... so we do this
local t = {}
for p, c in utf8.codes(what) do table.insert(t, c) end
table.remove(t, #t) -- pop off the last one
what = utf8.char(table.unpack(t))
else
-- shouldn't be possible with the test in keyDown, but I've been wrong before, so just in case...
keyWatcher:stop()
return true
end
else
local c = ev:getCharacters() -- are we sure this will always return nil if it's not a "printable" character?
if c then what = what .. c end
end
end
-- if we get here, we've either already captured what we wanted/needed or we don't recognize it;
-- either way, pass the event on to the focused application for its own use
return false
end):start()
end)edit: forgot the |
|
@asmagill Appreciate the help. Love to see the code doing what it needs to. I would love to see in future the hammerspoon come up with a pre-built module supporting this features, where user can assign the key and abbreviation and let Hammerspoon takes all the scenes behind. I made a very little tweak to the above code, where I removed the keycode for "left" and "right" and add it to allowables. -- codes missing -- -- codes missing -- In addition to this, I have also added a timer of 8 seconds which will automatically stop the keywatcher in case we failed to (which could be in the case as you mentioned when we change the window or application or anything) hs.timer.doAfter(8, function() Let me explore on the possible use cases and tweaks needed, create a github repo and share with community the same. |
|
so considering things like TextExpander, they typically don't have a shortcut to activate them, they are just running their EventTaps all the time, so you could just do the same, and keep a buffer of the most recently typed keystrokes, and test the buffer each time a key is pressed, to see if you've matched any of the replacement shortcuts, then emit enough backspaces to remove the shortcut, then emit the keys to type the replacement text. |
|
I'm not sure if this Issue needs to stay open - I think HS already has everything it needs to do text expansion. If someone wants to write it up into a nice little module, we can look at including it, and if the performance turns out to be terrible, we can look at doing the work in C. What do people think? |
|
Looking for the same thing! Coming from windows which I use autohotkey. I can define convenient text replace like 'abc' -> 'whateveriwant', this is simply missing in Mac. |
|
@atjshop it's not missing, it's built into the OS, see System Preferences -> Keyboard -> Text. This issue is about whether or not Hammerspoon should also have such a feature, and my contention is that we already have enough basic functionality for someone to write such a feature in pure Lua :) Since nobody has disagreed since October, I'm going to close this issue out. |
|
That one does not work at lot of places, even not in chrome, slack and sublime etc. Autohotkey in Windows works everywhere, I miss the convenience |
|
@atjshop that is a fair point. Given that, your choices are buy TextExpander, or write an equivalent in Lua in your Hammerspoon config, then share it with the world for glory and fame :D |
|
I also implement something that can expand the text I typed. One thing I can't figure out how to achieve in hammerspoon is to get what character I actually insert into the textfield. |
|
@Ninlives I wish I had a good answer for you here, but I'm afraid I know very little about how the non-latin input methods actually work. It's possible that @knu would know though :) (and on the general topic of this issue - now would be a super good time for someone to write a generalised text-replacement script for Hammerspoon, because we have just added support for Lua based plugins - Spoons. I would be very happy to help anyone who wants to do it) |
|
I think we don't need to care about how the input method work, just watch the characters changed in the focused uielement 'AXTextField' or 'AXTextArea'. Since the focused uielement is easy to get in hammerspoon, I think this may be easier to make. Not very familiar with Apple's Cocoa API, maybe try to find the solution in the coming holiday :P |
|
@asmagill Thanks for the code snippet! It's working fine for me and solves my current needs for text expansion. A couple of questions:
|
|
Found the answer to my second question - add 0 as the delay value for for i = 1, utf8.len(what), 1 do hs.eventtap.keyStroke({}, "delete") endto: for i = 1, utf8.len(what), 1 do hs.eventtap.keyStroke({}, "delete", 0) endAlso added as a gist. |
|
Hey, @amitkot , thanks for sharing this! Do you mind explaining how to use it straight away? I just found out about Hammerspoon and plan to be looking deeper into the docs soon, but right now I want to setup the Text Expansion really quickly to get some work done. I have downloaded the app and created the I'm typing Would you mind giving a quick explanation on how to get it to work? |
|
Hey, guys, I was able to figure out how to use the script posted by @amitkot but it didn't fit my use case. So I gave it a try and adapted the code to my own way of using text expander.
One known (and accepted) limitation of this version is that the expansion only triggers if you type your word after pressing "Enter" or "Space" or "Up/Down/Left/Right" because the string buffer is cleared after each of these keys (or obviously if you type it for the first time after launching your HS config - meaning the buffer will be clean). One feature it has is that you can delete characters you may have mistyped and correct it and the expansion will work. You can find my snippet at: https://gist.github.com/fmaiabatista/672e543dae72bcd24dae2885cf7f4e88 For convenience: Funnily enough I'm posting this exactly one month later after discovering HS was able to support text expansion. 😄 |
|
Couple of comments on what otherwise looks pretty useful:
If your intention is that Otherwise, nice implementation which I may end up using myself! |
|
It would be cool to have that code available as a Spoon if you’re interested in doing that (with the one caveat that if it’s submitted to our official Spoon repo, it can’t be called Text Expander) |
|
+1 and I can't resist suggesting to call it HammerText :) |
|
Hi, @asmagill , thanks for the code improvement suggestions and encouraging words! I took another shot at the snippet and I believe I was able to simplify as well as prettify it. It's my first take on Lua so expect the code to be further improvable. 😄 @cmsj I really like the idea of turning this into a Spoon but have yet to better grasp the language and the recommendations on the Spoons API. @maxandersen I lol'd at the suggestion when I checked the expression's meaning (English not being my mother tongue). I don't believe there's a need for a name yet but if it becomes an official request to the Spoons repository I'll definitely think of this as one of the suggestions (although it has bad connotation 😥). Finally, the updated gist is here. As well as the raw paste for convenience: |
To kickstart you I converted your version + brought back ability to use functions in the expansion to a HammerText.spoon. Gist is here: https://gist.github.com/maxandersen/d09ebef333b0c7b7f947420e2a7bbbf5 Feel free to modify as you wish and submit as a spoon (wether you use the HammerText name or not :) One thing I did note is that my keyboard got completely useless if any error happened in the code as it made HammerSpoon console grab focus and/or my keypress was swalloed :) Might want to eventually guard it better against errors/exceptions. |
|
@maxandersen that gist is great, very helpful! One thing to be aware of - your example usage is incorrect, the keys (e.g. |
|
That syntax should be fine for keys that are just using a-z characters. |
|
@maxandersen you're quite right, it does work. I'm not sure why I thought that. I thought it wasn't working when I tried the other day but seems to work now. |
|
Very nice! I started playing with it and it is working quite well, |
|
I'm wet-behind-the-ears new to Hammerspoon. Love it so far. I found @fmaiabatista gist page at https://gist.github.com/fmaiabatista/672e543dae72bcd24dae2885cf7f4e88 - it worked when a few other versions didn't - my debugging skills are still rudimentary! Thanks for that work. But - I want the dynamic "text expansion" others have mentioned. I want YYYY-MM-DD- to be input when I type something like 'ddtt' (or a hotkey). However, this snippet of code causes issues - why, I can't tell right now: keywords = { 'dshc' is just a phone number. That works just fine. Clearly it can't "eval" the lookup value in this 'table'. I suspect I have to put in some bit of code that sets a var, and then sub the var name for the "function()..end" code in the 'keywords' structure. But then I wonder what happens when you're working at 11:58pm, and get past midnight so you'd want the variable to be re-evaluated. "Volatile" is a C++/C keyword. I don't suppose Lua has similar? Can anyone help? Even a small snippet of code I could graft into my init.lua at this point? Thanks in advance - Lua/Hammerspoon newbie, aka "UNIX Guru" |
|
Try this: --[[
*************
Text Expander v0.1
Based on: https://github.com/Hammerspoon/hammerspoon/issues/1042
How to "install":
- Simply copy and paste this code in your "init.lua".
How to use:
- Add your hotstrings (abbreviations that get expanded) to the "keywords" list following the example format.
- Save and reload your HammerSpoon config. The text expansion feature will start automatically.
Features:
- Text expansion starts automatically in your init.lua config.
- Hotstring expands immediately.
- Word buffer is cleared after pressing one of the "navigational" keys.
PS: The default keys should give a good enough workflow so I didn't bother including other keys.
If you'd like to clear the buffer with more keys simply add them to the "navigational keys" conditional.
Limitations:
- Can't expand hotstring if it's immediately typed after an expansion. Meaning that typing "..name..name" will result in "My name..name".
This is intentional since the hotstring could be a part of the expanded string and this could cause a loop.
In that case you have to type one of the "buffer-clearing" keys that are included in the "navigational keys" conditional (which is very often the case).
*************
--]]
keywords = {
["..name"] = "My name",
["..addr"] = "My address",
["..test"] = function() return "test" end,
}
expander = (function()
local word = ""
local keyMap = require"hs.keycodes".map
local keyWatcher
local DEBUG = false
-- create an "event listener" function that will run whenever the event happens
keyWatcher = hs.eventtap.new({ hs.eventtap.event.types.keyDown }, function(ev)
local keyCode = ev:getKeyCode()
local char = ev:getCharacters()
-- if "delete" key is pressed
if keyCode == keyMap["delete"] then
if #word > 0 then
-- remove the last char from a string with support to utf8 characters
local t = {}
for _, chars in utf8.codes(word) do table.insert(t, chars) end
table.remove(t, #t)
word = utf8.char(table.unpack(t))
if DEBUG then print("Word after deleting:", word) end
end
return false -- pass the "delete" keystroke on to the application
end
-- append char to "word" buffer
word = word .. char
if DEBUG then print("Word after appending:", word) end
-- if one of these "navigational" keys is pressed
if keyCode == keyMap["return"]
or keyCode == keyMap["space"]
or keyCode == keyMap["up"]
or keyCode == keyMap["down"]
or keyCode == keyMap["left"]
or keyCode == keyMap["right"] then
word = "" -- clear the buffer
end
if DEBUG then print("Word to check if hotstring:", word) end
-- finally, if "word" is a hotstring
if keywords[word] then
for i = 1, utf8.len(word), 1 do hs.eventtap.keyStroke({}, "delete", 0) end -- delete the abbreviation
if type(keywords[word]) == "function" then
hs.eventtap.keyStrokes(keywords[word]())
else
hs.eventtap.keyStrokes(keywords[word]) -- expand the word
end
word = "" -- clear the buffer
end
return false -- pass the event on to the application
end):start() -- start the eventtap
-- return keyWatcher to assign this functionality to the "expander" variable to prevent garbage collection
return keyWatcher
end)() -- this is a self-executing function because we want to start the text expander feature automatically in out init.lua |
|
Error message that results: 2022-04-06 17:27:59: 17:27:59 ERROR: LuaSkin: hs.eventtap callback error: /Users/me/.hammerspoon/init.lua:85: ERROR: incorrect type 'function' for argument 1 (expected string) stack traceback: Here's the code, modified, that portion, at least: keywords = { That's from your line with "test" init - Thanks again. |
|
This is the important part: if type(keywords[word]) == "function" then
hs.eventtap.keyStrokes(keywords[word]())
else
hs.eventtap.keyStrokes(keywords[word]) -- expand the word
end |
|
That was it! gist bit me in the butt. I found a post - maybe even from this thread - from 2016 - "gist1" - with one init.lua. Then someone forked it - "gist2". Then "gist1" showed an update date that was newer (2020) than update date in "gist2" (I dunno, 2018 or 2019). I think the entire if...end that checked for a function was not in "gist2". I've saved files to disk, and could possibly retrace my steps, if necessary. Basically, moral of THIS story is to make sure it checks for type 'function' and issues pcall() (looks like good ol' fashioned eval() but I'll learn about that one some other day). Great job, Chris. And you didn't even see the entire source - I thought we were all singing from the same hymnal, but that was not the case. THAT's a lesson I've learned multiple times in my career, sorry to say ;-) |
|
And in order to add regular expression support, how hard would that be, so for example when I type NPAPIII-\d+ it does a lookup on that jira ticket? |
|
Also, I noticed when this expander is enabled, hs.pasteboard.setContents(result) does not work. |
I am thinking of using Hammerspoon for text expanding in mac , which is obviously similar to Text Expander software. Any Idea of how to achieve this in Hammerspoon.
I used hotkey binding with
hs.eventtap.keyStrokesyet this is not as comfortable as expanding an abbreviation.Example of what I have done.
hs.hotkey.bind({"alt"},"d",function()
hs.eventtap.keyStrokes(os.date("%B %d, %Y"))
end)
The problem with above is that also I need to memorise the shortcuts. Would be great if there is a way to abbreviate the word "date" to "Oct 14 2016" like that.
The text was updated successfully, but these errors were encountered: