Skip to content

Commit

Permalink
Add lookup table linking config values to onChange event functions to…
Browse files Browse the repository at this point in the history
… run when value changes. Hook up features.clickT

oFocus conditionals.
  • Loading branch information
AdamWagner committed Oct 4, 2020
1 parent 16309f1 commit 51936f9
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 108 deletions.
44 changes: 32 additions & 12 deletions lib/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ function utils.invoke(instance, name, ...) -- {{{
end
end -- }}}

function utils.cb(fn) -- {{{
return function()
return fn
end
end -- }}}

function utils.extract(list, comp, transform, ...) -- {{{
-- from moses.lua
-- extracts value from a list
Expand Down Expand Up @@ -225,13 +231,33 @@ function utils.setfield(f, v, t) -- {{{
end
end -- }}}

function utils.getfield(f, t) -- {{{
function utils.getfield(f, t, isSafe) -- {{{
-- FROM: https://www.lua.org/pil/14.1.html
-- TODO: add 'tryGet()' — safe get that *doesn't* cause other errors during startup

local v = t or _G -- start with the table of globals
local res = nil
for w in string.gmatch(f, "[%w_]+") do
v = v[w]

if type(v) ~= 'table' then
return v -- if v isn't table, return immediately
end

v = v[w] -- lookup next val

if v ~= nil then
res = v -- only update safe result if v not null
end

log.d('utils.getfield — key "word"',w)
log.d('utils.getfield — "v" is:', v)
log.d('utils.getfield — "res" is:', res)
end
if isSafe then -- return the last non-nil value found
if v ~= nil then return v
else return res end
else return v -- return the last value found regardless
end
return v
end -- }}}

function utils.max(t, transform) -- {{{
Expand All @@ -252,6 +278,8 @@ function utils.boolToNum(value) -- {{{
end -- }}}

function utils.toBool(val) -- {{{
if type(val) == 'boolean' then return val end
log.d(val)
local TRUE = {
['1'] = true,
['t'] = true,
Expand Down Expand Up @@ -409,14 +437,6 @@ function utils.zip(a, b) -- {{{
return rv
end -- }}}

-- function utils.zip(a, b)
-- local rv = {}
-- for k,v in pairs(a) do
-- rv[v] = b[k]
-- end
-- return rv
-- end

function utils.copyShallow(orig) -- {{{
-- FROM: https://github.com/XavierCHN/go/blob/master/game/go/scripts/vscripts/utils/table.lua
local orig_type = type(orig)
Expand Down Expand Up @@ -527,7 +547,7 @@ function utils.levenshteinDistance(str1, str2) -- {{{
(char1[i] == char2[j] and 0 or 1))
end
end
return distance[len1][len2] / #str2 -- note
return distance[len1][len2] / #str2 -- note
end -- }}}

function flattenDict(tbl) -- {{{
Expand Down
109 changes: 60 additions & 49 deletions stackline/configmanager.lua
Original file line number Diff line number Diff line change
@@ -1,53 +1,24 @@
-- https://github.com/erento/lua-schema-validation
local v = require 'stackline.lib.valid'
local o = v.optional
local log = hs.logger.new('sline.conf')
log.setLogLevel('debug')
log.setLogLevel('info')
log.i("Loading module")

local M = {}

-- Validators & type lookup
local v = require 'stackline.lib.valid'
local o = v.optional
local is_color = v.is_table { -- {{{
white = o(v.is_number()),
red = o(v.is_number()),
red = o(v.is_number()),
green = o(v.is_number()),
blue = o(v.is_number()),
blue = o(v.is_number()),
alpha = o(v.is_number()),
} -- }}}

local cb = function(fn) -- {{{
return function()
return fn
end
end -- }}}

local function unknownType(v) -- {{{
local function unknownTypeValidator(v) -- {{{
log.i("Not validating: ", schemaType)
return true
end -- }}}

local M = {}

function M:getPathSchema(path) -- {{{
local _type = u.getfield(path, self.schema) -- lookup type in schema
if not _type then
return false
end
local validator = self.types[_type]()
return _type, validator
end -- }}}

function M.generateValidator(schemaType) -- {{{
if type(schemaType) == 'table' then
local children = u.map(schemaType, M.generateValidator)
log.d('validator children:\n', hs.inspect(children))
return v.is_table(children)
else
log.i('schemaType:', schemaType)
return
M.types[schemaType] and M.types[schemaType]() -- returns a fn to be called with value to validate
or unknownType -- unknown types are assumed-valid
end
end -- }}}

M.types = { -- {{{
['string'] = {
validator = v.is_string,
Expand Down Expand Up @@ -78,7 +49,6 @@ M.types = { -- {{{
coerce = u.identity,
},
} -- }}}

M.schema = { -- {{{
paths = {
getStackIdxs = 'string',
Expand Down Expand Up @@ -111,6 +81,28 @@ M.schema = { -- {{{
},
} -- }}}

function M:getPathSchema(path) -- {{{
local _type = u.getfield(path, self.schema) -- lookup type in schema
if not _type then
return false
end
local validator = self.types[_type].validator()
return _type, validator
end -- }}}
function M.generateValidator(schemaType) -- {{{
if type(schemaType) == 'table' then
local children = u.map(schemaType, M.generateValidator)
log.d('validator children:\n', hs.inspect(children))
return v.is_table(children)
else
log.i('schemaType:', schemaType)
return
M.types[schemaType] and M.types[schemaType].validator() -- returns a fn to be called with value to validate
or unknownTypeValidator -- unknown types are assumed-valid
end
end -- }}}

-- Config manager
function M:init(conf) -- {{{
log.i('Initializing configmanager…')
self:validate(conf)
Expand Down Expand Up @@ -138,9 +130,13 @@ function M:validate(conf) -- {{{
self.autosuggestions = u.keys(u.flatten(self.conf))
else
local invalidKeys = table.concat(u.keys(u.flatten(err)), ", ")
hs.notify.show('Invalid stackline config!',
'invalid keys:' .. invalidKeys,
'Please refer to the default conf file.')
hs.notify.new(nil, {
title = 'Invalid stackline config!',
subTitle = 'invalid keys:' .. invalidKeys,
informativeText = 'Please refer to the default conf file.',
withdrawAfter = 10
}):send()

log.e('Invalid stackline config:\n', hs.inspect(err))
end

Expand All @@ -159,7 +155,16 @@ function M:autosuggest(path) -- {{{
end
table.sort(scores, asc)
log.d(hs.inspect(scores))
return scores[1][2], scores[2][2] -- return the best 2 matches

local result1, result2 = scores[1][2], scores[2][2] -- return the best 2 matches

hs.notify.new(nil, {
title = 'Did you mean?',
subTitle = string.format('"%s"', result1),
informativeText = string.format('"%s" is not a default stackline config path', path),
withdrawAfter = 10
}):send()

end -- }}}

function M:getOrSet(path, val) -- {{{
Expand All @@ -173,10 +178,13 @@ end -- }}}
function M:get(path) -- {{{
-- @path is a dot-separated string (e.g., 'appearance.color')
-- return full config if no path provided
if path == nil then
return self.conf
if path == nil then return self.conf end

local ok, val = pcall(u.getfield, path, self.conf)

if ok then return val
else self:autosuggest(path)
end
return u.getfield(path, self.conf)
end -- }}}

function M:set(path, val) -- {{{
Expand All @@ -185,15 +193,18 @@ function M:set(path, val) -- {{{
non-existent path segments will be set to an empty table ]]

local _type, validator = self:getPathSchema(path) -- lookup type in schema
val = self.types[_type].coerce(val) -- coerce val to correct type when possible

if not _type then
hs.notify.show('Did you mean?', self:autosuggest(path), string.format(
'"%s" is not a default stackline config path', path))
self:autosuggest(path)
else
local isValid, err = validator(val) -- validate val is appropriate type
local isValid, err = validator(val) -- validate val is appropriate type
if isValid then
log.d('Setting', path, 'to', val)
u.setfield(path, val, self.conf)

local onChange = u.getfield(path, self.events, true).onChange
if type(onChange) == 'function' then onChange() end
else
log.e(hs.inspect(err))
end
Expand Down
95 changes: 49 additions & 46 deletions stackline/stackline.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
require("hs.ipc")
u = require 'stackline.lib.utils'

local wf = hs.window.filter -- just an alias
local u = require 'stackline.lib.utils'
local cb = u.invoke
local wf = hs.window.filter -- just an alias
local u = require 'stackline.lib.utils'
local cb = u.invoke
local log = hs.logger.new('stackline')
log.setLogLevel('debug')
log.i("Loading module")
Expand All @@ -12,18 +12,33 @@ stackline = {}
stackline.config = require 'stackline.stackline.configManager'
stackline.window = require 'stackline.stackline.window'

stackline.focusedScreen = nil
function stackline.init(userConfig) -- {{{
log.i('starting stackline')

stackline.config:init( -- init config with default conf + user overrides
table.merge(require 'stackline.conf', userConfig)
)

stackline.manager = require('stackline.stackline.stackmanager'):init()

stackline.manager:update() -- always update window state on start

if stackline.config:get('features.clickToFocus') then
hs.alert.show('clickTracker has started')
log.i('FEAT: ClickTracker starting')
stackline.clickTracker:start()
end
end -- }}}

stackline.wf = wf.new():setOverrideFilter{ -- {{{
stackline.wf = wf.new():setOverrideFilter{
visible = true, -- (i.e. not hidden and not minimized)
fullscreen = false,
currentSpace = true,
allowRoles = 'AXStandardWindow',
} -- }}}
}

local click = hs.eventtap.event.types['leftMouseDown'] -- print hs.eventtap.event.types to see all event types
stackline.clickTracker = hs.eventtap.new({click}, -- {{{
function(e)
stackline.clickTracker = hs.eventtap.new({click}, function(e) -- {{{
-- Listen for left mouse click events
-- if indicator containing the clickAt position can be found, focus that indicator's window
local clickAt = hs.geometry.point(e:location().x, e:location().y)
Expand All @@ -35,34 +50,27 @@ function(e)
end) -- }}}

stackline.refreshClickTracker = function() -- {{{
local turnedOn = stackline.config:get('features.clickToFocus')

if stackline.clickTracker:isEnabled() then
stackline.clickTracker:stop()
stackline.clickTracker:stop() -- always stop if running
end
if turnedOn then -- only start if feature is enabled
log.d('features.clickToFocus is enabled!')
hs.alert.show('clickTracker has refreshed')
stackline.clickTracker:start()
else
log.d('features.clickToFocus is DISABLED ❌')
stackline.clickTracker:stop() -- double-stop if disabled
stackline.clickTracker = nil -- erase if disabled
end
stackline.clickTracker:start()
end -- }}}

function stackline.start(userConfig) -- {{{
log.i('starting stackline')

-- init config with default conf + user overrides
stackline.config:init(
table.merge(
require 'stackline.conf',
userConfig
)
)

stackline.manager = require('stackline.stackline.stackmanager'):init()
stackline.manager:update() -- always update window state on start
stackline.clickTracker:start()
end -- }}}

-- 0.30s delay debounces querying via Hammerspoon & yabai
-- yabai is only queried if Hammerspoon query results are different than current state
stackline.queryWindowState = hs.timer.delayed.new(
0.30,
function() stackline.manager:update() end
-- cb(stackline.manager, 'update') → This should work but it doesn't
stackline.queryWindowState = hs.timer.delayed.new(0.30, function()
-- 0.30s delay debounces querying via Hammerspoon & yabai
-- yabai is only queried if Hammerspoon query results are different than current state
stackline.manager:update()
end
)

function stackline.redrawWinIndicator(hsWin, _app, _event) -- {{{
Expand Down Expand Up @@ -96,29 +104,24 @@ stackline.windowEvents = { -- {{{
wf.windowMinimized,
} -- }}}

-- On each win evt above, query window state & check if refersh needed
stackline.wf:subscribe(stackline.windowEvents, function() -- {{{
-- callback args: window, app, event
stackline.queryWindowState:start()
end) -- }}}

stackline.wf:subscribe(wf.windowFocused, stackline.redrawWinIndicator)

local unfocused = { wf.windowNotVisible, wf.windowUnfocused }
stackline.wf:subscribe(unfocused, stackline.redrawWinIndicator)
-- On each win evt listed, simply *redraw* indicators
-- No need for heavyweight query + refresh
stackline.wf:subscribe({
wf.windowFocused,
wf.windowNotVisible,
wf.windowUnfocused,
}, stackline.redrawWinIndicator)

-- On space switch, query window state & refresh, plus refresh click tracker
hs.spaces.watcher.new(function() -- {{{
-- Added 2020-08-12 to fill the gap of hs._asm.undocumented.spaces
stackline.queryWindowState:start()
stackline.refreshClickTracker()
end):start() -- }}}

-- Delayed start (stackline module needs to be loaded globally before it can reference its own methods)
-- TODO: Add instructions to README.md to call stackline:start(userPrefs) from init.lua, and remove this.

-- hs.timer.doUntil(function() -- {{{
-- return stackline.manager
-- end, function()
-- stackline.start()
-- end, 0.1) -- }}}

return stackline
Loading

0 comments on commit 51936f9

Please sign in to comment.