From 4ea76e54b966cbaf6302247e92a401f322c55f2a Mon Sep 17 00:00:00 2001 From: Ein Verne Date: Mon, 24 Aug 2020 15:40:17 +0800 Subject: [PATCH] Add hammerspoon config --- hammerspoon/.gitignore | 2 + hammerspoon/README.md | 149 +++++ hammerspoon/Spoons/AClock.spoon/docs.json | 79 +++ hammerspoon/Spoons/AClock.spoon/init.lua | 57 ++ hammerspoon/Spoons/BingDaily.spoon/docs.json | 41 ++ hammerspoon/Spoons/BingDaily.spoon/init.lua | 63 ++ hammerspoon/Spoons/Calendar.spoon/docs.json | 41 ++ hammerspoon/Spoons/Calendar.spoon/init.lua | 183 ++++++ .../Spoons/CircleClock.spoon/docs.json | 41 ++ hammerspoon/Spoons/CircleClock.spoon/init.lua | 116 ++++ .../Spoons/CircleClock.spoon/watchbg.png | Bin 0 -> 28336 bytes hammerspoon/Spoons/ClipShow.spoon/docs.json | 283 +++++++++ hammerspoon/Spoons/ClipShow.spoon/init.lua | 394 ++++++++++++ hammerspoon/Spoons/CountDown.spoon/docs.json | 163 +++++ hammerspoon/Spoons/CountDown.spoon/init.lua | 129 ++++ hammerspoon/Spoons/HCalendar.spoon/docs.json | 41 ++ hammerspoon/Spoons/HCalendar.spoon/init.lua | 179 ++++++ hammerspoon/Spoons/HSearch.spoon/docs.json | 159 +++++ hammerspoon/Spoons/HSearch.spoon/hs_btabs.lua | 55 ++ .../Spoons/HSearch.spoon/hs_datamuse.lua | 73 +++ hammerspoon/Spoons/HSearch.spoon/hs_emoji.lua | 87 +++ hammerspoon/Spoons/HSearch.spoon/hs_note.lua | 96 +++ hammerspoon/Spoons/HSearch.spoon/hs_time.lua | 87 +++ hammerspoon/Spoons/HSearch.spoon/hs_v2ex.lua | 63 ++ .../Spoons/HSearch.spoon/hs_yddict.lua | 94 +++ hammerspoon/Spoons/HSearch.spoon/init.lua | 256 ++++++++ .../Spoons/HSearch.spoon/resources/chrome.png | Bin 0 -> 7920 bytes .../Spoons/HSearch.spoon/resources/emoji.png | Bin 0 -> 7843 bytes .../HSearch.spoon/resources/justnote.png | Bin 0 -> 31419 bytes .../Spoons/HSearch.spoon/resources/menus.png | Bin 0 -> 437 bytes .../Spoons/HSearch.spoon/resources/safari.png | Bin 0 -> 7593 bytes .../Spoons/HSearch.spoon/resources/tabs.png | Bin 0 -> 2385 bytes .../HSearch.spoon/resources/taskkill.png | Bin 0 -> 7198 bytes .../HSearch.spoon/resources/thesaurus.png | Bin 0 -> 4723 bytes .../Spoons/HSearch.spoon/resources/time.png | Bin 0 -> 35916 bytes .../Spoons/HSearch.spoon/resources/v2ex.png | Bin 0 -> 3185 bytes .../Spoons/HSearch.spoon/resources/youdao.png | Bin 0 -> 5970 bytes hammerspoon/Spoons/KSheet.spoon/docs.json | 119 ++++ hammerspoon/Spoons/KSheet.spoon/init.lua | 214 +++++++ hammerspoon/Spoons/ModalMgr.spoon/docs.json | 253 ++++++++ hammerspoon/Spoons/ModalMgr.spoon/init.lua | 189 ++++++ .../ReloadConfiguration.spoon/docs.json | 90 +++ .../Spoons/ReloadConfiguration.spoon/init.lua | 49 ++ hammerspoon/Spoons/SpeedMenu.spoon/docs.json | 79 +++ hammerspoon/Spoons/SpeedMenu.spoon/init.lua | 111 ++++ hammerspoon/Spoons/TimeFlow.spoon/docs.json | 41 ++ hammerspoon/Spoons/TimeFlow.spoon/init.lua | 153 +++++ hammerspoon/Spoons/TimeFlow.spoon/timebg.png | Bin 0 -> 18390 bytes hammerspoon/Spoons/UnsplashZ.spoon/docs.json | 41 ++ hammerspoon/Spoons/UnsplashZ.spoon/init.lua | 52 ++ hammerspoon/Spoons/WinWin.spoon/docs.json | 406 +++++++++++++ hammerspoon/Spoons/WinWin.spoon/init.lua | 278 +++++++++ hammerspoon/autoscript.lua | 25 + hammerspoon/config-example.lua | 95 +++ hammerspoon/ime.lua | 72 +++ hammerspoon/init.lua | 566 ++++++++++++++++++ hammerspoon/usb.lua | 15 + 57 files changed, 5779 insertions(+) create mode 100644 hammerspoon/.gitignore create mode 100644 hammerspoon/README.md create mode 100644 hammerspoon/Spoons/AClock.spoon/docs.json create mode 100644 hammerspoon/Spoons/AClock.spoon/init.lua create mode 100644 hammerspoon/Spoons/BingDaily.spoon/docs.json create mode 100644 hammerspoon/Spoons/BingDaily.spoon/init.lua create mode 100644 hammerspoon/Spoons/Calendar.spoon/docs.json create mode 100644 hammerspoon/Spoons/Calendar.spoon/init.lua create mode 100644 hammerspoon/Spoons/CircleClock.spoon/docs.json create mode 100644 hammerspoon/Spoons/CircleClock.spoon/init.lua create mode 100644 hammerspoon/Spoons/CircleClock.spoon/watchbg.png create mode 100644 hammerspoon/Spoons/ClipShow.spoon/docs.json create mode 100644 hammerspoon/Spoons/ClipShow.spoon/init.lua create mode 100644 hammerspoon/Spoons/CountDown.spoon/docs.json create mode 100644 hammerspoon/Spoons/CountDown.spoon/init.lua create mode 100644 hammerspoon/Spoons/HCalendar.spoon/docs.json create mode 100644 hammerspoon/Spoons/HCalendar.spoon/init.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/docs.json create mode 100644 hammerspoon/Spoons/HSearch.spoon/hs_btabs.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/hs_datamuse.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/hs_emoji.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/hs_note.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/hs_time.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/hs_v2ex.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/hs_yddict.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/init.lua create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/chrome.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/emoji.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/justnote.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/menus.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/safari.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/tabs.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/taskkill.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/thesaurus.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/time.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/v2ex.png create mode 100644 hammerspoon/Spoons/HSearch.spoon/resources/youdao.png create mode 100644 hammerspoon/Spoons/KSheet.spoon/docs.json create mode 100644 hammerspoon/Spoons/KSheet.spoon/init.lua create mode 100644 hammerspoon/Spoons/ModalMgr.spoon/docs.json create mode 100644 hammerspoon/Spoons/ModalMgr.spoon/init.lua create mode 100644 hammerspoon/Spoons/ReloadConfiguration.spoon/docs.json create mode 100644 hammerspoon/Spoons/ReloadConfiguration.spoon/init.lua create mode 100644 hammerspoon/Spoons/SpeedMenu.spoon/docs.json create mode 100644 hammerspoon/Spoons/SpeedMenu.spoon/init.lua create mode 100644 hammerspoon/Spoons/TimeFlow.spoon/docs.json create mode 100644 hammerspoon/Spoons/TimeFlow.spoon/init.lua create mode 100644 hammerspoon/Spoons/TimeFlow.spoon/timebg.png create mode 100644 hammerspoon/Spoons/UnsplashZ.spoon/docs.json create mode 100644 hammerspoon/Spoons/UnsplashZ.spoon/init.lua create mode 100644 hammerspoon/Spoons/WinWin.spoon/docs.json create mode 100644 hammerspoon/Spoons/WinWin.spoon/init.lua create mode 100644 hammerspoon/autoscript.lua create mode 100644 hammerspoon/config-example.lua create mode 100644 hammerspoon/ime.lua create mode 100644 hammerspoon/init.lua create mode 100644 hammerspoon/usb.lua diff --git a/hammerspoon/.gitignore b/hammerspoon/.gitignore new file mode 100644 index 0000000..a13862d --- /dev/null +++ b/hammerspoon/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +private diff --git a/hammerspoon/README.md b/hammerspoon/README.md new file mode 100644 index 0000000..d4f3807 --- /dev/null +++ b/hammerspoon/README.md @@ -0,0 +1,149 @@ +# hammerspoon configuration + +hammerspoon is my configuration for [Hammerspoon](http://www.hammerspoon.org/). It has highly modal-based, vim-style keybindings, provides some functionality like desktop widgets, window management, application launcher, instant search, aria2 frontend ... etc. + +## Get started + +- Install [Hammerspoon](http://www.hammerspoon.org/) first. +- `git clone https://github.com/einverne/dotfiles.git ~/dotfiles` +- `ln -s ~/dotfiles/hammerspoon ~/.hammerspoon` +- Reload the configutation. + +## Keep update + +`cd ~/dotfiles && git pull` + +## How to use +Use Karabier-Elements to set caps lock as hyper key. Press caps lock is just like press Cmd+Control+Option+Shift at the same time. + +## Reload config + +- Hyper key + R, reload hammerspoon config + +## Hyper key windows management + +- Hyper key + H, set windows to left half of screen +- Hyper key + L, right half of screen +- Hyper key + J, bottom half +- Hyper key + K, top half +- Hyper key + F, full screen + +### Windows management mode + +Option+r Enter windows management: + +- ASDW to move windows position +- HL/JK to set windows to left, right, up, down half of screen +- Y/O/U/I to set windows to LeftUp, RightUp, LeftDown, LeftDown corner +- Left/Right/Up/Down same as HL/JK +- F to set windows to full screen +- C to set windows to center +- Esc/Q to exit +- Tab to show help + +### Switch windows +I personally use the application called Context to switch between different windows, however this config provider another way to quickly switch between windows. Try with `Option+Tab`. + +## Toggle hammerspoon console + +Option+z + +## Move windows between monitors + +- Hyper key + N, to move current window to next monitor +- Hpper key + P, to move current window to previous monitor + +### Application launcher + +Press option + a to enter application launcher. The shorcut information will show on the center of the screen. But I personally prefer Alfred. + + +Just press opt, plus A or C or R… to start. If need help, press tab to toggle the keybindings cheatsheet. + +Press opt + ? to toggle the help panel, which will show all opt related keybindings. + +### Screenshots + +These screenshots demostrate what awesome-hammerspoon is capable of. Learn more about [built-in Spoons](https://github.com/ashfinal/awesome-hammerspoon/wiki/The-built-in-Spoons). + +#### Desktop widgets + + +![widgets](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-deskwidgets.png) + + +#### Window manpulation + R + + +![winresize](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-winresize.gif) + + + +#### aria2 Frontend + D + + +![hsearch](https://github.com/ashfinal/bindata/raw/master/screenshots/awesome-hammerspoon-aria2.png) + +You need to [run aria2 with RPC enabled](https://github.com/ashfinal/awesome-hammerspoon/wiki/Run-aria2-with-rpc-enabled) before using this. Config aria2 host and token in `~/.hammerspoon/private/config.lua`, then you're ready to go. + +```lua +hsaria2_host = "http://localhost:6800/jsonrpc" -- default host +hsaria2_secret = "token" -- YOUR OWN SECRET +``` + + +## Customization + + +```shell +cp ~/.hammerspoon/config-example.lua ~/.hammerspoon/private/config.lua +``` + +Then modify the file `~/.hammerspoon/private/config.lua`: + +- Add/remove Spoons. + + Define `hspoon_list` to decide which Spoons (a distributing format of Hammerspoon module) to be loaded. There are 15 built-in Spoons, learn about them at [here](https://github.com/ashfinal/awesome-hammerspoon/wiki/The-built-in-Spoons). + + *There are more Spoons at [official spoon repository](http://www.hammerspoon.org/Spoons/) (you may need a little config before using them).* + +- Customize keybindings + + Please read `~/.hammerspoon/private/config.lua`for more details. + +Finally press `cmd + ctrl + shift + r` to reload the configuration. + + +## Contribute + + +- Improve existing Spoons + + A "Spoon" is just a directory, right-click on it -> "Show Package Contents". + + Feel free to file issues or open PRs. + +- Create new Spoons + + Some resources you may find helpful: + + [Learn Lua in Y minutes](http://learnxinyminutes.com/docs/lua/) + + [Getting Started with Hammerspoon](http://www.hammerspoon.org/go/) + + [Hammerspoon API Docs](http://www.hammerspoon.org/docs/index.html) + + [hammerspoon/SPOONS.md at master · Hammerspoon/hammerspoon](https://github.com/Hammerspoon/hammerspoon/blob/master/SPOONS.md) + + +## Thanks to + + +[https://github.com/zzamboni/oh-my-hammerspoon](https://github.com/zzamboni/oh-my-hammerspoon) + +[https://github.com/scottcs/dot_hammerspoon](https://github.com/scottcs/dot_hammerspoon) + +[https://github.com/dharmapoudel/hammerspoon-config](https://github.com/dharmapoudel/hammerspoon-config) + +[http://tracesof.net/uebersicht/](http://tracesof.net/uebersicht/) + diff --git a/hammerspoon/Spoons/AClock.spoon/docs.json b/hammerspoon/Spoons/AClock.spoon/docs.json new file mode 100644 index 0000000..190808f --- /dev/null +++ b/hammerspoon/Spoons/AClock.spoon/docs.json @@ -0,0 +1,79 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "type" : "Module", + "desc" : "Just another clock, floating above all", + "Constructor" : [ + + ], + "doc" : "Just another clock, floating above all\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/AClock.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/AClock.spoon.zip)", + "Field" : [ + + ], + "items" : [ + { + "doc" : "Show AClock, if already showing, just hide it.", + "stripped_doc" : [ + "Show AClock, if already showing, just hide it." + ], + "def" : "AClock:toggleShow()", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "AClock:toggleShow()", + "type" : "Method", + "returns" : [ + + ], + "name" : "toggleShow", + "desc" : "Show AClock, if already showing, just hide it." + } + ], + "Method" : [ + { + "doc" : "Show AClock, if already showing, just hide it.", + "stripped_doc" : [ + "Show AClock, if already showing, just hide it." + ], + "def" : "AClock:toggleShow()", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "AClock:toggleShow()", + "type" : "Method", + "returns" : [ + + ], + "name" : "toggleShow", + "desc" : "Show AClock, if already showing, just hide it." + } + ], + "Command" : [ + + ], + "name" : "AClock" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/AClock.spoon/init.lua b/hammerspoon/Spoons/AClock.spoon/init.lua new file mode 100644 index 0000000..a991e37 --- /dev/null +++ b/hammerspoon/Spoons/AClock.spoon/init.lua @@ -0,0 +1,57 @@ +--- === AClock === +--- +--- Just another clock, floating above all +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/AClock.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/AClock.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "AClock" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +function obj:init() + self.canvas = hs.canvas.new({x=0, y=0, w=0, h=0}):show() + self.canvas[1] = { + type = "text", + text = "", + textFont = "Impact", + textSize = 130, + textColor = {hex="#1891C3"}, + textAlignment = "center", + } +end + +--- AClock:toggleShow() +--- Method +--- Show AClock, if already showing, just hide it. +--- + +function obj:toggleShow() + if self.timer then + self.timer:stop() + self.timer = nil + self.canvas:hide() + else + local mainScreen = hs.screen.mainScreen() + local mainRes = mainScreen:fullFrame() + self.canvas:frame({ + x = (mainRes.w-300)/2, + y = (mainRes.h-230)/2, + w = 300, + h = 230 + }) + self.canvas[1].text = os.date("%H:%M") + self.canvas:show() + self.timer = hs.timer.doAfter(4, function() + self.canvas:hide() + self.timer = nil + end) + end +end + +return obj diff --git a/hammerspoon/Spoons/BingDaily.spoon/docs.json b/hammerspoon/Spoons/BingDaily.spoon/docs.json new file mode 100644 index 0000000..03b9e5e --- /dev/null +++ b/hammerspoon/Spoons/BingDaily.spoon/docs.json @@ -0,0 +1,41 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "desc" : "Use Bing daily picture as your wallpaper, automatically.", + "type" : "Module", + "Deprecated" : [ + + ], + "Constructor" : [ + + ], + "doc" : "Use Bing daily picture as your wallpaper, automatically.\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/BingDaily.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/BingDaily.spoon.zip)", + "items" : [ + + ], + "Command" : [ + + ], + "Method" : [ + + ], + "Field" : [ + + ], + "name" : "BingDaily" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/BingDaily.spoon/init.lua b/hammerspoon/Spoons/BingDaily.spoon/init.lua new file mode 100644 index 0000000..b765fba --- /dev/null +++ b/hammerspoon/Spoons/BingDaily.spoon/init.lua @@ -0,0 +1,63 @@ +--- === BingDaily === +--- +--- Use Bing daily picture as your wallpaper, automatically. +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BingDaily.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BingDaily.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "BingDaily" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +local function curl_callback(exitCode, stdOut, stdErr) + if exitCode == 0 then + obj.task = nil + obj.last_pic = hs.http.urlParts(obj.full_url).lastPathComponent + local localpath = os.getenv("HOME") .. "/.Trash/" .. hs.http.urlParts(obj.full_url).lastPathComponent + hs.screen.mainScreen():desktopImageURL("file://" .. localpath) + else + print(stdOut, stdErr) + end +end + +local function bingRequest() + local 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" + local json_req_url = "http://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1" + hs.http.asyncGet(json_req_url, {["User-Agent"]=user_agent_str}, function(stat,body,header) + if stat == 200 then + if pcall(function() hs.json.decode(body) end) then + local decode_data = hs.json.decode(body) + local pic_url = decode_data.images[1].url + local pic_name = hs.http.urlParts(pic_url).lastPathComponent + if obj.last_pic ~= pic_name then + obj.full_url = "https://www.bing.com" .. pic_url + if obj.task then + obj.task:terminate() + obj.task = nil + end + local localpath = os.getenv("HOME") .. "/.Trash/" .. hs.http.urlParts(obj.full_url).lastPathComponent + obj.task = hs.task.new("/usr/bin/curl", curl_callback, {"-A", user_agent_str, obj.full_url, "-o", localpath}) + obj.task:start() + end + end + else + print("Bing URL request failed!") + end + end) +end + +function obj:init() + if obj.timer == nil then + obj.timer = hs.timer.doEvery(3*60*60, function() bingRequest() end) + obj.timer:setNextTrigger(5) + else + obj.timer:start() + end +end + +return obj diff --git a/hammerspoon/Spoons/Calendar.spoon/docs.json b/hammerspoon/Spoons/Calendar.spoon/docs.json new file mode 100644 index 0000000..6cff5d8 --- /dev/null +++ b/hammerspoon/Spoons/Calendar.spoon/docs.json @@ -0,0 +1,41 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "type" : "Module", + "desc" : "A calendar inset into the desktop", + "Constructor" : [ + + ], + "doc" : "A calendar inset into the desktop\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/Calendar.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/Calendar.spoon.zip)", + "Field" : [ + + ], + "Command" : [ + + ], + "Method" : [ + + ], + "items" : [ + + ], + "name" : "Calendar" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/Calendar.spoon/init.lua b/hammerspoon/Spoons/Calendar.spoon/init.lua new file mode 100644 index 0000000..527a706 --- /dev/null +++ b/hammerspoon/Spoons/Calendar.spoon/init.lua @@ -0,0 +1,183 @@ +--- === Calendar === +--- +--- A calendar inset into the desktop +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Calendar.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Calendar.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "Calendar" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +obj.calw = 260 +obj.calh = 184 + +local function updateCalCanvas() + local titlestr = os.date("%B %Y") + obj.canvas[2].text = titlestr + local current_year = os.date("%Y") + local current_month = os.date("%m") + local current_day = os.date("%d") + local firstday_of_nextmonth = os.time{year=current_year, month=current_month+1, day=1} + local maxday_of_currentmonth = os.date("*t", firstday_of_nextmonth-24*60*60).day + local weekday_of_firstday = os.date("*t", os.time{year=current_year, month=current_month, day=1}).wday + local needed_rownum = math.ceil((weekday_of_firstday+maxday_of_currentmonth-1)/7) + + for i=1,needed_rownum do + for k=1,7 do + local caltable_idx = 7*(i-1)+k + local pushbacked_value = caltable_idx-weekday_of_firstday + 2 + if pushbacked_value <= 0 or pushbacked_value > maxday_of_currentmonth then + obj.canvas[9+caltable_idx].text = "" + else + obj.canvas[9+caltable_idx].text = pushbacked_value + end + if pushbacked_value == math.tointeger(current_day) then + obj.canvas[58].frame.x = tostring((10+(obj.calw-20)/8*k)/obj.calw) + obj.canvas[58].frame.y = tostring((10+(obj.calh-20)/8*(i+1))/obj.calh) + end + end + end + -- update yearweek + local yearweek_of_firstday = hs.execute("date -v1d +'%W'") + for i=1,6 do + local yearweek_rowvalue = math.tointeger(yearweek_of_firstday)+i-1 + obj.canvas[51+i].text = yearweek_rowvalue + if i > needed_rownum then + obj.canvas[51+i].text = "" + end + end + -- trim the canvas + obj.canvas:size({ + w = obj.calw, + h = 20+(obj.calh-20)/8*(needed_rownum+2) + }) +end + +function obj:init() + local caltodaycolor = {red=1, blue=1, green=1, alpha=0.3} + local calcolor = {red=235/255, blue=235/255, green=235/255} + local calbgcolor = {red=0, blue=0, green=0, alpha=0.3} + local weeknumcolor = {red=246/255, blue=246/255, green=246/255, alpha=0.5} + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + + obj.canvas = hs.canvas.new({ + x = cres.w-obj.calw-20, + y = cres.h-obj.calh-20, + w = obj.calw, + h = obj.calh + }):show() + + obj.canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) + obj.canvas:level(hs.canvas.windowLevels.desktopIcon) + + obj.canvas[1] = { + id = "cal_bg", + type = "rectangle", + action = "fill", + fillColor = calbgcolor, + roundedRectRadii = {xRadius = 10, yRadius = 10}, + } + + obj.canvas[2] = { + id = "cal_title", + type = "text", + text = "", + textFont = "Courier", + textSize = 16, + textColor = calcolor, + textAlignment = "center", + frame = { + x = tostring(10/obj.calw), + y = tostring(10/obj.calw), + w = tostring(1-20/obj.calw), + h = tostring((obj.calh-20)/8/obj.calh) + } + } + + local weeknames = {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"} + for i=1,#weeknames do + obj.canvas[2+i] = { + id = "cal_weekday", + type = "text", + text = weeknames[i], + textFont = "Courier", + textSize = 16, + textColor = calcolor, + textAlignment = "center", + frame = { + x = tostring((10+(obj.calw-20)/8*i)/obj.calw), + y = tostring((10+(obj.calh-20)/8)/obj.calh), + w = tostring((obj.calw-20)/8/obj.calw), + h = tostring((obj.calh-20)/8/obj.calh) + } + } + end + + -- Create 7x6 calendar table + for i=1,6 do + for k=1,7 do + obj.canvas[9+7*(i-1)+k] = { + type = "text", + text = "", + textFont = "Courier", + textSize = 16, + textColor = calcolor, + textAlignment = "center", + frame = { + x = tostring((10+(obj.calw-20)/8*k)/obj.calw), + y = tostring((10+(obj.calh-20)/8*(i+1))/obj.calh), + w = tostring((obj.calw-20)/8/obj.calw), + h = tostring((obj.calh-20)/8/obj.calh) + } + } + end + end + + -- Create yearweek column + for i=1,6 do + obj.canvas[51+i] = { + type = "text", + text = "", + textFont = "Courier", + textSize = 16, + textColor = weeknumcolor, + textAlignment = "center", + frame = { + x = tostring(10/obj.calw), + y = tostring((10+(obj.calh-20)/8*(i+1))/obj.calh), + w = tostring((obj.calw-20)/8/obj.calw), + h = tostring((obj.calh-20)/8/obj.calh) + } + } + end + + -- today cover rectangle + obj.canvas[58] = { + type = "rectangle", + action = "fill", + fillColor = caltodaycolor, + roundedRectRadii = {xRadius = 3, yRadius = 3}, + frame = { + x = tostring((10+(obj.calw-20)/8)/obj.calw), + y = tostring((10+(obj.calh-20)/8*2)/obj.calh), + w = tostring((obj.calw-20)/8/obj.calw), + h = tostring((obj.calh-20)/8/obj.calh) + } + } + + if obj.timer == nil then + obj.timer = hs.timer.doEvery(1800, function() updateCalCanvas() end) + obj.timer:setNextTrigger(0) + else + obj.timer:start() + end +end + +return obj diff --git a/hammerspoon/Spoons/CircleClock.spoon/docs.json b/hammerspoon/Spoons/CircleClock.spoon/docs.json new file mode 100644 index 0000000..21642e6 --- /dev/null +++ b/hammerspoon/Spoons/CircleClock.spoon/docs.json @@ -0,0 +1,41 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "type" : "Module", + "Deprecated" : [ + + ], + "desc" : "A circleclock inset into the desktop", + "Constructor" : [ + + ], + "Field" : [ + + ], + "Method" : [ + + ], + "Command" : [ + + ], + "items" : [ + + ], + "doc" : "A circleclock inset into the desktop\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/CircleClock.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/CircleClock.spoon.zip)", + "name" : "CircleClock" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/CircleClock.spoon/init.lua b/hammerspoon/Spoons/CircleClock.spoon/init.lua new file mode 100644 index 0000000..ba07901 --- /dev/null +++ b/hammerspoon/Spoons/CircleClock.spoon/init.lua @@ -0,0 +1,116 @@ +--- === CircleClock === +--- +--- A circleclock inset into the desktop +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/CircleClock.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/CircleClock.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "CircleClock" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +local function updateClock() + local secnum = math.tointeger(os.date("%S")) + local minnum = math.tointeger(os.date("%M")) + local hournum = math.tointeger(os.date("%I")) + local secangle = 6*secnum + local minangle = 6*minnum+6/60*secnum + local hourangle = 30*hournum+30/60*minnum+30/60/60*secnum + + obj.canvas[3].endAngle = secangle + obj.canvas[7].endAngle = minangle + -- hourangle may be larger than 360 at 12pm-1pm + if hourangle >= 360 then + hourangle = hourangle-360 + end + obj.canvas[5].endAngle = hourangle +end + +function obj:init() + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + self.canvas = hs.canvas.new({ + x = cres.w-300-20, + y = 100, + w = 200, + h = 200 + }):show() + obj.canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) + obj.canvas:level(hs.canvas.windowLevels.desktopIcon) + obj.canvas[1] = { + id = "watch_image", + type = "image", + image = hs.image.imageFromPath(self.spoonPath .. "/watchbg.png"), + } + obj.canvas[2] = { + id = "watch_circle", + type = "circle", + radius = "40%", + action = "stroke", + strokeColor = {hex="#9E9E9E", alpha=0.3}, + } + obj.canvas[3] = { + id = "watch_sechand", + type = "arc", + radius = "40%", + fillColor = {hex="#9E9E9E", alpha=0.1}, + strokeColor = {hex="#9E9E9E", alpha=0.3}, + endAngle = 0, + } + obj.canvas[4] = { + id = "watch_hourcircle", + type = "circle", + action = "stroke", + radius = "20%", + strokeWidth = 3, + strokeColor = {hex="#FFFFFF", alpha=0.1}, + } + obj.canvas[5] = { + id = "watch_hourarc", + type = "arc", + action = "stroke", + radius = "20%", + arcRadii = false, + strokeWidth = 3, + strokeColor = {hex="#EC6D27", alpha=0.75}, + endAngle = 0, + } + obj.canvas[6] = { + id = "watch_mincircle", + type = "circle", + action = "stroke", + radius = "27%", + strokeWidth = 3, + strokeColor = {hex="#FFFFFF", alpha=0.1}, + } + obj.canvas[7] = { + id = "watch_minarc", + type = "arc", + action = "stroke", + radius = "27%", + arcRadii = false, + strokeWidth = 3, + strokeColor = {hex="#1891C3", alpha=0.75}, + endAngle = 0, + } + if obj.timer == nil then + obj.timer = hs.timer.doEvery(1, function() updateClock() end) + else + obj.timer:start() + end +end + +return obj diff --git a/hammerspoon/Spoons/CircleClock.spoon/watchbg.png b/hammerspoon/Spoons/CircleClock.spoon/watchbg.png new file mode 100644 index 0000000000000000000000000000000000000000..ec387da845dcf5f1bd613bd8c6e12be632f86118 GIT binary patch literal 28336 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 literal 0 HcmV?d00001 diff --git a/hammerspoon/Spoons/ClipShow.spoon/docs.json b/hammerspoon/Spoons/ClipShow.spoon/docs.json new file mode 100644 index 0000000..7351939 --- /dev/null +++ b/hammerspoon/Spoons/ClipShow.spoon/docs.json @@ -0,0 +1,283 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "desc" : "Show the content of system clipboard", + "type" : "Module", + "Constructor" : [ + + ], + "doc" : "Show the content of system clipboard\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/ClipShow.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/ClipShow.spoon.zip)", + "Method" : [ + { + "desc" : "Process the content of system clipboard and show\/hide the canvas", + "def" : "ClipShow:toggleShow()", + "stripped_doc" : [ + "Process the content of system clipboard and show\/hide the canvas" + ], + "doc" : "Process the content of system clipboard and show\/hide the canvas", + "notes" : [ + + ], + "signature" : "ClipShow:toggleShow()", + "type" : "Method", + "returns" : [ + + ], + "name" : "toggleShow", + "parameters" : [ + + ] + }, + { + "desc" : "Open content of the clipboard in default browser with specific refstr.", + "def" : "ClipShow:openInBrowserWithRef(refstr)", + "stripped_doc" : [ + "Open content of the clipboard in default browser with specific refstr.", + "" + ], + "doc" : "Open content of the clipboard in default browser with specific refstr.\n\nParameters:\n * refstr - A optional string specifying which refstr to use. If nil, then open this content in browser directly. The \"refstr\" could be something like this: `https:\/\/www.bing.com\/search?q=`.", + "notes" : [ + + ], + "signature" : "ClipShow:openInBrowserWithRef(refstr)", + "type" : "Method", + "returns" : [ + + ], + "name" : "openInBrowserWithRef", + "parameters" : [ + " * refstr - A optional string specifying which refstr to use. If nil, then open this content in browser directly. The \"refstr\" could be something like this: `https:\/\/www.bing.com\/search?q=`." + ] + }, + { + "desc" : "Save clipboard session so we can restore it later", + "def" : "ClipShow:saveToSession()", + "stripped_doc" : [ + "Save clipboard session so we can restore it later" + ], + "doc" : "Save clipboard session so we can restore it later", + "notes" : [ + + ], + "signature" : "ClipShow:saveToSession()", + "type" : "Method", + "returns" : [ + + ], + "name" : "saveToSession", + "parameters" : [ + + ] + }, + { + "desc" : "Restore the lastsession of system clipboard", + "def" : "ClipShow:restoreLastSession()", + "stripped_doc" : [ + "Restore the lastsession of system clipboard" + ], + "doc" : "Restore the lastsession of system clipboard", + "notes" : [ + + ], + "signature" : "ClipShow:restoreLastSession()", + "type" : "Method", + "returns" : [ + + ], + "name" : "restoreLastSession", + "parameters" : [ + + ] + }, + { + "desc" : "Save content of current canvas to a file, the default location is `~\/Desktop\/`.", + "def" : "ClipShow:saveToFile()", + "stripped_doc" : [ + "Save content of current canvas to a file, the default location is `~\/Desktop\/`." + ], + "doc" : "Save content of current canvas to a file, the default location is `~\/Desktop\/`.", + "notes" : [ + + ], + "signature" : "ClipShow:saveToFile()", + "type" : "Method", + "returns" : [ + + ], + "name" : "saveToFile", + "parameters" : [ + + ] + }, + { + "desc" : "Open local file with specific command.", + "def" : "ClipShow:openWithCommand(command)", + "stripped_doc" : [ + "Open local file with specific command.", + "" + ], + "doc" : "Open local file with specific command.\n\nParameters:\n * command - A string specifying which command to use. The \"command\" is something like this: `\/usr\/local\/bin\/mvim`.", + "notes" : [ + + ], + "signature" : "ClipShow:openWithCommand(command)", + "type" : "Method", + "returns" : [ + + ], + "name" : "openWithCommand", + "parameters" : [ + " * command - A string specifying which command to use. The \"command\" is something like this: `\/usr\/local\/bin\/mvim`." + ] + } + ], + "Command" : [ + + ], + "Field" : [ + + ], + "items" : [ + { + "desc" : "Open content of the clipboard in default browser with specific refstr.", + "def" : "ClipShow:openInBrowserWithRef(refstr)", + "stripped_doc" : [ + "Open content of the clipboard in default browser with specific refstr.", + "" + ], + "doc" : "Open content of the clipboard in default browser with specific refstr.\n\nParameters:\n * refstr - A optional string specifying which refstr to use. If nil, then open this content in browser directly. The \"refstr\" could be something like this: `https:\/\/www.bing.com\/search?q=`.", + "notes" : [ + + ], + "signature" : "ClipShow:openInBrowserWithRef(refstr)", + "type" : "Method", + "returns" : [ + + ], + "name" : "openInBrowserWithRef", + "parameters" : [ + " * refstr - A optional string specifying which refstr to use. If nil, then open this content in browser directly. The \"refstr\" could be something like this: `https:\/\/www.bing.com\/search?q=`." + ] + }, + { + "desc" : "Open local file with specific command.", + "def" : "ClipShow:openWithCommand(command)", + "stripped_doc" : [ + "Open local file with specific command.", + "" + ], + "doc" : "Open local file with specific command.\n\nParameters:\n * command - A string specifying which command to use. The \"command\" is something like this: `\/usr\/local\/bin\/mvim`.", + "notes" : [ + + ], + "signature" : "ClipShow:openWithCommand(command)", + "type" : "Method", + "returns" : [ + + ], + "name" : "openWithCommand", + "parameters" : [ + " * command - A string specifying which command to use. The \"command\" is something like this: `\/usr\/local\/bin\/mvim`." + ] + }, + { + "desc" : "Restore the lastsession of system clipboard", + "def" : "ClipShow:restoreLastSession()", + "stripped_doc" : [ + "Restore the lastsession of system clipboard" + ], + "doc" : "Restore the lastsession of system clipboard", + "notes" : [ + + ], + "signature" : "ClipShow:restoreLastSession()", + "type" : "Method", + "returns" : [ + + ], + "name" : "restoreLastSession", + "parameters" : [ + + ] + }, + { + "desc" : "Save content of current canvas to a file, the default location is `~\/Desktop\/`.", + "def" : "ClipShow:saveToFile()", + "stripped_doc" : [ + "Save content of current canvas to a file, the default location is `~\/Desktop\/`." + ], + "doc" : "Save content of current canvas to a file, the default location is `~\/Desktop\/`.", + "notes" : [ + + ], + "signature" : "ClipShow:saveToFile()", + "type" : "Method", + "returns" : [ + + ], + "name" : "saveToFile", + "parameters" : [ + + ] + }, + { + "desc" : "Save clipboard session so we can restore it later", + "def" : "ClipShow:saveToSession()", + "stripped_doc" : [ + "Save clipboard session so we can restore it later" + ], + "doc" : "Save clipboard session so we can restore it later", + "notes" : [ + + ], + "signature" : "ClipShow:saveToSession()", + "type" : "Method", + "returns" : [ + + ], + "name" : "saveToSession", + "parameters" : [ + + ] + }, + { + "desc" : "Process the content of system clipboard and show\/hide the canvas", + "def" : "ClipShow:toggleShow()", + "stripped_doc" : [ + "Process the content of system clipboard and show\/hide the canvas" + ], + "doc" : "Process the content of system clipboard and show\/hide the canvas", + "notes" : [ + + ], + "signature" : "ClipShow:toggleShow()", + "type" : "Method", + "returns" : [ + + ], + "name" : "toggleShow", + "parameters" : [ + + ] + } + ], + "name" : "ClipShow" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/ClipShow.spoon/init.lua b/hammerspoon/Spoons/ClipShow.spoon/init.lua new file mode 100644 index 0000000..f36532a --- /dev/null +++ b/hammerspoon/Spoons/ClipShow.spoon/init.lua @@ -0,0 +1,394 @@ +--- === ClipShow === +--- +--- Show the content of system clipboard +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ClipShow.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ClipShow.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "ClipShow" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +obj.canvas = nil +obj.ccount = nil +obj.lastsession = nil + + +function obj:init() + obj.canvas = hs.canvas.new({x=0, y=0, w=0, h=0}) + obj.canvas[1] = { + type = "rectangle", + action = "fill", + fillColor = {hex="#000000", alpha=0.75} + } + obj.canvas[2] = { + type = "segments", + strokeColor = {hex = "#FFFFFF", alpha = 0.1}, + coordinates = { + {x="1%", y="72%"}, + {x="72%", y="72%"} + } + } + obj.canvas[3] = { + type = "segments", + strokeColor = {hex = "#FFFFFF", alpha = 0.1}, + coordinates = { + {x="72%", y="1%"}, + {x="72%", y="99%"} + } + } + obj.canvas[4] = {type = "text", text = ""} + obj.canvas[5] = {type = "text"} + obj.canvas:level(hs.canvas.windowLevels.tornOffMenu) +end + +local function isFileKinds(val, tbl) + for _, v in ipairs(tbl) do + if val == v then + return true + end + end + return false +end + +-- Resize the ClipShow canvas +function obj:adjustCanvas() + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + obj.canvas:frame({ + x = cres.x+cres.w*0.15/2, + y = cres.y+cres.h*0.25/2, + w = cres.w*0.85, + h = cres.h*0.75 + }) +end + +-- Fill clipshowM's keybindings in sidebar +function obj:fillModalKeys() + if #obj.canvas < 6 then + local modal = spoon.ModalMgr.modal_list["clipshowM"] + if modal then + local keys_pool = {} + for _, v in ipairs(modal.keys) do + table.insert(keys_pool, v.msg) + end + for idx, val in ipairs(keys_pool) do + obj.canvas[idx + 5] = { + type = "text", + text = val, + textFont = "Courier-Bold", + textSize = 16, + textColor = {hex = "#2390FF", alpha = 1}, + frame = { + x = "74%", + y = tostring(idx * 30 / (obj.canvas:frame().h - 60)), + w = "24%", + h = tostring(30 / (obj.canvas:frame().h - 60)) + } + } + end + end + end +end + +--- ClipShow:toggleShow() +--- Method +--- Process the content of system clipboard and show/hide the canvas +--- + +function obj:processClipboard() + local clip_type = hs.pasteboard.typesAvailable() + if clip_type.image then + if clip_type.URL then + local urltbl = hs.pasteboard.readURL() + if urltbl.filePath then + -- local file + local fex = urltbl.filePath:match(".*%.(%w+)$") or "" + local image_ex = {"jpeg", "jpg", "gif", "png", "bmp", "tiff", "icns"} + local text_ex = {"", "txt", "md", "markdown", "mkd", "rst", "org", "sh", "zsh", "json", "yml", "mk", "config", "conf", "pub", "gitignore"} + if isFileKinds(fex:lower(), image_ex) then + local imagedata = hs.image.imageFromPath(urltbl.filePath) + obj.canvas[4] = { + type = "image", + image = imagedata, + frame = { + x = "1%", + y = "1%", + w = "70%", + h = "70%" + } + } + elseif isFileKinds(fex:lower(), text_ex) then + -- text file format + local file_handler = io.open(urltbl.filePath) + if file_handler then + local file_content = file_handler:read(1000) + if file_content then + obj.canvas[4] = { + type = "text", + text = file_content, + textSize = 20, + frame = { + x = "1%", + y = "1%", + w = "70%", + h = "70%" + } + } + file_handler:close() + else + -- Maybe directory + local dir_name = urltbl.filePath + obj.canvas[4] = { + type = "text", + text = dir_name, + frame = { + x = "1%", + y = "1%", + w = "70%", + h = "70%" + } + } + end + else + print("-- ClipShow: No access to this file!") + end + else + local stringdata = table.concat(hs.pasteboard.readString(nil, true)) + obj.canvas[4] = { + type = "text", + text = stringdata, + frame = { + x = "1%", + y = "1%", + w = "70%", + h = "70%" + } + } + end + else + -- Remote image + local imagedata = hs.pasteboard.readImage() + obj.canvas[4] = { + type = "image", + image = imagedata, + frame = { + x = "1%", + y = "1%", + w = "70%", + h = "70%" + } + } + end + else + -- Image fragement + local imagedata = hs.pasteboard.readImage() + obj.canvas[4] = { + type = "image", + image = imagedata, + frame = { + x = "1%", + y = "1%", + w = "70%", + h = "70%" + } + } + end + obj:adjustCanvas() + obj:fillModalKeys() + obj.canvas:show() + elseif clip_type.URL then + local urltbl = hs.pasteboard.readURL(nil, true) + if urltbl then + if #urltbl > 1 then + local stringdata = table.concat(hs.pasteboard.readString(nil, true)) + obj.canvas[4] = { + type = "text", + text = stringdata, + frame = { + x = "1%", + y = "1%", + w = "70%", + h = "70%" + } + } + obj:adjustCanvas() + obj:fillModalKeys() + obj.canvas:show() + else + local browser = hs.urlevent.getDefaultHandler("http") + hs.urlevent.openURLWithBundle(urltbl[1].url, browser) + end + else + local browser = hs.urlevent.getDefaultHandler("http") + hs.urlevent.openURLWithBundle(urltbl[1].url, browser) + end + elseif clip_type.string then + local stringdata = table.concat(hs.pasteboard.readString(nil, true)) + obj.canvas[4] = { + type = "text", + text = stringdata, + frame = { + x = "1%", + y = "1%", + w = "70%", + h = "70%" + } + } + obj:adjustCanvas() + obj:fillModalKeys() + obj.canvas:show() + end +end + +function obj:toggleShow() + if obj.canvas:isShowing() then + obj.canvas:hide() + else + local change_count = hs.pasteboard.changeCount() + -- Only if content of the clipboard changed then we redraw the canvas + if change_count ~= obj.ccount then + obj:processClipboard() + obj.ccount = change_count + else + obj:adjustCanvas() + obj.canvas:show() + end + end +end + +--- ClipShow:openInBrowserWithRef(refstr) +--- Method +--- Open content of the clipboard in default browser with specific refstr. +--- +--- Parameters: +--- * refstr - A optional string specifying which refstr to use. If nil, then open this content in browser directly. The "refstr" could be something like this: `https://www.bing.com/search?q=`. + +local function acquireText() + if obj.canvas:isShowing() then + if obj.canvas[4].type == "text" then + return obj.canvas[4].text + else + return hs.pasteboard.readString() or "" + end + else + return hs.pasteboard.readString() or "" + end +end + +function obj:openInBrowserWithRef(refstr) + local querystr = acquireText() + if refstr then + local encoded_query = hs.http.encodeForQuery(refstr .. querystr) + local defaultbrowser = hs.urlevent.getDefaultHandler("http") + hs.urlevent.openURLWithBundle(encoded_query, defaultbrowser) + else + local defaultbrowser = hs.urlevent.getDefaultHandler("http") + hs.urlevent.openURLWithBundle(querystr, defaultbrowser) + end +end + +--- ClipShow:saveToSession() +--- Method +--- Save clipboard session so we can restore it later +--- + +function obj:saveToSession() + obj.lastsession = hs.pasteboard.readAllData() + if obj.canvas:isShowing() then + if obj.canvas[4].type == "text" then + local cdraw = obj.canvas[4].text + hs.pasteboard.writeObjects(cdraw) + obj.canvas[5] = { + type = "text", + text = cdraw, + textSize = 16, + textAlignment = "center", + frame = { + x = "1%", + y = "73%", + w = "70%", + h = "26%" + } + } + elseif obj.canvas[4].type == "image" then + local cdraw = obj.canvas[4].image + hs.pasteboard.writeObjects(cdraw) + obj.canvas[5] = { + type = "image", + image = cdraw, + frame = { + x = "1%", + y = "73%", + w = "70%", + h = "26%" + } + } + end + end +end + +--- ClipShow:restoreLastSession() +--- Method +--- Restore the lastsession of system clipboard +--- + +function obj:restoreLastSession() + if obj.lastsession then + hs.pasteboard.writeAllData(obj.lastsession) + obj:processClipboard() + obj.ccount = change_count + end +end + +--- ClipShow:saveToFile() +--- Method +--- Save content of current canvas to a file, the default location is `~/Desktop/`. +--- + +function obj:saveToFile() + if obj.canvas:isShowing() then + if obj.canvas[4].type == "image" then + local cdraw = obj.canvas[4].image + local file_name = os.date("%Y-%m-%d %H:%M:%S") + cdraw:saveToFile("~/Desktop/" .. file_name .. ".png", true) + elseif obj.canvas[4].type == "text" then + local ctext = obj.canvas[4].text + local file_name = os.date("%Y-%m-%d %H:%M:%S") + local full_path = os.getenv("HOME") .. "/Desktop/" .. file_name .. ".txt" + local file_handler = io.open(full_path, "w") + file_handler:write(ctext) + file_handler:close() + end + end +end + +--- ClipShow:openWithCommand(command) +--- Method +--- Open local file with specific command. +--- +--- Parameters: +--- * command - A string specifying which command to use. The "command" is something like this: `/usr/local/bin/mvim`. + +function obj:openWithCommand(command) + if obj.canvas:isShowing() then + if obj.canvas[4].type == "text" then + local urltbl = hs.pasteboard.readURL() + if urltbl then + if urltbl.filePath then + os.execute(command .. " " .. urltbl.filePath) + else + os.execute(command) + end + else + os.execute(command) + end + end + end +end +return obj diff --git a/hammerspoon/Spoons/CountDown.spoon/docs.json b/hammerspoon/Spoons/CountDown.spoon/docs.json new file mode 100644 index 0000000..c5687f4 --- /dev/null +++ b/hammerspoon/Spoons/CountDown.spoon/docs.json @@ -0,0 +1,163 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "type" : "Module", + "desc" : "Tiny countdown with visual indicator", + "Constructor" : [ + + ], + "Field" : [ + + ], + "Method" : [ + { + "parameters" : [ + " * minutes - How many minutes" + ], + "stripped_doc" : [ + "Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance.", + "" + ], + "desc" : "Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance.", + "doc" : "Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance.\n\nParameters:\n * minutes - How many minutes", + "notes" : [ + + ], + "signature" : "CountDown:startFor(minutes)", + "type" : "Method", + "returns" : [ + + ], + "name" : "startFor", + "def" : "CountDown:startFor(minutes)" + }, + { + "parameters" : [ + + ], + "stripped_doc" : [ + "Pause or resume the existing countdown." + ], + "desc" : "Pause or resume the existing countdown.", + "doc" : "Pause or resume the existing countdown.", + "notes" : [ + + ], + "signature" : "CountDown:pauseOrResume()", + "type" : "Method", + "returns" : [ + + ], + "name" : "pauseOrResume", + "def" : "CountDown:pauseOrResume()" + }, + { + "parameters" : [ + " * progress - an number specifying the value of progress (0.0 - 1.0)" + ], + "stripped_doc" : [ + "Set the progress of visual indicator to `progress`.", + "" + ], + "desc" : "Set the progress of visual indicator to `progress`.", + "doc" : "Set the progress of visual indicator to `progress`.\n\nParameters:\n * progress - an number specifying the value of progress (0.0 - 1.0)", + "notes" : [ + + ], + "signature" : "CountDown:setProgress(progress)", + "type" : "Method", + "returns" : [ + + ], + "name" : "setProgress", + "def" : "CountDown:setProgress(progress)" + } + ], + "Command" : [ + + ], + "items" : [ + { + "parameters" : [ + + ], + "stripped_doc" : [ + "Pause or resume the existing countdown." + ], + "desc" : "Pause or resume the existing countdown.", + "doc" : "Pause or resume the existing countdown.", + "notes" : [ + + ], + "signature" : "CountDown:pauseOrResume()", + "type" : "Method", + "returns" : [ + + ], + "name" : "pauseOrResume", + "def" : "CountDown:pauseOrResume()" + }, + { + "parameters" : [ + " * progress - an number specifying the value of progress (0.0 - 1.0)" + ], + "stripped_doc" : [ + "Set the progress of visual indicator to `progress`.", + "" + ], + "desc" : "Set the progress of visual indicator to `progress`.", + "doc" : "Set the progress of visual indicator to `progress`.\n\nParameters:\n * progress - an number specifying the value of progress (0.0 - 1.0)", + "notes" : [ + + ], + "signature" : "CountDown:setProgress(progress)", + "type" : "Method", + "returns" : [ + + ], + "name" : "setProgress", + "def" : "CountDown:setProgress(progress)" + }, + { + "parameters" : [ + " * minutes - How many minutes" + ], + "stripped_doc" : [ + "Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance.", + "" + ], + "desc" : "Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance.", + "doc" : "Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance.\n\nParameters:\n * minutes - How many minutes", + "notes" : [ + + ], + "signature" : "CountDown:startFor(minutes)", + "type" : "Method", + "returns" : [ + + ], + "name" : "startFor", + "def" : "CountDown:startFor(minutes)" + } + ], + "doc" : "Tiny countdown with visual indicator\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/CountDown.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/CountDown.spoon.zip)", + "name" : "CountDown" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/CountDown.spoon/init.lua b/hammerspoon/Spoons/CountDown.spoon/init.lua new file mode 100644 index 0000000..8b7e983 --- /dev/null +++ b/hammerspoon/Spoons/CountDown.spoon/init.lua @@ -0,0 +1,129 @@ +--- === CountDown === +--- +--- Tiny countdown with visual indicator +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/CountDown.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/CountDown.spoon.zip) + +local obj = {} +obj.__index = obj + +-- Metadata +obj.name = "CountDown" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +obj.canvas = nil +obj.timer = nil + +function obj:init() + self.canvas = hs.canvas.new({x=0, y=0, w=0, h=0}):show() + self.canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) + self.canvas:level(hs.canvas.windowLevels.status) + self.canvas:alpha(0.35) + self.canvas[1] = { + type = "rectangle", + action = "fill", + fillColor = hs.drawing.color.osx_red, + frame = {x="0%", y="0%", w="0%", h="100%"} + } + self.canvas[2] = { + type = "rectangle", + action = "fill", + fillColor = hs.drawing.color.osx_green, + frame = {x="0%", y="0%", w="0%", h="100%"} + } +end + +--- CountDown:startFor(minutes) +--- Method +--- Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance. +--- +--- Parameters: +--- * minutes - How many minutes + +local function canvasCleanup() + if obj.timer then + obj.timer:stop() + obj.timer = nil + end + obj.canvas[1].frame.w = "0%" + obj.canvas[2].frame.x = "0%" + obj.canvas[2].frame.w = "0%" + obj.canvas:frame({x=0, y=0, w=0, h=0}) +end + +function obj:startFor(minutes) + if obj.timer then + canvasCleanup() + else + local mainScreen = hs.screen.mainScreen() + local mainRes = mainScreen:fullFrame() + obj.canvas:frame({x=mainRes.x, y=mainRes.y+mainRes.h-5, w=mainRes.w, h=5}) + -- Set minimum visual step to 2px (i.e. Make sure every trigger updates 2px on screen at least.) + local minimumStep = 2 + local secCount = math.ceil(60*minutes) + obj.loopCount = 0 + if mainRes.w/secCount >= 2 then + obj.timer = hs.timer.doEvery(1, function() + obj.loopCount = obj.loopCount+1/secCount + obj:setProgress(obj.loopCount, minutes) + end) + else + local interval = 2/(mainRes.w/secCount) + obj.timer = hs.timer.doEvery(interval, function() + obj.loopCount = obj.loopCount+1/mainRes.w*2 + obj:setProgress(obj.loopCount, minutes) + end) + end + end + + return self +end + +--- CountDown:pauseOrResume() +--- Method +--- Pause or resume the existing countdown. +--- + +function obj:pauseOrResume() + if obj.timer then + if obj.timer:running() then + obj.timer:stop() + else + obj.timer:start() + end + end +end + +--- CountDown:setProgress(progress) +--- Method +--- Set the progress of visual indicator to `progress`. +--- +--- Parameters: +--- * progress - an number specifying the value of progress (0.0 - 1.0) + +function obj:setProgress(progress, notifystr) + if obj.canvas:frame().h == 0 then + -- Make the canvas actully visible + local mainScreen = hs.screen.mainScreen() + local mainRes = mainScreen:fullFrame() + obj.canvas:frame({x=mainRes.x, y=mainRes.y+mainRes.h-5, w=mainRes.w, h=5}) + end + if progress >= 1 then + canvasCleanup() + if notifystr then + hs.notify.new({ + title = "Time(" .. notifystr .. " mins) is up!", + informativeText = "Now is " .. os.date("%X") + }):send() + end + else + obj.canvas[1].frame.w = tostring(progress) + obj.canvas[2].frame.x = tostring(progress) + obj.canvas[2].frame.w = tostring(1-progress) + end +end + +return obj diff --git a/hammerspoon/Spoons/HCalendar.spoon/docs.json b/hammerspoon/Spoons/HCalendar.spoon/docs.json new file mode 100644 index 0000000..725dfec --- /dev/null +++ b/hammerspoon/Spoons/HCalendar.spoon/docs.json @@ -0,0 +1,41 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "type" : "Module", + "desc" : "A horizonal calendar inset into the desktop", + "Constructor" : [ + + ], + "items" : [ + + ], + "Field" : [ + + ], + "Command" : [ + + ], + "Method" : [ + + ], + "doc" : "A horizonal calendar inset into the desktop\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/HCalendar.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/HCalendar.spoon.zip)", + "name" : "HCalendar" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/HCalendar.spoon/init.lua b/hammerspoon/Spoons/HCalendar.spoon/init.lua new file mode 100644 index 0000000..f98dfce --- /dev/null +++ b/hammerspoon/Spoons/HCalendar.spoon/init.lua @@ -0,0 +1,179 @@ +--- === HCalendar === +--- +--- A horizonal calendar inset into the desktop +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HCalendar.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HCalendar.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "HCalendar" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +obj.hcalw = 31*24+20 +obj.hcalh = 100 + +local function updateHcalCanvas() + local titlestr = os.date("%B %Y") + obj.canvas[3].text = titlestr + local currentyear = os.date("%Y") + local currentmonth = os.date("%m") + local currentday = os.date("%d") + local weeknames = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"} + local firstdayofnextmonth = os.time{year=currentyear, month=currentmonth+1, day=1} + local lastdayofcurrentmonth = os.date("*t", firstdayofnextmonth-24*60*60).day + for i=1,lastdayofcurrentmonth do + local weekdayofqueriedday = os.date("*t", os.time{year=currentyear, month=currentmonth, day=i}).wday + local mappedweekdaystr = weeknames[weekdayofqueriedday] + obj.canvas[3+i].text = mappedweekdaystr + obj.canvas[65+i].text = i + if mappedweekdaystr == "Sa" or mappedweekdaystr == "Su" then + obj.canvas[3+i].textColor = {hex="#FF7878"} + obj.canvas[34+i].fillColor = {hex="#FF7878"} + obj.canvas[65+i].textColor = {hex="#FF7878"} + else + -- Restore the default colors + obj.canvas[3+i].textColor = {hex="#FFFFFF"} + obj.canvas[34+i].fillColor = {hex="#FFFFFF", alpha=0.5} + obj.canvas[65+i].textColor = {hex="#FFFFFF"} + end + if i == math.tointeger(currentday) then + obj.canvas[34+i].fillColor = {hex="#00BAFF", alpha=0.8} + obj.canvas[97].frame.x = tostring((10+24*(i-1))/obj.hcalw) + end + end + -- hide extra day + for i=lastdayofcurrentmonth+1, 31 do + obj.canvas[3+i].text = "" + obj.canvas[34+i].fillColor.alpha = 0 + obj.canvas[65+i].text = "" + end + -- Adjust the size of clipmask to clip the canvas + obj.canvas[2].frame.w = tostring((lastdayofcurrentmonth*24+20)/obj.hcalw) +end + +function obj:init() + local hcalbgcolor = {hex="#000000", alpha=0.3} + local hcaltitlecolor = {hex="#FFFFFF", alpha=0.3} + local todaycolor = {hex="#FFFFFF", alpha=0.2} + local midlinecolor = {hex="#FFFFFF", alpha=0.5} + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + obj.canvas = hs.canvas.new({ + x = 40, + y = cres.h-obj.hcalh-40, + w = obj.hcalw, + h = obj.hcalh, + }):show() + + obj.canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) + obj.canvas:level(hs.canvas.windowLevels.desktopIcon) + + -- Use one pseudo element to clip the canvas + obj.canvas[1] = { + type = "rectangle", + action = "clip", + roundedRectRadii = {xRadius = 10, yRadius = 10}, + } + + -- *NOW* we can draw our actual "visible" parts + obj.canvas[2] = { + id = "hcal_bg", + type = "rectangle", + action = "fill", + fillColor = hcalbgcolor, + roundedRectRadii = {xRadius = 10, yRadius = 10}, + } + + obj.canvas[3] = { + id = "hcal_title", + type = "text", + text = "", + textSize = 18, + textColor = hcaltitlecolor, + textAlignment = "left", + frame = { + x = tostring(10/obj.hcalw), + y = tostring(10/obj.hcalh), + w = tostring(1-20/obj.hcalw), + h = "30%" + } + } + + -- upside weekday string + for i=4, 4+30 do + obj.canvas[i] = { + type = "text", + text = "", + textFont = "Courier-Bold", + textSize = 13, + textAlignment = "center", + frame = { + x = tostring((10+24*(i-4))/obj.hcalw), + y = "45%", + w = tostring(24/(obj.hcalw-20)), + h = "23%" + } + } + end + + -- midline rectangle + for i=35, 35+30 do + obj.canvas[i] = { + type = "rectangle", + action = "fill", + fillColor = midlinecolor, + frame = { + x = tostring((10+24*(i-35))/obj.hcalw), + y = "65%", + w = tostring(24/(obj.hcalw-20)), + h = "4%" + } + } + end + + -- downside day string + for i=66, 66+30 do + obj.canvas[i] = { + type = "text", + text = "", + textFont = "Courier-Bold", + textSize = 13, + textAlignment = "center", + frame = { + x = tostring((10+24*(i-66))/obj.hcalw), + y = "70%", + w = tostring(24/(obj.hcalw-20)), + h = "23%" + } + } + end + + -- today cover rectangle + obj.canvas[97] = { + type = "rectangle", + action = "fill", + fillColor = todaycolor, + roundedRectRadii = {xRadius = 3, yRadius = 3}, + frame = { + x = tostring(10/obj.hcalw), + y = "44%", + w = tostring(24/(obj.hcalw-20)), + h = "46%" + } + } + + if obj.timer == nil then + obj.timer = hs.timer.doEvery(1800, function() updateHcalCanvas() end) + obj.timer:setNextTrigger(0) + else + obj.timer:start() + end + +end + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/docs.json b/hammerspoon/Spoons/HSearch.spoon/docs.json new file mode 100644 index 0000000..2c95492 --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/docs.json @@ -0,0 +1,159 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "type" : "Module", + "desc" : "Hammerspoon Search", + "Constructor" : [ + + ], + "Field" : [ + + ], + "Method" : [ + { + "doc" : "Trigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual.", + "stripped_doc" : [ + "Trigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual." + ], + "parameters" : [ + + ], + "def" : "HSearch:switchSource()", + "notes" : [ + + ], + "signature" : "HSearch:switchSource()", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Trigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual.", + "name" : "switchSource" + }, + { + "doc" : "Load new sources from `HSearch.search_path`, the search_path defaults to `~\/.hammerspoon\/private\/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual.", + "stripped_doc" : [ + "Load new sources from `HSearch.search_path`, the search_path defaults to `~\/.hammerspoon\/private\/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual." + ], + "parameters" : [ + + ], + "def" : "HSearch:loadSources()", + "notes" : [ + + ], + "signature" : "HSearch:loadSources()", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Load new sources from `HSearch.search_path`, the search_path defaults to `~\/.hammerspoon\/private\/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual.", + "name" : "loadSources" + }, + { + "doc" : "Toggle the display of HSearch", + "stripped_doc" : [ + "Toggle the display of HSearch" + ], + "parameters" : [ + + ], + "def" : "HSearch:toggleShow()", + "notes" : [ + + ], + "signature" : "HSearch:toggleShow()", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Toggle the display of HSearch", + "name" : "toggleShow" + } + ], + "Command" : [ + + ], + "doc" : "Hammerspoon Search\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/HSearch.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/HSearch.spoon.zip)", + "items" : [ + { + "doc" : "Load new sources from `HSearch.search_path`, the search_path defaults to `~\/.hammerspoon\/private\/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual.", + "stripped_doc" : [ + "Load new sources from `HSearch.search_path`, the search_path defaults to `~\/.hammerspoon\/private\/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual." + ], + "parameters" : [ + + ], + "def" : "HSearch:loadSources()", + "notes" : [ + + ], + "signature" : "HSearch:loadSources()", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Load new sources from `HSearch.search_path`, the search_path defaults to `~\/.hammerspoon\/private\/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual.", + "name" : "loadSources" + }, + { + "doc" : "Trigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual.", + "stripped_doc" : [ + "Trigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual." + ], + "parameters" : [ + + ], + "def" : "HSearch:switchSource()", + "notes" : [ + + ], + "signature" : "HSearch:switchSource()", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Trigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual.", + "name" : "switchSource" + }, + { + "doc" : "Toggle the display of HSearch", + "stripped_doc" : [ + "Toggle the display of HSearch" + ], + "parameters" : [ + + ], + "def" : "HSearch:toggleShow()", + "notes" : [ + + ], + "signature" : "HSearch:toggleShow()", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Toggle the display of HSearch", + "name" : "toggleShow" + } + ], + "name" : "HSearch" + } +] diff --git a/hammerspoon/Spoons/HSearch.spoon/hs_btabs.lua b/hammerspoon/Spoons/HSearch.spoon/hs_btabs.lua new file mode 100644 index 0000000..e5af8a0 --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/hs_btabs.lua @@ -0,0 +1,55 @@ +local obj={} +obj.__index = obj + +obj.name = "browserTabs" +obj.version = "1.0" +obj.author = "ashfinal " + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +-- Define the source's overview. A unique `keyword` key should exist, so this source can be found. +obj.overview = {text="Type t ⇥ to search safari/chrome Tabs.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/tabs.png"), keyword="t"} +-- Define the notice when a long-time request is being executed. It could be `nil`. +obj.notice = {text="Requesting data, please wait a while …"} + +local function browserTabsRequest() + local safari_running = hs.application.applicationsForBundleID("com.apple.Safari") + local chooser_data = {} + if #safari_running > 0 then + 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') + -- Notice `output` key and its `arg`. The built-in output contains `browser`, `safari`, `chrome`, `firefon`, `clipboard`, `keystrokes`. You can define new output type if you like. + if stat then + chooser_data = hs.fnutils.imap(data, function(item) + return {text=item[1], subText=item[2], image=hs.image.imageFromPath(obj.spoonPath .. "/resources/safari.png"), output="safari", arg=item[2]} + end) + end + end + local chrome_running = hs.application.applicationsForBundleID("com.google.Chrome") + if #chrome_running > 0 then + 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') + if stat then + for idx,val in pairs(data) do + -- Usually we want to open chrome tabs in Google Chrome. + table.insert(chooser_data, {text=val[1], subText=val[2], image=hs.image.imageFromPath(obj.spoonPath .. "/resources/chrome.png"), output="chrome", arg=val[2]}) + end + end + end + -- Return specific table as hs.chooser's data, other keys except for `text` could be optional. + return chooser_data +end + +-- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices +obj.init_func = browserTabsRequest +-- Insert a friendly tip at the head so users know what to do next. +obj.description = {text="Browser Tabs Search", subText="Search and select one item to open in corresponding browser.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/tabs.png")} + +-- As the user is typing, the callback function will be called for every keypress. The returned value is a table. +obj.callback = nil + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/hs_datamuse.lua b/hammerspoon/Spoons/HSearch.spoon/hs_datamuse.lua new file mode 100644 index 0000000..ca7f05f --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/hs_datamuse.lua @@ -0,0 +1,73 @@ +local obj={} +obj.__index = obj + +obj.name = "thesaurusDM" +obj.version = "1.0" +obj.author = "ashfinal " + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +-- Define the source's overview. A unique `keyword` key should exist, so this source can be found. +obj.overview = {text="Type s ⇥ to request English Thesaurus.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/thesaurus.png"), keyword="s"} +-- Define the notice when a long-time request is being executed. It could be `nil`. +obj.notice = nil + +local function dmTips() + local chooser_data = { + {text="Datamuse Thesaurus", subText="Type something to get more words like it …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/thesaurus.png")} + } + return chooser_data +end + +-- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices +obj.init_func = dmTips +-- Insert a friendly tip at the head so users know what to do next. +-- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here +obj.description = nil + +-- As the user is typing, the callback function will be called for every keypress. The returned value is a table. + +local function thesaurusRequest(querystr) + local datamuse_baseurl = 'http://api.datamuse.com' + if string.len(querystr) > 0 then + local encoded_query = hs.http.encodeForQuery(querystr) + local query_url = datamuse_baseurl .. '/words?ml=' .. encoded_query .. '&max=20' + + hs.http.asyncGet(query_url, nil, function(status, data) + if status == 200 then + if pcall(function() hs.json.decode(data) end) then + local decoded_data = hs.json.decode(data) + if #decoded_data > 0 then + local chooser_data = hs.fnutils.imap(decoded_data, function(item) + return {text = item.word, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/thesaurus.png"), output="keystrokes", arg=item.word} + end) + -- Because we don't know when asyncGet will return data, we have to refresh hs.chooser choices in this callback. + if spoon.HSearch then + -- Make sure HSearch spoon is running now + spoon.HSearch.chooser:choices(chooser_data) + spoon.HSearch.chooser:refreshChoicesCallback() + end + end + end + end + end) + else + local chooser_data = { + {text="Datamuse Thesaurus", subText="Type something to get more words like it …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/thesaurus.png")} + } + if spoon.HSearch then + spoon.HSearch.chooser:choices(chooser_data) + spoon.HSearch.chooser:refreshChoicesCallback() + end + end +end + +obj.callback = thesaurusRequest + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/hs_emoji.lua b/hammerspoon/Spoons/HSearch.spoon/hs_emoji.lua new file mode 100644 index 0000000..4f465ea --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/hs_emoji.lua @@ -0,0 +1,87 @@ +local obj={} +obj.__index = obj + +obj.name = "MLemoji" +obj.version = "1.0" +obj.author = "ashfinal " + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +-- Define the source's overview. A unique `keyword` key should exist, so this source can be found. +obj.overview = {text="Type e ⇥ to find relevant Emoji.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/emoji.png"), keyword="e"} +-- Define the notice when a long-time request is being executed. It could be `nil`. +obj.notice = nil + +local function emojiTips() + local chooser_data = { + {text="Relevant Emoji", subText="Type something to find relevant emoji from text …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/emoji.png")} + } + return chooser_data +end +-- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices +obj.init_func = emojiTips +-- Insert a friendly tip at the head so users know what to do next. +-- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here +obj.description = nil + +-- As the user is typing, the callback function will be called for every keypress. The returned value is a table. + +-- Some global objects +local emoji_database_path = "/System/Library/Input Methods/CharacterPalette.app/Contents/Resources/CharacterDB.sqlite3" +obj.database = hs.sqlite3.open(emoji_database_path) +obj.canvas = hs.canvas.new({x=0, y=0, w=96, h=96}) + +local function getEmojiDesc(arg) + for w in obj.database:rows("SELECT info FROM unihan_dict WHERE uchr=\'" .. arg .. "\'") do + return w[1] + end +end + +local function emojiRequest(querystr) + local emoji_baseurl = 'https://emoji.getdango.com' + if string.len(querystr) > 0 then + local encoded_query = hs.http.encodeForQuery(querystr) + local query_url = emoji_baseurl .. '/api/emoji?q=' .. encoded_query + + hs.http.asyncGet(query_url, nil, function(status, data) + if status == 200 then + if pcall(function() hs.json.decode(data) end) then + local decoded_data = hs.json.decode(data) + if decoded_data.results and #decoded_data.results > 0 then + local chooser_data = hs.fnutils.imap(decoded_data.results, function(item) + obj.canvas[1] = {type="text", text=item.text, textSize=64, frame={x="15%", y="10%", w="100%", h="100%"}} + local hexcode = string.format("%#X", utf8.codepoint(item.text)) + local emoji_description = getEmojiDesc(item.text) + local formatted_desc = string.gsub(emoji_description, "|||||||||||||||", "") + return {text = formatted_desc, image=obj.canvas:imageFromCanvas(), subText="Hex Code: " .. hexcode, outputType="keystrokes", arg=item.text} + end) + -- Because we don't know when asyncGet will return data, we have to refresh hs.chooser choices in this callback. + if spoon.HSearch then + -- Make sure HSearch spoon is running now + spoon.HSearch.chooser:choices(chooser_data) + spoon.HSearch.chooser:refreshChoicesCallback() + end + end + end + end + end) + else + local chooser_data = { + {text="Relevant Emoji", subText="Type something to find relevant emoji from text …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/emoji.png")} + } + if spoon.HSearch then + spoon.HSearch.chooser:choices(chooser_data) + spoon.HSearch.chooser:refreshChoicesCallback() + end + end +end + +obj.callback = emojiRequest + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/hs_note.lua b/hammerspoon/Spoons/HSearch.spoon/hs_note.lua new file mode 100644 index 0000000..d9ac615 --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/hs_note.lua @@ -0,0 +1,96 @@ +local obj={} +obj.__index = obj + +obj.name = "justNote" +obj.version = "1.0" +obj.author = "ashfinal " + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +-- Define the source's overview. A unique `keyword` key should exist, so this source can be found. +obj.overview = {text="Type n ⇥ to Note something.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/justnote.png"), keyword="n"} +-- Define the notice when a long-time request is being executed. It could be `nil`. +obj.notice = nil +-- Define the hotkeys, which will be enabled/disabled automatically. You need to add your keybindings into this table manually. +obj.hotkeys = {} + +local function justNoteRequest() + local note_history = hs.settings.get("just.another.note") or {} + if #note_history == 0 then + local chooser_data = {{text="Write something and press Enter.", subText="Your notes is automatically saved, selected item will be erased.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/justnote.png")}} + return chooser_data + else + local chooser_data = hs.fnutils.imap(note_history, function(item) + return {uuid=item.uuid, text=item.content, subText=item.ctime, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/justnote.png"), output="noteremove", arg=item.uuid} + end) + return chooser_data + end +end +-- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices +obj.init_func = justNoteRequest +-- Insert a friendly tip at the head so users know what to do next. +obj.description = nil +-- As the user is typing, the callback function will be called for every keypress. The returned value is a table. + +local function isInNoteHistory(value, tbl) + for idx,val in pairs(tbl) do + if val.uuid == value then + return true + end + end + return false +end + +local function justNoteStore() + if spoon.HSearch then + local querystr = string.gsub(spoon.HSearch.chooser:query(), "%s+$", "") + if string.len(querystr) > 0 then + local query_hash = hs.hash.SHA1(querystr) + local note_history = hs.settings.get("just.another.note") or {} + if not isInNoteHistory(query_hash, note_history) then + table.insert(note_history, {uuid=query_hash, ctime="Created at "..os.date(), content=querystr}) + hs.settings.set("just.another.note", note_history) + end + end + end +end + +local store_trigger = hs.hotkey.new("", "return", nil, function() + justNoteStore() + if spoon.HSearch then + local chooser_data = justNoteRequest() + spoon.HSearch.chooser:choices(chooser_data) + spoon.HSearch.chooser:query("") + end +end) +table.insert(obj.hotkeys, store_trigger) + +obj.callback = nil + +-- Define a new output type +local function removeNote(arg) + local note_history = hs.settings.get("just.another.note") or {} + for idx,val in pairs(note_history) do + if val.uuid == arg then + table.remove(note_history, idx) + hs.settings.set("just.another.note", note_history) + end + local chooser_data = justNoteRequest() + if spoon.HSearch then + spoon.HSearch.chooser:choices(chooser_data) + end + end +end +obj.new_output = { + name = "noteremove", + func = removeNote +} + + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/hs_time.lua b/hammerspoon/Spoons/HSearch.spoon/hs_time.lua new file mode 100644 index 0000000..9157e69 --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/hs_time.lua @@ -0,0 +1,87 @@ +local obj={} +obj.__index = obj + +obj.name = "timeDelta" +obj.version = "1.0" +obj.author = "ashfinal " + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +-- Define the source's overview. A unique `keyword` key should exist, so this source can be found. +obj.overview = {text="Type d ⇥ to format/query Date.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/time.png"), keyword="d"} +-- Define the notice when a long-time request is being executed. It could be `nil`. +obj.notice = nil + +-- Some global objects +obj.exec_args = { + '+"%Y-%m-%d"', + '+"%H:%M:%S %p"', + '+"%A, %B %d, %Y"', + '+"%Y-%m-%d %H:%M:%S %p"', + '+"%a, %b %d, %y"', + '+"%m/%d/%y %H:%M %p"', + '', + '-u', +} + +local function timeRequest() + local chooser_data = hs.fnutils.imap(obj.exec_args, function(item) + local exec_result = hs.execute("date " .. item) + return {text=exec_result, subText="date " .. item, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/time.png"), output="keystrokes", arg=exec_result} + end) + return chooser_data +end +-- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices +obj.init_func = timeRequest +-- Insert a friendly tip at the head so users know what to do next. +-- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here +obj.description = {text="Date Query", subText="Type +/-1d (or y, m, w, H, M, S) to query date forward or backward.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/time.png")} + +-- As the user is typing, the callback function will be called for every keypress. The returned value is a table. + +local function splitBySpace(str) + local tmptbl = {} + for w in string.gmatch(str,"[+-]?%d+[ymdwHMS]") do table.insert(tmptbl,w) end + return tmptbl +end + +local function timeDeltaRequest(querystr) + if string.len(querystr) > 0 then + local valid_inputs = splitBySpace(querystr) + if #valid_inputs > 0 then + local addv_before = hs.fnutils.imap(valid_inputs, function(item) + return "-v" .. item + end) + local vv_var = table.concat(addv_before, " ") + local chooser_data = hs.fnutils.imap(obj.exec_args, function(item) + local new_exec_command = "date " .. vv_var .. " " .. item + local new_exec_result = hs.execute(new_exec_command) + return {text=new_exec_result, subText=new_exec_command, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/time.png"), output="keystrokes", arg=new_exec_result} + end) + 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(obj.spoonPath .. "/resources/time.png")} + table.insert(chooser_data, 1, source_desc) + if spoon.HSearch then + -- Make sure HSearch spoon is running now + spoon.HSearch.chooser:choices(chooser_data) + end + end + else + local chooser_data = timeRequest() + 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(obj.spoonPath .. "/resources/time.png")} + table.insert(chooser_data, 1, source_desc) + if spoon.HSearch then + -- Make sure HSearch spoon is running now + spoon.HSearch.chooser:choices(chooser_data) + end + end +end + +obj.callback = timeDeltaRequest + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/hs_v2ex.lua b/hammerspoon/Spoons/HSearch.spoon/hs_v2ex.lua new file mode 100644 index 0000000..65d18ab --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/hs_v2ex.lua @@ -0,0 +1,63 @@ +local obj={} +obj.__index = obj + +obj.name = "v2exPosts" +obj.version = "1.0" +obj.author = "ashfinal " + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +-- Define the source's overview. A unique `keyword` key should exist, so this source can be found. +obj.overview = {text="Type v ⇥ to fetch v2ex posts.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/v2ex.png"), keyword="v"} +-- Define the notice when a long-time request is being executed. It could be `nil`. +obj.notice = {text="Requesting data, please wait a while …"} + +local function v2exRequest() + local query_url = 'https://www.v2ex.com/api/topics/latest.json' + local stat, body = hs.http.asyncGet(query_url, nil, function(status, data) + if status == 200 then + if pcall(function() hs.json.decode(data) end) then + local decoded_data = hs.json.decode(data) + if #decoded_data > 0 then + local chooser_data = hs.fnutils.imap(decoded_data, function(item) + local sub_content = string.gsub(item.content, "\r\n", " ") + local function trim_content() + if utf8.len(sub_content) > 40 then + return string.sub(sub_content, 1, utf8.offset(sub_content, 40)-1) + else + return sub_content + end + end + local final_content = trim_content() + return {text=item.title, subText=final_content, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/v2ex.png"), outputType="browser", arg=item.url} + end) + local source_desc = {text="v2ex Posts", subText="Select some item to get it opened in default browser …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/v2ex.png")} + table.insert(chooser_data, 1, source_desc) + -- Because we don't know when asyncGet will return data, we have to refresh hs.chooser choices in this callback. + if spoon.HSearch then + -- Make sure HSearch spoon is running now + spoon.HSearch.chooser:choices(chooser_data) + spoon.HSearch.chooser:refreshChoicesCallback() + end + end + end + end + end) +end +-- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices +obj.init_func = v2exRequest +-- Insert a friendly tip at the head so users know what to do next. +-- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here +obj.description = nil + +-- As the user is typing, the callback function will be called for every keypress. The returned value is a table. + +obj.callback = nil + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/hs_yddict.lua b/hammerspoon/Spoons/HSearch.spoon/hs_yddict.lua new file mode 100644 index 0000000..35fc0ea --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/hs_yddict.lua @@ -0,0 +1,94 @@ +local obj={} +obj.__index = obj + +obj.name = "youdaoDict" +obj.version = "1.0" +obj.author = "ashfinal " + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +-- Define the source's overview. A unique `keyword` key should exist, so this source can be found. +obj.overview = {text="Type y ⇥ to use Yaodao dictionary.", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/youdao.png"), keyword="y"} +-- Define the notice when a long-time request is being executed. It could be `nil`. +obj.notice = nil + +local function youdaoTips() + local chooser_data = { + {text="Youdao Dictionary", subText="Type something to get it translated …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/youdao.png")} + } + return chooser_data +end + +-- Define the function which will be called when the `keyword` triggers a new source. The returned value is a table. Read more: http://www.hammerspoon.org/docs/hs.chooser.html#choices +obj.init_func = youdaoTips +-- Insert a friendly tip at the head so users know what to do next. +-- As this source highly relys on queryChangedCallback, we'd better tip users in callback instead of here +obj.description = nil + +-- As the user is typing, the callback function will be called for every keypress. The returned value is a table. + +local function basic_extract(arg) + if arg then return arg.explains else return {} end +end +local function web_extract(arg) + if arg then + local value = hs.fnutils.imap(arg, function(item) + return item.key .. table.concat(item.value, ",") + end) + return value + else + return {} + end +end + +local function youdaoInstantTrans(querystr) + local youdao_keyfrom = 'hsearch' + local youdao_apikey = '1199732752' + local youdao_baseurl = 'http://fanyi.youdao.com/openapi.do?keyfrom=' .. youdao_keyfrom .. '&key=' .. youdao_apikey .. '&type=data&doctype=json&version=1.1&q=' + if string.len(querystr) > 0 then + local encoded_query = hs.http.encodeForQuery(querystr) + local query_url = youdao_baseurl .. encoded_query + + hs.http.asyncGet(query_url, nil, function(status, data) + if status == 200 then + if pcall(function() hs.json.decode(data) end) then + local decoded_data = hs.json.decode(data) + if decoded_data.errorCode == 0 then + local basictrans = basic_extract(decoded_data.basic) + local webtrans = web_extract(decoded_data.web) + local dictpool = hs.fnutils.concat(basictrans, webtrans) + if #dictpool > 0 then + local chooser_data = hs.fnutils.imap(dictpool, function(item) + return {text=item, image=hs.image.imageFromPath(obj.spoonPath .. "/resources/youdao.png"), output="clipboard", arg=item} + end) + -- Because we don't know when asyncGet will return data, we have to refresh hs.chooser choices in this callback. + if spoon.HSearch then + -- Make sure HSearch spoon is running now + spoon.HSearch.chooser:choices(chooser_data) + spoon.HSearch.chooser:refreshChoicesCallback() + end + end + end + end + end + end) + else + local chooser_data = { + {text="Youdao Dictionary", subText="Type something to get it translated …", image=hs.image.imageFromPath(obj.spoonPath .. "/resources/youdao.png")} + } + if spoon.HSearch then + spoon.HSearch.chooser:choices(chooser_data) + spoon.HSearch.chooser:refreshChoicesCallback() + end + end +end + +obj.callback = youdaoInstantTrans + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/init.lua b/hammerspoon/Spoons/HSearch.spoon/init.lua new file mode 100644 index 0000000..3f420e7 --- /dev/null +++ b/hammerspoon/Spoons/HSearch.spoon/init.lua @@ -0,0 +1,256 @@ +--- === HSearch === +--- +--- Hammerspoon Search +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HSearch.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HSearch.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "HSearch" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +obj.sources = {} +obj.sources_overview = {} +obj.search_path = {hs.configdir .. "/private/hsearch_dir", obj.spoonPath} +obj.hotkeys = {} +obj.source_kw = nil + +function obj:restoreOutput() + obj.output_pool = {} + -- Define the built-in output type + local function openWithBrowser(arg) + local default_browser = hs.urlevent.getDefaultHandler('http') + hs.urlevent.openURLWithBundle(arg, default_browser) + end + local function openWithSafari(arg) + hs.urlevent.openURLWithBundle(arg, "com.apple.Safari") + end + local function openWithChrome(arg) + hs.urlevent.openURLWithBundle(arg, "com.google.Chrome") + end + local function openWithFirefox(arg) + hs.urlevent.openURLWithBundle(arg, "org.mozilla.firefox") + end + local function copyToClipboard(arg) + hs.pasteboard.setContents(arg) + end + local function sendKeyStrokes(arg) + local cwin = hs.window.orderedWindows()[1] + cwin:focus() + hs.eventtap.keyStrokes(arg) + end + obj.output_pool["browser"] = openWithBrowser + obj.output_pool["safari"] = openWithSafari + obj.output_pool["chrome"] = openWithChrome + obj.output_pool["firefox"] = openWithFirefox + obj.output_pool["clipboard"] = copyToClipboard + obj.output_pool["keystrokes"] = sendKeyStrokes +end + +function obj:init() + obj.chooser = hs.chooser.new(function(chosen) + obj.trigger:disable() + -- Disable all hotkeys + for _,val in pairs(obj.hotkeys) do + for i=1,#val do + val[i]:disable() + end + end + if chosen ~= nil then + if chosen.output then + obj.output_pool[chosen.output](chosen.arg) + end + end + end) + obj.chooser:rows(9) +end + +--- HSearch:switchSource() +--- Method +--- Tigger new source according to hs.chooser's query string and keyword. Only for debug purpose in usual. +--- + +function obj:switchSource() + local querystr = obj.chooser:query() + if string.len(querystr) > 0 then + local matchstr = string.match(querystr, "^%w+") + if matchstr == querystr then + -- First we try to switch source according to the querystr + if obj.sources[querystr] then + obj.source_kw = querystr + obj.chooser:query('') + obj.chooser:choices(nil) + obj.chooser:queryChangedCallback() + obj.sources[querystr]() + else + local row_content = obj.chooser:selectedRowContents() + local row_kw = row_content.keyword + -- Then try to switch source according to selected row + if obj.sources[row_kw] then + obj.source_kw = row_kw + obj.chooser:query('') + obj.chooser:choices(nil) + obj.chooser:queryChangedCallback() + obj.sources[row_kw]() + else + obj.source_kw = nil + local chooser_data = { + {text="No source found!", subText="Maybe misspelled the keyword?"}, + {text="Want to add your own source?", subText="Feel free to read the code and open PRs. :)"} + } + obj.chooser:choices(chooser_data) + obj.chooser:queryChangedCallback() + hs.eventtap.keyStroke({"cmd"}, "a") + end + end + else + obj.source_kw = nil + local chooser_data = { + {text="Invalid Keyword", subText="Trigger keyword must only consist of alphanumeric characters."} + } + obj.chooser:choices(chooser_data) + obj.chooser:queryChangedCallback() + hs.eventtap.keyStroke({"cmd"}, "a") + end + else + local row_content = obj.chooser:selectedRowContents() + local row_kw = row_content.keyword + if obj.sources[row_kw] then + obj.source_kw = row_kw + obj.chooser:query('') + obj.chooser:choices(nil) + obj.chooser:queryChangedCallback() + obj.sources[row_kw]() + else + obj.source_kw = nil + -- If no matching source then show sources overview + local chooser_data = obj.sources_overview + obj.chooser:choices(chooser_data) + obj.chooser:queryChangedCallback() + end + end + if obj.source_kw then + for key,val in pairs(obj.hotkeys) do + if key == obj.source_kw then + for i=1,#val do + val[i]:enable() + end + else + for i=1,#val do + val[i]:disable() + end + end + end + else + for _,val in pairs(obj.hotkeys) do + for i=1,#val do + val[i]:disable() + end + end + end +end + +--- HSearch:loadSources() +--- Method +--- Load new sources from `HSearch.search_path`, the search_path defaults to `~/.hammerspoon/private/hsearch_dir` and the HSearch Spoon directory. Only for debug purpose in usual. +--- + +function obj:loadSources() + obj.sources = {} + obj.sources_overview = {} + obj:restoreOutput() + for _,dir in ipairs(obj.search_path) do + local file_list = io.popen("find " .. dir .. " -type f -name '*.lua'") + for file in file_list:lines() do + -- Exclude self + if file ~= obj.spoonPath .. "/init.lua" then + local f = loadfile(file) + if f then + local source = f() + local output = source.new_output + if output then obj.output_pool[output.name] = output.func end + local overview = source.overview + -- Gather souces overview from files + table.insert(obj.sources_overview, overview) + local hotkey = source.hotkeys + if hotkey then obj.hotkeys[overview.keyword] = hotkey end + local function sourceFunc() + local notice = source.notice + if notice then obj.chooser:choices({notice}) end + local request = source.init_func + if request then + local chooser_data = request() + if chooser_data then + local desc = source.description + if desc then table.insert(chooser_data, 1, desc) end + end + obj.chooser:choices(chooser_data) + else + obj.chooser:choices(nil) + end + if source.callback then + obj.chooser:queryChangedCallback(source.callback) + else + obj.chooser:queryChangedCallback() + end + obj.chooser:searchSubText(true) + end + -- Add this source to sources pool, so it can found and triggered. + obj.sources[overview.keyword] = sourceFunc + end + end + end + end +end + +--- HSearch:toggleShow() +--- Method +--- Toggle the display of HSearch +--- + +function obj:toggleShow() + if #obj.sources_overview == 0 then + -- If it's the first time HSearch shows itself, then load all sources from files + obj:loadSources() + -- Show sources overview, so users know what to do next. + obj.chooser:choices(obj.sources_overview) + end + if obj.chooser:isVisible() then + obj.chooser:hide() + obj.trigger:disable() + for _,val in pairs(obj.hotkeys) do + for i=1,#val do + val[i]:disable() + end + end + else + if obj.trigger == nil then + obj.trigger = hs.hotkey.bind("", "tab", nil, function() obj:switchSource() end) + else + obj.trigger:enable() + end + for key,val in pairs(obj.hotkeys) do + if key == obj.source_kw then + for i=1,#val do + val[i]:enable() + end + end + end + obj.chooser:show() + end +end + +return obj diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/chrome.png b/hammerspoon/Spoons/HSearch.spoon/resources/chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..08e2cc4118d759f9c6058fbafc99a1f73d389c7d GIT binary patch literal 7920 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 literal 0 HcmV?d00001 diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/justnote.png b/hammerspoon/Spoons/HSearch.spoon/resources/justnote.png new file mode 100644 index 0000000000000000000000000000000000000000..1bd87e0b21b5a6d36366c350b8305a594ce4c823 GIT binary patch literal 31419 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 z z41PTSjOX=rW_JmRLhSUxe%Jyc$<1)C1#Cg(1v`E9kRh%Eqbv-1r^n=|artp_iGWmm z=mY-+WThJh+V5|R56c6p&ZT_)Rm7wRtj7dp%gNscd8xVml{8HWWw*?pid&QB3vPhqGcLP=&li}^Do}hrd zQ*b_-=+p%&+TFG9K}VqfVMA!{8(@H5Y_J!6qpv%`5%PW0@n!{|>p86&lMf|@ZkgkK z&x*mtG>bZN9eW{13!mF1xTA{SvYvjM6$!2X@gS^6=7v^gS|bId5qk%Dx${Ql3D;>N)gTFb=n+D;ClfkYY@>-W|r99gO@LH=1ORU`AWZ}NEUiq4_6uZj!Eys zCT(^g=!-{)Da{!n_K2xTB}|gUkgNRO>O;gF^A@cXH%vZhw_2aqz9=m;p)9wL2|2Vb zg2>lc;Eqk1Zgl?`LHBf2sDOYk&>?6Bu11pKJg|3=gFk*<1fu^UC^`{5sQi9J{l2dE zJ0PDEVrPRmbiG*kb=&KvQuGyf7#P@>GW`TPY9hZeUs$FZMWy7>f44xIcd?rKFOW2r z2DOpHWnt`kGOT@31u_5FU{do~fqrHr(Wv15sFs^^F`Ya*y6|e+8(YaXMJy(2Ev9jd zCWhmNl5A1A#=;bnrjz7-V-uQAKSCI;FIaWu4RefZ1>?5W1xWzt)h={6?P!)z4_k|Q z!x0u7q%Mc0j5x3O8V_Sue623lIPj{yJ5s#pcrf(JR3lxN?8}#_cY)|rC2xom^0NU4 zX*RU!Hnp}otNRA=FQEDxYHq!Tghki^TGk9mpp`}A)j(Vk9ube~e^1l*4AU_B8krpZ z<6j8zh#Y|fBBRnO7OtiBq_h+RH|G7$&YDgC*&-HU1QH?>A!Hlpcn3R+zygO_uKt){?Ibp9q!|*v zkzS_dihTIJ=mOsR=bSWBDY!oMf}7pk)H!|8sEP0t73{Tjg3kge!5I`WDC0`I23SDJ zJeaOe)Q^t*PF_AQQfX4FIP~J><0=3Bo*uD@Tg-KGOpYD+rbjol|6+t@bmwlG zIJL_ssAti)eV7LR49&BysXuIyJ;0Gle6H|YPeDq|*~v1JB=0LZa&iQn;`rKlE4v0a zAz#M-*n*1KrtF%gJ6qJNsPq62YpA^XITgDl(7Z~6y+=;H=@l2fhu-)Snu zF4ngMCqG5HYsKV!rrj>3(vnF02Wej>LSUDJwYmd3HvTRI_mh-${%U~EwM-z@Ay4ft zH}=OIc1e?)jzvCQquTj4^g6jF7L5#ja=a$#!ReUJ+v793d+y`w9V_QUh%(_>q=Yk* z@Y4U39I|F!Gae7Y(m2rbGXnVv1yb|`F9m8?9zLb+%7!wg-e^H$ICU8jq4OHwQ6L9~ zCZavy7Nqp-;pVDh}Tbcg2X{MN+>dk4`=oyEd6TBm|u zLB(rHaQ%!SIq$>Xfz6<3`_#=ZQ0g2g#P7{)jecr%^7bamUqBt6!S+CkIm4SnfPuBq zayCIUh7203Pm9 zSgF6A%Fs|~WV|>1Lp3++iI}+XYaWzgovat1FC8tWj`5#hgmc;J#rg`~c(F7Mpm05g zKGFvUOrwmscSAJ`ZOgSSth9?9#k{vbIg9?^gFhuG<@^CEIZt3_ z)G5LPtvVwQBU;Gc?#I$G?4tptFIIUtloI4O$ig}#yBZqwobMyUr8g@Vg$vd_6x$sc z6t!@*#f6CWSZ1FwFSfBR7jL1!cF03StJV%4Sk$??I}9;x3mQKLJLzNu0(pQ zEcQ&|9eU6ulxS?wkqJ*DoDJ*x<(1iL(xklHQ&zKX7x<6G)_{L*vzt{Ou(kyn8>bOZ3(S+7}{>a{xazV!)h zE{LQ9-uvcRw^ConU)b6Nn{#~i8JQl&mWo)_EVg<1eP>^rVLzJamt4g|?acRc{M{?$ zmL8eAeIu+cKv^_1r8^iH*6;sg0g#ILX~|t=`&C2#uwDe-#1z$1@xNnd_$IYRd4`6y zxzrnXJ_g3>y2#F4ZVqerpHcQJnS!8bnG2>35;%9w8J()63D=-UU*PKM7iV zwC;3(I?!czMNV^{V^MF!s8f?Ip`eIVSH6{cj0zKNw1*2b-?>B>UqpnQA$!KQAv!-) zWB&@P6#BT*V})N77L$%z4}DiPhvOChxd`1_W}DyPy0N8>%Y2E@mod6nJI4`&j(ys3 z#UE-(^>wMlN*CAYF6x(i|BZld3aOk}rjf?-eby;q@8%R!+C#tKj+h;E7<|gysvLzf zTa4~wj7cJKs#I;D+YrF%#CE^6l*#1{x~n=|i2z~A zsWxwbxRZ0ECJU5+rT%+HcEWdKR%fW_f`kWe?FrSIzanDbMN*!v9bAbGw@WD;vmR@6 z{mX1*O>xxM=~cVI;-k8x_;hV>n~Sj)chazba5-^@?7xVkEh5s{;0J}>+n=!`6Iw_N zTP?IUkExL+P@^y*9NZ^nG=ibH$Nim&fN?E{!fFcBT2K@~mL_7 zwC0dqT&{%iP?Ery{m;&AdZQ0xZ&v9Q%)0^q`%bFcDPEh|Y1|-rKHEcdB0B_Ph1|6neIf#p*EiKQ|mZuCHJRK<41d$v)0DPzDkqvi?Emud(evS zC#Fnp0`J1xD)AXcmx~P|erGs;C0;o;r2~~fivXx z1NjqAQq67a#8K1LO&th*;_4&Pk>)?_9o%<6#8{I!=FxO2Ma{J2q#r^bLS&8$2%S8m zpuFs~Mz*Zzld*gDocub8o&-5;bcT)nftUVgra}K#5&nNBNc`@0AD#WH2Fvx=hUPna z{`W>m84>|>y<*qKq^HzV90_p!dkczqN~i8;Sg7Z{00@;#?T)?yw;8YB!0n!w2$nl9 zs}0ht7Pn?od(qx!eBQu?WMk&HLhZA9@}HXTDp7Vx!?}*aMZWRxgX!5MKinzG;RVpm z{A$D$^=GQsH^fJLx-Q&=Z} z=zVU8HUJRXNgZ&ANSeOu(ghOHPVNOWO0zICCR$sO*JWYA>_Ryp<`H+~ZmCV820#Cc zGFBt8F25LpRjypV?&Gsud(uW`j4#L*1%8$Ar(eqtIu#Spe+ z3J+kmCsl~1sF|%Bryj-@Iv=Ppz1z0eX{!WH8< zA79%fc8%=A2LVI{(nUL4c?~>#ut8XldYlJ@P?A!$=FsU06Z{M#xfNaukl8w{bxM%r zSa(tTt$shAUzGX$e)xUZOIa0dK<<`Mul2k#bequOuYHVVAV$4sg&sy5m-x~XdymU_ zoZ2vWz*)NelC>ojzFpc*!O91Ge9b_mWTx_;vl>gYh&+?DL@R%-=p`1 ze266L=mdlv7?c@=z55JzaIy(iZ1cLsn%yW;vp%tTxR?-0f?S-J^LOhtqWHWYo4@tM z<*@ns84!Xuv>@(2{TU(g`VZ_5pEHld_K~5~@#ujl)TUxtLf_IqeXq65u*cjKUQAMq z5>E75?3pJDvD!LPkyUio;VW--M~@W%Z0l$|)aBFb6^&Vi86?5L?h@mRvG7(D^!VFn>ZHYw`cQ{L~I}bAK4 zK9%~V#}%?4JKT+gr*QVunA9&U$J|E`k z4{@iG!|{f@Obk^MW9>lQ?iex>d0gL9_?W20Q#X8fKS6AdEI+CmAEUhL*Ah5-z_N8N z4~s=ySeVAEn*^M`eiTCxBx9Y`z@@;rbX^|rp*+cYA_QPdOE3MHj&SlqqyHykSt+E& zgJFPxt{;ti=Pz)l8-|xPe^UD9K5tHH%aszyUheK;{O(7eTi&9Txm*m-?+52!YIDy{ z>uql^HE8RTOc40c=OwzD7=x9*I5ER=CJt7spt`DTe(XWgoUHEm4>|T)NypN=^q#ZwyP*C&>UDQ+%xbeVYTi=<(+x)A?CtzFC9WbPCV)b>;dC?f%wKbefOt& zc{yQ~VUtq-pqU`J91V0T91W~@+WCK+I8^0Amnw<*`mqz9v~sUh&fQ(A37WL2#`O`u zA&FAL=;R7Uj1_vJRu|sR_8`QTPg_J1V~XvDN8dwqqFGDe*Bg^Y6kz|$vcvBT+o>pu z@BgQ=KyB_c#C=UUy-Tg7LgvIA26R?_msF9@vNXVxvbiU9d59^FW_m}zG?JLKX~{do zfyQzTe`+mOT)NfU5L+<4l*0y^AOexwHwZ85K_7J~^AF#OtW6@ru>sMe*0rQ*=fUiP ziOP%G9h2Fn-`8)?zfaG*xJ8D;S~6&Ac!?9mS0=$?8$MLEnTMCW_2d)Xc;2yiMDk2e)Y3iJ2VvgJ}S=C z7aV$!svVX{!aKumfLqO~UmnzocG)jO%Rk9vv4p;8kh$LY%v(OOKU`fr83P0~D(2jO z>7Tk`y0M)>KftODS;tr9@`bg6HS`DU`g8==jc}ploa<0iZa#5De`#|OS^vn=in!cl zJ^on6eqeTeGBn+{tX}9Pr5C76*&letIC>=OjagFbiBX7w7=`%j0Yq|F0ne{xg04wL zbHjb{cL5FtDm0h4P#iqDzU5f_tnzClVe25PEGw&iW_kTyrznVR59qg}pxdeT^B~)1 zcxGCrkjKlJ`>feMmT43JztTQw!z~<7^OrwfkudMf?c=TQfFg8eIbrX6eXkvQ-vp`X z_vTWwQ*>Y`a!~mw)bVZVraeeSq??D_FBwKmdCj{plKLyO1?dn`^bI;0qXhp(f5Q~-34UV#t>69{>&2R zoq*k_JZB|DAS%rJnZWpU*49n);sX!zLB(Q*o7PB~NOXuw{zaCJ3Nf@NfEDyVe}}){ zbf+GlmxKLi(SF1Q*H3Ol=||T3xVNNz@6B%a-M46#=hxl)aLD95 zu!n>$ItY`BhogZ>!29~_!i`iK>kA||$>1Hdv?@^2+-Q)lTzbalnK(?>#{#W4{hN~n zQvq~-AnqC7e7UYd<-3R`6xUKY6CLpag3{|9A}8w^)Nd742vxQje9vn+4*+2kz8;6z z1X*Q*IsxL(n74cm%YC`z@(`+*)$%9%^M=kS_!ct>2UqE{sTb`D1~ZL0k0uQC`riiq z;bIK3UoA0aYhe0?h=ANu!n>5t^;V?1{j+$;$ZSOyJ}-Jfgey0>`R}Lo`A*sm^aC9? z=iVhO^fOq7FXYDN+do*-;>RlG1g|DyG8__I`?GL;ueK!#HjN^Q%&N2AHV{XQR=Qd4 zD#U-cfYS^$Ra`ke>`o{#=1P76{x!4WEIB`+p0+Yk!e?Jq8Axj4CtZx0f-&}KNH4z! zRkS;!bHxl=C=6PA?o?A7o-dC@Rmxj%SQ=(W_dn6^!vgf~8E&ejK<;uWz=^bClg}Tb zUcaHt(|h@nk^ud`=Fnz>y3ivYE+nQgI5z%zy^HW`uAaDz z!g(2lG|NWE^=C#98wgHDn>w@?anuWOs=t|E$eaRN{;Kve48#7x;=c|t^Kea;FT5l#cvM_(S?N6lgw_YV`8A@#ca2K+i zYsT$DT@OpKB^)+#N8A?Cp23@%Fy$!o)baRYB`@(uwwXh=bx1w1>hnmW*{=?Tb z(uzdMyBLlacB~lvl}2yNiTu;`hu_D6V$2r34dzm> zL|#vvJyDEjSH2ddF)d4{55D@WUA#m5;VYC3#?Qe3bNnUHQuC%7g^|ydBxqTXj4QPc zls_C@^w#xu2rJB0f#JB>3j+$gMnbNA^HyF-wz~HKb z>(v4K!-9JDMrPuWAC{Mm5r<6TNx!U<*nHdjnXHZKHXo|R;XuHTOvyitB-G>}vmOJ0 z9mA?mnG@>K%rwYY@E{2wg8kvV@~H;1W$)hz#8o{9`QnK{vhXu0yzn&zMVrh;eiL$h zIGr~G;nIgEN`tIC!s{|gmeDk(N7UQF;2E+H`hB&cd~Dj>#G=aK!?-IIZyzTck;y>&vs$=c9X(v2%_5tOhNd zca|2qvpG(c{X5fM8^aC^{ms7C$l!Kih2$ zYxpt~d5wC~X!)y*FI9{Hg(}|v;zL0H5Ye1Sex8jI z(TZe`&LOd|?Ip1k{Q>@ErrC<`O(aicdKt0QUqdA2(3tm7EPpRm>O!M7<*r8DF7bpQ zT)pGna$u3wDKmVr7D={hN~^_%td9niPr4Z(Xu$J~Z-s7pFWr%Hbx_^V-21p$`Vwcm z%&siYtzt|)xi$ZttFgy8psn85*66)>SVUp>6_8Q=45$F@WjJ%}Xt1F0F{gx0U17M! zB|+@X@L=#|)rJA>mM22QNSJF9G%f7;Jlrhd$f-15KBa=bXV=I`O~bW?mvE21H6 z9$!E~y(_{^Vrf4IkhG|XaooZy<<2*v^MmJZz)bfrBLePr<@~<;Z}^8l*e#06VXHMW zrGv2woK6GOUA;^TOf!lXEAXL9PfI7QbqYl$EYGiP6Y3rP()uqTxE+ET1+q(JPigIo zd=}~w09tNTd&0+gT}*u+&z1Q{={0sp%K>pzZ%n*!nt%}hM(9@;W8)A#w|cdKD()H= z=}>Ox85D=UwYhW83C3ZC%HBJ)XxsAmuAS)C(iiJ-ugK9u1AV^oGHzT>g+3k8Y%=Y7 zJ~E*E+@x8<(113RS$GkhA4*7ewh8P@U^{xpaw|ryWmhX<9CE&(S2ep+?4vxmqqRAR z12f0+Qmo760p>ju-p&~DF>{ytOasOcbOEye6OhdjqnXJHk5L&l3W1(qpBd&)IR}mH zwIB1bqFp!}Z%a#LmpQP;B~N|80eilJ?Nx1vvsF(t>v}@9ppHWDj%!u@{Usty@_NY% zOU>?e30UOlHD8JNkiqtfa8&`S2_x32zkaFLk5r9iKbCJ5v5~E1d0Db4|0j{5c_;kT z$&TX8Eoz>ge|fL}t>AOjq;Zr00Fb1M0O;)hJaF`h*^Je~+R5F9A0VQ>a76;X{L+)I zy}2i{LfNnH(<0ad5FI~9uh>;Zz=N91AIVn105Y<540^LI#}yd=jo%%L-&uPdBs(=< z;^Fx0j(>$K)r!&ma@0*=N45WdV8)X9oHA@!AWFTZC2++JuJKEzBu5Us_ul1^W_cTWjPI zKb*QkvP^#ntq)tIN4UOaw%(pOom8PRo~stEpCLqEX|iq+P+XJpK$=!DvBh;s3**f^ z{u!ar6W(Uw=HUhAy_Y-KUda{_*vxWu`{a7EFyvv0??T$ieC*yE8>HLf3lRY_d_hbw zo}_NS-X1la@rv?dgbgE9@c=;?iq%wgWVMLQ;M>d0^9*b%)fbI<(W|`X$99)^?A+$& zm1?Xcs_3Z@LT$FtGe~sfWYV)@MBh@liMliqa3>AMSvRe4IRe~{PQzlBb}kK%B~Esl zBAhoI18Ck7^bqWjszv;^BlQyGInrOO?2tTtIoC-tzx_68)L8Tq3t9--pp7Aklp_TO zC=+R0UJ8x=)u3O=Qu?qXbEPwVF*Y!#Ub;bBWcFs@0MK1l|QGfMz1se%~Evix2&JSKF3R8=BhMB%dn{8dPJrI8Z$8ezpTNb-k7@Uz9cEOxBnhea(LMgn z0ekAuw;ftvXDYQ~ZhNZSs`_h`kk0rOo@x}({tAegV z_}Qsn+p++HpWDg4eUqvuTcyEr0upZ(j2}66NF~-gbKDl9xZ}z3GSqSe?0@lP&D$nC ztEc}^vtvZ}v9m;L7$*#-OIKyS;7r~_K#6^nne)@%PNjit*#ip78#Ew)H{E1_-n>N$ zw;57h@J_B~{r0cdvbzK2|xK zN@7W$(cvAq&M*dJzim8pqv2FUdZ({tNeJcAd}y1*Y{Kt$B_e>g-TJvenZd`;7FOX-ElHDZ;j<%FGsiB2Uz+p4;AB za!dyo;y-ZlR`EH-MMnG{)NY|>8B0}7!}GQ}t7`n+rb?nYrm#>Iao900O6hwZE0SQq zoAhTxsiQK^>pCwS_p3k4G0L)4v-}qfrP!t}9x<;dM<{(`!Z5VxY zwyEvEOlz9DOYamwyZ$s0gxyViektm!<3W#D#+M_G(&KBRKX>y}iA(_!7jPaFW}I8S zE5)_!nFu})OaIBmTQ^HU7n-~NJzGS>es?6^K_fPpQZB*vv~rQG(rIam02i|>rKE`= zm|~0mMKR-VRCywC2InI1AX5!;NMgDP42|$0rc@02J)CylOTyvdy>r7~cv8+FMJKnq@QDk9)nito^etfX#u=M%) z{)DfJ%0;QJe;HN&%fi4f2OEv=Rw3mpQ_EJrzJf5_S_>px&!Lh(kE6MH#;nimW0o5` z;E2s@NI96~L<5XeEPtV1PC@4Zp z*rKX$#Bzu!z<=?EluT^nX1&9{eSU<4%6QSRuomNZ7GZl@P!16N^!!94L_<{W^%b%` zox1yUBrPAVP_#8%K2tbNcC^3aS}W&br#_A>)Ln~XsyTloWpqB*;ry56NfX}iUz0Q= z&z`~725umPtyRlsm9k|lS(Dv1DQvIZDjw}>6>!0MJd|wHxuO5 zaEA&ftUiTk*p0TBz^^8i9F-1`$IyS}?9Sz)M(en{>e9!EHNe!%>Ihcaqv8XDU8f9D z>{@*c7^pUaq}Qx>+y2iERr#IpSm4+Qzp%I}vVOW!j&uSk{hMIxNE;xxgZ9=J!f~QTqws5FN*7_5$xWm4?VYQ9x-k}|Y!@6BWOn_8zqVoV zo6G}shV}V1iARXBylN&{t>|x<-q1OFF z;pXE?)!gcAbQ3ZcqSq%$a-Q#)x_Vpp0dKvJb`#KzHn8hYI*}qZCs40rIPut-Vo7PG z(^e;%*MFN05dHp*VjE8{Dg>+&?cSv?_Z-xAhRu+Ntui@aNzUQli--0AoB=Z+Xshfv ztoc!WTK+3naJbHHco@TT@~}_#ByZ%YACR%_qbFP5Q1)B9Ik8Hl6MjS5arXmGUUv#U zQ-b-0NqkoFS%vs(_Xr9O3JxWYZN=!tcI4$`hHVo_zwjS-`zQgD z3;FQx_&^3A#}G3s9mq(eMbuuqvP(wXVw@wJTxp`vWPB>2Jm-O8+u9mp_m11e5RM41dAT}BpV$Z&_Y)~$Zz?Fq)1GpB^pQK@2_VLKpOj= zXB8Dnr1S<&cAx&FU~0`Z=|F<$WVM^cWC1IrIHmn!*?Bw_VJ*lP7dW+w`Lm-l%y=aG zyO(99dS4dZqUq?{{Fw71h4P}A4VF+^?2)tfk^4|Stf7QE89r3M&tK9IL?&=RpIMIztJ(0A!Xx?AB|B#W8@}?W^ zBYB%}o|1Fz0;SpyG{Z&`H}P^x4Yd@|%|Mn*ftB2|m#-QX*eCKaboKZp0eXgw1n#xwa>zxazXX|D2*oxBMdVnoZgwR< zeLstqBU~+;Y)zwdj&TG z_uC}%=bkpMf)lfju8_YV`gz{B(4S4Z;Vl55Ot(J4Xj)!2iB`XCX zu%sOB8H48%^$-eRTW8*@?sFXF`OgGy4~_gI5r(4TU9utMQ{5Tnhtq&+OSTW#(K<@{ zeXIMuD)$qml%{02)XoYDhxhS!1L-K(X8l!Oi;=gjWN)}n!^-$>9%WDk6ea%xv^@5o z4Rvp^1Q(5r|M~%qW_~&*NFC@OEDG~~cvkA^1h!Kd&2YL&M?xt^*h`tW>LPFkS5mAO z6lm9ht0AXYm(#-L`Rz$5XDnQ8t#&Fo3}$bmV5gqO%|5e7iD|i~lYoQfV03T^@+%Z< z^aM1L61zPQQ@O5SKw=IS>|J!`Pb)VB6lO54@%-ggtfms0WKrEF-5}QF+5D4I) zYX0o}+!Vclt!4UvB0&V6b`AMtnGlgJr1HCOPjBxYvji4sST7c%vP>ZPrXqGOyVbF0 zH(+<8jwA66JqnZLnp z`-plVG{<}*Anp)kTWWHyv|mDTqBx^xN_c09#S@r#}Mt{<{? zGtp5Mfw&*DOa}UUpC!WEyA~K5=hDm}<{Dcj%oei9Nh}XOj}@z`)Z^w@EpeY(PE|=kQ6Ksnjdo2++^3W0K9?u3iajEBw5@$eKX3C0mbIRN8!KVX~kum}!?pcVoyj z0br(3O{-7R?hw;6@Txq1>2Ea-kX$Bg&)WJcTjYB}nr7-)GO zD~3W#Ku7T}(S^nz<9a)CgxI+*QCQ(7d~RlVkIWI-IQ3gR;Rt@(Zf_THNZV8{4_ChQl7!wz7<9 zp_9vnidbcU(;-oOTi7=;M_YV({C6kX2Y1L*K|WV6*1MP+ez9 zKacLU)z7~%%@W)cQ@?*HJDpmQ{Fgg^C7UMZWv_3!^zfwzjon8sBDeiVq@hE;lcbOw z=}DqA5id4#90Fz_%g_%dD?^p6OX)%iryP+cPWL8BBAd9r%Bwu#vu1q_>qGZE20is9 zIL$bb(8ne;p1#sU!+#5SKg?}pMyV~cUV|@1GOlQ~i+BGk*M9Rr+9!e6$=&o9fU4^rWa!OcM<@EhM z?KYQG%mv;vGun4LnfA#~IYYbE4MFJ1jE%*^mGVr&|2)Gi~HW${7U~(It@& zvKKk*zKvku%r@8IT0P>k3bW$fB2^4FjQD9BoVZ^6 zPiS!)f0YuRapc!0t55aDOf^TV`E_I9-KkDbJ)5fs{=Mvl^Uo@o_T zirVK<(ha(G%={C~181)9&T^NcmwS6zuUK8IjB8)4$Kn)p8aj1s=vC^V&3IU;<8Lz{ ziTN;LaVbLTrnWIIZMCx;YG5wY;#oq=Jj2N|ril2dQ%&{28nx(yd}a54M`=>2t_FFC z=KZriA9s;Xw-U<4Z~soie?OGIzs*N+`bLR@;7^H-5Fmz)0{8#<3lxk3g}?OV9hx=p R#)A2HWF!?Os=gZs{6BR?Y?A;0 literal 0 HcmV?d00001 diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/menus.png b/hammerspoon/Spoons/HSearch.spoon/resources/menus.png new file mode 100644 index 0000000000000000000000000000000000000000..b6c6ae570f8376e3aa579c62e908eed82c172757 GIT binary patch literal 437 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 C{aIN6 literal 0 HcmV?d00001 diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/thesaurus.png b/hammerspoon/Spoons/HSearch.spoon/resources/thesaurus.png new file mode 100644 index 0000000000000000000000000000000000000000..f0c1b7b42fe38ca3e5953858cd092028fe113ee5 GIT binary patch literal 4723 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 literal 0 HcmV?d00001 diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/time.png b/hammerspoon/Spoons/HSearch.spoon/resources/time.png new file mode 100644 index 0000000000000000000000000000000000000000..6ac38c6f9370f9d4cafc5bff5fbca138e44b938e GIT binary patch literal 35916 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 literal 0 HcmV?d00001 diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/v2ex.png b/hammerspoon/Spoons/HSearch.spoon/resources/v2ex.png new file mode 100644 index 0000000000000000000000000000000000000000..d657b93d199b4d077d37a5cef392d5bfc674a89e GIT binary patch literal 3185 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 F{{zcuRPz7; literal 0 HcmV?d00001 diff --git a/hammerspoon/Spoons/KSheet.spoon/docs.json b/hammerspoon/Spoons/KSheet.spoon/docs.json new file mode 100644 index 0000000..7030cca --- /dev/null +++ b/hammerspoon/Spoons/KSheet.spoon/docs.json @@ -0,0 +1,119 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "desc" : "Keybindings cheatsheet for current application", + "Deprecated" : [ + + ], + "type" : "Module", + "Constructor" : [ + + ], + "doc" : "Keybindings cheatsheet for current application\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/KSheet.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/KSheet.spoon.zip)", + "Method" : [ + { + "doc" : "Show current application's keybindings in a webview", + "name" : "show", + "desc" : "Show current application's keybindings in a webview", + "stripped_doc" : [ + "Show current application's keybindings in a webview" + ], + "notes" : [ + + ], + "signature" : "KSheet:show()", + "type" : "Method", + "returns" : [ + + ], + "def" : "KSheet:show()", + "parameters" : [ + + ] + }, + { + "doc" : "Hide the cheatsheet webview", + "name" : "hide", + "desc" : "Hide the cheatsheet webview", + "stripped_doc" : [ + "Hide the cheatsheet webview" + ], + "notes" : [ + + ], + "signature" : "KSheet:hide()", + "type" : "Method", + "returns" : [ + + ], + "def" : "KSheet:hide()", + "parameters" : [ + + ] + } + ], + "Command" : [ + + ], + "Field" : [ + + ], + "items" : [ + { + "doc" : "Hide the cheatsheet webview", + "name" : "hide", + "desc" : "Hide the cheatsheet webview", + "stripped_doc" : [ + "Hide the cheatsheet webview" + ], + "notes" : [ + + ], + "signature" : "KSheet:hide()", + "type" : "Method", + "returns" : [ + + ], + "def" : "KSheet:hide()", + "parameters" : [ + + ] + }, + { + "doc" : "Show current application's keybindings in a webview", + "name" : "show", + "desc" : "Show current application's keybindings in a webview", + "stripped_doc" : [ + "Show current application's keybindings in a webview" + ], + "notes" : [ + + ], + "signature" : "KSheet:show()", + "type" : "Method", + "returns" : [ + + ], + "def" : "KSheet:show()", + "parameters" : [ + + ] + } + ], + "name" : "KSheet" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/KSheet.spoon/init.lua b/hammerspoon/Spoons/KSheet.spoon/init.lua new file mode 100644 index 0000000..e6ef77a --- /dev/null +++ b/hammerspoon/Spoons/KSheet.spoon/init.lua @@ -0,0 +1,214 @@ +--- === KSheet === +--- +--- Keybindings cheatsheet for current application +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "KSheet" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +-- Workaround for "Dictation" menuitem +hs.application.menuGlyphs[148]="fn fn" + +obj.commandEnum = { + cmd = '⌘', + shift = '⇧', + alt = '⌥', + ctrl = '⌃', +} + +function obj:init() + self.sheetView = hs.webview.new({x=0, y=0, w=0, h=0}) + self.sheetView:windowTitle("CheatSheets") + self.sheetView:windowStyle("utility") + self.sheetView:allowGestures(true) + self.sheetView:allowNewWindows(false) + self.sheetView:level(hs.drawing.windowLevels.modalPanel) +end + +local function processMenuItems(menustru) + local menu = "" + for pos,val in pairs(menustru) do + if type(val) == "table" then + -- TODO: Remove menubar items with no shortcuts in them + if val.AXRole == "AXMenuBarItem" and type(val.AXChildren) == "table" then + menu = menu .. "
    " + menu = menu .. "
  • " .. val.AXTitle .. "
  • " + menu = menu .. processMenuItems(val.AXChildren[1]) + menu = menu .. "
