Skip to content

Use yabai signals for events #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,7 @@ cmd + ctrl - right : yabai -m window east --stack $(yabai -m query --windows --w

```sh
# Get the repo
git clone https://github.com/AdamWagner/stackline.git ~/Downloads/stackline
cd ~/Downloads/stackline

# Symlink stackline modules to your hammerspoon config dir
ln -sr ./stackline ~/.hammerspoon/stackline
ln -sr ./utils ~/.hammerspoon/utils
ln -sr ./bin ~/.hammerspoon/bin
git clone https://github.com/AdamWagner/stackline.git ~/hammerspoon/stackline

# Make stackline run when hammerspoon launches
cd ~/.hammerspoon
Expand Down
49 changes: 13 additions & 36 deletions bin/yabai-get-stacks
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/local/bin/dash
#!/bin/dash

# ↑ dash for fast startup

Expand All @@ -13,18 +13,18 @@ USAGE
windowFullscreened, windowUnfullscreened
See ../stackline/core.lua

RETURNS window stack data
RETURNS window stack data
as a json array of "stacks"
where each stack is an array of windows:
[
[
{ "id": "123abc", … },
[
{ "id": "123abc", … },
{…},
{…},
{…},
],
],
[…]
]
]

DEPENDS on 'yabai' & 'jq'
EOF
Expand All @@ -38,43 +38,20 @@ fi
# Set path per /etc/paths
# Ensures that non-standard binaries `yabai` & `jq` are available
# Alternatively, you may specify absolute paths
PATH=$(/usr/libexec/path_helper)

# The unfortunate `sleep` command
# Sadly, on a 16" "macbook pro, at least 0.03 delay
# is required to ensure `yabai -m query …` returns the focused window and not
# the *previously* focused window.
# Without the sleep, the active stacked-window indicator remains on the
# previously active stacked-window once every ~1 in 5 focus events —
# particularly when changing focus rapidly.

# Why does yabai query occasionally not return new state
# when called by Hammerspoon's `hs.window.filter.windowFocused` event?
# Hypothesis: if a yabai query blocks a subsequent query, sequential calls may
# accumulate and eventually fall behind the current state? Actually, this
# doesn't really make sense, wouldn't this mean the last call would be *late*,
# and therefore be slow, but reflect the latest state?

# ALTERNATIVE?
# For events that will certainly result in a stack indicator update (e.g., windowFocused),
# we could call this continuously w/ exponential backoff *until the response is different*.
# This seems like it would provide the best of both worlds: responsive updates
# when the yabai query reflects the change, and eventual consistency when it doesn't.
# The cost is additional calls to `yabai -m query …`, which may be significant.
# There may be other costs I'm not considering, e.g., what happens when there
# are 5 focus events in 5 seconds?
sleep 0.03
eval $(/usr/libexec/path_helper)

# The main course
yabai -m query --windows --space \
yabai -m query --windows --space $YABAI_SPACE \
| jq --raw-output --compact-output --monochrome-output '
map(del(.title)) # titles may break lua json parsing
map(with_entries(select(
.key == ("id", "app", "subrole", "frame", "focused", "stack-index", "visible")
))) # select only the fields that we need
| map(select(
.subrole == "AXStandardWindow" and
.subrole == "AXStandardWindow" and
.visible == 1)) # minimized == 0 may be preferrable?
| map(.frameFlat = "\(.frame.x)|\(.frame.y)") # frame x,y to string to group wins → stacks
| sort_by(.["stack-index"])
| group_by(.frameFlat) # … the aforementioned grouping
| map(select(length > 1)) # we only care about *stacks*, which contain > 1 window
'
'

98 changes: 64 additions & 34 deletions stackline/core.lua
Original file line number Diff line number Diff line change
@@ -1,47 +1,77 @@
local _ = require 'stackline.utils.utils'
require("hs.ipc")

local Stack = require 'stackline.stackline.stack'
local tut = require 'stackline.utils.table-utils'

-- This file is trash: lowercase globals, copy/paste duplication in
-- update_stack_data_redraw() just to pass 'shouldClean':true :(
print(hs.settings.bundleID)

wsi = Stack:newStackManager()
wf = hs.window.filter.default
function getOrSet(key, val)
local existingVal = hs.settings.get(key)
if existingVal == nil then
hs.settings.set(key, val)
return val
end
return existingVal
end

local win_added = {
hs.window.filter.windowCreated,
hs.window.filter.windowUnhidden,
hs.window.filter.windowUnminimized,
}
local showIcons = getOrSet("showIcons", false)
wsi = Stack:newStackManager(showIcons)

local win_removed = {
hs.window.filter.windowDestroyed,
hs.window.filter.windowHidden,
hs.window.filter.windowMinimized,
hs.window.filter.windowMoved,
local shouldRestack = tut.Set{
"application_terminated",
"application_launched",
"window_created",
"window_destroyed",
"window_resized",
"window_moved",
"toggle_icons",
}

-- NOTE: windowMoved captures movement OR resize events
local win_changed = {
hs.window.filter.windowFocused,
hs.window.filter.windowUnfocused,
hs.window.filter.windowFullscreened,
hs.window.filter.windowUnfullscreened,
local shouldClean = tut.Set{
"application_hidden",
"application_visible",
"window_deminimized",
"window_minimized",
}

-- TODO: convert to use wsi.update method
-- ./stack.lua:15
local added_changed = tut.mergeArrays(win_added, win_changed)
function configHandler(_, msgID, msg)
if msgID == 900 then
return "version:2.0a"
end

if msgID == 500 then
key, value = msg:match(".+:([%a_-]+):([%a%d_-]+)")
if key == "toggle_icons" then
showIcons = not showIcons
hs.settings.set("showIcons", showIcons)
end

if shouldRestack[key] then
wsi.cleanup()
wsi = Stack:newStackManager(showIcons)
end
wsi.update(shouldClean[key])
end
return "ok"
end

function yabaiSignalHandler(_, msgID, msg)
if msgID == 900 then
return "version:2.0a"
end

if msgID == 500 then
event = msg:match(".+:([%a_]+)")

wf:subscribe(added_changed, (function(_win, _app, event)
_.pheader(event)
wsi:update()
end))
if shouldRestack[event] then
wsi.cleanup()
wsi = Stack:newStackManager(showIcons)
end
wsi.update(shouldClean[event])
end
return "ok"
end

wf:subscribe(win_removed, (function(_win, _app, event)
_.pheader(event)
-- look(win)
-- print(app)
wsi:update(true)
end))

ipcEventsPort = hs.ipc.localPort("stackline-events", yabaiSignalHandler)
ipcConfigPort = hs.ipc.localPort("stackline-config", configHandler)
45 changes: 4 additions & 41 deletions stackline/stack.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,24 @@ local under = require 'stackline.utils.underscore'

local Stack = {}

-- TODO: include hs.task functionality from core.lua in the Stack module directly

function Stack:toggleIcons() -- {{{
self.showIcons = not self.showIcons
Stack.update()
end -- }}}

function Stack:each_win_id(fn) -- {{{
_.each(self.tabStacks, function(stack)
-- hs.alert.show('running each win id')
_.pheader('stack')
-- _.p(stack)
-- _.p(under.values(stack))
local winIds = _.map(under.values(stack), function(w)
return w.id
end)
print(hs.inspect(winIds))

for i = 1, #winIds do
-- ┌────────────────────┐
-- the main event!
-- the main event!
-- └────────────────────┘
-- hs.alert.show(winIds[i])

fn(winIds[i]) -- Call the `fn` provided with win ID

-- hs.alert.show('inside final loop')

-- DEBUG
print(hs.inspect(winIds))
-- print(winIds[i])
end
end)
end -- }}}
Expand All @@ -51,21 +38,9 @@ function Stack:findWindow(wid) -- {{{
end -- }}}

function Stack:cleanup() -- {{{
_.p('# to be cleaned: ', _.length(self.tabStacks))
_.p('keys be cleaned: ', _.keys(self.tabStacks))

for key, stack in pairs(self.tabStacks) do
-- DEBUG:
-- _.p(stack)
_.pheader('stack keys')
_.p(_.map(stack, function(w)
return _.pick(w, {'id', 'app'})
end))

-- For each window, clear canvas element
_.each(stack, function(w)
_.pheader('window indicator in cleanup')
print(w.indicator)
w.indicator:delete()
end)

Expand All @@ -74,20 +49,16 @@ function Stack:cleanup() -- {{{
end -- }}}

function Stack:newStack(stack, stackId) -- {{{
print('stack data #:', #stack)
print('stack ID: ', stackId)
local extantStack = self.tabStacks[stackId]
if not extantStack then
self.tabStacks[stackId] = {}
end

for k, w in pairs(stack) do
if not extantStack then
print('First run')
local win = Window:new(w)
win:process(self.showIcons, k)
win.indicator:show()
-- _.p(win)
win.stackId = stackId -- set stackId on win for easy lookup later
self.tabStacks[stackId][win.id] = win

Expand All @@ -97,11 +68,9 @@ function Stack:newStack(stack, stackId) -- {{{

if (type(extantWin) == 'nil') or
not (extantWin.focused == win.focused) then
print('Needs updated:', extantWin.app)
extantWin.indicator:delete()
win:process(self.showIcons, k)
win.indicator:show()
-- _.p(win)
win.stackId = stackId -- set stackId on win for easy lookup later
self.tabStacks[stackId][win.id] = win
end
Expand All @@ -114,32 +83,26 @@ function Stack:ingest(windowData) -- {{{
local stackId = table.concat(_.map(winGroup, function(w)
return w.id
end), '')
print(stackId)
Stack:newStack(winGroup, stackId)
end)
end -- }}}

function Stack:update(shouldClean) -- {{{

_.pheader('value of "shouldClean:"')
_.p(shouldClean)
print('\n\n')
if shouldClean then
_.pheader('running cleanup')
Stack:cleanup()
end

local yabai_get_stacks = 'stackline/bin/yabai-get-stacks'

hs.task.new("/usr/local/bin/dash", function(_code, stdout)
hs.task.new("/bin/dash", function(_code, stdout)
local windowData = hs.json.decode(stdout)
Stack:ingest(windowData)
end, {yabai_get_stacks}):start()
end -- }}}

function Stack:newStackManager()
function Stack:newStackManager(showIcons)
self.tabStacks = {}
self.showIcons = false
self.showIcons = showIcons
return {
ingest = function(windowData)
return self:ingest(windowData)
Expand Down
Loading