diff --git a/hammerspoon/.hammerspoon/init.lua b/hammerspoon/.hammerspoon/init.lua new file mode 100644 index 0000000..8671f61 --- /dev/null +++ b/hammerspoon/.hammerspoon/init.lua @@ -0,0 +1,325 @@ +-------------------------------------------------------------------------- +hyperModeAppMappings = { + { 'a', 'Alacritty' }, -- "A" for "Apple Music" + { 's', 'Safari' }, -- "S" for "Safari" + { 'c', 'Telegram' }, -- "C for "Chat" + { 'd', 'Discord' }, -- "D for "Discord" + { 'f', 'Finder' }, -- "F" for "Finder" + { 'g', 'Mail' }, -- "G" for "Gmail" + { 'z', 'Slack' }, -- "Z" for "zzz" + { 'n', 'Notion' }, -- "N" for "Notion" + { 'm', 'Music' }, -- "M" for "Music" + { 'w', 'World of Warcraft Classic' }, -- "W" for "World of Warcraft" +} + +for i, mapping in ipairs(hyperModeAppMappings) do + local key = mapping[1] + local app = mapping[2] + hs.hotkey.bind({'shift', 'ctrl', 'alt', 'cmd'}, key, function() + if (type(app) == 'string') then + hs.application.open(app) + elseif (type(app) == 'function') then + app() + else + hs.logger.new('hyper'):e('Invalid mapping for Hyper +', key) + end + end) +end +-------------------------------------------------------------------------- +hs.window.animationDuration = 0 + +-- +-----------------+ +-- | | | +-- | HERE | | +-- | | | +-- +-----------------+ +function hs.window.left(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + f.y = max.y + f.w = max.w / 2 + f.h = max.h + win:setFrame(f) +end + +-- +-----------------+ +-- | | | +-- | | HERE | +-- | | | +-- +-----------------+ +function hs.window.right(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + (max.w / 2) + f.y = max.y + f.w = max.w / 2 + f.h = max.h + win:setFrame(f) +end + +-- +-----------------+ +-- | HERE | +-- +-----------------+ +-- | | +-- +-----------------+ +function hs.window.up(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + f.w = max.w + f.y = max.y + f.h = max.h / 2 + win:setFrame(f) +end + +-- +-----------------+ +-- | | +-- +-----------------+ +-- | HERE | +-- +-----------------+ +function hs.window.down(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + f.w = max.w + f.y = max.y + (max.h / 2) + f.h = max.h / 2 + win:setFrame(f) +end + +-- +-----------------+ +-- | HERE | | +-- +--------+ | +-- | | +-- +-----------------+ +function hs.window.upLeft(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + f.y = max.y + f.w = max.w/2 + f.h = max.h/2 + win:setFrame(f) +end + +-- +-----------------+ +-- | | +-- +--------+ | +-- | HERE | | +-- +-----------------+ +function hs.window.downLeft(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + f.y = max.y + (max.h / 2) + f.w = max.w/2 + f.h = max.h/2 + win:setFrame(f) +end + +-- +-----------------+ +-- | | +-- | +--------| +-- | | HERE | +-- +-----------------+ +function hs.window.downRight(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + (max.w / 2) + f.y = max.y + (max.h / 2) + f.w = max.w/2 + f.h = max.h/2 + + win:setFrame(f) +end + +-- +-----------------+ +-- | | HERE | +-- | +--------| +-- | | +-- +-----------------+ +function hs.window.upRight(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + (max.w / 2) + f.y = max.y + f.w = max.w/2 + f.h = max.h/2 + win:setFrame(f) +end + +-- +--------------+ +-- | | | | +-- | | HERE | | +-- | | | | +-- +---------------+ +function hs.window.centerWithFullHeight(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + (max.w / 5) + f.w = max.w * 3/5 + f.y = max.y + f.h = max.h + win:setFrame(f) +end + +-- +-----------------+ +-- | | | +-- | HERE | | +-- | | | +-- +-----------------+ +function hs.window.left40(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + f.y = max.y + f.w = max.w * 0.4 + f.h = max.h + win:setFrame(f) +end + +-- +-----------------+ +-- | | | +-- | | HERE | +-- | | | +-- +-----------------+ +function hs.window.right60(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + (max.w * 0.4) + f.y = max.y + f.w = max.w * 0.6 + f.h = max.h + win:setFrame(f) +end + +function hs.window.nextScreen(win) + local currentScreen = win:screen() + local allScreens = hs.screen.allScreens() + currentScreenIndex = hs.fnutils.indexOf(allScreens, currentScreen) + nextScreenIndex = currentScreenIndex + 1 + + if allScreens[nextScreenIndex] then + win:moveToScreen(allScreens[nextScreenIndex]) + else + win:moveToScreen(allScreens[1]) + end +end + +windowLayoutMode = hs.hotkey.modal.new({}, 'F16') + +windowLayoutMode.entered = function() + windowLayoutMode.statusMessage:show() +end +windowLayoutMode.exited = function() + windowLayoutMode.statusMessage:hide() +end + +-- Bind the given key to call the given function and exit WindowLayout mode +function windowLayoutMode.bindWithAutomaticExit(mode, modifiers, key, fn) + mode:bind(modifiers, key, function() + mode:exit() + fn() + end) +end + +local status, windowMappings = pcall(require, 'keyboard.windows-bindings') + +-- if not status then +-- windowMappings = require('keyboard.windows-bindings-defaults') +-- end + +local windowMappings = { + modifiers = {'ctrl'}, + showHelp = false, + trigger = 's', + mappings = { + { {}, 'return', 'maximize' }, + { {}, 'space', 'centerWithFullHeight' }, + { {}, 'h', 'left' }, + { {}, 'j', 'down' }, + { {}, 'k', 'up' }, + { {}, 'l', 'right' }, + { {'shift'}, 'h', 'left40' }, + { {'shift'}, 'l', 'right60' }, + { {}, 'i', 'upLeft' }, + { {}, 'o', 'upRight' }, + { {}, ',', 'downLeft' }, + { {}, '.', 'downRight' }, + { {}, 'n', 'nextScreen' }, + { {}, 'right', 'moveOneScreenEast' }, + { {}, 'left', 'moveOneScreenWest' }, + } +} + +local modifiers = windowMappings.modifiers +local showHelp = windowMappings.showHelp +local trigger = windowMappings.trigger +local mappings = windowMappings.mappings + +function getModifiersStr(modifiers) + local modMap = { shift = '⇧', ctrl = '⌃', alt = '⌥', cmd = '⌘' } + local retVal = '' + + for i, v in ipairs(modifiers) do + retVal = retVal .. modMap[v] + end + + return retVal +end + +local msgStr = getModifiersStr(modifiers) +msgStr = 'Window Layout Mode (' .. msgStr .. (string.len(msgStr) > 0 and '+' or '') .. trigger .. ')' + +for i, mapping in ipairs(mappings) do + local modifiers, trigger, winFunction = table.unpack(mapping) + local hotKeyStr = getModifiersStr(modifiers) + + if showHelp == true then + if string.len(hotKeyStr) > 0 then + msgStr = msgStr .. (string.format('\n%10s+%s => %s', hotKeyStr, trigger, winFunction)) + else + msgStr = msgStr .. (string.format('\n%11s => %s', trigger, winFunction)) + end + end + + windowLayoutMode:bindWithAutomaticExit(modifiers, trigger, function() + --example: hs.window.focusedWindow():upRight() + local fw = hs.window.focusedWindow() + fw[winFunction](fw) + end) +end + +local message = require('keyboard.status-message') +windowLayoutMode.statusMessage = message.new(msgStr) + +-- Use modifiers+trigger to toggle WindowLayout Mode +hs.hotkey.bind(modifiers, trigger, function() + windowLayoutMode:enter() +end) +windowLayoutMode:bind(modifiers, trigger, function() + windowLayoutMode:exit() +end) diff --git a/hammerspoon/.hammerspoon/keyboard/control-escape.lua b/hammerspoon/.hammerspoon/keyboard/control-escape.lua new file mode 100644 index 0000000..2624ed6 --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/control-escape.lua @@ -0,0 +1,43 @@ +-- Credit for this implementation goes to @arbelt and @jasoncodes 🙇⚡️😻 +-- +-- https://gist.github.com/arbelt/b91e1f38a0880afb316dd5b5732759f1 +-- https://github.com/jasoncodes/dotfiles/blob/ac9f3ac/hammerspoon/control_escape.lua + +sendEscape = false +lastMods = {} + +ctrlKeyHandler = function() + sendEscape = false +end + +ctrlKeyTimer = hs.timer.delayed.new(0.15, ctrlKeyHandler) + +ctrlHandler = function(evt) + local newMods = evt:getFlags() + if lastMods["ctrl"] == newMods["ctrl"] then + return false + end + if not lastMods["ctrl"] then + lastMods = newMods + sendEscape = true + ctrlKeyTimer:start() + else + if sendEscape then + keyUpDown({}, 'escape') + end + lastMods = newMods + ctrlKeyTimer:stop() + end + return false +end + +ctrlTap = hs.eventtap.new({hs.eventtap.event.types.flagsChanged}, ctrlHandler) +ctrlTap:start() + +otherHandler = function(evt) + sendEscape = false + return false +end + +otherTap = hs.eventtap.new({hs.eventtap.event.types.keyDown}, otherHandler) +otherTap:start() diff --git a/hammerspoon/.hammerspoon/keyboard/delete-words.lua b/hammerspoon/.hammerspoon/keyboard/delete-words.lua new file mode 100644 index 0000000..c43a5bd --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/delete-words.lua @@ -0,0 +1,70 @@ +local log = hs.logger.new('delete-words.lua', 'debug') + +local isInTerminal = function() + app = hs.application.frontmostApplication():name() + return app == 'iTerm2' or app == 'Terminal' +end + +-- Use option + h to delete previous word +hs.hotkey.bind({'alt'}, 'h', function() + if isInTerminal() then + keyUpDown({'ctrl'}, 'w') + else + keyUpDown({'alt'}, 'delete') + end +end) + +-- Use option + l to delete next word +hs.hotkey.bind({'alt'}, 'l', function() + if isInTerminal() then + keyUpDown({}, 'escape') + keyUpDown({}, 'd') + else + keyUpDown({'alt'}, 'forwarddelete') + end +end) + +-- Use control + u to delete to beginning of line +-- +-- In bash, control + u automatically deletes to the beginning of the line, so +-- we don't need (or want) this hotkey in the terminal. If this hotkey was +-- enabled in the terminal, it would break the standard control + u behavior. +-- Therefore, we only enable this hotkey for non-terminal apps. +local wf = hs.window.filter.new():setFilters({iTerm2 = false, Terminal = false}) +enableHotkeyForWindowsMatchingFilter(wf, hs.hotkey.new({'ctrl'}, 'u', function() + keyUpDown({'cmd'}, 'delete') +end)) + +-- Use control + ; to delete to end of line +-- +-- I prefer to use control+h/j/k/l to move left/down/up/right by one pane in all +-- multi-pane apps (e.g., iTerm, various editors). That's convenient and +-- consistent, but it conflicts with the default macOS binding for deleting to +-- the end of the line (i.e., control+k). To maintain that very useful +-- functionality, and to keep it on the home row, this hotkey binds control+; to +-- delete to the end of the line. +hs.hotkey.bind({'ctrl'}, ';', function() + -- If we're in the terminal, then temporarily disable our custom control+k + -- hotkey used for pane navigation, then fire control+k to delete to the end + -- of the line, and then renable the control+k hotkey. + -- + -- If we're not in the terminal, then just select to the end of the line and + -- then delete the selected text. + if isInTerminal() then + hotkeyForControlK = hs.fnutils.find(hs.hotkey.getHotkeys(), function(hotkey) + return hotkey.idx == '⌃K' + end) + if hotkeyForControlK then hotkeyForControlK:disable() end + + keyUpDown({'ctrl'}, 'k') + + -- Allow some time for the control+k keystroke to fire asynchronously before + -- we re-enable our custom control+k hotkey. + hs.timer.doAfter(0.2, function() + if hotkeyForControlK then hotkeyForControlK:enable() end + end) + else + keyUpDown({'cmd', 'shift'}, 'right') + keyUpDown({}, 'forwarddelete') + end +end) diff --git a/hammerspoon/.hammerspoon/keyboard/hyper-apps-defaults.lua b/hammerspoon/.hammerspoon/keyboard/hyper-apps-defaults.lua new file mode 100644 index 0000000..ac51019 --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/hyper-apps-defaults.lua @@ -0,0 +1,14 @@ +-- Default keybindings for launching apps in Hyper Mode +-- +-- To launch _your_ most commonly-used apps via Hyper Mode, create a copy of +-- this file, save it as `hyper-apps.lua`, and edit the table below to configure +-- your preferred shortcuts. +return { + { 'a', 'Alacritty' }, -- "A" for "Apple Music" + { 's', 'Safari' }, -- "B" for "Browser" + { 'c', 'Telegram' }, -- "C for "Chat" + { 'f', 'Finder' }, -- "F" for "Finder" + { 'g', 'Mail' }, -- "G" for "Gmail" + { 'z', 'Slack' }, -- "S" for "Slack" + -- { 't', 'Telegram' }, -- "T" for "Terminal" +} diff --git a/hammerspoon/.hammerspoon/keyboard/hyper.lua b/hammerspoon/.hammerspoon/keyboard/hyper.lua new file mode 100644 index 0000000..7f99fe8 --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/hyper.lua @@ -0,0 +1,19 @@ +local status, hyperModeAppMappings = pcall(require, 'keyboard.hyper-apps') + +if not status then + hyperModeAppMappings = require('keyboard.hyper-apps-defaults') +end + +for i, mapping in ipairs(hyperModeAppMappings) do + local key = mapping[1] + local app = mapping[2] + hs.hotkey.bind({'shift', 'ctrl', 'alt', 'cmd'}, key, function() + if (type(app) == 'string') then + hs.application.open(app) + elseif (type(app) == 'function') then + app() + else + hs.logger.new('hyper'):e('Invalid mapping for Hyper +', key) + end + end) +end diff --git a/hammerspoon/.hammerspoon/keyboard/init.lua b/hammerspoon/.hammerspoon/keyboard/init.lua new file mode 100644 index 0000000..4b10359 --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/init.lua @@ -0,0 +1,42 @@ +local log = hs.logger.new('init.lua', 'debug') + +-- Use Control+` to reload Hammerspoon config +hs.hotkey.bind({'ctrl'}, '`', nil, function() + hs.reload() +end) + +keyUpDown = function(modifiers, key) + -- Un-comment & reload config to log each keystroke that we're triggering + -- log.d('Sending keystroke:', hs.inspect(modifiers), key) + + hs.eventtap.keyStroke(modifiers, key, 0) +end + +-- Subscribe to the necessary events on the given window filter such that the +-- given hotkey is enabled for windows that match the window filter and disabled +-- for windows that don't match the window filter. +-- +-- windowFilter - An hs.window.filter object describing the windows for which +-- the hotkey should be enabled. +-- hotkey - The hs.hotkey object to enable/disable. +-- +-- Returns nothing. +enableHotkeyForWindowsMatchingFilter = function(windowFilter, hotkey) + windowFilter:subscribe(hs.window.filter.windowFocused, function() + hotkey:enable() + end) + + windowFilter:subscribe(hs.window.filter.windowUnfocused, function() + hotkey:disable() + end) +end + +require('keyboard.control-escape') +require('keyboard.delete-words') +require('keyboard.hyper') +require('keyboard.markdown') +require('keyboard.microphone') +require('keyboard.panes') +require('keyboard.windows') + +hs.notify.new({title='Hammerspoon', informativeText='Ready to rock 🤘'}):send() diff --git a/hammerspoon/.hammerspoon/keyboard/markdown.lua b/hammerspoon/.hammerspoon/keyboard/markdown.lua new file mode 100644 index 0000000..3497520 --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/markdown.lua @@ -0,0 +1,112 @@ +function wrapSelectedText(wrapCharacters) + -- Preserve the current contents of the system clipboard + local originalClipboardContents = hs.pasteboard.getContents() + + -- Copy the currently-selected text to the system clipboard + keyUpDown('cmd', 'c') + + -- Allow some time for the command+c keystroke to fire asynchronously before + -- we try to read from the clipboard + hs.timer.doAfter(0.2, function() + -- Construct the formatted output and paste it over top of the + -- currently-selected text + local selectedText = hs.pasteboard.getContents() + local wrappedText = wrapCharacters .. selectedText .. wrapCharacters + hs.pasteboard.setContents(wrappedText) + keyUpDown('cmd', 'v') + + -- Allow some time for the command+v keystroke to fire asynchronously before + -- we restore the original clipboard + hs.timer.doAfter(0.2, function() + hs.pasteboard.setContents(originalClipboardContents) + end) + end) +end + +function inlineLink() + -- Fetch URL from the system clipboard + local linkUrl = hs.pasteboard.getContents() + + -- Copy the currently-selected text to use as the link text + keyUpDown('cmd', 'c') + + -- Allow some time for the command+c keystroke to fire asynchronously before + -- we try to read from the clipboard + hs.timer.doAfter(0.2, function() + -- Construct the formatted output and paste it over top of the + -- currently-selected text + local linkText = hs.pasteboard.getContents() + local markdown = '[' .. linkText .. '](' .. linkUrl .. ')' + hs.pasteboard.setContents(markdown) + keyUpDown('cmd', 'v') + + -- Allow some time for the command+v keystroke to fire asynchronously before + -- we restore the original clipboard + hs.timer.doAfter(0.2, function() + hs.pasteboard.setContents(linkUrl) + end) + end) +end + +-------------------------------------------------------------------------------- +-- Define Markdown Mode +-- +-- Markdown Mode allows you to perform common Markdown-formatting tasks anywhere +-- that you're editing text. Use Control+m to turn on Markdown mode. Then, use +-- any shortcut below to perform a formatting action. For example, to format the +-- selected text as bold in Markdown, hit Control+m, and then b. +-- +-- b => wrap the selected text in double asterisks ("b" for "bold") +-- c => wrap the selected text in backticks ("c" for "code") +-- i => wrap the selected text in single asterisks ("i" for "italic") +-- s => wrap the selected text in double tildes ("s" for "strikethrough") +-- l => convert the currently-selected text to an inline link, using a URL +-- from the clipboard ("l" for "link") +-------------------------------------------------------------------------------- + +markdownMode = hs.hotkey.modal.new({}, 'F20') + +local message = require('keyboard.status-message') +markdownMode.statusMessage = message.new('Markdown Mode (control-m)') +markdownMode.entered = function() + markdownMode.statusMessage:show() +end +markdownMode.exited = function() + markdownMode.statusMessage:hide() +end + +-- Bind the given key to call the given function and exit Markdown mode +function markdownMode.bindWithAutomaticExit(mode, key, fn) + mode:bind({}, key, function() + mode:exit() + fn() + end) +end + +markdownMode:bindWithAutomaticExit('b', function() + wrapSelectedText('**') +end) + +markdownMode:bindWithAutomaticExit('i', function() + wrapSelectedText('*') +end) + +markdownMode:bindWithAutomaticExit('s', function() + wrapSelectedText('~~') +end) + +markdownMode:bindWithAutomaticExit('l', function() + inlineLink() +end) + +markdownMode:bindWithAutomaticExit('c', function() + wrapSelectedText('`') +end) + +-- Use Control+m to toggle Markdown Mode +hs.hotkey.bind({'ctrl'}, 'm', function() + markdownMode:enter() +end) +markdownMode:bind({'ctrl'}, 'm', function() + markdownMode:exit() +end) diff --git a/hammerspoon/.hammerspoon/keyboard/microphone.lua b/hammerspoon/.hammerspoon/keyboard/microphone.lua new file mode 100644 index 0000000..fe24506 --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/microphone.lua @@ -0,0 +1,60 @@ +local message = require('keyboard.status-message') + +local messageMuting = message.new('muted 🎤') +local messageHot = message.new('hot 🎤') +local lastMods = {} +local recentlyClicked = false +local secondClick = false + +displayStatus = function() + -- Check if the active mic is muted + if hs.audiodevice.defaultInputDevice():muted() then + messageMuting:notify() + else + messageHot:notify() + end +end +displayStatus() + +toggle = function(device) + if device:muted() then + device:setMuted(false) + else + device:setMuted(true) + end +end + +fnKeyHandler = function() + recentlyClicked = false +end + +controlKeyTimer = hs.timer.delayed.new(0.3, fnKeyHandler) + +fnHandler = function(event) + local device = hs.audiodevice.defaultInputDevice() + local newMods = event:getFlags() + + -- fn keyDown + if newMods['fn'] == true then + toggle(device) + if recentlyClicked == true then + displayStatus() + secondClick = true + end + recentlyClicked = true + controlKeyTimer:start() + + -- fn keyUp + elseif lastMods['fn'] == true and newMods['fn'] == nil then + if secondClick then + secondClick = false + else + toggle(device) + end + end + + lastMods = newMods +end + +fnKey = hs.eventtap.new({hs.eventtap.event.types.flagsChanged}, fnHandler) +fnKey:start() diff --git a/hammerspoon/.hammerspoon/keyboard/panes.lua b/hammerspoon/.hammerspoon/keyboard/panes.lua new file mode 100644 index 0000000..cc31ffe --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/panes.lua @@ -0,0 +1,43 @@ +local itermHotkeyMappings = { + -- Use control + dash to split panes horizontally + { + from = {{'ctrl'}, '-'}, + to = {{'cmd', 'shift'}, 'd'} + }, + + -- Use control + pipe to split panes vertically + { + from = {{'ctrl', 'shift'}, '\\'}, + to = {{'cmd'}, 'd'} + }, + + -- Use control + h/j/k/l to move left/down/up/right by one pane + { + from = {{'ctrl'}, 'h'}, + to = {{'cmd', 'alt'}, 'left'} + }, + { + from = {{'ctrl'}, 'j'}, + to = {{'cmd', 'alt'}, 'down'} + }, + { + from = {{'ctrl'}, 'k'}, + to = {{'cmd', 'alt'}, 'up'} + }, + { + from = {{'ctrl'}, 'l'}, + to = {{'cmd', 'alt'}, 'right'} + }, +} + +local terminalWindowFilter = hs.window.filter.new('iTerm2') +local itermHotkeys = hs.fnutils.each(itermHotkeyMappings, function(mapping) + local fromMods = mapping['from'][1] + local fromKey = mapping['from'][2] + local toMods = mapping['to'][1] + local toKey = mapping['to'][2] + local hotkey = hs.hotkey.new(fromMods, fromKey, function() + keyUpDown(toMods, toKey) + end) + enableHotkeyForWindowsMatchingFilter(terminalWindowFilter, hotkey) +end) diff --git a/hammerspoon/.hammerspoon/keyboard/status-message.lua b/hammerspoon/.hammerspoon/keyboard/status-message.lua new file mode 100644 index 0000000..9294ad2 --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/status-message.lua @@ -0,0 +1,67 @@ +local drawing = require 'hs.drawing' +local geometry = require 'hs.geometry' +local screen = require 'hs.screen' +local styledtext = require 'hs.styledtext' + +local statusmessage = {} +statusmessage.new = function(messageText) + local buildParts = function(messageText) + local frame = screen.primaryScreen():frame() + + local styledTextAttributes = { + font = { name = 'Monaco', size = 24 }, + } + + local styledText = styledtext.new('🔨 ' .. messageText, styledTextAttributes) + + local styledTextSize = drawing.getTextDrawingSize(styledText) + local textRect = { + x = frame.w - styledTextSize.w - 40, + y = frame.h - styledTextSize.h, + w = styledTextSize.w + 40, + h = styledTextSize.h + 40, + } + local text = drawing.text(textRect, styledText):setAlpha(0.7) + + local background = drawing.rectangle( + { + x = frame.w - styledTextSize.w - 45, + y = frame.h - styledTextSize.h - 3, + w = styledTextSize.w + 15, + h = styledTextSize.h + 6 + } + ) + background:setRoundedRectRadii(10, 10) + background:setFillColor({ red = 0, green = 0, blue = 0, alpha=0.6 }) + + return background, text + end + + return { + _buildParts = buildParts, + show = function(self) + self:hide() + + self.background, self.text = self._buildParts(messageText) + self.background:show() + self.text:show() + end, + hide = function(self) + if self.background then + self.background:delete() + self.background = nil + end + if self.text then + self.text:delete() + self.text = nil + end + end, + notify = function(self, seconds) + local seconds = seconds or 1 + self:show() + hs.timer.delayed.new(seconds, function() self:hide() end):start() + end + } +end + +return statusmessage diff --git a/hammerspoon/.hammerspoon/keyboard/windows-bindings-defaults.lua b/hammerspoon/.hammerspoon/keyboard/windows-bindings-defaults.lua new file mode 100644 index 0000000..ec6aaa3 --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/windows-bindings-defaults.lua @@ -0,0 +1,48 @@ +-- Default keybindings for WindowLayout Mode +-- +-- To customize the key bindings for WindowLayout Mode, create a copy of this +-- file, save it as `windows-bindings.lua`, and edit the table below to +-- configure your preferred shortcuts. + +-------------------------------------------------------------------------------- +-- Define WindowLayout Mode +-- +-- WindowLayout Mode allows you to manage window layout using keyboard shortcuts +-- that are on the home row, or very close to it. Use Control+s to turn +-- on WindowLayout mode. Then, use any shortcut below to perform a window layout +-- action. For example, to send the window left, press and release +-- Control+s, and then press h. +-- +-- h/j/k/l => send window to the left/bottom/top/right half of the screen +-- i => send window to the upper left quarter of the screen +-- o => send window to the upper right quarter of the screen +-- , => send window to the lower left quarter of the screen +-- . => send window to the lower right quarter of the screen +-- return => make window full screen +-- n => send window to the next monitor +-- left => send window to the monitor on the left (if there is one) +-- right => send window to the monitor on the right (if there is one) +-------------------------------------------------------------------------------- + +return { + modifiers = {'ctrl'}, + showHelp = false, + trigger = 's', + mappings = { + { {}, 'return', 'maximize' }, + { {}, 'space', 'centerWithFullHeight' }, + { {}, 'h', 'left' }, + { {}, 'j', 'down' }, + { {}, 'k', 'up' }, + { {}, 'l', 'right' }, + { {'shift'}, 'h', 'left40' }, + { {'shift'}, 'l', 'right60' }, + { {}, 'i', 'upLeft' }, + { {}, 'o', 'upRight' }, + { {}, ',', 'downLeft' }, + { {}, '.', 'downRight' }, + { {}, 'n', 'nextScreen' }, + { {}, 'right', 'moveOneScreenEast' }, + { {}, 'left', 'moveOneScreenWest' }, + } +} diff --git a/hammerspoon/.hammerspoon/keyboard/windows.lua b/hammerspoon/.hammerspoon/keyboard/windows.lua new file mode 100644 index 0000000..99b422b --- /dev/null +++ b/hammerspoon/.hammerspoon/keyboard/windows.lua @@ -0,0 +1,274 @@ +hs.window.animationDuration = 0 + +-- +-----------------+ +-- | | | +-- | HERE | | +-- | | | +-- +-----------------+ +function hs.window.left(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + f.y = max.y + f.w = max.w / 2 + f.h = max.h + win:setFrame(f) +end + +-- +-----------------+ +-- | | | +-- | | HERE | +-- | | | +-- +-----------------+ +function hs.window.right(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + (max.w / 2) + f.y = max.y + f.w = max.w / 2 + f.h = max.h + win:setFrame(f) +end + +-- +-----------------+ +-- | HERE | +-- +-----------------+ +-- | | +-- +-----------------+ +function hs.window.up(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + f.w = max.w + f.y = max.y + f.h = max.h / 2 + win:setFrame(f) +end + +-- +-----------------+ +-- | | +-- +-----------------+ +-- | HERE | +-- +-----------------+ +function hs.window.down(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + f.w = max.w + f.y = max.y + (max.h / 2) + f.h = max.h / 2 + win:setFrame(f) +end + +-- +-----------------+ +-- | HERE | | +-- +--------+ | +-- | | +-- +-----------------+ +function hs.window.upLeft(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + f.y = max.y + f.w = max.w/2 + f.h = max.h/2 + win:setFrame(f) +end + +-- +-----------------+ +-- | | +-- +--------+ | +-- | HERE | | +-- +-----------------+ +function hs.window.downLeft(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + f.y = max.y + (max.h / 2) + f.w = max.w/2 + f.h = max.h/2 + win:setFrame(f) +end + +-- +-----------------+ +-- | | +-- | +--------| +-- | | HERE | +-- +-----------------+ +function hs.window.downRight(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + (max.w / 2) + f.y = max.y + (max.h / 2) + f.w = max.w/2 + f.h = max.h/2 + + win:setFrame(f) +end + +-- +-----------------+ +-- | | HERE | +-- | +--------| +-- | | +-- +-----------------+ +function hs.window.upRight(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + (max.w / 2) + f.y = max.y + f.w = max.w/2 + f.h = max.h/2 + win:setFrame(f) +end + +-- +--------------+ +-- | | | | +-- | | HERE | | +-- | | | | +-- +---------------+ +function hs.window.centerWithFullHeight(win) + local f = win:frame() + local screen = win:screen() + local max = screen:fullFrame() + + f.x = max.x + (max.w / 5) + f.w = max.w * 3/5 + f.y = max.y + f.h = max.h + win:setFrame(f) +end + +-- +-----------------+ +-- | | | +-- | HERE | | +-- | | | +-- +-----------------+ +function hs.window.left40(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + f.y = max.y + f.w = max.w * 0.4 + f.h = max.h + win:setFrame(f) +end + +-- +-----------------+ +-- | | | +-- | | HERE | +-- | | | +-- +-----------------+ +function hs.window.right60(win) + local f = win:frame() + local screen = win:screen() + local max = screen:frame() + + f.x = max.x + (max.w * 0.4) + f.y = max.y + f.w = max.w * 0.6 + f.h = max.h + win:setFrame(f) +end + +function hs.window.nextScreen(win) + local currentScreen = win:screen() + local allScreens = hs.screen.allScreens() + currentScreenIndex = hs.fnutils.indexOf(allScreens, currentScreen) + nextScreenIndex = currentScreenIndex + 1 + + if allScreens[nextScreenIndex] then + win:moveToScreen(allScreens[nextScreenIndex]) + else + win:moveToScreen(allScreens[1]) + end +end + +windowLayoutMode = hs.hotkey.modal.new({}, 'F16') + +windowLayoutMode.entered = function() + windowLayoutMode.statusMessage:show() +end +windowLayoutMode.exited = function() + windowLayoutMode.statusMessage:hide() +end + +-- Bind the given key to call the given function and exit WindowLayout mode +function windowLayoutMode.bindWithAutomaticExit(mode, modifiers, key, fn) + mode:bind(modifiers, key, function() + mode:exit() + fn() + end) +end + +local status, windowMappings = pcall(require, 'keyboard.windows-bindings') + +if not status then + windowMappings = require('keyboard.windows-bindings-defaults') +end + +local modifiers = windowMappings.modifiers +local showHelp = windowMappings.showHelp +local trigger = windowMappings.trigger +local mappings = windowMappings.mappings + +function getModifiersStr(modifiers) + local modMap = { shift = '⇧', ctrl = '⌃', alt = '⌥', cmd = '⌘' } + local retVal = '' + + for i, v in ipairs(modifiers) do + retVal = retVal .. modMap[v] + end + + return retVal +end + +local msgStr = getModifiersStr(modifiers) +msgStr = 'Window Layout Mode (' .. msgStr .. (string.len(msgStr) > 0 and '+' or '') .. trigger .. ')' + +for i, mapping in ipairs(mappings) do + local modifiers, trigger, winFunction = table.unpack(mapping) + local hotKeyStr = getModifiersStr(modifiers) + + if showHelp == true then + if string.len(hotKeyStr) > 0 then + msgStr = msgStr .. (string.format('\n%10s+%s => %s', hotKeyStr, trigger, winFunction)) + else + msgStr = msgStr .. (string.format('\n%11s => %s', trigger, winFunction)) + end + end + + windowLayoutMode:bindWithAutomaticExit(modifiers, trigger, function() + --example: hs.window.focusedWindow():upRight() + local fw = hs.window.focusedWindow() + fw[winFunction](fw) + end) +end + +local message = require('keyboard.status-message') +windowLayoutMode.statusMessage = message.new(msgStr) + +-- Use modifiers+trigger to toggle WindowLayout Mode +hs.hotkey.bind(modifiers, trigger, function() + windowLayoutMode:enter() +end) +windowLayoutMode:bind(modifiers, trigger, function() + windowLayoutMode:exit() +end)