diff --git a/.hammerspoon/Spoons/Quitter.spoon/init.lua b/.hammerspoon/Spoons/Quitter.spoon/init.lua
new file mode 100644
index 00000000..3e31d695
--- /dev/null
+++ b/.hammerspoon/Spoons/Quitter.spoon/init.lua
@@ -0,0 +1,93 @@
+--- === Quitter ===
+--
+-- Quits configured apps if they have not been used since a specified amount of time.
+
+local obj = {}
+obj.__index = obj
+
+-- Metadata
+
+obj.name = "Quitter"
+obj.version = "0.1"
+obj.author = "Alpha Chen <alpha@kejadlen.dev>"
+obj.license = "MIT - https://opensource.org/licenses/MIT"
+
+obj.logger = hs.logger.new("quitter", "debug")
+obj.lastFocused = {}
+
+--- Quitter.quitAppsAfter
+--- Variable
+--- Table of application bundle IDs to quit when unused. Use `mdls -name
+--- --kMDItemCFBundleIdentifier -r /path/to/app` to find unknown bundle IDs.
+
+obj.quitAppsAfter = {}
+
+--- Quitter:start()
+--- Method
+--- Start Quitter
+---
+--- Parameters:
+--- * None
+function obj:start()
+ self:reset()
+
+ -- Reset if we're waking from sleep
+ hs.caffeinate.watcher.new(function(event)
+ if event ~= hs.caffeinate.watcher.systemDidWake then return end
+ self:reset()
+ end)
+
+ -- Set last focused time for relevant apps
+ hs.window.filter.default:subscribe(hs.window.filter.windowFocused, function(window, appName)
+ local bundleID = window:application():bundleID()
+ if not self.quitAppsAfter[bundleID] then return end
+ self.lastFocused[bundleID] = os.time()
+ end)
+
+ self.timer = hs.timer.doEvery(60, function()
+ self:reap()
+ end):start()
+
+ return self
+end
+
+function obj:reset()
+ hs.fnutils.ieach(hs.application.runningApplications(), function(app)
+ local bundleID = app:bundleID()
+
+ local duration = self.quitAppsAfter[bundleID]
+ if not duration then return false end
+
+ self.lastFocused[bundleID] = os.time()
+ end)
+end
+
+function obj:reap()
+ self.logger.d("reaping")
+ local appsToQuit = hs.fnutils.ifilter(hs.application.runningApplications(), function(app)
+ -- Don't quit the currently focused app
+ if app:isFrontmost() then return false end
+
+ local duration = self.quitAppsAfter[app:bundleID()]
+ if not duration then return false end
+
+ local lastFocused = self.lastFocused[app:bundleID()]
+ if not lastFocused then return false end
+
+ self.logger.d("app: " .. app:name() .. " last focused at " .. lastFocused)
+
+ return (os.time() - lastFocused) > duration
+ end)
+
+ for _,app in pairs(appsToQuit) do
+ hs.notify.new({
+ title = "Hammerspoon",
+ informativeText = "Quitting " .. app:name(),
+ withdrawAfter = 2,
+ }):send()
+ app:kill()
+ self.lastFocused[app:bundleID()] = nil
+ end
+end
+
+return obj
diff --git a/.hammerspoon/init.lua b/.hammerspoon/init.lua
index e9d4dce2..6af5a083 100644
--- a/.hammerspoon/init.lua
+++ b/.hammerspoon/init.lua
@@ -1,7 +1,8 @@
local mash = {"cmd", "alt", "ctrl"}
local smash = {"cmd", "alt", "ctrl", "shift"}
--- Window management
+-- Window management --
+
wm = {
left = hs.layout.left50,
right = hs.layout.right50,
@@ -22,7 +23,33 @@ hs.hotkey.bind(mash, "h", function() move("left") end)
hs.hotkey.bind(mash, "l", function() move("right") end)
hs.hotkey.bind(mash, "m", function() move("max") end)
+-- Defeat paste blocking --
+
+hs.hotkey.bind({"cmd", "alt"}, "v", function() hs.eventtap.keyStrokes(hs.pasteboard.getContents()) end)
+
+-- Debugging --
+
+-- hs.hotkey.bind(mash, "d", function()
+-- end)
+
+-- Spoons --
+
+hs.loadSpoon("Quitter")
+spoon.Quitter.quitAppsAfter = {
+ -- mdls -name kMDItemCFBundleIdentifier -r /path/to/app
+ ["com.apple.iChat"] = 600,
+ ["com.freron.MailMate"] = 600,
+ ["com.kapeli.dashdoc"] = 600,
+ ["com.reederapp.macOS"] = 600,
+ ["com.tinyspeck.slackmacgap"] = 600,
+}
+spoon.Quitter:start()
+
hs.loadSpoon("ReloadConfiguration")
spoon.ReloadConfiguration:start()
-hs.alert.show("Config loaded")
+hs.notify.new({
+ title = "Hammerspoon",
+ informativeText = "Config loaded",
+ withdrawAfter = 2,
+}):send()