From bfebfa9eff04895494569a64184121852d6353cc Mon Sep 17 00:00:00 2001 From: Alin Panaitiu Date: Mon, 3 Aug 2020 16:49:17 +0300 Subject: [PATCH 1/2] Use yabai signals for events --- README.md | 8 +--- bin/yabai-get-stacks | 49 ++++++---------------- stackline/core.lua | 98 ++++++++++++++++++++++++++++--------------- stackline/stack.lua | 45 ++------------------ stackline/window.lua | 35 +++++++++------- utils/table-utils.lua | 38 +++++++++++------ 6 files changed, 128 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 5ef9003..5e6c0d0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bin/yabai-get-stacks b/bin/yabai-get-stacks index 7d02bc1..7e50c2e 100755 --- a/bin/yabai-get-stacks +++ b/bin/yabai-get-stacks @@ -1,4 +1,4 @@ -#!/usr/local/bin/dash +#!/bin/dash # ↑ dash for fast startup @@ -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 @@ -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 - ' + ' diff --git a/stackline/core.lua b/stackline/core.lua index 8a7ba37..4f38b35 100644 --- a/stackline/core.lua +++ b/stackline/core.lua @@ -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{ + "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_launched", + "application_terminated", + "application_visible", + "window_created", + "window_destroyed", + "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) \ No newline at end of file diff --git a/stackline/stack.lua b/stackline/stack.lua index b2eb4a5..43286b0 100644 --- a/stackline/stack.lua +++ b/stackline/stack.lua @@ -5,8 +5,6 @@ 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() @@ -14,28 +12,17 @@ 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 -- }}} @@ -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) @@ -74,8 +49,6 @@ 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] = {} @@ -83,11 +56,9 @@ function Stack:newStack(stack, stackId) -- {{{ 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 @@ -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 @@ -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) diff --git a/stackline/window.lua b/stackline/window.lua index d91265e..ac72dd8 100644 --- a/stackline/window.lua +++ b/stackline/window.lua @@ -43,12 +43,8 @@ function Window.__eq(a, b) -- {{{ -- FIXME: unused as of 2020-07-31 local t1 = a.id local t2 = b.id - print(a.id, a.focused) - print(t2, b.focused) local existComp = {id = a.id, frame = a.frameFlat, focused = a.focused} local currComp = {id = b.id, frame = b.frameFlat, focused = b.focused} - -- _.p('A Compare:', existComp) - -- _.p('B Compare:', currComp) local isEqual = _.isEqual(existComp, currComp) return isEqual end -- }}} @@ -70,15 +66,15 @@ function Test:new(name, age) return test end -local amy = Test:new('amy', 18) -local adam = Test:new('adam', 33) -local carl = Test:new('carl', 18) +-- local amy = Test:new('amy', 18) +-- local adam = Test:new('adam', 33) +-- local carl = Test:new('carl', 18) -print('amy equals adam?', amy == adam) -print('amy equals carl?', amy == carl) -print('amy plus adam?', (amy + adam)) -print('amy plus carl?', (amy + carl)) -print('amy plus amy?', (amy + amy)) +-- print('amy equals adam?', amy == adam) +-- print('amy equals carl?', amy == carl) +-- print('amy plus adam?', (amy + adam)) +-- print('amy plus carl?', (amy + carl)) +-- print('amy plus amy?', (amy + amy)) -- }}} -- TODO: ↑ Convert to .__eq metatable @@ -93,8 +89,9 @@ function Window:process(showIcons, currTabIdx) -- {{{ local unfocused_color = {white = 0.9, alpha = 0.30} local focused_color = {white = 0.9, alpha = 0.99} local padding = 4 + local iconPadding = 4 local aspectRatio = 5 - local size = 25 + local size = 32 local width = self.showIcons and size or (size / aspectRatio) @@ -113,6 +110,13 @@ function Window:process(showIcons, currTabIdx) -- {{{ h = size, } + self.icon_rect = { + x = iconPadding, + y = self.indicator_rect.y + iconPadding, + w = self.indicator_rect.w - (iconPadding * 2), + h = self.indicator_rect.h - (iconPadding * 2), + } + self.color_opts = { bg = self.focused and focused_color or unfocused_color, canvasAlpha = self.focused and 1 or 0.2, @@ -132,19 +136,20 @@ function Window:draw_indicator() -- {{{ self.indicator = hs.canvas.new(self.canvas_frame) local width = self.indicator_rect.w + local radius = self.showIcons and (self.indicator_rect.w / 4.0) or 4.0 self.indicator:appendElements({ type = "rectangle", action = "fill", fillColor = self.color_opts.bg, frame = self.indicator_rect, - roundedRectRadii = {xRadius = 2.0, yRadius = 2.0}, + roundedRectRadii = {xRadius = radius, yRadius = radius}, }) if self.showIcons then self.indicator:appendElements({ type = "image", image = self:iconFromAppName(), - frame = self.indicator_rect, + frame = self.icon_rect, imageAlpha = self.color_opts.imageAlpha, }) end diff --git a/utils/table-utils.lua b/utils/table-utils.lua index 7915f53..5e46d6a 100644 --- a/utils/table-utils.lua +++ b/utils/table-utils.lua @@ -3,6 +3,7 @@ local tostring = tostring local type = type local pairs = pairs +local ipairs = ipairs local string = string local table = table local load = load @@ -31,7 +32,7 @@ function t2s(t) local result = {} do rL[rL.cL]._f,rL[rL.cL]._s,rL[rL.cL]._var = pairs(t) - --result[#result + 1] = "{\n"..string.rep("\t",levels+1) + --result[#result + 1] = "{\n"..string.rep("\t",levels+1) result[#result + 1] = "{" -- Non pretty version rL[rL.cL].t = t while true do @@ -119,7 +120,7 @@ function t2spp(t) local result = {} do rL[rL.cL]._f,rL[rL.cL]._s,rL[rL.cL]._var = pairs(t) - result[#result + 1] = "{\n"..string.rep("\t",levels+1) + result[#result + 1] = "{\n"..string.rep("\t",levels+1) --result[#result + 1] = "{" -- Non pretty version rL[rL.cL].t = t while true do @@ -212,7 +213,7 @@ end -- nil, boolean, number, string, table -- The other three types (function, userdata and thread) get their tostring values stored and end up as a string ID. function t2sr(t) - if type(t) ~= 'table' then return nil, 'Expected table parameter' end + if type(t) ~= 'table' then return nil, 'Expected table parameter' end local rL = {cL = 1} -- Table to track recursion into nested tables (cL = current recursion level) rL[rL.cL] = {} local tabIndex = {} -- Table to store a list of tables indexed into a string and their variable name @@ -278,7 +279,7 @@ function t2sr(t) -- k is of the type function, userdata or thread key = 't'..rL[rL.cL].tabIndex..'.'..tostring(k) --rL[rL.cL].str = rL[rL.cL].str..'\\n'..key..'=' - result[#result + 1] = "\n"..key.."=" + result[#result + 1] = "\n"..key.."=" end -- if type(k)ends end -- if not k and rL.cL == 1 then ends if key then @@ -377,12 +378,21 @@ function mergeArrays(t1,t2,duplicates,isduplicate) if add then table.insert(t2, t1[i]) end - end + end return t2 end -- Function to check whether value v is in array t1 -- if equal is a given function then equal is called with a value from the table and the value to compare. If it returns true then the values are considered equal +function contains(t1,v) + for i = 1,#t1 do + if t1[i] == v then + return true + end + end + return false -- Value v not in t1 +end + function inArray(t1,v,equal) equal = (type(equal)=="function" and equal) or function(v1,v2) return v1==v2 @@ -392,7 +402,6 @@ function inArray(t1,v,equal) return i -- Value v found in t1 at ith location end end - return false -- Value v not in t1 end function emptyTable(t) @@ -446,7 +455,7 @@ function copyTable(t1,t2,full,map,tabDone) end else -- type(v) = ="table" - if full then + if full then if type(k) == "table" then local kp if not tabDone[k] then @@ -494,7 +503,7 @@ function compareTables(t1,t2,traversing) end traversing = traversing or {} traversing[t1] = t2 -- t1 is being traversed to match it to t2 - local donet2 = {} -- To mark which keys are taken + local donet2 = {} -- To mark which keys are taken for k,v in pairs(t1) do --print(k,v) if type(v) == "number" or type(v) == "string" or type(v) == "boolean" or type(v) == "function" or type(v) == "thread" or type(v) == "userdata" then @@ -649,7 +658,7 @@ function diffTable(t1,t2,map,tabDone,diff) end keyTabs[kt1] = k if t1[kt1] == nil or t1[kt1] ~= v then - diff[t1][kt1] = v + diff[t1][kt1] = v diffDirty = true end else -- if type(k) == "table" then else @@ -657,9 +666,9 @@ function diffTable(t1,t2,map,tabDone,diff) if t1[k] ~= v then diff[t1][k] = v diffDirty = true - end + end end -- if type(k) == "table" then ends - else --if type(v) ~= "table" then + else --if type(v) ~= "table" then -- v == "table" if type(k) == "table" then -- Both v and k are tables @@ -712,7 +721,7 @@ function diffTable(t1,t2,map,tabDone,diff) diffDirty = true end else - -- k is a table + -- k is a table -- get the t2 counterpart if it was found if not keyTabs[k] then diff[t1][k] = setnil @@ -724,3 +733,8 @@ function diffTable(t1,t2,map,tabDone,diff) return diffDirty and diff end +function Set (list) + local set = {} + for _, l in ipairs(list) do set[l] = true end + return set +end \ No newline at end of file From 1bdc159e13cd0395a46f56e7cab6e58462705179 Mon Sep 17 00:00:00 2001 From: Alin Panaitiu Date: Mon, 3 Aug 2020 18:06:44 +0300 Subject: [PATCH 2/2] Fix cases where stack needs to be redrawn --- stackline/core.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stackline/core.lua b/stackline/core.lua index 4f38b35..8afe86f 100644 --- a/stackline/core.lua +++ b/stackline/core.lua @@ -18,6 +18,10 @@ local showIcons = getOrSet("showIcons", false) wsi = Stack:newStackManager(showIcons) local shouldRestack = tut.Set{ + "application_terminated", + "application_launched", + "window_created", + "window_destroyed", "window_resized", "window_moved", "toggle_icons", @@ -25,11 +29,7 @@ local shouldRestack = tut.Set{ local shouldClean = tut.Set{ "application_hidden", - "application_launched", - "application_terminated", "application_visible", - "window_created", - "window_destroyed", "window_deminimized", "window_minimized", }