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.keyStrokes yet 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.
What do you mean by abbreviate the word "date" to "Oct 14 2016"? Are you looking for something that can look at the nearest word to the cursor and use it as a look up for other text to replace? At present, I don't think Hammerspoon can do that...
You might get close by using hs.eventtap...
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 hs.chooser -- have a hotkey pop one up and have it's entries choose what to "type" based upon what you choose from the list... probably easier and less prone to timing errors than an eventtap based solution.
@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?
watcher is a misnomer in this case and I shouldn't have used that word since we do use it in other modules to mean a specific type of monitoring... any object created with hs.eventtap.new is in effect a "watcher" since its callback function will be invoked whenever any event of the type(s) specified occur. Specifically you'll want to watch for keyUp and keyDown events... I don't think any others will matter, but you'll need to experiment
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 :start() to actually start the eventtap!_
_edit2: couple of other edits so this actually works, though still needs more testing..._
@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 --
elseif keyCode == keyMap["up"] or keyCode == keyMap["down"] then -- should others be in here?
keyWatcher:stop()
return false -- pass these on
-- codes missing --
-- codes missing --
if keyCode ~= keyMap["left"] and keyCode ~= keyMap["right"] then
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
-- 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()
keyWatcher:stop()
end)
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.
I mean, the code works perfectly when typing English words because the characters I insert are the same as the key sequences I type. But when I want to type, e.g. Chinese words, I have to use a input method, input some pinyin(English characters represent the pronunciation of a Chinese character), choose the character I want to type, then the character will finally insert into the textfield. These characters can't be recorded using keyDown and keyUp event. Is there a way to do this in hammerspoon? Or maybe I should go deep into object-c to make it?
Forgive my poor English :P
@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 hs.eventtap.keyStroke, i.e. change:
for i = 1, utf8.len(what), 1 do hs.eventtap.keyStroke({}, "delete") end
to:
for i = 1, utf8.len(what), 1 do hs.eventtap.keyStroke({}, "delete", 0) end
Also 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 ~/.hammerspoon/init.lua with your gist's content. Tried reloading config and stuff but it didn't work.
I'm typing date and name but it isn't working... I have also tried using Alt + D to start the watcher and also changing alt with ctrl and then reloading the config but I haven't had success so far.
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.
1) I'm used to type prefix characters whenever I want to expand something.
2) I like it to expand automatically
3) I didn't want to bind the watcher to a hotkey so I made it a self-executing function so it starts automatically when HS starts.
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:
-- Auxiliary function needed to run the feature
function table.contains(table, term)
for key in pairs(table) do
if key == term then
return true
end
end
return false
end
--[[
*************
Text Expander v0
Based on: https://github.com/Hammerspoon/hammerspoon/issues/1042
Features:
1) Expands automatically
2) Buffer tracks delete and
*************
--]]
keywords = {
["..name"] = "Felipe Maia",
}
expander = (function()
local word = ""
local keyMap = require"hs.keycodes".map
local keyWatcher
local DEBUG = false
keyWatcher = hs.eventtap.new({ hs.eventtap.event.types.keyUp, hs.eventtap.event.types.keyDown }, function(ev)
local keyCode = ev:getKeyCode()
local eventType = ev:getType()
local char = ev:getCharacters()
-- capture keydown keyboard event
if eventType == hs.eventtap.event.types.keyDown then
-- if "delete" key was pressed, update "word" buffer and send keystroke on to application
if keyCode == keyMap["delete"] then
if #word > 0 then
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
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, clear buffer
if keyCode == keyMap["return"] or keyCode == keyMap["space"] or keyCode == keyMap["left"] or keyCode == keyMap["up"] or keyCode == keyMap["down"] or keyCode == keyMap["right"] then
word = ""
end
if DEBUG then print("Word to check if hotstring:", word) end
-- finally, check if word is a hotstring and expand it. clear buffer afterwards.
if table.contains(keywords, word) then
local output = keywords[word]
for i = 1, utf8.len(word), 1 do hs.eventtap.keyStroke({}, "delete", 0) end
hs.eventtap.keyStrokes(output)
word = ""
end
end
return false -- pass the event on to the application
end):start()
end)() -- this is a self-executing function because we want to start the text expander feature automatically
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:
contains function to table... lua treats any value other than false and nil as "true", so you can change if table.contains(keywords, word) then to if keywords[word] then and achieve the same thingexpander actually containing nothing because the function it calls doesn't return anything, so technically expander = isn't necessary... in fact, if you're adding this to your init.lua file, or another file it loads with require or dofile, you can get rid of expander = (function() and the final end)() -- this... completely because the code will be executed as part of loading Hammerspoon.keyWatcher is local to your function (or file if you follow the previous point), it will eventually be collected and expansion will stop working unexpectedly because eventtaps are stopped when their userdata is collected.If your intention is that expander contains the eventtap so that it doesn't get collected, then ignore my second point above, but add return keyWatcher as a line right after end):start() and before end)() -- ...
Otherwise, nice implementation which I may end up using myself!
It would be cool to have that code available as a Spoon if you鈥檙e interested in doing that (with the one caveat that if it鈥檚 submitted to our official Spoon repo, it can鈥檛 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:
--[[
*************
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",
}
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
hs.eventtap.keyStrokes(keywords[word]) -- expand the word
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
@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 馃槬).
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. nname appear to need to be ["nname"]). I get a syntax error otherwise. Having said that, my Lua is very weak...
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 changed the prefix character to 'ff', so that I鈥痗ould add '.' (and also 'tab') to the navigational keys.
That way, i can expand several expressions separated by dots, for example to write Rails database queries, like User.microposts.last.comments
Most helpful comment
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.