" + elseif val.AXRole == "AXMenuItem" and not val.AXChildren then + if not (val.AXMenuItemCmdChar == '' and val.AXMenuItemCmdGlyph == '') then + local CmdModifiers = '' + for key, value in pairs(val.AXMenuItemCmdModifiers) do + CmdModifiers = CmdModifiers .. obj.commandEnum[value] + end + local CmdChar = val.AXMenuItemCmdChar + local CmdGlyph = hs.application.menuGlyphs[val.AXMenuItemCmdGlyph] or '' + local CmdKeys = CmdChar .. CmdGlyph + menu = menu .. "
  • " .. CmdModifiers .. " " .. CmdKeys .. "
    " .. " " .. val.AXTitle .. "
  • " + end + elseif val.AXRole == "AXMenuItem" and type(val.AXChildren) == "table" then + menu = menu .. processMenuItems(val.AXChildren[1]) + end + end + end + return menu +end + +local function generateHtml(application) + local app_title = application:title() + local menuitems_tree = application:getMenuItems() + local allmenuitems = processMenuItems(menuitems_tree) + + local html = [[ + + + + + + +
    +
    ]] .. app_title .. [[
    +
    +
    +
    ]] .. allmenuitems .. [[
    +
    + +
    + + + + + ]] + + return html +end + +--- KSheet:show() +--- Method +--- Show current application's keybindings in a webview +--- + +function obj:show() + local capp = hs.application.frontmostApplication() + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + self.sheetView:frame({ + x = cres.x+cres.w*0.15/2, + y = cres.y+cres.h*0.25/2, + w = cres.w*0.85, + h = cres.h*0.75 + }) + local webcontent = generateHtml(capp) + self.sheetView:html(webcontent) + self.sheetView:show() +end + +--- KSheet:hide() +--- Method +--- Hide the cheatsheet webview +--- + +function obj:hide() + self.sheetView:hide() +end + +return obj diff --git a/hammerspoon/Spoons/ModalMgr.spoon/docs.json b/hammerspoon/Spoons/ModalMgr.spoon/docs.json new file mode 100644 index 0000000..3e4f0bd --- /dev/null +++ b/hammerspoon/Spoons/ModalMgr.spoon/docs.json @@ -0,0 +1,253 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "type" : "Module", + "desc" : "Modal keybindings environment management. Just an wrapper of `hs.hotkey.modal`.", + "Constructor" : [ + + ], + "doc" : "Modal keybindings environment management. Just an wrapper of `hs.hotkey.modal`.\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/ModalMgr.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/ModalMgr.spoon.zip)", + "Method" : [ + { + "desc" : "Create a new modal keybindings environment", + "stripped_doc" : [ + "Create a new modal keybindings environment", + "" + ], + "parameters" : [ + " * id - A string specifying ID of new modal keybindings" + ], + "doc" : "Create a new modal keybindings environment\n\nParameters:\n * id - A string specifying ID of new modal keybindings", + "notes" : [ + + ], + "signature" : "ModalMgr:new(id)", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:new(id)", + "name" : "new" + }, + { + "desc" : "Toggle the cheatsheet display of current modal environments's keybindings.", + "stripped_doc" : [ + "Toggle the cheatsheet display of current modal environments's keybindings.", + "" + ], + "parameters" : [ + " * iterList - An table specifying IDs of modal environments or active_list. Optional, defaults to all active environments.", + " * force - A optional boolean value to force show cheatsheet, defaults to `nil` (automatically)." + ], + "doc" : "Toggle the cheatsheet display of current modal environments's keybindings.\n\nParameters:\n * iterList - An table specifying IDs of modal environments or active_list. Optional, defaults to all active environments.\n * force - A optional boolean value to force show cheatsheet, defaults to `nil` (automatically).", + "notes" : [ + + ], + "signature" : "ModalMgr:toggleCheatsheet([idList], [force])", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:toggleCheatsheet([idList], [force])", + "name" : "toggleCheatsheet" + }, + { + "desc" : "Activate all modal environment in `idList`.", + "stripped_doc" : [ + "Activate all modal environment in `idList`.", + "" + ], + "parameters" : [ + " * idList - An table specifying IDs of modal environments", + " * trayColor - An optional string (e.g. #000000) specifying the color of modalTray, defaults to `nil`.", + " * showKeys - A optional boolean value to show all available keybindings, defaults to `nil`." + ], + "doc" : "Activate all modal environment in `idList`.\n\nParameters:\n * idList - An table specifying IDs of modal environments\n * trayColor - An optional string (e.g. #000000) specifying the color of modalTray, defaults to `nil`.\n * showKeys - A optional boolean value to show all available keybindings, defaults to `nil`.", + "notes" : [ + + ], + "signature" : "ModalMgr:activate(idList, [trayColor], [showKeys])", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:activate(idList, [trayColor], [showKeys])", + "name" : "activate" + }, + { + "desc" : "Deactivate modal environments in `idList`.", + "stripped_doc" : [ + "Deactivate modal environments in `idList`.", + "" + ], + "parameters" : [ + " * idList - An table specifying IDs of modal environments" + ], + "doc" : "Deactivate modal environments in `idList`.\n\nParameters:\n * idList - An table specifying IDs of modal environments", + "notes" : [ + + ], + "signature" : "ModalMgr:deactivate(idList)", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:deactivate(idList)", + "name" : "deactivate" + }, + { + "desc" : "Deactivate all active modal environments.", + "stripped_doc" : [ + "Deactivate all active modal environments." + ], + "parameters" : [ + + ], + "doc" : "Deactivate all active modal environments.", + "notes" : [ + + ], + "signature" : "ModalMgr:deactivateAll()", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:deactivateAll()", + "name" : "deactivateAll" + } + ], + "Command" : [ + + ], + "Field" : [ + + ], + "items" : [ + { + "desc" : "Activate all modal environment in `idList`.", + "stripped_doc" : [ + "Activate all modal environment in `idList`.", + "" + ], + "parameters" : [ + " * idList - An table specifying IDs of modal environments", + " * trayColor - An optional string (e.g. #000000) specifying the color of modalTray, defaults to `nil`.", + " * showKeys - A optional boolean value to show all available keybindings, defaults to `nil`." + ], + "doc" : "Activate all modal environment in `idList`.\n\nParameters:\n * idList - An table specifying IDs of modal environments\n * trayColor - An optional string (e.g. #000000) specifying the color of modalTray, defaults to `nil`.\n * showKeys - A optional boolean value to show all available keybindings, defaults to `nil`.", + "notes" : [ + + ], + "signature" : "ModalMgr:activate(idList, [trayColor], [showKeys])", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:activate(idList, [trayColor], [showKeys])", + "name" : "activate" + }, + { + "desc" : "Deactivate modal environments in `idList`.", + "stripped_doc" : [ + "Deactivate modal environments in `idList`.", + "" + ], + "parameters" : [ + " * idList - An table specifying IDs of modal environments" + ], + "doc" : "Deactivate modal environments in `idList`.\n\nParameters:\n * idList - An table specifying IDs of modal environments", + "notes" : [ + + ], + "signature" : "ModalMgr:deactivate(idList)", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:deactivate(idList)", + "name" : "deactivate" + }, + { + "desc" : "Deactivate all active modal environments.", + "stripped_doc" : [ + "Deactivate all active modal environments." + ], + "parameters" : [ + + ], + "doc" : "Deactivate all active modal environments.", + "notes" : [ + + ], + "signature" : "ModalMgr:deactivateAll()", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:deactivateAll()", + "name" : "deactivateAll" + }, + { + "desc" : "Create a new modal keybindings environment", + "stripped_doc" : [ + "Create a new modal keybindings environment", + "" + ], + "parameters" : [ + " * id - A string specifying ID of new modal keybindings" + ], + "doc" : "Create a new modal keybindings environment\n\nParameters:\n * id - A string specifying ID of new modal keybindings", + "notes" : [ + + ], + "signature" : "ModalMgr:new(id)", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:new(id)", + "name" : "new" + }, + { + "desc" : "Toggle the cheatsheet display of current modal environments's keybindings.", + "stripped_doc" : [ + "Toggle the cheatsheet display of current modal environments's keybindings.", + "" + ], + "parameters" : [ + " * iterList - An table specifying IDs of modal environments or active_list. Optional, defaults to all active environments.", + " * force - A optional boolean value to force show cheatsheet, defaults to `nil` (automatically)." + ], + "doc" : "Toggle the cheatsheet display of current modal environments's keybindings.\n\nParameters:\n * iterList - An table specifying IDs of modal environments or active_list. Optional, defaults to all active environments.\n * force - A optional boolean value to force show cheatsheet, defaults to `nil` (automatically).", + "notes" : [ + + ], + "signature" : "ModalMgr:toggleCheatsheet([idList], [force])", + "type" : "Method", + "returns" : [ + + ], + "def" : "ModalMgr:toggleCheatsheet([idList], [force])", + "name" : "toggleCheatsheet" + } + ], + "name" : "ModalMgr" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/ModalMgr.spoon/init.lua b/hammerspoon/Spoons/ModalMgr.spoon/init.lua new file mode 100644 index 0000000..2c7a61d --- /dev/null +++ b/hammerspoon/Spoons/ModalMgr.spoon/init.lua @@ -0,0 +1,189 @@ +--- === ModalMgr === +--- +--- Modal keybindings environment management. Just an wrapper of `hs.hotkey.modal`. +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ModalMgr.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ModalMgr.spoon.zip) + +local obj = {} +obj.__index = obj + +-- Metadata +obj.name = "ModalMgr" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +obj.modal_tray = nil +obj.which_key = nil +obj.modal_list = {} +obj.active_list = {} +obj.supervisor = nil + +function obj:init() + hsupervisor_keys = hsupervisor_keys or {{"cmd", "shift", "ctrl"}, "Q"} + obj.supervisor = hs.hotkey.modal.new(hsupervisor_keys[1], hsupervisor_keys[2], 'Initialize Modal Environment') + obj.supervisor:bind(hsupervisor_keys[1], hsupervisor_keys[2], "Reset Modal Environment", function() obj.supervisor:exit() end) + hshelp_keys = hshelp_keys or {{"alt", "shift"}, "/"} + obj.supervisor:bind(hshelp_keys[1], hshelp_keys[2], "Toggle Help Panel", function() obj:toggleCheatsheet({all=obj.supervisor}) end) + obj.modal_tray = hs.canvas.new({x = 0, y = 0, w = 0, h = 0}) + obj.modal_tray:level(hs.canvas.windowLevels.tornOffMenu) + obj.modal_tray[1] = { + type = "circle", + action = "fill", + fillColor = {hex = "#FFFFFF", alpha = 0.7}, + } + obj.which_key = hs.canvas.new({x = 0, y = 0, w = 0, h = 0}) + obj.which_key:level(hs.canvas.windowLevels.tornOffMenu) + obj.which_key[1] = { + type = "rectangle", + action = "fill", + fillColor = {hex = "#EEEEEE", alpha = 0.95}, + roundedRectRadii = {xRadius = 10, yRadius = 10}, + } +end + +--- ModalMgr:new(id) +--- Method +--- Create a new modal keybindings environment +--- +--- Parameters: +--- * id - A string specifying ID of new modal keybindings + +function obj:new(id) + obj.modal_list[id] = hs.hotkey.modal.new() +end + +--- ModalMgr:toggleCheatsheet([idList], [force]) +--- Method +--- Toggle the cheatsheet display of current modal environments's keybindings. +--- +--- Parameters: +--- * iterList - An table specifying IDs of modal environments or active_list. Optional, defaults to all active environments. +--- * force - A optional boolean value to force show cheatsheet, defaults to `nil` (automatically). + +function obj:toggleCheatsheet(iterList, force) + if obj.which_key:isShowing() and not force then + obj.which_key:hide() + else + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + obj.which_key:frame({ + x = cres.x + cres.w / 5, + y = cres.y + cres.h / 5, + w = cres.w / 5 * 3, + h = cres.h / 5 * 3 + }) + local keys_pool = {} + local tmplist = iterList or obj.active_list + for i, v in pairs(tmplist) do + if type(v) == "string" then + -- It appears to be idList + for _, m in ipairs(obj.modal_list[v].keys) do + table.insert(keys_pool, m.msg) + end + elseif type(i) == "string" then + -- It appears to be active_list + for _, m in pairs(v.keys) do + table.insert(keys_pool, m.msg) + end + end + end + for idx, val in ipairs(keys_pool) do + if idx % 2 == 1 then + obj.which_key[idx + 1] = { + type = "text", + text = keys_pool[idx], + textFont = "Courier-Bold", + textSize = 16, + textColor = {hex = "#2390FF", alpha = 1}, + textAlignment = "left", + frame = { + x = tostring(40 / (cres.w / 5 * 3)), + y = tostring((30 + (idx - math.ceil(idx / 2)) * math.ceil((cres.h / 5 * 3 - 60) / #keys_pool) * 2) / (cres.h / 5 * 3)), + w = tostring((1 - 80 / (cres.w / 5 * 3)) / 2), + h = tostring(math.ceil((cres.h / 5 * 3 - 60) / #keys_pool) * 2 / (cres.h / 5 * 3)) + } + } + else + obj.which_key[idx + 1] = { + type = "text", + text = keys_pool[idx], + textFont = "Courier-Bold", + textSize = 16, + textColor = {hex = "#2390FF"}, + textAlignment = "right", + frame = { + x = "50%", + y = tostring((30 + (idx - math.ceil(idx / 2) - 1) * math.ceil((cres.h / 5 * 3 - 60) / #keys_pool) * 2) / (cres.h / 5 * 3)), + w = tostring((1 - 80 / (cres.w / 5 * 3)) / 2), + h = tostring(math.ceil((cres.h / 5 * 3 - 60) / #keys_pool) * 2 / (cres.h / 5 * 3)) + } + } + end + end + obj.which_key:show() + end +end + +--- ModalMgr:activate(idList, [trayColor], [showKeys]) +--- Method +--- Activate all modal environment in `idList`. +--- +--- Parameters: +--- * idList - An table specifying IDs of modal environments +--- * trayColor - An optional string (e.g. #000000) specifying the color of modalTray, defaults to `nil`. +--- * showKeys - A optional boolean value to show all available keybindings, defaults to `nil`. + +function obj:activate(idList, trayColor, showKeys) + for _, val in ipairs(idList) do + obj.modal_list[val]:enter() + obj.active_list[val] = obj.modal_list[val] + end + if trayColor then + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + local lcres = cscreen:absoluteToLocal(cres) + obj.modal_tray:frame(cscreen:localToAbsolute{ + x = cres.w - 40, + y = cres.h - 40, + w = 20, + h = 20 + }) + obj.modal_tray[1].fillColor = {hex = trayColor, alpha = 0.7} + obj.modal_tray:show() + end + if showKeys then + obj:toggleCheatsheet(idList, true) + end +end + +--- ModalMgr:deactivate(idList) +--- Method +--- Deactivate modal environments in `idList`. +--- +--- Parameters: +--- * idList - An table specifying IDs of modal environments + +function obj:deactivate(idList) + for _, val in ipairs(idList) do + obj.modal_list[val]:exit() + obj.active_list[val] = nil + end + obj.modal_tray:hide() + for i = 2, #obj.which_key do + obj.which_key:removeElement(2) + end + obj.which_key:hide() +end + +--- ModalMgr:deactivateAll() +--- Method +--- Deactivate all active modal environments. +--- + +function obj:deactivateAll() + obj:deactivate(obj.active_list) +end + +return obj diff --git a/hammerspoon/Spoons/ReloadConfiguration.spoon/docs.json b/hammerspoon/Spoons/ReloadConfiguration.spoon/docs.json new file mode 100644 index 0000000..50c62f2 --- /dev/null +++ b/hammerspoon/Spoons/ReloadConfiguration.spoon/docs.json @@ -0,0 +1,90 @@ +[ + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "ReloadConfiguration:bindHotkeys(mapping)", + "desc": "Binds hotkeys for ReloadConfiguration", + "doc": "Binds hotkeys for ReloadConfiguration\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * reloadConfiguration - This will cause the configuration to be reloaded", + "name": "bindHotkeys", + "parameters": [ + " * mapping - A table containing hotkey modifier/key details for the following items:", + " * reloadConfiguration - This will cause the configuration to be reloaded" + ], + "signature": "ReloadConfiguration:bindHotkeys(mapping)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "ReloadConfiguration:start()", + "desc": "Start ReloadConfiguration", + "doc": "Start ReloadConfiguration\n\nParameters:\n * None", + "name": "start", + "parameters": [ + " * None" + ], + "signature": "ReloadConfiguration:start()", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [ + { + "def": "ReloadConfiguration.watch_paths", + "desc": "List of directories to watch for changes, defaults to hs.configdir", + "doc": "List of directories to watch for changes, defaults to hs.configdir", + "name": "watch_paths", + "signature": "ReloadConfiguration.watch_paths", + "stripped_doc": "", + "type": "Variable" + } + ], + "desc": "Adds a hotkey to reload the hammerspoon configuration, and a pathwatcher to automatically reload on changes.", + "doc": "Adds a hotkey to reload the hammerspoon configuration, and a pathwatcher to automatically reload on changes.\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ReloadConfiguration.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ReloadConfiguration.spoon.zip)", + "items": [ + { + "def": "ReloadConfiguration:bindHotkeys(mapping)", + "desc": "Binds hotkeys for ReloadConfiguration", + "doc": "Binds hotkeys for ReloadConfiguration\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * reloadConfiguration - This will cause the configuration to be reloaded", + "name": "bindHotkeys", + "parameters": [ + " * mapping - A table containing hotkey modifier/key details for the following items:", + " * reloadConfiguration - This will cause the configuration to be reloaded" + ], + "signature": "ReloadConfiguration:bindHotkeys(mapping)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "ReloadConfiguration:start()", + "desc": "Start ReloadConfiguration", + "doc": "Start ReloadConfiguration\n\nParameters:\n * None", + "name": "start", + "parameters": [ + " * None" + ], + "signature": "ReloadConfiguration:start()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "ReloadConfiguration.watch_paths", + "desc": "List of directories to watch for changes, defaults to hs.configdir", + "doc": "List of directories to watch for changes, defaults to hs.configdir", + "name": "watch_paths", + "signature": "ReloadConfiguration.watch_paths", + "stripped_doc": "", + "type": "Variable" + } + ], + "name": "ReloadConfiguration", + "stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ReloadConfiguration.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ReloadConfiguration.spoon.zip)", + "submodules": [], + "type": "Module" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/ReloadConfiguration.spoon/init.lua b/hammerspoon/Spoons/ReloadConfiguration.spoon/init.lua new file mode 100644 index 0000000..bb83f67 --- /dev/null +++ b/hammerspoon/Spoons/ReloadConfiguration.spoon/init.lua @@ -0,0 +1,49 @@ +--- === ReloadConfiguration === +--- +--- Adds a hotkey to reload the hammerspoon configuration, and a pathwatcher to automatically reload on changes. +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ReloadConfiguration.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ReloadConfiguration.spoon.zip) + +local obj = {} +obj.__index = obj + +-- Metadata +obj.name = "ReloadConfiguration" +obj.version = "1.0" +obj.author = "Jon Lorusso " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + + +--- ReloadConfiguration.watch_paths +--- Variable +--- List of directories to watch for changes, defaults to hs.configdir +obj.watch_paths = { hs.configdir } + +--- ReloadConfiguration:bindHotkeys(mapping) +--- Method +--- Binds hotkeys for ReloadConfiguration +--- +--- Parameters: +--- * mapping - A table containing hotkey modifier/key details for the following items: +--- * reloadConfiguration - This will cause the configuration to be reloaded +function obj:bindHotkeys(mapping) + local def = { reloadConfiguration = hs.fnutils.partial(hs.reload, self) } + hs.spoons.bindHotkeysToSpec(def, mapping) +end + +--- ReloadConfiguration:start() +--- Method +--- Start ReloadConfiguration +--- +--- Parameters: +--- * None +function obj:start() + self.watchers = {} + for _,dir in pairs(self.watch_paths) do + self.watchers[dir] = hs.pathwatcher.new(dir, hs.reload):start() + end + return self +end + +return obj diff --git a/hammerspoon/Spoons/SpeedMenu.spoon/docs.json b/hammerspoon/Spoons/SpeedMenu.spoon/docs.json new file mode 100644 index 0000000..aa34d28 --- /dev/null +++ b/hammerspoon/Spoons/SpeedMenu.spoon/docs.json @@ -0,0 +1,79 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "desc" : "Menubar netspeed meter", + "Deprecated" : [ + + ], + "type" : "Module", + "Constructor" : [ + + ], + "doc" : "Menubar netspeed meter\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/SpeedMenu.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/SpeedMenu.spoon.zip)", + "Field" : [ + + ], + "Command" : [ + + ], + "items" : [ + { + "doc" : "Redetect the active interface, darkmode …And redraw everything.", + "desc" : "Redetect the active interface, darkmode …And redraw everything.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Redetect the active interface, darkmode …And redraw everything." + ], + "notes" : [ + + ], + "signature" : "SpeedMenu:rescan()", + "type" : "Method", + "returns" : [ + + ], + "def" : "SpeedMenu:rescan()", + "name" : "rescan" + } + ], + "Method" : [ + { + "doc" : "Redetect the active interface, darkmode …And redraw everything.", + "desc" : "Redetect the active interface, darkmode …And redraw everything.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Redetect the active interface, darkmode …And redraw everything." + ], + "notes" : [ + + ], + "signature" : "SpeedMenu:rescan()", + "type" : "Method", + "returns" : [ + + ], + "def" : "SpeedMenu:rescan()", + "name" : "rescan" + } + ], + "name" : "SpeedMenu" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/SpeedMenu.spoon/init.lua b/hammerspoon/Spoons/SpeedMenu.spoon/init.lua new file mode 100644 index 0000000..d4ecdac --- /dev/null +++ b/hammerspoon/Spoons/SpeedMenu.spoon/init.lua @@ -0,0 +1,111 @@ +--- === SpeedMenu === +--- +--- Menubar netspeed meter +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "SpeedMenu" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +function obj:init() + self.menubar = hs.menubar.new() + obj:rescan() +end + +local function data_diff() + local in_seq = hs.execute(obj.instr) + local out_seq = hs.execute(obj.outstr) + local in_diff = in_seq - obj.inseq + local out_diff = out_seq - obj.outseq + if in_diff/1024 > 1024 then + obj.kbin = string.format("%6.2f", in_diff/1024/1024) .. ' mb/s' + else + obj.kbin = string.format("%6.2f", in_diff/1024) .. ' kb/s' + end + if out_diff/1024 > 1024 then + obj.kbout = string.format("%6.2f", out_diff/1024/1024) .. ' mb/s' + else + obj.kbout = string.format("%6.2f", out_diff/1024) .. ' kb/s' + end + local disp_str = '⥄ ' .. obj.kbout .. '\n⥂ ' .. obj.kbin + if obj.darkmode then + obj.disp_str = hs.styledtext.new(disp_str, {font={size=9.0, color={hex="#FFFFFF"}}}) + else + obj.disp_str = hs.styledtext.new(disp_str, {font={size=9.0, color={hex="#000000"}}}) + end + obj.menubar:setTitle(obj.disp_str) + obj.inseq = in_seq + obj.outseq = out_seq +end + +--- SpeedMenu:rescan() +--- Method +--- Redetect the active interface, darkmode …And redraw everything. +--- + +function obj:rescan() + obj.interface = hs.network.primaryInterfaces() + obj.darkmode = hs.osascript.applescript('tell application "System Events"\nreturn dark mode of appearance preferences\nend tell') + local menuitems_table = {} + if obj.interface then + -- Inspect active interface and create menuitems + local interface_detail = hs.network.interfaceDetails(obj.interface) + if interface_detail.AirPort then + local ssid = interface_detail.AirPort.SSID + table.insert(menuitems_table, { + title = "SSID: " .. ssid, + tooltip = "Copy SSID to clipboard", + fn = function() hs.pasteboard.setContents(ssid) end + }) + end + if interface_detail.IPv4 then + local ipv4 = interface_detail.IPv4.Addresses[1] + table.insert(menuitems_table, { + title = "IPv4: " .. ipv4, + tooltip = "Copy IPv4 to clipboard", + fn = function() hs.pasteboard.setContents(ipv4) end + }) + end + if interface_detail.IPv6 then + local ipv6 = interface_detail.IPv6.Addresses[1] + table.insert(menuitems_table, { + title = "IPv6: " .. ipv6, + tooltip = "Copy IPv6 to clipboard", + fn = function() hs.pasteboard.setContents(ipv6) end + }) + end + local macaddr = hs.execute('ifconfig ' .. obj.interface .. ' | grep ether | awk \'{print $2}\'') + table.insert(menuitems_table, { + title = "MAC Addr: " .. macaddr, + tooltip = "Copy MAC Address to clipboard", + fn = function() hs.pasteboard.setContents(macaddr) end + }) + -- Start watching the netspeed delta + obj.instr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $7}\'' + obj.outstr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $10}\'' + + obj.inseq = hs.execute(obj.instr) + obj.outseq = hs.execute(obj.outstr) + + if obj.timer then + obj.timer:stop() + obj.timer = nil + end + obj.timer = hs.timer.doEvery(1, data_diff) + end + table.insert(menuitems_table, { + title = "Rescan Network Interfaces", + fn = function() obj:rescan() end + }) + obj.menubar:setTitle("⚠︎") + obj.menubar:setMenu(menuitems_table) +end + +return obj diff --git a/hammerspoon/Spoons/TimeFlow.spoon/docs.json b/hammerspoon/Spoons/TimeFlow.spoon/docs.json new file mode 100644 index 0000000..1e4f630 --- /dev/null +++ b/hammerspoon/Spoons/TimeFlow.spoon/docs.json @@ -0,0 +1,41 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "type" : "Module", + "desc" : "A widget showing time flown in one year.", + "Constructor" : [ + + ], + "doc" : "A widget showing time flown in one year.\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/TimeFlow.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/TimeFlow.spoon.zip)", + "Field" : [ + + ], + "items" : [ + + ], + "Command" : [ + + ], + "Method" : [ + + ], + "name" : "TimeFlow" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/TimeFlow.spoon/init.lua b/hammerspoon/Spoons/TimeFlow.spoon/init.lua new file mode 100644 index 0000000..e508e11 --- /dev/null +++ b/hammerspoon/Spoons/TimeFlow.spoon/init.lua @@ -0,0 +1,153 @@ +--- === TimeFlow === +--- +--- A widget showing time flown in one year. +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/TimeFlow.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/TimeFlow.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "TimeFlow" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +-- Internal function used to find our location, so we know where to load files from +local function script_path() + local str = debug.getinfo(2, "S").source:sub(2) + return str:match("(.*/)") +end + +obj.spoonPath = script_path() + +local function updateElapsedCanvas() + local nowtable = os.date("*t") + local nowyday = nowtable.yday + local nowhour = string.format("%2s", nowtable.hour) + local nowmin = string.format("%2s", nowtable.min) + local nowsec = string.format("%2s", nowtable.sec) + local timestr = nowyday.." days "..nowhour.." hours "..nowmin.." min "..nowsec.." sec" + local secs_since_epoch = os.time() + local nowyear = nowtable.year + local yearstartsecs_since_epoch = os.time({year=nowyear, month=1, day=1, hour=0}) + local nowyear_elapsed_secs = secs_since_epoch - yearstartsecs_since_epoch + local yearendsecs_since_epoch = os.time({year=nowyear+1, month=1, day=1, hour=0}) + local nowyear_total_secs = yearendsecs_since_epoch - yearstartsecs_since_epoch + local elapsed_percent = nowyear_elapsed_secs/nowyear_total_secs + if obj.canvas:isShowing() then + obj.canvas[3].text = timestr + obj.canvas[6].frame.w = tostring(240/280*elapsed_percent) + end +end + +function obj:init() + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + obj.canvas = hs.canvas.new({ + x = cres.w-280-20, + y = 400, + w = 280, + h = 125 + }):show() + obj.canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) + obj.canvas:level(hs.canvas.windowLevels.desktopIcon) + -- canvas background + obj.canvas[1] = { + action = "fill", + type = "rectangle", + fillColor = {hex="#000000", alpha=0.2}, + roundedRectRadii = {xRadius=5, yRadius=5}, + } + -- title + obj.canvas[2] = { + type = "text", + text = "Time Elapsed", + textSize = 14, + textColor = {hex="#FFFFFF", alpha=0.3}, + frame = { + x = tostring(10/280), + y = tostring(10/125), + w = tostring(260/280), + h = tostring(25/125), + } + } + -- time + obj.canvas[3] = { + type = "text", + text = "", + textColor = {hex="#A6AAC3"}, + textSize = 17, + textAlignment = "center", + frame = { + x = tostring(0/280), + y = tostring(35/125), + w = tostring(280/280), + h = tostring(25/125), + } + } + -- indicator background + obj.canvas[4] = { + type = "image", + image = hs.image.imageFromPath(self.spoonPath .. "/timebg.png"), + frame = { + x = tostring(10/280), + y = tostring(65/125), + w = tostring(260/280), + h = tostring(50/125), + } + } + -- light indicator + obj.canvas[5] = { + action = "fill", + type = "rectangle", + fillColor = {hex="#FFFFFF", alpha=0.2}, + frame = { + x = tostring(20/280), + y = tostring(75/125), + w = tostring(240/280), + h = tostring(20/125), + } + } + -- indicator mask + obj.canvas[6] = { + action = "fill", + type = "rectangle", + frame = { + x = tostring(20/280), + y = tostring(75/125), + w = tostring(240/280), + h = tostring(20/125), + } + } + -- color indicator + obj.canvas[7] = { + action = "fill", + type = "rectangle", + frame = { + x = tostring(20/280), + y = tostring(75/125), + w = tostring(240/280), + h = tostring(20/125), + }, + fillGradient="linear", + fillGradientColors = { + {hex = "#00A0F7"}, + {hex = "#92D2E5"}, + {hex = "#4BE581"}, + {hex = "#EAF25E"}, + {hex = "#F4CA55"}, + {hex = "#E04E4E"}, + }, + } + obj.canvas[7].compositeRule = "sourceAtop" + + if obj.timer == nil then + obj.timer = hs.timer.doEvery(1, function() updateElapsedCanvas() end) + else + obj.timer:start() + end +end + +return obj diff --git a/hammerspoon/Spoons/TimeFlow.spoon/timebg.png b/hammerspoon/Spoons/TimeFlow.spoon/timebg.png new file mode 100644 index 0000000000000000000000000000000000000000..c34723a0819ed0fe743ebaf17aa646d36a2d359d GIT binary patch literal 18390 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 literal 0 HcmV?d00001 diff --git a/hammerspoon/Spoons/UnsplashZ.spoon/docs.json b/hammerspoon/Spoons/UnsplashZ.spoon/docs.json new file mode 100644 index 0000000..507aa0e --- /dev/null +++ b/hammerspoon/Spoons/UnsplashZ.spoon/docs.json @@ -0,0 +1,41 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "desc" : "Use unsplash images as wallpaper", + "type" : "Module", + "Constructor" : [ + + ], + "doc" : "Use unsplash images as wallpaper\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/UnsplashZ.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/UnsplashZ.spoon.zip)", + "Method" : [ + + ], + "Command" : [ + + ], + "items" : [ + + ], + "Field" : [ + + ], + "name" : "UnsplashZ" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/UnsplashZ.spoon/init.lua b/hammerspoon/Spoons/UnsplashZ.spoon/init.lua new file mode 100644 index 0000000..473f255 --- /dev/null +++ b/hammerspoon/Spoons/UnsplashZ.spoon/init.lua @@ -0,0 +1,52 @@ +--- === UnsplashZ === +--- +--- Use unsplash images as wallpaper +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/UnsplashZ.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/UnsplashZ.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "UnsplashZ" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +local function curl_callback(exitCode, stdOut, stdErr) + if exitCode == 0 then + obj.task = nil + obj.last_pic = hs.http.urlParts(obj.pic_url).lastPathComponent + local localpath = os.getenv("HOME") .. "/.Trash/" .. hs.http.urlParts(obj.pic_url).lastPathComponent + + hs.screen.mainScreen():desktopImageURL("file://" .. localpath) + else + print(stdOut, stdErr) + end +end + +local function unsplashRequest() + local 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" + obj.pic_url = hs.execute([[ /usr/bin/curl 'https://source.unsplash.com/1600x900/?nature' | perl -ne ' print "$1" if /href="([^"]+)"/ ' ]]) + if obj.last_pic ~= obj.pic_url then + if obj.task then + obj.task:terminate() + obj.task = nil + end + local localpath = os.getenv("HOME") .. "/.Trash/" .. hs.http.urlParts(obj.pic_url).lastPathComponent + obj.task = hs.task.new("/usr/bin/curl", curl_callback, {"-A", user_agent_str, obj.pic_url, "-o", localpath}) + obj.task:start() + end +end + +function obj:init() + if obj.timer == nil then + obj.timer = hs.timer.doEvery(3*60*60, function() unsplashRequest() end) + obj.timer:setNextTrigger(5) + else + obj.timer:start() + end +end + +return obj diff --git a/hammerspoon/Spoons/WinWin.spoon/docs.json b/hammerspoon/Spoons/WinWin.spoon/docs.json new file mode 100644 index 0000000..56b73b0 --- /dev/null +++ b/hammerspoon/Spoons/WinWin.spoon/docs.json @@ -0,0 +1,406 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + { + "doc" : "An integer specifying how many gridparts the screen should be divided into. Defaults to 30.", + "desc" : "An integer specifying how many gridparts the screen should be divided into. Defaults to 30.", + "parameters" : [ + + ], + "stripped_doc" : [ + "An integer specifying how many gridparts the screen should be divided into. Defaults to 30." + ], + "notes" : [ + + ], + "signature" : "WinWin.gridparts", + "type" : "Variable", + "returns" : [ + + ], + "def" : "WinWin.gridparts", + "name" : "gridparts" + } + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "type" : "Module", + "desc" : "Windows manipulation", + "Constructor" : [ + + ], + "doc" : "Windows manipulation\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/WinWin.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/WinWin.spoon.zip)", + "Method" : [ + { + "doc" : "Move the focused window in the `direction` by on step. The step scale equals to the width\/height of one gridpart.\n\nParameters:\n * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`.", + "desc" : "Move the focused window in the `direction` by on step. The step scale equals to the width\/height of one gridpart.", + "parameters" : [ + " * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`." + ], + "stripped_doc" : [ + "Move the focused window in the `direction` by on step. The step scale equals to the width\/height of one gridpart.", + "" + ], + "notes" : [ + + ], + "signature" : "WinWin:stepMove(direction)", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:stepMove(direction)", + "name" : "stepMove" + }, + { + "doc" : "Resize the focused window in the `direction` by on step.\n\nParameters:\n * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`.", + "desc" : "Resize the focused window in the `direction` by on step.", + "parameters" : [ + " * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`." + ], + "stripped_doc" : [ + "Resize the focused window in the `direction` by on step.", + "" + ], + "notes" : [ + + ], + "signature" : "WinWin:stepResize(direction)", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:stepResize(direction)", + "name" : "stepResize" + }, + { + "doc" : "Stash current windows's position and size.", + "desc" : "Stash current windows's position and size.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Stash current windows's position and size." + ], + "notes" : [ + + ], + "signature" : "WinWin:stash()", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:stash()", + "name" : "stash" + }, + { + "doc" : "Move and resize the focused window.\n\nParameters:\n * option - A string specifying the option, valid strings are: `halfleft`, `halfright`, `halfup`, `halfdown`, `cornerNW`, `cornerSW`, `cornerNE`, `cornerSE`, `center`, `fullscreen`, `expand`, `shrink`.", + "desc" : "Move and resize the focused window.", + "parameters" : [ + " * option - A string specifying the option, valid strings are: `halfleft`, `halfright`, `halfup`, `halfdown`, `cornerNW`, `cornerSW`, `cornerNE`, `cornerSE`, `center`, `fullscreen`, `expand`, `shrink`." + ], + "stripped_doc" : [ + "Move and resize the focused window.", + "" + ], + "notes" : [ + + ], + "signature" : "WinWin:moveAndResize(option)", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:moveAndResize(option)", + "name" : "moveAndResize" + }, + { + "doc" : "Move the focused window between all of the screens in the `direction`.\n\nParameters:\n * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`, `next`.", + "desc" : "Move the focused window between all of the screens in the `direction`.", + "parameters" : [ + " * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`, `next`." + ], + "stripped_doc" : [ + "Move the focused window between all of the screens in the `direction`.", + "" + ], + "notes" : [ + + ], + "signature" : "WinWin:moveToScreen(direction)", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:moveToScreen(direction)", + "name" : "moveToScreen" + }, + { + "doc" : "Undo the last window manipulation. Only those \"moveAndResize\" manipulations can be undone.", + "desc" : "Undo the last window manipulation. Only those \"moveAndResize\" manipulations can be undone.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Undo the last window manipulation. Only those \"moveAndResize\" manipulations can be undone." + ], + "notes" : [ + + ], + "signature" : "WinWin:undo()", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:undo()", + "name" : "undo" + }, + { + "doc" : "Redo the window manipulation. Only those \"moveAndResize\" manipulations can be undone.", + "desc" : "Redo the window manipulation. Only those \"moveAndResize\" manipulations can be undone.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Redo the window manipulation. Only those \"moveAndResize\" manipulations can be undone." + ], + "notes" : [ + + ], + "signature" : "WinWin:redo()", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:redo()", + "name" : "redo" + }, + { + "doc" : "Center the cursor on the focused window.", + "desc" : "Center the cursor on the focused window.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Center the cursor on the focused window." + ], + "notes" : [ + + ], + "signature" : "WinWin:centerCursor()", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:centerCursor()", + "name" : "centerCursor" + } + ], + "Field" : [ + + ], + "items" : [ + { + "doc" : "An integer specifying how many gridparts the screen should be divided into. Defaults to 30.", + "desc" : "An integer specifying how many gridparts the screen should be divided into. Defaults to 30.", + "parameters" : [ + + ], + "stripped_doc" : [ + "An integer specifying how many gridparts the screen should be divided into. Defaults to 30." + ], + "notes" : [ + + ], + "signature" : "WinWin.gridparts", + "type" : "Variable", + "returns" : [ + + ], + "def" : "WinWin.gridparts", + "name" : "gridparts" + }, + { + "doc" : "Center the cursor on the focused window.", + "desc" : "Center the cursor on the focused window.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Center the cursor on the focused window." + ], + "notes" : [ + + ], + "signature" : "WinWin:centerCursor()", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:centerCursor()", + "name" : "centerCursor" + }, + { + "doc" : "Move and resize the focused window.\n\nParameters:\n * option - A string specifying the option, valid strings are: `halfleft`, `halfright`, `halfup`, `halfdown`, `cornerNW`, `cornerSW`, `cornerNE`, `cornerSE`, `center`, `fullscreen`, `expand`, `shrink`.", + "desc" : "Move and resize the focused window.", + "parameters" : [ + " * option - A string specifying the option, valid strings are: `halfleft`, `halfright`, `halfup`, `halfdown`, `cornerNW`, `cornerSW`, `cornerNE`, `cornerSE`, `center`, `fullscreen`, `expand`, `shrink`." + ], + "stripped_doc" : [ + "Move and resize the focused window.", + "" + ], + "notes" : [ + + ], + "signature" : "WinWin:moveAndResize(option)", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:moveAndResize(option)", + "name" : "moveAndResize" + }, + { + "doc" : "Move the focused window between all of the screens in the `direction`.\n\nParameters:\n * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`, `next`.", + "desc" : "Move the focused window between all of the screens in the `direction`.", + "parameters" : [ + " * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`, `next`." + ], + "stripped_doc" : [ + "Move the focused window between all of the screens in the `direction`.", + "" + ], + "notes" : [ + + ], + "signature" : "WinWin:moveToScreen(direction)", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:moveToScreen(direction)", + "name" : "moveToScreen" + }, + { + "doc" : "Redo the window manipulation. Only those \"moveAndResize\" manipulations can be undone.", + "desc" : "Redo the window manipulation. Only those \"moveAndResize\" manipulations can be undone.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Redo the window manipulation. Only those \"moveAndResize\" manipulations can be undone." + ], + "notes" : [ + + ], + "signature" : "WinWin:redo()", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:redo()", + "name" : "redo" + }, + { + "doc" : "Stash current windows's position and size.", + "desc" : "Stash current windows's position and size.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Stash current windows's position and size." + ], + "notes" : [ + + ], + "signature" : "WinWin:stash()", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:stash()", + "name" : "stash" + }, + { + "doc" : "Move the focused window in the `direction` by on step. The step scale equals to the width\/height of one gridpart.\n\nParameters:\n * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`.", + "desc" : "Move the focused window in the `direction` by on step. The step scale equals to the width\/height of one gridpart.", + "parameters" : [ + " * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`." + ], + "stripped_doc" : [ + "Move the focused window in the `direction` by on step. The step scale equals to the width\/height of one gridpart.", + "" + ], + "notes" : [ + + ], + "signature" : "WinWin:stepMove(direction)", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:stepMove(direction)", + "name" : "stepMove" + }, + { + "doc" : "Resize the focused window in the `direction` by on step.\n\nParameters:\n * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`.", + "desc" : "Resize the focused window in the `direction` by on step.", + "parameters" : [ + " * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`." + ], + "stripped_doc" : [ + "Resize the focused window in the `direction` by on step.", + "" + ], + "notes" : [ + + ], + "signature" : "WinWin:stepResize(direction)", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:stepResize(direction)", + "name" : "stepResize" + }, + { + "doc" : "Undo the last window manipulation. Only those \"moveAndResize\" manipulations can be undone.", + "desc" : "Undo the last window manipulation. Only those \"moveAndResize\" manipulations can be undone.", + "parameters" : [ + + ], + "stripped_doc" : [ + "Undo the last window manipulation. Only those \"moveAndResize\" manipulations can be undone." + ], + "notes" : [ + + ], + "signature" : "WinWin:undo()", + "type" : "Method", + "returns" : [ + + ], + "def" : "WinWin:undo()", + "name" : "undo" + } + ], + "Command" : [ + + ], + "name" : "WinWin" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/WinWin.spoon/init.lua b/hammerspoon/Spoons/WinWin.spoon/init.lua new file mode 100644 index 0000000..ef7204c --- /dev/null +++ b/hammerspoon/Spoons/WinWin.spoon/init.lua @@ -0,0 +1,278 @@ +--- === WinWin === +--- +--- Windows manipulation +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/WinWin.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/WinWin.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "WinWin" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +-- Windows manipulation history. Only the last operation is stored. +obj.history = {} + +--- WinWin.gridparts +--- Variable +--- An integer specifying how many gridparts the screen should be divided into. Defaults to 30. +obj.gridparts = 30 + +--- WinWin:stepMove(direction) +--- Method +--- Move the focused window in the `direction` by on step. The step scale equals to the width/height of one gridpart. +--- +--- Parameters: +--- * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`. +function obj:stepMove(direction) + local cwin = hs.window.focusedWindow() + if cwin then + local cscreen = cwin:screen() + local cres = cscreen:fullFrame() + local stepw = cres.w/obj.gridparts + local steph = cres.h/obj.gridparts + local wtopleft = cwin:topLeft() + if direction == "left" then + cwin:setTopLeft({x=wtopleft.x-stepw, y=wtopleft.y}) + elseif direction == "right" then + cwin:setTopLeft({x=wtopleft.x+stepw, y=wtopleft.y}) + elseif direction == "up" then + cwin:setTopLeft({x=wtopleft.x, y=wtopleft.y-steph}) + elseif direction == "down" then + cwin:setTopLeft({x=wtopleft.x, y=wtopleft.y+steph}) + end + else + hs.alert.show("No focused window!") + end +end + +--- WinWin:stepResize(direction) +--- Method +--- Resize the focused window in the `direction` by on step. +--- +--- Parameters: +--- * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`. +function obj:stepResize(direction) + local cwin = hs.window.focusedWindow() + if cwin then + local cscreen = cwin:screen() + local cres = cscreen:fullFrame() + local stepw = cres.w/obj.gridparts + local steph = cres.h/obj.gridparts + local wsize = cwin:size() + if direction == "left" then + cwin:setSize({w=wsize.w-stepw, h=wsize.h}) + elseif direction == "right" then + cwin:setSize({w=wsize.w+stepw, h=wsize.h}) + elseif direction == "up" then + cwin:setSize({w=wsize.w, h=wsize.h-steph}) + elseif direction == "down" then + cwin:setSize({w=wsize.w, h=wsize.h+steph}) + end + else + hs.alert.show("No focused window!") + end +end + +--- WinWin:stash() +--- Method +--- Stash current windows's position and size. +--- + +local function isInHistory(windowid) + for idx,val in ipairs(obj.history) do + if val[1] == windowid then + return idx + end + end + return false +end + +function obj:stash() + local cwin = hs.window.focusedWindow() + local winid = cwin:id() + local winf = cwin:frame() + local id_idx = isInHistory(winid) + if id_idx then + -- Bring recently used window id up, so they wouldn't get removed because of exceeding capacity + if id_idx == 100 then + local tmptable = obj.history[id_idx] + table.remove(obj.history, id_idx) + table.insert(obj.history, 1, tmptable) + -- Make sure the history for each application doesn't reach the maximum (100 items) + local id_history = obj.history[1][2] + if #id_history > 100 then table.remove(id_history) end + table.insert(id_history, 1, winf) + else + local id_history = obj.history[id_idx][2] + if #id_history > 100 then table.remove(id_history) end + table.insert(id_history, 1, winf) + end + else + -- Make sure the history of window id doesn't reach the maximum (100 items). + if #obj.history > 100 then table.remove(obj.history) end + -- Stash new window id and its first history + local newtable = {winid, {winf}} + table.insert(obj.history, 1, newtable) + end +end + +--- WinWin:moveAndResize(option) +--- Method +--- Move and resize the focused window. +--- +--- Parameters: +--- * option - A string specifying the option, valid strings are: `halfleft`, `halfright`, `halfup`, `halfdown`, `cornerNW`, `cornerSW`, `cornerNE`, `cornerSE`, `center`, `fullscreen`, `expand`, `shrink`. + +function obj:moveAndResize(option) + local cwin = hs.window.focusedWindow() + if cwin then + local cscreen = cwin:screen() + local cres = cscreen:fullFrame() + local stepw = cres.w/obj.gridparts + local steph = cres.h/obj.gridparts + local wf = cwin:frame() + if option == "halfleft" then + cwin:setFrame({x=cres.x, y=cres.y, w=cres.w/2, h=cres.h}) + elseif option == "halfright" then + cwin:setFrame({x=cres.x+cres.w/2, y=cres.y, w=cres.w/2, h=cres.h}) + elseif option == "halfup" then + cwin:setFrame({x=cres.x, y=cres.y, w=cres.w, h=cres.h/2}) + elseif option == "halfdown" then + cwin:setFrame({x=cres.x, y=cres.y+cres.h/2, w=cres.w, h=cres.h/2}) + elseif option == "cornerNW" then + cwin:setFrame({x=cres.x, y=cres.y, w=cres.w/2, h=cres.h/2}) + elseif option == "cornerNE" then + cwin:setFrame({x=cres.x+cres.w/2, y=cres.y, w=cres.w/2, h=cres.h/2}) + elseif option == "cornerSW" then + cwin:setFrame({x=cres.x, y=cres.y+cres.h/2, w=cres.w/2, h=cres.h/2}) + elseif option == "cornerSE" then + cwin:setFrame({x=cres.x+cres.w/2, y=cres.y+cres.h/2, w=cres.w/2, h=cres.h/2}) + elseif option == "fullscreen" then + cwin:setFrame({x=cres.x, y=cres.y, w=cres.w, h=cres.h}) + elseif option == "center" then + cwin:centerOnScreen() + elseif option == "expand" then + cwin:setFrame({x=wf.x-stepw, y=wf.y-steph, w=wf.w+(stepw*2), h=wf.h+(steph*2)}) + elseif option == "shrink" then + cwin:setFrame({x=wf.x+stepw, y=wf.y+steph, w=wf.w-(stepw*2), h=wf.h-(steph*2)}) + end + else + hs.alert.show("No focused window!") + end +end + +--- WinWin:moveToScreen(direction) +--- Method +--- Move the focused window between all of the screens in the `direction`. +--- +--- Parameters: +--- * direction - A string specifying the direction, valid strings are: `left`, `right`, `up`, `down`, `next`. +function obj:moveToScreen(direction) + local cwin = hs.window.focusedWindow() + if cwin then + local cscreen = cwin:screen() + if direction == "up" then + cwin:moveOneScreenNorth() + elseif direction == "down" then + cwin:moveOneScreenSouth() + elseif direction == "left" then + cwin:moveOneScreenWest() + elseif direction == "right" then + cwin:moveOneScreenEast() + elseif direction == "next" then + cwin:moveToScreen(cscreen:next()) + end + else + hs.alert.show("No focused window!") + end +end + +--- WinWin:undo() +--- Method +--- Undo the last window manipulation. Only those "moveAndResize" manipulations can be undone. +--- + +function obj:undo() + local cwin = hs.window.focusedWindow() + local winid = cwin:id() + -- Has this window been stored previously? + local id_idx = isInHistory(winid) + if id_idx then + -- Bring recently used window id up, so they wouldn't get removed because of exceeding capacity + if id_idx == 100 then + local tmptable = obj.history[id_idx] + table.remove(obj.history, id_idx) + table.insert(obj.history, 1, tmptable) + local id_history = obj.history[1][2] + cwin:setFrame(id_history[1]) + -- Rewind the history + local tmpframe = id_history[1] + table.remove(id_history, 1) + table.insert(id_history, tmpframe) + else + local id_history = obj.history[id_idx][2] + cwin:setFrame(id_history[1]) + local tmpframe = id_history[1] + table.remove(id_history, 1) + table.insert(id_history, tmpframe) + end + end +end + +--- WinWin:redo() +--- Method +--- Redo the window manipulation. Only those "moveAndResize" manipulations can be undone. +--- + +function obj:redo() + local cwin = hs.window.focusedWindow() + local winid = cwin:id() + -- Has this window been stored previously? + local id_idx = isInHistory(winid) + if id_idx then + -- Bring recently used window id up, so they wouldn't get removed because of exceeding capacity + if id_idx == 100 then + local tmptable = obj.history[id_idx] + table.remove(obj.history, id_idx) + table.insert(obj.history, 1, tmptable) + local id_history = obj.history[1][2] + cwin:setFrame(id_history[#id_history]) + -- Play the history + local tmpframe = id_history[#id_history] + table.remove(id_history) + table.insert(id_history, 1, tmpframe) + else + local id_history = obj.history[id_idx][2] + cwin:setFrame(id_history[#id_history]) + local tmpframe = id_history[#id_history] + table.remove(id_history) + table.insert(id_history, 1, tmpframe) + end + end +end + +--- WinWin:centerCursor() +--- Method +--- Center the cursor on the focused window. +--- + +function obj:centerCursor() + local cwin = hs.window.focusedWindow() + local wf = cwin:frame() + local cscreen = cwin:screen() + local cres = cscreen:fullFrame() + if cwin then + -- Center the cursor one the focused window + hs.mouse.setAbsolutePosition({x=wf.x+wf.w/2, y=wf.y+wf.h/2}) + else + -- Center the cursor on the screen + hs.mouse.setAbsolutePosition({x=cres.x+cres.w/2, y=cres.y+cres.h/2}) + end +end + +return obj diff --git a/hammerspoon/autoscript.lua b/hammerspoon/autoscript.lua new file mode 100644 index 0000000..6db4fcb --- /dev/null +++ b/hammerspoon/autoscript.lua @@ -0,0 +1,25 @@ +log = hs.logger.new('autoscript', 'debug') +local cmdArr = { + "cd /Users/einverne/Sync/wiki/ && /bin/bash auto-push.sh", +} + +function shell(cmd) + hs.alert.show("execute") + log.i('execute') + output, status, t, rc = hs.execute(string.format("%s", cmd), true) + -- result, output, err = hs.osascript.applescript(string.format('do shell script "%s"', cmd)) + log.i(output) + log.i(status) + log.i(t) + log.i(rc) +end + +function runAutoScripts() + for key, cmd in ipairs(cmdArr) do + shell(cmd) + end +end + + +myTimer = hs.timer.doEvery(1800, runAutoScripts) +myTimer:start() diff --git a/hammerspoon/config-example.lua b/hammerspoon/config-example.lua new file mode 100644 index 0000000..14c25a1 --- /dev/null +++ b/hammerspoon/config-example.lua @@ -0,0 +1,95 @@ +-- Specify Spoons which will be loaded +hspoon_list = { + "AClock", + "BingDaily", + -- "Calendar", + "CircleClock", + "ClipShow", + "CountDown", + "FnMate", + "HCalendar", + "HSaria2", + "HSearch", + -- "KSheet", + "SpeedMenu", + -- "TimeFlow", + -- "UnsplashZ", + "WinWin", +} + +-- appM environment keybindings. Bundle `id` is prefered, but application `name` will be ok. +hsapp_list = { + {key = 'a', name = 'Atom'}, + {key = 'c', id = 'com.google.Chrome'}, + {key = 'd', name = 'ShadowsocksX'}, + {key = 'e', name = 'Emacs'}, + {key = 'f', name = 'Finder'}, + {key = 'i', name = 'iTerm'}, + {key = 'k', name = 'KeyCastr'}, + {key = 'l', name = 'Sublime Text'}, + {key = 'm', name = 'MacVim'}, + {key = 'o', name = 'LibreOffice'}, + {key = 'p', name = 'mpv'}, + {key = 'r', name = 'VimR'}, + {key = 's', name = 'Safari'}, + {key = 't', name = 'Terminal'}, + {key = 'v', id = 'com.apple.ActivityMonitor'}, + {key = 'w', name = 'Mweb'}, + {key = 'y', id = 'com.apple.systempreferences'}, +} + +-- Modal supervisor keybinding, which can be used to temporarily disable ALL modal environments. +hsupervisor_keys = {{"cmd", "shift", "ctrl"}, "Q"} + +-- Reload Hammerspoon configuration +hsreload_keys = {{"cmd", "shift", "ctrl"}, "R"} + +-- Toggle help panel of this configuration. +hshelp_keys = {{"alt", "shift"}, "/"} + +-- aria2 RPC host address +hsaria2_host = "http://localhost:6800/jsonrpc" +-- aria2 RPC host secret +hsaria2_secret = "token" + +---------------------------------------------------------------------------------------------------- +-- Those keybindings below could be disabled by setting to {"", ""} or {{}, ""} + +-- Window hints keybinding: Focuse to any window you want +hswhints_keys = {"alt", "tab"} + +-- appM environment keybinding: Application Launcher +hsappM_keys = {"alt", "A"} + +-- clipshowM environment keybinding: System clipboard reader +hsclipsM_keys = {"alt", "C"} + +-- Toggle the display of aria2 frontend +hsaria2_keys = {"alt", "D"} + +-- Launch Hammerspoon Search +hsearch_keys = {"alt", "G"} + +-- Read Hammerspoon and Spoons API manual in default browser +hsman_keys = {"alt", "H"} + +-- countdownM environment keybinding: Visual countdown +hscountdM_keys = {"alt", "I"} + +-- Lock computer's screen +hslock_keys = {"alt", "L"} + +-- resizeM environment keybinding: Windows manipulation +hsresizeM_keys = {"alt", "R"} + +-- cheatsheetM environment keybinding: Cheatsheet copycat +hscheats_keys = {"alt", "S"} + +-- Show digital clock above all windows +hsaclock_keys = {"alt", "T"} + +-- Type the URL and title of the frontmost web page open in Google Chrome or Safari. +hstype_keys = {"alt", "V"} + +-- Toggle Hammerspoon console +hsconsole_keys = {"alt", "Z"} diff --git a/hammerspoon/ime.lua b/hammerspoon/ime.lua new file mode 100644 index 0000000..15504a4 --- /dev/null +++ b/hammerspoon/ime.lua @@ -0,0 +1,72 @@ +local function Chinese() + hs.keycodes.currentSourceID("im.rime.inputmethod.Squirrel.Rime") +end + +local function English() + hs.keycodes.currentSourceID("com.apple.keylayout.ABC") +end + +-- app to expected ime config +local app2Ime = { + {'/System/Library/CoreServices/Finder.app', 'English'}, + {'/Applications/Alfred 4.app', 'English'}, + {'/Applications/Bitwarden.app', 'English'}, + -- {'/Applications/iTerm.app', 'English'}, + {'/Applications/Xcode.app', 'English'}, + {'/Applications/GoldenDict.app', 'English'}, + {'/Applications/Google Chrome.app', 'Chinese'}, + {'/Applications/DingTalk.app', 'Chinese'}, + {'/Applications/Kindle.app', 'English'}, + {'/Applications/NeteaseMusic.app', 'Chinese'}, + {'/Applications/WeChat.app', 'Chinese'}, + {'/Applications/Lark.app', 'Chinese'}, + {'/Applications/System Preferences.app', 'English'}, + {'/Applications/Dash.app', 'English'}, + {'/Applications/MindNode.app', 'Chinese'}, + {'/Applications/Preview.app', 'Chinese'}, + {'/Applications/Obsidian.app', 'Chinese'}, + {'/Applications/wechatwebdevtools.app', 'English'}, + {'/Applications/Sketch.app', 'English'}, + {'/Users/einverne/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/201.8538.31/IntelliJ IDEA.app', 'Chinese'}, +} + +function updateFocusAppInputMethod() + local focusAppPath = hs.window.frontmostWindow():application():path() + -- hs.alert.show(focusAppPath) + for index, app in pairs(app2Ime) do + local appPath = app[1] + local expectedIme = app[2] + + if focusAppPath == appPath then + if expectedIme == 'English' then + English() + else + Chinese() + end + break + end + end +end + +-- helper hotkey to figure out the app path and name of current focused window +hs.hotkey.bind({'ctrl', 'cmd'}, ".", function() + hs.alert.show("App path: " + ..hs.window.focusedWindow():application():path() + .."\n" + .."App name: " + ..hs.window.focusedWindow():application():name() + .."\n" + .."IM source id: " + ..hs.keycodes.currentSourceID()) +end) + +-- Handle cursor focus and application's screen manage. +function applicationWatcher(appName, eventType, appObject) + if (eventType == hs.application.watcher.activated) then + updateFocusAppInputMethod() + end +end + +appWatcher = hs.application.watcher.new(applicationWatcher) +appWatcher:start() + diff --git a/hammerspoon/init.lua b/hammerspoon/init.lua new file mode 100644 index 0000000..dd060e4 --- /dev/null +++ b/hammerspoon/init.lua @@ -0,0 +1,566 @@ +local LOGLEVEL = 'debug' + +log = hs.logger.new('init', 'debug') + +require 'autoscript' +require 'ime' +-- require 'usb' + +-- hs.loadSpoon("ReloadConfiguration") +-- spoon.ReloadConfiguration:start() +-- hs.alert.show("Config reload!") + +-- Hyper key in Sierra +k = hs.hotkey.modal.new({}, "F17") + +-- Enter Hyper Mode when F18 (Hyper/Capslock) is pressed +pressedF18 = function() k:enter() end + +-- Leave Hyper Mode when F18 (Hyper/Capslock) is pressed, +-- send ESCAPE if no other keys are pressed. +releasedF18 = function() k:exit() end + +f18 = hs.hotkey.bind({}, 'F18', pressedF18, releasedF18) + + + +hyper = {"ctrl", "alt", "cmd", "shift"} +function moveWindow(direction) + return function() + local win = hs.window.focusedWindow() + local app = win:application() + local app_name = app:name() + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + if direction == "left" then + f.x = max.x + f.y = max.y + f.w = (max.w / 2) + f.h = max.h + elseif direction == "right" then + f.x = (max.x + (max.w / 2)) + f.y = max.y + f.w = (max.w / 2) + f.h = max.h + elseif direction == "full" then + f.x = max.x + f.y = max.y + f.w = max.w + f.h = max.h + elseif direction == "up" then + f.x = max.x + f.y = max.y + f.w = max.w + f.h = (max.h / 2) + elseif direction == "down" then + f.x = max.x + f.y = max.y + (max.h / 2) + f.w = max.w + f.h = max.h / 2 + elseif direction == "normal" then + f.x = (max.x + (max.w / 8)) + 6 + f.y = max.y + f.w = (max.w * 3 / 4) - 12 + f.h = max.h + end + win:setFrame(f, 0.0) + end +end +hs.hotkey.bind(hyper, "H", moveWindow("left")) +hs.hotkey.bind(hyper, "L", moveWindow("right")) +hs.hotkey.bind(hyper, "K", moveWindow("up")) +hs.hotkey.bind(hyper, "J", moveWindow("down")) +hs.hotkey.bind(hyper, "F", moveWindow("full")) + +-- hs.hotkey.bind(hyper, "C", function() hs.application.launchOrFocus("Google Chrome") end) +function moveWindowToDisplay(d) + return function() + local displays = hs.screen.allScreens() + log.i(displays) + local win = hs.window.focusedWindow() + win:moveToScreen(displays[d], false, true) + end +end + +-- hs.hotkey.bind(hyper, "m", moveWindowToDisplay(1)) +-- hs.hotkey.bind(hyper, "8", moveWindowToDisplay(2)) +-- hs.hotkey.bind(hyper, "9", moveWindowToDisplay(3)) + +function movieWinBetweenMonitors(d) + return function() + local win = hs.window.focusedWindow() + -- get the screen where the focused window is displayed, a.k.a current screen + local screen = win:screen() + -- compute the unitRect of the focused window relative to the current screen + -- and move the window to the next screen setting the same unitRect + -- https://www.hammerspoon.org/docs/hs.window.html#move + if d == 'next' then + win:move(win:frame():toUnitRect(screen:frame()), screen:next(), true, 0) + elseif d == 'previous' then + win:move(win:frame():toUnitRect(screen:frame()), screen:previous(), true, 0) + end + end +end + +hs.hotkey.bind(hyper, 'N', movieWinBetweenMonitors('next')) +hs.hotkey.bind(hyper, 'P', movieWinBetweenMonitors('previous')) + + + +local grid = require 'hs.grid' +hs.hotkey.bind(hyper, ",", grid.show) + +middle_monitor="DELL U2414H" +left_monitor="DELL P2417H" +right_monitor="DELL U2412M" + +local reading_layout = { + {"Google Chrome", nil, middle_monitor, hs.layout.maximized, nil, nil}, + {"iTerm", nil, left_monitor, hs.layout.maximized, nil, nil}, + {"Miwork", nil, right_monitor, hs.layout.top50, nil, nil}, +} + +hs.hotkey.bind(hyper, "1", function() + hs.application.launchOrFocus('Google Chrome') + hs.application.launchOrFocus('iTerm') + hs.application.launchOrFocus('Miwork') + + hs.layout.apply(reading_layout) +end) + +local coding_layout = { + {"IntelliJ IDEA Ultimate", nil, middle_monitor, hs.layout.maximized, nil, nil}, + {"iTerm", nil, left_monitor, hs.layout.maximized, nil, nil}, +} + +hs.hotkey.bind(hyper, "2", function() + hs.application.launchOrFocus('IntelliJ IDEA') + + hs.layout.apply(coding_layout) +end) + + +wifiWatcher = nil +homeSSID = "EinVerne_5G" +lastSSID = hs.wifi.currentNetwork() + +workSSID = "MIOffice-5G" + +function ssidChangedCallback() + newSSID = hs.wifi.currentNetwork() + + if newSSID == homeSSID and lastSSID ~= homeSSID then + -- We just joined our home WiFi network + hs.audiodevice.defaultOutputDevice():setVolume(25) + hs.alert.show("Welcome home!") + -- result = hs.network.configuration:setLocation("Home") + -- hs.alert.show(result) + elseif newSSID ~= homeSSID and lastSSID == homeSSID then + -- We just departed our home WiFi network + hs.audiodevice.defaultOutputDevice():setVolume(0) + hs.alert.show("left home!") + -- result = hs.network.configuration:setLocation("Automatic") + -- hs.alert.show(result) + end + + if newSSID == workSSID then + hs.alert.show("work karabiner setup") + hs.execute("'/Library/Application Support/org.pqrs/Karabiner-Elements/bin/karabiner_cli' --select-profile Work") + else + hs.alert.show("built-in karabiner setup") + hs.execute("'/Library/Application Support/org.pqrs/Karabiner-Elements/bin/karabiner_cli' --select-profile Built-in") + end + + lastSSID = newSSID +end + +wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback) +wifiWatcher:start() + +networkConf = hs.network.configuration.open() +location = networkConf:location() + +hs.hotkey.bind({"cmd", "alt", "ctrl"}, "H", function() + hs.execute('/usr/sbin/networksetup -switchtolocation Home') +end) +hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() + hs.execute('/usr/sbin/networksetup -switchtolocation Automatic') +end) + + + +hs.hotkey.alertDuration = 0 +hs.hints.showTitleThresh = 0 +hs.window.animationDuration = 0 + +-- Use the standardized config location, if present +custom_config = hs.fs.pathToAbsolute(os.getenv("HOME") .. '/.config/hammerspoon/private/config.lua') +if custom_config then + print("Loading custom config") + dofile( os.getenv("HOME") .. "/.config/hammerspoon/private/config.lua") + privatepath = hs.fs.pathToAbsolute(hs.configdir .. '/private/config.lua') + if privatepath then + hs.alert("You have config in both .config/hammerspoon and .hammerspoon/private.\nThe .config/hammerspoon one will be used.") + end +else + -- otherwise fallback to 'classic' location. + if not privatepath then + privatepath = hs.fs.pathToAbsolute(hs.configdir .. '/private') + -- Create `~/.hammerspoon/private` directory if not exists. + hs.fs.mkdir(hs.configdir .. '/private') + end + privateconf = hs.fs.pathToAbsolute(hs.configdir .. '/private/config.lua') + if privateconf then + -- Load awesomeconfig file if exists + require('private/config') + end +end + +hsreload_keys = {hyper, "R"} +hsreload_keys = hsreload_keys or {{"cmd", "shift", "ctrl"}, "R"} +if string.len(hsreload_keys[2]) > 0 then + hs.hotkey.bind(hsreload_keys[1], hsreload_keys[2], "Reload Configuration", function() hs.reload() end) + hs.notify.new({title="Hammerspoon config reloaded", informativeText="Manually trigged via keyboard shortcut"}):send() +end + +-- ModalMgr Spoon must be loaded explicitly, because this repository heavily relies upon it. +hs.loadSpoon("ModalMgr") + +-- Define default Spoons which will be loaded later +if not hspoon_list then + hspoon_list = { + "AClock", + "BingDaily", + "CircleClock", + "ClipShow", + "CountDown", + "HCalendar", + --"HSaria2", + "SpeedMenu", + "WinWin", + } +end + +-- Load those Spoons +for _, v in pairs(hspoon_list) do + hs.loadSpoon(v) +end + +---------------------------------------------------------------------------------------------------- +-- Then we create/register all kinds of modal keybindings environments. +---------------------------------------------------------------------------------------------------- +-- Register windowHints (Register a keybinding which is NOT modal environment with modal supervisor) +hswhints_keys = hswhints_keys or {"alt", "tab"} +if string.len(hswhints_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hswhints_keys[1], hswhints_keys[2], 'Show Window Hints', function() + spoon.ModalMgr:deactivateAll() + hs.hints.windowHints() + end) +end + +---------------------------------------------------------------------------------------------------- +-- appM modal environment +spoon.ModalMgr:new("appM") +local cmodal = spoon.ModalMgr.modal_list["appM"] +cmodal:bind('', 'escape', 'Deactivate appM', function() spoon.ModalMgr:deactivate({"appM"}) end) +cmodal:bind('', 'Q', 'Deactivate appM', function() spoon.ModalMgr:deactivate({"appM"}) end) +cmodal:bind('', 'tab', 'Toggle Cheatsheet', function() spoon.ModalMgr:toggleCheatsheet() end) +if not hsapp_list then + hsapp_list = { + {key = 'f', name = 'Finder'}, + {key = 'c', name = 'Google Chrome'}, + {key = 's', name = 'Safari'}, + {key = 't', name = 'Terminal'}, + {key = 'v', id = 'com.apple.ActivityMonitor'}, + {key = 'y', id = 'com.apple.systempreferences'}, + } +end +for _, v in ipairs(hsapp_list) do + if v.id then + local located_name = hs.application.nameForBundleID(v.id) + if located_name then + cmodal:bind('', v.key, located_name, function() + hs.application.launchOrFocusByBundleID(v.id) + spoon.ModalMgr:deactivate({"appM"}) + end) + end + elseif v.name then + cmodal:bind('', v.key, v.name, function() + hs.application.launchOrFocus(v.name) + spoon.ModalMgr:deactivate({"appM"}) + end) + end +end + +-- Then we register some keybindings with modal supervisor +hsappM_keys = hsappM_keys or {"alt", "A"} +if string.len(hsappM_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hsappM_keys[1], hsappM_keys[2], "Enter AppM Environment", function() + spoon.ModalMgr:deactivateAll() + -- Show the keybindings cheatsheet once appM is activated + spoon.ModalMgr:activate({"appM"}, "#FFBD2E", true) + end) +end + +---------------------------------------------------------------------------------------------------- +-- clipshowM modal environment +if spoon.ClipShow then + spoon.ModalMgr:new("clipshowM") + local cmodal = spoon.ModalMgr.modal_list["clipshowM"] + cmodal:bind('', 'escape', 'Deactivate clipshowM', function() + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + cmodal:bind('', 'Q', 'Deactivate clipshowM', function() + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + cmodal:bind('', 'N', 'Save this Session', function() + spoon.ClipShow:saveToSession() + end) + cmodal:bind('', 'R', 'Restore last Session', function() + spoon.ClipShow:restoreLastSession() + end) + cmodal:bind('', 'B', 'Open in Browser', function() + spoon.ClipShow:openInBrowserWithRef() + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + cmodal:bind('', 'S', 'Search with Bing', function() + spoon.ClipShow:openInBrowserWithRef("https://www.bing.com/search?q=") + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + cmodal:bind('', 'M', 'Open in MacVim', function() + spoon.ClipShow:openWithCommand("/usr/local/bin/mvim") + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + cmodal:bind('', 'F', 'Save to Desktop', function() + spoon.ClipShow:saveToFile() + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + cmodal:bind('', 'H', 'Search in Github', function() + spoon.ClipShow:openInBrowserWithRef("https://github.com/search?q=") + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + cmodal:bind('', 'G', 'Search with Google', function() + spoon.ClipShow:openInBrowserWithRef("https://www.google.com/search?q=") + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + cmodal:bind('', 'L', 'Open in Sublime Text', function() + spoon.ClipShow:openWithCommand("/usr/local/bin/subl") + spoon.ClipShow:toggleShow() + spoon.ModalMgr:deactivate({"clipshowM"}) + end) + + -- Register clipshowM with modal supervisor + hsclipsM_keys = hsclipsM_keys or {"alt", "C"} + if string.len(hsclipsM_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hsclipsM_keys[1], hsclipsM_keys[2], "Enter clipshowM Environment", function() + -- We need to take action upon hsclipsM_keys is pressed, since pressing another key to showing ClipShow panel is redundant. + spoon.ClipShow:toggleShow() + -- Need a little trick here. Since the content type of system clipboard may be "URL", in which case we don't need to activate clipshowM. + if spoon.ClipShow.canvas:isShowing() then + spoon.ModalMgr:deactivateAll() + spoon.ModalMgr:activate({"clipshowM"}) + end + end) + end +end + +---------------------------------------------------------------------------------------------------- +-- Register HSaria2 +if spoon.HSaria2 then + -- First we need to connect to aria2 rpc host + hsaria2_host = hsaria2_host or "http://localhost:6800/jsonrpc" + hsaria2_secret = hsaria2_secret or "token" + spoon.HSaria2:connectToHost(hsaria2_host, hsaria2_secret) + + hsaria2_keys = hsaria2_keys or {"alt", "D"} + if string.len(hsaria2_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hsaria2_keys[1], hsaria2_keys[2], 'Toggle aria2 Panel', function() spoon.HSaria2:togglePanel() end) + end +end + +---------------------------------------------------------------------------------------------------- +-- Register Hammerspoon Search +if spoon.HSearch then + hsearch_keys = hsearch_keys or {"alt", "G"} + if string.len(hsearch_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hsearch_keys[1], hsearch_keys[2], 'Launch Hammerspoon Search', function() spoon.HSearch:toggleShow() end) + end +end + +---------------------------------------------------------------------------------------------------- +-- Register Hammerspoon API manual: Open Hammerspoon manual in default browser +hsman_keys = hsman_keys or {"alt", "H"} +if string.len(hsman_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hsman_keys[1], hsman_keys[2], "Read Hammerspoon Manual", function() + hs.doc.hsdocs.forceExternalBrowser(true) + hs.doc.hsdocs.moduleEntitiesInSidebar(true) + hs.doc.hsdocs.help() + end) +end + +---------------------------------------------------------------------------------------------------- +-- countdownM modal environment +if spoon.CountDown then + spoon.ModalMgr:new("countdownM") + local cmodal = spoon.ModalMgr.modal_list["countdownM"] + cmodal:bind('', 'escape', 'Deactivate countdownM', function() spoon.ModalMgr:deactivate({"countdownM"}) end) + cmodal:bind('', 'Q', 'Deactivate countdownM', function() spoon.ModalMgr:deactivate({"countdownM"}) end) + cmodal:bind('', 'tab', 'Toggle Cheatsheet', function() spoon.ModalMgr:toggleCheatsheet() end) + cmodal:bind('', '0', '5 Minutes Countdown', function() + spoon.CountDown:startFor(5) + spoon.ModalMgr:deactivate({"countdownM"}) + end) + for i = 1, 9 do + cmodal:bind('', tostring(i), string.format("%s Minutes Countdown", 10 * i), function() + spoon.CountDown:startFor(10 * i) + spoon.ModalMgr:deactivate({"countdownM"}) + end) + end + cmodal:bind('', 'return', '25 Minutes Countdown', function() + spoon.CountDown:startFor(25) + spoon.ModalMgr:deactivate({"countdownM"}) + end) + cmodal:bind('', 'space', 'Pause/Resume CountDown', function() + spoon.CountDown:pauseOrResume() + spoon.ModalMgr:deactivate({"countdownM"}) + end) + + -- Register countdownM with modal supervisor + hscountdM_keys = hscountdM_keys or {"alt", "I"} + if string.len(hscountdM_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hscountdM_keys[1], hscountdM_keys[2], "Enter countdownM Environment", function() + spoon.ModalMgr:deactivateAll() + -- Show the keybindings cheatsheet once countdownM is activated + spoon.ModalMgr:activate({"countdownM"}, "#FF6347", true) + end) + end +end + +---------------------------------------------------------------------------------------------------- +-- Register lock screen +hslock_keys = hslock_keys or {"alt", "L"} +if string.len(hslock_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hslock_keys[1], hslock_keys[2], "Lock Screen", function() + hs.caffeinate.lockScreen() + end) +end + +---------------------------------------------------------------------------------------------------- +-- resizeM modal environment +if spoon.WinWin then + spoon.ModalMgr:new("resizeM") + local cmodal = spoon.ModalMgr.modal_list["resizeM"] + cmodal:bind('', 'escape', 'Deactivate resizeM', function() spoon.ModalMgr:deactivate({"resizeM"}) end) + cmodal:bind('', 'Q', 'Deactivate resizeM', function() spoon.ModalMgr:deactivate({"resizeM"}) end) + cmodal:bind('', 'tab', 'Toggle Cheatsheet', function() spoon.ModalMgr:toggleCheatsheet() end) + cmodal:bind('', 'A', 'Move Leftward', function() spoon.WinWin:stepMove("left") end, nil, function() spoon.WinWin:stepMove("left") end) + cmodal:bind('', 'D', 'Move Rightward', function() spoon.WinWin:stepMove("right") end, nil, function() spoon.WinWin:stepMove("right") end) + cmodal:bind('', 'W', 'Move Upward', function() spoon.WinWin:stepMove("up") end, nil, function() spoon.WinWin:stepMove("up") end) + cmodal:bind('', 'S', 'Move Downward', function() spoon.WinWin:stepMove("down") end, nil, function() spoon.WinWin:stepMove("down") end) + cmodal:bind('', 'H', 'Lefthalf of Screen', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("halfleft") end) + cmodal:bind('', 'L', 'Righthalf of Screen', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("halfright") end) + cmodal:bind('', 'K', 'Uphalf of Screen', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("halfup") end) + cmodal:bind('', 'J', 'Downhalf of Screen', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("halfdown") end) + cmodal:bind('', 'Y', 'NorthWest Corner', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("cornerNW") end) + cmodal:bind('', 'O', 'NorthEast Corner', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("cornerNE") end) + cmodal:bind('', 'U', 'SouthWest Corner', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("cornerSW") end) + cmodal:bind('', 'I', 'SouthEast Corner', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("cornerSE") end) + cmodal:bind('', 'F', 'Fullscreen', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("fullscreen") end) + cmodal:bind('', 'C', 'Center Window', function() spoon.WinWin:stash() spoon.WinWin:moveAndResize("center") end) + cmodal:bind('', '=', 'Stretch Outward', function() spoon.WinWin:moveAndResize("expand") end, nil, function() spoon.WinWin:moveAndResize("expand") end) + cmodal:bind('', '-', 'Shrink Inward', function() spoon.WinWin:moveAndResize("shrink") end, nil, function() spoon.WinWin:moveAndResize("shrink") end) + cmodal:bind('shift', 'H', 'Move Leftward', function() spoon.WinWin:stepResize("left") end, nil, function() spoon.WinWin:stepResize("left") end) + cmodal:bind('shift', 'L', 'Move Rightward', function() spoon.WinWin:stepResize("right") end, nil, function() spoon.WinWin:stepResize("right") end) + cmodal:bind('shift', 'K', 'Move Upward', function() spoon.WinWin:stepResize("up") end, nil, function() spoon.WinWin:stepResize("up") end) + cmodal:bind('shift', 'J', 'Move Downward', function() spoon.WinWin:stepResize("down") end, nil, function() spoon.WinWin:stepResize("down") end) + cmodal:bind('', 'left', 'Move to Left Monitor', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("left") end) + cmodal:bind('', 'right', 'Move to Right Monitor', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("right") end) + cmodal:bind('', 'up', 'Move to Above Monitor', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("up") end) + cmodal:bind('', 'down', 'Move to Below Monitor', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("down") end) + cmodal:bind('', 'space', 'Move to Next Monitor', function() spoon.WinWin:stash() spoon.WinWin:moveToScreen("next") end) + cmodal:bind('', '[', 'Undo Window Manipulation', function() spoon.WinWin:undo() end) + cmodal:bind('', ']', 'Redo Window Manipulation', function() spoon.WinWin:redo() end) + cmodal:bind('', '`', 'Center Cursor', function() spoon.WinWin:centerCursor() end) + + -- Register resizeM with modal supervisor + hsresizeM_keys = hsresizeM_keys or {"alt", "R"} + if string.len(hsresizeM_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hsresizeM_keys[1], hsresizeM_keys[2], "Enter resizeM Environment", function() + -- Deactivate some modal environments or not before activating a new one + spoon.ModalMgr:deactivateAll() + -- Show an status indicator so we know we're in some modal environment now + spoon.ModalMgr:activate({"resizeM"}, "#B22222") + end) + end +end + +---------------------------------------------------------------------------------------------------- +-- cheatsheetM modal environment (Because KSheet Spoon is NOT loaded, cheatsheetM will NOT be activated) +if spoon.KSheet then + spoon.ModalMgr:new("cheatsheetM") + local cmodal = spoon.ModalMgr.modal_list["cheatsheetM"] + cmodal:bind('', 'escape', 'Deactivate cheatsheetM', function() + spoon.KSheet:hide() + spoon.ModalMgr:deactivate({"cheatsheetM"}) + end) + cmodal:bind('', 'Q', 'Deactivate cheatsheetM', function() + spoon.KSheet:hide() + spoon.ModalMgr:deactivate({"cheatsheetM"}) + end) + + -- Register cheatsheetM with modal supervisor + hscheats_keys = hscheats_keys or {"alt", "S"} + if string.len(hscheats_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hscheats_keys[1], hscheats_keys[2], "Enter cheatsheetM Environment", function() + spoon.KSheet:show() + spoon.ModalMgr:deactivateAll() + spoon.ModalMgr:activate({"cheatsheetM"}) + end) + end +end + +---------------------------------------------------------------------------------------------------- +-- Register AClock +if spoon.AClock then + hsaclock_keys = hsaclock_keys or {"alt", "T"} + if string.len(hsaclock_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hsaclock_keys[1], hsaclock_keys[2], "Toggle Floating Clock", function() spoon.AClock:toggleShow() end) + end +end + +---------------------------------------------------------------------------------------------------- +-- Register browser tab typist: Type URL of current tab of running browser in markdown format. i.e. [title](link) +hstype_keys = hstype_keys or {"alt", "V"} +if string.len(hstype_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hstype_keys[1], hstype_keys[2], "Type Browser Link", function() + local safari_running = hs.application.applicationsForBundleID("com.apple.Safari") + local chrome_running = hs.application.applicationsForBundleID("com.google.Chrome") + if #safari_running > 0 then + local stat, data = hs.applescript('tell application "Safari" to get {URL, name} of current tab of window 1') + if stat then hs.eventtap.keyStrokes("[" .. data[2] .. "](" .. data[1] .. ")") end + elseif #chrome_running > 0 then + local stat, data = hs.applescript('tell application "Google Chrome" to get {URL, title} of active tab of window 1') + if stat then hs.eventtap.keyStrokes("[" .. data[2] .. "](" .. data[1] .. ")") end + end + end) +end + +---------------------------------------------------------------------------------------------------- +-- Register Hammerspoon console +hsconsole_keys = hsconsole_keys or {"alt", "Z"} +if string.len(hsconsole_keys[2]) > 0 then + spoon.ModalMgr.supervisor:bind(hsconsole_keys[1], hsconsole_keys[2], "Toggle Hammerspoon Console", function() hs.toggleConsole() end) +end + +---------------------------------------------------------------------------------------------------- +-- Finally we initialize ModalMgr supervisor +spoon.ModalMgr.supervisor:enter() diff --git a/hammerspoon/usb.lua b/hammerspoon/usb.lua new file mode 100644 index 0000000..28bb32b --- /dev/null +++ b/hammerspoon/usb.lua @@ -0,0 +1,15 @@ + +log = hs.logger.new('autoscript', 'debug') + +function keyboardCallback(data) +-- if data.eventType == "added" then +-- log.i(data.productName .. data.vendorName .. data.vendorID .. data.productID .. "added") +-- end +-- if data.eventType == "removed" then +-- log.i(data.productName .. data.vendorName .. data.vendorID .. data.productID .. "removed") +-- end + log.i(data) +end + +local keyboardWatcher = hs.usb.watcher.new(keyboardCallback) +keyboardWatcher:start()