├── .gitignore
├── LICENSE
├── README.md
├── init.lua
├── misc
└── bingdaily.lua
├── modalmgr.lua
├── modes
├── basicmode.lua
├── cheatsheet.lua
├── clipshow.lua
├── hsearch.lua
└── indicator.lua
├── preload.lua
├── resources
├── emoji.png
├── justnote.png
├── menus.png
├── safari.png
├── tabs.png
├── taskkill.png
├── thesaurus.png
├── time.png
├── timebg.png
├── v2ex.png
├── watchbg.png
└── youdao.png
└── widgets
├── analogclock.lua
├── aria2.lua
├── caffeine.lua
├── calendar.lua
├── hcalendar.lua
├── netspeed.lua
└── timelapsed.lua
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | private
3 | Spoons
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 ashfinal
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 | # Awesome-hammerspoon, as advertised.
2 |
3 | Awesome-hammerspoon is my collection of lua scripts for [Hammerspoon](http://www.hammerspoon.org/). It has highly modal-based, vim-styled key bindings, provides some functionality like desktop widgets, window management, application launcher, Alfred-like search, aria2 GUI, dictionary translation, cheatsheets, take notes ... etc.
4 |
5 | ## Get started
6 |
7 | 1. Install [Hammerspoon](http://www.hammerspoon.org/) first.
8 | 2. `git clone --depth 1 https://github.com/jeoygin/awesome-hammerspoon.git ~/.hammerspoon`
9 | 3. Reload the configutation.
10 |
11 | and you're set.
12 |
13 | ## Keep update
14 |
15 | See [awesome-hammerspoon whiteboard](https://github.com/ashfinal/awesome-hammerspoon/projects/2) for project changlog and todos.
16 |
17 | `cd ~/.hammerspoon && git pull`
18 |
19 | ## What's modal-based key bindings?
20 |
21 |
22 | More details
23 |
24 | Well... simply to say, it allows you using S key to resize windows in `resize` mode, but in `app` mode, to launch Safari, in `timer` mode, to set a 10-mins timer... something like that. During all progress, you don't have to press extra keys.
25 |
26 |
27 | And this means a lot.
28 |
29 | * It's scene-wise, you can use same key bindings to do different jobs in different scenes. You don't worry to run out of your hotkey bindings, and twist your fingers to press ⌘ + ⌃ + ⌥ + ⇧ + C in the end.
30 |
31 | * Less keystrokes, less memory pressure. You can press ⌥ + A to enter `app` mode, release, then press single key S to launch Safari, or C to lauch Chrome. Sounds good? You keep your pace, no rush.
32 |
33 | * Easy to extend, you can create your own modals if you like. For example, `Finder` mode, in which you press T to open Terminal here, press S to send files to predefined path, press C to upload images to cloud storage.
34 |
35 |
36 |
37 | ## How to use?
38 |
39 | So, following above procedures, you have reloaded Hammerspoon's configutation. Let's see what we've got here.
40 |
41 | ### 1. Desktop Widgets
42 |
43 |
44 | More details
45 |
46 | As you may have noticed, there are two clean, nice-looking desktop widgets, analogclock and hcalendar. Usually we don't interact with them, but I do hope you like them.
47 |
48 | 
49 |
50 | *There are also other widgets [calendar](https://github.com/ashfinal/awesome-hammerspoon/blob/master/widgets/calendar.lua), [time elapsed](https://github.com/ashfinal/awesome-hammerspoon/blob/master/widgets/timelapsed.lua), maybe more …*
51 |
52 |
53 |
54 | ### 2. More Widgets and Modes
55 |
56 |
57 | More details
58 |
59 | There is actually more besides these. Now you can press ⌥ + R to enter `resize` mode, or press ⌥ + A to enter `app` mode …and start to explore.
60 |
61 | In order to make one single keystroke work in two scenes, you may want to know in which scene you are now. If you enter certain scene (and forget to exit, and wonder why your regular typing doesn't work as expected), see if there is a small circle in the bottom right corner, which indicates different scenes with different color. If that's the fact, then you realize you need to press ⎋, exit current scene, dismiss the circle, and get back to your work.
62 |
63 | Key bindings available:
64 |
65 | | Key bindings | Movement |
66 | | --------------------------- | -------------------------- |
67 | | ⌥ + A | Enter `app` mode |
68 | | ⌥ + C | Enter `clipboard` mode |
69 | | ⌥ + D | Launch aria2 GUI . |
70 | | ⌥ + G | Launch hammer search |
71 | | ⌥ + I | Enter `timer` mode |
72 | | ⌥ + R | Enter `resize` mode |
73 | | ⌥ + S | Enter `cheatsheet` mode |
74 | | ⌥ + T | Show current time |
75 | | ⌥ + v | Enter `view` mode |
76 | | ⌥ + Z | Toggle Hammerspoon console |
77 | | ⌥ + ⇥ | Show window hints |
78 |
79 | *In most modes, you can use Q, or ⎋ to quit back. And switch from one mode to another directly.*
80 |
81 |
82 |
83 | ### 3. Window Management(resize mode) ⌥ + R
84 |
85 |
86 | More details
87 |
88 | 
89 |
90 | Use [/] to cycle through active windows.
91 |
92 | Use H/J/K/L to resize windows to 1/2 of screen.
93 |
94 | Use Y/U/I/O to resize windows to 1/4 of screen.
95 |
96 | Use ⇧ + H/J/K/L to **move** windows around.
97 |
98 | Use ␣ or ⇡⇣⇠⇢⇠ to **move** windows to **other screens**.
99 |
100 | Use ⇧ + Y/U/I/O to **resize** windows.
101 |
102 | Use =, - to **expand**/**shrink** the window size.
103 |
104 | Use F to put windows to fullscreen, use C to put windows to center of screen, use ⇧ + C to resize windows to predefined size and center them.
105 |
106 |
107 |
108 | ### 4. App Launcher(app mode) ⌥ + A
109 |
110 |
111 | More details
112 |
113 | Use F to launch Finder or focus the existing window; S for Safari; T for Terminal; V for Activity Monitor; Y for System Preferences... etc.
114 |
115 | If you want to define your own hotkeys, please create `~/.hammerspoon/private/awesomeconfig.lua` file, then add something like below:
116 |
117 | applist = {
118 | {shortcut = 'i',appname = 'iTerm'},
119 | {shortcut = 'l',appname = 'Sublime Text'},
120 | {shortcut = 'm',appname = 'MacVim'},
121 | {shortcut = 'o',appname = 'LibreOffice'},
122 | {shortcut = 'r',appname = 'Firefox'},
123 | }
124 |
125 | **UPDATE:** Now you can press ⇥ to toggle key bindings, also available in `resize`, `view`, `timer` mode.
126 |
127 | 
128 |
129 |
130 |
131 | ### 5. Hammer Search ⌥ + G
132 |
133 |
134 | More details
135 |
136 | Now you can do lots of things with Hammerspoon search: search Safari tabs, dictionary translation, kill active application, English thesaurus, get latest posts from v2ex, emoji search, take notes … etc. And feel free to add your own source!
137 |
138 | 
139 |
140 | **NOTICE:** If you heavily rely on instant translation(youdao dict), please consider applying for your own API key at here:
141 |
142 | [http://fanyi.youdao.com/openapi?path=data-mode](http://fanyi.youdao.com/openapi?path=data-mode)
143 |
144 | Then add them to `~/.hammerspoon/private/awesomeconfig.lua`:
145 |
146 | youdaokeyfrom = 'hsearch' -- keyfrom
147 | youdaoapikey = '1199732752' -- API key
148 |
149 |
150 |
151 | ### 6. Aria2 GUI ⌥ + D
152 |
153 |
154 | More details
155 |
156 | This is a "native" frontend for [aria2](https://github.com/aria2/aria2).
157 |
158 | 
159 |
160 | You need to run aria2 with RPC enabled before using this. Config aria2 host and token in `~/.hammerspoon/private/awesomeconfig.lua`, then you're ready to go.
161 |
162 | aria2_host = "http://localhost:6800/jsonrpc" -- default host
163 | aria2_token = "token" -- YOUR OWN TOKEN
164 |
165 | Add new task (regular URL or BTfile or Metafile) from aria2 "toolbar", click certain task item to pause/resume the download, or open completed files. While holding down `⌘` key, you click certain item, that will stop the download, or remove the completed/error task. It will notify you if there is any completed download or any error, even aria2 window is closed. And you can batch add tasks from your pasteboard, one URL per line.
166 |
167 |
168 |
169 | ### 7. Timer Indicator(timer mode) ⌥ + I
170 |
171 |
172 | More details
173 |
174 | Have you noticed this issue on macos? There is 5 pixel tall blank at the bottom of the screen for non-native fullscreen window, which is sometimes disturbing. Let's make the blank more useful. When you set a timer, this will draw a colored line to fill that blank, meanwhile, show progress of the timer.
175 |
176 | 
177 |
178 | Press 0 to set a 5-mins timer, ↩︎ to set a 25-mins timer.
179 |
180 | Press 1 to set a 10-mins timer;
181 |
182 | Press 2 to set a 20-mins timer;
183 |
184 | ...
185 |
186 | Press 9 to set a 90-mins timer.
187 |
188 |
189 |
190 | ### 8. Cheatsheet(cheatsheet mode) ⌥ + S
191 |
192 |
193 | More details
194 |
195 | It shows the cheatsheet of current application's hotkeys. Code comes from [here](https://github.com/dharmapoudel/hammerspoon-config).
196 |
197 | Let the picture talk:
198 |
199 | 
200 |
201 |
202 |
203 | ### 9. Clipboard Show ⌥ + C
204 |
205 |
206 | More details
207 |
208 | It shows the content of your clipboard. If text or image type then display it with proper size, if hyperlink type then use default browser to open it. Click the display block it will destory itself.
209 |
210 | I usually use this to display QR image for cellphone's faster scanning, or display some text for better reading. And I never need to do this below: focus the default browser, click the address bar, paste the URL and press Enter to go.
211 |
212 |
213 |
214 | ### 10. Other Stuff
215 |
216 |
217 | Tmux-styled Clock ⌥ + T
218 |
219 | Works even when you're watching video in fullscreen.
220 |
221 | 
222 |
223 |
224 |
225 |
226 | Windows Hint ⌥ + ⇥
227 |
228 | Focus to your windows easier.
229 |
230 | 
231 |
232 |
233 |
234 |
235 | View Mode ⌥ + V
236 |
237 | Use H/J/K/L to scroll around.
238 |
239 | Use ⌃/⇧ + H/J/K/L to move mouse around.
240 |
241 | Use ,/. for mouse left/right click.
242 |
243 |
244 |
245 |
246 | Netspeed Monitor
247 |
248 | Watch your netspeed sitting on the menubar. Support macos's darkmode.
249 |
250 |
251 |
252 |
253 | Bing Wallpaper
254 |
255 | Automatically use Bing daily picture for your wallpaper.
256 |
257 |
258 |
259 |
260 | Lock Screen ⌘ + ⌃ + ⇧ + L
261 |
262 | No description.
263 |
264 |
265 |
266 |
267 | And More...
268 |
269 | For whatever mode, you can always use:
270 |
271 | ⌘ + ⌥ + ⇠ to resize windows to left-half of screen
272 |
273 | ⌘ + ⌥ + ⇢ to resize windows to right-half of screen
274 |
275 | ⌘ + ⌥ + ⇡ to resize windows to fullscreen
276 |
277 | ⌘ + ⌥ + ⇣ to put windows to predefined size
278 |
279 | ⌘ + ⌥ + ↩︎ to put windows to center of screen
280 |
281 |
282 |
283 | ## Customization
284 |
285 |
286 | More details
287 |
288 | Modify the file `~/.hammerspoon/private/awesomeconfig.lua`, you should create it before doing below things.
289 |
290 | 1. Add application launching hotkey
291 |
292 | See the section `App launcher(app mode)` above.
293 |
294 | 2. Add/Remove the plugin modules
295 |
296 | default modules:
297 |
298 | module_list = {
299 | "widgets/netspeed",
300 | "widgets/calendar",
301 | "widgets/hcalendar",
302 | "widgets/analogclock",
303 | "widgets/timelapsed",
304 | "widgets/aria2",
305 | "modes/basicmode",
306 | "modes/indicator",
307 | "modes/clipshow",
308 | "modes/cheatsheet",
309 | "modes/hsearch",
310 | "misc/bingdaily",
311 | }
312 |
313 | For example, remove `bingdaily` module, add your own module `mymodule`:
314 |
315 | module_list = {
316 | "widgets/netspeed",
317 | "widgets/calendar",
318 | "widgets/hcalendar",
319 | "widgets/analogclock",
320 | "widgets/timelapsed",
321 | "widgets/aria2",
322 | "modes/basicmode",
323 | "modes/indicator",
324 | "modes/clipshow",
325 | "modes/cheatsheet",
326 | "modes/hsearch",
327 | "private/mymodule",
328 | }
329 |
330 | 3. Modify/Remove the default key bindings
331 |
332 | Available key binding variables:
333 |
334 | | Action | Variable | Default value |
335 | | -------------------------- | --------------------------- | ------------------------------- |
336 | | Reload Configuration | hsreload_keys | {{"cmd", "shift", "ctrl"}, "R"} |
337 | | Toggle Modal Supervisor | modalmgr_keys | {{"cmd", "shift", "ctrl"}, "Q"} |
338 | | Toggle Hammerspoon Console | toggleconsole_keys | {{"alt"}, "Z"} |
339 | | Lock Screen | lockscreen_keys | {{"cmd", "shift", "ctrl"}, "L"} |
340 | | Enter Application Mode | appM_keys | {{"alt"}, "A"} |
341 | | Enter Clipboard Mode | clipboardM_keys | {"alt"}, "C"} |
342 | | Launch Aria2 GUI . | aria2_keys . | {"alt"}, "D"} |
343 | | Launch Hammer Search | hsearch_keys | {{"alt"}, "G"} |
344 | | Enter Timer Mode | timerM_keys | {{"alt"}, "T"} |
345 | | Enter Resize Mode | resizeM_keys | {{"alt"}, "R"} |
346 | | Enter Cheatsheet Mode | cheatsheetM_keys | {{"alt"}, "S"} |
347 | | Show Digital Clock | showtime_keys | {{"alt"}, "T"} |
348 | | Enter View Mode | viewM_keys | {{"alt"}, "V"} |
349 | | Show Window hints | winhints_keys | {{"alt"}, "tab"} |
350 | | Lefthalf of Screen | resizeextra_lefthalf_keys | {{"cmd", "alt"}, "left"} |
351 | | Righthalf of Screen | resizeextra_righthalf_keys | {{"cmd", "alt"}, "right"} |
352 | | Fullscreen | resizeextra_fullscreen_keys | {{"cmd", "alt"}, "up"} |
353 | | Resize & Center | resizeextra_fcenter_keys | {{"cmd", "alt"}, "down"} |
354 | | Center Window | resizeextra_center_keys | {{"cmd", "alt"}, "return"} |
355 |
356 | For example, to modify `Toggle Modal Supervisor` key binding:
357 |
358 | modalmgr_keys = {{"alt"}, "F"}
359 |
360 | To completely remove `Lock Screen` key binding:
361 |
362 | lockscreen_keys = {{}, ""}
363 |
364 | 4. Global options
365 |
366 | These options should be put into `~/.hammerspoon/private/awesomeconfig.lua` file.
367 |
368 | ``` lua
369 | aria2_host = "http://localhost:6800/jsonrpc" -- default host
370 | aria2_token = "token" -- YOUR OWN TOKEN
371 | aria2_refresh_interval = 1 -- How often the frontend should request data from the host
372 | aria2_show_items_max = 5 -- How many items the frontend should show
373 |
374 | -- When enter `resize/app/timer` mode show or hide applauncher tips automatically.
375 | show_resize_tips = true/false
376 | show_applauncher_tips = true/false
377 | show_timer_tips = true/false
378 |
379 | hotkey_tips_bg = "light"/"dark" -- Make the hotkey tips' background light or dark
380 |
381 | -- Put analogclock to somewhere by defining center point.
382 | aclockcenter = {x=200,y=200}
383 |
384 | -- Put calendar to somewhere by defining topleft point.
385 | caltopleft = {2000,200}
386 |
387 | -- Put timelapsed to somewhere by defining topleft point.
388 | timelapsetopleft = {200,1800}
389 | ```
390 |
391 |
392 |
393 | ## FAQ
394 |
395 |
396 | How can I integrate my little script into this configutation?
397 |
398 | Use `private` folder and `~/.hammerspoon/private/awesomeconfig.lua` file.
399 |
400 | If your script is just a few lines, then put it into `~/.hammerspoon/private/awesomeconfig.lua` file. If it is long enough, create a file in `private` folder, e.g. `mymodule.lua` (Wow, you just create a "module" without extra code), then include this module in `~/.hammerspoon/private/awesomeconfig.lua` file.
401 |
402 | module_list = {
403 | ...
404 | "private/mymodule",
405 | }
406 |
407 |
408 |
409 | ## Thanks to
410 |
411 |
412 | More details
413 |
414 | [http://www.hammerspoon.org/](http://www.hammerspoon.org/)
415 |
416 | [https://github.com/zzamboni/oh-my-hammerspoon](https://github.com/zzamboni/oh-my-hammerspoon)
417 |
418 | [https://github.com/scottcs/dot_hammerspoon](https://github.com/scottcs/dot_hammerspoon)
419 |
420 | [https://github.com/dharmapoudel/hammerspoon-config](https://github.com/dharmapoudel/hammerspoon-config)
421 |
422 | [http://tracesof.net/uebersicht/](http://tracesof.net/uebersicht/)
423 |
424 |
425 |
426 | ## Welcome to
427 |
428 | Share your scripts and thoughts.
429 |
430 | : )
431 |
--------------------------------------------------------------------------------
/init.lua:
--------------------------------------------------------------------------------
1 | require "preload"
2 |
3 | hs.hotkey.alertDuration=0
4 | hs.hints.showTitleThresh = 0
5 | hs.window.animationDuration = 0
6 |
7 | white = hs.drawing.color.white
8 | black = hs.drawing.color.black
9 | blue = hs.drawing.color.blue
10 | osx_red = hs.drawing.color.osx_red
11 | osx_green = hs.drawing.color.osx_green
12 | osx_yellow = hs.drawing.color.osx_yellow
13 | tomato = hs.drawing.color.x11.tomato
14 | dodgerblue = hs.drawing.color.x11.dodgerblue
15 | firebrick = hs.drawing.color.x11.firebrick
16 | lawngreen = hs.drawing.color.x11.lawngreen
17 | lightseagreen = hs.drawing.color.x11.lightseagreen
18 | purple = hs.drawing.color.x11.purple
19 | royalblue = hs.drawing.color.x11.royalblue
20 | sandybrown = hs.drawing.color.x11.sandybrown
21 | black50 = {red=0,blue=0,green=0,alpha=0.5}
22 | darkblue = {red=24/255,blue=195/255,green=145/255,alpha=1}
23 | gray = {red=246/255,blue=246/255,green=246/255,alpha=0.3}
24 |
25 | mod0 = {"cmd", "ctrl", "shift"}
26 | appmod = {"cmd", "ctrl"}
27 |
28 | privatepath = hs.fs.pathToAbsolute(hs.configdir..'/private')
29 | if privatepath == nil then
30 | hs.fs.mkdir(hs.configdir..'/private')
31 | end
32 | privateconf = hs.fs.pathToAbsolute(hs.configdir..'/private/awesomeconfig.lua')
33 | if privateconf ~= nil then
34 | require('private/awesomeconfig')
35 | end
36 |
37 | hsreload_keys = hsreload_keys or {mod0, "R"}
38 | if string.len(hsreload_keys[2]) > 0 then
39 | hs.hotkey.bind(hsreload_keys[1], hsreload_keys[2], "Reload Configuration", function() hs.reload() end)
40 | end
41 |
42 | if modalmgr == nil then
43 | showtime_lkeys = showtime_lkeys or {mod0, "T"}
44 | if string.len(showtime_lkeys[2]) > 0 then
45 | hs.hotkey.bind(showtime_lkeys[1], showtime_lkeys[2], 'Show Digital Clock', function() show_time() end)
46 | end
47 |
48 | show_screen_numbers_lkeys = show_screen_numbers_lkeys or {mod0, "Q"}
49 | if string.len(show_screen_numbers_lkeys[2]) > 0 then
50 | hs.hotkey.bind(show_screen_numbers_lkeys[1], show_screen_numbers_lkeys[2], 'Show Screen Numbers', function() show_screen_numbers() end)
51 | end
52 | end
53 |
54 | showhotkey_keys = showhotkey_keys or {mod0, "space"}
55 | if string.len(showhotkey_keys[2]) > 0 then
56 | hs.hotkey.bind(showhotkey_keys[1], showhotkey_keys[2], "Toggle Hotkeys Cheatsheet", function() showavailableHotkey() end)
57 | end
58 |
59 | times = {}
60 |
61 | function destroy_time(idx)
62 | local time = times[idx]
63 | if time then
64 | if time.draw then
65 | time.draw:delete()
66 | time.draw = nil
67 | end
68 | times[idx] = nil
69 | end
70 | end
71 |
72 | function show_time()
73 | for i=1,#hs.screen.allScreens() do
74 | local screen = hs.screen.allScreens()[i]
75 | if not times[screen:id()] then
76 | local time = {}
77 | times[screen:id()] = time
78 | local mainRes = screen:fullFrame()
79 | local localMainRes = screen:absoluteToLocal(mainRes)
80 | local time_str = hs.styledtext.new(os.date("%H:%M"),{font={name="Impact",size=120},color=darkblue,paragraphStyle={alignment="center"}})
81 | local timeframe = hs.geometry.rect(screen:localToAbsolute((localMainRes.w-300)/2,(localMainRes.h-200)/2,300,150))
82 | time.draw = hs.drawing.text(timeframe,time_str)
83 | time.draw:setLevel(hs.drawing.windowLevels.overlay)
84 | time.draw:show()
85 | if time.ttimer == nil then
86 | time.ttimer = hs.timer.doAfter(1, function() destroy_time(screen:id()) end)
87 | else
88 | time.ttimer:start()
89 | end
90 | else
91 | local time = times[screen:id()]
92 | if time.ttimer then
93 | time.ttimer:stop()
94 | end
95 | destroy_time(screen:id())
96 | end
97 | end
98 | end
99 |
100 | screen_numbers = {}
101 |
102 | function destroy_screen_number(idx)
103 | local screen_number = screen_numbers[idx]
104 | if screen_number then
105 | if screen_number.draw then
106 | screen_number.draw:delete()
107 | screen_number.draw = nil
108 | end
109 | screen_numbers[idx] = nil
110 | end
111 | end
112 |
113 | function show_screen_numbers()
114 | for i=1,#hs.screen.allScreens() do
115 | local screen = hs.screen.allScreens()[i]
116 | if not screen_numbers[screen:id()] then
117 | screen_number = {}
118 | screen_numbers[screen:id()] = screen_number
119 | local mainRes = screen:fullFrame()
120 | local localMainRes = screen:absoluteToLocal(mainRes)
121 | local number_str = hs.styledtext.new(i,{font={name="Impact",size=160},color=lawngreen,paragraphStyle={alignment="center"}})
122 | local numberframe = hs.geometry.rect(screen:localToAbsolute((localMainRes.w-300)/2,(localMainRes.h-240)/2,300,200))
123 | screen_number.draw = hs.drawing.text(numberframe,number_str)
124 | screen_number.draw:setLevel(hs.drawing.windowLevels.overlay)
125 | screen_number.draw:show()
126 | if screen_number.ttimer == nil then
127 | screen_number.ttimer = hs.timer.doAfter(1, function() destroy_screen_number(screen:id()) end)
128 | else
129 | screen_number.ttimer:start()
130 | end
131 | else
132 | screen_number = screen_numbers[screen:id()]
133 | if screen_number.ttimer then
134 | screen_number.ttimer:stop()
135 | end
136 | destroy_screen_number(screen:id())
137 | end
138 | end
139 | end
140 |
141 | function showavailableHotkey()
142 | if not hotkeytext then
143 | local hotkey_list=hs.hotkey.getHotkeys()
144 | local mainScreen = hs.screen.mainScreen()
145 | local mainRes = mainScreen:fullFrame()
146 | local localMainRes = mainScreen:absoluteToLocal(mainRes)
147 | local width = math.min(864, localMainRes.w * 3 / 5)
148 | local height = math.min(540, localMainRes.h * 3 / 5)
149 | local hkbgrect = hs.geometry.rect(mainScreen:localToAbsolute((localMainRes.w-width)/2,(localMainRes.h-height)/2,width,height))
150 | hotkeybg = hs.drawing.rectangle(hkbgrect)
151 | -- hotkeybg:setStroke(false)
152 | if not hotkey_tips_bg then hotkey_tips_bg = "light" end
153 | if hotkey_tips_bg == "light" then
154 | hotkeybg:setFillColor({red=238/255,blue=238/255,green=238/255,alpha=0.95})
155 | elseif hotkey_tips_bg == "dark" then
156 | hotkeybg:setFillColor({red=0,blue=0,green=0,alpha=0.95})
157 | end
158 | hotkeybg:setRoundedRectRadii(10,10)
159 | hotkeybg:setLevel(hs.drawing.windowLevels.modalPanel)
160 | hotkeybg:behavior(hs.drawing.windowBehaviors.stationary)
161 | local hktextrect = hs.geometry.rect(hkbgrect.x+40,hkbgrect.y+30,hkbgrect.w-80,hkbgrect.h-60)
162 | hotkeytext = hs.drawing.text(hktextrect,"")
163 | hotkeytext:setLevel(hs.drawing.windowLevels.modalPanel)
164 | hotkeytext:behavior(hs.drawing.windowBehaviors.stationary)
165 | hotkeytext:setClickCallback(nil,function() hotkeytext:delete() hotkeytext=nil hotkeybg:delete() hotkeybg=nil end)
166 | hotkey_filtered = {}
167 | for i=1,#hotkey_list do
168 | if hotkey_list[i].idx ~= hotkey_list[i].msg then
169 | table.insert(hotkey_filtered,hotkey_list[i])
170 | end
171 | end
172 | local availablelen = 80
173 | local hkstr = ''
174 | for i=2,#hotkey_filtered,2 do
175 | local tmpstr = hotkey_filtered[i-1].msg .. hotkey_filtered[i].msg
176 | if string.len(tmpstr)<= availablelen then
177 | local tofilllen = availablelen-string.len(hotkey_filtered[i-1].msg)
178 | hkstr = hkstr .. string.format('%-80s', hotkey_filtered[i-1].msg) .. string.format('%'..tofilllen..'s',hotkey_filtered[i].msg) .. '\n'
179 | else
180 | hkstr = hkstr .. hotkey_filtered[i-1].msg .. '\n' .. hotkey_filtered[i].msg .. '\n'
181 | end
182 | end
183 | if math.fmod(#hotkey_filtered,2) == 1 then hkstr = hkstr .. hotkey_filtered[#hotkey_filtered].msg end
184 | local hkstr_styled = hs.styledtext.new(hkstr, {font={name="Courier-Bold",size=16}, color=dodgerblue, paragraphStyle={lineSpacing=12.0,lineBreak='truncateMiddle'}, shadow={offset={h=0,w=0},blurRadius=0.5,color=darkblue}})
185 | hotkeytext:setStyledText(hkstr_styled)
186 | hotkeybg:show()
187 | hotkeytext:show()
188 | else
189 | hotkeytext:delete()
190 | hotkeytext=nil
191 | hotkeybg:delete()
192 | hotkeybg=nil
193 | end
194 | end
195 |
196 | modal_list = {}
197 |
198 | function modal_stat(color,alpha)
199 | if not modal_tray then
200 | local mainScreen = hs.screen.mainScreen()
201 | local mainRes = mainScreen:fullFrame()
202 | local localMainRes = mainScreen:absoluteToLocal(mainRes)
203 | modal_tray = hs.canvas.new(mainScreen:localToAbsolute({x=localMainRes.w-40,y=localMainRes.h-40,w=20,h=20}))
204 | modal_tray[1] = {action="fill",type="circle",fillColor=white}
205 | modal_tray[1].fillColor.alpha=0.7
206 | modal_tray[2] = {action="fill",type="circle",fillColor=white,radius="40%"}
207 | modal_tray:level(hs.canvas.windowLevels.status)
208 | modal_tray:clickActivating(false)
209 | modal_tray:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces + hs.canvas.windowBehaviors.stationary)
210 | modal_tray._default.trackMouseDown = true
211 | end
212 | modal_tray:show()
213 | modal_tray[2].fillColor = color
214 | modal_tray[2].fillColor.alpha = alpha
215 | end
216 |
217 | activeModals = {}
218 | function exit_others(excepts)
219 | function isInExcepts(value,tbl)
220 | for i=1,#tbl do
221 | if tbl[i] == value then
222 | return true
223 | end
224 | end
225 | return false
226 | end
227 | if excepts == nil then excepts = {} end
228 | for i = 1, #activeModals do
229 | if not isInExcepts(activeModals[i].id, excepts) then
230 | activeModals[i].modal:exit()
231 | end
232 | end
233 | end
234 |
235 | function move_win(direction)
236 | local win = hs.window.focusedWindow()
237 | local screen = win:screen()
238 | local screens = hs.screen.allScreens()
239 | if win then
240 | if direction == 'up' then win:moveOneScreenNorth() end
241 | if direction == 'down' then win:moveOneScreenSouth() end
242 | if direction == 'left' then win:moveOneScreenWest() end
243 | if direction == 'right' then win:moveOneScreenEast() end
244 | if direction == 'next' then win:moveToScreen(screen:next()) end
245 | if direction == 'first' then win:moveToScreen(screens[1]) end
246 | if direction == 'second' then win:moveToScreen(screens[2]) end
247 | if direction == 'third' then win:moveToScreen(screens[3]) end
248 | if direction == 'fourth' then win:moveToScreen(screens[4]) end
249 | end
250 | end
251 |
252 | function resize_win(direction)
253 | local win = hs.window.focusedWindow()
254 | if win then
255 | local f = win:frame()
256 | local screen = win:screen()
257 | local localf = screen:absoluteToLocal(f)
258 | local max = screen:fullFrame()
259 | local stepw = max.w/30
260 | local steph = max.h/30
261 | if direction == "right" then
262 | localf.w = localf.w+stepw
263 | local absolutef = screen:localToAbsolute(localf)
264 | win:setFrame(absolutef)
265 | end
266 | if direction == "left" then
267 | localf.w = localf.w-stepw
268 | local absolutef = screen:localToAbsolute(localf)
269 | win:setFrame(absolutef)
270 | end
271 | if direction == "up" then
272 | localf.h = localf.h-steph
273 | local absolutef = screen:localToAbsolute(localf)
274 | win:setFrame(absolutef)
275 | end
276 | if direction == "down" then
277 | localf.h = localf.h+steph
278 | local absolutef = screen:localToAbsolute(localf)
279 | win:setFrame(absolutef)
280 | end
281 | if direction == "halfright" then
282 | localf.x = max.w/2 localf.y = 0 localf.w = max.w/2 localf.h = max.h
283 | local absolutef = screen:localToAbsolute(localf)
284 | win:setFrame(absolutef)
285 | end
286 | if direction == "halfleft" then
287 | localf.x = 0 localf.y = 0 localf.w = max.w/2 localf.h = max.h
288 | local absolutef = screen:localToAbsolute(localf)
289 | win:setFrame(absolutef)
290 | end
291 | if direction == "halfup" then
292 | localf.x = 0 localf.y = 0 localf.w = max.w localf.h = max.h/2
293 | local absolutef = screen:localToAbsolute(localf)
294 | win:setFrame(absolutef)
295 | end
296 | if direction == "halfdown" then
297 | localf.x = 0 localf.y = max.h/2 localf.w = max.w localf.h = max.h/2
298 | local absolutef = screen:localToAbsolute(localf)
299 | win:setFrame(absolutef)
300 | end
301 | if direction == "cornerNE" then
302 | localf.x = max.w/2 localf.y = 0 localf.w = max.w/2 localf.h = max.h/2
303 | local absolutef = screen:localToAbsolute(localf)
304 | win:setFrame(absolutef)
305 | end
306 | if direction == "cornerSE" then
307 | localf.x = max.w/2 localf.y = max.h/2 localf.w = max.w/2 localf.h = max.h/2
308 | local absolutef = screen:localToAbsolute(localf)
309 | win:setFrame(absolutef)
310 | end
311 | if direction == "cornerNW" then
312 | localf.x = 0 localf.y = 0 localf.w = max.w/2 localf.h = max.h/2
313 | local absolutef = screen:localToAbsolute(localf)
314 | win:setFrame(absolutef)
315 | end
316 | if direction == "cornerSW" then
317 | localf.x = 0 localf.y = max.h/2 localf.w = max.w/2 localf.h = max.h/2
318 | local absolutef = screen:localToAbsolute(localf)
319 | win:setFrame(absolutef)
320 | end
321 | if direction == "center" then
322 | localf.x = (max.w-localf.w)/2 localf.y = (max.h-localf.h)/2
323 | local absolutef = screen:localToAbsolute(localf)
324 | win:setFrame(absolutef)
325 | end
326 | if direction == "fcenter" then
327 | localf.x = stepw*5 localf.y = steph*5 localf.w = stepw*20 localf.h = steph*20
328 | local absolutef = screen:localToAbsolute(localf)
329 | win:setFrame(absolutef)
330 | end
331 | if direction == "fullscreen" then
332 | win:toggleFullScreen()
333 | end
334 | if direction == "maximize" then
335 | localf.x = 0 localf.y = 0 localf.w = max.w localf.h = max.h
336 | local absolutef = screen:localToAbsolute(localf)
337 | win:setFrame(absolutef)
338 | end
339 | if direction == "shrink" then
340 | localf.x = localf.x+stepw localf.y = localf.y+steph localf.w = localf.w-(stepw*2) localf.h = localf.h-(steph*2)
341 | local absolutef = screen:localToAbsolute(localf)
342 | win:setFrame(absolutef)
343 | end
344 | if direction == "expand" then
345 | localf.x = localf.x-stepw localf.y = localf.y-steph localf.w = localf.w+(stepw*2) localf.h = localf.h+(steph*2)
346 | local absolutef = screen:localToAbsolute(localf)
347 | win:setFrame(absolutef)
348 | end
349 | if direction == "mright" then
350 | localf.x = localf.x+stepw
351 | local absolutef = screen:localToAbsolute(localf)
352 | win:setFrame(absolutef)
353 | end
354 | if direction == "mleft" then
355 | localf.x = localf.x-stepw
356 | local absolutef = screen:localToAbsolute(localf)
357 | win:setFrame(absolutef)
358 | end
359 | if direction == "mup" then
360 | localf.y = localf.y-steph
361 | local absolutef = screen:localToAbsolute(localf)
362 | win:setFrame(absolutef)
363 | end
364 | if direction == "mdown" then
365 | localf.y = localf.y+steph
366 | local absolutef = screen:localToAbsolute(localf)
367 | win:setFrame(absolutef)
368 | end
369 | if direction == "ccursor" then
370 | localf.x = localf.x+localf.w/2 localf.y = localf.y+localf.h/2
371 | hs.mouse.setRelativePosition({x=localf.x,y=localf.y},screen)
372 | end
373 | else
374 | hs.alert.show("No focused window!")
375 | end
376 | end
377 |
378 | function highlightActiveWin()
379 | if fw() then
380 | local rect = hs.drawing.rectangle(fw():frame())
381 | rect:setStrokeColor({["red"]=1, ["blue"]=0, ["green"]=1, ["alpha"]=1})
382 | rect:setStrokeWidth(5)
383 | rect:setFill(false)
384 | rect:show()
385 | hs.timer.doAfter(0.3, function() rect:delete() end)
386 | end
387 | end
388 |
389 | -- Fetch next index but cycle back when at the end
390 | --
391 | -- > getNextIndex({1,2,3}, 3)
392 | -- 1
393 | -- > getNextIndex({1}, 1)
394 | -- 1
395 | -- @return int
396 | local function getNextIndex(table, currentIndex)
397 | nextIndex = currentIndex + 1
398 | if nextIndex > #table then
399 | nextIndex = 1
400 | end
401 |
402 | return nextIndex
403 | end
404 |
405 | local function getNextWindow(windows, window)
406 | if type(windows) == "string" then
407 | windows = hs.application.find(windows):allWindows()
408 | end
409 |
410 | windows = hs.fnutils.filter(windows, function(win)
411 | return win:isStandard() and win:isVisible()
412 | end)
413 |
414 | -- need to sort by ID, since the default order of the window
415 | -- isn't usable when we change the mainWindow
416 | -- since mainWindow is always the first of the windows
417 | -- hence we would always get the window succeeding mainWindow
418 | table.sort(windows, function(w1, w2)
419 | return w1:id() > w2:id()
420 | end)
421 |
422 | lastIndex = hs.fnutils.indexOf(windows, window)
423 | if not lastIndex then return window end
424 |
425 | return windows[getNextIndex(windows, lastIndex)]
426 | end
427 |
428 | -- Needed to enable cycling of application windows
429 | lastToggledApplication = ''
430 |
431 | function launchOrCycleFocus(applicationName)
432 | return function()
433 | local nextWindow = nil
434 | local targetWindow = nil
435 | local focusedWindow = hs.window.focusedWindow()
436 | local lastToggledApplication = focusedWindow and focusedWindow:application():name()
437 |
438 | if not focusedWindow then return nil end
439 | if lastToggledApplication == applicationName then
440 | nextWindow = getNextWindow(applicationName, focusedWindow)
441 | -- Becoming main means
442 | -- * gain focus (although docs say differently?)
443 | -- * next call to launchOrFocus will focus the main window <- important
444 | -- * when fetching allWindows() from an application mainWindow will be the first one
445 | --
446 | -- If we have two applications, each with multiple windows
447 | -- i.e:
448 | --
449 | -- Google Chrome: {window1} {window2}
450 | -- Firefox: {window1} {window2} {window3}
451 | --
452 | -- and we want to move between Google Chrome {window2} and Firefox {window3}
453 | -- when pressing the hotkeys for those applications, then using becomeMain
454 | -- we cycle until those windows (i.e press hotkey twice for Chrome) have focus
455 | -- and then the launchOrFocus will trigger that specific window.
456 | nextWindow:becomeMain()
457 | nextWindow:focus()
458 | else
459 | hs.application.launchOrFocus(applicationName)
460 | end
461 |
462 | if nextWindow then
463 | targetWindow = nextWindow
464 | else
465 | targetWindow = hs.window.focusedWindow()
466 | end
467 |
468 | if not targetWindow then
469 | return nil
470 | end
471 | end
472 | end
473 |
474 | function activateApp(appname)
475 | launchOrCycleFocus(appname)()
476 | local app = hs.application.find(appname)
477 | if app then
478 | app:activate()
479 | hs.timer.doAfter(0.1, highlightActiveWin)
480 | app:unhide()
481 | end
482 | end
483 |
484 | resize_win_bindings = {
485 | { key = {mod0, "left"}, dir = "halfleft", tip = "Lefthalf of Screen" },
486 | { key = {mod0, "right"}, dir = "halfright", tip = "Righthalf of Screen" },
487 | { key = {mod0, "up"}, dir = "halfup", tip = "Uphalf of Screen" },
488 | { key = {mod0, "down"}, dir = "halfdown", tip = "Downhalf of Screen" },
489 | { key = {mod0, "O"}, dir = "cornerNE", tip = "NorthEast Corner" },
490 | { key = {mod0, "Y"}, dir = "cornerNW", tip = "NorthWest Corner" },
491 | { key = {mod0, "U"}, dir = "cornerSW", tip = "SouthWest Corner" },
492 | { key = {mod0, "I"}, dir = "cornerSE", tip = "SouthEast Corner" },
493 | { key = {mod0, "C"}, dir = "center", tip = "Center Window" },
494 | { key = {mod0, "M"}, dir = "maximize", tip = "Maximize Window" },
495 | { key = {mod0, "F"}, dir = "fullscreen", tip = "Fullscreen Window" },
496 | }
497 |
498 | move_win_bindings = {
499 | { key = {mod0, "n"}, dir = "next", tip = "Move to next screen" },
500 | { key = {mod0, "1"}, dir = "first", tip = "Move to first screen" },
501 | { key = {mod0, "2"}, dir = "second", tip = "Move to second screen" },
502 | { key = {mod0, "3"}, dir = "third", tip = "Move to third screen" },
503 | { key = {mod0, "4"}, dir = "fourth", tip = "Move to fourth screen" },
504 | }
505 |
506 | applist = {
507 | {shortcut = 'c', appname = 'Google Chrome'},
508 | {shortcut = 'e', appname = 'Microsoft Excel'},
509 | {shortcut = 'f', appname = 'Finder'},
510 | {shortcut = 'i', appname = 'Amazon Chime'},
511 | {shortcut = 'j', appname = 'IntelliJ IDEA'},
512 | {shortcut = 'm', appname = 'NeteaseMusic'},
513 | {shortcut = 'o', appname = 'Microsoft Outlook'},
514 | {shortcut = 'p', appname = 'Microsoft PowerPoint'},
515 | {shortcut = 'r', appname = 'Firefox'},
516 | {shortcut = 't', appname = 'iTerm2'},
517 | {shortcut = 'w', appname = 'Microsoft Word'},
518 | {shortcut = 'x', appname = 'WeChat'},
519 | }
520 |
521 | hs.fnutils.each(resize_win_bindings, function(item)
522 | hs.hotkey.bind(item.key[1], item.key[2], item.tip, function() resize_win(item.dir) end)
523 | end)
524 |
525 | hs.fnutils.each(move_win_bindings, function(item)
526 | hs.hotkey.bind(item.key[1], item.key[2], item.tip, function() move_win(item.dir) end)
527 | end)
528 |
529 | hs.fnutils.each(applist, function(item)
530 | hs.hotkey.bind(appmod, item.shortcut, item.appname, function() activateApp(item.appname) end)
531 | end)
532 |
533 | if not module_list then
534 | module_list = {
535 | "widgets/caffeine",
536 | "widgets/netspeed",
537 | "widgets/calendar",
538 | "widgets/hcalendar",
539 | "widgets/analogclock",
540 | "widgets/timelapsed",
541 | "widgets/aria2",
542 | "modes/basicmode",
543 | "modes/indicator",
544 | "modes/clipshow",
545 | "modes/cheatsheet",
546 | "modes/hsearch",
547 | "misc/bingdaily",
548 | }
549 | end
550 |
551 | hs.fnutils.each(module_list, function(module)
552 | require(module)
553 | end)
554 |
555 | if #modal_list > 0 then require("modalmgr") end
556 |
557 | globalGC = hs.timer.doEvery(180, collectgarbage)
558 | globalScreenWatcher = hs.screen.watcher.newWithActiveScreen(function(activeChanged)
559 | if activeChanged then
560 | exit_others()
561 | clipshowclear()
562 | if modal_tray then modal_tray:delete() modal_tray = nil end
563 | if hotkeytext then hotkeytext:delete() hotkeytext = nil end
564 | if hotkeybg then hotkeybg:delete() hotkeybg = nil end
565 | for i=1,#hs.screen.allScreens() do
566 | local screen = hs.screen.allScreens()[i]
567 | destroy_time(screen:id())
568 | destroy_screen_number(screen:id())
569 | end
570 | if cheatsheet_view then cheatsheet_view:delete() cheatsheet_view = nil end
571 | end
572 | end):start()
573 |
574 | hs.alert.show("Config Loaded")
575 |
--------------------------------------------------------------------------------
/misc/bingdaily.lua:
--------------------------------------------------------------------------------
1 | user_agent_str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4"
2 | json_req_url = "http://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1"
3 | desktop_picture_db = os.getenv("HOME")..'/Library/Application Support/Dock/desktoppicture.db'
4 | bing_image_dir = os.getenv("HOME").."/Library/Caches/org.hammerspoon.Hammerspoon/images/"
5 |
6 | function bingDailyRequest()
7 | hs.http.asyncGet(json_req_url, {["User-Agent"]=user_agent_str}, function(stat,body,header)
8 | if stat == 200 then
9 | if pcall(function() hs.json.decode(body) end) then
10 | local decode_data = hs.json.decode(body)
11 | local pic_url = decode_data.images[1].url
12 | local pic_name = hs.http.urlParts(pic_url).lastPathComponent
13 | if bing_last_set_pic ~= pic_name then
14 | local full_url = "https://www.bing.com"..pic_url
15 | downloadBingImage(full_url)
16 | end
17 | end
18 | else
19 | print("Bing URL request failed!")
20 | end
21 | end)
22 | end
23 |
24 | function downloadBingImage(url)
25 | local function curl_callback(exitCode,stdOut,stdErr)
26 | if exitCode == 0 then
27 | bing_curl_task = nil
28 | bing_last_set_pic = hs.http.urlParts(url).lastPathComponent
29 | local localpath = bing_image_dir..hs.http.urlParts(url).lastPathComponent
30 | bingSetAsWallpaper(localpath)
31 | os.execute("ls -t "..bing_image_dir.." | tail -n+30 | xargs -I{} rm -f "..bing_image_dir.."/{}")
32 | else
33 | print(stdOut,stdErr)
34 | end
35 | end
36 | if bing_curl_task then
37 | bing_curl_task:terminate()
38 | bing_curl_task = nil
39 | end
40 | local localpath = bing_image_dir..hs.http.urlParts(url).lastPathComponent
41 | local mkdir_output, mkdir_status = os.execute("mkdir -p "..bing_image_dir)
42 | if not mkdir_status then
43 | print("Failed to create directory: "..bing_image_dir)
44 | return
45 | end
46 | bing_curl_task = hs.task.new("/usr/bin/curl",curl_callback,{"-A",user_agent_str,url,"-o",localpath})
47 | bing_curl_task:start()
48 | end
49 |
50 | function setAsWallpaperByApplescript(filepath)
51 | local applescript = 'tell application "System Events"\nset picture of every desktop to "'..filepath..'"\nend tell'
52 | local stat, data = hs.osascript.applescript(applescript)
53 | if not stat then
54 | print("AppleScript failed.")
55 | end
56 | end
57 |
58 | function setAsWallpaperByShellscript(filepath)
59 | local query_script = 'sqlite3 "'..desktop_picture_db..'" "select value from data" 2>/dev/null | grep -v "'..hs.fs.displayName(filepath)..'" 2>/dev/null'
60 | local to_update, query_status = hs.execute(query_script)
61 | if not query_status or string.len(to_update) == 0 then
62 | print("No need to set desktop picture by shell script")
63 | return
64 | end
65 |
66 | local shellscript = "sqlite3 \""..desktop_picture_db.."\" \"update data set value = '"..filepath.."'\" && killall Dock"
67 | local outout, status, type, rc = hs.execute(shellscript)
68 | if not status then
69 | print("ShellScript failed.")
70 | end
71 | end
72 |
73 | function bingSetAsWallpaper(filepath)
74 | if hs.fs.displayName(desktop_picture_db) then
75 | setAsWallpaperByShellscript(filepath)
76 | end
77 | setAsWallpaperByApplescript(filepath)
78 | end
79 |
80 | if bingdaily_timer == nil then
81 | bingdaily_timer = hs.timer.doEvery(3*60*60, function() bingDailyRequest() end)
82 | bingdaily_timer:setNextTrigger(5)
83 | else
84 | bingdaily_timer:start()
85 | end
86 |
--------------------------------------------------------------------------------
/modalmgr.lua:
--------------------------------------------------------------------------------
1 | modalmgr_keys = modalmgr_keys or {{"alt"}, "space"}
2 | modalmgr = hs.hotkey.modal.new(modalmgr_keys[1], modalmgr_keys[2], 'Enter Main Mode')
3 | local modalpkg = {}
4 | modalpkg.id = "mainM"
5 | modalpkg.modal = modalmgr
6 | table.insert(modal_list, modalpkg)
7 |
8 | function modalmgr:entered()
9 | for i=1,#modal_list do
10 | if modal_list[i].id == "mainM" then
11 | table.insert(activeModals, modal_list[i])
12 | end
13 | end
14 | showavailableHotkey()
15 | end
16 |
17 | function modalmgr:exited()
18 | if modal_tray then modal_tray:hide() end
19 | if hotkeytext then
20 | hotkeytext:delete()
21 | hotkeytext=nil
22 | hotkeybg:delete()
23 | hotkeybg=nil
24 | end
25 | end
26 |
27 | modalmgr:bind("", "space", "Alfred 3", function() exit_others() activateApp("Alfred 3") end)
28 | modalmgr:bind("", "escape", "Exit Main Mode", function() modalmgr:exit() end)
29 | modalmgr:bind("", "Q", "Exit Main Mode", function() modalmgr:exit() end)
30 |
31 | if appM then
32 | appM_keys = appM_keys or {"", "A"}
33 | if string.len(appM_keys[2]) > 0 then
34 | appM:bind(modalmgr_keys[1], modalmgr_keys[2], "Enter Main Mode", function() exit_others() modalmgr:enter() end)
35 | modalmgr:bind(appM_keys[1], appM_keys[2], 'Enter Application Mode', function() exit_others() appM:enter() end)
36 | end
37 | end
38 |
39 | if clipboardM then
40 | clipboardM_keys = clipboardM_keys or {"", "C"}
41 | if string.len(clipboardM_keys[2]) > 0 then
42 | clipboardM:bind(modalmgr_keys[1], modalmgr_keys[2], "Enter Main Mode", function() exit_others() modalmgr:enter() end)
43 | modalmgr:bind(clipboardM_keys[1], clipboardM_keys[2], 'Enter Clipboard Mode', function() exit_others() clipboardM:enter() end)
44 | end
45 | end
46 |
47 | if aria2_loaded then
48 | aria2_keys = aria2_keys or {"", "D"}
49 | if string.len(aria2_keys[2]) > 0 then
50 | modalmgr:bind('', 'D', 'Launch aria2 Frontend', function()
51 | exit_others()
52 | if aria2_drawer then aria2_drawer:delete() aria2_drawer = nil end
53 | aria2_Init()
54 | end)
55 | end
56 | end
57 |
58 | if hsearch_loaded then
59 | hsearch_keys = hsearch_keys or {"", "G"}
60 | if string.len(hsearch_keys[2]) > 0 then
61 | modalmgr:bind(hsearch_keys[1], hsearch_keys[2], 'Launch Hammer Search', function() exit_others() launchChooser() end)
62 | end
63 | end
64 |
65 | if timerM then
66 | timerM_keys = timerM_keys or {"", "I"}
67 | if string.len(timerM_keys[2]) > 0 then
68 | timerM:bind(modalmgr_keys[1], modalmgr_keys[2], "Enter Main Mode", function() exit_others() modalmgr:enter() end)
69 | modalmgr:bind(timerM_keys[1], timerM_keys[2], 'Enter Timer Mode', function() exit_others() timerM:enter() end)
70 | end
71 | end
72 |
73 | if resizeM then
74 | resizeM_keys = resizeM_keys or {"", "R"}
75 | if string.len(resizeM_keys[2]) > 0 then
76 | resizeM:bind(modalmgr_keys[1], modalmgr_keys[2], "Enter Main Mode", function() exit_others() modalmgr:enter() end)
77 | modalmgr:bind(resizeM_keys[1], resizeM_keys[2], 'Enter Resize Mode', function() exit_others() resizeM:enter() end)
78 | end
79 | end
80 |
81 | if cheatsheetM then
82 | cheatsheetM_keys = cheatsheetM_keys or {"", "S"}
83 | if string.len(cheatsheetM_keys[2]) > 0 then
84 | cheatsheetM:bind(modalmgr_keys[1], modalmgr_keys[2], "Enter Main Mode", function() exit_others() modalmgr:enter() end)
85 | modalmgr:bind(cheatsheetM_keys[1], cheatsheetM_keys[2], 'Enter Cheatsheet Mode', function() exit_others() cheatsheetM:enter() end)
86 | end
87 | end
88 |
89 | showtime_keys = showtime_keys or {"", "T"}
90 | if string.len(showtime_keys[2]) > 0 then
91 | modalmgr:bind(showtime_keys[1], showtime_keys[2], 'Show Digital Clock', function() exit_others() show_time() end)
92 | end
93 |
94 | show_screen_numbers_keys = show_screen_numbers_keys or {"", "N"}
95 | if string.len(show_screen_numbers_keys[2]) > 0 then
96 | modalmgr:bind(show_screen_numbers_keys[1], show_screen_numbers_keys[2], 'Show Screen Numbers', function() exit_others() show_screen_numbers() end)
97 | end
98 |
99 | if viewM then
100 | viewM_keys = viewM_keys or {"", "V"}
101 | if string.len(viewM_keys[2]) > 0 then
102 | viewM:bind(modalmgr_keys[1], modalmgr_keys[2], "Enter Main Mode", function() exit_others() modalmgr:enter() end)
103 | modalmgr:bind(viewM_keys[1], viewM_keys[2], 'Enter View Mode', function() exit_others() viewM:enter() end)
104 | end
105 | end
106 |
107 | toggleconsole_keys = toggleconsole_keys or {"", "Z"}
108 | if string.len(toggleconsole_keys[2]) > 0 then
109 | modalmgr:bind(toggleconsole_keys[1], toggleconsole_keys[2], 'Toggle Hammerspoon Console', function() exit_others() hs.toggleConsole() end)
110 | end
111 |
112 | winhints_keys = winhints_keys or {"", "tab"}
113 | if string.len(winhints_keys[2]) > 0 then
114 | modalmgr:bind(winhints_keys[1], winhints_keys[2], 'Show Windows Hint', function() exit_others() hs.hints.windowHints() end)
115 | end
116 |
117 |
--------------------------------------------------------------------------------
/modes/basicmode.lua:
--------------------------------------------------------------------------------
1 | viewM = hs.hotkey.modal.new()
2 | local modalpkg = {}
3 | modalpkg.id = "viewM"
4 | modalpkg.modal = viewM
5 | table.insert(modal_list, modalpkg)
6 |
7 | function viewM:entered()
8 | modal_stat(royalblue,0.7)
9 | for i=1,#modal_list do
10 | if modal_list[i].id == "viewM" then
11 | table.insert(activeModals, modal_list[i])
12 | end
13 | end
14 | if hotkeytext then
15 | hotkeytext:delete()
16 | hotkeytext=nil
17 | hotkeybg:delete()
18 | hotkeybg=nil
19 | end
20 | end
21 |
22 | function viewM:exited()
23 | modal_tray:hide()
24 | for i=1,#activeModals do
25 | if activeModals[i].id == "viewM" then
26 | table.remove(activeModals, i)
27 | end
28 | end
29 | if hotkeytext then
30 | hotkeytext:delete()
31 | hotkeytext=nil
32 | hotkeybg:delete()
33 | hotkeybg=nil
34 | end
35 | end
36 |
37 | viewM:bind('', 'escape', function() viewM:exit() end)
38 | viewM:bind('', 'Q', function() viewM:exit() end)
39 | viewM:bind('', 'tab', function() showavailableHotkey() end)
40 | viewM:bind('', 'H', 'Scroll Leftward', function() hs.eventtap.scrollWheel({1,0},{},"line") end, nil, function() hs.eventtap.scrollWheel({1,0},{},"line") end)
41 | viewM:bind('', 'L', 'Scroll Rightward', function() hs.eventtap.scrollWheel({-1,0},{},"line") end, nil, function() hs.eventtap.scrollWheel({-1,0},{},"line") end)
42 | viewM:bind('', 'J', 'Scroll Downward', function() hs.eventtap.scrollWheel({0,-1},{},"line") end, nil, function() hs.eventtap.scrollWheel({0,-1},{},"line") end)
43 | viewM:bind('', 'K', 'Scroll Upward', function() hs.eventtap.scrollWheel({0,1},{},"line") end, nil, function() hs.eventtap.scrollWheel({0,1},{},"line") end)
44 | viewM:bind('ctrl', 'H', 'Move Mouse Leftward by 50px', function() moveMouseBy(-50,0) end, nil, function() moveMouseBy(-50,0) end)
45 | viewM:bind('ctrl', 'L', 'Move Mouse Rightward by 50px', function() moveMouseBy(50,0) end, nil, function() moveMouseBy(50,0) end)
46 | viewM:bind('ctrl', 'K', 'Move Mouse Upward by 50px', function() moveMouseBy(0,-50) end, nil, function() moveMouseBy(0,-50) end)
47 | viewM:bind('ctrl', 'J', 'Move Mouse Downward by 50px', function() moveMouseBy(0,50) end, nil, function() moveMouseBy(0,50) end)
48 | viewM:bind('shift', 'H', 'Move Mouse Leftward by 10px', function() moveMouseBy(-10,0) end, nil, function() moveMouseBy(-10,0) end)
49 | viewM:bind('shift', 'L', 'Move Mouse Rightward by 10px', function() moveMouseBy(10,0) end, nil, function() moveMouseBy(10,0) end)
50 | viewM:bind('shift', 'K', 'Move Mouse Upward by 10px', function() moveMouseBy(0,-10) end, nil, function() moveMouseBy(0,-10) end)
51 | viewM:bind('shift', 'J', 'Move Mouse Downward by 10px', function() moveMouseBy(0,10) end, nil, function() moveMouseBy(0,10) end)
52 | viewM:bind({'ctrl','shift'}, 'H', 'Move Mouse Leftward by 1px', function() moveMouseBy(-1,0) end, nil, function() moveMouseBy(-1,0) end)
53 | viewM:bind({'ctrl','shift'}, 'L', 'Move Mouse Rightward by 1px', function() moveMouseBy(1,0) end, nil, function() moveMouseBy(1,0) end)
54 | viewM:bind({'ctrl','shift'}, 'K', 'Move Mouse Upward by 1px', function() moveMouseBy(0,-1) end, nil, function() moveMouseBy(0,-1) end)
55 | viewM:bind({'ctrl','shift'}, 'J', 'Move Mouse Downward by 1px', function() moveMouseBy(0,1) end, nil, function() moveMouseBy(0,1) end)
56 | viewM:bind('', ',', 'Left Mouse Click', function() clickWithMouse('left') end, nil, nil)
57 | viewM:bind('', '.', 'Right Mouse Click', function() clickWithMouse('right') end, nil, nil)
58 |
59 | function moveMouseBy(offsetx,offsety)
60 | local currentpos = hs.mouse.getRelativePosition()
61 | local newpos = hs.geometry.point(currentpos.x+offsetx,currentpos.y+offsety)
62 | hs.mouse.setRelativePosition(newpos)
63 | end
64 |
65 | function clickWithMouse(opts)
66 | local currentpos = hs.mouse.getRelativePosition()
67 | if opts == 'left' then
68 | hs.eventtap.leftClick(currentpos)
69 | elseif opts == 'right' then
70 | hs.eventtap.rightClick(currentpos)
71 | end
72 | end
73 |
74 | resizeM = hs.hotkey.modal.new()
75 | local modalpkg = {}
76 | modalpkg.id = "resizeM"
77 | modalpkg.modal = resizeM
78 | table.insert(modal_list, modalpkg)
79 |
80 | function resizeM:entered()
81 | modal_stat(firebrick,0.7)
82 | resize_current_winnum = 1
83 | resize_win_list = hs.window.visibleWindows()
84 | for i=1,#modal_list do
85 | if modal_list[i].id == "resizeM" then
86 | table.insert(activeModals, modal_list[i])
87 | end
88 | end
89 | if hotkeytext then
90 | hotkeytext:delete()
91 | hotkeytext=nil
92 | hotkeybg:delete()
93 | hotkeybg=nil
94 | end
95 | if show_resize_tips == nil then show_resize_tips = true end
96 | if show_resize_tips == true then showavailableHotkey() end
97 | end
98 |
99 | function resizeM:exited()
100 | modal_tray:hide()
101 | for i=1,#activeModals do
102 | if activeModals[i].id == "resizeM" then
103 | table.remove(activeModals, i)
104 | end
105 | end
106 | if hotkeytext then
107 | hotkeytext:delete()
108 | hotkeytext=nil
109 | hotkeybg:delete()
110 | hotkeybg=nil
111 | end
112 | end
113 |
114 | resizeM:bind('', 'escape', function() resizeM:exit() end)
115 | resizeM:bind('', 'Q', function() resizeM:exit() end)
116 | resizeM:bind('', 'tab', function() showavailableHotkey() end)
117 | resizeM:bind('shift', 'Y', 'Shrink Leftward', function() resize_win('left') end, nil, function() resize_win('left') end)
118 | resizeM:bind('shift', 'O', 'Stretch Rightward', function() resize_win('right') end, nil, function() resize_win('right') end)
119 | resizeM:bind('shift', 'U', 'Stretch Downward', function() resize_win('down') end, nil, function() resize_win('down') end)
120 | resizeM:bind('shift', 'I', 'Shrink Upward', function() resize_win('up') end, nil, function() resize_win('up') end)
121 | resizeM:bind('', 'F', 'Fullscreen', function() resize_win('fullscreen') end, nil, nil)
122 | resizeM:bind('', 'M', 'Maximize Window', function() resize_win('maximize') end, nil, nil)
123 | resizeM:bind('', 'C', 'Center Window', function() resize_win('center') end, nil, nil)
124 | resizeM:bind('shift', 'C', 'Resize & Center', function() resize_win('fcenter') end, nil, nil)
125 | resizeM:bind('', 'H', 'Lefthalf of Screen', function() resize_win('halfleft') end, nil, nil)
126 | resizeM:bind('', 'J', 'Downhalf of Screen', function() resize_win('halfdown') end, nil, nil)
127 | resizeM:bind('', 'K', 'Uphalf of Screen', function() resize_win('halfup') end, nil, nil)
128 | resizeM:bind('', 'L', 'Righthalf of Screen', function() resize_win('halfright') end, nil, nil)
129 | resizeM:bind('', 'Y', 'NorthWest Corner', function() resize_win('cornerNW') end, nil, nil)
130 | resizeM:bind('', 'U', 'SouthWest Corner', function() resize_win('cornerSW') end, nil, nil)
131 | resizeM:bind('', 'I', 'SouthEast Corner', function() resize_win('cornerSE') end, nil, nil)
132 | resizeM:bind('', 'O', 'NorthEast Corner', function() resize_win('cornerNE') end, nil, nil)
133 | resizeM:bind('', '=', 'Stretch Outward', function() resize_win('expand') end, nil, function() resize_win('expand') end)
134 | resizeM:bind('', '-', 'Shrink Inward', function() resize_win('shrink') end, nil, function() resize_win('shrink') end)
135 | resizeM:bind('shift', 'H', 'Move Leftward', function() resize_win('mleft') end, nil, function() resize_win('mleft') end)
136 | resizeM:bind('shift', 'L', 'Move Rightward', function() resize_win('mright') end, nil, function() resize_win('mright') end)
137 | resizeM:bind('shift', 'J', 'Move Downward', function() resize_win('mdown') end, nil, function() resize_win('mdown') end)
138 | resizeM:bind('shift', 'K', 'Move Upward', function() resize_win('mup') end, nil, function() resize_win('mup') end)
139 | resizeM:bind('', '`', 'Center Cursor', function() resize_win('ccursor') end, nil, nil)
140 | resizeM:bind('', '[', 'Focus Westward', function() cycle_wins_pre() end, nil, function() cycle_wins_pre() end)
141 | resizeM:bind('', ']', 'Focus Eastward', function() cycle_wins_next() end, nil, function() cycle_wins_next() end)
142 | resizeM:bind('', 'up', 'Move to monitor above', function() move_win('up') end, nil, nil)
143 | resizeM:bind('', 'down', 'Move to monitor below', function() move_win('down') end, nil, nil)
144 | resizeM:bind('', 'right', 'Move to monitor right', function() move_win('right') end, nil, nil)
145 | resizeM:bind('', 'left', 'Move to monitor left', function() move_win('left') end, nil, nil)
146 | resizeM:bind('', 'space', 'Move to next monitor', function() move_win('next') end, nil, nil)
147 |
148 | function cycle_wins_next()
149 | resize_win_list[resize_current_winnum]:focus()
150 | resize_current_winnum = resize_current_winnum + 1
151 | if resize_current_winnum > #resize_win_list then resize_current_winnum = 1 end
152 | end
153 |
154 | function cycle_wins_pre()
155 | resize_win_list[resize_current_winnum]:focus()
156 | resize_current_winnum = resize_current_winnum - 1
157 | if resize_current_winnum < 1 then resize_current_winnum = #resize_win_list end
158 | end
159 |
160 | appM = hs.hotkey.modal.new()
161 | local modalpkg = {}
162 | modalpkg.id = "appM"
163 | modalpkg.modal = appM
164 | table.insert(modal_list, modalpkg)
165 |
166 | function appM:entered()
167 | for i=1,#modal_list do
168 | if modal_list[i].id == "appM" then
169 | table.insert(activeModals, modal_list[i])
170 | end
171 | end
172 | if hotkeytext then
173 | hotkeytext:delete()
174 | hotkeytext=nil
175 | hotkeybg:delete()
176 | hotkeybg=nil
177 | end
178 | if show_applauncher_tips == nil then show_applauncher_tips = true end
179 | if show_applauncher_tips == true then showavailableHotkey() end
180 | end
181 |
182 | function appM:exited()
183 | for i=1,#activeModals do
184 | if activeModals[i].id == "appM" then
185 | table.remove(activeModals, i)
186 | end
187 | end
188 | if hotkeytext then
189 | hotkeytext:delete()
190 | hotkeytext=nil
191 | hotkeybg:delete()
192 | hotkeybg=nil
193 | end
194 | end
195 |
196 | appM:bind('', 'escape', function() appM:exit() end)
197 | appM:bind('', 'Q', function() appM:exit() end)
198 | appM:bind('', 'tab', function() showavailableHotkey() end)
199 |
200 | if not applist then
201 | applist = {
202 | {shortcut = 'f',appname = 'Finder'},
203 | {shortcut = 's',appname = 'Safari'},
204 | {shortcut = 't',appname = 'Terminal'},
205 | {shortcut = 'v',appname = 'Activity Monitor'},
206 | {shortcut = 'y',appname = 'System Preferences'},
207 | }
208 | end
209 |
210 | for i = 1, #applist do
211 | appM:bind('', applist[i].shortcut, applist[i].appname, function()
212 | activateApp(applist[i].appname)
213 | appM:exit()
214 | if hotkeytext then
215 | hotkeytext:delete()
216 | hotkeytext=nil
217 | hotkeybg:delete()
218 | hotkeybg=nil
219 | end
220 | end)
221 | end
222 |
--------------------------------------------------------------------------------
/modes/cheatsheet.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------
2 | --/ Cheatsheet Copycat /--
3 | ------------------------------------------------------------------------
4 |
5 | hs.application.menuGlyphs[148]="fn fn"
6 |
7 | commandEnum = {
8 | cmd = '⌘',
9 | shift = '⇧',
10 | alt = '⌥',
11 | ctrl = '⌃',
12 | }
13 |
14 | function getAllMenuItems(t)
15 | local menu = ""
16 | for pos,val in pairs(t) do
17 | if type(val)=="table" then
18 | -- TODO: Remove menubar items with no shortcuts in them
19 | if val.AXRole =="AXMenuBarItem" and type(val.AXChildren) == "table" then
20 | menu = menu..""
21 | menu = menu.."- "..val.AXTitle.."
"
22 | menu = menu.. getAllMenuItems(val.AXChildren[1])
23 | menu = menu.."
"
24 | elseif val.AXRole =="AXMenuItem" and not val.AXChildren then
25 | if not (val.AXMenuItemCmdChar == '' and val.AXMenuItemCmdGlyph == '') then
26 | local CmdModifiers = ''
27 | for key, value in pairs(val.AXMenuItemCmdModifiers) do
28 | CmdModifiers = CmdModifiers..commandEnum[value]
29 | end
30 | local CmdChar = val.AXMenuItemCmdChar
31 | local CmdGlyph = hs.application.menuGlyphs[val.AXMenuItemCmdGlyph] or ''
32 | local CmdKeys = CmdChar..CmdGlyph
33 | menu = menu..""..CmdModifiers.." "..CmdKeys.."
".." "..val.AXTitle.."
"
34 | end
35 | elseif val.AXRole == "AXMenuItem" and type(val.AXChildren) == "table" then
36 | menu = menu..getAllMenuItems(val.AXChildren[1])
37 | end
38 |
39 | end
40 | end
41 | return menu
42 | end
43 |
44 | function generateHtml()
45 | local focusedApp = hs.application.frontmostApplication()
46 | local appTitle = focusedApp:title()
47 | local allMenuItems = focusedApp:getMenuItems()
48 | local myMenuItems = getAllMenuItems(allMenuItems)
49 |
50 | local html = [[
51 |
52 |
53 |
54 |
132 |
133 |
134 |
135 | ]]..appTitle..[[
136 |
137 |
138 | ]]..myMenuItems..[[
139 |
140 |
141 |
149 |
150 |
158 |
159 |
160 | ]]
161 |
162 | return html
163 | end
164 |
165 | function showCheatsheet()
166 | if not cheatsheet_view then
167 | local mainScreen = hs.screen.mainScreen()
168 | local mainRes = mainScreen:fullFrame()
169 | local localMainRes = mainScreen:absoluteToLocal(mainRes)
170 | local cheatsheet_rect = mainScreen:localToAbsolute({
171 | x = (localMainRes.w-1080)/2,
172 | y = (localMainRes.h-600)/2,
173 | w = 1080,
174 | h = 600,
175 | })
176 | cheatsheet_view = hs.webview.new(cheatsheet_rect)
177 | :windowTitle("CheatSheets")
178 | :windowStyle("utility")
179 | :allowGestures(true)
180 | :allowNewWindows(false)
181 | :level(hs.drawing.windowLevels.modalPanel)
182 | end
183 | if cstimer ~= nil and cstimer:running() then
184 | cstimer:stop()
185 | end
186 | cheatsheet_view:show()
187 | cheatsheet_view:html(generateHtml())
188 | end
189 |
190 | cheatsheetM = hs.hotkey.modal.new()
191 | local modalpkg = {}
192 | modalpkg.id = "cheatsheetM"
193 | modalpkg.modal = cheatsheetM
194 | table.insert(modal_list, modalpkg)
195 |
196 | function cheatsheetM:entered()
197 | for i=1,#modal_list do
198 | if modal_list[i].id == "cheatsheetM" then
199 | table.insert(activeModals, modal_list[i])
200 | end
201 | end
202 | showCheatsheet()
203 | end
204 |
205 | function cheatsheetM:exited()
206 | for i=1,#activeModals do
207 | if activeModals[i].id == "cheatsheetM" then
208 | table.remove(activeModals, i)
209 | end
210 | end
211 | if cheatsheet_view ~= nil then
212 | cheatsheet_view:hide()
213 | if cstimer == nil then
214 | cstimer = hs.timer.doAfter(10*60, function()
215 | if cheatsheet_view ~= nil then
216 | cheatsheet_view:delete()
217 | cheatsheet_view = nil
218 | end
219 | end)
220 | else
221 | cstimer:start()
222 | end
223 | end
224 | end
225 |
226 | cheatsheetM:bind('', 'escape', function() cheatsheetM:exit() end)
227 | cheatsheetM:bind('', 'Q', function() cheatsheetM:exit() end)
228 |
--------------------------------------------------------------------------------
/modes/clipshow.lua:
--------------------------------------------------------------------------------
1 | function clipshow()
2 | if clipDrawn == nil then
3 | local mainScreen = hs.screen.mainScreen()
4 | local mainRes = mainScreen:fullFrame()
5 | local localMainRes = mainScreen:absoluteToLocal(mainRes)
6 | clipType = hs.pasteboard.typesAvailable()
7 | if clipType.image == true then
8 | local imagedata = hs.pasteboard.readImage()
9 | local imagesize = imagedata:size()
10 | if imagesize.w < 480 and imagesize.h < 480 then
11 | centerimgframe = hs.geometry.rect(mainScreen:localToAbsolute((localMainRes.w-480)/2,(localMainRes.h-480)/2,480,480))
12 | else
13 | centerimgframe = hs.geometry.rect(mainScreen:localToAbsolute((localMainRes.w-imagesize.w)/2,(localMainRes.h-imagesize.h)/2,imagesize.w,imagesize.h))
14 | end
15 | imageshow = hs.drawing.image(centerimgframe,imagedata)
16 | imageshow:setLevel(hs.drawing.windowLevels.modalPanel)
17 | imageshow:setBehavior(hs.drawing.windowBehaviors.stationary)
18 | imageshow:show()
19 | clipDrawn = true
20 | imageshow:setClickCallback(nil,function() imageshow:delete() clipboardM:exit() clipDrawn=nil end)
21 | elseif clipType.URL == true then
22 | local URLdata = hs.pasteboard.readURL()
23 | local defaultbrowser = hs.urlevent.getDefaultHandler('http')
24 | hs.urlevent.openURLWithBundle(URLdata,defaultbrowser)
25 | clipboardM:exit()
26 | elseif clipType.styledText == true then
27 | local textdata = hs.pasteboard.readString()
28 | -- textdata = hs.pasteboard.readStyledText()
29 | local matchurl = string.match(textdata,'https?://%w[-.%w]*:?%d*/?[%w_.~!*:@&+$/?%%#=-]*')
30 | if matchurl == textdata then
31 | local defaultbrowser = hs.urlevent.getDefaultHandler('http')
32 | hs.urlevent.openURLWithBundle(textdata,defaultbrowser)
33 | clipboardM:exit()
34 | else
35 | local bgframe = mainScreen:localToAbsolute(hs.geometry.rect(localMainRes.x,localMainRes.h/5,localMainRes.w,localMainRes.h/5*3))
36 | clipbackground = hs.drawing.rectangle(bgframe)
37 | clipbackground:setLevel(hs.drawing.windowLevels.modalPanel)
38 | clipbackground:setBehavior(hs.drawing.windowBehaviors.stationary)
39 | clipbackground:setFill(true)
40 | clipbackground:setFillColor({red=0,blue=0,green=0,alpha=0.75})
41 | clipbackground:show()
42 | textframe = hs.geometry.rect(bgframe.x+20,bgframe.y+20,bgframe.w-40,bgframe.h-40)
43 | textshow = hs.drawing.text(textframe,textdata)
44 | textshow:setLevel(hs.drawing.windowLevels.modalPanel)
45 | textshow:setBehavior(hs.drawing.windowBehaviors.stationary)
46 | if string.len(textdata) < 180 then
47 | textshow:setTextSize(80.0)
48 | else
49 | textshow:setTextSize(50.0)
50 | end
51 | textshow:show()
52 | clipDrawn = true
53 | clipbackground:setClickCallback(nil,function() clipbackground:delete() textshow:delete() clipboardM:exit() clipDrawn=nil end)
54 | end
55 | else
56 | hs.alert.show("Empty clipboard or unsupported type.")
57 | if clipboardM then clipboardM:exit() end
58 | end
59 | end
60 | end
61 |
62 | function clipshowclear()
63 | if clipDrawn == true then
64 | if imageshow ~= nil then imageshow:delete() imageshow=nil end
65 | if clipbackground ~= nil then clipbackground:delete() clipbackground=nil textshow:delete() textshow=nil end
66 | clipDrawn = nil
67 | end
68 | end
69 |
70 | clipboardM = hs.hotkey.modal.new()
71 | local modalpkg = {}
72 | modalpkg.id = "clipboardM"
73 | modalpkg.modal = clipboardM
74 | table.insert(modal_list, modalpkg)
75 |
76 | function clipboardM:entered()
77 | for i=1,#modal_list do
78 | if modal_list[i].id == "clipboardM" then
79 | table.insert(activeModals, modal_list[i])
80 | end
81 | end
82 | clipshow()
83 | end
84 |
85 | function clipboardM:exited()
86 | for i=1,#activeModals do
87 | if activeModals[i].id == "clipboardM" then
88 | table.remove(activeModals, i)
89 | end
90 | end
91 | clipshowclear()
92 | end
93 |
94 | clipboardM:bind('', 'escape', function() clipboardM:exit() end)
95 | clipboardM:bind('', 'Q', function() clipboardM:exit() end)
96 |
--------------------------------------------------------------------------------
/modes/hsearch.lua:
--------------------------------------------------------------------------------
1 | hsearch_loaded = true
2 | if youdaokeyfrom == nil then youdaokeyfrom = 'hsearch' end
3 | if youdaoapikey == nil then youdaoapikey = '1199732752' end
4 | chooserSourceTable = {}
5 | chooserSourceOverview = {}
6 |
7 | function switchSource()
8 | local function isInKeywords(value, tbl)
9 | for i=1,#tbl do
10 | if tbl[i].kw == value then
11 | sourcetable_index = i
12 | return true
13 | end
14 | end
15 | return false
16 | end
17 | local querystr = search_chooser:query()
18 | if string.len(querystr) > 0 then
19 | local matchstr = string.match(querystr,"^%w+")
20 | if matchstr == querystr then
21 | if isInKeywords(querystr, chooserSourceTable) then
22 | search_chooser:query('')
23 | chooser_data = {}
24 | search_chooser:choices(chooser_data)
25 | search_chooser:queryChangedCallback()
26 | chooserSourceTable[sourcetable_index].func()
27 | else
28 | local selected_content = search_chooser:selectedRowContents()
29 | local source_kw = selected_content.sourceKeyword or ""
30 | if isInKeywords(source_kw, chooserSourceTable) then
31 | search_chooser:query('')
32 | chooser_data = {}
33 | search_chooser:choices(chooser_data)
34 | search_chooser:queryChangedCallback()
35 | chooserSourceTable[sourcetable_index].func()
36 | else
37 | sourcetable_index = nil
38 | chooser_data = {}
39 | local source_desc = {text="No source found!", subText="Maybe misspelled the keyword?"}
40 | table.insert(chooser_data, 1, source_desc)
41 | local more_tips = {text="Want to add your own source?", subText="Feel free to read the code and open PRs. :)"}
42 | table.insert(chooser_data, 2, more_tips)
43 | search_chooser:choices(chooser_data)
44 | search_chooser:queryChangedCallback()
45 | hs.eventtap.keyStroke({"cmd"}, "a")
46 | end
47 | end
48 | else
49 | sourcetable_index = nil
50 | chooser_data = {}
51 | local source_desc = {text="Invalid Keyword", subText="Trigger keyword must only consist of alphanumeric characters."}
52 | table.insert(chooser_data, 1, source_desc)
53 | search_chooser:choices(chooser_data)
54 | search_chooser:queryChangedCallback()
55 | hs.eventtap.keyStroke({"cmd"}, "a")
56 | end
57 | else
58 | local selected_content = search_chooser:selectedRowContents()
59 | local source_kw = selected_content.sourceKeyword or ""
60 | if isInKeywords(source_kw, chooserSourceTable) then
61 | search_chooser:query('')
62 | chooser_data = {}
63 | search_chooser:choices(chooser_data)
64 | search_chooser:queryChangedCallback()
65 | chooserSourceTable[sourcetable_index].func()
66 | else
67 | sourcetable_index = nil
68 | chooser_data = chooserSourceOverview
69 | search_chooser:choices(chooser_data)
70 | search_chooser:queryChangedCallback()
71 | end
72 | end
73 | if hs_emoji_data then hs_emoji_data:close() hs_emoji_data = nil end
74 | if sourcetable_index == nil then
75 | if justnotetrigger then justnotetrigger:disable() end
76 | else
77 | if chooserSourceTable[sourcetable_index].kw ~= "n" then
78 | if justnotetrigger then justnotetrigger:disable() end
79 | end
80 | end
81 | end
82 |
83 | function launchChooser()
84 | if sourcetrigger == nil then
85 | sourcetrigger = hs.hotkey.bind("","tab",nil,switchSource)
86 | else
87 | sourcetrigger:enable()
88 | end
89 | if chooserSourceTable[sourcetable_index] then
90 | if chooserSourceTable[sourcetable_index].kw == "n" then
91 | if justnotetrigger then justnotetrigger:enable() end
92 | end
93 | end
94 | if search_chooser == nil then
95 | chooser_data = {}
96 | search_chooser = hs.chooser.new(function(chosen)
97 | sourcetrigger:disable()
98 | if justnotetrigger then justnotetrigger:disable() end
99 | if chosen ~= nil then
100 | if chosen.outputType == "safari" then
101 | hs.urlevent.openURLWithBundle(chosen.url,"com.apple.Safari")
102 | elseif chosen.outputType == "chrome" then
103 | hs.urlevent.openURLWithBundle(chosen.url,"com.google.Chrome")
104 | elseif chosen.outputType == "firefox" then
105 | hs.urlevent.openURLWithBundle(chosen.url,"org.mozilla.firefox")
106 | elseif chosen.outputType == "browser" then
107 | local defaultbrowser = hs.urlevent.getDefaultHandler('http')
108 | hs.urlevent.openURLWithBundle(chosen.url,defaultbrowser)
109 | elseif chosen.outputType == "clipboard" then
110 | hs.pasteboard.setContents(chosen.clipText)
111 | elseif chosen.outputType == "keystrokes" then
112 | hs.window.orderedWindows()[1]:focus()
113 | hs.eventtap.keyStrokes(chosen.typingText)
114 | elseif chosen.outputType == "taskkill" then
115 | os.execute("kill -9 "..chosen.pid)
116 | elseif chosen.outputType == "menuclick" then
117 | hs_belongto_app:activate()
118 | hs_belongto_app:selectMenuItem(chosen.menuitem)
119 | elseif chosen.outputType == "noteremove" then
120 | justnotetrigger:disable()
121 | for idx,val in pairs(hs_justnote_history) do
122 | if val.uuid == chosen.uuid then
123 | table.remove(hs_justnote_history,idx)
124 | hs.settings.set("just.another.note", hs_justnote_history)
125 | end
126 | end
127 | justNoteRequest()
128 | search_chooser:choices(chooser_data)
129 | end
130 | end
131 | end)
132 | search_chooser:query('')
133 | search_chooser:queryChangedCallback()
134 | chooser_data = chooserSourceOverview
135 | search_chooser:choices(chooser_data)
136 | search_chooser:rows(9)
137 | end
138 | search_chooser:show()
139 | end
140 |
141 | function browserTabsRequest()
142 | local safari_running = hs.application'com.apple.Safari'
143 | if safari_running then
144 | local stat, data= hs.osascript.applescript('tell application "Safari"\nset winlist to tabs of windows\nset tablist to {}\nrepeat with i in winlist\nif (count of i) > 0 then\nrepeat with currenttab in i\nset tabinfo to {name of currenttab as unicode text, URL of currenttab}\ncopy tabinfo to the end of tablist\nend repeat\nend if\nend repeat\nreturn tablist\nend tell')
145 | if stat then
146 | chooser_data = hs.fnutils.imap(data, function(item)
147 | return {text=item[1], subText=item[2], image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/safari.png")), outputType="safari", url=item[2]}
148 | end)
149 | end
150 | end
151 | local chrome_running = hs.application'com.google.Chrome'
152 | if chrome_running then
153 | local stat, data= hs.osascript.applescript('tell application "Google Chrome"\nset winlist to tabs of windows\nset tablist to {}\nrepeat with i in winlist\nif (count of i) > 0 then\nrepeat with currenttab in i\nset tabinfo to {name of currenttab as unicode text, URL of currenttab}\ncopy tabinfo to the end of tablist\nend repeat\nend if\nend repeat\nreturn tablist\nend tell')
154 | if stat then
155 | for idx,val in pairs(data) do
156 | table.insert(chooser_data, {text=val[1], subText=val[2], image=hs.image.imageFromPath("/Applications/Google Chrome.app/Contents/Resources/document.icns"), outputType="chrome", url=val[2]})
157 | end
158 | end
159 | end
160 | end
161 |
162 | function browserSource()
163 | local browsersource_overview = {text="Type t ⇥ to search safari/chrome Tabs.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/tabs.png")), sourceKeyword="t"}
164 | table.insert(chooserSourceOverview,browsersource_overview)
165 | function browserFunc()
166 | local source_desc = {text="Requesting data, please wait a while …"}
167 | table.insert(chooser_data, 1, source_desc)
168 | search_chooser:choices(chooser_data)
169 | browserTabsRequest()
170 | local source_desc = {text="Browser Tabs Search", subText="Search and select one item to open in corresponding browser.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."./resources/tabs.png"))}
171 | table.insert(chooser_data, 1, source_desc)
172 | search_chooser:choices(chooser_data)
173 | search_chooser:queryChangedCallback()
174 | search_chooser:searchSubText(true)
175 | end
176 | local sourcepkg = {}
177 | sourcepkg.kw = "t"
178 | sourcepkg.func = browserFunc
179 | table.insert(chooserSourceTable,sourcepkg)
180 | end
181 |
182 | browserSource()
183 |
184 | function youdaoInstantTrans(querystr)
185 | local youdao_baseurl = 'http://fanyi.youdao.com/openapi.do?keyfrom='..youdaokeyfrom..'&key='..youdaoapikey..'&type=data&doctype=json&version=1.1&q='
186 | if string.len(querystr) > 0 then
187 | local encoded_query = hs.http.encodeForQuery(querystr)
188 | local query_url = youdao_baseurl..encoded_query
189 |
190 | hs.http.asyncGet(query_url,nil,function(status,data)
191 | if status == 200 then
192 | if pcall(function() hs.json.decode(data) end) then
193 | local decoded_data = hs.json.decode(data)
194 | if decoded_data.errorCode == 0 then
195 | if decoded_data.basic then
196 | basictrans = decoded_data.basic.explains
197 | else
198 | basictrans = {}
199 | end
200 | if decoded_data.web then
201 | webtrans = hs.fnutils.imap(decoded_data.web,function(item) return item.key..' '..table.concat(item.value,',') end)
202 | else
203 | webtrans = {}
204 | end
205 | dictpool = hs.fnutils.concat(basictrans,webtrans)
206 | if #dictpool > 0 then
207 | chooser_data = hs.fnutils.imap(dictpool, function(item)
208 | return {text=item, image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/youdao.png")), outputType="clipboard", clipText=item}
209 | end)
210 | search_chooser:choices(chooser_data)
211 | search_chooser:refreshChoicesCallback()
212 | end
213 | end
214 | end
215 | end
216 | end)
217 | else
218 | chooser_data = {}
219 | local source_desc = {text="Youdao Dictionary", subText="Type something to get it translated …", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/youdao.png"))}
220 | table.insert(chooser_data, 1, source_desc)
221 | search_chooser:choices(chooser_data)
222 | end
223 | end
224 |
225 | function youdaoSource()
226 | local youdaosource_overview = {text="Type y ⇥ to use Yaodao dictionary.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/youdao.png")), sourceKeyword="y"}
227 | table.insert(chooserSourceOverview,youdaosource_overview)
228 | function youdaoFunc()
229 | local source_desc = {text="Youdao Dictionary", subText="Type something to get it translated …", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/youdao.png"))}
230 | table.insert(chooser_data, 1, source_desc)
231 | search_chooser:choices(chooser_data)
232 | search_chooser:queryChangedCallback(youdaoInstantTrans)
233 | end
234 | local sourcepkg = {}
235 | sourcepkg.kw = "y"
236 | sourcepkg.func = youdaoFunc
237 | table.insert(chooserSourceTable,sourcepkg)
238 | end
239 |
240 | youdaoSource()
241 |
242 | --------------------------------------------------------------------------------
243 | -- Add a new source - kill processes
244 | -- First request processes info and store them into $chooser_data$
245 |
246 | local function splitByLine(str)
247 | local tailtrimmedstr = string.gsub(str,"%s+$","")
248 | local tmptbl = {}
249 | for w in string.gmatch(tailtrimmedstr,"[^\n]+") do table.insert(tmptbl,w) end
250 | if #tmptbl == 1 then
251 | local trimmedstr = string.gsub(tmptbl[1],"%s","")
252 | return trimmedstr
253 | else
254 | local tmptbl2 = {}
255 | for _,val in pairs(tmptbl) do
256 | local trimmedstr = string.gsub(val,"%s","")
257 | table.insert(tmptbl2,trimmedstr)
258 | end
259 | return tmptbl2
260 | end
261 | end
262 |
263 | function appsInfoRequest()
264 | local taskname_tbl = splitByLine(hs.execute("ps -ero ucomm"))
265 | local pid_tbl = splitByLine(hs.execute("ps -ero pid"))
266 | local comm_tbl = splitByLine(hs.execute("ps -ero command"))
267 | for i=2,#taskname_tbl do
268 | local taskname = taskname_tbl[i]
269 | local pid = tonumber(pid_tbl[i])
270 | local comm = comm_tbl[i]
271 | local appbundle = hs.application.applicationForPID(pid)
272 | local function getBundleID()
273 | if appbundle then
274 | return appbundle:bundleID()
275 | end
276 | end
277 | local bundleid = getBundleID() or "nil"
278 | local function getAppImage()
279 | if bundleid ~= "nil" then
280 | return hs.image.imageFromAppBundle(bundleid)
281 | else
282 | return hs.image.iconForFileType("public.unix-executable")
283 | end
284 | end
285 | local appimage = getAppImage()
286 | local appinfoitem = {text=taskname.."#"..pid.." "..bundleid, subText=comm, image=appimage, outputType="taskkill", pid=pid}
287 | table.insert(chooser_data,appinfoitem)
288 | end
289 | end
290 |
291 | -- Then we wrap the worker into appkillSource
292 |
293 | function appKillSource()
294 | -- Give some tips for this source
295 | local appkillsource_overview = {text="Type k ⇥ to Kill running process.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/taskkill.png")), sourceKeyword="k"}
296 | table.insert(chooserSourceOverview,appkillsource_overview)
297 | -- Run the function below when triggered.
298 | function appkillFunc()
299 | -- Request appsinfo
300 | appsInfoRequest()
301 | -- More tips
302 | local source_desc = {text="Kill Processes", subText="Search and select some items to get them killed.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/taskkill.png"))}
303 | table.insert(chooser_data, 1, source_desc)
304 | -- Make $chooser_data$ appear in search_chooser
305 | search_chooser:choices(chooser_data)
306 | -- Run some code or do nothing while querystring changed
307 | search_chooser:queryChangedCallback()
308 | -- Do something when select one item in search_chooser
309 | end
310 | local sourcepkg = {}
311 | -- Give this source a trigger keyword
312 | sourcepkg.kw = "k"
313 | sourcepkg.func = appkillFunc
314 | -- Add this source to SourceTable
315 | table.insert(chooserSourceTable,sourcepkg)
316 | end
317 |
318 | -- Run the function once, so search_chooser can actually see the new source
319 | appKillSource()
320 |
321 | -- New source - kill processes End here
322 | --------------------------------------------------------------------------------
323 |
324 |
325 | --------------------------------------------------------------------------------
326 | -- New source - Datamuse thesaurus
327 |
328 | function thesaurusRequest(querystr)
329 | local datamuse_baseurl = 'http://api.datamuse.com'
330 | if string.len(querystr) > 0 then
331 | local encoded_query = hs.http.encodeForQuery(querystr)
332 | local query_url = datamuse_baseurl..'/words?ml='..encoded_query..'&max=20'
333 |
334 | hs.http.asyncGet(query_url,nil,function(status,data)
335 | if status == 200 then
336 | if pcall(function() hs.json.decode(data) end) then
337 | local decoded_data = hs.json.decode(data)
338 | if #decoded_data > 0 then
339 | chooser_data = hs.fnutils.imap(decoded_data, function(item)
340 | return {text = item.word, image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/thesaurus.png")), outputType="keystrokes", typingText=item.word}
341 | end)
342 | search_chooser:choices(chooser_data)
343 | search_chooser:refreshChoicesCallback()
344 | end
345 | end
346 | end
347 | end)
348 | else
349 | chooser_data = {}
350 | local source_desc = {text="Datamuse Thesaurus", subText="Type something to get more words like it …", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/thesaurus.png"))}
351 | table.insert(chooser_data, 1, source_desc)
352 | search_chooser:choices(chooser_data)
353 | end
354 | end
355 |
356 | function thesaurusSource()
357 | local thesaurus_overview = {text="Type s ⇥ to request English Thesaurus.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/thesaurus.png")), sourceKeyword="s"}
358 | table.insert(chooserSourceOverview,thesaurus_overview)
359 | function thesaurusFunc()
360 | local source_desc = {text="Datamuse Thesaurus", subText="Type something to get more words like it …", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/thesaurus.png"))}
361 | table.insert(chooser_data, 1, source_desc)
362 | search_chooser:choices(chooser_data)
363 | search_chooser:queryChangedCallback(thesaurusRequest)
364 | end
365 | local sourcepkg = {}
366 | sourcepkg.kw = "s"
367 | sourcepkg.func = thesaurusFunc
368 | -- Add this source to SourceTable
369 | table.insert(chooserSourceTable,sourcepkg)
370 | end
371 |
372 | thesaurusSource()
373 |
374 | -- New source - Datamuse Thesaurus End here
375 | --------------------------------------------------------------------------------
376 |
377 | --------------------------------------------------------------------------------
378 | -- New source - Menuitems Search
379 |
380 | function table.clone(org)
381 | return {table.unpack(org)}
382 | end
383 |
384 | function getMenuChain(t)
385 | for pos,val in pairs(t) do
386 | if type(val) == "table" then
387 | if type(val.AXChildren) == "table" then
388 | hs_currentlevel = hs_currentlevel + 1
389 | hs_currententry[hs_currentlevel] = val.AXTitle
390 | getMenuChain(val.AXChildren[1])
391 | hs_currentlevel = hs_currentlevel - 1
392 | for i=hs_currentlevel+1,#hs_currententry do
393 | table.remove(hs_currententry,i)
394 | end
395 | elseif val.AXRole == "AXMenuItem" and not val.AXChildren then
396 | if val.AXTitle ~= "" then
397 | local upperlevel = table.clone(hs_currententry)
398 | table.insert(upperlevel,val.AXTitle)
399 | table.insert(hs_menuchain,upperlevel)
400 | end
401 | end
402 | end
403 | end
404 | end
405 |
406 | function MenuitemsRequest()
407 | local frontmost_win = hs.window.orderedWindows()[1]
408 | hs_belongto_app = frontmost_win:application()
409 | local all_menuitems = hs_belongto_app:getMenuItems()
410 | hs_menuchain = {}
411 | hs_currententry = {}
412 | hs_currentlevel = 0
413 | getMenuChain(all_menuitems)
414 | for idx,val in pairs(hs_menuchain) do
415 | local menuitem = {text=val[#val], subText=table.concat(val," | "), image=hs.image.imageFromAppBundle(hs_belongto_app:bundleID()), outputType="menuclick", menuitem=val}
416 | table.insert(chooser_data,menuitem)
417 | end
418 | end
419 |
420 | function MenuitemsSource()
421 | local menuitems_overview = {text="Type m ⇥ to search Menuitems.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/menus.png")), sourceKeyword="m"}
422 | table.insert(chooserSourceOverview,menuitems_overview)
423 | function menuitemsFunc()
424 | MenuitemsRequest()
425 | local source_desc = {text="Menuitems Search", subText="Search and select some menuitem to get it clicked.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/menus.png"))}
426 | table.insert(chooser_data, 1, source_desc)
427 | search_chooser:choices(chooser_data)
428 | search_chooser:queryChangedCallback()
429 | search_chooser:searchSubText(true)
430 | end
431 | local sourcepkg = {}
432 | sourcepkg.kw = "m"
433 | sourcepkg.func = menuitemsFunc
434 | -- Add this source to SourceTable
435 | table.insert(chooserSourceTable,sourcepkg)
436 | end
437 |
438 | MenuitemsSource()
439 |
440 | -- New source - Menuitems Search End here
441 | --------------------------------------------------------------------------------
442 |
443 | --------------------------------------------------------------------------------
444 | -- New source - v2ex Posts
445 |
446 | function v2exRequest()
447 | local query_url = 'https://www.v2ex.com/api/topics/latest.json'
448 | local stat, body = hs.http.asyncGet(query_url,nil,function(status,data)
449 | if status == 200 then
450 | if pcall(function() hs.json.decode(data) end) then
451 | local decoded_data = hs.json.decode(data)
452 | if #decoded_data > 0 then
453 | chooser_data = hs.fnutils.imap(decoded_data, function(item)
454 | local sub_content = string.gsub(item.content,"\r\n"," ")
455 | local function trim_content()
456 | if utf8.len(sub_content) > 40 then
457 | return string.sub(sub_content,1,utf8.offset(sub_content,40)-1)
458 | else
459 | return sub_content
460 | end
461 | end
462 | local final_content = trim_content()
463 | return {text=item.title, subText=final_content, image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/v2ex.png")), outputType="browser", url=item.url}
464 | end)
465 | local source_desc = {text="v2ex Posts", subText="Select some item to get it opened in default browser …", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/v2ex.png"))}
466 | table.insert(chooser_data, 1, source_desc)
467 | search_chooser:choices(chooser_data)
468 | search_chooser:refreshChoicesCallback()
469 | end
470 | end
471 | end
472 | end)
473 | end
474 |
475 | function v2exSource()
476 | local v2ex_overview = {text="Type v ⇥ to fetch v2ex posts.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/v2ex.png")), sourceKeyword="v"}
477 | table.insert(chooserSourceOverview,v2ex_overview)
478 | function v2exFunc()
479 | local source_desc = {text="Requesting data, please wait a while …"}
480 | table.insert(chooser_data, 1, source_desc)
481 | search_chooser:choices(chooser_data)
482 | v2exRequest()
483 | search_chooser:queryChangedCallback()
484 | search_chooser:searchSubText(true)
485 | end
486 | local sourcepkg = {}
487 | sourcepkg.kw = "v"
488 | sourcepkg.func = v2exFunc
489 | table.insert(chooserSourceTable,sourcepkg)
490 | end
491 |
492 | v2exSource()
493 |
494 | -- New source - v2ex Posts End here
495 | --------------------------------------------------------------------------------
496 |
497 | --------------------------------------------------------------------------------
498 | -- New source - Emoji Source
499 |
500 | function emojiRequest(querystr)
501 | local emoji_baseurl = 'https://emoji.getdango.com'
502 | if string.len(querystr) > 0 then
503 | local encoded_query = hs.http.encodeForQuery(querystr)
504 | local query_url = emoji_baseurl..'/api/emoji?q='..encoded_query
505 |
506 | hs.http.asyncGet(query_url,nil,function(status,data)
507 | if status == 200 then
508 | if pcall(function() hs.json.decode(data) end) then
509 | local decoded_data = hs.json.decode(data)
510 | if decoded_data.results and #decoded_data.results > 0 then
511 | if not hs_emoji_data then
512 | local emoji_database_path = "/System/Library/Input Methods/CharacterPalette.app/Contents/Resources/CharacterDB.sqlite3"
513 | hs_emoji_data = hs.sqlite3.open(emoji_database_path)
514 | end
515 | if hs_emoji_canvas then hs_emoji_canvas:delete() hs_emoji_canvas=nil end
516 | local hs_emoji_canvas = hs.canvas.new({x=0,y=0,w=96,h=96})
517 | chooser_data = hs.fnutils.imap(decoded_data.results, function(item)
518 | hs_emoji_canvas[1] = {type="text",text=item.text,textSize=64,frame={x="15%",y="10%",w="100%",h="100%"}}
519 | local hexcode = string.format("%#X",utf8.codepoint(item.text))
520 | local function getEmojiDesc()
521 | for w in hs_emoji_data:rows("SELECT info FROM unihan_dict WHERE uchr=\'"..item.text.."\'") do
522 | return w[1]
523 | end
524 | end
525 | local emoji_description = getEmojiDesc()
526 | local formatted_desc = string.gsub(emoji_description,"|||||||||||||||","")
527 | return {text = formatted_desc, image=hs_emoji_canvas:imageFromCanvas(), subText="Hex Code: "..hexcode, outputType="keystrokes", typingText=item.text}
528 | end)
529 | search_chooser:choices(chooser_data)
530 | search_chooser:refreshChoicesCallback()
531 | end
532 | end
533 | end
534 | end)
535 | else
536 | chooser_data = {}
537 | local source_desc = {text="Relevant Emoji", subText="Type something to find relevant emoji from text …", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/emoji.png"))}
538 | table.insert(chooser_data, 1, source_desc)
539 | search_chooser:choices(chooser_data)
540 | end
541 | end
542 |
543 | function emojiSource()
544 | local emoji_overview = {text="Type e ⇥ to find relevant Emoji.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/emoji.png")), sourceKeyword="e"}
545 | table.insert(chooserSourceOverview,emoji_overview)
546 | function emojiFunc()
547 | local source_desc = {text="Relevant Emoji", subText="Type something to find relevant emoji from text …", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/emoji.png"))}
548 | table.insert(chooser_data, 1, source_desc)
549 | search_chooser:choices(chooser_data)
550 | search_chooser:queryChangedCallback(emojiRequest)
551 | end
552 | local sourcepkg = {}
553 | sourcepkg.kw = "e"
554 | sourcepkg.func = emojiFunc
555 | -- Add this source to SourceTable
556 | table.insert(chooserSourceTable,sourcepkg)
557 | end
558 |
559 | emojiSource()
560 |
561 | -- New source - Emoji Source End here
562 | --------------------------------------------------------------------------------
563 |
564 | --------------------------------------------------------------------------------
565 | -- New source - Time Source
566 |
567 | function timeRequest()
568 | hs_time_commands = {
569 | '+"%Y-%m-%d"',
570 | '+"%H:%M:%S %p"',
571 | '+"%A, %B %d, %Y"',
572 | '+"%Y-%m-%d %H:%M:%S %p"',
573 | '+"%a, %b %d, %y"',
574 | '+"%m/%d/%y %H:%M %p"',
575 | '',
576 | '-u',
577 | }
578 | chooser_data = hs.fnutils.imap(hs_time_commands, function(item)
579 | local exec_result = hs.execute("date "..item)
580 | return {text=exec_result, subText="date "..item, image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/time.png")), outputType="keystrokes", typingText=exec_result}
581 | end)
582 | end
583 |
584 | local function splitBySpace(str)
585 | local tmptbl = {}
586 | for w in string.gmatch(str,"[+-]?%d+[ymdwHMS]") do table.insert(tmptbl,w) end
587 | return tmptbl
588 | end
589 |
590 | function timeDeltaRequest(querystr)
591 | if string.len(querystr) > 0 then
592 | local valid_inputs = splitBySpace(querystr)
593 | if #valid_inputs > 0 then
594 | local addv_before = hs.fnutils.imap(valid_inputs, function(item)
595 | return "-v"..item
596 | end)
597 | local vv_var = table.concat(addv_before," ")
598 | for idx,val in pairs(hs_time_commands) do
599 | local new_exec_command = "date "..vv_var.." "..val
600 | local new_exec_result = hs.execute(new_exec_command)
601 | chooser_data[idx+1].text = new_exec_result
602 | chooser_data[idx+1].subText = new_exec_command
603 | chooser_data[idx+1].typingText = new_exec_result
604 | search_chooser:choices(chooser_data)
605 | end
606 | else
607 | timeRequest()
608 | local source_desc = {text="Date Query", subText="Type +/-1d (or y, m, w, H, M, S) to query date forward or backward.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/time.png"))}
609 | table.insert(chooser_data, 1, source_desc)
610 | search_chooser:choices(chooser_data)
611 | end
612 | else
613 | timeRequest()
614 | local source_desc = {text="Date Query", subText="Type +/-1d (or y, m, w, H, M, S) to query date forward or backward.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/time.png"))}
615 | table.insert(chooser_data, 1, source_desc)
616 | search_chooser:choices(chooser_data)
617 | end
618 | end
619 |
620 | function timeSource()
621 | local time_overview = {text="Type d ⇥ to format/query Date.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/time.png")), sourceKeyword="d"}
622 | table.insert(chooserSourceOverview,time_overview)
623 | function timeFunc()
624 | timeRequest()
625 | local source_desc = {text="Date Query", subText="Type +/-1d (or y, m, w, H, M, S) to query date forward or backward.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/time.png"))}
626 | table.insert(chooser_data, 1, source_desc)
627 | search_chooser:choices(chooser_data)
628 | search_chooser:queryChangedCallback(timeDeltaRequest)
629 | end
630 | local sourcepkg = {}
631 | sourcepkg.kw = "d"
632 | sourcepkg.func = timeFunc
633 | -- Add this source to SourceTable
634 | table.insert(chooserSourceTable,sourcepkg)
635 | end
636 |
637 | timeSource()
638 |
639 | -- New source - Time Source End here
640 | --------------------------------------------------------------------------------
641 |
642 | --------------------------------------------------------------------------------
643 | -- New source - Just Note Source
644 |
645 | local function isInNoteHistory(value, tbl)
646 | for idx,val in pairs(tbl) do
647 | if val.uuid == value then
648 | return true
649 | end
650 | end
651 | return false
652 | end
653 |
654 | function justNoteRequest()
655 | hs_justnote_history = hs.settings.get("just.another.note") or {}
656 | if #hs_justnote_history == 0 then
657 | chooser_data = {{text="Write something and press Enter.", subText="Your notes is automatically saved, selected item will be erased.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/justnote.png"))}}
658 | else
659 | chooser_data = hs.fnutils.imap(hs_justnote_history, function(item)
660 | return {uuid=item.uuid, text=item.content, subText=item.ctime, image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/justnote.png")), outputType="noteremove"}
661 | end)
662 | end
663 | end
664 |
665 | function justNoteStore()
666 | local querystr = string.gsub(search_chooser:query(),"%s+$","")
667 | if string.len(querystr) > 0 then
668 | local query_hash = hs.hash.SHA1(querystr)
669 | if not isInNoteHistory(query_hash, hs_justnote_history) then
670 | table.insert(hs_justnote_history,{uuid=query_hash, ctime="Created at "..os.date(), content=querystr})
671 | hs.settings.set("just.another.note",hs_justnote_history)
672 | justNoteRequest()
673 | search_chooser:choices(chooser_data)
674 | search_chooser:query("")
675 | end
676 | end
677 | end
678 |
679 | function justNoteSource()
680 | local justnote_overview = {text="Type n ⇥ to Note something.", image=hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir.."/resources/justnote.png")), sourceKeyword="n"}
681 | table.insert(chooserSourceOverview,justnote_overview)
682 | function justnoteFunc()
683 | justNoteRequest()
684 | if justnotetrigger == nil then
685 | justnotetrigger = hs.hotkey.bind("","return",nil,justNoteStore)
686 | else
687 | justnotetrigger:enable()
688 | end
689 | search_chooser:choices(chooser_data)
690 | search_chooser:queryChangedCallback()
691 | end
692 | local sourcepkg = {}
693 | sourcepkg.kw = "n"
694 | sourcepkg.func = justnoteFunc
695 | -- Add this source to SourceTable
696 | table.insert(chooserSourceTable,sourcepkg)
697 | end
698 |
699 | justNoteSource()
700 |
701 | -- New source - Just Note Source End here
702 | --------------------------------------------------------------------------------
703 |
--------------------------------------------------------------------------------
/modes/indicator.lua:
--------------------------------------------------------------------------------
1 | function timer_indicator(timelen)
2 | if not indicator_used then
3 | indicator_used = hs.drawing.rectangle({0,0,0,0})
4 | indicator_used:setStroke(false)
5 | indicator_used:setFill(true)
6 | indicator_used:setFillColor(osx_red)
7 | indicator_used:setAlpha(0.35)
8 | indicator_used:setLevel(hs.drawing.windowLevels.status)
9 | indicator_used:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces+hs.drawing.windowBehaviors.stationary)
10 | indicator_used:show()
11 |
12 | indicator_left = hs.drawing.rectangle({0,0,0,0})
13 | indicator_left:setStroke(false)
14 | indicator_left:setFill(true)
15 | indicator_left:setFillColor(osx_green)
16 | indicator_left:setAlpha(0.35)
17 | indicator_left:setLevel(hs.drawing.windowLevels.status)
18 | indicator_left:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces+hs.drawing.windowBehaviors.stationary)
19 | indicator_left:show()
20 |
21 | totaltime=timelen
22 | if totaltime > 45*60 then
23 | time_interval = 5
24 | else
25 | time_interval = 1
26 | end
27 | if indict_timer == nil then
28 | indict_timer = hs.timer.doEvery(time_interval,updateused)
29 | else
30 | indict_timer:start()
31 | end
32 | used_slice = 0
33 | else
34 | indict_timer:stop()
35 | indicator_used:delete()
36 | indicator_used=nil
37 | indicator_left:delete()
38 | indicator_left=nil
39 | end
40 | end
41 |
42 | function updateused()
43 | local mainScreen = hs.screen.mainScreen()
44 | local mainRes = mainScreen:fullFrame()
45 | local localMainRes = mainScreen:absoluteToLocal(mainRes)
46 | local timeslice = localMainRes.w/(60*totaltime/time_interval)
47 | used_slice = used_slice + timeslice*time_interval
48 | if used_slice > localMainRes.w then
49 | indict_timer:stop()
50 | indicator_used:delete()
51 | indicator_used=nil
52 | indicator_left:delete()
53 | indicator_left=nil
54 | hs.notify.new({title="Time("..totaltime.." mins) is up!", informativeText="Now is "..os.date("%X")}):send()
55 | else
56 | left_slice = localMainRes.w - used_slice
57 | local used_rect = mainScreen:localToAbsolute(hs.geometry.rect(localMainRes.x,localMainRes.h-5,used_slice,5))
58 | local left_rect = mainScreen:localToAbsolute(hs.geometry.rect(localMainRes.x+used_slice,localMainRes.h-5,left_slice,5))
59 | indicator_used:setFrame(used_rect)
60 | indicator_left:setFrame(left_rect)
61 | end
62 | end
63 |
64 | timerM = hs.hotkey.modal.new()
65 | local modalpkg = {}
66 | modalpkg.id = "timerM"
67 | modalpkg.modal = timerM
68 | table.insert(modal_list, modalpkg)
69 |
70 | function timerM:entered()
71 | modal_stat(purple,0.7)
72 | for i=1,#modal_list do
73 | if modal_list[i].id == "timerM" then
74 | table.insert(activeModals, modal_list[i])
75 | end
76 | end
77 | if hotkeytext then
78 | hotkeytext:delete()
79 | hotkeytext=nil
80 | hotkeybg:delete()
81 | hotkeybg=nil
82 | end
83 | if show_timer_tips == nil then show_timer_tips = true end
84 | if show_timer_tips == true then showavailableHotkey() end
85 | end
86 |
87 | function timerM:exited()
88 | modal_tray:hide()
89 | for i=1,#activeModals do
90 | if activeModals[i].id == "timerM" then
91 | table.remove(activeModals, i)
92 | end
93 | end
94 | if hotkeytext then
95 | hotkeytext:delete()
96 | hotkeytext=nil
97 | hotkeybg:delete()
98 | hotkeybg=nil
99 | end
100 | end
101 |
102 | timerM:bind('', 'escape', function() timerM:exit() end)
103 | timerM:bind('', 'Q', function() timerM:exit() end)
104 | timerM:bind('', 'tab', function() showavailableHotkey() end)
105 | timerM:bind('', '1', '10 minute countdown', function() timer_indicator(10) timerM:exit() end)
106 | timerM:bind('', '2', '20 minute countdown', function() timer_indicator(20) timerM:exit() end)
107 | timerM:bind('', '3', '30 minute countdown', function() timer_indicator(30) timerM:exit() end)
108 | timerM:bind('', '4', '40 minute countdown', function() timer_indicator(40) timerM:exit() end)
109 | timerM:bind('', '5', '50 minute countdown', function() timer_indicator(50) timerM:exit() end)
110 | timerM:bind('', '6', '60 minute countdown', function() timer_indicator(60) timerM:exit() end)
111 | timerM:bind('', '7', '70 minute countdown', function() timer_indicator(70) timerM:exit() end)
112 | timerM:bind('', '8', '80 minute countdown', function() timer_indicator(80) timerM:exit() end)
113 | timerM:bind('', '9', '90 minute countdown', function() timer_indicator(90) timerM:exit() end)
114 | timerM:bind('', '0', '5 minute countdown', function() timer_indicator(5) timerM:exit() end)
115 | timerM:bind('', 'return', '25 minute countdown', function() timer_indicator(25) timerM:exit() end)
116 |
--------------------------------------------------------------------------------
/preload.lua:
--------------------------------------------------------------------------------
1 | -- The default duration for animations, in seconds. Initial value is 0.2; set to 0 to disable animations.
2 | hs.window.animationDuration = 0
3 |
4 | -- auto reload config
5 | configFileWatcher =
6 | hs.pathwatcher.new(hs.configdir, function(files)
7 | local isLuaFileChange = utils.some(files, function(p)
8 | return not (string.match(p, "^.+/([^%.#][^/]+%.lua)$") == nil)
9 | end)
10 | if isLuaFileChange then
11 | hs.reload()
12 | end
13 | end):start()
14 |
15 | -- persist console history across launches
16 | hs.shutdownCallback = function() hs.settings.set('history', hs.console.getHistory()) end
17 | hs.console.setHistory(hs.settings.get('history'))
18 |
19 | -- ensure CLI installed
20 | hs.ipc.cliInstall()
21 |
22 | -- helpful aliases
23 | i = hs.inspect
24 | fw = hs.window.focusedWindow
25 | fmt = string.format
26 | bind = hs.hotkey.bind
27 | alert = hs.alert.show
28 | clear = hs.console.clearConsole
29 | reload = hs.reload
30 | pbcopy = hs.pasteboard.setContents
31 | std = hs.stdlib and require("hs.stdlib")
32 | utils = hs.fnutils
33 | hyper = {'⌘', '⌃'}
34 |
--------------------------------------------------------------------------------
/resources/emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/emoji.png
--------------------------------------------------------------------------------
/resources/justnote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/justnote.png
--------------------------------------------------------------------------------
/resources/menus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/menus.png
--------------------------------------------------------------------------------
/resources/safari.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/safari.png
--------------------------------------------------------------------------------
/resources/tabs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/tabs.png
--------------------------------------------------------------------------------
/resources/taskkill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/taskkill.png
--------------------------------------------------------------------------------
/resources/thesaurus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/thesaurus.png
--------------------------------------------------------------------------------
/resources/time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/time.png
--------------------------------------------------------------------------------
/resources/timebg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/timebg.png
--------------------------------------------------------------------------------
/resources/v2ex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/v2ex.png
--------------------------------------------------------------------------------
/resources/watchbg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/watchbg.png
--------------------------------------------------------------------------------
/resources/youdao.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeoygin/awesome-hammerspoon/9f3c18e6dae7d9b22fe609e79608fb6ca7496c83/resources/youdao.png
--------------------------------------------------------------------------------
/widgets/analogclock.lua:
--------------------------------------------------------------------------------
1 | seccolor = {red=158/255,blue=158/255,green=158/255,alpha=0.5}
2 | tofilledcolor = {red=1,blue=1,green=1,alpha=0.1}
3 | secfillcolor = {red=158/255,blue=158/255,green=158/255,alpha=0.1}
4 | mincolor = {red=24/255,blue=195/255,green=145/255,alpha=0.75}
5 | hourcolor = {red=236/255,blue=39/255,green=109/255,alpha=0.75}
6 |
7 | clocks = {}
8 |
9 | function showAnalogClock(screen)
10 | if not clocks[screen:id()] then
11 | clocks[screen:id()] = {}
12 | end
13 | local clock = clocks[screen:id()]
14 | clock.screen = screen
15 | clock.mainRes = screen:fullFrame()
16 | clock.localMainRes = screen:absoluteToLocal(clock.mainRes)
17 | if not aclockcenter then
18 | clock.center = {x=160,y=200}
19 | else
20 | clock.center = aclockcenter
21 | end
22 | local aclockcenter = clock.center
23 |
24 | local imagerect = hs.geometry.rect(screen:localToAbsolute(aclockcenter.x-100,aclockcenter.y-100,200,200))
25 | if not clock.imagedisp then
26 | clock.imagedisp = hs.drawing.image(imagerect,hs.fs.pathToAbsolute(hs.configdir..'/resources/watchbg.png'))
27 | clock.imagedisp:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
28 | clock.imagedisp:setLevel(hs.drawing.windowLevels.desktopIcon)
29 | clock.imagedisp:show()
30 | else
31 | clock.imagedisp:setFrame(imagerect)
32 | end
33 |
34 | local bgcircle = hs.drawing.arc(screen:localToAbsolute(aclockcenter),80,0,360)
35 | if clock.bgcircle then
36 | clock.bgcircle:delete()
37 | clock.bgcircle = nil
38 | end
39 | if not clock.bgcircle then
40 | clock.bgcircle = bgcircle
41 | clock.bgcircle:setFill(false)
42 | clock.bgcircle:setStrokeWidth(1)
43 | clock.bgcircle:setStrokeColor(seccolor)
44 | clock.bgcircle:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
45 | clock.bgcircle:setLevel(hs.drawing.windowLevels.desktopIcon)
46 | clock.bgcircle:show()
47 | end
48 |
49 | local mincircle = hs.drawing.arc(screen:localToAbsolute(aclockcenter),55,0,360)
50 | if clock.mincircle then
51 | clock.mincircle:delete()
52 | clock.mincircle = nil
53 | end
54 | if not clock.mincircle then
55 | clock.mincircle = mincircle
56 | clock.mincircle:setFill(false)
57 | clock.mincircle:setStrokeWidth(3)
58 | clock.mincircle:setStrokeColor(tofilledcolor)
59 | clock.mincircle:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
60 | clock.mincircle:setLevel(hs.drawing.windowLevels.desktopIcon)
61 | clock.mincircle:show()
62 | end
63 |
64 | local hourcircle = hs.drawing.arc(screen:localToAbsolute(aclockcenter),40,0,360)
65 | if clock.hourcircle then
66 | clock.hourcircle:delete()
67 | clock.hourcircle = nil
68 | end
69 | if not clock.hourcircle then
70 | clock.hourcircle = hourcircle
71 | clock.hourcircle:setFill(false)
72 | clock.hourcircle:setStrokeWidth(3)
73 | clock.hourcircle:setStrokeColor(tofilledcolor)
74 | clock.hourcircle:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
75 | clock.hourcircle:setLevel(hs.drawing.windowLevels.desktopIcon)
76 | clock.hourcircle:show()
77 | end
78 |
79 | local sechand = hs.drawing.arc(screen:localToAbsolute(aclockcenter),80,0,0)
80 | if clock.sechand then
81 | clock.sechand:delete()
82 | clock.sechand = nil
83 | end
84 | if not clock.sechand then
85 | clock.sechand = sechand
86 | clock.sechand:setFillColor(secfillcolor)
87 | clock.sechand:setStrokeWidth(1)
88 | clock.sechand:setStrokeColor(seccolor)
89 | clock.sechand:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
90 | clock.sechand:setLevel(hs.drawing.windowLevels.desktopIcon)
91 | clock.sechand:show()
92 | end
93 |
94 | local minhand1 = hs.drawing.arc(screen:localToAbsolute(aclockcenter),55,0,0)
95 | if clock.minhand1 then
96 | clock.minhand1:delete()
97 | clock.minhand1 = nil
98 | end
99 | if not clock.minhand1 then
100 | clock.minhand1 = minhand1
101 | clock.minhand1:setFill(false)
102 | -- minhand:setStrokeWidth(3)
103 | clock.minhand1:setStrokeColor(mincolor)
104 | clock.minhand1:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
105 | clock.minhand1:setLevel(hs.drawing.windowLevels.desktopIcon)
106 | clock.minhand1:show()
107 | end
108 |
109 | local minhand2 = hs.drawing.arc(screen:localToAbsolute(aclockcenter),54,0,0)
110 | if clock.minhand2 then
111 | clock.minhand2:delete()
112 | clock.minhand2 = nil
113 | end
114 | if not clock.minhand2 then
115 | clock.minhand2 = minhand2
116 | clock.minhand2:setFill(false)
117 | clock.minhand2:setStrokeColor(mincolor)
118 | clock.minhand2:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
119 | clock.minhand2:setLevel(hs.drawing.windowLevels.desktopIcon)
120 | clock.minhand2:show()
121 | end
122 |
123 | local minhand3 = hs.drawing.arc(screen:localToAbsolute(aclockcenter),53,0,0)
124 | if clock.minhand3 then
125 | clock.minhand3:delete()
126 | clock.minhand3 = nil
127 | end
128 | if not clock.minhand3 then
129 | clock.minhand3 = minhand3
130 | clock.minhand3:setFill(false)
131 | clock.minhand3:setStrokeColor(mincolor)
132 | clock.minhand3:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
133 | clock.minhand3:setLevel(hs.drawing.windowLevels.desktopIcon)
134 | clock.minhand3:show()
135 | end
136 |
137 | local hourhand1 = hs.drawing.arc(screen:localToAbsolute(aclockcenter),40,0,0)
138 | if clock.hourhand1 then
139 | clock.hourhand1:delete()
140 | clock.hourhand1 = nil
141 | end
142 | if not clock.hourhand1 then
143 | clock.hourhand1 = hourhand1
144 | clock.hourhand1:setFill(false)
145 | clock.hourhand1:setStrokeColor(hourcolor)
146 | clock.hourhand1:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
147 | clock.hourhand1:setLevel(hs.drawing.windowLevels.desktopIcon)
148 | clock.hourhand1:show()
149 | end
150 |
151 | local hourhand2 = hs.drawing.arc(screen:localToAbsolute(aclockcenter),39,0,0)
152 | if clock.hourhand2 then
153 | clock.hourhand2:delete()
154 | clock.hourhand2 = nil
155 | end
156 | local hourhand2 = hs.drawing.arc(aclockcenter,39,0,0)
157 | if not clock.hourhand2 then
158 | clock.hourhand2 = hourhand2
159 | clock.hourhand2:setFill(false)
160 | clock.hourhand2:setStrokeColor(hourcolor)
161 | clock.hourhand2:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
162 | clock.hourhand2:setLevel(hs.drawing.windowLevels.desktopIcon)
163 | clock.hourhand2:show()
164 | end
165 |
166 | local hourhand3 = hs.drawing.arc(screen:localToAbsolute(aclockcenter),38,0,0)
167 | if clock.hourhand3 then
168 | clock.hourhand3:delete()
169 | clock.hourhand3 = nil
170 | end
171 | if not clock.hourhand3 then
172 | clock.hourhand3 = hourhand3
173 | clock.hourhand3:setFill(false)
174 | clock.hourhand3:setStrokeColor(hourcolor)
175 | clock.hourhand3:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
176 | clock.hourhand3:setLevel(hs.drawing.windowLevels.desktopIcon)
177 | clock.hourhand3:show()
178 | end
179 |
180 | if clock.clocktimer == nil then
181 | clock.clocktimer = hs.timer.doEvery(1,function() updateClock(clock) end)
182 | else
183 | clock.clocktimer:start()
184 | end
185 | end
186 |
187 | function destroyAnalogClock(idx)
188 | if clocks[idx] then
189 | local clock = clocks[idx]
190 | if hs.screen.find(clock.screen:id()) then
191 | return
192 | end
193 | clock.clocktimer:stop()
194 | clock.clocktimer=nil
195 | clock.imagedisp:delete()
196 | clock.imagedisp=nil
197 | clock.bgcircle:delete()
198 | clock.bgcircle=nil
199 | clock.mincircle:delete()
200 | clock.mincircle=nil
201 | clock.hourcircle:delete()
202 | clock.hourcircle=nil
203 | clock.sechand:delete()
204 | clock.sechand=nil
205 | clock.minhand1:delete()
206 | clock.minhand1=nil
207 | clock.minhand2:delete()
208 | clock.minhand2=nil
209 | clock.minhand3:delete()
210 | clock.minhand3=nil
211 | clock.hourhand1:delete()
212 | clock.hourhand1=nil
213 | clock.hourhand2:delete()
214 | clock.hourhand2=nil
215 | clock.hourhand3:delete()
216 | clock.hourhand3=nil
217 | clocks[idx]=nil
218 | end
219 | end
220 |
221 | function updateClock(clock)
222 | local secnum = math.tointeger(os.date("%S"))
223 | local minnum = math.tointeger(os.date("%M"))
224 | local hournum = math.tointeger(os.date("%I"))
225 | local seceangle = 6*secnum
226 | local mineangle = 6*minnum+6/60*secnum
227 | local houreangle = 30*hournum+30/60*minnum+30/60/60*secnum
228 |
229 | clock.sechand:setArcAngles(0,seceangle)
230 | clock.minhand1:setArcAngles(0,mineangle)
231 | clock.minhand2:setArcAngles(0,mineangle)
232 | clock.minhand3:setArcAngles(0,mineangle)
233 | if houreangle >= 360 then
234 | houreangle = houreangle - 360
235 | end
236 | clock.hourhand1:setArcAngles(0,houreangle)
237 | clock.hourhand2:setArcAngles(0,houreangle)
238 | clock.hourhand3:setArcAngles(0,houreangle)
239 | end
240 |
241 | function showAnalogClocks()
242 | showAnalogClock(hs.screen.primaryScreen())
243 | end
244 |
245 | function destroyAnalogClocks()
246 | for i in pairs(clocks) do
247 | destroyAnalogClock(i)
248 | end
249 | end
250 |
251 | if not launch_analogclock then launch_analogclock = true end
252 | if launch_analogclock == true then
253 | showAnalogClocks()
254 | hs.screen.watcher.newWithActiveScreen(function(activeChanged)
255 | if activeChanged then
256 | destroyAnalogClocks()
257 | hs.timer.doAfter(3, function()
258 | print('Refresh Analog Clock')
259 | showAnalogClocks()
260 | end)
261 | end
262 | end):start()
263 | end
264 |
--------------------------------------------------------------------------------
/widgets/aria2.lua:
--------------------------------------------------------------------------------
1 | aria2_loaded = true
2 | if not aria2_host then aria2_host = "http://localhost:6800/jsonrpc" end
3 | if not aria2_token then aria2_token = "token" end
4 | if not aria2_show_items_max then aria2_show_items_max = 5 end
5 | if not aria2_refresh_interval then aria2_refresh_interval = 1 end
6 |
7 | local function pathToName(path)
8 | local tmptbl = {}
9 | for w in string.gmatch(path,"[^/]+") do table.insert(tmptbl,w) end
10 | return tmptbl[#tmptbl]
11 | end
12 |
13 | local function formatData(datanum)
14 | if datanum/1024 < 1024 then
15 | return string.format("%.2f",datanum/1024) .. ' KB'
16 | elseif datanum/1024/1024 < 1024 then
17 | return string.format("%.2f",datanum/1024/1024) .. ' MB'
18 | elseif datanum/1024/1024/1024 < 1024 then
19 | return string.format("%.2f",datanum/1024/1024/1024) .. ' GB'
20 | end
21 | end
22 |
23 | local function formatTime(secnum)
24 | local hourcount = math.modf(secnum/3600)
25 | local mincount = math.modf(math.fmod(secnum,3600)/60)
26 | local seccount = math.fmod(secnum,60)
27 | return string.format("%02d",hourcount)..":"..string.format("%02d",mincount)..":"..string.format("%02d",seccount)
28 | end
29 |
30 | local function splitByLine(str)
31 | local tailtrimmedstr = string.gsub(str,"%s+$","")
32 | local tmptbl = {}
33 | for w in string.gmatch(tailtrimmedstr,"[^\n]+") do table.insert(tmptbl,w) end
34 | if #tmptbl == 1 then
35 | local trimmedstr = string.gsub(tmptbl[1],"%s","")
36 | return trimmedstr
37 | else
38 | local tmptbl2 = {}
39 | for _,val in pairs(tmptbl) do
40 | local trimmedstr = string.gsub(val,"%s","")
41 | table.insert(tmptbl2,trimmedstr)
42 | end
43 | return tmptbl2
44 | end
45 | end
46 |
47 | function aria2_NewTask(tasktype,fileaddr)
48 | local task_req = {
49 | id = hs.hash.SHA1(os.time()),
50 | jsonrpc = "2.0",
51 | method = "aria2."..tasktype
52 | }
53 | if tasktype == "addUri" then
54 | if type(fileaddr) == "string" then
55 | task_req["params"] = { "token:"..aria2_token,{fileaddr} }
56 | elseif type(fileaddr) == "table" then
57 | task_req = {}
58 | for _,val in pairs(fileaddr) do
59 | local task_item = {
60 | id = hs.hash.SHA1(os.time()),
61 | jsonrpc = "2.0",
62 | method = "aria2."..tasktype,
63 | params = { "token:"..aria2_token,{val} }
64 | }
65 | table.insert(task_req,task_item)
66 | end
67 | end
68 | elseif tasktype == "addTorrent" or tasktype == "addMetalink" then
69 | local file_handle = io.open(fileaddr):read('a')
70 | if file_handle ~= nil then
71 | local encoded_file = hs.base64.encode(file_handle)
72 | task_req["params"] = { "token:"..aria2_token,encoded_file }
73 | end
74 | end
75 | if aria2_timer ~= nil then
76 | aria2_timer:stop()
77 | hs.http.asyncPost(aria2_host,hs.json.encode(task_req),nil,function(status,data)
78 | if status == 200 then
79 | if aria2_timer:nextTrigger() > aria2_refresh_interval then
80 | aria2_DrawCanvas()
81 | end
82 | end
83 | aria2_timer:start()
84 | aria2_timer:setNextTrigger(aria2_refresh_interval)
85 | end)
86 | end
87 | end
88 |
89 | function aria2_Commands(action,gid)
90 | local action_req = {
91 | id = hs.hash.SHA1(os.time()),
92 | jsonrpc = "2.0",
93 | method = "aria2."..action
94 | }
95 | if gid then
96 | action_req["params"] = { "token:"..aria2_token, gid }
97 | else
98 | action_req["params"] = { "token:"..aria2_token }
99 | end
100 | if aria2_timer ~= nil then
101 | aria2_timer:stop()
102 | hs.http.asyncPost(aria2_host,hs.json.encode(action_req),nil,function(status,data)
103 | if status == 200 then
104 | if aria2_timer:nextTrigger() > aria2_refresh_interval then
105 | aria2_DrawCanvas()
106 | end
107 | end
108 | aria2_timer:start()
109 | end)
110 | end
111 | end
112 |
113 | function aria2_DrawCanvas()
114 | local alltasks_req = {
115 | {
116 | id = hs.hash.SHA1(os.time()),
117 | jsonrpc = "2.0",
118 | method = "aria2.tellActive",
119 | params = { "token:"..aria2_token }
120 | },
121 | {
122 | id = hs.hash.SHA1(os.time()),
123 | jsonrpc = "2.0",
124 | method = "aria2.tellWaiting",
125 | params = { "token:"..aria2_token, 0, 5 }
126 | },
127 | {
128 | id = hs.hash.SHA1(os.time()),
129 | jsonrpc = "2.0",
130 | method = "aria2.tellStopped",
131 | params = { "token:"..aria2_token, 0, 5 }
132 | },
133 | }
134 |
135 | hs.http.asyncPost(aria2_host,hs.json.encode(alltasks_req),nil,function(status,data)
136 | if status == 200 then
137 | local decoded_data = hs.json.decode(data)
138 | aria2_canvas_holder = nil
139 | aria2_canvas_holder = {}
140 | aria2_items_count = 0
141 | aria2_active_items_count = 0
142 | for idx,tbl in pairs(decoded_data) do
143 | if type(tbl.result) == "table" then
144 | local result_tbl = tbl.result
145 | for key,val in pairs(result_tbl) do
146 | if aria2_items_count >= aria2_show_items_max then break end
147 | if val.files[1].path ~= "" then
148 | aria2_file_path = pathToName(val.files[1].path)
149 | else
150 | aria2_file_path = val.files[1].uris[1].uri
151 | end
152 | local file_size = formatData(val.totalLength)
153 | if val.status == "paused" then
154 | aria2_download_speed = formatData(0).."/s"
155 | else
156 | aria2_download_speed = formatData(val.downloadSpeed).."/s"
157 | end
158 | if val.totalLength == "0" then
159 | aria2_download_progress = string.format("%.4f",0)
160 | else
161 | aria2_download_progress = string.format("%.4f",val.completedLength/val.totalLength)
162 | end
163 | local progress_percent = tostring(aria2_download_progress*100).."%"
164 | local connection_number = val.connections
165 | if val.downloadSpeed == "0" or val.status == "paused" then
166 | aria2_remain_time = "--:--:--"
167 | else
168 | aria2_remain_time = formatTime(string.format("%.0f",(val.totalLength-val.completedLength)/val.downloadSpeed))
169 | end
170 | local aria2_canvas = hs.canvas.new({x=0,y=0,w=400,h=50})
171 | aria2_canvas._default.textSize = 12
172 | aria2_canvas._default.textColor = black
173 | aria2_canvas._default.trackMouseDown = true
174 | if val.status == "active" then
175 | aria2_canvas[1] = {type="rectangle",fillColor=osx_green}
176 | elseif val.status == "paused" then
177 | aria2_canvas[1] = {type="rectangle",fillColor=osx_yellow}
178 | elseif val.status == "complete" then
179 | aria2_canvas[1] = {type="rectangle",fillColor=black}
180 | elseif val.status == "error" or val.status == "removed" then
181 | aria2_canvas[1] = {type="rectangle",fillColor=osx_red}
182 | end
183 | aria2_canvas[1].fillColor.alpha = 0.1
184 | aria2_canvas[2] = {type="text",text=aria2_file_path,frame={x="2%",y="10%",w="60%",h="45%"},textAlignment="left",textLineBreak="truncateMiddle"}
185 | aria2_canvas[3] = {type="text",text=file_size,frame={x="64%",y="10%",w="15%",h="45%"},textAlignment="right"}
186 | aria2_canvas[4] = {type="text",text=aria2_download_speed,frame={x="83%",y="15%",w="15%",h="45%"},textSize=10,textAlignment="right"}
187 | aria2_canvas[5] = {action="fill",type="rectangle",fillColor=gray,frame={x="2%",y="55%",w="60%",h="25%"}}
188 | aria2_canvas[6] = {action="fill",type="rectangle",fillColor=dodgerblue,frame={x="2%",y="55%",w=tostring(0.6*aria2_download_progress),h="25%"}}
189 | aria2_canvas[7] = {type="text",text=progress_percent,frame={x="2%",y="55%",w="60%",h="45%"},textSize=10,textAlignment="right"}
190 | aria2_canvas[8] = {type="text",text=connection_number,frame={x="64%",y="55%",w="15%",h="45%"},textSize=10,textAlignment="center"}
191 | aria2_canvas[9] = {type="text",text=aria2_remain_time,frame={x="83%",y="55%",w="15%",h="45%"},textSize=10,textAlignment="right"}
192 | aria2_canvas:mouseCallback(function(canvas,event,id,x,y)
193 | if canvas == aria2_canvas and event == "mouseDown" then
194 | local modifiers_status = hs.eventtap.checkKeyboardModifiers()
195 | if modifiers_status.cmd == true then
196 | if val.status == "complete" or val.status == "removed" or val.status == "error" then
197 | aria2_Commands("removeDownloadResult",val.gid)
198 | else
199 | aria2_Commands("remove",val.gid)
200 | end
201 | else
202 | if val.status == "complete" then
203 | hs.execute("open -g "..val.files[1].path)
204 | elseif val.status == "active" then
205 | aria2_Commands("pause",val.gid)
206 | elseif val.status == "paused" then
207 | aria2_Commands("unpause",val.gid)
208 | end
209 | end
210 | end
211 | end)
212 | table.insert(aria2_canvas_holder,{gid=val.gid,status=val.status,canvas=aria2_canvas})
213 | if val.status == "active" then
214 | aria2_active_items_count = aria2_active_items_count + 1
215 | end
216 | aria2_items_count = aria2_items_count + 1
217 | end
218 | end
219 | end
220 | if aria2_drawer == nil then
221 | aria2_init_in_screen = hs.screen.mainScreen()
222 | local mainRes = aria2_init_in_screen:fullFrame()
223 | local localMainRes = aria2_init_in_screen:absoluteToLocal(mainRes)
224 | aria2_drawer = hs.canvas.new(aria2_init_in_screen:localToAbsolute({x=localMainRes.w-400,y=localMainRes.h-50*#aria2_canvas_holder-52,w=400,h=50*#aria2_canvas_holder}))
225 | aria2_drawer[1] = {type="rectangle",fillColor=white}
226 | aria2_drawer[1].fillColor.alpha = 0.8
227 | aria2_drawer:level(hs.canvas.windowLevels.tornOffMenu)
228 | aria2_drawer:clickActivating(false)
229 | aria2_drawer._default.trackMouseDown = true
230 | else
231 | for i=2,#aria2_drawer do
232 | aria2_drawer:removeElement(2)
233 | end
234 | local mainRes = aria2_init_in_screen:fullFrame()
235 | local localMainRes = aria2_init_in_screen:absoluteToLocal(mainRes)
236 | aria2_drawer:frame(aria2_init_in_screen:localToAbsolute({x=localMainRes.w-400,y=localMainRes.h-50*#aria2_canvas_holder-52,w=400,h=50*#aria2_canvas_holder}))
237 | end
238 | aria2_drawer:show()
239 | for idx,val in pairs(aria2_canvas_holder) do
240 | aria2_drawer[idx+1]={type="canvas",canvas=val.canvas,frame={x="0%",y=tostring(1/#aria2_canvas_holder*(idx-1)),w="100%",h=tostring(1/#aria2_canvas_holder)}}
241 | end
242 | -- TODO: Figure out why this is needed
243 | aria2_drawer:mouseCallback(function(canvas,event,id,x,y)
244 | print(canvas,event,id,x,y)
245 | end)
246 | end
247 | if aria2_timer ~= nil then aria2_timer:start() end
248 | end)
249 | end
250 |
251 |
252 | function aria2_IntervalRequest()
253 | local globalstat_req = {
254 | {
255 | id = hs.hash.SHA1(os.time()),
256 | jsonrpc = "2.0",
257 | method = "aria2.getGlobalStat",
258 | params = { "token:"..aria2_token }
259 | },
260 | }
261 | if not stopped_queue_cached then stopped_request_required = true end
262 | if stopped_request_required then
263 | local tmptbl = {
264 | id = hs.hash.SHA1(os.time()),
265 | jsonrpc = "2.0",
266 | method = "aria2.tellStopped",
267 | params = { "token:"..aria2_token, 0, 100 }
268 | }
269 | table.insert(globalstat_req,tmptbl)
270 | stopped_request_added = true
271 | else
272 | stopped_request_added = false
273 | end
274 |
275 | if aria2_drawer ~= nil then
276 | if aria2_drawer:isShowing() then
277 | if aria2_active_items_count and aria2_active_items_count > 0 then
278 | active_request_required = true
279 | else
280 | active_request_required = false
281 | end
282 | else
283 | active_request_required = false
284 | end
285 | else
286 | active_request_required = false
287 | end
288 |
289 | if active_request_required then
290 | for i=1,aria2_active_items_count do
291 | local tmptbl = {
292 | id = hs.hash.SHA1(os.time()),
293 | jsonrpc = "2.0",
294 | method = "aria2.tellStatus",
295 | params = { "token:"..aria2_token, aria2_canvas_holder[i].gid, {"totalLength","completedLength","downloadSpeed","connections"} }
296 | }
297 | table.insert(globalstat_req,tmptbl)
298 | end
299 | active_request_added = true
300 | else
301 | active_request_added = false
302 | end
303 |
304 | hs.http.asyncPost(aria2_host,hs.json.encode(globalstat_req),nil,function(status,data)
305 | if status == 200 then
306 | local decoded_data = hs.json.decode(data)
307 | local stoppednum = tonumber(decoded_data[1].result.numStopped)
308 | local activenum = tonumber(decoded_data[1].result.numActive)
309 | local waitingnum = tonumber(decoded_data[1].result.numWaiting)
310 | if not aria2_global_stoppednum then aria2_global_stoppednum = stoppednum end
311 | if not aria2_global_activenum then aria2_global_activenum = activenum end
312 | if not aria2_global_waitingnum then aria2_global_waitingnum = waitingnum end
313 | if stoppednum == 0 and activenum == 0 and waitingnum == 0 then
314 | if not aria2_lazy_request_interval then
315 | aria2_lazy_request_interval = aria2_refresh_interval + 1
316 | else
317 | aria2_lazy_request_interval = aria2_lazy_request_interval + 1
318 | end
319 | aria2_timer:setNextTrigger(aria2_lazy_request_interval)
320 | else
321 | aria2_timer:setNextTrigger(aria2_refresh_interval)
322 | end
323 | if activenum ~= aria2_global_activenum or waitingnum ~= aria2_global_waitingnum or stoppednum ~= aria2_global_stoppednum then
324 | if aria2_drawer ~= nil then
325 | if aria2_drawer:isShowing() then
326 | if aria2_timer ~= nil then
327 | aria2_timer:stop()
328 | aria2_DrawCanvas()
329 | end
330 | end
331 | end
332 | end
333 | if stoppednum ~= aria2_global_stoppednum then
334 | stopped_request_required = true
335 | else
336 | stopped_request_required = false
337 | end
338 | aria2_global_activenum = activenum
339 | aria2_global_waitingnum = waitingnum
340 | aria2_global_stoppednum = stoppednum
341 | if stopped_request_added then
342 | if stopped_queue_cached then
343 | local function isInStoppedQueue(value, tbl)
344 | for i=1,#tbl do
345 | if tbl[i] == value then
346 | return true
347 | end
348 | end
349 | return false
350 | end
351 | for idx,val in pairs(decoded_data[2].result) do
352 | if not isInStoppedQueue(val.gid,stopped_queue_list) then
353 | if val.files[1].path ~= "" then
354 | aria2_file_path = pathToName(val.files[1].path)
355 | else
356 | aria2_file_path = val.files[1].uris[1].uri
357 | end
358 | if val.errorCode == "0" then
359 | if val.status == "complete" then
360 | aria2_notify_title = "Download Succeed."
361 | end
362 | elseif val.errorCode == "31" then
363 | if val.status == "removed" then
364 | aria2_notify_title = ""
365 | end
366 | else
367 | aria2_notify_title = "Download Error! "..val.errorMessage
368 | end
369 | if aria2_notify_title ~= "" then
370 | hs.notify.new({title=aria2_notify_title, informativeText=aria2_file_path}):send()
371 | end
372 | end
373 | end
374 | end
375 | stopped_queue_list = {}
376 | for idx,val in pairs(decoded_data[2].result) do
377 | table.insert(stopped_queue_list,val.gid)
378 | end
379 | stopped_queue_cached = true
380 | end
381 | if active_request_added then
382 | if stopped_request_added then
383 | aria2_activerep_tbl_pos = 3
384 | else
385 | aria2_activerep_tbl_pos = 2
386 | end
387 | for i=aria2_activerep_tbl_pos,#decoded_data do
388 | local download_speed = formatData(decoded_data[i].result.downloadSpeed).."/s"
389 | if decoded_data[i].result.totalLength == "0" then
390 | aria2_download_progress = string.format("%.4f",0)
391 | else
392 | aria2_download_progress = string.format("%.4f",decoded_data[i].result.completedLength/decoded_data[i].result.totalLength)
393 | end
394 | local progress_percent = tostring(aria2_download_progress*100).."%"
395 | local connection_number = decoded_data[i].result.connections
396 | if decoded_data[i].result.downloadSpeed == "0" then
397 | aria2_remain_time = "--:--:--"
398 | else
399 | aria2_remain_time = formatTime(string.format("%.0f",(decoded_data[i].result.totalLength-decoded_data[i].result.completedLength)/decoded_data[i].result.downloadSpeed))
400 | end
401 | aria2_canvas_holder[i-(aria2_activerep_tbl_pos-1)].canvas[4].text = download_speed
402 | aria2_canvas_holder[i-(aria2_activerep_tbl_pos-1)].canvas[6].frame = {x="2%",y="55%",w=tostring(0.6*aria2_download_progress),h="25%"}
403 | aria2_canvas_holder[i-(aria2_activerep_tbl_pos-1)].canvas[7].text = progress_percent
404 | aria2_canvas_holder[i-(aria2_activerep_tbl_pos-1)].canvas[8].text = connection_number
405 | aria2_canvas_holder[i-(aria2_activerep_tbl_pos-1)].canvas[9].text = aria2_remain_time
406 | end
407 | end
408 | end
409 | end)
410 | end
411 |
412 | function aria2_DrawToolbar()
413 | if not aria2_toolbar then
414 | local mainScreen = hs.screen.mainScreen()
415 | local mainRes = mainScreen:fullFrame()
416 | local localMainRes = mainScreen:absoluteToLocal(mainRes)
417 | aria2_toolbar = hs.canvas.new(mainScreen:localToAbsolute({x=localMainRes.w-165,y=localMainRes.h-42,w=120,h=24}))
418 | aria2_toolbar:level(hs.canvas.windowLevels.tornOffMenu)
419 | aria2_toolbar:clickActivating(false)
420 | aria2_toolbar[1] = {action="fill",type="rectangle",fillColor=lightseagreen,roundedRectRadii={xRadius=3,yRadius=3}}
421 | aria2_toolbar[1].fillColor.alpha=0.3
422 | aria2_toolbar[2] = {type="text",text="➲",frame={x=0,y=0,w=tostring(1/4),h="100%"},textSize=20,textAlignment="center"}
423 | aria2_toolbar[3] = {type="text",text="❒",frame={x=tostring(1/4),y=0,w=tostring(1/4),h="100%"},textSize=20,textAlignment="center"}
424 | aria2_toolbar[4] = {type="text",text="♻︎",frame={x=tostring(2/4),y=0,w=tostring(1/4),h="100%"},textSize=20,textAlignment="center"}
425 | aria2_toolbar[5] = {type="text",text="✖︎",frame={x=tostring(3/4),y=0,w=tostring(1/4),h="100%"},textSize=20,textAlignment="center"}
426 | aria2_toolbar._default.trackMouseDown = true
427 |
428 | aria2_toolbar:mouseCallback(function(canvas,event,id,x,y)
429 | if event == "mouseDown" and id == 2 then
430 | local strfromclip = hs.pasteboard.readString()
431 | local single_url_or_batch_urls = splitByLine(strfromclip)
432 | aria2_NewTask("addUri",single_url_or_batch_urls)
433 | elseif event == "mouseDown" and id == 3 then
434 | status, data = hs.osascript.applescript('set filechooser to choose file with prompt "Select BT file(*.torrent) or Metafile(*.metafile|*.meta4)" of type {"torrent", "metafile", "meta4"}\nreturn POSIX path of filechooser')
435 | if status then
436 | local function extensionoffile(path)
437 | local tmptbl = {}
438 | for w in string.gmatch(path,"[^%.]+") do table.insert(tmptbl,w) end
439 | return tmptbl[#tmptbl]
440 | end
441 | if extensionoffile(data) == "torrent" then
442 | aria2_NewTask("addTorrent",data)
443 | elseif extensionoffile(data) == ".metafile" or extensionoffile(data) == ".meta4" then
444 | aria2_NewTask("addMetalink",data)
445 | end
446 | end
447 | elseif event == "mouseDown" and id == 4 then
448 | aria2_Commands("purgeDownloadResult")
449 | elseif event == "mouseDown" and id == 5 then
450 | aria2_toolbar:hide()
451 | aria2_drawer:hide()
452 | end
453 | end)
454 | end
455 | local mainScreen = hs.screen.mainScreen()
456 | local mainRes = mainScreen:fullFrame()
457 | local localMainRes = mainScreen:absoluteToLocal(mainRes)
458 | aria2_toolbar:frame(mainScreen:localToAbsolute({x=localMainRes.w-165,y=localMainRes.h-42,w=120,h=24}))
459 | aria2_toolbar:show()
460 | end
461 |
462 | function aria2_Init()
463 | local init_req = {
464 | id = hs.hash.SHA1(os.time()),
465 | jsonrpc = "2.0",
466 | method = "aria2.getVersion",
467 | params = { "token:"..aria2_token }
468 | }
469 | hs.http.asyncPost(aria2_host,hs.json.encode(init_req),nil,function(status,data)
470 | if status == 200 then
471 | aria2_DrawToolbar()
472 | aria2_DrawCanvas()
473 | if aria2_timer ~= nil then
474 | aria2_timer:start()
475 | aria2_timer:setNextTrigger(aria2_refresh_interval)
476 | else
477 | aria2_timer = hs.timer.doEvery(aria2_refresh_interval,aria2_IntervalRequest)
478 | end
479 | else
480 | hs.notify.new({title="aria2 not ready.", informativeText="Please check your configuration."}):send()
481 | end
482 | end)
483 | end
484 |
485 |
--------------------------------------------------------------------------------
/widgets/caffeine.lua:
--------------------------------------------------------------------------------
1 | local caffeine = hs.menubar.new()
2 |
3 | function setCaffeineDisplay(state)
4 | if state then
5 | caffeine:setTitle("AWAKE")
6 | else
7 | caffeine:setTitle("SLEEPY")
8 | end
9 | end
10 |
11 | function caffeineClicked()
12 | setCaffeineDisplay(hs.caffeinate.toggle("displayIdle"))
13 | end
14 |
15 | if caffeine then
16 | caffeine:setClickCallback(caffeineClicked)
17 | setCaffeineDisplay(hs.caffeinate.get("displayIdle"))
18 | end
19 |
--------------------------------------------------------------------------------
/widgets/calendar.lua:
--------------------------------------------------------------------------------
1 | caltodaycolor = hs.drawing.color.white
2 | calcolor = {red=235/255,blue=235/255,green=235/255}
3 | calbgcolor = {red=0,blue=0,green=0,alpha=0.3}
4 | calcmd='cal -h 2>/dev/null || cal'
5 |
6 | calendars = {}
7 |
8 | function drawToday(calendar)
9 | local currentmonth = tonumber(os.date("%m"))
10 | -- local todayyearweek = os.date("%W")
11 | local todayyearweek = hs.execute("date -v+1d +'%W'")
12 | -- Year week of last day of last month
13 | local ldlmyearweek = hs.execute("date -v"..currentmonth.."m -v1d -v+1d +'%W'")
14 | local rowofcurrentmonth = todayyearweek - ldlmyearweek
15 | local columnofcurrentmonth = os.date("*t").wday
16 | local splitw = 205
17 | local splith = 141
18 | local todaycoverrect = hs.geometry.rect(calendar.screen:localToAbsolute(calendar.caltopleft[1]+10+splitw/7*(columnofcurrentmonth-1),calendar.caltopleft[2]+10+splith/7*(rowofcurrentmonth+2),splitw/7,splith/7))
19 | if not calendar.todaycover then
20 | calendar.todaycover = hs.drawing.rectangle(todaycoverrect)
21 | calendar.todaycover:setStroke(false)
22 | calendar.todaycover:setRoundedRectRadii(3,3)
23 | calendar.todaycover:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
24 | calendar.todaycover:setLevel(hs.drawing.windowLevels.desktopIcon)
25 | calendar.todaycover:setFillColor(caltodaycolor)
26 | calendar.todaycover:setAlpha(0.3)
27 | calendar.todaycover:show()
28 | else
29 | calendar.todaycover:setFrame(todaycoverrect)
30 | end
31 | end
32 |
33 | function updateCal(calendar)
34 | local caltext = hs.styledtext.ansi(hs.execute(calcmd),{font={name="Courier",size=16},color=calcolor})
35 | calendar.caldraw:setStyledText(caltext)
36 | drawToday(calendar)
37 | end
38 |
39 | function showCalendar(screen)
40 | if not calendars[screen:id()] then
41 | calendars[screen:id()] = {}
42 | end
43 |
44 | local calendar = calendars[screen:id()]
45 | calendar.screen = screen
46 | local mainRes = screen:fullFrame()
47 | local localMainRes = screen:absoluteToLocal(mainRes)
48 | if not caltopleft then
49 | calendar.caltopleft = {localMainRes.w-330-20,localMainRes.h-161-84}
50 | else
51 | calendar.caltopleft = caltopleft
52 | end
53 |
54 | local bgrect = hs.geometry.rect(screen:localToAbsolute(calendar.caltopleft[1],calendar.caltopleft[2],230,161))
55 | if not calendar.calbg then
56 | calendar.calbg = hs.drawing.rectangle(bgrect)
57 | calendar.calbg:setFillColor(calbgcolor)
58 | calendar.calbg:setStroke(false)
59 | calendar.calbg:setRoundedRectRadii(10,10)
60 | calendar.calbg:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
61 | calendar.calbg:setLevel(hs.drawing.windowLevels.desktopIcon)
62 | calendar.calbg:show()
63 | else
64 | calendar.calbg:setFrame(bgrect)
65 | end
66 |
67 | local caltext = hs.styledtext.ansi(hs.execute(calcmd),{font={name="Courier",size=16},color=calcolor})
68 | local calrect = hs.geometry.rect(screen:localToAbsolute(calendar.caltopleft[1]+15,calendar.caltopleft[2]+10,230,161))
69 | if not calendar.caldraw then
70 | calendar.caldraw = hs.drawing.text(calrect,caltext)
71 | calendar.caldraw:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
72 | calendar.caldraw:setLevel(hs.drawing.windowLevels.desktopIcon)
73 | calendar.caldraw:show()
74 | else
75 | calendar.caldraw:setFrame(calrect)
76 | end
77 |
78 | drawToday(calendar)
79 | if calendar.caltimer == nil then
80 | calendar.caltimer = hs.timer.doEvery(1800,function() updateCal(calendar) end)
81 | else
82 | calendar.caltimer:start()
83 | end
84 | end
85 |
86 | function destroyCalendar(idx)
87 | if calendars[idx] then
88 | local calendar = calendars[idx]
89 | if hs.screen.find(calendar.screen:id()) then
90 | return
91 | end
92 | calendar.caltimer:stop()
93 | calendar.caltimer=nil
94 | calendar.todaycover:delete()
95 | calendar.todaycover=nil
96 | calendar.calbg:delete()
97 | calendar.calbg=nil
98 | calendar.caldraw:delete()
99 | calendar.caldraw=nil
100 | calendars[idx] = nil
101 | end
102 | end
103 |
104 | function showCalendars()
105 | showCalendar(hs.screen.primaryScreen())
106 | end
107 |
108 | function destroyCalendars()
109 | for i in pairs(calendars) do
110 | destroyCalendar(i)
111 | end
112 | end
113 |
114 | if not launch_calendar then launch_calendar=true end
115 | if launch_calendar == true then
116 | showCalendars()
117 | hs.screen.watcher.newWithActiveScreen(function(activeChanged)
118 | if activeChanged then
119 | destroyCalendars()
120 | hs.timer.doAfter(3, function()
121 | print('Refresh Calendar')
122 | showCalendars()
123 | end)
124 | end
125 | end):start()
126 | end
127 |
--------------------------------------------------------------------------------
/widgets/hcalendar.lua:
--------------------------------------------------------------------------------
1 | hcalbgcolor = {red=0,blue=0,green=0,alpha=0.3}
2 | hcaltitlecolor = {red=1,blue=1,green=1,alpha=0.3}
3 | offdaycolor = {red=255/255,blue=120/255,green=120/255,alpha=1}
4 | hcaltodaycolor = {red=1,blue=1,green=1,alpha=0.2}
5 | midlinecolor = {red=1,blue=1,green=1,alpha=0.5}
6 | midlinetodaycolor = {red=0,blue=1,green=186/255,alpha=0.8}
7 | midlineoffcolor = {red=1,blue=119/255,green=119/255,alpha=0.5}
8 |
9 | weeknames = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}
10 | hcaltitlewh = {180,32}
11 | hcaldaywh = {23.43,24}
12 |
13 | hcalendars = {}
14 |
15 | function showHCalendar(screen)
16 | if not hcalendars[screen:id()] then
17 | hcalendars[screen:id()] = {}
18 | end
19 | local hcalendar = hcalendars[screen:id()]
20 | hcalendar.screen = screen
21 | hcalendar.mainRes = screen:fullFrame()
22 | hcalendar.localMainRes = screen:absoluteToLocal(hcalendar.mainRes)
23 | if not hcaltopleft then
24 | hcalendar.topleft = {40, hcalendar.localMainRes.h-130-44}
25 | else
26 | hcalendar.topleft = hcaltopleft
27 | end
28 | local hcaltopleft = hcalendar.topleft
29 |
30 | local titlestr = os.date("%B %Y, Week %W")
31 | local title_rect = hs.geometry.rect(screen:localToAbsolute(hcaltopleft[1]+10,hcaltopleft[2]+15,hcaltitlewh[1],hcaltitlewh[2]))
32 | if not hcalendar.title then
33 | local styledtitle = hs.styledtext.new(titlestr,{font={size=18},color=hcaltitlecolor,paragraphStyle={alignment="left"}})
34 | hcalendar.title = hs.drawing.text(title_rect,styledtitle)
35 | hcalendar.title:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
36 | hcalendar.title:setLevel(hs.drawing.windowLevels.desktopIcon)
37 | hcalendar.title:show()
38 | else
39 | hcalendar.title:setText(titlestr)
40 | hcalendar.title:setFrame(title_rect)
41 | end
42 |
43 | local currentyear = os.date("%Y")
44 | local currentmonth = os.date("%m")
45 | local firstdayofnextmonth = os.time{year=currentyear, month=currentmonth+1, day=1}
46 | local maxdayofcurrentmonth = os.date("*t", firstdayofnextmonth-24*60*60).day
47 | local weekdayup = ""
48 | local daydown = ""
49 | local offday = {}
50 | for i=1,maxdayofcurrentmonth do
51 | local weekdayofquery = os.date("*t", os.time{year=currentyear, month=currentmonth, day=i}).wday
52 | local weekstr = weeknames[weekdayofquery]
53 | weekdayup = weekdayup .. weekstr .. ' '
54 | daydown = daydown .. string.format('%2s',i)..' '
55 | if weekstr == 'Sa' or weekstr == 'Su' then
56 | table.insert(offday,{i,weekstr..'\n'..string.format('%2s',i)})
57 | end
58 | end
59 | local caltext = weekdayup..'\n'..daydown
60 | local caltextrect = hs.geometry.rect(screen:localToAbsolute(hcaltopleft[1]+10,hcaltopleft[2]+15+hcaltitlewh[2],hcaldaywh[1]*maxdayofcurrentmonth,43))
61 | if not hcalendar.textdraw then
62 | local styledcaltext = hs.styledtext.new(caltext,{font={name="Courier-Bold",size=13},paragraphStyle={lineSpacing=8.0}})
63 | hcalendar.textdraw = hs.drawing.text(caltextrect,styledcaltext)
64 | hcalendar.textdraw:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
65 | hcalendar.textdraw:setLevel(hs.drawing.windowLevels.desktopIcon)
66 | hcalendar.textdraw:show()
67 | else
68 | hcalendar.textdraw:setText(caltext)
69 | hcalendar.textdraw:setFrame(caltextrect)
70 | end
71 |
72 | local midlinerect = hs.geometry.rect(screen:localToAbsolute(hcaltopleft[1]+10,hcaltopleft[2]+15+hcaltitlewh[2]+20,hcaldaywh[1]*maxdayofcurrentmonth-3,4))
73 | if not hcalendar.midlinedraw then
74 | hcalendar.midlinedraw = hs.drawing.rectangle(midlinerect)
75 | hcalendar.midlinedraw:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
76 | hcalendar.midlinedraw:setLevel(hs.drawing.windowLevels.desktopIcon)
77 | hcalendar.midlinedraw:setFillColor(midlinecolor)
78 | hcalendar.midlinedraw:setStroke(false)
79 | hcalendar.midlinedraw:show()
80 | else
81 | hcalendar.midlinedraw:setFrame(midlinerect)
82 | end
83 |
84 | local hcalbgrect = hs.geometry.rect(screen:localToAbsolute(hcaltopleft[1],hcaltopleft[2],hcaldaywh[1]*maxdayofcurrentmonth+20-3,102))
85 | if not hcalendar.bg then
86 | hcalendar.bg = hs.drawing.rectangle(hcalbgrect)
87 | hcalendar.bg:setFillColor(hcalbgcolor)
88 | hcalendar.bg:setStroke(false)
89 | hcalendar.bg:setRoundedRectRadii(10,10)
90 | hcalendar.bg:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
91 | hcalendar.bg:setLevel(hs.drawing.windowLevels.desktopIcon)
92 | hcalendar.bg:show()
93 | else
94 | hcalendar.bg:setFrame(hcalbgrect)
95 | end
96 |
97 | if hcalendar.offday_holder and #hcalendar.offday_holder > 0 then
98 | for i=1,#hcalendar.offday_holder do
99 | hcalendar.offday_holder[i]:delete()
100 | hcalendar.offdaymidline_holder[i]:delete()
101 | end
102 | end
103 |
104 | local offday_holder = {}
105 | local offdaymidline_holder = {}
106 | hcalendar.offday_holder = offday_holder
107 | hcalendar. offdaymidline_holder = offdaymidline_holder
108 | for i=1,#offday do
109 | local offdayrect = hs.geometry.rect(screen:localToAbsolute(hcaltopleft[1]+10+hcaldaywh[1]*(offday[i][1]-1),hcaltopleft[2]+15+hcaltitlewh[2],hcaldaywh[1],43))
110 | local offdaytext = hs.styledtext.new(offday[i][2],{font={name="Courier-Bold",size=13},color=offdaycolor,paragraphStyle={lineSpacing=8.0}})
111 | table.insert(offday_holder,hs.drawing.text(offdayrect,offdaytext))
112 | offday_holder[i]:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
113 | offday_holder[i]:setLevel(hs.drawing.windowLevels.desktopIcon)
114 | offday_holder[i]:show()
115 | local offdaymidlinerect = hs.geometry.rect(screen:localToAbsolute(hcaltopleft[1]+10+hcaldaywh[1]*(offday[i][1]-1)-3,hcaltopleft[2]+15+hcaltitlewh[2]+20,hcaldaywh[1],4))
116 | table.insert(offdaymidline_holder,hs.drawing.rectangle(offdaymidlinerect))
117 | offdaymidline_holder[i]:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
118 | offdaymidline_holder[i]:setLevel(hs.drawing.windowLevels.desktopIcon)
119 | offdaymidline_holder[i]:setFillColor(midlineoffcolor)
120 | offdaymidline_holder[i]:setStroke(false)
121 | offdaymidline_holder[i]:show()
122 | end
123 |
124 | local today = math.tointeger(os.date("%d"))
125 | local todayrect = hs.geometry.rect(screen:localToAbsolute(hcaltopleft[1]+10+hcaldaywh[1]*(today-1)-3,hcaltopleft[2]+15+hcaltitlewh[2],hcaldaywh[1],43))
126 | if not hcalendar.todaydraw then
127 | hcalendar.todaydraw = hs.drawing.rectangle(todayrect)
128 | hcalendar.todaydraw:setFillColor(hcaltodaycolor)
129 | hcalendar.todaydraw:setStroke(false)
130 | hcalendar.todaydraw:setRoundedRectRadii(3,3)
131 | hcalendar.todaydraw:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
132 | hcalendar.todaydraw:setLevel(hs.drawing.windowLevels.desktopIcon)
133 | hcalendar.todaydraw:show()
134 | else
135 | hcalendar.todaydraw:setFrame(todayrect)
136 | end
137 |
138 | todaymidlinerect = hs.geometry.rect(screen:localToAbsolute(hcaltopleft[1]+10+hcaldaywh[1]*(today-1)-3,hcaltopleft[2]+15+hcaltitlewh[2]+20,hcaldaywh[1],4))
139 | -- Don't know why the draw order is not correct
140 | if hcalendar.todaymidlinedraw then
141 | hcalendar.todaymidlinedraw:delete()
142 | hcalendar.todaymidlinedraw=nil
143 | end
144 | hcalendar.todaymidlinedraw = hs.drawing.rectangle(todaymidlinerect)
145 | hcalendar.todaymidlinedraw:setFillColor(midlinetodaycolor)
146 | hcalendar.todaymidlinedraw:setStroke(false)
147 | hcalendar.todaymidlinedraw:setBehavior(hs.drawing.windowBehaviors.canJoinAllSpaces)
148 | hcalendar.todaymidlinedraw:setLevel(hs.drawing.windowLevels.desktopIcon)
149 | hcalendar.todaymidlinedraw:show()
150 | end
151 |
152 | function destroyHCalendar(idx)
153 | if hcalendars[idx] then
154 | local hcalendar = hcalendars[idx]
155 | if hs.screen.find(hcalendar.screen:id()) then
156 | return
157 | end
158 | if hcalendar.title then
159 | hcalendr.title:delete()
160 | hcalendar.title=nil
161 | end
162 | if hcalendar.textdraw then
163 | hcalendar.textdraw:delete()
164 | hcalendar.textdraw=nil
165 | end
166 | if hcalendar.midlinedraw then
167 | hcalendar.midlinedraw:delete()
168 | hcalendar.midlinedraw=nil
169 | end
170 | if hcalendar.bg then
171 | hcalendar.bg:delete()
172 | hcalendar.bg=nil
173 | end
174 | if hcalendar.offday_holder then
175 | for i=1,#hcalendar.offday_holder do
176 | if hcalendar.offday_holder[i] then
177 | hcalendar.offday_holder[i]:delete()
178 | hcalendar.offday_holder[i]=nil
179 | end
180 | if hcalendar.offdaymidline_holder[i] then
181 | hcalendar.offdaymidline_holder[i]:delete()
182 | hcalendar.offdaymidline_holder[i]=nil
183 | end
184 | end
185 | end
186 | if hcalendar.todaydraw then
187 | hcalendar.todaydraw:delete()
188 | hcalendar.todaydraw=nil
189 | end
190 | if hcalendar.todaymidlinedraw then
191 | hcalendar.todaymidlinedraw:delete()
192 | hcalendar.todaymidlinedraw=nil
193 | end
194 | hcalendars[idx]=nil
195 | end
196 | end
197 |
198 | function showHCalendars()
199 | showHCalendar(hs.screen.primaryScreen())
200 | end
201 |
202 | function destroyHCalendars()
203 | for i in pairs(hcalendars) do
204 | destroyHCalendar(i)
205 | end
206 | end
207 |
208 | if not launch_hcalendar then launch_hcalendar=true end
209 | if launch_hcalendar == true then
210 | showHCalendars()
211 | if hcaltimer == nil then
212 | hcaltimer = hs.timer.doEvery(1800, function() showHCalendars() end)
213 | else
214 | hcaltimer:start()
215 | end
216 | hs.screen.watcher.newWithActiveScreen(function(activeChanged)
217 | if activeChanged then
218 | destroyHCalendars()
219 | hs.timer.doAfter(3, function()
220 | print('Refresh HCalendar')
221 | showHCalendars()
222 | end)
223 | end
224 | end):start()
225 | end
226 |
--------------------------------------------------------------------------------
/widgets/netspeed.lua:
--------------------------------------------------------------------------------
1 | function data_diff()
2 | local netspeed_in_str = 'netstat -ibn | grep -e ' .. netspeed_active_interface .. ' -m 1 | awk \'{print $7}\''
3 | local netspeed_out_str = 'netstat -ibn | grep -e ' .. netspeed_active_interface .. ' -m 1 | awk \'{print $10}\''
4 | local in_seq1 = hs.execute(netspeed_in_str)
5 | local out_seq1 = hs.execute(netspeed_out_str)
6 | if gainagain == nil then
7 | gainagain = hs.timer.doAfter(1,function()
8 | in_seq2 = hs.execute(netspeed_in_str)
9 | out_seq2 = hs.execute(netspeed_out_str)
10 | end)
11 | else
12 | gainagain:start()
13 | end
14 |
15 | if out_seq2 ~= nil then
16 | local in_diff = in_seq1 - in_seq2
17 | local out_diff = out_seq1 - out_seq2
18 | if in_diff/1024 > 1024 then
19 | kbin = string.format("%6.2f",in_diff/1024/1024) .. ' MB/s'
20 | else
21 | kbin = string.format("%6.2f",in_diff/1024) .. ' KB/s'
22 | end
23 | if out_diff/1024 > 1024 then
24 | kbout = string.format("%6.2f",out_diff/1024/1024) .. ' MB/s'
25 | else
26 | kbout = string.format("%6.2f",out_diff/1024) .. ' KB/s'
27 | end
28 | local disp_str = '⥄ '..kbout..'\n⥂ '..kbin
29 | if darkmode_status then
30 | styled_disp_str = hs.styledtext.new(disp_str,{font={size=9.0,color=white}})
31 | else
32 | styled_disp_str = hs.styledtext.new(disp_str,{font={size=9.0,color=black}})
33 | end
34 | netspeed_menubar:setTitle(styled_disp_str)
35 | end
36 | end
37 |
38 | netspeed_active_interface = hs.network.primaryInterfaces()
39 | local darkmode_status = hs.osascript.applescript('tell application "System Events"\nreturn dark mode of appearance preferences\nend tell')
40 | if netspeed_active_interface ~= false then
41 | if not netspeed_menubar then
42 | netspeed_menubar = hs.menubar.new()
43 | end
44 | data_diff()
45 | local interface_detail = hs.network.interfaceDetails(netspeed_active_interface)
46 | local menuitems_table = {}
47 | if interface_detail.AirPort then
48 | local ssid = interface_detail.AirPort.SSID
49 | table.insert(menuitems_table, {title="SSID: "..ssid, tooltip="Copy SSID to clipboard", fn=function() hs.pasteboard.setContents(ssid) end})
50 | end
51 | if interface_detail.IPv4 then
52 | local ipv4 = interface_detail.IPv4.Addresses[1]
53 | table.insert(menuitems_table, {title="IPv4: "..ipv4, tooltip="Copy IPv4 to clipboard", fn=function() hs.pasteboard.setContents(ipv4) end})
54 | end
55 | if interface_detail.IPv6 then
56 | local ipv6 = interface_detail.IPv6.Addresses[1]
57 | table.insert(menuitems_table, {title="IPv6: "..ipv6, tooltip="Copy IPv6 to clipboard", fn=function() hs.pasteboard.setContents(ipv6) end})
58 | end
59 | local macaddr = hs.execute('ifconfig '..netspeed_active_interface..' | grep ether | awk \'{print $2}\'')
60 | table.insert(menuitems_table, {title="MAC Addr: "..macaddr, tooltip="Copy MAC Address to clipboard", fn=function() hs.pasteboard.setContents(macaddr) end})
61 | netspeed_menubar:setMenu(menuitems_table)
62 | if nettimer == nil then
63 | nettimer = hs.timer.doEvery(1,data_diff)
64 | else
65 | nettimer:start()
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/widgets/timelapsed.lua:
--------------------------------------------------------------------------------
1 | timelapses = {}
2 |
3 | function showTimelapse(screen)
4 | if not timelapses[screen:id()] then
5 | timelapses[screen:id()] = {}
6 | end
7 | local timelapse = timelapses[screen:id()]
8 | timelapse.screen = screen
9 | timelapse.mainRes = screen:fullFrame()
10 | timelapse.localMainRes = screen:absoluteToLocal(timelapse.mainRes)
11 |
12 | if not timelapsetopleft then
13 | timelapse.topleft = {timelapse.localMainRes.w-280-120, timelapse.localMainRes.h-125-400}
14 | else
15 | timelapse.topleft = timelapsetopleft
16 | end
17 | local timelapsetopleft = timelapse.topleft
18 |
19 | local canvasrect = hs.geometry.rect(screen:localToAbsolute({x=timelapsetopleft[1],y=timelapsetopleft[2],w=280,h=125}))
20 | if not timelapse.canvas then
21 | timelapse.canvas = hs.canvas.new(canvasrect)
22 | timelapse.canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces)
23 | timelapse.canvas:level(hs.canvas.windowLevels.desktopIcon)
24 | timelapse.canvas:show()
25 | else
26 | timelapse.canvas:frame(canvasrect)
27 | end
28 |
29 | -- canvas background
30 | timelapse.canvas[1] = {
31 | action = "fill",
32 | type = "rectangle",
33 | fillColor = black,
34 | roundedRectRadii = {xRadius=5, yRadius=5},
35 | }
36 | timelapse.canvas[1].fillColor.alpha = .2
37 | -- title
38 | timelapse.canvas[2] = {
39 | type = "text",
40 | text = "Time Elapsed",
41 | textSize = 14,
42 | textColor = white,
43 | frame = {
44 | x = tostring(10/280),
45 | y = tostring(10/125),
46 | w = tostring(260/280),
47 | h = tostring(25/125),
48 | }
49 | }
50 | timelapse.canvas[2].textColor.alpha = .3
51 | -- time
52 | timelapse.canvas[3] = {
53 | type = "text",
54 | text = "",
55 | textColor = {hex="#A6AAC3"},
56 | textSize = 17,
57 | textAlignment = "center",
58 | frame = {
59 | x = tostring(0/280),
60 | y = tostring(35/125),
61 | w = tostring(280/280),
62 | h = tostring(25/125),
63 | }
64 | }
65 | -- indicator background
66 | timelapse.canvas[4] = {
67 | type = "image",
68 | image = hs.image.imageFromPath(hs.fs.pathToAbsolute(hs.configdir..'/resources/timebg.png')),
69 | frame = {
70 | x = tostring(10/280),
71 | y = tostring(65/125),
72 | w = tostring(260/280),
73 | h = tostring(50/125),
74 | }
75 | }
76 | -- light indicator
77 | timelapse.canvas[5] = {
78 | action = "fill",
79 | type = "rectangle",
80 | fillColor = white,
81 | frame = {
82 | x = tostring(20/280),
83 | y = tostring(75/125),
84 | w = tostring(240/280),
85 | h = tostring(20/125),
86 | }
87 | }
88 | timelapse.canvas[5].fillColor.alpha = .2
89 | -- indicator mask
90 | timelapse.canvas[6] = {
91 | action = "fill",
92 | type = "rectangle",
93 | frame = {
94 | x = tostring(20/280),
95 | y = tostring(75/125),
96 | w = tostring(240/280),
97 | h = tostring(20/125),
98 | }
99 | }
100 | -- color indicator
101 | timelapse.canvas[7] = {
102 | action = "fill",
103 | type = "rectangle",
104 | frame = {
105 | x = tostring(20/280),
106 | y = tostring(75/125),
107 | w = tostring(240/280),
108 | h = tostring(20/125),
109 | },
110 | fillGradient="linear",
111 | fillGradientColors = {
112 | {hex = "#00A0F7"},
113 | {hex = "#92D2E5"},
114 | {hex = "#4BE581"},
115 | {hex = "#EAF25E"},
116 | {hex = "#F4CA55"},
117 | {hex = "#E04E4E"},
118 | },
119 | }
120 | timelapse.canvas[7].compositeRule = "sourceAtop"
121 |
122 | if timelapse.elapsedTimer == nil then
123 | timelapse.elapsedTimer = hs.timer.doEvery(1, function() updateElapsedCanvas(timelapse) end)
124 | else
125 | timelapse.elapsedTimer:start()
126 | end
127 | end
128 |
129 | function destroyTimelapse(idx)
130 | if timelapses[idx] then
131 | local timelapse = timelapses[idx]
132 | if hs.screen.find(timelapse.screen:id()) then
133 | return
134 | end
135 | if timelapse.elapsedTimer then
136 | timelapse.elapsedTimer:stop()
137 | timelapse.elapsedTimer=nil
138 | end
139 | if timelapse.canvas then
140 | timelapse.canvas:delete()
141 | timelapse.canvas=nil
142 | end
143 | timelapses[idx]=nil
144 | end
145 | end
146 |
147 | function updateElapsedCanvas(timelapse)
148 | local nowtable = os.date("*t")
149 | local nowyday = nowtable.yday
150 | local nowhour = string.format("%2s", nowtable.hour)
151 | local nowmin = string.format("%2s", nowtable.min)
152 | local nowsec = string.format("%2s", nowtable.sec)
153 | local timestr = nowyday.." days "..nowhour.." hours "..nowmin.." min "..nowsec.." sec"
154 | local secs_since_epoch = os.time()
155 | local nowyear = nowtable.year
156 | local yearstartsecs_since_epoch = os.time({year=nowyear, month=1, day=1, hour=0})
157 | local nowyear_elapsed_secs = secs_since_epoch - yearstartsecs_since_epoch
158 | local yearendsecs_since_epoch = os.time({year=nowyear+1, month=1, day=1, hour=0})
159 | local nowyear_total_secs = yearendsecs_since_epoch - yearstartsecs_since_epoch
160 | local elapsed_percent = nowyear_elapsed_secs/nowyear_total_secs
161 | if timelapse.canvas:isShowing() then
162 | timelapse.canvas[3].text = timestr
163 | timelapse.canvas[6].frame.w = tostring(240/280*elapsed_percent)
164 | end
165 | end
166 |
167 | function showTimelapses()
168 | showTimelapse(hs.screen.primaryScreen())
169 | end
170 |
171 | function destroyTimelapses()
172 | for i in pairs(timelapses) do
173 | destroyTimelapse(i)
174 | end
175 | end
176 |
177 | if not launch_timelapse then launch_timelapse = true end
178 | if launch_timelapse == true then
179 | showTimelapses()
180 | hs.screen.watcher.newWithActiveScreen(function(activeChanged)
181 | if activeChanged then
182 | destroyTimelapses()
183 | hs.timer.doAfter(3, function()
184 | print('Refresh Timelapse')
185 | showTimelapses()
186 | end)
187 | end
188 | end):start()
189 | end
190 |
191 |
--------------------------------------------------------------------------------