dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

commit 17f72846b21552f0163704f58c7460fa6002b029
parent 0c01a138f500dfbb9766f20f6e4cf030ea92c9a6
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Sun, 14 Sep 2025 18:02:32 +0200

hammerspoon: config

Diffstat:
Mdot.map | 1+
Ahammerspoon/Spoons/ClipboardTool.spoon/init.lua | 525+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahammerspoon/Spoons/PaperWM.spoon | 1+
Ahammerspoon/Spoons/Swipe.spoon | 1+
Ahammerspoon/auto-toggle-vertical-horizontal-tabs.lua | 20++++++++++++++++++++
Ahammerspoon/init.lua | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahammerspoon/mydebug.lua | 26++++++++++++++++++++++++++
Ahammerspoon/numpad-cursor.lua | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahammerspoon/spoonconfig/paperwm.lua | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahammerspoon/spoonconfig/swipe.lua | 25+++++++++++++++++++++++++
10 files changed, 832 insertions(+), 0 deletions(-)

diff --git a/dot.map b/dot.map @@ -66,6 +66,7 @@ helix: ~/.config/helix bitmonero: ~/.config/bitmonero spotify/spot-last-checked: ~/.local/share/spot-last-checked ripgrep: ~/.config/ripgrep +hammerspoon: ~/.hammerspoon # Config: sioyek # for macOS: diff --git a/hammerspoon/Spoons/ClipboardTool.spoon/init.lua b/hammerspoon/Spoons/ClipboardTool.spoon/init.lua @@ -0,0 +1,525 @@ +--- === ClipboardTool === +--- +--- Keep a history of the clipboard for text entries and manage the entries with a context menu +--- +--- Originally based on TextClipboardHistory.spoon by Diego Zamboni with additional functions provided by a context menu +--- and on [code by VFS](https://github.com/VFS/.hammerspoon/blob/master/tools/clipboard.lua), but with many changes and some contributions and inspiration from [asmagill](https://github.com/asmagill/hammerspoon-config/blob/master/utils/_menus/newClipper.lua). +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ClipboardTool.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/ClipboardTool.spoon.zip) + +local obj = {} +obj.__index = obj + +-- Metadata +obj.name = "ClipboardTool" +obj.version = "0.7" +obj.author = "Alfred Schilken <alfred@schilken.de>" +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +local getSetting = function(label, default) + return hs.settings.get(obj.name .. "." .. label) or default +end +local setSetting = function(label, value) + hs.settings.set(obj.name .. "." .. label, value) + return value +end + +--- ClipboardTool.frequency +--- Variable +--- Speed in seconds to check for clipboard changes. If you check too frequently, you will degrade performance, if you check sparsely you will loose copies. Defaults to 0.8. +obj.frequency = 0.8 + +--- ClipboardTool.hist_size +--- Variable +--- How many items to keep on history. Defaults to 100 +obj.hist_size = 100 + +--- ClipboardTool.max_entry_size +--- Variable +--- maximum size of a text entry +obj.max_entry_size = 4990 + +--- ClipboardTool.max_size +--- Variable +--- Whether to check the maximum size of an entry. Defaults to `false`. +obj.max_size = getSetting("max_size", false) + +--- ClipboardTool.show_copied_alert +--- Variable +--- If `true`, show an alert when a new item is added to the history, i.e. has been copied. +obj.show_copied_alert = true + +--- ClipboardTool.honor_ignoredidentifiers +--- Variable +--- If `true`, check the data identifiers set in the pasteboard and ignore entries which match those listed in `ClipboardTool.ignoredIdentifiers`. The list of identifiers comes from http://nspasteboard.org. Defaults to `true` +obj.honor_ignoredidentifiers = true + +--- ClipboardTool.paste_on_select +--- Variable +--- Whether to auto-type the item when selecting it from the menu. Can be toggled on the fly from the chooser. Defaults to `false`. +obj.paste_on_select = getSetting("paste_on_select", false) + +--- ClipboardTool.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("ClipboardTool") + +--- ClipboardTool.ignoredIdentifiers +--- Variable +--- Types of clipboard entries to ignore, see http://nspasteboard.org. Code from https://github.com/asmagill/hammerspoon-config/blob/master/utils/_menus/newClipper.lua. +--- +--- Notes: +--- * Default value (don't modify unless you know what you are doing): +--- ``` +--- { +--- ["de.petermaurer.TransientPasteboardType"] = true, -- Transient : Textpander, TextExpander, Butler +--- ["com.typeit4me.clipping"] = true, -- Transient : TypeIt4Me +--- ["Pasteboard generator type"] = true, -- Transient : Typinator +--- ["com.agilebits.onepassword"] = true, -- Confidential : 1Password +--- ["org.nspasteboard.TransientType"] = true, -- Universal, Transient +--- ["org.nspasteboard.ConcealedType"] = true, -- Universal, Concealed +--- ["org.nspasteboard.AutoGeneratedType"] = true, -- Universal, Automatic +--- } +--- ``` +obj.ignoredIdentifiers = { + ["de.petermaurer.TransientPasteboardType"] = true, -- Transient : Textpander, TextExpander, Butler + ["com.typeit4me.clipping"] = true, -- Transient : TypeIt4Me + ["Pasteboard generator type"] = true, -- Transient : Typinator + ["com.agilebits.onepassword"] = true, -- Confidential : 1Password + ["org.nspasteboard.TransientType"] = true, -- Universal, Transient + ["org.nspasteboard.ConcealedType"] = true, -- Universal, Concealed + ["org.nspasteboard.AutoGeneratedType"] = true, -- Universal, Automatic +} + +--- ClipboardTool.deduplicate +--- Variable +--- Whether to remove duplicates from the list, keeping only the latest one. Defaults to `true`. +obj.deduplicate = true + +--- ClipboardTool.show_in_menubar +--- Variable +--- Whether to show a menubar item to open the clipboard history. Defaults to `true` +obj.show_in_menubar = true + +--- ClipboardTool.menubar_title +--- Variable +--- String to show in the menubar if `ClipboardTool.show_in_menubar` is `true`. Defaults to `"\u{1f4cb}"`, which is the [Unicode clipboard character](https://codepoints.net/U+1F4CB) +obj.menubar_title = "\u{1f4cb}" + +--- ClipboardTool.display_max_length +--- Variable +--- Number of characters to which each clipboard item will be truncated, when displaying in the menu. This only truncates in display, the full content will be used for searching and for pasting. +obj.display_max_length = 200 + +---------------------------------------------------------------------- + +-- Internal variable - Chooser/menu object +obj.selectorobj = nil +-- Internal variable - Cache for focused window to work around the current window losing focus after the chooser comes up +obj.prevFocusedWindow = nil +-- Internal variable - Timer object to look for pasteboard changes +obj.timer = nil + +local pasteboard = require("hs.pasteboard") -- http://www.hammerspoon.org/docs/hs.pasteboard.html +local hashfn = require("hs.hash").MD5 + +-- Keep track of last change counter +local last_change = nil +-- Array to store the clipboard history +local clipboard_history = nil + +-- Internal function - persist the current history so it survives across restarts +function _persistHistory() + setSetting("items", clipboard_history) +end + +--- ClipboardTool:togglePasteOnSelect() +--- Method +--- Toggle the value of `ClipboardTool.paste_on_select` +--- +--- Parameters: +--- * None +function obj:togglePasteOnSelect() + self.paste_on_select = setSetting("paste_on_select", not self.paste_on_select) + hs.notify.show("ClipboardTool", "Paste-on-select is now " .. (self.paste_on_select and "enabled" or "disabled"), "") +end + +function obj:toggleMaxSize() + self.max_size = setSetting("max_size", not self.max_size) + hs.notify.show("ClipboardTool", "Max Size is now " .. (self.max_size and "enabled" or "disabled"), "") +end + +-- Internal method - process the selected item from the chooser. An item may invoke special actions, defined in the `actions` variable. +function obj:_processSelectedItem(value) + local actions = { + none = function() end, + clear = hs.fnutils.partial(self.clearAll, self), + toggle_paste_on_select = hs.fnutils.partial(self.togglePasteOnSelect, self), + toggle_max_size = hs.fnutils.partial(self.toggleMaxSize, self), + } + if self.prevFocusedWindow ~= nil then + self.prevFocusedWindow:focus() + end + if value and type(value) == "table" then + if value.action and actions[value.action] then + actions[value.action](value) + elseif value.text then + if value.type == "text" then + pasteboard.setContents(value.data) + elseif value.type == "image" then + pasteboard.writeObjects(hs.image.imageFromURL(value.data)) + end + -- self:pasteboardToClipboard(value.text) + if self.paste_on_select then + hs.eventtap.keyStroke({ "cmd" }, "v") + end + end + last_change = pasteboard.changeCount() + end +end + +--- ClipboardTool:clearAll() +--- Method +--- Clears the clipboard and history +--- +--- Parameters: +--- * None +function obj:clearAll() + pasteboard.clearContents() + clipboard_history = {} + _persistHistory() + last_change = pasteboard.changeCount() +end + +--- ClipboardTool:clearLastItem() +--- Method +--- Clears the last added to the history +--- +--- Parameters: +--- * None +function obj:clearLastItem() + table.remove(clipboard_history, 1) + _persistHistory() + last_change = pasteboard.changeCount() +end + +-- Internal method: deduplicate the given list, and restrict it to the history size limit +function obj:dedupe_and_resize(list) + local res = {} + local hashes = {} + for i, v in ipairs(list) do + if #res < self.hist_size then + local hash = hashfn(v.content) + if (not self.deduplicate) or not hashes[hash] then + table.insert(res, v) + hashes[hash] = true + end + end + end + return res +end + +--- ClipboardTool:pasteboardToClipboard(item) +--- Method +--- Add the given string to the history +--- +--- Parameters: +--- * item - string to add to the clipboard history +--- +--- Returns: +--- * None +function obj:pasteboardToClipboard(item_type, item) + table.insert(clipboard_history, 1, { type = item_type, content = item }) + clipboard_history = self:dedupe_and_resize(clipboard_history) + _persistHistory() -- updates the saved history +end + +-- Internal method: actions of the context menu, special paste +function obj:pasteAllWithDelimiter(row, delimiter) + if self.prevFocusedWindow ~= nil then + self.prevFocusedWindow:focus() + end + print("pasteAllWithTab row:" .. row) + for ix = row, 1, -1 do + local entry = clipboard_history[ix] + print("pasteAllWithTab ix:" .. ix .. ":" .. entry) + -- pasteboard.setContents(entry) + -- os.execute("sleep 0.2") + -- hs.eventtap.keyStroke({"cmd"}, "v") + hs.eventtap.keyStrokes(entry.content) + -- os.execute("sleep 0.2") + hs.eventtap.keyStrokes(delimiter) + -- os.execute("sleep 0.2") + end +end + +-- Internal method: actions of the context menu, delete or rearrange of clips +function obj:manageClip(row, action) + print("manageClip row:" .. row .. ",action:" .. action) + if action == 0 then + table.remove(clipboard_history, row) + elseif action == 2 then + local i = 1 + local j = row + while i < j do + clipboard_history[i], clipboard_history[j] = clipboard_history[j], clipboard_history[i] + i = i + 1 + j = j - 1 + end + else + local value = clipboard_history[row] + local new = row + action + if new < 1 then + new = 1 + end + if new < row then + table.move(clipboard_history, new, row - 1, new + 1) + else + table.move(clipboard_history, row + 1, new, row) + end + clipboard_history[new] = value + end + self.selectorobj:refreshChoicesCallback() +end + +-- Internal method: +function obj:_showContextMenu(row) + print("_showContextMenu row:" .. row) + point = hs.mouse.getAbsolutePosition() + local menu = hs.menubar.new(false) + local menuTable = { + { + title = "Alle Schnipsel mit Tab einfügen", + fn = hs.fnutils.partial(self.pasteAllWithDelimiter, self, row, "\t"), + }, + { + title = "Alle Schnipsel mit Zeilenvorschub einfügen", + fn = hs.fnutils.partial(self.pasteAllWithDelimiter, self, row, "\n"), + }, + { title = "-" }, + { title = "Eintrag entfernen", fn = hs.fnutils.partial(self.manageClip, self, row, 0) }, + { title = "Eintrag an erste Stelle", fn = hs.fnutils.partial(self.manageClip, self, row, -100) }, + { title = "Eintrag nach oben", fn = hs.fnutils.partial(self.manageClip, self, row, -1) }, + { title = "Eintrag nach unten", fn = hs.fnutils.partial(self.manageClip, self, row, 1) }, + { title = "Tabelle invertieren", fn = hs.fnutils.partial(self.manageClip, self, row, 2) }, + { title = "-" }, + { title = "disabled item", disabled = true }, + { title = "checked item", checked = true }, + } + menu:setMenu(menuTable) + menu:popupMenu(point) + print(hs.inspect(point)) +end + +-- Internal function - fill in the chooser options, including the control options +function obj:_populateChooser(query) + query = query:lower() + menuData = {} + for k, v in pairs(clipboard_history) do + if v.type == "text" and (query == "" or v.content:lower():find(query)) then + table.insert( + menuData, + { text = string.sub(v.content, 0, obj.display_max_length), data = v.content, type = v.type } + ) + elseif v.type == "image" then + table.insert( + menuData, + { text = "《Image data》", type = v.type, data = v.content, image = hs.image.imageFromURL(v.content) } + ) + end + end + if #menuData == 0 then + table.insert( + menuData, + { text = "", subText = "《Clipboard is empty》", action = "none", image = hs.image.imageFromName("NSCaution") } + ) + else + table.insert( + menuData, + { text = "《Clear Clipboard History》", action = "clear", image = hs.image.imageFromName("NSTrashFull") } + ) + end + table.insert(menuData, { + text = "《" .. (self.paste_on_select and "Disable" or "Enable") .. " Paste-on-select》", + action = "toggle_paste_on_select", + image = (self.paste_on_select and hs.image.imageFromName("NSSwitchEnabledOn") or hs.image.imageFromName( + "NSSwitchEnabledOff" + )), + }) + table.insert(menuData, { + text = "《" .. (self.max_size and "Disable" or "Enable") .. " max size " .. self.max_entry_size .. "》", + action = "toggle_max_size", + image = (self.max_size and hs.image.imageFromName("NSSwitchEnabledOn") or hs.image.imageFromName( + "NSSwitchEnabledOff" + )), + }) + self.logger.df("Returning menuData = %s", hs.inspect(menuData)) + return menuData +end + +--- ClipboardTool:shouldBeStored() +--- Method +--- Verify whether the pasteboard contents matches one of the values in `ClipboardTool.ignoredIdentifiers` +--- +--- Parameters: +--- * None +function obj:shouldBeStored() + -- Code from https://github.com/asmagill/hammerspoon-config/blob/master/utils/_menus/newClipper.lua + local goAhead = true + for i, v in ipairs(hs.pasteboard.pasteboardTypes()) do + if self.ignoredIdentifiers[v] then + goAhead = false + break + end + end + if goAhead then + for i, v in ipairs(hs.pasteboard.contentTypes()) do + if self.ignoredIdentifiers[v] then + goAhead = false + break + end + end + end + return goAhead +end + +-- Internal method: +function obj:reduceSize(text) + print(#text .. " ? " .. tostring(max_entry_size)) + local endingpos = 3000 + local lastLowerPos = 3000 + repeat + lastLowerPos = endingpos + _, endingpos = string.find(text, "\n\n", endingpos + 1) + print("endingpos:" .. endingpos) + until endingpos > obj.max_entry_size + return string.sub(text, 1, lastLowerPos) +end + +--- ClipboardTool:checkAndStorePasteboard() +--- Method +--- If the pasteboard has changed, we add the current item to our history and update the counter +--- +--- Parameters: +--- * None +function obj:checkAndStorePasteboard() + now = pasteboard.changeCount() + if now > last_change then + if (not self.honor_ignoredidentifiers) or self:shouldBeStored() then + current_clipboard = pasteboard.getContents() + self.logger.df("current_clipboard = %s", tostring(current_clipboard)) + if (current_clipboard == nil) and (pasteboard.readImage() ~= nil) then + current_clipboard = pasteboard.readImage() + self:pasteboardToClipboard("image", current_clipboard:encodeAsURLString()) + if self.show_copied_alert then + hs.alert.show("Copied image") + end + self.logger.df( + "Adding image (hashed) %s to clipboard history clipboard", + hashfn(current_clipboard:encodeAsURLString()) + ) + elseif current_clipboard ~= nil then + local size = #current_clipboard + if obj.max_size and size > obj.max_entry_size then + local answer = hs.dialog.blockAlert( + "Clipboard", + "The maximum size of " .. obj.max_entry_size .. " was exceeded.", + "Copy partially", + "Copy all", + "NSCriticalAlertStyle" + ) + print("answer: " .. answer) + if answer == "Copy partially" then + current_clipboard = self:reduceSize(current_clipboard) + size = #current_clipboard + end + end + if self.show_copied_alert then + hs.alert.show("Copied " .. size .. " chars") + end + self.logger.df("Adding %s to clipboard history", current_clipboard) + self:pasteboardToClipboard("text", current_clipboard) + else + self.logger.df("Ignoring nil clipboard content") + end + else + self.logger.df("Ignoring pasteboard entry because it matches ignoredIdentifiers") + end + last_change = now + end +end + +--- ClipboardTool:start() +--- Method +--- Start the clipboard history collector +--- +--- Parameters: +--- * None +function obj:start() + obj.logger.level = 0 + clipboard_history = self:dedupe_and_resize(getSetting("items", {})) -- If no history is saved on the system, create an empty history + last_change = pasteboard.changeCount() -- keeps track of how many times the pasteboard owner has changed // Indicates a new copy has been made + self.selectorobj = hs.chooser.new(hs.fnutils.partial(self._processSelectedItem, self)) + self.selectorobj:choices(hs.fnutils.partial(self._populateChooser, self, "")) + self.selectorobj:queryChangedCallback(function(query) + self.selectorobj:choices(hs.fnutils.partial(self._populateChooser, self, query)) + end) + self.selectorobj:rightClickCallback(hs.fnutils.partial(self._showContextMenu, self)) + --Checks for changes on the pasteboard. Is it possible to replace with eventtap? + self.timer = hs.timer.new(self.frequency, hs.fnutils.partial(self.checkAndStorePasteboard, self)) + self.timer:start() + if self.show_in_menubar then + self.menubaritem = + hs.menubar.new():setTitle(obj.menubar_title):setClickCallback(hs.fnutils.partial(self.toggleClipboard, self)) + end +end + +--- ClipboardTool:showClipboard() +--- Method +--- Display the current clipboard list in a chooser +--- +--- Parameters: +--- * None +function obj:showClipboard() + if self.selectorobj ~= nil then + self.selectorobj:refreshChoicesCallback() + self.prevFocusedWindow = hs.window.focusedWindow() + self.selectorobj:show() + else + hs.notify.show("ClipboardTool not properly initialized", "Did you call ClipboardTool:start()?", "") + end +end + +--- ClipboardTool:toggleClipboard() +--- Method +--- Show/hide the clipboard list, depending on its current state +--- +--- Parameters: +--- * None +function obj:toggleClipboard() + if self.selectorobj:isVisible() then + self.selectorobj:hide() + else + self:showClipboard() + end +end + +--- ClipboardTool:bindHotkeys(mapping) +--- Method +--- Binds hotkeys for ClipboardTool +--- +--- Parameters: +--- * mapping - A table containing hotkey objifier/key details for the following items: +--- * show_clipboard - Display the clipboard history chooser +--- * toggle_clipboard - Show/hide the clipboard history chooser +function obj:bindHotkeys(mapping) + local def = { + show_clipboard = hs.fnutils.partial(self.showClipboard, self), + toggle_clipboard = hs.fnutils.partial(self.toggleClipboard, self), + } + hs.spoons.bindHotkeysToSpec(def, mapping) + obj.mapping = mapping +end + +return obj diff --git a/hammerspoon/Spoons/PaperWM.spoon b/hammerspoon/Spoons/PaperWM.spoon @@ -0,0 +1 @@ +Subproject commit 5bc47411eaf4dbbfb950d23d1ec7daf3a0427470 diff --git a/hammerspoon/Spoons/Swipe.spoon b/hammerspoon/Spoons/Swipe.spoon @@ -0,0 +1 @@ +Subproject commit c56520507d98e663ae0e1228e41cac690557d4aa diff --git a/hammerspoon/auto-toggle-vertical-horizontal-tabs.lua b/hammerspoon/auto-toggle-vertical-horizontal-tabs.lua @@ -0,0 +1,20 @@ +-- AUTOMATICALLY SWITCH BETWEEN VERTICAL AND HORIZONTAL TABS (IN BRAVE) +-- Caveat: does not work when opening tabs in the background though, since the +-- window title does not change then :/ +PrevTabCount = 0 +Wf_braveWindowTitle = hs.window.filter + .new({ "Brave Browser" }) + :setOverrideFilter({ allowRoles = "AXStandardWindow" }) + :subscribe(hs.window.filter.windowTitleChanged, function() + local success, tabCount = hs.osascript.applescript('tell application "Brave Browser" to count tab in first window') + if not success then + return + end + local threshold = 9 + if (tabCount > threshold and PrevTabCount <= threshold) or (tabCount <= threshold and PrevTabCount > threshold) then + -- ctrl-alt-shift-9 bound to Vertical Tab Toggling in Brave Settings + -- brave://settings/system/shortcuts + hs.eventtap.keyStroke({ "ctrl", "alt", "shift" }, "9", 0, "Brave Browser") + end + PrevTabCount = tabCount + end) diff --git a/hammerspoon/init.lua b/hammerspoon/init.lua @@ -0,0 +1,108 @@ +local hyper = { "alt", "cmd", "shift", "ctrl" } +local eventtap = hs.eventtap +local events = hs.eventtap.event.types +local eventProps = hs.eventtap.event.properties +local spaces = hs.spaces +local bindkey = hs.hotkey.bind + +-- Mouse events +local mouse = { + l = eventtap.leftClick, + r = eventtap.rightClick, + pos = hs.mouse.absolutePosition, +} + +-- Press a key +local key = eventtap.keyStroke + +-- Set up WM +require("spoonconfig.paperwm").setup({ + paperWmMod = { "alt", "shift" }, + paperWmMoveMod = { "alt", "ctrl", "shift" }, +}) + +hs.loadSpoon("ClipboardTool") +spoon.ClipboardTool:bindHotkeys({ + show_clipboard = { hyper, "c" }, +}) +spoon.ClipboardTool.show_copied_alert = false +spoon.ClipboardTool:start() + +require("spoonconfig.swipe") + +require("numpad-cursor") + +-- Automatically switch vertical/horizontal tabs in Brave +require("auto-toggle-vertical-horizontal-tabs") + +bindkey(hyper, "r", function() + hs.reload() + hs.notify.show("Config reloaded", "New config applied", "") +end) + +-- Copy current mouse position +bindkey(hyper, "m", function() + local pos = mouse.pos() + hs.pasteboard.setContents(string.format("{x = %f, y = %f}", pos.x, pos.y)) +end) + +-- "fn" is actually needed for this to work for some reason +-- https://github.com/Hammerspoon/hammerspoon/issues/1946#issuecomment-449604954 +bindkey({}, "f6", function() + key({ "fn", "ctrl" }, "right") +end) +bindkey({}, "f5", function() + key({ "fn", "ctrl" }, "left") +end) + +-- Toggle terminal +bindkey({ "alt" }, "space", function() + local appName = "Alacritty" + local runningApp = hs.application.get(appName) + if runningApp ~= nil and runningApp:isFrontmost() then + runningApp:hide() + else + hs.application.open(appName) + end +end) + +-- Emacs +bindkey(hyper, "e", function() + hs.application.open("Emacs") +end) + +-- Play/pause mpd +bindkey({}, 100, function() + hs.execute("/usr/local/bin/mpc toggle") +end) + +-- bind other keyboard keys +Kbd_tap = eventtap.new({ hs.eventtap.event.types.systemDefined }, function(ev) + -- http://www.hammerspoon.org/docs/hs.eventtap.event.html#systemKey + ev = ev:systemKey() + -- http://stackoverflow.com/a/1252776/1521064 + local next = next + -- Check empty table + if next(ev) then + if ev.key == "EJECT" and ev.down then + hs.execute("/run/current-system/sw/bin/alacritty -e /usr/local/bin/ncmpcpp") + end + end +end) +Kbd_tap:start() + +-- bind mouse keys +Mousebtn_tap = eventtap.new({ events.otherMouseDown }, function(ev) + local mouseBtn = ev:getProperty(eventProps.mouseEventButtonNumber) + if mouseBtn == 3 then + spaces.toggleLaunchPad() + return true + elseif mouseBtn == 4 then + spaces.toggleMissionControl() + return true + end + return false +end) +Mousebtn_tap:start() + +-- require("mydebug"):monitorKeys() diff --git a/hammerspoon/mydebug.lua b/hammerspoon/mydebug.lua @@ -0,0 +1,26 @@ +local eventtap = hs.eventtap +local events = hs.eventtap.event.types +local eventProps = hs.eventtap.event.properties +local log = hs.logger.new("init", "debug") + +local monitorKeys = function() + eventtap + .new( + { events.keyDown, events.otherMouseDown }, + function(event) --watch the keyDown event, trigger the function every time there is a keydown + local evtype = event:getType() + if evtype == events.keyDown then + log.i(events[evtype], event:getUnicodeString(), event:getKeyCode()) + elseif evtype == events.otherMouseDown then + log.i(events[evtype], event:getProperty(eventProps.mouseEventButtonNumber)) + end + return false --keeps the event propagating + end + ) + :start() --start our watcher +end +return { + monitorKeys = function() + monitorKeys() + end, +} diff --git a/hammerspoon/numpad-cursor.lua b/hammerspoon/numpad-cursor.lua @@ -0,0 +1,60 @@ +local icons = { + [[ASCII: +2.................3 +................... +................... +................... +................... +................... +................... +................... +................... +1.................4]], +} + +local keypad_cursor = hs.hotkey.modal.new({}, 71) + +-- When enabling, show a menubar icon +function keypad_cursor:entered() + keypad_cursor._menubar_icon = hs.menubar.new() + keypad_cursor._menubar_icon:setTitle("NK") + keypad_cursor._menubar_icon:setIcon(icons[1]) +end + +-- When disabling, hide the menubar icon +function keypad_cursor:exited() + keypad_cursor._menubar_icon:delete() +end + +local keyNoDelay = function(modifiers, character) + hs.eventtap.event.newKeyEvent(modifiers, string.lower(character), true):post() + hs.eventtap.event.newKeyEvent(modifiers, string.lower(character), false):post() +end + +local binds = { + [86] = function() + keyNoDelay({}, "left") + end, + [91] = function() + keyNoDelay({}, "up") + end, + [88] = function() + keyNoDelay({}, "right") + end, + [84] = function() + keyNoDelay({}, "down") + end, + [89] = function() + keyNoDelay({}, "home") + end, + [83] = function() + keyNoDelay({}, "end") + end, + [71] = function() + keypad_cursor:exit() + end, +} + +for k, f in pairs(binds) do + keypad_cursor:bind({}, k, f, nil, f) +end diff --git a/hammerspoon/spoonconfig/paperwm.lua b/hammerspoon/spoonconfig/paperwm.lua @@ -0,0 +1,65 @@ +return { + setup = function(mods) + PaperWM = hs.loadSpoon("PaperWM") + local paperWmMod = mods.paperWmMod + local paperWmMoveMod = mods.paperWmMoveMod + + PaperWM:bindHotkeys({ + -- switch to a new focused window in tiled grid + focus_left = { paperWmMod, "left" }, + focus_right = { paperWmMod, "right" }, + focus_up = { paperWmMod, "up" }, + focus_down = { paperWmMod, "down" }, + + -- switch windows by cycling forward/backward + -- (forward = down or right, backward = up or left) + focus_prev = { paperWmMod, "k" }, + focus_next = { paperWmMod, "j" }, + + -- move windows around in tiled grid + swap_left = { paperWmMoveMod, "left" }, + swap_left = { paperWmMoveMod, "h" }, + swap_right = { paperWmMoveMod, "right" }, + swap_right = { paperWmMoveMod, "l" }, + swap_up = { paperWmMoveMod, "up" }, + swap_up = { paperWmMoveMod, "k" }, + swap_down = { paperWmMoveMod, "down" }, + swap_down = { paperWmMoveMod, "j" }, + + -- alternative: swap entire columns, rather than + -- individual windows (to be used instead of + -- swap_left / swap_right bindings) + -- swap_column_left = {{"alt", "cmd", "shift"}, "left"}, + -- swap_column_right = {{"alt", "cmd", "shift"}, "right"}, + + -- position and resize focused window + center_window = { paperWmMod, "c" }, + full_width = { paperWmMod, "f" }, + cycle_width = { paperWmMod, "r" }, + cycle_height = { paperWmMoveMod, "r" }, + + -- increase/decrease width + increase_width = { paperWmMod, "l" }, + decrease_width = { paperWmMod, "h" }, + + -- move focused window into / out of a column + slurp_in = { paperWmMod, "i" }, + barf_out = { paperWmMod, "o" }, + + -- move the focused window into / out of the tiling layer + toggle_floating = { { "alt", "cmd", "shift" }, "escape" }, + + -- move focused window to a new space and tile + move_window_1 = { paperWmMoveMod, "1" }, + move_window_2 = { paperWmMoveMod, "2" }, + move_window_3 = { paperWmMoveMod, "3" }, + move_window_4 = { paperWmMoveMod, "4" }, + move_window_5 = { paperWmMoveMod, "5" }, + move_window_6 = { paperWmMoveMod, "6" }, + move_window_7 = { paperWmMoveMod, "7" }, + move_window_8 = { paperWmMoveMod, "8" }, + move_window_9 = { paperWmMoveMod, "9" }, + }) + PaperWM:start() + end, +} diff --git a/hammerspoon/spoonconfig/swipe.lua b/hammerspoon/spoonconfig/swipe.lua @@ -0,0 +1,25 @@ +-- use three finger swipe to focus nearby window +local key = hs.eventtap.keyStroke +local current_id, threshold +Swipe = hs.loadSpoon("Swipe") +Swipe:start(3, function(direction, distance, id) + if id == current_id then + if distance > threshold then + threshold = math.huge -- only trigger once per swipe + + -- use "natural" scrolling + if direction == "left" then + key({ "ctrl", "shift" }, "tab") + elseif direction == "right" then + key({ "ctrl" }, "tab") + elseif direction == "up" then + key({ "cmd" }, "t") + elseif direction == "down" then + key({ "cmd" }, "w") + end + end + else + current_id = id + threshold = 0.2 -- swipe distance > 10% of trackpad + end +end)