From 7bd9f0d9f201663b583d1468918394c0fc10bb4b Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Sat, 5 Aug 2017 13:44:00 +0100 Subject: [PATCH] Major update to Hammerspoon config - Use Spoons (installed via Makefile targets): - RoundedCorners - HeadphoneAutoPause (with AutoResume patch) - Move windows management into separate file --- hammerspoon/Makefile | 46 ++- .../Spoons/HeadphoneAutoPause.spoon.patch | 35 ++ .../Spoons/HeadphoneAutoPause.spoon/docs.json | 352 ++++++++++++++++++ .../Spoons/HeadphoneAutoPause.spoon/init.lua | 157 ++++++++ .../Spoons/RoundedCorners.spoon/docs.json | 39 ++ .../Spoons/RoundedCorners.spoon/init.lua | 125 +++++++ .../ext/{grid.patch => grid.lua.patch} | 0 hammerspoon/init.lua | 195 ++-------- hammerspoon/window_management.lua | 159 ++++++++ 9 files changed, 941 insertions(+), 167 deletions(-) create mode 100644 hammerspoon/Spoons/HeadphoneAutoPause.spoon.patch create mode 100644 hammerspoon/Spoons/HeadphoneAutoPause.spoon/docs.json create mode 100644 hammerspoon/Spoons/HeadphoneAutoPause.spoon/init.lua create mode 100644 hammerspoon/Spoons/RoundedCorners.spoon/docs.json create mode 100644 hammerspoon/Spoons/RoundedCorners.spoon/init.lua rename hammerspoon/ext/{grid.patch => grid.lua.patch} (100%) create mode 100644 hammerspoon/window_management.lua diff --git a/hammerspoon/Makefile b/hammerspoon/Makefile index b0a1103..4c32f69 100644 --- a/hammerspoon/Makefile +++ b/hammerspoon/Makefile @@ -1,15 +1,51 @@ .SILENT: +install: ext/grid.lua installSpoons +update: update_ext/grid.lua updateSpoons + +# +# Spoons +# + +SPOONS = RoundedCorners \ + HeadphoneAutoPause +SPOONS_DIR = Spoons +SPOON_PATHS = $(foreach s,$(SPOONS),$(shell echo $(SPOONS_DIR)/$(s).spoon)) + +.PHONY: installSpoons +installSpoons: $(SPOON_PATHS) + +.PHONY: removeSpoons +removeSpoons: + $(foreach dir,$(SPOON_PATHS),(test -d "$(dir)" && echo "removing $(dir)" && rm -rf "$(dir)") || exit 0;) + # $(foreach dir,$(SPOON_PATHS),echo "removing $(dir)";) + +.PHONY: updateSpoons +updateSpoons: removeSpoons installSpoons + +$(SPOONS_DIR)/%.spoon: + echo "fetching $@..." && \ + curl -s -L -o "$@.zip" \ + "https://github.com/Hammerspoon/Spoons/raw/master/$@.zip" && \ + unzip -d $(SPOONS_DIR) "$@.zip" && \ + rm "$@.zip" && \ + ( test -f "$@.patch" && patch -p0 < "$@.patch" ) || exit 0 + + +# +# Core extentions and patching +# + ext/grid.lua: - echo "fetching ext/grid.lua..." && \ - curl -s -L -o ext/grid.lua \ - https://raw.githubusercontent.com/Hammerspoon/hammerspoon/master/extensions/grid/init.lua && \ - make patch_ext/grid.lua + echo "fetching $@..." && \ + curl -s -L -o $@ \ + https://raw.githubusercontent.com/Hammerspoon/hammerspoon/master/extensions/grid/init.lua && \ + make patch_$@ .PHONY: patch_ext/grid.lua patch_ext/grid.lua: echo "patching ext/grid.lua..." && \ - patch -p0 < ext/grid.patch + patch -p0 < ext/grid.lua.patch .PHONY: remove_ext/grid.lua remove_ext/grid.lua: diff --git a/hammerspoon/Spoons/HeadphoneAutoPause.spoon.patch b/hammerspoon/Spoons/HeadphoneAutoPause.spoon.patch new file mode 100644 index 0000000..d1d0630 --- /dev/null +++ b/hammerspoon/Spoons/HeadphoneAutoPause.spoon.patch @@ -0,0 +1,35 @@ +--- Spoons/HeadphoneAutoPause.spoon/init.lua 2017-06-30 10:08:18.000000000 +0100 ++++ Spoons/HeadphoneAutoPause.patched.spoon/init.lua 2017-08-05 11:57:44.000000000 +0100 +@@ -37,6 +37,13 @@ obj.control = { + vox = false + } + ++--- HeadphoneAutoPause.autoResume ++--- Variable ++--- Boolean value indicating if music should be automatically resumed when headphones are plugged in again. Only works if music was automatically paused when headphones were unplugged. ++--- ++--- Default value: `true` ++obj.autoResume = true ++ + --- HeadphoneAutoPause.defaultControlFns(app) + --- Method + --- Generate the most common set of application control definition. +@@ -91,11 +98,13 @@ function obj:audiodevwatch(dev_uid, even + if event_name == 'jack' then + if dev:jackConnected() then + self.logger.d("Headphones connected") +- for app, playercontrol in pairs(self.controlfns) do +- if self.control[app] and hs.appfinder.appFromName(playercontrol.appname) and wasplaying[app] then +- self.logger.df("Resuming playback in %s", playercontrol.appname) +- hs.notify.show("Headphones plugged", "Resuming " .. playercontrol.appname .. " playback", "") +- playercontrol.play() ++ if self.autoResume then ++ for app, playercontrol in pairs(self.controlfns) do ++ if self.control[app] and hs.appfinder.appFromName(playercontrol.appname) and wasplaying[app] then ++ self.logger.df("Resuming playback in %s", playercontrol.appname) ++ hs.notify.show("Headphones plugged", "Resuming " .. playercontrol.appname .. " playback", "") ++ playercontrol.play() ++ end + end + end + else diff --git a/hammerspoon/Spoons/HeadphoneAutoPause.spoon/docs.json b/hammerspoon/Spoons/HeadphoneAutoPause.spoon/docs.json new file mode 100644 index 0000000..c223caf --- /dev/null +++ b/hammerspoon/Spoons/HeadphoneAutoPause.spoon/docs.json @@ -0,0 +1,352 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + { + "doc" : "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.", + "stripped_doc" : [ + "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon." + ], + "desc" : "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause.logger", + "type" : "Variable", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause.logger", + "name" : "logger" + }, + { + "doc" : "Table containing one key per application, with the value indicating whether HeadphoneAutoPause should try to pause\/unpause that application in response to the headphone being plugged\/unplugged. The key name must ideally correspond to the name of the corresponding `hs.*` module. Default value:\n```\n{\n itunes = true,\n spotify = true,\n deezer = true,\n vox = false -- Vox has built-in headphone detection support\n}\n```", + "stripped_doc" : [ + "Table containing one key per application, with the value indicating whether HeadphoneAutoPause should try to pause\/unpause that application in response to the headphone being plugged\/unplugged. The key name must ideally correspond to the name of the corresponding `hs.*` module. Default value:", + "```", + "{", + " itunes = true,", + " spotify = true,", + " deezer = true,", + " vox = false -- Vox has built-in headphone detection support", + "}", + "```" + ], + "desc" : "Table containing one key per application, with the value indicating whether HeadphoneAutoPause should try to pause\/unpause that application in response to the headphone being plugged\/unplugged. The key name must ideally correspond to the name of the corresponding `hs.*` module. Default value:", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause.control", + "type" : "Variable", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause.control", + "name" : "control" + }, + { + "doc" : "Table containing control functions for each application to control.\nThe keys must correspond to the values in `HeadphoneAutoPause.control`, and the value is a table with the following elements:\n * `appname` - application name (case-sensitive, as the application appears to the system)\n * `isPlaying` - function that returns a true value if the application is playing\n * `play` - function that starts playback in the application\n * `pause` - function that pauses playback in the application\n\nThe default value includes definitions for iTunes, Spotify, Deezer and Vox, using the corresponding functions from `hs.itunes`, `hs.spotify`, `hs.deezer` and `hs.vox`, respectively.", + "stripped_doc" : [ + "Table containing control functions for each application to control.", + "The keys must correspond to the values in `HeadphoneAutoPause.control`, and the value is a table with the following elements:", + " * `appname` - application name (case-sensitive, as the application appears to the system)", + " * `isPlaying` - function that returns a true value if the application is playing", + " * `play` - function that starts playback in the application", + " * `pause` - function that pauses playback in the application", + "", + "The default value includes definitions for iTunes, Spotify, Deezer and Vox, using the corresponding functions from `hs.itunes`, `hs.spotify`, `hs.deezer` and `hs.vox`, respectively." + ], + "desc" : "Table containing control functions for each application to control.", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause.controlfns", + "type" : "Variable", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause.controlfns", + "name" : "controlfns" + } + ], + "stripped_doc" : [ + + ], + "Deprecated" : [ + + ], + "desc" : "Play\/pause music players when headphones are connected\/disconnected", + "type" : "Module", + "Constructor" : [ + + ], + "doc" : "Play\/pause music players when headphones are connected\/disconnected\n\nDownload: [https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/HeadphoneAutoPause.spoon.zip](https:\/\/github.com\/Hammerspoon\/Spoons\/raw\/master\/Spoons\/HeadphoneAutoPause.spoon.zip)", + "Method" : [ + { + "doc" : "Generate the most common set of application control definition.\n\nParameters:\n * app - name of the application, with its correct letter casing (i.e. \"iTunes\"). The name as provided will be used to find the running application, and its lowercase version will be used to find the corresponding `hs.*` module.\n\nReturns:\n * A table in the correct format for `HeadphoneAutoPause.controlfns`, using the lower-case value of `app` as the module name (for example, if app = \"iTunes\", the module loaded will be `hs.itunes`, and assuming the functions `isPlaying()`, `play()` and `pause()` exist in that module.", + "stripped_doc" : [ + "Generate the most common set of application control definition.", + "" + ], + "desc" : "Generate the most common set of application control definition.", + "parameters" : [ + " * app - name of the application, with its correct letter casing (i.e. \"iTunes\"). The name as provided will be used to find the running application, and its lowercase version will be used to find the corresponding `hs.*` module.", + "" + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause.defaultControlFns(app)", + "type" : "Method", + "returns" : [ + " * A table in the correct format for `HeadphoneAutoPause.controlfns`, using the lower-case value of `app` as the module name (for example, if app = \"iTunes\", the module loaded will be `hs.itunes`, and assuming the functions `isPlaying()`, `play()` and `pause()` exist in that module." + ], + "def" : "HeadphoneAutoPause.defaultControlFns(app)", + "name" : "defaultControlFns" + }, + { + "doc" : "Callback function to use as an audio device watcher, to pause\/unpause the application on headphones plugged\/unplugged", + "stripped_doc" : [ + "Callback function to use as an audio device watcher, to pause\/unpause the application on headphones plugged\/unplugged" + ], + "desc" : "Callback function to use as an audio device watcher, to pause\/unpause the application on headphones plugged\/unplugged", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause:audiodevwatch(dev_uid, event_name)", + "type" : "Method", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause:audiodevwatch(dev_uid, event_name)", + "name" : "audiodevwatch" + }, + { + "doc" : "Start headphone detection on all audio devices that support it", + "stripped_doc" : [ + "Start headphone detection on all audio devices that support it" + ], + "desc" : "Start headphone detection on all audio devices that support it", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause:start()", + "type" : "Method", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause:start()", + "name" : "start" + }, + { + "doc" : "Stop headphone detection", + "stripped_doc" : [ + "Stop headphone detection" + ], + "desc" : "Stop headphone detection", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause:stop()", + "type" : "Method", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause:stop()", + "name" : "stop" + } + ], + "Command" : [ + + ], + "items" : [ + { + "doc" : "Table containing one key per application, with the value indicating whether HeadphoneAutoPause should try to pause\/unpause that application in response to the headphone being plugged\/unplugged. The key name must ideally correspond to the name of the corresponding `hs.*` module. Default value:\n```\n{\n itunes = true,\n spotify = true,\n deezer = true,\n vox = false -- Vox has built-in headphone detection support\n}\n```", + "stripped_doc" : [ + "Table containing one key per application, with the value indicating whether HeadphoneAutoPause should try to pause\/unpause that application in response to the headphone being plugged\/unplugged. The key name must ideally correspond to the name of the corresponding `hs.*` module. Default value:", + "```", + "{", + " itunes = true,", + " spotify = true,", + " deezer = true,", + " vox = false -- Vox has built-in headphone detection support", + "}", + "```" + ], + "desc" : "Table containing one key per application, with the value indicating whether HeadphoneAutoPause should try to pause\/unpause that application in response to the headphone being plugged\/unplugged. The key name must ideally correspond to the name of the corresponding `hs.*` module. Default value:", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause.control", + "type" : "Variable", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause.control", + "name" : "control" + }, + { + "doc" : "Table containing control functions for each application to control.\nThe keys must correspond to the values in `HeadphoneAutoPause.control`, and the value is a table with the following elements:\n * `appname` - application name (case-sensitive, as the application appears to the system)\n * `isPlaying` - function that returns a true value if the application is playing\n * `play` - function that starts playback in the application\n * `pause` - function that pauses playback in the application\n\nThe default value includes definitions for iTunes, Spotify, Deezer and Vox, using the corresponding functions from `hs.itunes`, `hs.spotify`, `hs.deezer` and `hs.vox`, respectively.", + "stripped_doc" : [ + "Table containing control functions for each application to control.", + "The keys must correspond to the values in `HeadphoneAutoPause.control`, and the value is a table with the following elements:", + " * `appname` - application name (case-sensitive, as the application appears to the system)", + " * `isPlaying` - function that returns a true value if the application is playing", + " * `play` - function that starts playback in the application", + " * `pause` - function that pauses playback in the application", + "", + "The default value includes definitions for iTunes, Spotify, Deezer and Vox, using the corresponding functions from `hs.itunes`, `hs.spotify`, `hs.deezer` and `hs.vox`, respectively." + ], + "desc" : "Table containing control functions for each application to control.", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause.controlfns", + "type" : "Variable", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause.controlfns", + "name" : "controlfns" + }, + { + "doc" : "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.", + "stripped_doc" : [ + "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon." + ], + "desc" : "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause.logger", + "type" : "Variable", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause.logger", + "name" : "logger" + }, + { + "doc" : "Callback function to use as an audio device watcher, to pause\/unpause the application on headphones plugged\/unplugged", + "stripped_doc" : [ + "Callback function to use as an audio device watcher, to pause\/unpause the application on headphones plugged\/unplugged" + ], + "desc" : "Callback function to use as an audio device watcher, to pause\/unpause the application on headphones plugged\/unplugged", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause:audiodevwatch(dev_uid, event_name)", + "type" : "Method", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause:audiodevwatch(dev_uid, event_name)", + "name" : "audiodevwatch" + }, + { + "doc" : "Generate the most common set of application control definition.\n\nParameters:\n * app - name of the application, with its correct letter casing (i.e. \"iTunes\"). The name as provided will be used to find the running application, and its lowercase version will be used to find the corresponding `hs.*` module.\n\nReturns:\n * A table in the correct format for `HeadphoneAutoPause.controlfns`, using the lower-case value of `app` as the module name (for example, if app = \"iTunes\", the module loaded will be `hs.itunes`, and assuming the functions `isPlaying()`, `play()` and `pause()` exist in that module.", + "stripped_doc" : [ + "Generate the most common set of application control definition.", + "" + ], + "desc" : "Generate the most common set of application control definition.", + "parameters" : [ + " * app - name of the application, with its correct letter casing (i.e. \"iTunes\"). The name as provided will be used to find the running application, and its lowercase version will be used to find the corresponding `hs.*` module.", + "" + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause.defaultControlFns(app)", + "type" : "Method", + "returns" : [ + " * A table in the correct format for `HeadphoneAutoPause.controlfns`, using the lower-case value of `app` as the module name (for example, if app = \"iTunes\", the module loaded will be `hs.itunes`, and assuming the functions `isPlaying()`, `play()` and `pause()` exist in that module." + ], + "def" : "HeadphoneAutoPause.defaultControlFns(app)", + "name" : "defaultControlFns" + }, + { + "doc" : "Start headphone detection on all audio devices that support it", + "stripped_doc" : [ + "Start headphone detection on all audio devices that support it" + ], + "desc" : "Start headphone detection on all audio devices that support it", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause:start()", + "type" : "Method", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause:start()", + "name" : "start" + }, + { + "doc" : "Stop headphone detection", + "stripped_doc" : [ + "Stop headphone detection" + ], + "desc" : "Stop headphone detection", + "parameters" : [ + + ], + "notes" : [ + + ], + "signature" : "HeadphoneAutoPause:stop()", + "type" : "Method", + "returns" : [ + + ], + "def" : "HeadphoneAutoPause:stop()", + "name" : "stop" + } + ], + "Field" : [ + + ], + "name" : "HeadphoneAutoPause" + } +] diff --git a/hammerspoon/Spoons/HeadphoneAutoPause.spoon/init.lua b/hammerspoon/Spoons/HeadphoneAutoPause.spoon/init.lua new file mode 100644 index 0000000..fe5b2da --- /dev/null +++ b/hammerspoon/Spoons/HeadphoneAutoPause.spoon/init.lua @@ -0,0 +1,157 @@ +--- === HeadphoneAutoPause === +--- +--- Play/pause music players when headphones are connected/disconnected +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HeadphoneAutoPause.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HeadphoneAutoPause.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "HeadphoneAutoPause" +obj.version = "0.1" +obj.author = "Diego Zamboni " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +--- HeadphoneAutoPause.logger +--- Variable +--- Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon. +obj.logger = hs.logger.new('HeadphoneAutoPause') + +--- HeadphoneAutoPause.control +--- Variable +--- Table containing one key per application, with the value indicating whether HeadphoneAutoPause should try to pause/unpause that application in response to the headphone being plugged/unplugged. The key name must ideally correspond to the name of the corresponding `hs.*` module. Default value: +--- ``` +--- { +--- itunes = true, +--- spotify = true, +--- deezer = true, +--- vox = false -- Vox has built-in headphone detection support +--- } +--- ``` +obj.control = { + itunes = true, + spotify = true, + deezer = true, + vox = false +} + +--- HeadphoneAutoPause.autoResume +--- Variable +--- Boolean value indicating if music should be automatically resumed when headphones are plugged in again. Only works if music was automatically paused when headphones were unplugged. +--- +--- Default value: `true` +obj.autoResume = true + +--- HeadphoneAutoPause.defaultControlFns(app) +--- Method +--- Generate the most common set of application control definition. +--- +--- Parameters: +--- * app - name of the application, with its correct letter casing (i.e. "iTunes"). The name as provided will be used to find the running application, and its lowercase version will be used to find the corresponding `hs.*` module. +--- +--- Returns: +--- * A table in the correct format for `HeadphoneAutoPause.controlfns`, using the lower-case value of `app` as the module name (for example, if app = "iTunes", the module loaded will be `hs.itunes`, and assuming the functions `isPlaying()`, `play()` and `pause()` exist in that module. +function obj.defaultControlFns(app) + local lcapp=string.lower(app) + return({ appname = app, + isPlaying = hs[lcapp].isPlaying, + play = hs[lcapp].play, + pause = hs[lcapp].pause }) +end + +--- HeadphoneAutoPause.controlfns +--- Variable +--- Table containing control functions for each application to control. +--- The keys must correspond to the values in `HeadphoneAutoPause.control`, and the value is a table with the following elements: +--- * `appname` - application name (case-sensitive, as the application appears to the system) +--- * `isPlaying` - function that returns a true value if the application is playing +--- * `play` - function that starts playback in the application +--- * `pause` - function that pauses playback in the application +--- +--- The default value includes definitions for iTunes, Spotify, Deezer and Vox, using the corresponding functions from `hs.itunes`, `hs.spotify`, `hs.deezer` and `hs.vox`, respectively. +obj.controlfns = { + itunes = obj.defaultControlFns('iTunes'), + spotify = obj.defaultControlFns('Spotify'), + deezer = obj.defaultControlFns('Deezer'), + vox = { appname = 'Vox', + isPlaying = function() return (hs.vox.getPlayerState() == 1) end, + play = hs.vox.play, + pause = hs.vox.pause, + } +} + +-- Internal cache of previous playback state when headhpones are +-- unplugged, to allow resuming playback automatically only if the app +-- was previously playing. +local wasplaying = {} +-- Internal cache of audio devices and their watcher functions +local devs = {} + +--- HeadphoneAutoPause:audiodevwatch(dev_uid, event_name) +--- Method +--- Callback function to use as an audio device watcher, to pause/unpause the application on headphones plugged/unplugged +function obj:audiodevwatch(dev_uid, event_name) + self.logger.df("Audiodevwatch args: %s, %s", dev_uid, event_name) + dev = hs.audiodevice.findDeviceByUID(dev_uid) + if event_name == 'jack' then + if dev:jackConnected() then + self.logger.d("Headphones connected") + if self.autoResume then + for app, playercontrol in pairs(self.controlfns) do + if self.control[app] and hs.appfinder.appFromName(playercontrol.appname) and wasplaying[app] then + self.logger.df("Resuming playback in %s", playercontrol.appname) + hs.notify.show("Headphones plugged", "Resuming " .. playercontrol.appname .. " playback", "") + playercontrol.play() + end + end + end + else + self.logger.d("Headphones disconnected") + -- Cache current state to know whether we should resume + -- when the headphones are connected again + for app, playercontrol in pairs(self.controlfns) do + if self.control[app] and hs.appfinder.appFromName(playercontrol.appname) then + wasplaying[app] = playercontrol.isPlaying() + if wasplaying[app] then + self.logger.df("Pausing %s", playercontrol.appname) + hs.notify.show("Headphones unplugged", "Pausing " .. playercontrol.appname, "") + playercontrol.pause() + end + end + end + end + end +end + +--- HeadphoneAutoPause:start() +--- Method +--- Start headphone detection on all audio devices that support it +function obj:start() + for i,dev in ipairs(hs.audiodevice.allOutputDevices()) do + if dev:jackConnected() ~= nil then + if dev.watcherCallback ~= nil then + self.logger.df("Setting up watcher for audio device %s (UID %s)", dev:name(), dev:uid()) + devs[dev:uid()]=dev:watcherCallback(hs.fnutils.partial(self.audiodevwatch, self)) + devs[dev:uid()]:watcherStart() + else + self.logger.w("Your version of Hammerspoon does not support audio device watchers - please upgrade") + end + end + end +end + +--- HeadphoneAutoPause:stop() +--- Method +--- Stop headphone detection +function obj:stop() + for id,dev in pairs(devs) do + if dev and dev:watcherIsRunning() then + dev:watcherStop() + devs[id]=nil + end + end +end + +return obj diff --git a/hammerspoon/Spoons/RoundedCorners.spoon/docs.json b/hammerspoon/Spoons/RoundedCorners.spoon/docs.json new file mode 100644 index 0000000..32c5c54 --- /dev/null +++ b/hammerspoon/Spoons/RoundedCorners.spoon/docs.json @@ -0,0 +1,39 @@ +[ + { + "doc" : "Give your screens rounded corners", + "items" : [ + { + "doc" : "Controls whether corners are drawn on all screens or just the primary screen. Defaults to true", + "type" : "Variable", + "name" : "allScreens", + "def" : "RoundedCorners.allScreens" + }, + { + "doc" : "Controls which level of the screens the corners are drawn at. See `hs.canvas.windowLevels` for more information. Defaults to `screenSaver + 1`", + "type" : "Variable", + "name" : "level", + "def" : "RoundedCorners.level" + }, + { + "doc" : "Controls the radius of the rounded corners, in points. Defaults to 6", + "type" : "Variable", + "name" : "radius", + "def" : "RoundedCorners.radius" + }, + { + "doc" : "Starts RoundedCorners\n\nParameters:\n * None\n\nReturns:\n * The RoundedCorners object\n\nNotes:\n * This will draw the rounded screen corners and start watching for changes in screen sizes\/layouts, reacting accordingly", + "type" : "Method", + "name" : "start", + "def" : "RoundedCorners:start()" + }, + { + "doc" : "Stops RoundedCorners\n\nParameters:\n * None\n\nReturns:\n * The RoundedCorners object\n\nNotes:\n * This will remove all rounded screen corners and stop watching for changes in screen sizes\/layouts", + "type" : "Method", + "name" : "stop", + "def" : "RoundedCorners:stop()" + } + ], + "name" : "RoundedCorners", + "desc" : "Give your screens rounded corners" + } +] diff --git a/hammerspoon/Spoons/RoundedCorners.spoon/init.lua b/hammerspoon/Spoons/RoundedCorners.spoon/init.lua new file mode 100644 index 0000000..6e1bbdf --- /dev/null +++ b/hammerspoon/Spoons/RoundedCorners.spoon/init.lua @@ -0,0 +1,125 @@ +--- === RoundedCorners === +--- +--- Give your screens rounded corners +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/RoundedCorners.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/RoundedCorners.spoon.zip) +local obj = {} +obj.__index = obj + +-- Metadata +obj.name = "RoundedCorners" +obj.version = "1.0" +obj.author = "Chris Jones " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +obj.corners = {} +obj.screenWatcher = nil + +--- RoundedCorners.allScreens +--- Variable +--- Controls whether corners are drawn on all screens or just the primary screen. Defaults to true +obj.allScreens = true + +--- RoundedCorners.radius +--- Variable +--- Controls the radius of the rounded corners, in points. Defaults to 6 +obj.radius = 6 + +--- RoundedCorners.level +--- Variable +--- Controls which level of the screens the corners are drawn at. See `hs.canvas.windowLevels` for more information. Defaults to `screenSaver + 1` +obj.level = hs.canvas.windowLevels["screenSaver"] + 1 + +-- 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() + +function obj:init() + self.screenWatcher = hs.screen.watcher.new(function() self:screensChanged() end) +end + +--- RoundedCorners:start() +--- Method +--- Starts RoundedCorners +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * The RoundedCorners object +--- +--- Notes: +--- * This will draw the rounded screen corners and start watching for changes in screen sizes/layouts, reacting accordingly +function obj:start() + self.screenWatcher:start() + self:render() + return self +end + +--- RoundedCorners:stop() +--- Method +--- Stops RoundedCorners +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * The RoundedCorners object +--- +--- Notes: +--- * This will remove all rounded screen corners and stop watching for changes in screen sizes/layouts +function obj:stop() + self.screenWatcher:stop() + self:deleteAllCorners() + return self +end + +-- Delete all the corners +function obj:deleteAllCorners() + hs.fnutils.each(self.corners, function(corner) corner:delete() end) + self.corners = {} +end + +-- React to the screens having changed +function obj:screensChanged() + self:deleteAllCorners() + self:render() +end + +-- Get the screens to draw on, given the user's settings +function obj:getScreens() + if self.allScreens then + return hs.screen.allScreens() + else + return {hs.screen.primaryScreen()} + end +end + +-- Draw the corners +function obj:render() + local screens = self:getScreens() + local radius = self.radius + hs.fnutils.each(screens, function(screen) + local screenFrame = screen:fullFrame() + local cornerData = { + { frame={x=screenFrame.x, y=screenFrame.y}, center={x=radius,y=radius} }, + { frame={x=screenFrame.x + screenFrame.w - radius, y=screenFrame.y}, center={x=0,y=radius} }, + { frame={x=screenFrame.x, y=screenFrame.y + screenFrame.h - radius}, center={x=radius,y=0} }, + { frame={x=screenFrame.x + screenFrame.w - radius, y=screenFrame.y + screenFrame.h - radius}, center={x=0,y=0} }, + } + for _,data in pairs(cornerData) do + self.corners[#self.corners+1] = hs.canvas.new({x=data.frame.x,y=data.frame.y,w=radius,h=radius}):appendElements( + { action="build", type="rectangle", }, + { action="clip", type="circle", center=data.center, radius=radius, reversePath=true, }, + { action="fill", type="rectangle", frame={x=0, y=0, w=radius, h=radius, }, fillColor={ alpha=1, }}, + { type="resetClip", } + ):behavior(hs.canvas.windowBehaviors.canJoinAllSpaces):show() + end + end) +end + +return obj diff --git a/hammerspoon/ext/grid.patch b/hammerspoon/ext/grid.lua.patch similarity index 100% rename from hammerspoon/ext/grid.patch rename to hammerspoon/ext/grid.lua.patch diff --git a/hammerspoon/init.lua b/hammerspoon/init.lua index deeabc0..251ccca 100644 --- a/hammerspoon/init.lua +++ b/hammerspoon/init.lua @@ -1,167 +1,38 @@ --- --- configuration --- +-- luacheck: read_globals hs -local animationDuration = 0.0 -local gridSizes = { default = '30x20', interactive = '8x4' } -local gridTextSize = 100 -local margins = { w = 4, h = 4 } +-- Reload config hotkey +hs.hotkey.bind({"cmd", "alt", "ctrl"}, "R", hs.reload) + +-------------------------------------------------------------------------------- +-- Set Hammerspoon options +-------------------------------------------------------------------------------- + +hs.autoLaunch(true) +hs.consoleOnTop(true) +hs.dockIcon(false) +hs.menuIcon(true) +hs.console.alpha(0.90) +hs.console.behaviorAsLabels { 'moveToActiveSpace' } + +-------------------------------------------------------------------------------- +-- Require modules +-------------------------------------------------------------------------------- + +require('window_management'):init() + +-------------------------------------------------------------------------------- +-- Load Spoons +-------------------------------------------------------------------------------- + +-- Draw pretty rounded corners on all screens. +hs.loadSpoon('RoundedCorners') +spoon.RoundedCorners:start() + +-- Automatically pause music when headphones are unplugged. +hs.loadSpoon('HeadphoneAutoPause') +spoon.HeadphoneAutoPause.autoResume = false +spoon.HeadphoneAutoPause:start() --- --- setup --- - -local grid = require('ext.grid') - -hs.window.animationDuration=animationDuration -grid.setGrid(gridSizes.default) -grid.setMargins(margins) -grid.ui.textSize = gridTextSize - - --- --- helpers --- - -function adjustGridWindow(x, y, w, h) - return function() - grid.adjustWindow( - function(cell) - cell.x,cell.y,cell.w,cell.h = x, y, w, h - end - ) - end -end - - --- --- resize to grid --- - --- show interactive grid menu -hs.hotkey.bind( - {"cmd", "ctrl"}, "4", - function() - grid.setGrid(gridSizes.interactive) - grid.toggleShow( - function() - grid.setGrid(gridSizes.default) - end - ) - end -) - --- left half -hs.hotkey.bind({"cmd", "ctrl"}, "J", adjustGridWindow(0, 0, 15, 20)) --- right half -hs.hotkey.bind({"cmd", "ctrl"}, "L", adjustGridWindow(15, 0, 15, 20)) --- top half -hs.hotkey.bind({"cmd", "ctrl"}, "I", adjustGridWindow(0, 0, 30, 10)) --- bottom half -hs.hotkey.bind({"cmd", "ctrl"}, "K", adjustGridWindow(0, 10, 30, 10)) - --- left narrow -hs.hotkey.bind({"ctrl", "alt"}, "U", adjustGridWindow(0, 0, 12, 20)) --- right narrow -hs.hotkey.bind({"ctrl", "alt"}, "O", adjustGridWindow(18, 0, 12, 20)) - --- left wide -hs.hotkey.bind({"cmd", "ctrl"}, "U", adjustGridWindow(0, 0, 18, 20)) --- right wide -hs.hotkey.bind({"cmd", "ctrl"}, "O", adjustGridWindow(12, 0, 18, 20)) - --- left fat -hs.hotkey.bind({"ctrl", "alt"}, "J", adjustGridWindow(0, 0, 21, 20)) --- right wide -hs.hotkey.bind({"ctrl", "alt"}, "L", adjustGridWindow(9, 0, 21, 20)) --- top fat -hs.hotkey.bind({"ctrl", "alt"}, "I", adjustGridWindow(0, 0, 30, 14)) --- bottom wide -hs.hotkey.bind({"ctrl", "alt"}, "K", adjustGridWindow(0, 6, 30, 14)) - --- top left quarter -hs.hotkey.bind({"cmd", "ctrl", "shift"}, "J", adjustGridWindow(0, 0, 15, 10)) --- top right quarter -hs.hotkey.bind({"cmd", "ctrl", "shift"}, "I", adjustGridWindow(15, 0, 15, 10)) --- bottom right quarter -hs.hotkey.bind({"cmd", "ctrl", "shift"}, "L", adjustGridWindow(15, 10, 15, 10)) --- bottom left quarter -hs.hotkey.bind({"cmd", "ctrl", "shift"}, "K", adjustGridWindow(0, 10, 15, 10)) - --- center narrow small -hs.hotkey.bind({"ctrl", "alt"}, "\\", adjustGridWindow(9, 0, 12, 20)) --- center narrow -hs.hotkey.bind({"cmd", "ctrl"}, "\\", adjustGridWindow(7, 0, 16, 20)) - --- center medium small -hs.hotkey.bind({"ctrl", "alt"}, "'", adjustGridWindow(6, 0, 18, 20)) --- center medium -hs.hotkey.bind({"cmd", "ctrl"}, "'", adjustGridWindow(5, 0, 20, 20)) - --- center wide small -hs.hotkey.bind({"ctrl", "alt"}, ";", adjustGridWindow(4, 0, 22, 20)) --- center wide -hs.hotkey.bind({"cmd", "ctrl"}, ";", adjustGridWindow(3, 0, 24, 20)) - --- center wide -hs.hotkey.bind({"cmd", "ctrl"}, "H", function() grid.maximizeWindow() end) - - --- --- move between displays --- - --- move to screen to the left -hs.hotkey.bind( - {"cmd", "ctrl"}, ",", - function() - local win = hs.window.focusedWindow() - win:moveOneScreenWest() - grid.snap(win) - end -) - --- move to screen to the right -hs.hotkey.bind( - {"cmd", "ctrl"}, ".", - function() - local win = hs.window.focusedWindow() - win:moveOneScreenEast() - grid.snap(win) - end -) - --- move to screen above -hs.hotkey.bind( - {"cmd", "ctrl"}, "P", - function() - local win = hs.window.focusedWindow() - win:moveOneScreenNorth() - grid.snap(win) - end -) - --- move to screen bellow -hs.hotkey.bind( - {"cmd", "ctrl"}, "N", - function() - local win = hs.window.focusedWindow() - win:moveOneScreenSouth() - grid.snap(win) - end -) - - --- -- the end --- - --- reload config -hs.hotkey.bind( - {"cmd", "alt", "ctrl"}, "R", - function() - hs.reload() - end -) hs.alert.show("Hammerspoon loaded") diff --git a/hammerspoon/window_management.lua b/hammerspoon/window_management.lua new file mode 100644 index 0000000..cf43e60 --- /dev/null +++ b/hammerspoon/window_management.lua @@ -0,0 +1,159 @@ +-- luacheck: read_globals hs + +local wm = { + grid = require('ext.grid'), + + -- configuration + animationDuration = 0.0, + gridSizes = { default = '30x20', interactive = '8x4' }, + gridTextSize = 50, + margins = { w = 4, h = 4 } +} + +function wm:init () + -- setup + hs.window.animationDuration = self.animationDuration + self.grid.setGrid(self.gridSizes.default) + self.grid.setMargins(self.margins) + self.grid.ui.textSize = self.gridTextSize + + + -- + -- resize to grid + -- + + -- show interactive grid menu + hs.hotkey.bind( + {"cmd", "ctrl"}, "2", + function() + self.grid.setGrid(self.gridSizes.interactive) + self.grid.show( + function() + self.grid.setGrid(self.gridSizes.default) + end + ) + end + ) + + -- left half + hs.hotkey.bind({"cmd", "ctrl"}, "J", self.adjustWindow(0, 0, 15, 20)) + -- right half + hs.hotkey.bind({"cmd", "ctrl"}, "L", self.adjustWindow(15, 0, 15, 20)) + -- top half + hs.hotkey.bind({"cmd", "ctrl"}, "I", self.adjustWindow(0, 0, 30, 10)) + -- bottom half + hs.hotkey.bind({"cmd", "ctrl"}, "K", self.adjustWindow(0, 10, 30, 10)) + + -- left narrow + hs.hotkey.bind({"ctrl", "alt"}, "U", self.adjustWindow(0, 0, 12, 20)) + -- right narrow + hs.hotkey.bind({"ctrl", "alt"}, "O", self.adjustWindow(18, 0, 12, 20)) + + -- left wide + hs.hotkey.bind({"cmd", "ctrl"}, "U", self.adjustWindow(0, 0, 18, 20)) + -- right wide + hs.hotkey.bind({"cmd", "ctrl"}, "O", self.adjustWindow(12, 0, 18, 20)) + + -- left fat + hs.hotkey.bind({"ctrl", "alt"}, "J", self.adjustWindow(0, 0, 21, 20)) + -- right wide + hs.hotkey.bind({"ctrl", "alt"}, "L", self.adjustWindow(9, 0, 21, 20)) + -- top fat + hs.hotkey.bind({"ctrl", "alt"}, "I", self.adjustWindow(0, 0, 30, 14)) + -- bottom wide + hs.hotkey.bind({"ctrl", "alt"}, "K", self.adjustWindow(0, 6, 30, 14)) + + -- top left quarter + hs.hotkey.bind({"cmd", "ctrl", "shift"}, "J", self.adjustWindow(0, 0, 15, 10)) + -- top right quarter + hs.hotkey.bind({"cmd", "ctrl", "shift"}, "I", self.adjustWindow(15, 0, 15, 10)) + -- bottom right quarter + hs.hotkey.bind({"cmd", "ctrl", "shift"}, "L", self.adjustWindow(15, 10, 15, 10)) + -- bottom left quarter + hs.hotkey.bind({"cmd", "ctrl", "shift"}, "K", self.adjustWindow(0, 10, 15, 10)) + + -- center narrow small + hs.hotkey.bind({"ctrl", "alt"}, "\\", self.adjustWindow(9, 0, 12, 20)) + -- center narrow + hs.hotkey.bind({"cmd", "ctrl"}, "\\", self.adjustWindow(7, 0, 16, 20)) + + -- center medium small + hs.hotkey.bind({"ctrl", "alt"}, "'", self.adjustWindow(6, 0, 18, 20)) + -- center medium + hs.hotkey.bind({"cmd", "ctrl"}, "'", self.adjustWindow(5, 0, 20, 20)) + + -- center wide small + hs.hotkey.bind({"ctrl", "alt"}, ";", self.adjustWindow(4, 0, 22, 20)) + -- center wide + hs.hotkey.bind({"cmd", "ctrl"}, ";", self.adjustWindow(3, 0, 24, 20)) + + -- maximized + hs.hotkey.bind({"cmd", "ctrl"}, "H", self.grid.maximizeWindow) + + + -- + -- move between displays + -- + + -- move to screen to the left + hs.hotkey.bind( + {"cmd", "ctrl"}, ",", + function() + local win = hs.window.focusedWindow() + win:moveOneScreenWest() + self.grid.snap(win) + end + ) + + -- move to screen to the right + hs.hotkey.bind( + {"cmd", "ctrl"}, ".", + function() + local win = hs.window.focusedWindow() + win:moveOneScreenEast() + self.grid.snap(win) + end + ) + + -- move to screen above + hs.hotkey.bind( + {"cmd", "ctrl"}, "P", + function() + local win = hs.window.focusedWindow() + win:moveOneScreenNorth() + self.grid.snap(win) + end + ) + + -- move to screen bellow + hs.hotkey.bind( + {"cmd", "ctrl"}, "N", + function() + local win = hs.window.focusedWindow() + win:moveOneScreenSouth() + self.grid.snap(win) + end + ) +end + + +-- +-- private methods +-- + +wm.adjustWindow = function (x, y, w, h) + return function() + wm.grid.adjustWindow( + function(cell) + cell.x = x + cell.y = y + cell.w = w + cell.h = h + end + ) + end +end + + +-- the end +return wm