Skip to content
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

Closed
gopinathshiva opened this issue Oct 14, 2016 · 34 comments
Closed

Creating Text Expander using Hammerspoon #1042

gopinathshiva opened this issue Oct 14, 2016 · 34 comments

Comments

@gopinathshiva
Copy link

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.

@asmagill
Copy link
Member

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.

@asmagill
Copy link
Member

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.

@gopinathshiva
Copy link
Author

@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?

@asmagill
Copy link
Member

asmagill commented Oct 20, 2016

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:

  • is checking for the Command key modifier sufficient to abort on application hotkey combinations?
  • should there be a timeout so it stops the capture if you delay long enough? (see hs.timer)
  • some sort of window or application watcher to detect if you change focus while in event capture mode? (see hs.application.watcher and/or hs.window.watcher)
  • some sort of visual feedback so you know that it's in capture mode? (see hs.drawing and/or hs.alert)
  • some sort of feedback when a match isn't found? (see hs.alert)
  • are there other cases where it should abort or terminate early?

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...

@gopinathshiva
Copy link
Author

@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.

@cmsj
Copy link
Member

cmsj commented Oct 26, 2016

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.

@cmsj
Copy link
Member

cmsj commented Oct 26, 2016

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?

@atjshop
Copy link

atjshop commented Mar 17, 2017

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.

@cmsj
Copy link
Member

cmsj commented Mar 17, 2017

@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.

@cmsj cmsj closed this as completed Mar 17, 2017
@atjshop
Copy link

atjshop commented Mar 17, 2017

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

@cmsj
Copy link
Member

cmsj commented Mar 17, 2017

@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

@Ninlives
Copy link

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

@cmsj
Copy link
Member

cmsj commented Apr 27, 2017

@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)

@Ninlives
Copy link

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

@amitkot
Copy link

amitkot commented Jul 11, 2017

@asmagill Thanks for the code snippet! It's working fine for me and solves my current needs for text expansion.

A couple of questions:

  • Is there a technical reason for the keywatcher to require the Alt+d to start listening, instead of running all the time? Will it have a performance cost to leave the watcher running constantly?
  • Deleting every char of the trigger word is slow, is there a way to delete them faster? Perhaps as @Ninlives suggested, if I got it correctly, to access the text area and directly manipulate its contents?

@amitkot
Copy link

amitkot commented Jul 12, 2017

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.

@fmaiabatista
Copy link

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?

@fmaiabatista
Copy link

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. 😄

@asmagill
Copy link
Member

Couple of comments on what otherwise looks pretty useful:

  • you don't need to add the 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 thing
  • expander 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.
  • because 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!

@cmsj
Copy link
Member

cmsj commented Aug 25, 2019

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)

@maxandersen
Copy link

+1 and I can't resist suggesting to call it HammerText :)

@fmaiabatista
Copy link

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
Copy link

@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.

@andrewferrier
Copy link

@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...

@maxandersen
Copy link

That syntax should be fine for keys that are just using a-z characters.

@andrewferrier
Copy link

@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.

@paul-louyot
Copy link

Very nice! I started playing with it and it is working quite well,
I changed the prefix character to 'ff', so that I could 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

@bill-hudacek
Copy link

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"] = "AAAEEENNNN",
["ddtt"] = function() return os.date("%B %d, %Y") end,
}

'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"

@latenitefilms
Copy link
Contributor

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

@bill-hudacek
Copy link

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:

[C]: in function 'hs.eventtap.keyStrokes'

/Users/donnahudacek/.hammerspoon/init.lua:85: in function </Users/me/.hammerspoon/init.lua:49>

Here's the code, modified, that portion, at least:

keywords = {
["DSG"] = "user@gmail.com",
["dshh"] = "street address",
["dshc"] = "phoneNo",
["ddtt"] = function() return "test" end,
}

That's from your line with "test" init - Thanks again.

@latenitefilms
Copy link
Contributor

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

@bill-hudacek
Copy link

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 ;-)

@relipse
Copy link

relipse commented May 30, 2023

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?

@relipse
Copy link

relipse commented May 31, 2023

Also, I noticed when this expander is enabled, hs.pasteboard.setContents(result) does not work.
If I comment it out, hs.pasteboard.setContents(result) works.
Any ideas why this would be?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests