|
| 1 | +local _ = require 'stackline.utils.utils' |
| 2 | +local spaces = require("hs._asm.undocumented.spaces") |
| 3 | +local screen = require 'hs.screen' |
| 4 | +local u = require 'stackline.utils.underscore' |
| 5 | +local fnutils = require("hs.fnutils") |
| 6 | + |
| 7 | +--[[ |
| 8 | +The goal of this file is to eliminate the need to 'shell out' to yabai to query |
| 9 | +window data needed to render stackline, which would address |
| 10 | +https://github.com/AdamWagner/stackline/issues/8. The main problem with relying |
| 11 | +on yabai is that a 0.03s sleep is required in the yabai script to ensure that |
| 12 | +the changes that triggered hammerspoon's window event subscriber are, in fact, |
| 13 | +represented in the query response from yabai. There are probably secondary |
| 14 | +downsides, such as overall performance, and specifically *yabai* performance |
| 15 | +(I've noticed that changing focus is slower when lots of yabai queries are |
| 16 | +happening simultaneously). |
| 17 | +
|
| 18 | +┌────────┐ |
| 19 | +│ Status │ |
| 20 | +└────────┘ |
| 21 | +We're not yet using any of the code in this file to actually render the |
| 22 | +indiators or query ata — all of that is still achieved via the "old" methods. |
| 23 | +
|
| 24 | +However, this file IS being required by ./core.lua and runs one every window focus |
| 25 | +event, and the resulting "stack" data is printed to the hammerspoon console. |
| 26 | +
|
| 27 | +The stack data structure differs from that used in ./stack.lua enough that it |
| 28 | +won't work as a drop-in replacement. I think that's fine (and it wouldn't be |
| 29 | +worth attempting to make this a non-breaking change, esp. since zero people rely |
| 30 | +on it as of 2020-08-02. |
| 31 | +
|
| 32 | +┌──────┐ |
| 33 | +│ Next │ |
| 34 | +└──────┘ |
| 35 | +- [ ] Integrate appropriate functionality in this file into the Stack module |
| 36 | +- [ ] Update key Stack module functions to have basic compatiblity with the new data structure |
| 37 | +- [ ] Simplify / refine Stack functions to leverage the benefits of having access to the hs.window module for each tracked window |
| 38 | +- [ ] Integrate appropriate functionality in this file into the Core module |
| 39 | +- [ ] … see if there's anything left and decide where it should live |
| 40 | +
|
| 41 | +┌───────────┐ |
| 42 | +│ WIP NOTES │ |
| 43 | +└───────────┘ |
| 44 | +Much of the functionality in this file should either be integrated into |
| 45 | +stack.lua or core.lua — I don't think a new file is needed. |
| 46 | +
|
| 47 | +Rather than calling out to the script ../bin/yabai-get-stacks, we're using |
| 48 | +hammerspoon's mature (if complicated) hs.window.filter and hs.window modules to |
| 49 | +achieve the same goal natively within hammerspon. |
| 50 | +
|
| 51 | +There might be other benefits in addition to fixing the problems that inspired |
| 52 | +#8: We get "free" access to the *hammerspoon* window module in the window data |
| 53 | +tracked by stackline, which will probably make it easier to implement |
| 54 | +enhancements that we haven't even considered yet. This approach should also be |
| 55 | +easier to maintain, *and* we get to drop the jq dependency! |
| 56 | +
|
| 57 | +--]] |
| 58 | + |
| 59 | +local wfd = hs.window.filter.new():setOverrideFilter{ -- {{{ |
| 60 | + visible = true, -- (i.e. not hidden and not minimized) |
| 61 | + fullscreen = false, |
| 62 | + currentSpace = true, |
| 63 | + allowRoles = 'AXStandardWindow', |
| 64 | +} -- }}} |
| 65 | + |
| 66 | +function getSpaces() -- {{{ |
| 67 | + return fnutils.mapCat(screen.allScreens(), function(s) |
| 68 | + return spaces.layout()[s:spacesUUID()] |
| 69 | + end) |
| 70 | +end -- }}} |
| 71 | + |
| 72 | +function getActiveSpaceIndex() -- {{{ |
| 73 | + local s = getSpaces() |
| 74 | + local activeSpace = spaces.activeSpace() |
| 75 | + return _.indexOf(s, activeSpace) |
| 76 | +end -- }}} |
| 77 | + |
| 78 | +function makeStackId(win, winSpaceId) -- {{{ |
| 79 | + -- generate stackId from spaceId & frame values |
| 80 | + -- example: "302|35|63|1185|741" |
| 81 | + local frame = win:frame():floor() |
| 82 | + local x = frame.x |
| 83 | + local y = frame.y |
| 84 | + local w = frame.w |
| 85 | + local h = frame.h |
| 86 | + return table.concat({winSpaceId, x, y, w, h}, '|') |
| 87 | +end -- }}} |
| 88 | + |
| 89 | +function mapWin(hsWindow) -- {{{ |
| 90 | + return { |
| 91 | + stackId = makeStackId(hsWindow, hsWindow:spaces()[1]), |
| 92 | + id = hsWindow:id(), |
| 93 | + x = hsWindow:frame().x, |
| 94 | + y = hsWindow:frame().y, |
| 95 | + app = hsWindow:application():name(), |
| 96 | + title = hsWindow:title(), |
| 97 | + frame = hsWindow:frame(), |
| 98 | + _win = hsWindow, |
| 99 | + } |
| 100 | +end -- }}} |
| 101 | + |
| 102 | +function lenGreaterThanOne(t) |
| 103 | + return #t > 1 |
| 104 | +end |
| 105 | + |
| 106 | +function makeStacksFromWindows(ws) -- {{{ |
| 107 | + local windows = u.map(ws, mapWin) |
| 108 | + local groupedWindows = _.groupBy(windows, 'stackId') |
| 109 | + -- stacks contain more than one window, |
| 110 | + -- so ignore groups with only 1 window |
| 111 | + local stacks = hs.fnutils.filter(groupedWindows, lenGreaterThanOne) |
| 112 | + return stacks |
| 113 | +end -- }}} |
| 114 | + |
| 115 | +function winToHs(win) |
| 116 | + return win._win |
| 117 | +end |
| 118 | + |
| 119 | +function stackOccluded(stack) -- {{{ |
| 120 | + -- FIXES: When a stack that has "zoom-parent": 1 occludes another stack, the |
| 121 | + -- occluded stack's indicators shouldn't be displaed |
| 122 | + -- https://github.com/AdamWagner/stackline/issues/11 |
| 123 | + |
| 124 | + -- Returns true if any non-stack window occludes the stack's frame. |
| 125 | + -- This can occur when an unstacked window is zoomed to cover a stack. |
| 126 | + -- In this situation, we want to *hide* the occluded stack's indicators |
| 127 | + -- TODO: Convert to Stack instance method (wouldn't need to pass in the 'stack' arg) |
| 128 | + |
| 129 | + function notInStack(hsWindow) |
| 130 | + local stackWindowsHs = u.map(u.values(stack), winToHs) |
| 131 | + local isInStack = u.include(stackWindowsHs, hsWindow) |
| 132 | + return not isInStack |
| 133 | + end |
| 134 | + |
| 135 | + -- NOTE: under.filter works with tables |
| 136 | + -- _.filter only works with "list-like" tables |
| 137 | + local nonStackWindows = u.filter(wfd:getWindows(), notInStack) |
| 138 | + |
| 139 | + function isStackInside(nonStackWindow) |
| 140 | + local stackFrame = stack[1]._win:frame() |
| 141 | + return stackFrame:inside(nonStackWindow:frame()) |
| 142 | + end |
| 143 | + |
| 144 | + return u.any(_.map(nonStackWindows, isStackInside)) |
| 145 | +end -- }}} |
| 146 | + |
| 147 | +-- luacheck: ignore |
| 148 | +function stacksOccluded(stacks) -- {{{ |
| 149 | + -- NOTE: This *could* be a simple one-liner |
| 150 | + local occludedStacks = _.map(stacks, stackOccluded) |
| 151 | + _.pheader('occluded stacks:') |
| 152 | + _.p(occludedStacks) |
| 153 | + return occludedStacks |
| 154 | +end -- }}} |
| 155 | + |
| 156 | +function windowsCurrentSpace() -- {{{ |
| 157 | + local ws = wfd:getWindows() |
| 158 | + local stacks = makeStacksFromWindows(ws) |
| 159 | + _.pheader('STACKS!') |
| 160 | + _.p(stacks, 3) |
| 161 | + stacksOccluded(stacks) |
| 162 | +end -- }}} |
| 163 | + |
| 164 | +wfd:subscribe(hs.window.filter.windowFocused, windowsCurrentSpace) |
| 165 | + |
| 166 | +windowsCurrentSpace() |
| 167 | + |
0 commit comments