285 lines
12 KiB
Lua
285 lines
12 KiB
Lua
--- === hs.watchable ===
|
|
---
|
|
--- A minimalistic Key-Value-Observer framework for Lua.
|
|
---
|
|
--- This module allows you to generate a table with a defined label or path that can be used to share data with other modules or code. Other modules can register as watchers to a specific key-value pair within the watchable object table and will be automatically notified when the key-value pair changes.
|
|
---
|
|
--- The goal is to provide a mechanism for sharing state information between separate and (mostly) unrelated code easily and in an independent fashion.
|
|
|
|
local USERDATA_TAG = "hs.watchable"
|
|
-- local module = require(USERDATA_TAG..".internal")
|
|
local module = {}
|
|
|
|
-- private variables and methods -----------------------------------------
|
|
|
|
local mt_object, mt_watcher
|
|
mt_object = {
|
|
__watchers = {},
|
|
__objects = setmetatable({}, {__mode = "kv"}),
|
|
__values = setmetatable({}, {__mode = "k"}),
|
|
__canChange = setmetatable({}, {__mode = "k"}),
|
|
__name = USERDATA_TAG,
|
|
__type = USERDATA_TAG,
|
|
__index = function(self, index)
|
|
return mt_object.__values[self][index]
|
|
end,
|
|
__newindex = function(self, index, value)
|
|
local oldValue = mt_object.__values[self][index]
|
|
mt_object.__values[self][index] = value
|
|
if oldValue ~= value then
|
|
local objectPath = mt_object.__objects[self]
|
|
if mt_object.__watchers[objectPath] then
|
|
if mt_object.__watchers[objectPath][index] then
|
|
for _, v in pairs(mt_object.__watchers[objectPath][index]) do
|
|
if v._active and v._callback then
|
|
v._callback(v, objectPath, index, oldValue, value)
|
|
end
|
|
end
|
|
end
|
|
if mt_object.__watchers[objectPath]["*"] then
|
|
for _, v in pairs(mt_object.__watchers[objectPath]["*"]) do
|
|
if v._active and v._callback then
|
|
v._callback(v, objectPath, index, oldValue, value)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
__len = function(self)
|
|
return #mt_object.__values[self]
|
|
end,
|
|
__pairs = function(self) return pairs(mt_object.__values[self]) end,
|
|
__tostring = function(self) return USERDATA_TAG .. " table for path " .. mt_object.__objects[self] end,
|
|
}
|
|
-- mt_object.__metatable = mt_object.__index
|
|
|
|
mt_watcher = {
|
|
__name = USERDATA_TAG .. ".watcher",
|
|
__type = USERDATA_TAG .. ".watcher",
|
|
__index = {
|
|
--- hs.watchable:pause() -> watchableObject
|
|
--- Method
|
|
--- Temporarily stop notifications about the key-value pair(s) watched by this watchableObject.
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * the watchableObject
|
|
pause = function(self) self._active = false ; return self end,
|
|
--- hs.watchable:resume() -> watchableObject
|
|
--- Method
|
|
--- Resume notifications about the key-value pair(s) watched by this watchableObject which were previously paused.
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * the watchableObject
|
|
resume = function(self) self._active = true ; return self end,
|
|
--- hs.watchable:release() -> nil
|
|
--- Method
|
|
--- Removes the watchableObject so that key-value pairs watched by this object no longer generate notifications.
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * nil
|
|
release = function(self)
|
|
self._active = false
|
|
if mt_object.__watchers[self._objPath][self._objKey] then -- may have already been removed by gc
|
|
for _,v in pairs(mt_object.__watchers[self._objPath][self._objKey]) do
|
|
if v == self then mt_object.__watchers[self._objPath][self._objKey] = nil end
|
|
end
|
|
end
|
|
setmetatable(self, nil)
|
|
return nil
|
|
end,
|
|
--- hs.watchable:callback(fn) -> watchableObject
|
|
--- Method
|
|
--- Change or remove the callback function for the watchableObject.
|
|
---
|
|
--- Parameters:
|
|
--- * `fn` - a function, or an explicit nil to remove, specifying the new callback function to receive notifications for this watchableObject
|
|
---
|
|
--- Returns:
|
|
--- * the watchableObject
|
|
---
|
|
--- Notes:
|
|
--- * see [hs.watchable.watch](#watch) for a description of the arguments the callback function should expect.
|
|
callback = function(self, ...)
|
|
local args = table.pack(...)
|
|
local callback = args[1]
|
|
if not callback and args.n == 0 then
|
|
self._callback = nil
|
|
return self
|
|
elseif type(callback) == "function" then
|
|
self._callback = callback
|
|
return self
|
|
else
|
|
error("callback must be a function", 2)
|
|
end
|
|
end,
|
|
--- hs.watchable:value([key]) -> currentValue
|
|
--- Method
|
|
--- Get the current value for the key-value pair being watched by the watchableObject
|
|
---
|
|
--- Parameters:
|
|
--- * `key` - if the watchableObject was defined with a key of "*", this argument is required and specifies the specific key of the watched table to retrieve the value for. If a specific key was specified when the watchableObject was defined, this argument is ignored.
|
|
---
|
|
--- Returns:
|
|
--- * The current value for the key-value pair being watched by the watchableObject. May be nil.
|
|
value = function(self, key)
|
|
local lookupKey = self._objKey
|
|
if lookupKey == "*" and key == nil then
|
|
error("key required for watched path with wildcard key", 2)
|
|
elseif lookupKey == "*" then
|
|
lookupKey = key
|
|
end
|
|
local object = mt_object.__objects[self._objPath]
|
|
return object and object[lookupKey]
|
|
end,
|
|
--- hs.watchable:change([key], value) -> watchableObject
|
|
--- Method
|
|
--- Externally change the value of the key-value pair being watched by the watchableObject
|
|
---
|
|
--- Parameters:
|
|
--- * `key` - if the watchableObject was defined with a key of "*", this argument is required and specifies the specific key of the watched table to change the value of. If a specific key was specified when the watchableObject was defined, this argument must not be provided.
|
|
--- * `value` - the new value for the key.
|
|
---
|
|
--- Returns:
|
|
--- * the watchableObject
|
|
---
|
|
--- Notes:
|
|
--- * if external changes are not allowed for the specified path, this method generates an error
|
|
change = function(self, ...)
|
|
local args = table.pack(...)
|
|
local key, value
|
|
if args.n == 1 then
|
|
key, value = nil, args[1]
|
|
elseif args.n == 2 then
|
|
key, value = args[1], args[2]
|
|
else
|
|
error("value or key, value arguments expected", 2)
|
|
end
|
|
local lookupKey = self._objKey
|
|
if lookupKey == "*" and key == nil then
|
|
error("key required for watched path with wildcard key", 2)
|
|
elseif lookupKey == "*" then
|
|
lookupKey = key
|
|
end
|
|
local object = mt_object.__objects[self._objPath]
|
|
if object and mt_object.__canChange[object] then
|
|
object[lookupKey] = value
|
|
else
|
|
error("external changes disallowed for watched path " .. self._objPath, 2)
|
|
end
|
|
end,
|
|
},
|
|
__gc = function(self) self.release(self) end,
|
|
__tostring = function(self) return USERDATA_TAG .. ".watcher for path " .. self._path end,
|
|
}
|
|
-- mt_watcher.__metatable = mt_watcher.__index
|
|
|
|
-- Public interface ------------------------------------------------------
|
|
|
|
--- hs.watchable.new(path, [externalChanges]) -> table
|
|
--- Constructor
|
|
--- Creates a table that can be watched by other modules for key changes
|
|
---
|
|
--- Parameters:
|
|
--- * `path` - the global name for this internal table that external code can refer to the table as.
|
|
--- * `externalChanges` - an optional boolean, default false, specifying whether external code can make changes to keys within this table (bi-directional communication).
|
|
---
|
|
--- Returns:
|
|
--- * a table with metamethods which will notify external code which is registered to watch this table for key-value changes.
|
|
---
|
|
--- Notes:
|
|
--- * This constructor is used by code which wishes to share state information which other code may register to watch.
|
|
---
|
|
--- * You may specify any string name as a path, but it must be unique -- an error will occur if the path name has already been registered.
|
|
--- * All key-value pairs stored within this table are potentially watchable by external code -- if you wish to keep some data private, do not store it in this table.
|
|
--- * `externalChanges` will apply to *all* keys within this table -- if you wish to only allow some keys to be externally modifiable, you will need to register separate paths.
|
|
--- * If external changes are enabled, you will need to register your own watcher with [hs.watchable.watch](#watch) if action is required when external changes occur.
|
|
module.new = function(path, allowChange)
|
|
allowChange = allowChange or false
|
|
if type(path) ~= "string" then error ("path must be a string", 2) end
|
|
if mt_object.__objects[path] then
|
|
error(path .. " already registered", 2)
|
|
end
|
|
local self = setmetatable({}, mt_object)
|
|
mt_object.__objects[path] = self
|
|
mt_object.__objects[self] = path
|
|
mt_object.__canChange[self] = allowChange
|
|
mt_object.__values[self] = {}
|
|
return self
|
|
end
|
|
|
|
--- hs.watchable.watch(path, [key], callback) -> watchableObject
|
|
--- Constructor
|
|
--- Creates a watcher that will be invoked when the specified key in the specified path is modified.
|
|
---
|
|
--- Parameters:
|
|
--- * `path` - a string specifying the path to watch. If `key` is not provided, then this should be a string of the form "path.key" where the key will be identified as the string after the last "."
|
|
--- * `key` - if provided, a string specifying the specific key within the path to watch.
|
|
--- * `callback` - an optional function which will be invoked when changes occur to the key specified within the path. The function should expect the following arguments:
|
|
--- * `watcher` - the watcher object itself
|
|
--- * `path` - the path being watched
|
|
--- * `key` - the specific key within the path which invoked this callback
|
|
--- * `old` - the old value for this key, may be nil
|
|
--- * `new` - the new value for this key, may be nil
|
|
---
|
|
--- Returns:
|
|
--- * a watchableObject
|
|
---
|
|
--- Notes:
|
|
--- * This constructor is used by code which wishes to watch state information which is being shared by other code.
|
|
---
|
|
--- * The callback function is invoked after the new value has already been set -- the callback is a "didChange" notification, not a "willChange" notification.
|
|
---
|
|
--- * If the key (specified as a separate argument or as the final component of path) is "*", then all key-value pair changes that occur for the table specified by the path will invoke a callback. This is a shortcut for watching an entire table, rather than just a specific key-value pair of the table.
|
|
--- * It is possible to register a watcher for a path that has not been registered with [hs.watchable.new](#new) yet. Retrieving the current value with [hs.watchable:value](#value) in such a case will return nil.
|
|
module.watch = function(path, key, callback)
|
|
if type(path) ~= "string" then error ("path must be a string", 2) end
|
|
if type(key) == "function" or type(key) == "nil" then
|
|
callback = key
|
|
local objPath, objKey = path:match("^(.+)%.([^%.]+)$")
|
|
if not (objPath and objKey) then error ("malformed path; must be of the form 'path.key' or path and key must be separate arguments", 2) end
|
|
path = objPath
|
|
key = objKey
|
|
end
|
|
if type(callback) ~= "function" and type(callback) ~= "nil" then error ("callback must be a function or nil", 2) end
|
|
|
|
local objPath, objKey = path, key
|
|
|
|
local self = setmetatable({
|
|
_path = objPath .. "." .. objKey,
|
|
_objKey = objKey,
|
|
_objPath = objPath,
|
|
_active = true,
|
|
_callback = callback,
|
|
}, mt_watcher)
|
|
|
|
if not mt_object.__watchers[objPath] then mt_object.__watchers[objPath] = {} end
|
|
if not mt_object.__watchers[objPath][objKey] then mt_object.__watchers[objPath][objKey] = setmetatable({}, {__mode = "v"}) end
|
|
table.insert(mt_object.__watchers[objPath][objKey], self)
|
|
|
|
return self
|
|
end
|
|
|
|
-- Return Module Object --------------------------------------------------
|
|
|
|
-- for debugging, may remove in the future
|
|
setmetatable(module, {
|
|
__index = function(_, key)
|
|
return ({
|
|
mt_object = mt_object,
|
|
mt_watcher = mt_watcher,
|
|
})[key] or nil -- the "or nil" isn't necessary but it makes our purpose clearer
|
|
end,
|
|
})
|
|
|
|
return module
|