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 0000000..ec387da Binary files /dev/null and b/hammerspoon/Spoons/CircleClock.spoon/watchbg.png differ 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 0000000..08e2cc4 Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/chrome.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/emoji.png b/hammerspoon/Spoons/HSearch.spoon/resources/emoji.png new file mode 100644 index 0000000..820f604 Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/emoji.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/justnote.png b/hammerspoon/Spoons/HSearch.spoon/resources/justnote.png new file mode 100644 index 0000000..1bd87e0 Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/justnote.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/menus.png b/hammerspoon/Spoons/HSearch.spoon/resources/menus.png new file mode 100644 index 0000000..b6c6ae5 Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/menus.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/safari.png b/hammerspoon/Spoons/HSearch.spoon/resources/safari.png new file mode 100644 index 0000000..6f493c4 Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/safari.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/tabs.png b/hammerspoon/Spoons/HSearch.spoon/resources/tabs.png new file mode 100644 index 0000000..b514f19 Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/tabs.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/taskkill.png b/hammerspoon/Spoons/HSearch.spoon/resources/taskkill.png new file mode 100644 index 0000000..d2d3a5f Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/taskkill.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/thesaurus.png b/hammerspoon/Spoons/HSearch.spoon/resources/thesaurus.png new file mode 100644 index 0000000..f0c1b7b Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/thesaurus.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/time.png b/hammerspoon/Spoons/HSearch.spoon/resources/time.png new file mode 100644 index 0000000..6ac38c6 Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/time.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/v2ex.png b/hammerspoon/Spoons/HSearch.spoon/resources/v2ex.png new file mode 100644 index 0000000..d657b93 Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/v2ex.png differ diff --git a/hammerspoon/Spoons/HSearch.spoon/resources/youdao.png b/hammerspoon/Spoons/HSearch.spoon/resources/youdao.png new file mode 100644 index 0000000..2e0ef1e Binary files /dev/null and b/hammerspoon/Spoons/HSearch.spoon/resources/youdao.png differ 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 0000000..c34723a Binary files /dev/null and b/hammerspoon/Spoons/TimeFlow.spoon/timebg.png differ 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()