half-baked re-implementation of the major parts of sdorfehs in Hammerspoon
at main 485 lines 14 kB view raw
1-- take control of a new hs.window and resize it to a particular frame 2-- return a window table object 3spoonfish.frame_capture = function(space_id, frame_id, hswin) 4 local win = { 5 ["win"] = hswin, 6 ["frame"] = frame_id, 7 ["space"] = space_id, 8 ["app_pid"] = hswin:application():pid(), 9 } 10 table.insert(spoonfish.windows, win) 11 spoonfish.window_reframe(win) 12 spoonfish.window_restack(win, 1) 13 return win 14end 15 16-- move the top window of a frame to the bottom of the stack, raise the next 17-- available or last window to the top 18spoonfish._frame_cycle = function(space_id, frame_id, reverse, complain) 19 local wnf = spoonfish.windows_not_visible(space_id) 20 21 if table.count(wnf) == 0 then 22 if complain then 23 spoonfish.frame_message(space_id, spoonfish.spaces[space_id].frame_current, 24 "No more windows") 25 end 26 return 27 end 28 29 local fwin = spoonfish.frame_top_window(space_id, frame_id) 30 if fwin then 31 -- move this top window to the bottom of the stack 32 spoonfish.window_restack(fwin, spoonfish.position.BACK) 33 end 34 35 -- find the first window that is not in a frame and bring it forth 36 local cwin = wnf[1] 37 if reverse then 38 cwin = wnf[table.count(wnf)] 39 end 40 if cwin ~= nil then 41 cwin["frame"] = frame_id 42 spoonfish.frame_raise_window(space_id, frame_id, cwin) 43 end 44end 45spoonfish.frame_cycle = function(space_id, frame_id, complain) 46 spoonfish._frame_cycle(space_id, frame_id, false, complain) 47end 48spoonfish.frame_reverse_cycle = function(space_id, frame_id, complain) 49 spoonfish._frame_cycle(space_id, frame_id, true, complain) 50end 51 52-- return a table of frame ids touching frame_id on side dir 53spoonfish.frame_find_touching = function(space_id, frame_id, dir) 54 local cur = spoonfish.spaces[space_id].frames[frame_id].rect 55 local found = {} 56 57 if dir == spoonfish.direction.LEFT then 58 -- x+w touching the x of the current frame, with same y 59 for i, f in pairs(spoonfish.spaces[space_id].frames) do 60 if f.rect.x + f.rect.w == cur.x and f.rect.y == cur.y then 61 found[i] = true 62 end 63 end 64 -- or just x+w touching the x of the current frame 65 for i, f in pairs(spoonfish.spaces[space_id].frames) do 66 if f.rect.x + f.rect.w == cur.x then 67 found[i] = true 68 end 69 end 70 elseif dir == spoonfish.direction.RIGHT then 71 -- x touching the x+w of the current frame, with same y 72 for i, f in pairs(spoonfish.spaces[space_id].frames) do 73 if f.rect.x == cur.x + cur.w and f.rect.y == cur.y then 74 found[i] = true 75 end 76 end 77 -- or just x touching the x+w of the current frame 78 for i, f in pairs(spoonfish.spaces[space_id].frames) do 79 if f.rect.x == cur.x + cur.w then 80 found[i] = true 81 end 82 end 83 elseif dir == spoonfish.direction.DOWN then 84 -- y touching the y+h of the current frame, with same x 85 for i, f in pairs(spoonfish.spaces[space_id].frames) do 86 if f.rect.y == cur.y + cur.h and f.rect.x == cur.x then 87 found[i] = true 88 end 89 end 90 -- or just y touching the y+h of the current frame 91 for i, f in pairs(spoonfish.spaces[space_id].frames) do 92 if f.rect.y == cur.y + cur.h then 93 found[i] = true 94 end 95 end 96 elseif dir == spoonfish.direction.UP then 97 -- y+h touching the y of the current frame, with same x 98 for i, f in pairs(spoonfish.spaces[space_id].frames) do 99 if f.rect.y + f.rect.h == cur.y and f.rect.x == cur.x then 100 found[i] = true 101 end 102 end 103 -- or just y+h touching the y of the current frame 104 for i, f in pairs(spoonfish.spaces[space_id].frames) do 105 if f.rect.y + f.rect.h == cur.y then 106 found[i] = true 107 end 108 end 109 else 110 error("frame_find_touching: bogus direction") 111 end 112 113 return table.keys(found) 114end 115 116spoonfish.frame_rect_with_gap = function(space_id, frame_id) 117 local rect = spoonfish.spaces[space_id].frames[frame_id].rect 118 local hgap = spoonfish.gap / 2 119 local grect = spoonfish.inset(rect, hgap) 120 local srect = spoonfish.spaces[space_id].rect 121 122 if rect.x == srect.x then 123 -- touching left side 124 grect.x = grect.x + hgap 125 grect.w = grect.w - hgap 126 end 127 128 if rect.y == srect.y then 129 -- touching top 130 grect.y = grect.y + hgap 131 grect.h = grect.h - hgap 132 end 133 134 if (rect.x + rect.w) == (srect.x + srect.w) then 135 -- touching right side 136 grect.w = grect.w - hgap 137 end 138 139 if (rect.y + rect.h) == (srect.y + srect.h) then 140 -- touching bottom 141 grect.h = grect.h - hgap 142 end 143 144 return grect 145end 146 147-- give focus to frame and raise its active window 148spoonfish.frame_focus = function(space_id, frame_id, raise) 149 if spoonfish.spaces[space_id].frames[frame_id] == nil then 150 error("bogus frame " .. frame_id .. " on space " .. space_id) 151 return 152 end 153 154 local fc = spoonfish.spaces[space_id].frame_current 155 local wof = spoonfish.frame_top_window(space_id, frame_id) 156 if wof then 157 spoonfish.window_reframe(wof) 158 if raise then 159 wof["win"]:focus() 160 end 161 spoonfish.window_reborder(wof) 162 end 163 164 if frame_id ~= fc then 165 spoonfish.spaces[space_id].frame_current = frame_id 166 spoonfish.spaces[space_id].frame_previous = fc 167 168 spoonfish.frame_message(space_id, frame_id, "Frame " .. frame_id) 169 end 170end 171 172-- split a frame horizontally 173spoonfish.frame_horizontal_split = function(space_id, frame_id) 174 return spoonfish.frame_split(space_id, frame_id, false) 175end 176 177-- raise a window in a given frame, assigning it to that frame 178spoonfish.frame_raise_window = function(space_id, frame_id, win) 179 spoonfish.window_reframe(win) 180 spoonfish.window_show(win) 181 win["win"]:focus() 182 spoonfish.window_restack(win, spoonfish.position.FRONT) 183 spoonfish.window_reborder(win) 184end 185 186spoonfish.frame_resize_interactively = function(space_id, frame_id) 187 if table.count(spoonfish.spaces[space_id].frames) == 1 then 188 spoonfish.frame_message(space_id, 189 table.keys(spoonfish.spaces[space_id].frames)[1], 190 "Cannot resize only frame", false) 191 return 192 end 193 194 spoonfish.resizing = true 195 spoonfish.frame_message(space_id, frame_id, "Resize frame", true) 196end 197 198-- shrink or grow a given frame 199spoonfish.frame_resize = function(space_id, frame_id, dir) 200 local origrect = spoonfish.inset( 201 spoonfish.spaces[space_id].frames[frame_id].rect, 0) 202 local srect = spoonfish.spaces[space_id].rect 203 local resized = {} 204 205 if dir == spoonfish.direction.LEFT or dir == spoonfish.direction.RIGHT or 206 dir == spoonfish.direction.UP or dir == spoonfish.direction.DOWN then 207 local amt = spoonfish.resize_unit 208 local xy = "x" 209 local wh = "w" 210 211 if dir == spoonfish.direction.LEFT or dir == spoonfish.direction.UP then 212 -- shrink 213 amt = -amt 214 end 215 216 if dir == spoonfish.direction.UP or dir == spoonfish.direction.DOWN then 217 xy = "y" 218 wh = "h" 219 end 220 221 if (origrect[xy] == srect[xy]) and 222 (origrect[xy] + origrect[wh] == srect[xy] + srect[wh]) then 223 -- the original frame can't be resized in this direction, don't bother 224 -- resizing any others 225 return 226 end 227 228 for i, f in pairs(spoonfish.spaces[space_id].frames) do 229 if (f.rect[xy] == srect[xy]) and 230 (f.rect[xy] + f.rect[wh] == srect[xy] + srect[wh]) then 231 -- this frame can't be resized in this direction 232 else 233 -- shrink/grow frames with this frame's coord 234 if f.rect[xy] == origrect[xy] then 235 if (f.rect[xy] + f.rect[wh]) == (srect[xy] + srect[wh]) then 236 -- frame is on the right/bottom screen edge, keep its edge there by 237 -- moving it left or up 238 f.rect[xy] = f.rect[xy] - amt 239 end 240 f.rect[wh] = f.rect[wh] + amt 241 resized[i] = true 242 end 243 244 -- grow/shrink frames with edge of this frame's shrunken/grown edge 245 if (f.rect[xy] + f.rect[wh] == origrect[xy]) or 246 (f.rect[xy] == origrect[xy] + origrect[wh]) then 247 if (f.rect[xy] + f.rect[wh]) == (srect[xy] + srect[wh]) then 248 -- frame is on the right/bottom screen edge, keep its edge there 249 f.rect[xy] = f.rect[xy] + amt 250 end 251 f.rect[wh] = f.rect[wh] - amt 252 resized[i] = true 253 end 254 end 255 end 256 else 257 error("frame_resize: bogus direction") 258 return 259 end 260 261 spoonfish.draw_frames(space_id) 262 263 for _, w in ipairs(spoonfish.windows) do 264 if w["space"] == space_id and resized[w["frame"]] then 265 spoonfish.window_reframe(w) 266 end 267 end 268end 269 270-- split a frame vertically or horizontally 271spoonfish.frame_split = function(space_id, frame_id, vertical) 272 local old_rect = spoonfish.spaces[space_id].frames[frame_id].rect 273 local new_rect = {} 274 275 -- halve current frame 276 if vertical then 277 new_rect = hs.geometry.rect( 278 old_rect.x, 279 old_rect.y, 280 math.floor(old_rect.w / 2), 281 old_rect.h 282 ) 283 else 284 new_rect = hs.geometry.rect( 285 old_rect.x, 286 old_rect.y, 287 old_rect.w, 288 math.floor(old_rect.h / 2) 289 ) 290 end 291 spoonfish.spaces[space_id].frames[frame_id].rect = new_rect 292 293 -- reframe all windows in that old frame 294 for _, w in ipairs(spoonfish.windows) do 295 if w["space"] == space_id and w["frame"] == frame_id then 296 spoonfish.window_reframe(w) 297 end 298 end 299 300 local new_frame_id = table.count(spoonfish.spaces[space_id].frames) + 1 301 local new_frame_rect = {} 302 303 if vertical then 304 new_frame_rect = hs.geometry.rect( 305 new_rect.x + new_rect.w, 306 new_rect.y, 307 old_rect.w - new_rect.w, 308 new_rect.h 309 ) 310 else 311 new_frame_rect = hs.geometry.rect( 312 new_rect.x, 313 new_rect.y + new_rect.h, 314 new_rect.w, 315 old_rect.h - new_rect.h 316 ) 317 end 318 spoonfish.spaces[space_id].frames[new_frame_id] = { rect = new_frame_rect } 319 320 spoonfish.draw_frames(space_id) 321 spoonfish.frame_focus(space_id, new_frame_id, true) 322 323 -- we'll probably want to go to this frame on tab 324 spoonfish.spaces[space_id].frame_previous = new_frame_id 325end 326 327-- swap the front-most windows of two frames 328spoonfish.frame_swap = function(space_id, frame_id_from, frame_id_to) 329 local fwin = spoonfish.frame_top_window(space_id, frame_id_from) 330 local twin = spoonfish.frame_top_window(space_id, frame_id_to) 331 332 if fwin ~= nil then 333 fwin["frame"] = frame_id_to 334 spoonfish.window_reframe(fwin) 335 end 336 if twin ~= nil then 337 twin["frame"] = frame_id_from 338 spoonfish.window_reframe(twin) 339 end 340 spoonfish.frame_focus(space_id, frame_id_to, true) 341end 342 343-- remove current frame 344spoonfish.frame_remove = function(space_id) 345 if table.count(spoonfish.spaces[space_id].frames) == 1 then 346 spoonfish.frame_message(space_id, 347 table.keys(spoonfish.spaces[space_id].frames)[1], 348 "Cannot remove only frame", false) 349 return 350 end 351 352 local id_removing = spoonfish.spaces[space_id].frame_current 353 354 -- reframe all windows in the current frame and renumber higher frames 355 for _, w in ipairs(spoonfish.windows) do 356 if w["space"] == space_id then 357 if w["frame"] == id_removing then 358 w["frame"] = 0 359 spoonfish.window_reframe(w) 360 elseif w["frame"] > id_removing then 361 w["frame"] = w["frame"] - 1 362 end 363 end 364 end 365 366 -- shift other frame numbers down 367 table.remove(spoonfish.spaces[space_id].frames, id_removing) 368 369 if spoonfish.spaces[space_id].frame_previous > id_removing then 370 spoonfish.spaces[space_id].frame_previous = 371 spoonfish.spaces[space_id].frame_previous - 1 372 end 373 374 -- TODO: actually resize other frames 375 376 spoonfish.frame_focus(space_id, spoonfish.spaces[space_id].frame_previous, 377 true) 378end 379 380-- split a frame vertically 381spoonfish.frame_vertical_split = function(space_id, frame_id) 382 return spoonfish.frame_split(space_id, frame_id, true) 383end 384 385-- return the first window in this frame 386spoonfish.frame_top_window = function(space_id, frame_id) 387 for _, w in ipairs(spoonfish.windows) do 388 if w["space"] == space_id and w["frame"] == frame_id then 389 return w 390 end 391 end 392end 393 394spoonfish.frame_message_timer = nil 395spoonfish._frame_message = nil 396spoonfish.frame_message = function(space_id, frame_id, message, sticky) 397 if spoonfish.frame_message_timer ~= nil then 398 spoonfish.frame_message_timer:stop() 399 end 400 401 if spoonfish._frame_message ~= nil then 402 spoonfish._frame_message:delete() 403 spoonfish._frame_message = nil 404 end 405 406 if message == nil then 407 return 408 end 409 410 if spoonfish.spaces[space_id].frames[frame_id] == nil then 411 return 412 end 413 local frame = spoonfish.spaces[space_id].frames[frame_id].rect 414 415 local textFrame = hs.drawing.getTextDrawingSize(message, 416 { size = spoonfish.frame_message_font_size }) 417 local lwidth = textFrame.w + 30 418 local lheight = textFrame.h + 10 419 420 spoonfish._frame_message = hs.canvas.new { 421 x = frame.x + (frame.w / 2) - (lwidth / 2), 422 y = frame.y + (frame.h / 2) - (lheight / 2), 423 w = lwidth, 424 h = lheight, 425 }:level(hs.canvas.windowLevels.popUpMenu) 426 427 spoonfish._frame_message[1] = { 428 id = "1", 429 type = "rectangle", 430 action = "fill", 431 center = { 432 x = lwidth / 2, 433 y = lheight / 2, 434 }, 435 fillColor = { green = 0, blue = 0, red = 0, alpha = 0.9 }, 436 roundedRectRadii = { xRadius = 15, yRadius = 15 }, 437 } 438 spoonfish._frame_message[2] = { 439 id = "2", 440 type = "text", 441 frame = { 442 x = 0, 443 y = ((lheight - spoonfish.frame_message_font_size) / 2) - 1, 444 h = "100%", 445 w = "100%", 446 }, 447 textAlignment = "center", 448 textColor = { white = 1.0 }, 449 textSize = spoonfish.frame_message_font_size, 450 text = message, 451 } 452 453 spoonfish._frame_message:show() 454 455 if not sticky then 456 spoonfish.frame_message_timer = hs.timer.doAfter( 457 spoonfish.frame_message_secs, function() 458 if spoonfish._frame_message ~= nil then 459 spoonfish._frame_message:delete() 460 spoonfish._frame_message = nil 461 end 462 spoonfish.frame_message_timer = nil 463 end) 464 end 465end 466 467-- for debugging 468spoonfish.draw_frames = function(space_id) 469 if not spoonfish.debug_frames then 470 return 471 end 472 473 for i, f in pairs(spoonfish.spaces[space_id].frames) do 474 if f.outline == nil then 475 f.outline = hs.drawing.rectangle(f.rect) 476 else 477 f.outline:setFrame(f.rect) 478 end 479 f.outline:setLevel(hs.drawing.windowLevels.normal) 480 f.outline:setStrokeColor({ ["hex"] = "#ff0000" }) 481 f.outline:setStrokeWidth(4) 482 f.outline:setFill(false) 483 f.outline:show() 484 end 485end