├── .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 | ![widgets](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-deskwidgets.png) 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 | ![winresize](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-winresize.gif) 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 | ![tips](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-tips.png) 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 | ![hsearch](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-hsearch.gif) 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 | ![hsearch](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-aria2.png) 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 | ![timeralert](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-timeralert.png) 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 | ![cheatsheet](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-cheatsheet.png) 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 | ![tmuxtime](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-tmuxtime.png) 222 | 223 |
224 | 225 |
226 | Windows Hint + 227 | 228 | Focus to your windows easier. 229 | 230 | ![windowshint](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-windowshint.png) 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.."" 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 | --------------------------------------------------------------------------------