├── README.md └── init.lua /README.md: -------------------------------------------------------------------------------- 1 | ## Anycomplete 2 | 3 | The magic of Google Autocomplete while you're typing. Anywhere. 4 | 5 | ![](http://i.imgur.com/kYoE7hs.gif) 6 | 7 | ### Installation 8 | 9 | Anycomplete is an extension for [Hammerspoon](http://hammerspoon.org/). Once Hammerspoon is installed, you can install the Autocomplete Spoon: 10 | 11 | $ git clone https://github.com/nathancahill/anycomplete.git ~/.hammerspoon/Spoons/Anycomplete.spoon 12 | 13 | To initialize, add to `~/.hammerspoon/init.lua` (creating it if it does not exist): 14 | 15 | ```lua 16 | anycomplete = hs.loadSpoon("Anycomplete") 17 | anycomplete.bindHotkeys() 18 | ``` 19 | 20 | Reload the Hammerspoon config. 21 | 22 | ### Usage 23 | 24 | Trigger with the hotkey `⌃⌥⌘G`. Once you start typing, suggestions will populate. 25 | They can be choosen with `⌘1-9` or by pressing the arrow keys and Enter. 26 | Pressing `⌘C` copies the selected item to the clipboard. 27 | 28 | The hotkey can be changed by passing an argument to 29 | `bindHotkeys` call (in your `~/.hammerspoon/init.lua` file) 30 | such as: 31 | 32 | ```lua 33 | anycomplete:bindHotkeys({{"cmd", "ctrl"}, "L"}) 34 | ``` 35 | 36 | ### Warning 37 | 38 | Google might block your IP address if you use this. See [#26](https://github.com/nathancahill/Anycomplete/issues/26). 39 | 40 | ### Privacy 41 | 42 | No keystrokes are sent to Google until you trigger the hotkey and start typing. If you prefer DuckDuckGo, 43 | set the `engine` option: 44 | 45 | ```lua 46 | anycomplete = hs.loadSpoon("Anycomplete") 47 | anycomplete.engine = "duckduckgo" 48 | anycomplete.bindHotkeys() 49 | ``` 50 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | --- === Anycomplete === 2 | --- 3 | --- The magic of Google Autocomplete while you're typing. Anywhere. 4 | --- 5 | --- Configurable properties (with default values): 6 | --- engine = "google", -- or "duckduckgo" 7 | --- 8 | --- Download: [https://github.com/nathancahill/Anycomplete](https://github.com/nathancahill/Anycomplete) 9 | 10 | local obj = {} 11 | 12 | -- Options 13 | obj.engine = "google" 14 | 15 | -- Metadata 16 | obj.name = "Anycomplete" 17 | obj.version = "1.0" 18 | obj.author = "Nathan Cahill " 19 | obj.homepage = "https://github.com/nathancahill/Anycomplete" 20 | obj.license = "MIT - https://opensource.org/licenses/MIT" 21 | 22 | local endpoints = { 23 | google = "https://suggestqueries.google.com/complete/search?client=firefox&q=%s", 24 | duckduckgo = "https://duckduckgo.com/ac/?q=%s", 25 | } 26 | 27 | -- Anycomplete 28 | function obj:anycomplete() 29 | local current = hs.application.frontmostApplication() 30 | local tab = nil 31 | local copy = nil 32 | local choices = {} 33 | 34 | local chooser = hs.chooser.new(function(choosen) 35 | if copy then copy:delete() end 36 | if tab then tab:delete() end 37 | current:activate() 38 | 39 | if not choosen then return end 40 | hs.eventtap.keyStrokes(choosen.text) 41 | end) 42 | 43 | -- Removes all items in list 44 | function reset() 45 | chooser:choices({}) 46 | end 47 | 48 | tab = hs.hotkey.bind('', 'tab', function() 49 | local id = chooser:selectedRow() 50 | local item = choices[id] 51 | -- If no row is selected, but tab was pressed 52 | if not item then return end 53 | chooser:query(item.text) 54 | reset() 55 | updateChooser() 56 | end) 57 | 58 | copy = hs.hotkey.bind('cmd', 'c', function() 59 | local id = chooser:selectedRow() 60 | local item = choices[id] 61 | if item then 62 | chooser:hide() 63 | hs.pasteboard.setContents(item.text) 64 | hs.alert.show("Copied to clipboard", 1) 65 | else 66 | hs.alert.show("No search result to copy", 1) 67 | end 68 | end) 69 | 70 | function updateChooser() 71 | local string = chooser:query() 72 | local query = hs.http.encodeForQuery(string) 73 | -- Reset list when no query is given 74 | if string:len() == 0 then return reset() end 75 | 76 | hs.http.asyncGet(string.format(endpoints[self.engine], query), nil, function(status, data) 77 | if not data then return end 78 | 79 | local ok, results = pcall(function() return hs.json.decode(data) end) 80 | if not ok then return end 81 | 82 | if self.engine == "google" then 83 | choices = hs.fnutils.imap(results[2], function(result) 84 | return { 85 | ["text"] = result, 86 | } 87 | end) 88 | elseif self.engine == "duckduckgo" then 89 | choices = hs.fnutils.imap(results, function(result) 90 | return { 91 | ["text"] = result["phrase"], 92 | } 93 | end) 94 | end 95 | 96 | chooser:choices(choices) 97 | end) 98 | end 99 | 100 | chooser:queryChangedCallback(updateChooser) 101 | chooser:searchSubText(false) 102 | chooser:show() 103 | end 104 | 105 | function obj:bindHotkeys(mapping) 106 | mapping = mapping or {{"cmd", "alt", "ctrl"}, "g"} 107 | hs.hotkey.bind(mapping[1], mapping[2], function() obj:anycomplete() end) 108 | end 109 | 110 | return obj 111 | --------------------------------------------------------------------------------