Major update to Hammerspoon config

- Use Spoons (installed via Makefile targets):
  - RoundedCorners
  - HeadphoneAutoPause (with AutoResume patch)
- Move windows management into separate file
This commit is contained in:
2017-08-05 13:44:00 +01:00
parent 1acf58b4c5
commit 7bd9f0d9f2
9 changed files with 941 additions and 167 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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"
}
]

View File

@@ -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 <diego@zzamboni.org>"
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

View File

@@ -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"
}
]

View File

@@ -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 <cmsj@tenshu.net>"
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

View File

@@ -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")

View File

@@ -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