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