half-baked re-implementation of the major parts of sdorfehs in Hammerspoon
at main 260 lines 6.7 kB view raw
1spoonfish = {} 2 3require("spoonfish/frames") 4require("spoonfish/windows") 5require("spoonfish/events") 6require("spoonfish/utils") 7 8 9-- default configuration, can be overridden in loading init.lua before calling 10-- spoonfish.start() 11 12-- prefix key (with control) 13spoonfish.prefix_key = "a" 14 15-- set sizes to 0 to disable 16spoonfish.border_color = "#000000" 17spoonfish.border_size = 4 18spoonfish.shadow_color = "#000000" 19spoonfish.shadow_size = 8 20 21-- space to put between windows in adjoining frames 22spoonfish.gap = 22 23 24-- program to send 'new window' to for 'c' command 25spoonfish.terminal = "iTerm2" 26 27-- for per-frame messages 28spoonfish.frame_message_secs = 1 29spoonfish.frame_message_font_size = 18 30 31-- increment to resize interactively 32spoonfish.resize_unit = 10 33 34-- for these lists, anything not starting with ^ will be run through 35-- escape_pattern to escape dashes and other special characters, so be sure 36-- to escape such characters manually in ^-prefixed patterns 37spoonfish.apps_to_watch = { 38 "^" .. spoonfish.terminal, 39 "^Firefox", 40} 41-- these override apps_to_watch 42spoonfish.apps_to_ignore = { 43} 44spoonfish.windows_to_ignore = { 45 "Picture-in-Picture", 46 "^Open ", 47 "^Save ", 48 "^Export", 49} 50 51 52-- let's go 53spoonfish.start = function() 54 local s = spoonfish 55 56 s.direction = { 57 LEFT = 1, 58 RIGHT = 2, 59 DOWN = 3, 60 UP = 4, 61 } 62 63 s.position = { 64 FRONT = 1, 65 BACK = 2, 66 REMOVE = 3, 67 } 68 69 s.initialized = false 70 s.events = hs.uielement.watcher 71 72 -- windows, array by window stack order 73 s.windows = {} 74 75 -- apps, keyed by pid 76 s.apps = {} 77 78 -- debugging flags 79 s.debug_frames = false 80 81 s.log = hs.logger.new("spoonfish", "debug") 82 83 -- spaces and frame rects, keyed by frame number 84 s.spaces = {} 85 for _, space_id in 86 pairs(hs.spaces.spacesForScreen(hs.screen.mainScreen():getUUID())) do 87 s.spaces[space_id] = { rect = hs.screen.mainScreen():frame() } 88 s.spaces[space_id].frames = {} 89 s.spaces[space_id].frames[1] = { 90 rect = hs.screen.mainScreen():frame(), 91 } 92 s.spaces[space_id].frame_previous = 1 93 s.spaces[space_id].frame_current = 1 94 95 if space_id == hs.spaces.activeSpaceOnScreen() then 96 spoonfish.draw_frames(space_id) 97 end 98 end 99 100 -- watch for new apps launched 101 s.app_watcher = hs.application.watcher.new(s.app_meta_event) 102 s.app_watcher:start() 103 104 -- watch existing apps 105 local apps = hs.application.runningApplications() 106 for i = 1, #apps do 107 s.watch_app(apps[i]) 108 end 109 110 -- watch when switching spaces 111 s.spaces_watcher = hs.spaces.watcher.new(spoonfish.spaces_event) 112 s.spaces_watcher:start() 113 114 s.initialized = true 115 s.in_modal = false 116 s.send_modal = false 117 s.resizing = false 118 119 s.eventtap = hs.eventtap.new({ hs.eventtap.event.types.keyDown }, 120 function(event) 121 local key = hs.keycodes.map[event:getKeyCode()] 122 local flags = event:getFlags() 123 local ctrl = flags:containExactly({ "ctrl" }) 124 local nomod = flags:containExactly({}) or flags:containExactly({ "shift" }) 125 126 local cs = hs.spaces.activeSpaceOnScreen() 127 local space = s.spaces[cs] 128 129 -- not sure why arrow keys come through with fn down 130 if key == "up" or key == "down" or key == "left" or key == "right" then 131 if flags:containExactly({ "ctrl", "fn" }) then 132 ctrl = true 133 elseif flags:containExactly({ "fn" }) then 134 nomod = true 135 end 136 end 137 138 if event:getType() ~= hs.eventtap.event.types.keyDown then 139 return false 140 end 141 142 if s.resizing then 143 if key == "down" or key == "left" or key == "right" or key == "up" then 144 if nomod then 145 s.frame_resize(cs, space.frame_current, s.dir_from_string(key)) 146 end 147 else 148 -- any other key will exit 149 s.resizing = false 150 s.frame_message(cs, space.frame_current, nil) 151 return true 152 end 153 154 -- redisplay the frame message as it probably just changed size 155 s.frame_message(cs, space.frame_current, "Resize frame", true) 156 157 return true 158 end 159 160 if not s.in_modal then 161 if ctrl and key == spoonfish.prefix_key then 162 if s.send_modal then 163 s.send_modal = false 164 return false 165 end 166 167 s.in_modal = true 168 return true 169 end 170 171 -- not in modal, let event happen as normal 172 return false 173 end 174 175 -- we're in modal, so anything after this point will reset it 176 s.in_modal = false 177 178 -- in-modal key bindings 179 if flags:containExactly({ "shift" }) then 180 key = string.upper(key) 181 end 182 183 s.ignore_events = true 184 185 -- TODO: put these in a table for dynamic reassignment 186 187 if key == "tab" then 188 if nomod or ctrl then 189 s.frame_focus(cs, space.frame_previous, true) 190 end 191 elseif key == "down" or key == "left" or key == "right" or key == "up" then 192 local touching = s.frame_find_touching(cs, space.frame_current, 193 s.dir_from_string(key)) 194 if touching[1] then 195 if nomod then 196 s.frame_focus(cs, touching[1], true) 197 elseif ctrl then 198 s.frame_swap(cs, space.frame_current, touching[1]) 199 end 200 end 201 elseif key == "space" then 202 if nomod or ctrl then 203 s.frame_cycle(cs, space.frame_current, true) 204 end 205 elseif key == spoonfish.prefix_key then 206 if nomod then 207 s.send_modal = true 208 hs.eventtap.keyStroke({ "ctrl" }, spoonfish.prefix_key) 209 else 210 s.frame_reverse_cycle(cs, space.frame_current, true) 211 end 212 elseif key == "c" then 213 if nomod then 214 -- create terminal window 215 spoonfish.ignore_events = false 216 local a = hs.appfinder.appFromName(spoonfish.terminal) 217 if a == nil then 218 hs.osascript.applescript("tell application \"" .. spoonfish.terminal 219 .. "\" to activate") 220 else 221 a:setFrontmost(false) 222 hs.eventtap.keyStroke({ "cmd" }, "n") 223 end 224 end 225 elseif key == "p" then 226 if nomod or ctrl then 227 s.frame_reverse_cycle(cs, space.frame_current, true) 228 end 229 elseif key == "n" then 230 if nomod or ctrl then 231 s.frame_cycle(cs, space.frame_current, true) 232 end 233 elseif key == "r" then 234 if nomod then 235 s.frame_resize_interactively(cs, space.frame_current) 236 end 237 elseif key == "R" then 238 if nomod then 239 s.frame_remove(cs, space.frame_current) 240 end 241 elseif key == "s" then 242 if nomod then 243 s.frame_horizontal_split(cs, space.frame_current) 244 end 245 elseif key == "S" then 246 if nomod then 247 s.frame_vertical_split(cs, space.frame_current) 248 end 249 end 250 251 hs.timer.doAfter(0.25, function() 252 spoonfish.ignore_events = false 253 end) 254 255 -- swallow event 256 return true 257 end):start() 258end 259 260return spoonfish