├── .gitignore ├── LICENSE.md ├── README.md ├── assets └── screencast.gif └── hhann └── init.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Szymon Kaliski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HHANN 2 | ## Hackable Hammerspoon Screen Annotator 3 | 4 |

5 | 6 |

7 | 8 | ## Setup 9 | 10 | 1. copy `hhann/` folder to `~/.hammerspoon` 11 | 2. require the library in your `init.lua`: 12 | ```lua 13 | local hhann = require('hhann') 14 | ``` 15 | 16 | ## Example Configuration 17 | 18 | ```lua 19 | local hhann = require('hhann') 20 | local module = {} 21 | 22 | local ultra = { 'ctrl', 'alt', 'cmd' } 23 | local hotkey = hs.hotkey.modal.new(ultra, 'a') 24 | 25 | function hotkey:entered() 26 | hhann.start() 27 | hhann.startAnnotating() 28 | end 29 | 30 | function hotkey:exited() 31 | hhann.stopAnnotating() 32 | hhann.hide() 33 | end 34 | 35 | hotkey:bind(ultra, 'c', function() hhann.clear() end) 36 | hotkey:bind(ultra, 'a', function() hotkey:exit() end) 37 | hotkey:bind(ultra, 't', function() hhann.toggleAnnotating() end) 38 | ``` 39 | 40 | Hit `ctrl + cmd + alt + a` to enter `hhann` mode, in that mode: 41 | - hold left mouse button to draw 42 | - hit `ctrl + cmd + alt + c` to clear screen 43 | - hit `ctrl + cmd + alt + t` to toggle drawing on/off 44 | - hit `ctrl + cmd + alt + a` to exit 45 | 46 | -------------------------------------------------------------------------------- /assets/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szymonkaliski/hhann/fc80a51585481c5b966622a2d23be7c6ab5fa0be/assets/screencast.gif -------------------------------------------------------------------------------- /hhann/init.lua: -------------------------------------------------------------------------------- 1 | local cache = {} 2 | local module = { cache = cache } 3 | 4 | local COLOR = { red = 200, green = 0, blue = 0, alpha = 0.8 } 5 | local COLOR_DIM = { red = 200, green = 0, blue = 0, alpha = 0.4 } 6 | 7 | -- grabs screen with active window, unless it's Finder's desktop - then defaults to mouse position 8 | local activeScreen = function() 9 | local activeWindow = hs.window.focusedWindow() 10 | 11 | if activeWindow and activeWindow:role() ~= 'AXScrollArea' then 12 | return activeWindow:screen() 13 | else 14 | return hs.mouse.getCurrentScreen() 15 | end 16 | end 17 | 18 | local updateMode = function() 19 | local mouse = hs.mouse.getRelativePosition() 20 | local x, y = mouse.x, mouse.y 21 | local color = cache.isAnnotating and COLOR or COLOR_DIM 22 | 23 | cache.canvas[1] = { 24 | center = { x = x + 6, y = y - 6 }, 25 | type = 'circle', 26 | radius = 3, 27 | action = 'fill', 28 | fillColor = color, 29 | } 30 | end 31 | 32 | module.startAnnotating = function() 33 | local upsertCurrentDrawing = function(x, y) 34 | table.insert(cache.currentSegments, { x = x, y = y }) 35 | 36 | cache.canvas[cache.currentId] = { 37 | id = cache.currentId, 38 | type = 'segments', 39 | coordinates = cache.currentSegments, 40 | action = 'stroke', 41 | strokeColor = COLOR, 42 | strokeWidth = 2, 43 | } 44 | end 45 | 46 | -- empty callback with mouse events "catches" the input 47 | cache.canvas 48 | :canvasMouseEvents(false, false, false, true) 49 | :mouseCallback(function(_, _, _, _, _) end) 50 | 51 | -- actual drawing event - mouse dragged 52 | cache.tapDrag = hs.eventtap.new({ 53 | hs.eventtap.event.types.leftMouseDown, 54 | hs.eventtap.event.types.leftMouseDragged 55 | }, function(e) 56 | -- TODO: marge `tapMove` and `tapDrag`, could recognise based on `eventType` 57 | local frame = cache.canvas:frame() 58 | local eventType = e:getType() 59 | local eventLocation = e:location() 60 | local x = eventLocation['x'] - frame.x 61 | local y = eventLocation['y'] - frame.y 62 | 63 | if eventType == hs.eventtap.event.types.leftMouseDown then 64 | cache.currentId = cache.currentId + 1 65 | cache.currentSegments = {} 66 | 67 | upsertCurrentDrawing(x, y) 68 | elseif eventType == hs.eventtap.event.types.leftMouseDragged then 69 | upsertCurrentDrawing(x, y) 70 | end 71 | 72 | cache.canvas[1].center = { x = x + 6, y = y - 6 } 73 | end) 74 | 75 | cache.tapDrag:start() 76 | 77 | cache.isAnnotating = true 78 | 79 | updateMode() 80 | end 81 | 82 | module.stopAnnotating = function() 83 | if cache.canvas then 84 | cache.canvas 85 | :canvasMouseEvents(false, false, false, false) 86 | :mouseCallback(nil) 87 | end 88 | 89 | if cache.tapDrag then 90 | cache.tapDrag:stop() 91 | cache.tapDrag = nil 92 | end 93 | 94 | cache.isAnnotating = false 95 | 96 | updateMode() 97 | end 98 | 99 | local setup = function() 100 | if not cache.canvas then 101 | cache.canvas = hs.canvas.new({ x = 0, y = 0, w = 0, h = 0 }) 102 | :level(hs.canvas.windowLevels.overlay) 103 | :behavior({ 104 | hs.canvas.windowBehaviors.transient, 105 | hs.canvas.windowBehaviors.moveToActiveSpace 106 | }) 107 | end 108 | 109 | local screen = activeScreen() 110 | 111 | cache.canvas 112 | :size(screen:frame()) 113 | :frame(screen:frame()) 114 | :show() 115 | 116 | cache.currentId = math.max(#cache.canvas, 1) 117 | 118 | if cache.tapMove then 119 | cache.tapMove:stop() 120 | end 121 | 122 | cache.tapMove = hs.eventtap.new({ 123 | hs.eventtap.event.types.mouseMoved 124 | }, function(e) 125 | local frame = cache.canvas:frame() 126 | local eventLocation = e:location() 127 | local x = eventLocation['x'] - frame.x 128 | local y = eventLocation['y'] - frame.y 129 | 130 | if cache.canvas[1] then 131 | cache.canvas[1].center = { x = x + 6, y = y - 6 } 132 | end 133 | end) 134 | 135 | cache.tapMove:start() 136 | 137 | updateMode() 138 | end 139 | 140 | module.start = function() 141 | setup() 142 | end 143 | 144 | module.clear = function() 145 | if cache.canvas then 146 | local wasAnnotating = cache.isAnnotating 147 | 148 | if wasAnnotating then 149 | module.stopAnnotating() 150 | end 151 | 152 | cache.canvas:delete() 153 | cache.canvas = nil 154 | 155 | setup() 156 | 157 | if wasAnnotating then 158 | module.startAnnotating() 159 | end 160 | end 161 | end 162 | 163 | module.toggleAnnotating = function() 164 | if cache.isAnnotating then 165 | module.stopAnnotating() 166 | else 167 | module.startAnnotating() 168 | end 169 | end 170 | 171 | module.hide = function() 172 | cache.canvas:hide() 173 | cache.tapMove:stop() 174 | end 175 | 176 | module.stop = function() 177 | if cache.canvas then 178 | cache.canvas:delete() 179 | cache.canvas = nil 180 | end 181 | 182 | if cache.tapDrag then 183 | cache.tapDrag:stop() 184 | cache.tapDrag = nil 185 | end 186 | 187 | if cache.tapMove then 188 | cache.tapMove:stop() 189 | cache.tapMove = nil 190 | end 191 | end 192 | 193 | return module 194 | --------------------------------------------------------------------------------