├── apps ├── hammerspoon_install_cli.lua ├── hammerspoon_toggle_console.lua ├── hammerspoon_config_reload.lua └── skype_mute.lua ├── init.lua ├── .gitignore ├── lib.lua ├── mouse └── locator.lua ├── audio └── headphones_watcher.lua ├── README.md ├── keyboard └── menubar_indicator.lua ├── misc ├── colorpicker.lua ├── statuslets.lua └── clipboard.lua └── windows └── manipulation.lua /apps/hammerspoon_install_cli.lua: -------------------------------------------------------------------------------- 1 | -- Ensure the IPC command line client is available 2 | hs.ipc.cliInstall() 3 | -------------------------------------------------------------------------------- /apps/hammerspoon_toggle_console.lua: -------------------------------------------------------------------------------- 1 | -- Toggle Hammerspoon Console 2 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, 'y', hs.toggleConsole) 3 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- Uncomment to set non-default log level 2 | -- hs.logger.defaultLogLevel = 'debug' 3 | 4 | -- Load other files 5 | require("lib") 6 | require("windows.manipulation") 7 | require("apps.skype_mute") 8 | require("mouse.locator") 9 | require("audio.headphones_watcher") 10 | require("misc.clipboard") 11 | require("misc.colorpicker") 12 | require("keyboard.menubar_indicator") 13 | require("apps.hammerspoon_toggle_console") 14 | require("apps.hammerspoon_install_cli") 15 | require("apps.hammerspoon_config_reload") 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | -------------------------------------------------------------------------------- /apps/hammerspoon_config_reload.lua: -------------------------------------------------------------------------------- 1 | ---- Configuration file management 2 | 3 | -- Manual config reload 4 | hs.hotkey.bind({"cmd", "alt", "ctrl"}, "R", hs.reload) 5 | 6 | -- Automatic config reload if any files in ~/.hammerspoon change 7 | function reloadConfig(files) 8 | doReload = false 9 | for _,file in pairs(files) do 10 | if file:sub(-4) == ".lua" then 11 | doReload = true 12 | end 13 | end 14 | if doReload then 15 | hs.reload() 16 | end 17 | end 18 | 19 | hs.pathwatcher.new(hs_config_dir, reloadConfig):start() 20 | 21 | ---- Notify when the configuration is loaded 22 | notify("Hammerspoon", "Config loaded") 23 | -------------------------------------------------------------------------------- /apps/skype_mute.lua: -------------------------------------------------------------------------------- 1 | ---- Skype stuff 2 | 3 | -- From https://github.com/cmsj/hammerspoon-config/blob/master/init.lua#L139 4 | 5 | -- Toggle Skype between muted/unmuted, whether it is focused or not 6 | function toggleSkypeMute() 7 | local skype = hs.appfinder.appFromName("Skype") 8 | if not skype then 9 | return 10 | end 11 | 12 | local lastapp = nil 13 | if not skype:isFrontmost() then 14 | lastapp = hs.application.frontmostApplication() 15 | skype:activate() 16 | end 17 | 18 | if not skype:selectMenuItem({"Conversations", "Mute Microphone"}) then 19 | skype:selectMenuItem({"Conversations", "Unmute Microphone"}) 20 | end 21 | 22 | if lastapp then 23 | lastapp:activate() 24 | end 25 | end 26 | 27 | hs.hotkey.bind({"alt", "cmd", "ctrl", "shift"}, 'v', toggleSkypeMute) 28 | -------------------------------------------------------------------------------- /lib.lua: -------------------------------------------------------------------------------- 1 | -- Some useful global variables 2 | hostname = hs.host.localizedName() 3 | logger = hs.logger.new('main') 4 | hs_config_dir = os.getenv("HOME") .. "/.hammerspoon/" 5 | 6 | -- Display a notification 7 | function notify(title, message) 8 | hs.notify.new({title=title, informativeText=message}):send() 9 | end 10 | 11 | -- Algorithm to choose whether white/black as the most contrasting to a given 12 | -- color, from http://gamedev.stackexchange.com/a/38561/73496 13 | function chooseContrastingColor(c) 14 | local L = 0.2126*(c.red*c.red) + 0.7152*(c.green*c.green) + 0.0722*(c.blue*c.blue) 15 | local black = { ["red"]=0.000,["green"]=0.000,["blue"]=0.000,["alpha"]=1 } 16 | local white = { ["red"]=1.000,["green"]=1.000,["blue"]=1.000,["alpha"]=1 } 17 | if L>0.5 then 18 | return black 19 | else 20 | return white 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /mouse/locator.lua: -------------------------------------------------------------------------------- 1 | ---- Mouse-related stuff 2 | 3 | -- Find my mouse pointer 4 | 5 | local mouseCircle = nil 6 | local mouseCircleTimer = nil 7 | 8 | function mouseHighlight() 9 | -- Delete an existing highlight if it exists 10 | if mouseCircle then 11 | mouseCircle:delete() 12 | if mouseCircleTimer then 13 | mouseCircleTimer:stop() 14 | end 15 | end 16 | -- Get the current co-ordinates of the mouse pointer 17 | mousepoint = hs.mouse.getAbsolutePosition () 18 | -- Prepare a big red circle around the mouse pointer 19 | mouseCircle = hs.drawing.circle(hs.geometry.rect(mousepoint.x-40, mousepoint.y-40, 80, 80)) 20 | mouseCircle:setStrokeColor({["red"]=1,["blue"]=0,["green"]=0,["alpha"]=1}) 21 | mouseCircle:setFill(false) 22 | mouseCircle:setStrokeWidth(5) 23 | mouseCircle:show() 24 | 25 | -- Set a timer to delete the circle after 3 seconds 26 | mouseCircleTimer = hs.timer.doAfter(3, function() mouseCircle:delete() end) 27 | end 28 | 29 | hs.hotkey.bind({"cmd","alt","ctrl"}, "D", mouseHighlight) 30 | -------------------------------------------------------------------------------- /audio/headphones_watcher.lua: -------------------------------------------------------------------------------- 1 | --- Audio-related stuff 2 | 3 | local spotify_was_playing = false 4 | 5 | -- Testing the new audiodevice watcher 6 | function audiowatch(arg) 7 | logger.df("Audiowatch arg: %s", arg) 8 | end 9 | 10 | hs.audiodevice.watcher.setCallback(audiowatch) 11 | hs.audiodevice.watcher.start() 12 | 13 | function spotify_pause() 14 | logger.df("Pausing Spotify") 15 | hs.spotify.pause() 16 | end 17 | function spotify_play() 18 | logger.df("Playing Spotify") 19 | hs.spotify.play() 20 | end 21 | 22 | -- Per-device watcher to detect headphones in/out 23 | function audiodevwatch(dev_uid, event_name, event_scope, event_element) 24 | logger.df("Audiodevwatch args: %s, %s, %s, %s", dev_uid, event_name, event_scope, event_element) 25 | dev = hs.audiodevice.findDeviceByUID(dev_uid) 26 | if event_name == 'jack' then 27 | if dev:jackConnected() then 28 | if spotify_was_playing then 29 | spotify_play() 30 | notify("Headphones plugged", "Spotify restarted") 31 | end 32 | else 33 | -- Cache current state to know whether we should resume 34 | -- when the headphones are connected again 35 | spotify_was_playing = hs.spotify.isPlaying() 36 | if spotify_was_playing then 37 | spotify_pause() 38 | notify("Headphones unplugged", "Spotify paused") 39 | end 40 | end 41 | end 42 | end 43 | 44 | for i,dev in ipairs(hs.audiodevice.allOutputDevices()) do 45 | dev:watcherCallback(audiodevwatch):watcherStart() 46 | logger.df("Setting up watcher for audio device %s", dev:name()) 47 | end 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My config files for [Hammerspoon](http://www.hammerspoon.org) 2 | 3 | ## Please note: this repository has been superseded by [dot-hammerspoon](https://github.com/zzamboni/dot-hammerspoon) and is out of date - **do not use it!** 4 | 5 | This repository contains my config files for Hammerspoon. This config 6 | has already replaced my use of the following apps: 7 | 8 | - [ClipMenu](http://www.clipmenu.com) - clipboard history, only 9 | supports text entries for now. See 10 | [clipboard.lua](misc/clipboard.lua). 11 | - `Shift-Cmd-v` shows the clipboard menu. 12 | - [Breakaway](http://www.macupdate.com/app/mac/23361/breakaway) - 13 | automatically pause/unpause music when headphones are 14 | unplugged/replugged. Only for Spotify app at the moment, and it 15 | needs latest Hammerspoon built from source (which includes the audio 16 | device watcher). See 17 | [headphones_watcher.lua](audio/headphones_watcher.lua). 18 | - [Spectacle](https://www.spectacleapp.com) - window 19 | manipulation. Only some shortcuts implemented, those that I use, but 20 | others should be easy to add. See 21 | [manipulation.lua](windows/manipulation.lua). 22 | - `Ctrl-Cmd-left/right/up/down` - resize the current window to the 23 | corresponding half of the screen. 24 | - `Ctrl-Alt-left/right/up/down` - resize and move the current window 25 | to the previous/next horizontal/vertical third of the screen. 26 | - `Ctrl-Alt-Cmd-F` or `Ctrl-Alt-Cmd-up` - maximize the current window. 27 | - `Ctrl-Alt-Cmd-left/right` - move the current window to the 28 | previous/next screen (if more than one monitor is plugged in). 29 | - [ShowyEdge](https://pqrs.org/osx/ShowyEdge/index.html.en) - menu bar 30 | coloring to indicate the currently selected keyboard layout (again, 31 | only the indicators I use are implemented, but others are very easy 32 | to add). See 33 | [menubar_indicator.lua](keyboard/menubar_indicator.lua). 34 | 35 | It additionally provides the following functionality: 36 | 37 | - Automatic/manual configuration reloading ([hammerspoon_config_reload.lua](apps/hammerspoon_config_reload.lua)) 38 | - `Ctrl-Alt-Cmd-r` - manual reload, or when any `*.lua` file in 39 | `~/.hammerspoon/` changes. 40 | - A color sampler/picker ([colorpicker.lua](misc/colorpicker.lua)) 41 | - `Ctrl-Alt-Cmd-c` toggles a full-screen color picker of the colors in 42 | `hs.drawing.color` (this is more impressive with a larger list of 43 | colors, like the one in 44 | [PR#611](https://github.com/Hammerspoon/hammerspoon/pull/611/files)). Clicking 45 | on any color will copy its name to the clipboard, Cmd-clicking 46 | copies its RGB code. 47 | - Mouse locator ([mouse/locator.lua](mouse/locator.lua)). 48 | - `Ctrl-Alt-Cmd-d` draws a red circle around the mouse for 3 seconds. 49 | - Skype mute/unmute ([skype_mute.lua](apps/skype_mute.lua)) 50 | - `Ctrl-Alt-Cmd-Shift-v` mutes/unmutes Skype, regardless of whether 51 | it's the frontmost application. 52 | 53 | It has drawn inspiration and code from many other places, including: 54 | 55 | - https://github.com/victorso/.hammerspoon/blob/master/tools/clipboard.lua 56 | - https://github.com/cmsj/hammerspoon-config/ 57 | - https://github.com/Hammerspoon/hammerspoon/wiki/Sample-Configurations 58 | 59 | -------------------------------------------------------------------------------- /keyboard/menubar_indicator.lua: -------------------------------------------------------------------------------- 1 | local as = require "hs.applescript" 2 | local scr = require "hs.screen" 3 | local draw = require "hs.drawing" 4 | local geom = require "hs.geometry" 5 | local col = draw.color 6 | 7 | ------- Functionality equivalent to ShowyEdge (https://pqrs.org/osx/ShowyEdge/index.html.en) 8 | 9 | -- Global enable/disable 10 | local enableIndicator = true 11 | -- Display on all monitors or just the current one? 12 | local allScreens = true 13 | -- Specify 0.0-1.0 to specify a percentage of the height of the menu bar, larger values indicate a fixed height in pixels 14 | local indicatorHeight = 1.0 15 | -- transparency (1.0 - fully opaque) 16 | local indicatorAlpha = 0.3 17 | -- show the indicator in all spaces (this includes full-screen mode) 18 | local indicatorInAllSpaces = true 19 | 20 | ---- Configuration of indicator colors 21 | 22 | -- Each indicator is made of an arbitrary number of segments, 23 | -- distributed evenly across the width of the screen. The table below 24 | -- indicates the colors to use for a given keyboard layout. The index 25 | -- is the name of the layout as it appears in the input source menu. 26 | -- If a layout is not found, then the indicators are removed when that 27 | -- layout is active. 28 | local colors = { 29 | -- Flag-like indicators 30 | ["Spanish"] = {col.green, col.white, col.red}, 31 | ["German"] = {col.black, col.red, col.yellow}, 32 | -- Contrived example of programmatically-generated colors 33 | -- ["U.S."] = ( 34 | -- function() res={} 35 | -- for i = 0,10,1 do 36 | -- table.insert(res, col.blue) 37 | -- table.insert(res, col.white) 38 | -- table.insert(res, col.red) 39 | -- end 40 | -- return res 41 | -- end)(), 42 | -- Solid colors 43 | -- ["Spanish"] = {col.red}, 44 | -- ["German"] = {col.yellow}, 45 | } 46 | 47 | ---------------------------------------------------------------------- 48 | 49 | local ind = nil 50 | 51 | function initIndicators() 52 | if ind ~= nil then 53 | delIndicators() 54 | end 55 | ind = {} 56 | end 57 | 58 | function delIndicators() 59 | if ind ~= nil then 60 | for i,v in ipairs(ind) do 61 | if v ~= nil then 62 | v:delete() 63 | end 64 | end 65 | ind = nil 66 | end 67 | end 68 | 69 | function getInputSource() 70 | ok, result = as.applescript('tell application "System Events" to tell process "SystemUIServer" to get the value of the first menu bar item of menu bar 1 whose description is "text input"') 71 | if ok then 72 | return result 73 | else 74 | return nil 75 | end 76 | end 77 | 78 | function drawIndicators(src) 79 | initIndicators() 80 | 81 | def = colors[src] 82 | logger.df("Indicator definition for %s: %s", src, hs.inspect(def)) 83 | if def ~= nil then 84 | if allScreens then 85 | screens = scr.allScreens() 86 | else 87 | screens = { scr.mainScreen() } 88 | end 89 | for i,screen in ipairs(screens) do 90 | local screeng = screen:fullFrame() 91 | local width = screeng.w / #def 92 | for i,v in ipairs(def) do 93 | if indicatorHeight >= 0.0 and indicatorHeight <= 1.0 then 94 | height = indicatorHeight*(screen:frame().y - screeng.y) 95 | else 96 | height = indicatorHeight 97 | end 98 | c = draw.rectangle(geom.rect(screeng.x+(width*(i-1)), screeng.y, 99 | width, height)) 100 | c:setFillColor(v) 101 | c:setFill(true) 102 | c:setAlpha(indicatorAlpha) 103 | c:setLevel(draw.windowLevels.overlay) 104 | c:setStroke(false) 105 | if indicatorInAllSpaces then 106 | c:setBehavior(draw.windowBehaviors.canJoinAllSpaces) 107 | end 108 | c:show() 109 | table.insert(ind, c) 110 | end 111 | end 112 | else 113 | logger.df("Removing indicators for %s because there is no color definitions for it.", src) 114 | delIndicators() 115 | end 116 | 117 | end 118 | 119 | function inputSourceChange() 120 | source = getInputSource() 121 | logger.df("New input source: " .. source) 122 | drawIndicators(source) 123 | end 124 | 125 | if enableIndicator then 126 | initIndicators() 127 | 128 | -- Initial draw 129 | drawIndicators(getInputSource()) 130 | -- Change whenever the input source changes 131 | hs.keycodes.inputSourceChanged(inputSourceChange) 132 | end 133 | -------------------------------------------------------------------------------- /misc/colorpicker.lua: -------------------------------------------------------------------------------- 1 | -- Display a sample/picker of all the colors defined in hs.drawing.color 2 | -- (or any other color table, see the binding at the end). 3 | -- The same keybinding toggles the color display. Clicking on any color 4 | -- will copy its name to the clipboard, cmd-click will copy its RGB code. 5 | 6 | local draw = require('hs.drawing') 7 | local scr = require('hs.screen') 8 | local geom = require('hs.geometry') 9 | local tap = require('hs.eventtap') 10 | 11 | -- This is where the drawing objects are stored. After first use, the 12 | -- created objects are cached (only shown/hidden as needed) so that 13 | -- they are shown much faster in future uses. 14 | -- toggleColorSamples() can handle multiple color tables at once, 15 | -- so these global caches are tables. 16 | local swatches = {} 17 | 18 | -- Are the indicators displayed at the moment? 19 | local indicators_shown = {} 20 | 21 | -- Algorithm to choose whether white/black as the most contrasting to a given 22 | -- color, from http://gamedev.stackexchange.com/a/38561/73496 23 | function contrastingColor(c) 24 | local L = 0.2126*(c.red*c.red) + 0.7152*(c.green*c.green) + 0.0722*(c.blue*c.blue) 25 | local black = { ["red"]=0.000,["green"]=0.000,["blue"]=0.000,["alpha"]=1 } 26 | local white = { ["red"]=1.000,["green"]=1.000,["blue"]=1.000,["alpha"]=1 } 27 | if L>0.5 then 28 | return black 29 | else 30 | return white 31 | end 32 | end 33 | 34 | -- Get the frame for a single swatch 35 | function getSwatchFrame(frame, hsize, vsize, column, row) 36 | return geom.rect(frame.x+(column*hsize), frame.y+(row*vsize), hsize, vsize) 37 | end 38 | 39 | -- Copy the name/code of the color to the clipboard, and remove the colors 40 | -- from the screen. 41 | function copyAndRemove(name, hex, tablename) 42 | local mods = tap.checkKeyboardModifiers() 43 | if mods.cmd then 44 | hs.pasteboard.setContents(hex) 45 | else 46 | hs.pasteboard.setContents(name) 47 | end 48 | toggleColorSamples(tablename) 49 | end 50 | 51 | -- Draw a single square on the screen 52 | function drawSwatch(tablename, swatchFrame, colorname, color) 53 | local swatch = draw.rectangle(swatchFrame) 54 | swatch:setFill(true) 55 | swatch:setFillColor(color) 56 | swatch:setStroke(false) 57 | swatch:setLevel(draw.windowLevels.overlay) 58 | swatch:show() 59 | table.insert(swatches[tablename], swatch) 60 | if colorname ~= "" then 61 | local hex = string.format("%02x%02x%02x", math.floor(255*color.red), math.floor(255*color.green), math.floor(255*color.blue)) 62 | local text = draw.text(swatchFrame, string.format("%s\n#%s", colorname, hex)) 63 | text:setTextColor(contrastingColor(color)) 64 | text:setTextStyle({ ["alignment"] = "center", ["size"]=16.0}) 65 | text:setLevel(draw.windowLevels.overlay+1) 66 | text:setClickCallback(nil, hs.fnutils.partial(copyAndRemove, colorname, hex, tablename)) 67 | text:show() 68 | table.insert(swatches[tablename],text) 69 | end 70 | end 71 | 72 | -- Toggle display on the screen of a grid with all the colors in the given colortable 73 | function toggleColorSamples(tablename, colortable) 74 | local screen = scr.mainScreen() 75 | local frame = screen:frame() 76 | 77 | if swatches[tablename] ~= nil then -- they are being displayed, remove them 78 | for i,color in ipairs(swatches[tablename]) do 79 | if indicators_shown then 80 | color:hide() 81 | else 82 | color:show() 83 | end 84 | end 85 | indicators_shown = not indicators_shown 86 | else -- display them 87 | swatches[tablename] = {} 88 | keys = {} 89 | 90 | -- Create sorted list of colors 91 | for colorname,color in pairs(colortable) do table.insert(keys, colorname) end 92 | table.sort(keys) 93 | 94 | -- Scale number of rows/columns according to the screen's aspect ratio 95 | local rows = math.ceil(math.sqrt(#keys)*(frame.w/frame.h)) 96 | local columns = math.ceil(math.sqrt(#keys)/(frame.w/frame.h)) 97 | local hsize = math.floor(frame.w / columns) 98 | local vsize = math.floor(frame.h / rows) 99 | 100 | for i = 1,(rows*columns),1 do -- fill the entire square 101 | local colorname = keys[i] 102 | local column = math.floor((i-1)/rows) 103 | local row = i-1-(column*rows) 104 | local swatchFrame = getSwatchFrame(frame,hsize,vsize,column,row) 105 | if colorname ~= nil then -- with the corresponding color swatch 106 | local color = colortable[colorname] 107 | drawSwatch(tablename,swatchFrame,colorname,color) 108 | else -- or with a gray swatch to fill up the rectangle 109 | drawSwatch(tablename,swatchFrame,"",draw.color.gray) 110 | end 111 | end 112 | indicators_shown = true 113 | end 114 | end 115 | 116 | -- Show/hide color samples 117 | -- Change the table and tablename if you want to handle multiple color tables 118 | hs.hotkey.bind({"ctrl", "alt", "cmd"}, "c", hs.fnutils.partial(toggleColorSamples, "default", draw.color)) 119 | -------------------------------------------------------------------------------- /windows/manipulation.lua: -------------------------------------------------------------------------------- 1 | -- Window management 2 | 3 | -- Defines for window maximize toggler 4 | local frameCache = {} 5 | local logger = hs.logger.new("windows") 6 | 7 | -- Resize current window 8 | 9 | function winresize(how) 10 | local win = hs.window.focusedWindow() 11 | local app = win:application():name() 12 | local windowLayout 13 | local newrect 14 | 15 | if how == "left" then 16 | newrect = hs.layout.left50 17 | elseif how == "right" then 18 | newrect = hs.layout.right50 19 | elseif how == "up" then 20 | newrect = {0,0,1,0.5} 21 | elseif how == "down" then 22 | newrect = {0,0.5,1,0.5} 23 | elseif how == "max" then 24 | newrect = hs.layout.maximized 25 | elseif how == "left_third" or how == "hthird-0" then 26 | newrect = {0,0,1/3,1} 27 | elseif how == "middle_third_h" or how == "hthird-1" then 28 | newrect = {1/3,0,1/3,1} 29 | elseif how == "right_third" or how == "hthird-2" then 30 | newrect = {2/3,0,1/3,1} 31 | elseif how == "top_third" or how == "vthird-0" then 32 | newrect = {0,0,1,1/3} 33 | elseif how == "middle_third_v" or how == "vthird-1" then 34 | newrect = {0,1/3,1,1/3} 35 | elseif how == "bottom_third" or how == "vthird-2" then 36 | newrect = {0,2/3,1,1/3} 37 | end 38 | 39 | win:move(newrect) 40 | end 41 | 42 | function winmovescreen(how) 43 | local win = hs.window.focusedWindow() 44 | if how == "left" then 45 | win:moveOneScreenWest() 46 | elseif how == "right" then 47 | win:moveOneScreenEast() 48 | end 49 | end 50 | 51 | -- Toggle a window between its normal size, and being maximized 52 | function toggle_window_maximized() 53 | local win = hs.window.focusedWindow() 54 | if frameCache[win:id()] then 55 | win:setFrame(frameCache[win:id()]) 56 | frameCache[win:id()] = nil 57 | else 58 | frameCache[win:id()] = win:frame() 59 | win:maximize() 60 | end 61 | end 62 | 63 | -- Move between thirds of the screen 64 | function get_horizontal_third(win) 65 | local frame=win:frame() 66 | local screenframe=win:screen():frame() 67 | local relframe=hs.geometry(frame.x-screenframe.x, frame.y-screenframe.y, frame.w, frame.h) 68 | local third = math.floor(3.01*relframe.x/screenframe.w) 69 | logger.df("Screen frame: %s", screenframe) 70 | logger.df("Window frame: %s, relframe %s is in horizontal third #%d", frame, relframe, third) 71 | return third 72 | end 73 | 74 | function get_vertical_third(win) 75 | local frame=win:frame() 76 | local screenframe=win:screen():frame() 77 | local relframe=hs.geometry(frame.x-screenframe.x, frame.y-screenframe.y, frame.w, frame.h) 78 | local third = math.floor(3.01*relframe.y/screenframe.h) 79 | logger.df("Screen frame: %s", screenframe) 80 | logger.df("Window frame: %s, relframe %s is in vertical third #%d", frame, relframe, third) 81 | return third 82 | end 83 | 84 | function left_third() 85 | local win = hs.window.focusedWindow() 86 | local third = get_horizontal_third(win) 87 | if third == 0 then 88 | winresize("hthird-0") 89 | else 90 | winresize("hthird-" .. (third-1)) 91 | end 92 | end 93 | 94 | function right_third() 95 | local win = hs.window.focusedWindow() 96 | local third = get_horizontal_third(win) 97 | if third == 2 then 98 | winresize("hthird-2") 99 | else 100 | winresize("hthird-" .. (third+1)) 101 | end 102 | end 103 | 104 | function up_third() 105 | local win = hs.window.focusedWindow() 106 | local third = get_vertical_third(win) 107 | if third == 0 then 108 | winresize("vthird-0") 109 | else 110 | winresize("vthird-" .. (third-1)) 111 | end 112 | end 113 | 114 | function down_third() 115 | local win = hs.window.focusedWindow() 116 | local third = get_vertical_third(win) 117 | if third == 2 then 118 | winresize("vthird-2") 119 | else 120 | winresize("vthird-" .. (third+1)) 121 | end 122 | end 123 | 124 | -------- Key bindings 125 | 126 | -- Halves of the screen 127 | hs.hotkey.bind({"ctrl", "cmd"}, "Left", hs.fnutils.partial(winresize, "left")) 128 | hs.hotkey.bind({"ctrl", "cmd"}, "Right", hs.fnutils.partial(winresize, "right")) 129 | hs.hotkey.bind({"ctrl", "cmd"}, "Up", hs.fnutils.partial(winresize, "up")) 130 | hs.hotkey.bind({"ctrl", "cmd"}, "Down", hs.fnutils.partial(winresize, "down")) 131 | 132 | -- Thirds of the screen 133 | hs.hotkey.bind({"ctrl", "alt" }, "Left", left_third) 134 | hs.hotkey.bind({"ctrl", "alt" }, "Right", right_third) 135 | hs.hotkey.bind({"ctrl", "alt" }, "Up", up_third) 136 | hs.hotkey.bind({"ctrl", "alt" }, "Down", down_third) 137 | 138 | -- Maximized 139 | hs.hotkey.bind({"ctrl", "alt", "cmd"}, "F", hs.fnutils.partial(winresize, "max")) 140 | hs.hotkey.bind({"ctrl", "alt", "cmd"}, "Up", hs.fnutils.partial(winresize, "max")) 141 | 142 | -- Move between screens 143 | hs.hotkey.bind({"ctrl", "alt", "cmd"}, "Left", hs.fnutils.partial(winmovescreen, "left")) 144 | hs.hotkey.bind({"ctrl", "alt", "cmd"}, "Right", hs.fnutils.partial(winmovescreen, "right")) 145 | -------------------------------------------------------------------------------- /misc/statuslets.lua: -------------------------------------------------------------------------------- 1 | ---- From https://github.com/cmsj/hammerspoon-config/blob/master/init.lua 2 | 3 | local statusletTimer 4 | 5 | -- Draw little text/dot pairs in the bottom right corner of the primary display, to indicate firewall/backup status of my machine 6 | function renderStatuslets() 7 | -- if (hostname ~= "pixukipa") then 8 | -- return 9 | -- end 10 | -- Destroy existing Statuslets 11 | if firewallStatusText then firewallStatusText:delete() end 12 | if firewallStatusDot then firewallStatusDot:delete() end 13 | if cccStatusText then cccStatusText:delete() end 14 | if cccStatusDot then cccStatusDot:delete() end 15 | if arqStatusText then arqStatusText:delete() end 16 | if arqStatusDot then arqStatusDot:delete() end 17 | 18 | -- Defines for statuslets - little coloured dots in the corner of my screen that give me status info, see: 19 | -- https://www.dropbox.com/s/3v2vyhi1beyujtj/Screenshot%202015-03-11%2016.13.25.png?dl=0 20 | local initialScreenFrame = hs.screen.allScreens()[1]:fullFrame() 21 | 22 | -- Start off by declaring the size of the text/circle objects and some anchor positions for them on screen 23 | local statusDotWidth = 10 24 | local statusTextWidth = 30 25 | local statusTextHeight = 15 26 | local statusText_x = initialScreenFrame.x + initialScreenFrame.w - statusDotWidth - statusTextWidth 27 | local statusText_y = initialScreenFrame.y + initialScreenFrame.h - statusTextHeight 28 | local statusDot_x = initialScreenFrame.x + initialScreenFrame.w - statusDotWidth 29 | local statusDot_y = statusText_y 30 | 31 | -- Now create the text/circle objects using the sizes/positions we just declared (plus a little fudging to make it all align properly) 32 | firewallStatusText = hs.drawing.text(hs.geometry.rect(statusText_x + 5, 33 | statusText_y - (statusTextHeight*2) + 2, 34 | statusTextWidth, 35 | statusTextHeight), "FW:") 36 | cccStatusText = hs.drawing.text(hs.geometry.rect(statusText_x, 37 | statusText_y - statusTextHeight + 1, 38 | statusTextWidth, 39 | statusTextHeight), "CCC:") 40 | arqStatusText = hs.drawing.text(hs.geometry.rect(statusText_x + 4, 41 | statusText_y, 42 | statusTextWidth, 43 | statusTextHeight), "Arq:") 44 | 45 | firewallStatusDot = hs.drawing.circle(hs.geometry.rect(statusDot_x, 46 | statusDot_y - (statusTextHeight*2) + 4, 47 | statusDotWidth, 48 | statusDotWidth)) 49 | cccStatusDot = hs.drawing.circle(hs.geometry.rect(statusDot_x, 50 | statusDot_y - statusTextHeight + 3, 51 | statusDotWidth, 52 | statusDotWidth)) 53 | arqStatusDot = hs.drawing.circle(hs.geometry.rect(statusDot_x, 54 | statusDot_y + 2, 55 | statusDotWidth, 56 | statusDotWidth)) 57 | 58 | -- Finally, configure the rendering style of the text/circle objects, clamp them to the desktop, and show them 59 | firewallStatusText:setBehaviorByLabels({"canJoinAllSpaces", "stationary"}):setTextSize(11):sendToBack():show() 60 | cccStatusText:setBehaviorByLabels({"canJoinAllSpaces", "stationary"}):setTextSize(11):sendToBack():show() 61 | arqStatusText:setBehaviorByLabels({"canJoinAllSpaces", "stationary"}):setTextSize(11):sendToBack():show() 62 | 63 | firewallStatusDot:setBehaviorByLabels({"canJoinAllSpaces", "stationary"}):setFillColor(hs.drawing.color.osx_yellow):setStroke(false):sendToBack():show() 64 | cccStatusDot:setBehaviorByLabels({"canJoinAllSpaces", "stationary"}):setFillColor(hs.drawing.color.osx_yellow):setStroke(false):sendToBack():show() 65 | arqStatusDot:setBehaviorByLabels({"canJoinAllSpaces", "stationary"}):setFillColor(hs.drawing.color.osx_yellow):setStroke(false):sendToBack():show() 66 | end 67 | 68 | function updateStatuslets() 69 | -- if (hostname ~= "pixukipa") then 70 | -- return 71 | -- end 72 | print("updateStatuslets") 73 | _,_,fwcode = os.execute('sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getstealthmode | grep "Stealth mode enabled"') 74 | _,_,ccccode = os.execute('grep -q "$(date +%d/%m/%Y)" ~/.cccLast') 75 | _,_,arqcode = os.execute('grep -q "Arq.*finished backup" /var/log/system.log') 76 | 77 | if fwcode == 0 then 78 | firewallStatusDot:setFillColor(hs.drawing.color.osx_green) 79 | else 80 | firewallStatusDot:setFillColor(hs.drawing.color.osx_red) 81 | end 82 | 83 | if ccccode == 0 then 84 | cccStatusDot:setFillColor(hs.drawing.color.osx_green) 85 | else 86 | cccStatusDot:setFillColor(hs.drawing.color.osx_red) 87 | end 88 | 89 | if arqcode == 0 then 90 | arqStatusDot:setFillColor(hs.drawing.color.osx_green) 91 | else 92 | arqStatusDot:setFillColor(hs.drawing.color.osx_red) 93 | end 94 | end 95 | 96 | -- Render our statuslets, trigger a timer to update them regularly, and do an initial update 97 | renderStatuslets() 98 | statusletTimer = hs.timer.new(hs.timer.minutes(5), updateStatuslets) 99 | statusletTimer:start() 100 | updateStatuslets() 101 | 102 | -------------------------------------------------------------------------------- /misc/clipboard.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | From https://github.com/victorso/.hammerspoon/blob/master/tools/clipboard.lua 3 | Modified by Diego Zamboni 4 | 5 | This is my attempt to implement a jumpcut replacement in Lua/Hammerspoon. 6 | It monitors the clipboard/pasteboard for changes, and stores the strings you copy to the transfer area. 7 | You can access this history on the menu (Unicode scissors icon). 8 | Clicking on any item will add it to your transfer area. 9 | If you open the menu while pressing option/alt, you will enter the Direct Paste Mode. This means that the selected item will be 10 | "typed" instead of copied to the active clipboard. 11 | The clipboard persists across launches. 12 | -> Ng irc suggestion: hs.settings.set("jumpCutReplacementHistory", clipboard_history) 13 | ]]-- 14 | 15 | -- Feel free to change those settings 16 | local frequency = 0.8 -- Speed in seconds to check for clipboard changes. If you check too frequently, you will loose performance, if you check sparsely you will loose copies 17 | local hist_size = 100 -- How many items to keep on history 18 | local label_length = 70 -- How wide (in characters) the dropdown menu should be. Copies larger than this will have their label truncated and end with "…" (unicode for elipsis ...) 19 | local honor_clearcontent = false --asmagill request. If any application clears the pasteboard, we also remove it from the history https://groups.google.com/d/msg/hammerspoon/skEeypZHOmM/Tg8QnEj_N68J 20 | local pasteOnSelect = false -- Auto-type on click 21 | 22 | -- Don't change anything bellow this line 23 | local jumpcut = hs.menubar.new() 24 | jumpcut:setTooltip("Clipboard history") 25 | local pasteboard = require("hs.pasteboard") -- http://www.hammerspoon.org/docs/hs.pasteboard.html 26 | local settings = require("hs.settings") -- http://www.hammerspoon.org/docs/hs.settings.html 27 | local last_change = pasteboard.changeCount() -- displays how many times the pasteboard owner has changed // Indicates a new copy has been made 28 | 29 | --Array to store the clipboard history 30 | local clipboard_history = settings.get("so.victor.hs.jumpcut") or {} --If no history is saved on the system, create an empty history 31 | 32 | -- Append a history counter to the menu 33 | function setTitle() 34 | if (#clipboard_history == 0) then 35 | jumpcut:setTitle("✂") -- Unicode magic 36 | else 37 | jumpcut:setTitle("✂") -- Unicode magic 38 | -- jumpcut:setTitle("✂ ("..#clipboard_history..")") -- updates the menu counter 39 | end 40 | end 41 | 42 | function putOnPaste(string,key) 43 | if (pasteOnSelect) then 44 | hs.eventtap.keyStrokes(string) 45 | pasteboard.setContents(string) 46 | last_change = pasteboard.changeCount() 47 | else 48 | if (key.alt == true) then -- If the option/alt key is active when clicking on the menu, perform a "direct paste", without changing the clipboard 49 | hs.eventtap.keyStrokes(string) -- Defeating paste blocking http://www.hammerspoon.org/go/#pasteblock 50 | else 51 | pasteboard.setContents(string) 52 | last_change = pasteboard.changeCount() -- Updates last_change to prevent item duplication when putting on paste 53 | end 54 | end 55 | end 56 | 57 | -- Clears the clipboard and history 58 | function clearAll() 59 | pasteboard.clearContents() 60 | clipboard_history = {} 61 | settings.set("so.victor.hs.jumpcut",clipboard_history) 62 | now = pasteboard.changeCount() 63 | setTitle() 64 | end 65 | 66 | -- Clears the last added to the history 67 | function clearLastItem() 68 | table.remove(clipboard_history,#clipboard_history) 69 | settings.set("so.victor.hs.jumpcut",clipboard_history) 70 | now = pasteboard.changeCount() 71 | setTitle() 72 | end 73 | 74 | function pasteboardToClipboard(item) 75 | -- Loop to enforce limit on qty of elements in history. Removes the oldest items 76 | while (#clipboard_history >= hist_size) do 77 | table.remove(clipboard_history,1) 78 | end 79 | table.insert(clipboard_history, item) 80 | settings.set("so.victor.hs.jumpcut",clipboard_history) -- updates the saved history 81 | setTitle() -- updates the menu counter 82 | end 83 | 84 | -- Dynamic menu by cmsj https://github.com/Hammerspoon/hammerspoon/issues/61#issuecomment-64826257 85 | populateMenu = function(key) 86 | setTitle() -- Update the counter every time the menu is refreshed 87 | menuData = {} 88 | if (#clipboard_history == 0) then 89 | table.insert(menuData, {title="None", disabled = true}) -- If the history is empty, display "None" 90 | else 91 | for k,v in pairs(clipboard_history) do 92 | if (string.len(v) > label_length) then 93 | table.insert(menuData,1, {title=string.sub(v,0,label_length).."…", fn = function() putOnPaste(v,key) end }) -- Truncate long strings 94 | else 95 | table.insert(menuData,1, {title=v, fn = function() putOnPaste(v,key) end }) 96 | end -- end if else 97 | end-- end for 98 | end-- end if else 99 | -- footer 100 | table.insert(menuData, {title="-"}) 101 | table.insert(menuData, {title="Clear All", fn = function() clearAll() end }) 102 | if (key.alt == true or pasteOnSelect) then 103 | table.insert(menuData, {title="Direct Paste Mode ✍", disabled=true}) 104 | end 105 | return menuData 106 | end 107 | 108 | -- If the pasteboard owner has changed, we add the current item to our history and update the counter. 109 | function storeCopy() 110 | now = pasteboard.changeCount() 111 | if (now > last_change) then 112 | current_clipboard = pasteboard.getContents() 113 | -- asmagill requested this feature. It prevents the history from keeping items removed by password managers 114 | if (current_clipboard == nil and honor_clearcontent) then 115 | clearLastItem() 116 | else 117 | pasteboardToClipboard(current_clipboard) 118 | end 119 | last_change = now 120 | end 121 | end 122 | 123 | --Checks for changes on the pasteboard. Is it possible to replace with eventtap? 124 | timer = hs.timer.new(frequency, storeCopy) 125 | timer:start() 126 | 127 | setTitle() --Avoid wrong title if the user already has something on his saved history 128 | jumpcut:setMenu(populateMenu) 129 | 130 | hs.hotkey.bind({"cmd", "shift"}, "v", function() jumpcut:popupMenu(hs.mouse.getAbsolutePosition()) end) 131 | --------------------------------------------------------------------------------