From f21c62237eb42df8acf4204bb2e3e5352f7b7ba1 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 03:11:50 -0800 Subject: [PATCH 01/12] Add the output of some future files. Otherwise the CI will be angry. --- tests/examples/text/gears/watcher/command1.output.txt | 1 + tests/examples/text/gears/watcher/command2.output.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/examples/text/gears/watcher/command1.output.txt create mode 100644 tests/examples/text/gears/watcher/command2.output.txt diff --git a/tests/examples/text/gears/watcher/command1.output.txt b/tests/examples/text/gears/watcher/command1.output.txt new file mode 100644 index 00000000..3750c6e1 --- /dev/null +++ b/tests/examples/text/gears/watcher/command1.output.txt @@ -0,0 +1 @@ +one two three diff --git a/tests/examples/text/gears/watcher/command2.output.txt b/tests/examples/text/gears/watcher/command2.output.txt new file mode 100644 index 00000000..2bdf67ab --- /dev/null +++ b/tests/examples/text/gears/watcher/command2.output.txt @@ -0,0 +1 @@ +three From 3feeec3a1f1876fcb60c875a8a7191bc9816b52f Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Mon, 16 Apr 2018 15:17:20 -0400 Subject: [PATCH 02/12] widget.base: Add template support. This allows some widgets to have declarative templates as first level elements. It can be used for lazy loading or conditional loading. This commit also improve get_children_by_id, but does not fix all the corner case. As proposed by psychon, this will require them to be moved to the hierarchy. --- lib/wibox/widget/base.lua | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/wibox/widget/base.lua b/lib/wibox/widget/base.lua index 904f5adc..1c16dd5c 100644 --- a/lib/wibox/widget/base.lua +++ b/lib/wibox/widget/base.lua @@ -510,6 +510,17 @@ local function drill(ids, content) -- Create layouts based on metatable's __call. local l = layout.is_widget and layout or layout() + -- Check if the widget added it's own element by ids + --FIXME #2181 + if l and l._private and l._private.by_id then + for k, v in pairs(l._private.by_id) do + ids[k] = ids[k] or {} + for _, v2 in ipairs(v) do + table.insert(ids[k], v2) + end + end + end + -- Get the number of children widgets (including nil widgets). local max, attributes, widgets = parse_table(content, l.allow_empty_widget) @@ -533,6 +544,14 @@ local function drill(ids, content) end end + -- Let some containers handle the template themselves. + -- This can be used for use cases such as lazy loading, repeaters or + -- conditional loaders. + if l._accept_templates and l.set_templates then + l:set_templates(widgets) + return l, id + end + -- Add all widgets. for k = 1, max do -- ipairs cannot be used on sparse tables. @@ -555,6 +574,7 @@ local function drill(ids, content) end end end + -- Replace all children (if any) with the new ones. if widgets then l:set_children(widgets) From e3976e0a192d469f36f72665993fc719694c60be Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 18 Oct 2020 01:08:35 -0700 Subject: [PATCH 03/12] timer: Replace `.data` by `._private`. First step toward converting it into a normal `gears.object` to later use a base class. --- lib/gears/timer.lua | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/gears/timer.lua b/lib/gears/timer.lua index ee1778d7..7904739a 100644 --- a/lib/gears/timer.lua +++ b/lib/gears/timer.lua @@ -87,11 +87,11 @@ local timer = { mt = {} } -- @method start -- @emits start function timer:start() - if self.data.source_id ~= nil then + if self._private.source_id ~= nil then gdebug.print_error(traceback("timer already started")) return end - self.data.source_id = glib.timeout_add(glib.PRIORITY_DEFAULT, self.data.timeout * 1000, function() + self._private.source_id = glib.timeout_add(glib.PRIORITY_DEFAULT, self._private.timeout * 1000, function() protected_call(self.emit_signal, self, "timeout") return true end) @@ -102,12 +102,12 @@ end -- @method stop -- @emits stop function timer:stop() - if self.data.source_id == nil then + if self._private.source_id == nil then gdebug.print_error(traceback("timer not started")) return end - glib.source_remove(self.data.source_id) - self.data.source_id = nil + glib.source_remove(self._private.source_id) + self._private.source_id = nil self:emit_signal("stop") end @@ -118,7 +118,7 @@ end -- @emits start -- @emits stop function timer:again() - if self.data.source_id ~= nil then + if self._private.source_id ~= nil then self:stop() end self:start() @@ -137,9 +137,9 @@ end local timer_instance_mt = { __index = function(self, property) if property == "timeout" then - return self.data.timeout + return self._private.timeout elseif property == "started" then - return self.data.source_id ~= nil + return self._private.source_id ~= nil end return timer[property] @@ -147,7 +147,7 @@ local timer_instance_mt = { __newindex = function(self, property, value) if property == "timeout" then - self.data.timeout = tonumber(value) + self._private.timeout = tonumber(value) self:emit_signal("property::timeout", value) end end @@ -167,7 +167,27 @@ function timer.new(args) args = args or {} local ret = object() - ret.data = { timeout = 0 } --TODO v5 rename to ._private + rawset(ret, "_private", { timeout = 0 }) + + -- Preserve backward compatibility with Awesome 4.0-4.3 use of "data" + -- rather then "_private". + rawset(ret, "data", setmetatable({}, { + __index = function(_, key) + gdebug.deprecate( + "gears.timer.data is deprecated, use normal properties", + {deprecated_in=5} + ) + return ret._private[key] + end, + __newindex = function(_, key, value) + gdebug.deprecate( + "gears.timer.data is deprecated, use normal properties", + {deprecated_in=5} + ) + ret._private[key] = value + end + })) + setmetatable(ret, timer_instance_mt) for k, v in pairs(args) do From 23d3969d8959907a507cd439b29d88d5f924fda2 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:12:40 -0800 Subject: [PATCH 04/12] timer: Enable the full `gears.object` property support. In the next few commits, this will be used to implement a replacment for `awful.widgets.watch`. --- lib/gears/timer.lua | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/gears/timer.lua b/lib/gears/timer.lua index 7904739a..2d594e16 100644 --- a/lib/gears/timer.lua +++ b/lib/gears/timer.lua @@ -59,6 +59,7 @@ local traceback = debug.traceback local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) local glib = require("lgi").GLib local object = require("gears.object") +local gtable = require("gears.table") local protected_call = require("gears.protected_call") local gdebug = require("gears.debug") @@ -134,24 +135,28 @@ end -- @param number -- @propemits true false -local timer_instance_mt = { - __index = function(self, property) - if property == "timeout" then - return self._private.timeout - elseif property == "started" then - return self._private.source_id ~= nil - end +function timer:get_timeout() + return self._private.timeout +end - return timer[property] - end, +function timer:get_started() + return self._private.source_id ~= nil +end - __newindex = function(self, property, value) - if property == "timeout" then - self._private.timeout = tonumber(value) - self:emit_signal("property::timeout", value) - end +function timer:set_started(value) + if value == self:get_started() then return end + + if value then + self:start() + else + self:stop() end -} +end + +function timer:set_timeout(value) + self._private.timeout = tonumber(value) + self:emit_signal("property::timeout", value) +end --- Create a new timer object. -- @tparam table args Arguments. @@ -165,7 +170,12 @@ local timer_instance_mt = { -- @constructorfct gears.timer function timer.new(args) args = args or {} - local ret = object() + local ret = object { + enable_properties = true, + enable_auto_signals = true, + } + + gtable.crush(ret, timer, true) rawset(ret, "_private", { timeout = 0 }) @@ -188,8 +198,6 @@ function timer.new(args) end })) - setmetatable(ret, timer_instance_mt) - for k, v in pairs(args) do ret[k] = v end From 537b44b57bbee4cc57e642f981f10661ef93459a Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:26:05 -0800 Subject: [PATCH 05/12] widget: Add a generic way to extend the declarative tree system. With this addition, it is now possible to let the right hand side part of the `key = value` to handle setting the value. --- lib/wibox/widget/base.lua | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/wibox/widget/base.lua b/lib/wibox/widget/base.lua index 1c16dd5c..0a634d86 100644 --- a/lib/wibox/widget/base.lua +++ b/lib/wibox/widget/base.lua @@ -471,7 +471,8 @@ end -- Read the table, separate attributes from widgets. local function parse_table(t, leave_empty) local max = 0 - local attributes, widgets = {}, {} + local attributes, widgets, with_handlers = {}, {}, {} + for k,v in pairs(t) do if type(k) == "number" then if v then @@ -481,20 +482,35 @@ local function parse_table(t, leave_empty) max = k end - widgets[k] = v + -- Those are not added "normally". They have their own callback + -- to add them. + if rawget(v, "_set_declarative_handler") then + with_handlers[k] = v + else + widgets[k] = v + end end + elseif type(v) == "table" and rawget(v, "_set_declarative_handler") then + with_handlers[k] = v else attributes[k] = v end end + -- Make sure there is no holes in the widgets table. + if #with_handlers > 0 then + for k in pairs(with_handlers) do + table.remove(widgets, k) + end + end + -- Pack the sparse table, if the container doesn't support sparse tables. if not leave_empty then widgets = gtable.from_sparse(widgets) max = #widgets end - return max, attributes, widgets + return max, attributes, widgets, with_handlers end -- Recursively build a container from a declarative table. @@ -522,7 +538,7 @@ local function drill(ids, content) end -- Get the number of children widgets (including nil widgets). - local max, attributes, widgets = parse_table(content, l.allow_empty_widget) + local max, attributes, widgets, with_handlers = parse_table(content, l.allow_empty_widget) -- Get the optional identifier to create a virtual widget tree to place -- in an "access table" to be able to retrieve the widget. @@ -579,6 +595,13 @@ local function drill(ids, content) if widgets then l:set_children(widgets) end + + -- Now that all the children are set, call the custom handlers. + -- The order is undefined. + for k, v in pairs(with_handlers) do + v:_set_declarative_handler(l, k, ids) + end + return l, id end From e6793376e58056320665c3dd4f075287eee04ec4 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:13:45 -0800 Subject: [PATCH 06/12] Add a new reactive programming module. This module allows to attach an "expression" to a widget property. This expression will be evaluated each time it's content may change. This is the first part of a trio of new APIs to improve setting dynamic values to a widget from 3 different angles. --- lib/gears/reactive.lua | 656 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 656 insertions(+) create mode 100644 lib/gears/reactive.lua diff --git a/lib/gears/reactive.lua b/lib/gears/reactive.lua new file mode 100644 index 00000000..f45ed4a5 --- /dev/null +++ b/lib/gears/reactive.lua @@ -0,0 +1,656 @@ +--- Utility module to convert functions to Excel like objects. +-- +-- When converting a function into a `gears.reactive` object, all +-- properties accessed by the function are monitored for changes. +-- +-- When such a change is detected, the `property::value` signal is emitted. When +-- used within a widget declarative construct, the property it is attached +-- to will also be automatically updated. +-- +-- Theory +-- ====== +-- +-- ![Client geometry](../images/gears_reactive.svg) +-- +-- To use this module and, more importantly, to understand how to write something +-- that actually works based on it, some background is required. Most AwesomeWM +-- objects are based on the `gears.object` module or it's `C` equivalent. Those +-- objects have "properties". Behind the scene, they have getters, setters and +-- a signal to notify the value changed. +-- +-- `gears.reactive` adds a firewall like sandbox around the function. It +-- intercept any `gears.object` instance trying to cross the sandbox boundary +-- and replace them with proxy objects. Those proxies have built-in +-- introspection code to detect how they are used. This is then converted into +-- a list of objects and signals to monitor. Once one of the monitored object +-- emits one of the monitored signal, then the whole function is re-evaluated. +-- Each time the function is evaluated, the "target" properties are updated. The +-- reactive function result or any call to external function from within goes +-- through the firewall again and any proxies are removed. +-- +-- That design has one big limitation. It cannot detect any changes which are +-- not directly part of `gears.object` instance. You cannot use random tables +-- and expect the function to be called when it's content change. To work +-- around this, it is recommanded to make "hub" objects to store the data used +-- within the reactive function. +-- +-- Recommanded usage +-- ================= +-- +-- The best way to use `gears.reactive` is when the value used within the +-- expressions are part of other objects. It can be a `gears.watcher`, but +-- it can also be a custom object: +-- +-- @DOC_wibox_widget_declarative_reactive_EXAMPLE@ +-- +-- Limitations +-- =========== +-- +-- `gears.reactive` is pushing Lua way beyond what it has been designed for. +-- Because of this, there is some limitations. +-- +-- * This module will **NOT** try to track the change of other +-- functions and methods called by the expression. It is **NOT** recursive +-- and only the top level properties are tracked. This is a feature, not a +-- bug. If it was recursive, this list of limitations or gotchas would be +-- endless. +-- * This only works with `gears.object` and Core API objects which implement +-- the `property::*****` signals correctly. If it is a regular Lua table +-- or the property signals are incorrectly used, the value changes cannot +-- be detected. If you find something that should work, but doesn't in +-- one of the AwesomeWM API, [please report a bug](https://github.com/awesomeWM/awesome/issues/new). +-- * More generally, when making a custom `gears.object` with custom setters, +-- it is the programmer responsibility to emit the signal. It is also +-- required to only emit those signals when the property actually changes to +-- avoid an unecessary re-evaluations. +-- * Changing the type of the variables accessed by the reactive function +-- (its "upvalues") after the reactive expression has been created wont +-- be detected. It will cause missed updates and, potentially, hard to debug +-- Lua errors within the proxy code itself. +-- * Internally, the engine tries its best to prevent the internal proxy objects +-- to leak out the sandbox. However this cannot be perfect, at least not +-- without adding limitation elsewhere. It is probably worth reporting a bug if +-- you encounter such an issue. But set your expectations, not all corner case +-- can be fixed. +-- * Don't use rawset in the expression. +-- * If the return value is a table, only the first 3 nesting levels are sanitized. +-- Avoid using nested tables in the returned value if you can. `gears.object` +-- instances *should* be fine. +-- * There is currently no way to disable a reactive expression once it's +-- been defined. This may change eventually. +-- * Rio Lua 5.1 (not LuaJIT 2.1) is currently not support. If you need it, +-- [please report a bug](https://github.com/awesomeWM/awesome/issues/new). +-- +-- @author Emmanuel Lepage-Vallee <elv1313@gmail.com> +-- @copyright 2017-2020 Emmanuel Lepage-Vallee +-- @classmod gears.reactive + +-- This file is provided under the BSD 2 clause license since it is better than +-- any existing implementation (most of which only support Lua 5.1). +-- +-- Copyright 2020 Emmanuel Lepage-Vallee <elv1313@gmail.com> +-- +-- Redistribution and use in source and binary forms, with or without modification, +-- are permitted provided that the following conditions are met: +-- +-- 1. Redistributions of source code must retain the above copyright notice, this +-- list of conditions and the following disclaimer. +-- +-- 2. Redistributions in binary form must reproduce the above copyright notice, +-- this list of conditions and the following disclaimer in the documentation +-- and/or other materials provided with the distribution. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +-- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +-- OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +-- IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +local gobject = require("gears.object") +local gtable = require("gears.table") +local gtimer = require("gears.timer") +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +-- It's only possible to know when something changes if it has signals. +local function is_gears_object(input) + return type(input) == "table" and rawget(input, "emit_signal") +end + +-- If the sandbox proxies were to leave the reactive expressions, bad +-- things would happen. Catch functions and constructors to make sure +-- we strip away the proxy. +local function is_callable(object) + local t = type(object) + + if t == "function" then return true end + + if not (t == "table" or t == "userdata") then return false end + + local mt = getmetatable(object) + + return mt and mt.__call ~= nil +end + +-- Get the upvalue current value or cached value. +local function get_upvalue(proxy_md) + if type(proxy_md) ~= "table" then return proxy_md end + + return proxy_md._reactive.getter and + proxy_md._reactive.getter() or proxy_md._reactive.input +end + +-- Attempt to remove the proxy references. There might be loops +-- and stackoverflows, so it will only go 3 level deep. With some +-- luck, the result wont be a table. Even then, hopefully it wont +-- contain "raw" sub-tables. If it contains `gears.object`, it's +-- safe since they will be proxied. +local sanitize_return = nil +sanitize_return = function(input, depth) + if (depth or 0) > 3 then return input end + + -- The first `input` is always a table, so it is safe to call `pairs`. + for k, v in pairs(input) do + local t = type(v) + + if t == "table" then + if v._reactive then + -- No need to go deeper unless the user used rawset. + input[k] = get_upvalue(v) + else + -- It's an unproxied raw table, dig further. + sanitize_return(v, (depth or 1) + 1) + end + end + end + + return input +end + +-- The actual code that connects the gears.object signals. +local function add_key(self, key) + if type(key) ~= "string" or not is_gears_object(get_upvalue(self)) then return end + + local input, objs = get_upvalue(self), self._reactive.origin.objects + + objs[input] = objs[input] or {} + + if objs[input][key] then return end + + objs[input][key] = true + + -- Prefer the `weak` version to avoid memory leaks. If nothing is holding a + -- reference, then the value wont change. Yes, there is some hairy corner cases. + -- Yes, the expression proxy itself is holding a strong reference so this wont + -- get used very often. + if input.weak_connect_signal then + input:weak_connect_signal("property::"..key, self._reactive.origin.callback) + else + input:connect_signal("property::"..key, self._reactive.origin.callback) + end +end + +local function gen_proxy_md(input, origin, getter) + assert(input and origin) + return { + input = (not getter) and input or nil, + getter = getter, + children = {}, + properties = {}, + origin = origin + } +end + +local create_proxy = nil + +-- Un-proxy the function input and proxy its output. +local function proxy_call(self, ...) + local newargs, newrets = {}, {} + + -- Remove the proxies before calling the function. + for _, v in ipairs{...} do + if type(v) == "table" and v._reactive then + v = get_upvalue(v) + end + table.insert(newargs, v) + end + + local rets + + if #newargs > 0 then + rets = {get_upvalue(self)(unpack(newargs))} + else + rets = {get_upvalue(self)(nil)} + end + + -- Add new proxies to make sure changes are detected if something + -- like `a_function().foo` is used. Since we still have legacy accessor + -- implemented as method (t:clients(), c:tags(), etc), this is actually + -- likely to happen. + for _, v in ipairs(rets) do + local ret, _ = create_proxy(v, self._reactive.origin) + table.insert(newrets, ret) + end + + return unpack(newrets) +end + +-- Build a tree of proxies or return the primitives. +local function proxy_index(self, key) + local up = get_upvalue(self) + + -- Since it would otherwise print a cryptic error. + assert( + type(up) == "table", + "Trying to index a "..type(up).." value in a `gears.reactive` expression." + ) + + local upk = up[key] + + -- Connect. + add_key(self, key) + + local ret, is_proxy = create_proxy(upk, self._reactive.origin) + + -- Always query the non-proxy upvalue. We cannot detect if they + -- change. + if is_proxy then + rawset(self, key, ret) + end + + return ret +end + +-- Set valuers trough the proxy. +local function proxy_newindex(self, key, value) + rawset(self, key, create_proxy(value, self._reactive.origin)) + + -- Strip the proxy before setting the value on the original object. + if type(value) == "table" and value._reactive then + value = get_upvalue(value) + end + + local v = get_upvalue(self) + + v[key] = value + + -- Connect. + if is_gears_object(v) and not self._reactive.origin.objects[self][key] then + add_key(self, key) + end +end + +-- It's possible that multiple proxy points to the same value. +-- While we could make a large map of all proxies, it's simpler +-- to just implement __eq/__lt/__le and be done. +local function proxy_equal(a, b) + a, b = get_upvalue(a), get_upvalue(b) + + return a == b +end + +local function proxy_lesser(a, b) + a, b = get_upvalue(a), get_upvalue(b) + + return a < b +end + +local function proxy_lesser_equal(a, b) + a, b = get_upvalue(a), get_upvalue(b) + + return a <= b +end + +local function proxy_tostring(o) + return tostring(get_upvalue(o)) +end + +-- Wrap tables and functions into a proxy object. +create_proxy = function(input, origin, getter) + local t = type(input) + local is_call = is_callable(input) + + -- Everything but the tables are immutable. + if t ~= "table" and not is_call then return input, false end + + -- Remove any foreign proxy. + if t ~= "function" and input._reactive and input._reactive.origin ~= origin then + input = get_upvalue(input) + end + + return setmetatable({_reactive = gen_proxy_md(input, origin, getter)}, { + __index = proxy_index, + __newindex = proxy_newindex, + __call = proxy_call, + __eq = proxy_equal, + __lt = proxy_lesser, + __le = proxy_lesser_equal, + __tostring = proxy_tostring + }), true +end + +-- Create a "fake" upvalue ref because otherwise the sandbox could +-- "leak" and set upvalue in actual code outside of the function. +-- By "leak", I mean the proxy would become visible from outside +-- of the sandbox by the reactive expression "siblings" sharing the +-- same environment. +local function copy_upvalue_reference(loaded, idx, value) + -- Make sure it has a fresh upvalue reference. Something that + -- we are sure cannot be shared with something else. + local function fake_upvalue_env() + return value + end + + debug.upvaluejoin(loaded, idx, fake_upvalue_env, 1) --luacheck: ignore +end + +-- Create a meta-getter for the original `fct` upvalue at index `idx`. +-- This will allow all the other code to dynamically get a "current" version +-- of the upvalue rather than something from the cache. It will also avoid +-- having to store a strong reference to the original value. +local function create_upvalue_getter(fct, idx) + local placeholder = nil + + local function fake_upvalue_getter() + return placeholder + end + + -- Using this, each time `fake_upvalue_getter` is called, it + -- will return the current upvalue. This means we don't have to + -- cache it. Thus, the cache cannot get outdated. The drawback if + -- that if the type changes from an object to a primitive, it + -- will explode. + debug.upvaluejoin(fake_upvalue_getter, 1, fct, idx) --luacheck: ignore + + return fake_upvalue_getter +end + +-- Create a copy of the function and replace it's environment. +local function sandbox(fct, env, vars, values) + -- We must serialize the function for several reasons. First of all, it + -- might get wrapped into multiple `gears.reactive` objects (which we should + -- handle differently, but currently allow) or share the "upvalue environemnt" + -- (this variables from the upper execution context and stack frames). + -- + -- For example, if 2 function both access the variables "foo" and "bar" + -- from the global context, they might end up with the same execution + -- environment. If we didn't create a copy, calling `debug.upvaluejoin` + -- woulc affect both functions. + local dump = string.dump(fct) + local loaded = load(dump, nil, nil, env) + + -- It doesn't seem possible to "just remove" the upvalues. It's not possible + -- to have the catch-all in the metatable. It would have been nicer since this + -- code is redundant with the metatable (which are still needed for _G and _ENV + -- content). + for name, k in pairs(vars) do + if is_callable(values[name]) or type(values[name]) == "table" then + -- For table, functions and objects, use a proxy upvalue. + copy_upvalue_reference( + loaded, + k, + create_proxy(values[name], env.__origin, create_upvalue_getter(fct ,k)) + ) + else + -- For string, booleans, numbers and function, use the real upvalue. + -- This means if it is changed by something else, the sandboxed + -- copy sees the change. + debug.upvaluejoin(loaded, k, fct, k) --luacheck: ignore + end + end + + return loaded +end + +-- `getfenv` and `setfenv` would simplify this a lot, but are not +-- present in newer versions of Lua. Technically, we could have 3 +-- implementation optimized for each Lua versions, but this one +-- seems to be portable (until now...). So while the performance +-- isn't as good as it could be, it's maintainable. +local function getfenv_compat(fct, root) + local vars, soft_env = {}, {} + + local origin = { + objects = {}, + callback = function() + root:emit_signal("property::value") + end + } + + for i = 1, math.huge do + local name, val = debug.getupvalue(fct, i) + + if name == nil then break end + + soft_env[name] = val + + if not vars[name] then + vars[name] = i + end + end + + -- Create the sandbox. + local self = {__origin = origin} + local sandboxed = sandbox(fct, self, vars, soft_env) + + return setmetatable(self, { + __index = function(_, key) + if _G[key] then + return create_proxy(_G[key], origin) + end + + return soft_env[key] + end, + __newindex = function(_, key, value) + -- This `if` might be dead code. + if vars[key] then + debug.setupvalue(sandboxed, vars[key], value) + + -- Do not try to disconnect the old one. It would make the code too complex. + if (not soft_env[key]) or get_upvalue(soft_env[key]) ~= value then + soft_env[key] = create_proxy(value, origin) + end + else + rawset(vars, key, create_proxy(value, origin)) + end + end, + __call = function() + return unpack(sanitize_return({sandboxed()})) + end + }) +end + +local module = {} + +local function real_set_value(self, force) + if self._private.delayed_started and not force then return end + + local function value_transaction() + -- If `get_value` was called, then this transaction is no longer + -- pending. + if not self._private.delayed_started then return end + + -- Reset the delay in case the setter causes the expression to + -- change. + self._private.delayed_started = false + + self._private.value = self._private.origin() + self._private.evaluated = true + + for _, target in pairs(self._private.targets) do + local obj, property = target[1], target[2] + obj[property] = self._private.value + end + end + + if self._private.delayed and not force then + gtimer.delayed_call(value_transaction) + self._private.delayed_started = true + else + self._private.delayed_started = true + value_transaction() + end +end + +--- A function which will be evaluated each time its content changes. +-- +-- @property expression +-- @param function +-- @propemits true false + +--- The current value of the expression. +-- @property value +-- @propemits false false + +--- Only evaluate once per event loop iteration. +-- +-- In most case this is a simple performance win, but there is some +-- case where you might want the expression to be evaluated each time +-- one of the upvalue "really" change rather than batch them. This +-- option is enabled by default. +-- +-- @property delayed +-- @tparam[opt=true] boolean delayed +-- @propemits true false + +function module:get_delayed() + return self._private.delayed +end + +function module:set_delayed(value) + if value == self._private.delayed then return end + + self._private.delayed = value + self:emit_signal("property::delayed", value) +end + +function module:set_expression(value) + self:disconnect() + self._private.origin = getfenv_compat(value, self) + + self:connect_signal("property::value", real_set_value) +end + +function module:get_value() + -- This will call `real_set_value`. + if (not self._private.evaluated) and self._private.origin then + self:refresh() + end + + if self._private.delayed_started then + real_set_value(self, true) + end + + return self._private.value +end + +function module:set_value() + assert(false, "A value cannot be set on a `gears.reactive` instance.") +end + +--- Disconnect all expression signals. +-- +-- @method disconnect + +function module:disconnect() + if self._private.origin then + for obj, properties in pairs(self._private.origin.__origin.objects) do + for property in pairs(properties) do + obj:disconnect_signal("property::"..property, self._private.origin.__origin.callback) + end + end + end +end + +--- Recompute the expression. +-- +-- When the expression uses a non-object upvalue, the changes cannot +-- be auto-retected. Calling `:refresh()` will immediatly recompute the +-- expression. +-- +-- @method refresh + +function module:refresh() + if self._private.origin then + self:emit_signal("property::value") + end +end + +-- Add a new target property and object. +function module:_add_target(object, property) + local hash = tostring(object)..property + if self._private.targets[hash] then return end + + self._private.targets[hash] = {object, property} + + self:emit_signal("target_added", object, property) + + if self._private.evaluated then + object[property] = self._private.value + else + real_set_value(self) + end +end + +--- Emitted when a new property is attached to this reactive expression. +-- +-- @signal target_added +-- @tparam gears.object object The object (often the widget). +-- @tparam string property The property name. + +function module:_set_declarative_handler(parent, key) + -- Lua "properties", aka table.foo must be strings. + assert( + type(key) == "string", + "gears.reactive can only be used ob object properties" + ) + + self:_add_target(parent, key) +end + +--- Create a new `gears.reactive` object. +-- @constructorfct gears.reactive +-- @tparam table args +-- @tparam function args.expression A function which accesses other `gears.object`. +-- @tparam gears.object args.object Any AwesomeWM object. +-- @tparam string args.property The name of the property to track. + +local function new(_, args) + -- It *can* be done. Actually, it is much easier to implement this module + -- on 5.1 since `setfenv` is much simpler than `debug.upvaluejoin`. However, + -- unless someone asks, then why support 2 incompatible code paths. Luajit 2.1 + -- supports `debug.upvaluejoin`. + assert( + debug.upvaluejoin, --luacheck: ignore + "Sorry, `gears.reactive` doesn't support Lua 5.1 at this time" + ) + + if type(args) == "function" then + args = {expression = args} + end + + local self = gobject { + enable_properties = true, + } + + rawset(self, "_private", { + targets = {}, + delayed = true + }) + + gtable.crush(self, module, true) + + gtable.crush(self, args, false) + + self:connect_signal("property::value", real_set_value) + + return self +end + +--@DOC_object_COMMON@ + +return setmetatable(module, {__call = new}) From 7ffee87528df5a05caa02c63cc6f60f986f4916f Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:16:36 -0800 Subject: [PATCH 07/12] tests: Test the `gears.reactive` module. It has both doc and unit tests. They are disabled for Lua 5.1 since this feature isn't working yet. --- spec/gears/object_spec.lua | 2 +- spec/gears/reactive_spec.lua | 213 ++++++++++++++++++ .../wibox/decl_doc/reactive_expr1.lua | 48 ++++ .../wibox/decl_doc/reactive_expr2.lua | 40 ++++ .../wibox/widget/declarative/reactive.lua | 48 ++++ 5 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 spec/gears/reactive_spec.lua create mode 100644 tests/examples/wibox/decl_doc/reactive_expr1.lua create mode 100644 tests/examples/wibox/decl_doc/reactive_expr2.lua create mode 100644 tests/examples/wibox/widget/declarative/reactive.lua diff --git a/spec/gears/object_spec.lua b/spec/gears/object_spec.lua index 7c6eb57f..2a7dd5fc 100644 --- a/spec/gears/object_spec.lua +++ b/spec/gears/object_spec.lua @@ -148,7 +148,7 @@ describe("gears.object", function() assert.is.equal(obj2.foo, 42) end) - it("dynamic property disabled", function() + it("dynamic property enabled", function() local class = {} function class:get_foo() return "bar" end diff --git a/spec/gears/reactive_spec.lua b/spec/gears/reactive_spec.lua new file mode 100644 index 00000000..b76484ae --- /dev/null +++ b/spec/gears/reactive_spec.lua @@ -0,0 +1,213 @@ +--------------------------------------------------------------------------- +-- @author Emmanuel Lepage-Vallee +-- @copyright 2020 Emmanuel Lepage-Vallee <elv1313@gmail.com> +--------------------------------------------------------------------------- +_G.awesome.connect_signal = function() end + +local reactive = require("gears.reactive") +local gobject = require("gears.object") + +-- Keep track of the number of time the value changed. +local change_counter, last_counter = 0, 0 + +local function has_changed() + local ret = change_counter > last_counter + last_counter = change_counter + return ret +end + +describe("gears.reactive", function() + -- Unsupported. + if not debug.upvaluejoin then return end -- luacheck: globals debug.upvaluejoin + + local myobject1 = gobject { + enable_properties = true, + enable_auto_signals = true + } + + local myobject2 = gobject { + enable_properties = true, + enable_auto_signals = true + } + + local myobject3 = gobject { + enable_properties = true, + enable_auto_signals = true + } + + -- This will create a property with a signal, we will need that later. + myobject3.bar = "baz" + myobject1.foo = 0 + + -- Using rawset wont add a signal. It means the change isn't visible to the + -- `gears.reactive` expression. However, we still want to make sure it can + -- use the raw property even without change detection. + rawset(myobject2, "obj3", myobject3) + + -- Use a string to compare the address. We can't use `==` since + -- `gears.reactive` re-implement it to emulate the `==` of the source + -- objects. + local hash, hash2, hash3 = tostring(myobject1), tostring(myobject2), tostring(print) + + -- Make sure the proxy wrapper isn't passed to the called functions. + local function check_no_proxy(obj) + assert.is.equal(rawget(obj, "_reactive"), nil) + assert.is.equal(hash, tostring(obj)) + end + + -- With args. + function myobject1:method1(a, b, obj) + -- Make sure the proxy isn't propagated. + assert.is.equal(hash, tostring(obj)) + assert.is.equal(hash, tostring(self)) + assert.is.falsy(obj._reactive) + assert.is.falsy(self._reactive) + + -- Check the arguments. + assert.is.equal(a, 1) + assert.is.equal(b, 2) + + return myobject2, 42 + end + + -- With no args. + function myobject1:method2(a) + assert(a == nil) + assert(not self._reactive) + assert(hash == tostring(self)) + end + + -- Create some _ENV variables. `gears.reactive` cannot detect the changes, + -- at least for now. This is to test if they can be used regardless. + local i, r = 1337, nil + + it("basic creation", function() + r = reactive(function() + -- Skip busted, it uses its own debug magic which collide with + -- gears.reactive sandboxes. + local assert, tostring = rawget(_G, "assert"), rawget(_G, "tostring") + + -- Using _G directly should bypass the proxy. It least until more + -- magic is implemented to stop it. So better test it too. + local realprint = _G.print + assert(tostring(realprint) == hash3) + + -- But the "local" one should be proxy-ed to prevent the internal + -- proxy objects from leaking when calling a function outside of the + -- sandbox. + assert(tostring(print) == hash3) + + -- Make sure we got a proxy. + assert(myobject1._reactive) + + assert(not myobject1:method2()) + + local newobject, other = myobject1:method1(1,2, myobject1) + + -- Make sure the returned objects are proxied properly. + assert(type(other) == "number") + assert(other == 42) + assert(newobject._reactive) + assert(tostring(newobject) == tostring(myobject2)) + assert(tostring(newobject) == hash2) + + -- Now call an upvalue local function + check_no_proxy(myobject1) + + return { + not_object = i, + object_expression = (myobject1.foo + 42), + nested_object_tree = myobject2.obj3.bar, + original_obj = myobject1 + } + end) + + r:connect_signal("property::value", function() + change_counter = change_counter + 1 + end) + + assert.is_false(has_changed()) + + -- Make sure that the reactive proxy didn't override the original value. + -- And yes, it's actually possible and there is explicit code to avoid + -- it. + assert.is.equal(hash, tostring(myobject1)) + end) + + it("basic_changes", function() + local val = r.value + + -- The delayed magic should be transparent. It will never work + -- in the unit test, but it should not cause any visible behavior + -- change. It would not be magic if it was. + assert(val) + + -- Disable delayed. + r._private.value = nil + r._private.evaluated = false + assert.is_true(r.delayed) + r.delayed = false + assert.is.falsy(r.delayed) + + val = r.value + assert(val) + + -- Make sure the proxy didn't leak into the return value + assert.is.falsy(rawget(val, "_reactive")) + assert.is.falsy(rawget(val.original_obj, "_reactive")) + + assert.is_true(has_changed()) + + assert.is.equal(r._private.value.object_expression, 42) + assert.is.equal(r._private.value.not_object, 1337) + + myobject1.foo = 1 + + assert.is_true(has_changed()) + + assert.is.equal(r._private.value.object_expression, 43) + + -- Known limitation. + i = 1338 + assert.is.equal(r._private.value.not_object, 1337) + r:refresh() + assert.is.equal(r._private.value.not_object, 1338) + + -- Ensure that nested (and raw-setted) object property changes + -- are detected. + assert.is.equal(r._private.value.nested_object_tree, "baz") + myobject3.bar = "bazz" + assert.is_true(has_changed()) + assert.is.equal(r._private.value.nested_object_tree, "bazz") + end) + + -- gears.reactive play with the metatable operators a lot. + -- Make sure one of them work. + it("test tostring", function() + local myobject4 = gobject { + enable_properties = true, + enable_auto_signals = true + } + + local mt = getmetatable(myobject4) + + mt.__tostring = function() return "lol" end + + local react = reactive(function() + _G.assert(myobject4._reactive) + _G.assert(tostring(myobject4) == "lol") + + return tostring(myobject4) + end) + + local val = react.value + + assert.is.equal(val, "lol") + end) + + it("test disconnect", function() + r:disconnect() + end) +end) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/tests/examples/wibox/decl_doc/reactive_expr1.lua b/tests/examples/wibox/decl_doc/reactive_expr1.lua new file mode 100644 index 00000000..e5ebf2ba --- /dev/null +++ b/tests/examples/wibox/decl_doc/reactive_expr1.lua @@ -0,0 +1,48 @@ +--DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_USAGE --DOC_NO_DASH +local parent = ... --DOC_HIDE +local gears = { --DOC_HIDE + object = require("gears.object"), --DOC_HIDE + reactive = require("gears.reactive") --DOC_HIDE +} --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + + -- It's important to set 'enable_auto_signals' to `true` or it wont work. + -- + -- Note that most AwesomeWM objects (and most modules) objects can be + -- used directly as long as they implement the signal `property::` spec. + -- + -- So you don't *need* a hub object, but it's safer to use one. + local my_hub = gears.object { + enable_properties = true, + enable_auto_signals = true + } + + --DOC_NEWLINE + + -- Better set a default value to avoid weirdness. + my_hub.some_property = 42 + + --DOC_NEWLINE + + -- This is an example, in practice do this in your + -- wibar widget declaration tree. + local w = wibox.widget { + markup = gears.reactive(function() + -- Each time `my_hub.some_property` changes, this will be + -- re-interpreted. + return '' .. (my_hub.some_property / 100) .. '' + end), + widget = wibox.widget.textbox + } + + --DOC_NEWLINE + + -- This will update the widget text to '13.37' + my_hub.some_property = 1337 + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w) --DOC_HIDE +assert(w.markup == "13.37") --DOC_HIDE +parent:add(w) --DOC_HIDE diff --git a/tests/examples/wibox/decl_doc/reactive_expr2.lua b/tests/examples/wibox/decl_doc/reactive_expr2.lua new file mode 100644 index 00000000..ed1cf5dc --- /dev/null +++ b/tests/examples/wibox/decl_doc/reactive_expr2.lua @@ -0,0 +1,40 @@ +--DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_USAGE --DOC_NO_DASH +local parent = ... --DOC_HIDE +local gears = { --DOC_HIDE + object = require("gears.object"), --DOC_HIDE + reactive = require("gears.reactive") --DOC_HIDE +} --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + +local my_hub = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +} --DOC_HIDE + +my_hub.some_property = 42 --DOC_HIDE + + -- For some larger function, it's a good idea to move them out of + -- the declarative construct for maintainability. + local my_reactive_object = gears.reactive(function() + if my_hub.some_property > 1000 then + return "The world is fine" + else + return "The world is on fire" + end + end) + + --DOC_NEWLINE + + local w = wibox.widget { + markup = my_reactive_object, + forced_height = 20, --DOC_HIDE + widget = wibox.widget.textbox + } + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w) --DOC_HIDE +assert(w.markup == "The world is on fire") --DOC_HIDE + +parent:add(w) --DOC_HIDE diff --git a/tests/examples/wibox/widget/declarative/reactive.lua b/tests/examples/wibox/widget/declarative/reactive.lua new file mode 100644 index 00000000..5dd49c3f --- /dev/null +++ b/tests/examples/wibox/widget/declarative/reactive.lua @@ -0,0 +1,48 @@ +--DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_USAGE +local parent = ... --DOC_HIDE +local gears = { --DOC_HIDE + object = require("gears.object"), --DOC_HIDE + reactive = require("gears.reactive") --DOC_HIDE +} --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + + -- It's important to set 'enable_auto_signals' to `true` or it wont work. + -- + -- Note that most AwesomeWM objects (and most modules) objects can be + -- used directly as long as they implement the signal `property::` spec. + -- + -- So you don't *need* a hub object, but it's safer to use one. + local my_hub = gears.object { + enable_properties = true, + enable_auto_signals = true + } + + --DOC_NEWLINE + + -- Better set a default value to avoid weirdness. + my_hub.some_property = 42 + + --DOC_NEWLINE + + -- This is an example, in practice do this in your + -- wibar widget declaration tree. + local w = wibox.widget { + markup = gears.reactive(function() + -- Each time `my_hub.some_property` changes, this will be + -- re-interpreted. + return '' .. (my_hub.some_property / 100) .. '' + end), + widget = wibox.widget.textbox + } + + --DOC_NEWLINE + + -- This will update the widget text to '13.37' + my_hub.some_property = 1337 + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w) --DOC_HIDE +assert(w.markup == "13.37") --DOC_HIDE +parent:add(w) --DOC_HIDE From 8f9d23b11bd9b220016b752c36bdfc7687da4ada Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:17:24 -0800 Subject: [PATCH 08/12] Add a new `gears.connection` module. This is the declarative DSL version of `:connect_signal`. It also has some magic to access the widet object using their IDs without the usual imperative code. --- build-utils/check_for_invalid_requires.lua | 1 + lib/gears/connection.lua | 628 +++++++++++++++++++++ 2 files changed, 629 insertions(+) create mode 100644 lib/gears/connection.lua diff --git a/build-utils/check_for_invalid_requires.lua b/build-utils/check_for_invalid_requires.lua index 1c9cd29c..9c2d5d58 100755 --- a/build-utils/check_for_invalid_requires.lua +++ b/build-utils/check_for_invalid_requires.lua @@ -63,6 +63,7 @@ local allowed_deps = { }, -- TODO: Get rid of these ["gears.surface"] = { ["wibox.hierarchy"] = true }, + ["gears.connection"] = { ["*"] = true }, } -- Turn "foo.bar.baz" into "foo.bar". Returns nil if there is nothing more to diff --git a/lib/gears/connection.lua b/lib/gears/connection.lua new file mode 100644 index 00000000..6b3aeb0d --- /dev/null +++ b/lib/gears/connection.lua @@ -0,0 +1,628 @@ +--- Object oriented way to connect objects. +-- +-- All AwesomeWM objects have a `emit_signal` method. +-- They allow to attach business logic to a property value change or random +-- types of events. +-- +-- The default way to attach business logic to signals is to use `connect_signal`. +-- It allows to call a function when the signal is emitted. This remains the most +-- common way to perform a connection. However, it is very verbose to use that +-- construct alongside the declarative widget system. `gears.connection` is much +-- easier to integrate in such constructs: +-- +-- @DOC_wibox_decl_doc_connection4_EXAMPLE@ +-- +-- Limitations +-- =========== +-- +-- * When used directly as a value to a declarative object +-- (`text = gears.connection{...}`), it is necessary to manually disconnect +-- the connectio if you want it to stop being auto-updated. +-- +-- @author Emmanuel Lepage-Vallee <elv1313@gmail.com> +-- @copyright 2019-2020 Emmanuel Lepage-Vallee +-- @classmod gears.connection + +local gobject = require("gears.object") +local gtable = require("gears.table") +local gtimer = require("gears.timer") + +local module = {} + +local function get_class(name) + if type(name) ~= "string" then return name end + + return _G[name] or require(name) +end + +local function gen_get_children_by_id(self) + return function(id) + return (self._private.env_ids or {})[id] or {} + end +end + +local function get_env(_, cb) + local env, position = nil, nil + + for i = 1, math.huge do + local name, val = debug.getupvalue(cb, i) + + if name == "_ENV" then + env, position = val, i + break + elseif name == nil then + break + end + end + + return env, position +end + +-- Expose the declarative tree `id`s to the callback as variables. +-- +-- Note that this runs into a slight conflict with the ability to +-- place a widget in multiple trees. If it happens, then obviously +-- there will be `id` conflicts. That's solvable by only using +-- `get_children_by_id` in such situation. As of now, there is no +-- code to track `id`s across trees. So the callback will only have the +-- ids from the tree it was last added to. +local function extend_env(self) + local cb, ids = self._private.callbacks[1], self._private.env_ids + + if (not cb) or (not ids) then return end + + self._private.env_init = true + + local env, position = get_env(self, cb) + + if not env then return end + + local gcbi = nil + + local new_env = setmetatable({}, { + __index = function(_, key) + if key == "get_children_by_id" then + gcbi = gcbi or gen_get_children_by_id(self) + return gcbi + elseif ids[key] and #ids[key] == 1 then + return ids[key][1] + end + + local v = env[key] + + if v then return v end + + return _G[key] + end, + __newindex = function(_, key, value) + _G[key] = value + end + }) + + debug.setupvalue(cb, position, new_env) +end + +local function set_target(self) + local p = self._private + + local has_target = p.target + and p.initiate ~= false + + local has_source = #p.sources >= 1 + and #p.source_properties >= 1 + + extend_env(self) + + for _, callback in ipairs(self._private.callbacks) do + local ret = callback( + p.sources[1], + p.target, + (p.sources[1] and p.source_properties[1]) and + p.sources[1][p.source_properties[1]] or nil + ) + + if self.target_property and self._private.target then + self._private.target[self.target_property] = ret + end + end + + if p.target_method then + p.target[p.target_method]() + end + + -- There isn't enough information to initiate anything yet. + if not (has_target and has_source) then return end + + if p.target_property then + p.target[p.target_property] = p.sources[1][p.source_properties[1]] + end +end + +-- When all properties necessary to set the initial value are set. +local function initiate(self) + if self._private.initiating then return end + + -- We don't know if properties will be overriden or if a callback/method + -- will be added. Better wait. + gtimer.delayed_call(function() + -- It might have been disabled since then. + if not self._private.enabled then return end + + set_target(self) + self._private.initiating = false + end) + + self._private.initiating = true +end + +function module:get_initiate() + return self._private.initiate +end + +--- If the property should be set when the target object is defined. +-- +-- It is **enabled** by default for convinience. +-- +-- @DOC_text_gears_connection_initiate_EXAMPLE@ +-- +-- @property initiate +-- @tparam[opt=true] boolean string initiate +-- @propemits true false + +function module:set_initiate(value) + if self._private.initiate == value then return end + self._private.initiate = value + self:emit_signal("property::initiate", value) + + if value then + initiate(self) + end +end + +--- Turn this connection on or off. +-- +-- @DOC_text_gears_connection_enabled_EXAMPLE@ +-- +-- @property enabled +-- @tparam boolean enabled +-- @see disconnect +-- @see reconnect +-- @propemits true false + +function module:get_enabled() + return self._private.enabled +end + +function module:set_enabled(value) + if value == self._private.enabled then return end + + self._private.enabled = value + self:emit_signal("property::enabled", value) +end + +--- A list of source object signals. +-- +-- @property signals +-- @tparam table signals +-- @propemits true false +-- @see signal +-- @see source_property + + +function module:get_signals() + return self._private.signals +end + +function module:set_signals(value) + self:disconnect() + self._private.signals = value + self:reconnect() + + self:emit_signal("property::signal", value[1]) + self:emit_signal("property::signals", value) + + initiate(self) +end + +function module:get_signal() + return self._private.signals < 2 and self._private.signals[1] or nil +end + +--- The (source) signal to monitor. +-- +-- Note that `signals` and `source_property` are also provided to simplify +-- common use cases. +-- +-- @property signal +-- @param string +-- @propemits true false +-- @see signals +-- @see source_property + +function module:set_signal(signal) + self._private.signals = {signal} +end + +--- The object for the right side object of the connection. +-- +-- When used in a widget declarative tree, this is implicit and +-- is the parent object. +-- +-- @DOC_text_gears_connection_target_EXAMPLE@ +-- +-- @property target +-- @tparam gears.object target +-- @propemits true false +-- @see target_property +-- @see target_method + +function module:get_target() + return self._private.target +end + +function module:set_target(target) + self._private.target = target + self:emit_signal("property::target", target) + initiate(self) +end + +--- The target object property to set when the source property changes. +-- +-- @property target_property +-- @tparam string target_property +-- @propemits true false +-- @see target +-- @see target_method +-- @see source_property + +function module:get_target_property() + return self._private.target_property +end + +function module:set_target_property(value) + self._private.target_property = value + self:emit_signal("property::target_property", value) + + initiate(self) +end + +--- Rather than use a property, call a method. +-- +-- @DOC_text_gears_connection_method_EXAMPLE@ +-- +-- @property target_method +-- @tparam string target_method +-- @propemits true false +-- @see target +-- @see target_property + +function module:get_target_method() + return self._private.target_method +end + +function module:set_target_method(value) + self._private.target_method = value + self:emit_signal("property::target_method", value) + + initiate(self) +end + +--- Use a whole class rather than an object as source. +-- +-- Many classes, like `client`, `tag`, `screen` and `naughty.notification` +-- provide class level signals. When any instance of those classes emit a +-- signal, it is forwarded to the class level signals. +-- +-- @DOC_text_gears_connection_class_EXAMPLE@ +-- +-- @property source_class +-- @tparam class|string source_class +-- @propemits true false +-- @see source +-- @see source_property + +function module:set_source_class(class) + self:disconnect() + self._private.source_class = get_class(class) + self:reconnect() + self:emit_signal("property::source_class", self._private.source_class) +end + +function module:get_source_class() + return self._private.source_class +end + +--- The source object (connection left hand side). +-- @property source +-- @tparam gears.object source +-- @propemits true false +-- @see sources +-- @see source_class + +function module:get_source() + return self._private.sources[1] +end + +function module:set_source(source) + + self:disconnect() + self._private.sources = {source} + self:reconnect() + + self:emit_signal("property::source", source) + self:emit_signal("property::sources", self._private.sources) + + initiate(self) +end + +--- The source object(s)/class property. +-- +-- @property source_property +-- @tparam string source_property +-- @propemits true false + +function module:get_source_property() + return #self._private.source_properties == 1 and + self._private.source_properties[1] or nil +end + +function module:set_source_property(prop) + self.source_properties = {prop} +end + +function module:get_source_properties() + return self._private.source_properties +end + +function module:set_source_properties(props) + self:disconnect() + self._private.source_properties = props + + local signals = {} + + self:reconnect() + + for _, prop in ipairs(props) do + table.insert(signals, "property::"..prop) + end + + self.signals = signals + + self:emit_signal("property::source_property", props[1]) + self:emit_signal("property::source_properties", props) + +end + + +--- A list of source objects (connection left hand side). +-- +-- If many objects have the same signal, it's not necessary +-- to make multiple `gears.connection`. They can share the same. +-- +-- @property sources +-- @tparam gears.object sources +-- @propemits true false +-- @see append_source_object +-- @see remove_source_object + +function module:get_sources() + return self._private.sources +end + +function module:set_sources(sources) + if not sources then + sources = {} + end + + self:disconnect() + self._private.sources = sources + self:reconnect() + + self:emit_signal("property::source", sources[1]) + self:emit_signal("property::sources", sources) + + initiate(self) +end + +--- Add a source object. +-- +-- @DOC_text_gears_connection_add_remove_EXAMPLE@ +-- +-- @method append_source_object +-- @tparam gears.object obj The object. +-- @see sources +-- @see remove_source_object +-- @see has_source_object +-- @see source +-- @see sources + +function module:append_source_object(obj) + if self:has_source_object(obj) then return end + + table.insert(self._private.sources, obj) + self:emit_signal("property::sources", self._private.sources) + + if #self._private.sources == 1 then + initiate(self) + end +end + +--- Remove a source object. +-- +-- @method remove_source_object +-- @tparam gears.object obj The object. +-- @see sources +-- @see append_source_object +-- @see has_source_object +-- @see source +-- @see sources + +function module:remove_source_object(obj) + for k, o in ipairs(self._private.sources) do + if obj == o then + table.remove(self._private.sources, k) + self:emit_signal("property::sources", self._private.sources) + return true + end + end + + return false +end + +--- Return true when `obj` is already a source object. +-- +-- @method module:has_source_object +-- @tparam gears.object obj The object. +-- @see append_source_object +-- @see remove_source_object +-- @see source +-- @see sources + +function module:has_source_object(obj) + for _, o in ipairs(self._private.sources) do + if o == obj then return true end + end + + return false +end + +--- A function called when the source changes. +-- +-- +-- @DOC_wibox_decl_doc_connection2_EXAMPLE@ +-- +-- The callback arguments are: +-- +-- callback = function(source, target, sig_arg1, ...) +-- /\ /\ /\ /\ +-- | | | | +-- The client -| | | | +-- It will be the widget -| | | +-- Signal first argument, the client -| | +-- All other signal arguments -| +-- +-- @property callback +-- @tparam function callback +-- @propemits true false + +function module:get_callback() + return self._private.callbacks[1] +end + +function module:set_callback(cb) + self._private.callbacks = {cb} + + self:emit_signal("property::callback", cb) + + self._private.env_init = false + + initiate(self) +end + +-- When used in a declarative tree, this will be the +-- object it is initiated from. The `key` can be a number, +-- in which case we do nothing. It can also be a string, +-- in which case it becomes `target_property` +function module:_set_declarative_handler(parent, key, ids) + self.target = parent + + self._private.env_ids = ids + self._private.env_init = false + + if type(key) == "string" then + self.target_property = key + end + + initiate(self) +end + +--- Disconnect this object. +-- +-- @method disconnect +-- @see reconnect + +function module:disconnect() + if self._private.source_class then + for _, sig in ipairs(self._private.signals) do + self._private.source_class.disconnect_signal( + sig, self._private._callback + ) + end + end + + for _, src in ipairs(self._private.sources) do + for _, sig in ipairs(self._private.signals) do + src:disconnect_signal(sig, self._private._callback) + end + end +end + +--- Reconnect this object. +-- +-- @method reconnect +-- @see disconnect + +function module:reconnect() + self:disconnect() + + if self._private.source_class then + for _, sig in ipairs(self._private.signals) do + self._private.source_class.connect_signal( + sig, self._private._callback + ) + end + end + + for _, src in ipairs(self._private.sources) do + for _, sig in ipairs(self._private.signals) do + src:connect_signal(sig, self._private._callback) + end + end +end + +--- Create a new `gears.connection` object. +-- +-- @constructorfct gears.connection +-- @tparam table args +-- @tparam boolean args.initiate If the property should be set when the target object is defined. +-- @tparam boolean args.enabled Turn this connection on or off. +-- @tparam boolean args.signals A list of source object signals. +-- @tparam string args.signal The (source) signal to monitor. +-- @tparam gears.object args.target The object for the right side object of the connection. +-- @tparam string args.target_property The target object property to set when the source property changes. +-- @tparam string args.target_method Rather than use a property, call a method. +-- @tparam class|string args.source_class Use a whole class rather than an object as source. +-- @tparam gears.object args.source The source object (connection left hand side). +-- @tparam string args.source_property The source object(s)/class property. +-- @tparam gears.object args.sources A list of source objects (connection left hand side). +-- @tparam function args.callback A function called when the source changes. + +local function new(_, args) + local self = gobject { + enable_properties = true, + } + + rawset(self, "_private", { + enabled = true, + signals = {}, + initiate = true, + sources = {}, + callbacks = {}, + source_properties = {}, + target = nil, + _callback = function() + if not self._private.enabled then return end + + set_target(self) + end + }) + + gtable.crush(self, module, true ) + gtable.crush(self, args , false) + + return self +end + +--@DOC_object_COMMON@ + +return setmetatable(module, {__call = new}) From 2376535e78c7e08827e9142cf7105c57dcd47291 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:19:05 -0800 Subject: [PATCH 09/12] tests: Test `gears.connection`. --- .../text/gears/connection/add_remove.lua | 39 +++++++++++ .../examples/text/gears/connection/class.lua | 42 ++++++++++++ .../text/gears/connection/enabled.lua | 53 +++++++++++++++ .../text/gears/connection/initiate.lua | 60 ++++++++++++++++ .../examples/text/gears/connection/method.lua | 42 ++++++++++++ .../examples/text/gears/connection/target.lua | 42 ++++++++++++ tests/examples/wibox/decl_doc/connection.lua | 19 ++++++ tests/examples/wibox/decl_doc/connection2.lua | 49 +++++++++++++ tests/examples/wibox/decl_doc/connection3.lua | 37 ++++++++++ tests/examples/wibox/decl_doc/connection4.lua | 68 +++++++++++++++++++ tests/examples/wibox/decl_doc/connection5.lua | 68 +++++++++++++++++++ 11 files changed, 519 insertions(+) create mode 100644 tests/examples/text/gears/connection/add_remove.lua create mode 100644 tests/examples/text/gears/connection/class.lua create mode 100644 tests/examples/text/gears/connection/enabled.lua create mode 100644 tests/examples/text/gears/connection/initiate.lua create mode 100644 tests/examples/text/gears/connection/method.lua create mode 100644 tests/examples/text/gears/connection/target.lua create mode 100644 tests/examples/wibox/decl_doc/connection.lua create mode 100644 tests/examples/wibox/decl_doc/connection2.lua create mode 100644 tests/examples/wibox/decl_doc/connection3.lua create mode 100644 tests/examples/wibox/decl_doc/connection4.lua create mode 100644 tests/examples/wibox/decl_doc/connection5.lua diff --git a/tests/examples/text/gears/connection/add_remove.lua b/tests/examples/text/gears/connection/add_remove.lua new file mode 100644 index 00000000..38b4dd45 --- /dev/null +++ b/tests/examples/text/gears/connection/add_remove.lua @@ -0,0 +1,39 @@ +--DOC_GEN_IMAGE + +local gears = {object = require("gears.object"), connection = require("gears.connection")} --DOC_HIDE + +-- When `source` changes, `target` is updated. +local my_source_object1 = gears.object { --DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +local my_source_object2 = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +local my_target_object1 = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +local conn = gears.connection { + source = my_source_object1, + target = my_target_object1, +} + +--DOC_NEWLINE + +conn:append_source_object(my_source_object1) +--DOC_NEWLINE +assert(conn:has_source_object(my_source_object1)) +--DOC_NEWLINE +assert(not conn:has_source_object(my_source_object2)) --DOC_HIDE +conn:append_source_object(my_source_object2) +assert(conn:has_source_object(my_source_object2)) --DOC_HIDE +local ret = --DOC_HIDE +conn:remove_source_object(my_source_object1) +assert(ret) --DOC_HIDE +assert(not conn:has_source_object(my_source_object1)) --DOC_HIDE +assert(not conn:remove_source_object({})) --DOC_HIDE diff --git a/tests/examples/text/gears/connection/class.lua b/tests/examples/text/gears/connection/class.lua new file mode 100644 index 00000000..f4186cfa --- /dev/null +++ b/tests/examples/text/gears/connection/class.lua @@ -0,0 +1,42 @@ +--DOC_GEN_IMAGE + +local gears = {object = require("gears.object"), connection = require("gears.connection")} --DOC_HIDE + +client.gen_fake {name = "foo"} --DOC_HIDE +client.gen_fake {name = "baz"} --DOC_HIDE + +local called = 0 --DOC_HIDE + +--DOC_NEWLINE + +local conn = gears.connection { + source_class = client, + signals = {"focused", "property::name"}, + initiate = false, + callback = function() + called = called + 1 --DOC_HIDE + -- do something + end +} + +assert(conn) --DOC_HIDE +assert(conn.signals[1] == "focused") --DOC_HIDE +assert(conn.signals[2] == "property::name") --DOC_HIDE +assert(conn.source_class == client) --DOC_HIDE +assert(conn.callback) --DOC_HIDE + +--DOC_NEWLINE + +-- This emit the `focused` signal. +screen[1].clients[1]:activate{} +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(called == 1) --DOC_HIDE + +--DOC_NEWLINE + +-- Changing the name emits `property::name`. +screen[1].clients[1].name = "bar" +client.emit_signal("property::name", screen[1].clients[1]) --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(called == 2) --DOC_HIDE + diff --git a/tests/examples/text/gears/connection/enabled.lua b/tests/examples/text/gears/connection/enabled.lua new file mode 100644 index 00000000..80a1d524 --- /dev/null +++ b/tests/examples/text/gears/connection/enabled.lua @@ -0,0 +1,53 @@ +--DOC_GEN_IMAGE + +local gears = {object = require("gears.object"), connection = require("gears.connection")} --DOC_HIDE + +-- When `source` changes, `target` is updated. +local my_source_object = gears.object { --DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +local my_target_object1 = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +local my_target_object2 = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + + +my_source_object.foo = 42 + +--DOC_NEWLINE + +local conn1 = gears.connection { + source = my_source_object, + source_property = "foo", + target = my_target_object1, + target_property = "bar" +} + +--DOC_NEWLINE + +local conn2 = gears.connection { + source = my_source_object, + source_property = "foo", + target = my_target_object2, + target_property = "bar" +} + +--DOC_NEWLINE + +conn1.enabled = true +conn2.enabled = false + +--DOC_NEWLINE + +-- conn1 should be enabled, but not conn2. +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(my_target_object1.bar == 42) +assert(my_target_object2.bar == nil) + diff --git a/tests/examples/text/gears/connection/initiate.lua b/tests/examples/text/gears/connection/initiate.lua new file mode 100644 index 00000000..cac004e6 --- /dev/null +++ b/tests/examples/text/gears/connection/initiate.lua @@ -0,0 +1,60 @@ +--DOC_GEN_IMAGE + +local gears = {object = require("gears.object"), connection = require("gears.connection")} --DOC_HIDE + +-- When `source` changes, `target` is updated. +local my_source_object = gears.object { --DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +local my_target_object1 = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +local my_target_object2 = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + + +my_source_object.foo = 42 + +--DOC_NEWLINE + +local conn = --DOC_HIDE +gears.connection { + source = my_source_object, + source_property = "foo", + target = my_target_object1, + target_property = "bar" +} + +assert(conn.source_property == "foo") --DOC_HIDE +assert(conn.source == my_source_object) --DOC_HIDE +assert(conn.target == my_target_object1) --DOC_HIDE +assert(conn.target_property == "bar") --DOC_HIDE +assert(conn.initiate) --DOC_HIDE + +--DOC_NEWLINE + +conn = --DOC_HIDE +gears.connection { + source = my_source_object, + source_property = "foo", + initiate = false, + target = my_target_object2, + target_property = "bar" +} + +--DOC_NEWLINE + +assert(not conn.initiate) --DOC_HIDE + +-- my_target_object1 should be initialized, but not my_target_object2. +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(my_target_object1.bar == 42) +assert(my_target_object2.bar == nil) + +conn.sources = {my_source_object, my_target_object1} --DOC_HIDE diff --git a/tests/examples/text/gears/connection/method.lua b/tests/examples/text/gears/connection/method.lua new file mode 100644 index 00000000..104045e7 --- /dev/null +++ b/tests/examples/text/gears/connection/method.lua @@ -0,0 +1,42 @@ +--DOC_GEN_IMAGE + +local gears = {object = require("gears.object"), connection = require("gears.connection")} --DOC_HIDE + +local called = false --DOC_HIDE + +-- When `source` changes, `target` is updated. +local my_source_object = gears.object { --DOC_HIDE + enable_properties = true, --DOC_HIDE + enable_auto_signals = true --DOC_HIDE +}--DOC_HIDE + +local my_target_object = gears.object {--DOC_HIDE + enable_properties = true, --DOC_HIDE + enable_auto_signals = true --DOC_HIDE +} --DOC_HIDE + +-- Declare a method. +function my_target_object:my_method() + called = true --DOC_HIDE + -- do something +end + +my_source_object.foo = 42 --DOC_HIDE + +--DOC_NEWLINE + +local conn = gears.connection { + source = my_source_object, + source_property = "foo", + target = my_target_object, + target_method = "my_method" +} + +assert(conn) --DOC_HIDE +assert(conn.target == my_target_object) --DOC_HIDE +assert(conn.enabled) --DOC_HIDE +assert(conn.target_method == "my_method") --DOC_HIDE + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(called) --DOC_HIDE + diff --git a/tests/examples/text/gears/connection/target.lua b/tests/examples/text/gears/connection/target.lua new file mode 100644 index 00000000..e15d96c7 --- /dev/null +++ b/tests/examples/text/gears/connection/target.lua @@ -0,0 +1,42 @@ +--DOC_GEN_IMAGE + +local gears = {object = require("gears.object"), connection = require("gears.connection")} --DOC_HIDE + +-- When `source` changes, `target` is updated. +local my_source_object = gears.object { --DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +local my_target_object = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +}--DOC_HIDE + +my_source_object.foo = 42 + +--DOC_NEWLINE + +local conn = gears.connection { + source = my_source_object, + source_property = "foo", + target = my_target_object, + target_property = "bar" +} + +assert(conn) --DOC_HIDE + +--DOC_NEWLINE + +-- This works because `initiate` is `true` by default. +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(my_target_object.bar == 42) + +--DOC_NEWLINE + +-- This works because the `source` object `foo` is connected to +-- the `target` object `bar` property. +my_source_object.foo = 1337 +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(my_target_object.bar == 1337) + diff --git a/tests/examples/wibox/decl_doc/connection.lua b/tests/examples/wibox/decl_doc/connection.lua new file mode 100644 index 00000000..1828e242 --- /dev/null +++ b/tests/examples/wibox/decl_doc/connection.lua @@ -0,0 +1,19 @@ +--DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_USAGE --DOC_NO_DASH +local parent = ... --DOC_HIDE +local gears = require("gears") --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + + local w = wibox.widget { + + -- Get the current cliently focused name. + gears.connection { + source_class = client, + signal = "focused", + source_property = "name", + destination_property = "text", + }, + + widget = wibox.widget.textbox + } + +parent:add(w) --DOC_HIDE diff --git a/tests/examples/wibox/decl_doc/connection2.lua b/tests/examples/wibox/decl_doc/connection2.lua new file mode 100644 index 00000000..edaad239 --- /dev/null +++ b/tests/examples/wibox/decl_doc/connection2.lua @@ -0,0 +1,49 @@ +--DOC_HIDE --DOC_NO_USAGE +local gears = require("gears") --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + +-- luacheck: globals my_textbox get_children_by_id --DOC_HIDE + + local w = wibox.widget { + -- Get the current cliently focused name. + text = gears.connection { + source_class = client, + signals = {"focused", "property::name"}, + initiate = false, + callback = function(source, target, sig_arg1, ...) --luacheck: no unused args + -- Since the class signal first arg is the source, this works! + assert(source == sig_arg1) + + --DOC_NEWLINE + + -- All widgets with IDs are visible from this callback! + assert(target and my_textbox) --DOC_HIDE + assert(target == my_textbox) + + --DOC_NEWLINE + + -- get_children_by_id can also be used! + assert(get_children_by_id("my_textbox")[1] == target) + + --DOC_NEWLINE + + if not source then return "Nothing!" end + + --DOC_NEWLINE + + return "Name: " .. source.name .. "!" + end + }, + forced_width = 100, --DOC_HIDE + forced_height = 20, --DOC_HIDE + id = "my_textbox", + widget = wibox.widget.textbox + } + + +--DOC_HIDE 1: delayed connection, 2: delayed layout, 3: delayed draw +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w.text == "Nothing!") --DOC_HIDE + diff --git a/tests/examples/wibox/decl_doc/connection3.lua b/tests/examples/wibox/decl_doc/connection3.lua new file mode 100644 index 00000000..c589a9ba --- /dev/null +++ b/tests/examples/wibox/decl_doc/connection3.lua @@ -0,0 +1,37 @@ +--DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_USAGE --DOC_NO_DASH +local parent= ... --DOC_HIDE +local gears = require("gears") --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + +local obj = nil --DOC_HIDE + +gears.timer = function()--DOC_HIDE + obj = gears.object { --DOC_HIDE + enable_properties = true, --DOC_HIDE + enable_auto_signals = true --DOC_HIDE + }--DOC_HIDE + return obj --DOC_HIDE +end--DOC_HIDE + + local w = wibox.widget { + + gears.connection { + source = gears.timer { + timeout = 5, + autostart = true, + }, + signal = "timeout", + callback = function(_, parent_widget) + parent_widget.text = "this will get called every 5 seconds" + end + }, + + widget = wibox.widget.textbox + } + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(obj) --DOC_HIDE +obj:emit_signal("timeout") --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w.text == "this will get called every 5 seconds") --DOC_HIDE +parent:add(w) --DOC_HIDE diff --git a/tests/examples/wibox/decl_doc/connection4.lua b/tests/examples/wibox/decl_doc/connection4.lua new file mode 100644 index 00000000..49c839b0 --- /dev/null +++ b/tests/examples/wibox/decl_doc/connection4.lua @@ -0,0 +1,68 @@ +--DOC_NO_USAGE --DOC_GEN_IMAGE --DOC_HIDE +local parent = ... --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE +local gears = require("gears") --DOC_HIDE + +local data = { --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE +} --DOC_HIDE + +local my_source_object = gears.object { --DOC_HIDE + enable_properties = true, --DOC_HIDE + enable_auto_signals = true --DOC_HIDE +}--DOC_HIDE + +my_source_object.value = 0 --DOC_HIDE + +-- luacheck: globals my_graph my_label my_progress --DOC_HIDE + +local w = --DOC_HIDE + wibox.widget { + { + { + id = "my_graph", + max_value = 30, + widget = wibox.widget.graph + }, + { + id = "my_label", + align = "center", + valign = "center", + widget = wibox.widget.textbox, + }, + layout = wibox.layout.stack + }, + id = "my_progress", + max_value = 30, + min_value = 0, + forced_height = 30, --DOC_HIDE + forced_width = 200, --DOC_HIDE + widget = wibox.container.radialprogressbar, + + --DOC_NEWLINE + -- Set the value of all 3 widgets. + gears.connection { + source = my_source_object, + source_property = "value", + callback = function(_, _, value) + my_graph:add_value(value) + my_label.text = value .. "mB/s" + my_progress.value = value + end + }, + } + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE + +for _, v in ipairs(data) do --DOC_HIDE + assert(v ~= nil) --DOC_HIDE + my_source_object.value = v --DOC_HIDE +end --DOC_HIDE + +parent:add(w) --DOC_HIDE + +--DOC_HIDE vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/tests/examples/wibox/decl_doc/connection5.lua b/tests/examples/wibox/decl_doc/connection5.lua new file mode 100644 index 00000000..d72c11db --- /dev/null +++ b/tests/examples/wibox/decl_doc/connection5.lua @@ -0,0 +1,68 @@ +--DOC_NO_USAGE --DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_DASH +local parent = ... --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE +local gears = require("gears") --DOC_HIDE + +local data = { --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE + 3, 5, 6,4, 11,15,19,29,17,17,14,0,0,3,1,0,0, 22, 17,7, 1,0,0,5, --DOC_HIDE +} --DOC_HIDE + +local my_source_object = gears.object { --DOC_HIDE + enable_properties = true, --DOC_HIDE + enable_auto_signals = true --DOC_HIDE +}--DOC_HIDE + +my_source_object.value = 0 --DOC_HIDE + +-- luacheck: globals my_graph my_label my_progress --DOC_HIDE + +local w = --DOC_HIDE + wibox.widget { + { + { + id = "my_graph", + max_value = 30, + widget = wibox.widget.graph + }, + { + id = "my_label", + align = "center", + valign = "center", + widget = wibox.widget.textbox, + }, + layout = wibox.layout.stack + }, + id = "my_progress", + max_value = 30, + min_value = 0, + forced_height = 30, --DOC_HIDE + forced_width = 200, --DOC_HIDE + widget = wibox.container.radialprogressbar, + + --DOC_NEWLINE + -- Set the value of all 3 widgets. + gears.connection { + source = my_source_object, + source_property = "value", + callback = function(_, _, value) + my_graph:add_value(value) + my_label.text = value .. "mB/s" + my_progress.value = value + end + }, + } + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE + +for _, v in ipairs(data) do --DOC_HIDE + assert(v ~= nil) --DOC_HIDE + my_source_object.value = v --DOC_HIDE +end --DOC_HIDE + +parent:add(w) --DOC_HIDE + +--DOC_HIDE vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 From 9d55d385c749c9f7218755ccc7796c746ab54109 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:20:41 -0800 Subject: [PATCH 10/12] Add a new `gears.watcher` module. It mostly replaces `awful.widgets.watch` with a more generic API. `awful.widgets.watch` was only useful for textbox (without abusing of its low-level callbacks). This can be used both as a generic polling module for files and commands. It can also be attached directly to widgets or mixed with `gears.reactive` to build an higher level output. --- lib/awful/spawn.lua | 154 +------ lib/awful/util.lua | 20 +- lib/gears/init.lua | 3 + lib/gears/watcher.lua | 733 +++++++++++++++++++++++++++++++ tests/examples/shims/awesome.lua | 2 + 5 files changed, 763 insertions(+), 149 deletions(-) create mode 100644 lib/gears/watcher.lua diff --git a/lib/awful/spawn.lua b/lib/awful/spawn.lua index 2ba1903f..8109faa6 100644 --- a/lib/awful/spawn.lua +++ b/lib/awful/spawn.lua @@ -224,56 +224,15 @@ local capi = client = client, } local lgi = require("lgi") -local Gio = lgi.Gio local GLib = lgi.GLib local util = require("awful.util") local gtable = require("gears.table") local gtimer = require("gears.timer") local aclient = require("awful.client") -local protected_call = require("gears.protected_call") +local watcher = require("gears.watcher") local spawn = {} - -local end_of_file -do - -- API changes, bug fixes and lots of fun. Figure out how a EOF is signalled. - local input - if not pcall(function() - -- No idea when this API changed, but some versions expect a string, - -- others a table with some special(?) entries - input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data("")) - end) then - input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data({})) - end - local line, length = input:read_line() - if not line then - -- Fixed in 2016: NULL on the C side is transformed to nil in Lua - end_of_file = function(arg) - return not arg - end - elseif tostring(line) == "" and #line ~= length then - -- "Historic" behaviour for end-of-file: - -- - NULL is turned into an empty string - -- - The length variable is not initialized - -- It's highly unlikely that the uninitialized variable has value zero. - -- Use this hack to detect EOF. - end_of_file = function(arg1, arg2) - return #arg1 ~= arg2 - end - else - assert(tostring(line) == "", "Cannot determine how to detect EOF") - -- The above uninitialized variable was fixed and thus length is - -- always 0 when line is NULL in C. We cannot tell apart an empty line and - -- EOF in this case. - require("gears.debug").print_warning("Cannot reliably detect EOF on an " - .. "GIOInputStream with this LGI version") - end_of_file = function(arg) - return tostring(arg) == "" - end - end -end - local function hash_command(cmd, rules) rules = rules or {} cmd = type(cmd) == "string" and cmd or table.concat(cmd, ';') @@ -392,38 +351,10 @@ end -- @treturn[1] Integer the PID of the forked process. -- @treturn[2] string Error message. -- @staticfct awful.spawn.with_line_callback -function spawn.with_line_callback(cmd, callbacks) - local stdout_callback, stderr_callback, done_callback, exit_callback = - callbacks.stdout, callbacks.stderr, callbacks.output_done, callbacks.exit - local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil - local pid, _, stdin, stdout, stderr = capi.awesome.spawn(cmd, - false, false, have_stdout, have_stderr, exit_callback) - if type(pid) == "string" then - -- Error - return pid - end - local done_before = false - local function step_done() - if have_stdout and have_stderr and not done_before then - done_before = true - return - end - if done_callback then - done_callback() - end - end - if have_stdout then - spawn.read_lines(Gio.UnixInputStream.new(stdout, true), - stdout_callback, step_done, true) - end - if have_stderr then - spawn.read_lines(Gio.UnixInputStream.new(stderr, true), - stderr_callback, step_done, true) - end - assert(stdin == nil) - return pid -end +-- It is still "officially" in `awful.spawn`, but since `gears.watcher` +-- depends on it, the implementation lives there. +spawn.with_line_callback = watcher._with_line_callback --- Asynchronously spawn a program and capture its output. -- (wraps `spawn.with_line_callback`). @@ -438,43 +369,10 @@ end -- @treturn[2] string Error message. -- @see spawn.with_line_callback -- @staticfct awful.spawn.easy_async -function spawn.easy_async(cmd, callback) - local stdout = '' - local stderr = '' - local exitcode, exitreason - local function parse_stdout(str) - stdout = stdout .. str .. "\n" - end - local function parse_stderr(str) - stderr = stderr .. str .. "\n" - end - local function done_callback() - return callback(stdout, stderr, exitreason, exitcode) - end - local exit_callback_fired = false - local output_done_callback_fired = false - local function exit_callback(reason, code) - exitcode = code - exitreason = reason - exit_callback_fired = true - if output_done_callback_fired then - return done_callback() - end - end - local function output_done_callback() - output_done_callback_fired = true - if exit_callback_fired then - return done_callback() - end - end - return spawn.with_line_callback( - cmd, { - stdout=parse_stdout, - stderr=parse_stderr, - exit=exit_callback, - output_done=output_done_callback - }) -end + +-- It is still "officially" in `awful.spawn`, but since `gears.watcher` +-- depends on it, the implementation lives there. +spawn.easy_async = watcher._easy_async --- Call `spawn.easy_async` with a shell. -- This calls `cmd` with `$SHELL -c` (via `awful.util.shell`). @@ -501,42 +399,8 @@ end -- operation finishes (e.g. due to end of file). -- @tparam[opt=false] boolean close Should the stream be closed after end-of-file? -- @staticfct awful.spawn.read_lines -function spawn.read_lines(input_stream, line_callback, done_callback, close) - local stream = Gio.DataInputStream.new(input_stream) - local function done() - if close then - stream:close() - end - stream:set_buffer_size(0) - if done_callback then - protected_call(done_callback) - end - end - local start_read, finish_read - start_read = function() - stream:read_line_async(GLib.PRIORITY_DEFAULT, nil, finish_read) - end - finish_read = function(obj, res) - local line, length = obj:read_line_finish(res) - if type(length) ~= "number" then - -- Error - print("Error in awful.spawn.read_lines:", tostring(length)) - done() - elseif end_of_file(line, length) then - -- End of file - done() - else - -- Read a line - -- This needs tostring() for older lgi versions which returned - -- "GLib.Bytes" instead of Lua strings (I guess) - protected_call(line_callback, tostring(line)) - -- Read the next line - start_read() - end - end - start_read() -end +spawn.read_lines = watcher._read_lines -- When a command should only be executed once or only have a single instance, -- track the SNID set on them to prevent multiple execution. diff --git a/lib/awful/util.lua b/lib/awful/util.lua index dddcafe7..8b7d5b1c 100644 --- a/lib/awful/util.lua +++ b/lib/awful/util.lua @@ -7,7 +7,6 @@ --------------------------------------------------------------------------- -- Grab environment we need -local os = os local assert = assert local load = loadstring or load -- luacheck: globals loadstring (compatibility with Lua 5.1) local loadfile = loadfile @@ -19,6 +18,7 @@ local gstring = require("gears.string") local grect = require("gears.geometry").rectangle local gcolor = require("gears.color") local gfs = require("gears.filesystem") +local watcher = require("gears.watcher") local capi = { awesome = awesome, @@ -31,8 +31,7 @@ local util = {} util.table = {} --- The default shell used when spawing processes. --- @param string -util.shell = os.getenv("SHELL") or "/bin/sh" +-- @tfield string awful.util.shell --- Execute a system command and road the output. -- This function implementation **has been removed** and no longer @@ -484,6 +483,19 @@ function util.round(x) return gmath.round(x) end -return util +return setmetatable(util, { + __index = function(_, key) + if key == "shell" then + return watcher._shell + end + end, + __newindex = function(_, key, value) + if key == "shell" then + watcher._shell = value + else + rawset(util, key, value) + end + end +}) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/init.lua b/lib/gears/init.lua index b8909de3..1ab9b1ec 100644 --- a/lib/gears/init.lua +++ b/lib/gears/init.lua @@ -23,6 +23,9 @@ return string = require("gears.string"); sort = require("gears.sort"); filesystem = require("gears.filesystem"); + reactive = require("gears.reactive"); + connection = require("gears.connection"); + watcher = require("gears.watcher"); } -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/watcher.lua b/lib/gears/watcher.lua new file mode 100644 index 00000000..83d557c0 --- /dev/null +++ b/lib/gears/watcher.lua @@ -0,0 +1,733 @@ +--- Fetch information at a specific interval. +-- +-- @author Emmanuel Lepage-Vallee <elv1313@gmail.com> +-- @copyright 2020 Emmanuel Lepage-Vallee +-- @classmod gears.watcher + +local capi = {awesome = awesome} +local protected_call = require("gears.protected_call") +local gtimer = require("gears.timer") +local gtable = require("gears.table") +local lgi = require("lgi") +local Gio = lgi.Gio +local GLib = lgi.GLib + +local module = {} + +-- This is awful.util.shell +module._shell = os.getenv("SHELL") or "/bin/sh" + +local end_of_file +do + -- API changes, bug fixes and lots of fun. Figure out how a EOF is signalled. + local input + if not pcall(function() + -- No idea when this API changed, but some versions expect a string, + -- others a table with some special(?) entries + input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data("")) + end) then + input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data({})) + end + local line, length = input:read_line() + if not line then + -- Fixed in 2016: NULL on the C side is transformed to nil in Lua + end_of_file = function(arg) + return not arg + end + elseif tostring(line) == "" and #line ~= length then + -- "Historic" behaviour for end-of-file: + -- - NULL is turned into an empty string + -- - The length variable is not initialized + -- It's highly unlikely that the uninitialized variable has value zero. + -- Use this hack to detect EOF. + end_of_file = function(arg1, arg2) + return #arg1 ~= arg2 + end + else + assert(tostring(line) == "", "Cannot determine how to detect EOF") + -- The above uninitialized variable was fixed and thus length is + -- always 0 when line is NULL in C. We cannot tell apart an empty line and + -- EOF in this case. + require("gears.debug").print_warning("Cannot reliably detect EOF on an " + .. "GIOInputStream with this LGI version") + end_of_file = function(arg) + return tostring(arg) == "" + end + end +end + +module._end_of_file = end_of_file + +function module._read_lines(input_stream, line_callback, done_callback, close) + local stream = Gio.DataInputStream.new(input_stream) + local function done() + if close then + stream:close() + end + stream:set_buffer_size(0) + if done_callback then + protected_call(done_callback) + end + end + local start_read, finish_read + start_read = function() + stream:read_line_async(GLib.PRIORITY_DEFAULT, nil, finish_read) + end + finish_read = function(obj, res) + local line, length = obj:read_line_finish(res) + if type(length) ~= "number" then + -- Error + print("Error in awful.spawn.read_lines:", tostring(length)) + done() + elseif end_of_file(line, length) then + -- End of file + done() + else + -- Read a line + -- This needs tostring() for older lgi versions which returned + -- "GLib.Bytes" instead of Lua strings (I guess) + protected_call(line_callback, tostring(line)) + + -- Read the next line + start_read() + end + end + start_read() +end + +function module._with_line_callback(cmd, callbacks) + local stdout_callback, stderr_callback, done_callback, exit_callback = + callbacks.stdout, callbacks.stderr, callbacks.output_done, callbacks.exit + local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil + local pid, _, stdin, stdout, stderr = capi.awesome.spawn(cmd, + false, false, have_stdout, have_stderr, exit_callback) + if type(pid) == "string" then + -- Error + return pid + end + + local done_before = false + local function step_done() + if have_stdout and have_stderr and not done_before then + done_before = true + return + end + if done_callback then + done_callback() + end + end + if have_stdout then + module._read_lines(Gio.UnixInputStream.new(stdout, true), + stdout_callback, step_done, true) + end + if have_stderr then + module._read_lines(Gio.UnixInputStream.new(stderr, true), + stderr_callback, step_done, true) + end + assert(stdin == nil) + return pid +end + +function module._easy_async(cmd, callback) + local stdout = '' + local stderr = '' + local exitcode, exitreason + local function parse_stdout(str) + stdout = stdout .. str .. "\n" + end + local function parse_stderr(str) + stderr = stderr .. str .. "\n" + end + local function done_callback() + return callback(stdout, stderr, exitreason, exitcode) + end + local exit_callback_fired = false + local output_done_callback_fired = false + local function exit_callback(reason, code) + exitcode = code + exitreason = reason + exit_callback_fired = true + if output_done_callback_fired then + return done_callback() + end + end + local function output_done_callback() + output_done_callback_fired = true + if exit_callback_fired then + return done_callback() + end + end + return module._with_line_callback( + cmd, { + stdout=parse_stdout, + stderr=parse_stderr, + exit=exit_callback, + output_done=output_done_callback + }) +end + +function module._read_async(path, callback, fail_callback) + local cancel = Gio.Cancellable() + + Gio.File.new_for_path(path):load_contents_async(cancel, function(file, task) + local content = file:load_contents_finish(task) + if content then + callback(path, content) + elseif fail_callback then + fail_callback(path) + end + end) + + return cancel +end + +-- Posix files and commands always end with a newline. +-- It is nearly always unwanted, so we strip it by default. +local function remove_posix_extra_newline(content) + -- Remove the trailing `\n` + if content:sub(-1) == '\n' then + content = content:sub(1, -2) + end + + return content +end + +--- Make sure we sort transactions in a way obsolete ones are +-- not used. + +local function add_transaction(self, transaction) + table.insert(self._private.transactions, transaction) +end + +local function remove_transaction(self, transaction) + + -- Too late, abort. + for _, cancel in pairs(transaction.pending) do + cancel:cancel() + end + + for k, t in ipairs(self._private.transactions) do + if t == transaction then + table.remove(self._private.transactions, k) + end + end + +end + +-- Keys can also be labels or object, # wont work. +local function count_files(files) + local ret = 0 + + for _ in pairs(files) do + ret = ret + 1 + end + + return ret +end + +-- When there is multiple files, we need to wait +-- until are of them are read. +local function gen_file_transaction(self) + if count_files(self.files) == 0 then return nil end + + local ret = { + counter = count_files(self.files), + files = gtable.clone(self.files, false), + filter = self.filter, + labels = {}, + content = {}, + failed = {}, + pending = {}, + + } + + local function finished(file, content) + assert(not ret.content[file]) + self:emit_signal("file::acquired", file, content) + ret.pending[file] = nil + + ret.counter = ret.counter - 1 + ret.content[file] = content + + if ret.counter > 0 then return end + + self:emit_signal("files::acquired", ret.content, ret.failed) + + -- Make the final table using the stored keys. + local contents = {} + + for path, ctn in pairs(ret.content) do + if self.strip_newline then + ctn = remove_posix_extra_newline(ctn) + end + + contents[self._private.file_keys[path]] = ctn + + if self.labels_as_properties and type(ret.labels[path]) == "string" then + local val + + if ret.filter then + val = ret.filter(ret.labels[path], ctn) + else + val = ctn + end + + -- Make sure the signals are not fired for nothing. + if val ~= self[ret.labels[path]] then + self[ret.labels[path]] = val + end + end + end + + local ctn = count_files(ret.files) == 1 and contents[next(contents)] or contents + + if ret.filter and not self.labels_as_properties then + self._private.value = ret.filter(ctn, ret.failed) + else + self._private.value = ctn + end + + self:emit_signal("property::value", self._private.value) + + remove_transaction(self, ret) + end + + local function read_error(file) + ret.pending[file] = nil + table.insert(ret.failed, file) + self:emit_signal("file::failed", file) + end + + for label, file in pairs(ret.files) do + ret.labels[file] = label + + local cancel = module._read_async(file, finished, read_error) + + ret.pending[file] = cancel + end + + return ret +end + +local modes_start, modes_abort = {}, {} + +modes_start["none"] = function() --[[nop]] end +modes_abort["none"] = function() --[[nop]] end + +modes_start["files"] = function(self) + if not self._private.init then return end + + local t = gen_file_transaction(self) + + add_transaction(self, t) +end + +modes_abort["files"] = function(self) + for _, t in ipairs(self._private.transactions) do + remove_transaction(self, t) + end +end + +modes_start["command"] = function(self) + if not self._private.init then return end + + local com = self._private.command + + if self._private.shell then + assert( + type(com) == "string", + "When using `gears.watcher` with `shell = true`, ".. + "the command must be a string" + ) + + com = {module._shell, '-c', com} + end + + module._easy_async(com, function(lines, stderr, err, errcode) + if self.strip_newline then + lines = remove_posix_extra_newline(lines) + end + + if self.filter then + self._private.value = self.filter(lines, stderr, err, errcode) + else + self._private.value = lines + end + end) +end + +modes_abort["command"] = function() + --TODO +end + +function module:_set_declarative_handler(parent, key, ids) + assert(type(key) == "string", "A watcher can only be attached to properties") + + table.insert(self._private.targets, { + ids = ids, + parent = parent, + property = key + }) + + if self._private.value then + parent[key] = self._private.value + end +end + +--- Abort the current transactions. +-- +-- If files are currently being read or commands executed, +-- abort it. This does prevent new transaction from being +-- started. Use `:stop()` for that. +-- +-- @method abort +-- @see stop + +function module:abort() + modes_abort[self._private.mode](self) +end + +--- Emitted when a file is read. +-- +-- @signal file::acquired +-- @tparam string path The path. +-- @tparam string content The file content. +-- @see files::acquired +-- @see file::failed + +--- When reading a file failed. +-- @signal file::failed +-- @tparam string path The path. + +--- Emitted when all files are read. +-- +-- @signal files::acquired +-- @tparam table contents Path as keys and content as values. +-- @tparam table failed The list of files which could not be read. + +--- A file path. +-- +-- @DOC_text_gears_watcher_simple_EXAMPLE@ +-- +-- @property file +-- @tparam string file +-- @propemits true false +-- @see files + +--- A list or map of files. +-- +-- It is often necessary to query multiple files. When reading from `proc`, +-- some data is split across multiple files, such as the battery charge. +-- +-- This property accepts 2 format. One uses is a plain table of paths while +-- the other is a label->path map. Depending on what is being read, both make +-- sense. +-- +-- **Simple path list:** +-- +-- @DOC_text_gears_watcher_files1_EXAMPLE@ +-- +-- **With labels:** +-- +-- @DOC_text_gears_watcher_files2_EXAMPLE@ +-- +-- @property files +-- @tparam table files +-- @propemits true false +-- @see labels_as_properties + +function module:get_file() + return self._private.files[1] +end + +function module:set_file(path) + self:set_files({path}) +end + +function module:get_files() + return self._private.files +end + +function module:set_files(paths) + self:abort() + + self._private.files = paths or {} + self._private.mode = "files" + + self:emit_signal("property::files", self._private.files ) + self:emit_signal("property::file" , select(2, next(self._private.files))) + + -- It is possible to give names to each files. For modules like + -- battery widgets, which require reading multiple long paths from + -- `proc`, it makes the user code more readable. + self._private.file_keys = {} + + for k, v in pairs(self._private.files) do + self._private.file_keys[v] = type(k) == "number" and v or k + end + + modes_start["files"](self) +end + +--- Add a file to the files list. +-- +-- @method append_file +-- @tparam string path The path. +-- @tparam[opt] string The key. + +function module:append_file(path, key) + self:abort() + + if self._private.files[path] then return end + + key = key or (#self._private.files + 1) + + self._private.mode = "files" + + self._private.files[key] = path + self._private.file_keys[path] = key + + self:emit_signal("property::files", self._private.files ) + self:emit_signal("property::file" , select(2, next(self._private.files))) + + modes_start["files"](self) +end + +--- Remove a file to the files list. +-- +-- @method remove_file +-- @tparam string path The path or the key. + +--- A filter to post-process the file or command. +-- +-- It can be used, for example, to convert the string to a number or turn +-- the various file content into the final value. The filter argument +-- depend on various `gears.watcher` properties (as documented below). +-- +-- **The callback parameters for a single file:** +-- (1) The file content (string) +-- +-- **The callback parameters for a multiple files (paths):** +-- (1) Tables with the paths as keys and string content as value. +-- +-- **The callback parameters for a multiple files (labels):** +-- (1) Tables with the keys as keys and string content as value. +-- +-- **The callback when `labels_as_properties` is true: +-- (1) The label name +-- (2) The content +-- +-- **The callback parameters for a command:** +-- (1) Stdout as first parameter +-- (2) Stderr as second parameter +-- (3) Exitreason +-- (4) Exitcode +-- +-- @property filter +-- @tparam function filter +-- @propemits true false + +function module:get_filter() + return self._private.filter +end + +function module:set_filter(filter) + self:abort() + + self._private.filter = filter + + self:emit_signal("property::filter", filter) + + modes_start[self._private.mode](self) +end + +--- A command. +-- +-- If you plan to use pipes or any shell features, do not +-- forget to also set `shell` to `true`. +-- +-- @DOC_text_gears_watcher_command1_EXAMPLE@ +-- +-- @property command +-- @tparam table|string command +-- @propemits true false + +function module:get_command() + return self._private.command +end + +function module:set_command(command) + self._private.command = command + self._private.mode = "command" + self:emit_signal("property::command") + modes_start["command"](self) +end + +--- Use a shell when calling the command. +-- +-- This means you can use `|`, `&&`, `$?` in your command. +-- +-- @DOC_text_gears_watcher_command2_EXAMPLE@ +--- +-- @property shell +-- @tparam[opt=false] boolean|string shell +-- @propemits true false + +function module:get_shell() + return self._private.shell or false +end + +function module:set_shell(shell) + self._private.shell = shell + self:emit_signal("property::shell") +end + +--- In files mode, when paths have labels, apply them as properties. +-- +-- @DOC_wibox_widget_declarative_watcher_EXAMPLE@ +-- +-- @property labels_as_properties +-- @tparam[opt=false] boolean labels_as_properties +-- @see files + +--- The interval between the content refresh. +-- +-- (in seconds) +-- +-- @property interval +-- @tparam number interval +-- @see gears.timer.timeout +-- @propemits true false + +-- There is not get_timeout/set_timeout, so we can't make aliases. +module.get_interval = gtimer.get_timeout +module.set_interval = gtimer.set_timeout + +--- The current value of the watcher. +-- +-- If there is no filter, this will be a string. If a filter is used, +-- then it is whatever it returns. +-- +-- @property value +-- @propemits false false + +function module:get_value() + return self._private.value +end + +--- Strip the extra trailing newline. +-- +-- All posix compliant text file and commands end with a newline. +-- Most of the time, this is inconvinient, so `gears.watcher` removes +-- them by default. Set this to `false` if this isn't the desired +-- behavior. +-- +-- @property strip_newline +-- @tparam[opt=true] boolean strip_newline +-- @propemits true false + +function module:get_strip_newline() + return self._private.newline +end + +function module:set_strip_newline(value) + self._private.newline = value + self:emit_signal("property::strip_newline", value) +end + +--- Start the timer. +-- @method start +-- @emits start +-- @baseclass gears.timer +-- @see stop + +--- Stop the timer. +-- @method stop +-- @emits stop +-- @baseclass gears.timer +-- @see abort +-- @see start + +--- The timer is started. +-- @property started +-- @tparam boolean started +-- @propemits false false +-- @baseclass gears.timer + +--- Restart the timer. +-- This is equivalent to stopping the timer if it is running and then starting +-- it. +-- @method again +-- @baseclass gears.timer +-- @emits start +-- @emits stop + +--- Create a new `gears.watcher` object. +-- +-- @constructorfct gears.watcher +-- @tparam table args +-- @tparam string args.file A file path. +-- @tparam table args.files A list or map of files. +-- @tparam function args.filter A filter to post-process the file or command. +-- @tparam table|string args.command A command (without a shell). +-- @tparam[opt=false] boolean args.shell Use a shell when calling the command. +-- @param args.initial_value The value to use before the first "real" value is acquired. +-- @tparam number args.interval The interval between the content refresh. +-- @tparam boolean args.labels_as_properties Set the file labels as properties. +-- @tparam boolean args.started The timer is started. + +local function new(_, args) + local ret = gtimer() + ret.timeout = 5000 + + local newargs = gtable.clone(args or {}, false) + + ret._private.mode = "none" + ret._private.transactions = {} + ret._private.targets = {} + ret._private.files = {} + + if newargs.autostart == nil then + newargs.autostart = true + end + + if newargs.strip_newline == nil then + newargs.strip_newline = true + end + + gtable.crush(ret, module , true ) + gtable.crush(ret, newargs, false) + + ret.shell = args.shell + -- ret:set_shell(args.shell) + + local function value_callback() + for _, target in ipairs(ret._private.targets) do + target.parent[target.property] = ret.value + end + end + + local function update_callback() + modes_start[ret._private.mode](ret) + end + + ret:connect_signal("property::value", value_callback) + ret:connect_signal("timeout", update_callback) + + if args.initial_value then + ret._private.value = args.initial_value + end + + ret._private.init = true + + if newargs.autostart then + ret:start() + modes_start[ret._private.mode](ret) + end + + return ret +end + +--@DOC_object_COMMON@ + +return setmetatable(module, {__call = new}) diff --git a/tests/examples/shims/awesome.lua b/tests/examples/shims/awesome.lua index 64b0da43..b33d7230 100644 --- a/tests/examples/shims/awesome.lua +++ b/tests/examples/shims/awesome.lua @@ -82,6 +82,8 @@ function awesome.xrdb_get_value() return nil end +function awesome.spawn() end + -- Always show deprecated messages awesome.version = "v9999" awesome.api_level = 9999 From b28a84fbc312de267a57d98999205e4f584f62c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:24:08 -0800 Subject: [PATCH 11/12] tests: Test `gears.watcher`. --- .../examples/text/gears/watcher/command1.lua | 18 ++ .../examples/text/gears/watcher/command2.lua | 19 ++ tests/examples/text/gears/watcher/files1.lua | 26 +++ tests/examples/text/gears/watcher/files2.lua | 34 ++++ tests/examples/text/gears/watcher/shell.lua | 14 ++ tests/examples/text/gears/watcher/simple.lua | 9 + .../wibox/widget/declarative/watcher.lua | 51 +++++ .../wibox/widget/progressbar/watcher.lua | 41 ++++ tests/test-gears-watcher.lua | 181 ++++++++++++++++++ 9 files changed, 393 insertions(+) create mode 100644 tests/examples/text/gears/watcher/command1.lua create mode 100644 tests/examples/text/gears/watcher/command2.lua create mode 100644 tests/examples/text/gears/watcher/files1.lua create mode 100644 tests/examples/text/gears/watcher/files2.lua create mode 100644 tests/examples/text/gears/watcher/shell.lua create mode 100644 tests/examples/text/gears/watcher/simple.lua create mode 100644 tests/examples/wibox/widget/declarative/watcher.lua create mode 100644 tests/examples/wibox/widget/progressbar/watcher.lua create mode 100644 tests/test-gears-watcher.lua diff --git a/tests/examples/text/gears/watcher/command1.lua b/tests/examples/text/gears/watcher/command1.lua new file mode 100644 index 00000000..10539457 --- /dev/null +++ b/tests/examples/text/gears/watcher/command1.lua @@ -0,0 +1,18 @@ +--DOC_NO_USAGE --DOC_GEN_OUTPUT +local gears = { watcher = require("gears.watcher") } --DOC_HIDE + +function gears.watcher._easy_async(_, cb) --DOC_HIDE + cb("one two three", "", nil, 0) --DOC_HIDE +end --DOC_HIDE + + local w = gears.watcher { + interval = 0.05, + command = "echo one two three", + } + + --DOC_NEWLINE + -- [wait some time] + + --DOC_NEWLINE + -- It will be "one two three" + print(w.value) diff --git a/tests/examples/text/gears/watcher/command2.lua b/tests/examples/text/gears/watcher/command2.lua new file mode 100644 index 00000000..6e979443 --- /dev/null +++ b/tests/examples/text/gears/watcher/command2.lua @@ -0,0 +1,19 @@ +--DOC_NO_USAGE --DOC_GEN_OUTPUT +local gears = { watcher = require("gears.watcher") } --DOC_HIDE + +function gears.watcher._easy_async(_, cb) --DOC_HIDE + cb("three", "", nil, 0) --DOC_HIDE +end --DOC_HIDE + + local w = gears.watcher { + interval = 0.05, + command = "echo one two three | cut -f3 -d ' '", + shell = true, + } + + --DOC_NEWLINE + -- [wait some time] + + --DOC_NEWLINE + -- It will be "one two three" + print(w.value) diff --git a/tests/examples/text/gears/watcher/files1.lua b/tests/examples/text/gears/watcher/files1.lua new file mode 100644 index 00000000..62e89588 --- /dev/null +++ b/tests/examples/text/gears/watcher/files1.lua @@ -0,0 +1,26 @@ +--DOC_GEN_IMAGE --DOC_NO_USAGE +local gears = { watcher = require("gears.watcher") } --DOC_HIDE + +function gears.watcher._read_async(path, callback, _) --DOC_HIDE + if path == '/sys/class/power_supply/AC/online' then --DOC_HIDE + callback('/sys/class/power_supply/AC/online', "1") --DOC_HIDE + elseif path == "/sys/class/power_supply/BAT0/status" then --DOC_HIDE + callback('/sys/class/power_supply/BAT0/status', "Full") --DOC_HIDE + end --DOC_HIDE +end --DOC_HIDE + + local mybatterycharging = gears.watcher { + files = { + "/sys/class/power_supply/AC/online", + "/sys/class/power_supply/BAT0/status" + }, + filter = function(content) + assert(content['/sys/class/power_supply/AC/online'] == "1") --DOC_HIDE + assert(content['/sys/class/power_supply/BAT0/status'] == "Full") --DOC_HIDE + return content['/sys/class/power_supply/AC/online' ] == "1" + or content['/sys/class/power_supply/BAT0/status'] == "Full" + end, + interval = 5, + } + + assert(mybatterycharging) --DOC_HIDE diff --git a/tests/examples/text/gears/watcher/files2.lua b/tests/examples/text/gears/watcher/files2.lua new file mode 100644 index 00000000..5bcf949d --- /dev/null +++ b/tests/examples/text/gears/watcher/files2.lua @@ -0,0 +1,34 @@ +--DOC_GEN_IMAGE --DOC_NO_USAGE +local gears = { watcher = require("gears.watcher") } --DOC_HIDE + +local called = 0 --DOC_HIDE + +function gears.watcher._read_async(path, callback) --DOC_HIDE + if path == '/sys/class/power_supply/AC/online' then --DOC_HIDE + called = called + 1 --DOC_HIDE + callback('/sys/class/power_supply/AC/online', "1") --DOC_HIDE + elseif path == "/sys/class/power_supply/BAT0/status" then --DOC_HIDE + called = called + 1 --DOC_HIDE + callback('/sys/class/power_supply/BAT0/status', "Full") --DOC_HIDE + end --DOC_HIDE +end --DOC_HIDE + + local mybatterycharging = gears.watcher { + files = { + plugged = "/sys/class/power_supply/AC/online", + charged = "/sys/class/power_supply/BAT0/status" + }, + filter = function(content) + assert(content.plugged == "1") --DOC_HIDE + assert(content.charged == "Full") --DOC_HIDE + return content.plugged == "1" or content.charged == "Full" + end, + interval = 5, + } + + require("gears.timer").run_delayed_calls_now() --DOC_HIDE + require("gears.timer").run_delayed_calls_now() --DOC_HIDE + require("gears.timer").run_delayed_calls_now() --DOC_HIDE + + assert(called >= 2) --DOC_HIDE + assert(mybatterycharging) --DOC_HIDE diff --git a/tests/examples/text/gears/watcher/shell.lua b/tests/examples/text/gears/watcher/shell.lua new file mode 100644 index 00000000..bca094e9 --- /dev/null +++ b/tests/examples/text/gears/watcher/shell.lua @@ -0,0 +1,14 @@ +--DOC_GEN_IMAGE +local gears = { watcher = require("gears.watcher") } --DOC_HIDE + +function gears.watcher._easy_async(_, callback) --DOC_HIDE + callback("three") --DOC_HIDE +end --DOC_HIDE + +local watcher = gears.watcher { + interval = 5, + command = "echo one:two:three | cut -f3 -d :", + shell = true, +} + +assert(watcher) --DOC_HIDE diff --git a/tests/examples/text/gears/watcher/simple.lua b/tests/examples/text/gears/watcher/simple.lua new file mode 100644 index 00000000..74145e7a --- /dev/null +++ b/tests/examples/text/gears/watcher/simple.lua @@ -0,0 +1,9 @@ +--DOC_GEN_IMAGE +local gears = { watcher = require("gears.watcher") } --DOC_HIDE + + local mybatterydischarging = gears.watcher { + file = "/sys/class/power_supply/BAT0/status", + interval = 5, + } + + assert(mybatterydischarging) --DOC_HIDE diff --git a/tests/examples/wibox/widget/declarative/watcher.lua b/tests/examples/wibox/widget/declarative/watcher.lua new file mode 100644 index 00000000..87cce23d --- /dev/null +++ b/tests/examples/wibox/widget/declarative/watcher.lua @@ -0,0 +1,51 @@ +--DOC_GEN_IMAGE --DOC_NO_USAGE +local parent = ... --DOC_HIDE +local gears = { --DOC_HIDE + watcher = require("gears.watcher"), --DOC_HIDE + reactive = require("gears.reactive"), --DOC_HIDE +} --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + +local called = {} --DOC_HIDE + +function gears.watcher._read_async(path, callback, _) --DOC_HIDE + if path == '/sys/class/power_supply/BAT0/energy_full' then --DOC_HIDE + callback('/sys/class/power_supply/BAT0/energy_full', "89470000") --DOC_HIDE + elseif path == '/sys/class/power_supply/BAT0/energy_now' then --DOC_HIDE + callback('/sys/class/power_supply/BAT0/energy_now', "85090000") --DOC_HIDE + end --DOC_HIDE +end --DOC_HIDE + + + local battery = gears.watcher { + labels_as_properties = true, + interval = 5, + files = { + capacity = "/sys/class/power_supply/BAT0/energy_full", + current = "/sys/class/power_supply/BAT0/energy_now" + }, + filter = function(label, content) + called[label] = true --DOC_HIDE + return tonumber(content) + end, + } + + --DOC_NEWLINE + + -- Use the above in a widget. + local w = wibox.widget { + text = gears.reactive(function() + return (battery.current / battery.capacity) .. "%" + end), + widget = wibox.widget.textbox + } + + require("gears.timer").run_delayed_calls_now() --DOC_HIDE + require("gears.timer").run_delayed_calls_now() --DOC_HIDE + require("gears.timer").run_delayed_calls_now() --DOC_HIDE + + assert(called["capacity"]) --DOC_HIDE + assert(called["current"]) --DOC_HIDE + assert(battery) --DOC_HIDE + +parent:add(w) --DOC_HIDE diff --git a/tests/examples/wibox/widget/progressbar/watcher.lua b/tests/examples/wibox/widget/progressbar/watcher.lua new file mode 100644 index 00000000..d848b0ad --- /dev/null +++ b/tests/examples/wibox/widget/progressbar/watcher.lua @@ -0,0 +1,41 @@ +--DOC_GEN_IMAGE --DOC_NO_USAGE --DOC_NO_DASH +local parent = ... --DOC_NO_USAGE --DOC_HIDE +local gears = { watcher = require("gears.watcher") } --DOC_HIDE +local wibox = { widget = require("wibox.widget") }--DOC_HIDE + +function gears.watcher._read_async(path, callback, _) --DOC_HIDE + if path == '/sys/class/power_supply/BAT0/energy_full' then --DOC_HIDE + callback('/sys/class/power_supply/BAT0/energy_full', "89470000") --DOC_HIDE + elseif path == '/sys/class/power_supply/BAT0/energy_now' then --DOC_HIDE + callback('/sys/class/power_supply/BAT0/energy_now', "85090000") --DOC_HIDE + end --DOC_HIDE +end --DOC_HIDE + + -- In this example, the value of energy_full is 89470000 and the value + -- of energy_now is 85090000. The result will be 95%. + local w = --DOC_HIDE + wibox.widget { + value = gears.watcher { + files = { + capacity = "/sys/class/power_supply/BAT0/energy_full", + current = "/sys/class/power_supply/BAT0/energy_now" + }, + filter = function(content) + return tonumber(content.current) / tonumber(content.capacity) + end, + interval = 5, + }, + forced_width = 100, --DOC_HIDE + forced_height = 30, --DOC_HIDE + max_value = 1, + min_value = 0, + widget = wibox.widget.progressbar + } + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w and w.value >= 0.94 and w.value <= 0.96) --DOC_HIDE + +parent:add(w) --DOC_HIDE + diff --git a/tests/test-gears-watcher.lua b/tests/test-gears-watcher.lua new file mode 100644 index 00000000..7933ae7d --- /dev/null +++ b/tests/test-gears-watcher.lua @@ -0,0 +1,181 @@ +--- Test for gears.watcher. + +local runner = require("_runner") +local watcher = require("gears.watcher") +local gtimer = require("gears.timer") +local gdebug = require("gears.debug") +local filesystem = require("gears.filesystem") +local util = require("awful.util") + +local obj1 = nil + +local sig_count = {} + +local steps = { + function() + obj1 = watcher { + interval = 0.05, + command = "echo one two three | cut -f3 -d ' '", + shell = true, + initial_value = 1337 + } + + assert(obj1.command == "echo one two three | cut -f3 -d ' '") + assert(obj1.shell) + assert(obj1.interval == 0.05) + assert(obj1.value == 1337) + + return true + end, + -- Test the filters. + function() + if obj1.value ~= "three" then return end + + obj1.filter = function(lines) + if lines == "three" then return "four" end + end + + return true + end, + -- Switch to file mode. + function() + if obj1.value ~= "four" then return end + + obj1.filter = function(lines) + return lines:match("theme") ~= nil + end + + obj1:connect_signal("file::acquired", function() + sig_count["file::acquired"] = sig_count["file::acquired"] or 0 + sig_count["file::acquired"] = sig_count["file::acquired"] + 1 + end) + + obj1:connect_signal("files::acquired", function() + sig_count["files::acquired"] = sig_count["files::acquired"] or 0 + sig_count["files::acquired"] = sig_count["files::acquired"] + 1 + end) + + obj1:connect_signal("file::failed", function() + sig_count["file::failed"] = sig_count["file::failed"] or 0 + sig_count["file::failed"] = sig_count["file::failed"] + 1 + end) + + obj1.file = filesystem.get_themes_dir().."/default/theme.lua" + assert(obj1.file == filesystem.get_themes_dir().."/default/theme.lua") + + os.execute("echo 12345 > /tmp/test_gears_watcher1") + os.execute("echo 67890 > /tmp/test_gears_watcher2") + os.execute("rm -rf /tmp/test_gears_watcher3") + + return true + end, + -- Remove the filter. + function() + if obj1.value ~= true then return end + + assert(sig_count["file::acquired"] > 0) + assert(sig_count["files::acquired"] > 0) + assert(not sig_count["file::failed"]) + + obj1.file = '/tmp/test_gears_watcher1' + obj1.filter = nil + + return true + end, + -- Test strip_newline. + function() + if obj1.value ~= "12345" then return end + + obj1.strip_newline = false + + return true + end, + -- Test append_file. + function() + if obj1.value ~= "12345\n" then return end + + obj1.strip_newline = true + obj1:append_file('/tmp/test_gears_watcher2', 'foo') + + return true + end, + -- Test remove_file. + function() + if type(obj1.value) ~= "table" or obj1.value["foo"] ~= "67890" then return end + + return true + end, + -- Test non-existant files. + function() + obj1.file = '/tmp/test_gears_watcher3' + + return true + end, + -- Test `started` and `:stop()` + function() + if not sig_count["file::failed"] then return end + + assert(obj1.started) + obj1.started = false + assert(not obj1.started) + + local real_error = gdebug.print_error + + local called = false + + gdebug.print_error = function() + called = true + end + + obj1:stop() + assert(not obj1.started) + assert(called) + called = false + + obj1:start() + assert(obj1.started) + assert(not called) + obj1:stop() + assert(not obj1.started) + assert(not called) + + gdebug.print_error = real_error + + return true + end, + -- Test `awful.util.shell` and `gears.timer` compatibility mode. + function() + assert(util.shell == watcher._shell) + watcher._shell = "foo" + assert(util.shell == "foo") + util.shell = "bar" + assert(watcher._shell == "bar") + + local called = false + + local real_deprecate = gdebug.deprecate + gdebug.deprecate = function() + called = true + end + + local t = gtimer { + timeout = 5 + } + + assert(t.data.timeout) + assert(called) + called = false + t.data.foo = "bar" + assert(t._private.foo == "bar") + assert(called) + t._private.foo = "baz" + assert(t.data.foo == "baz") + + gdebug.deprecate = real_deprecate + + return true + end, +} +runner.run_steps(steps) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 From 830e64bc8da8aabc2f7ff0950499a8ce927bcae4 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 29 Nov 2020 02:27:59 -0800 Subject: [PATCH 12/12] doc: Extend the widget documentation with the new modules. Adds: * gears.watcher * gears.reactive * gears.connection --- docs/03-declarative-layout.md | 140 +- docs/images/gears_reactive.svg | 2106 +++++++++++++++++ .../imagebox.lua | 0 3 files changed, 2202 insertions(+), 44 deletions(-) create mode 100644 docs/images/gears_reactive.svg rename tests/examples/wibox/widget/{declarative-pattern => declarative}/imagebox.lua (100%) diff --git a/docs/03-declarative-layout.md b/docs/03-declarative-layout.md index b989340d..7d3f3d75 100644 --- a/docs/03-declarative-layout.md +++ b/docs/03-declarative-layout.md @@ -315,50 +315,6 @@ Code: layout = wibox.layout.fixed.horizontal, } - - -### Accessing widgets - -For each widget or container, it is possible to add an `identifier` attribute -so that it can be accessed later. - -Widgets defined using `setup` can be accessed using these methods: - -* Avoiding the issue by using externally created widgets -* Using `my_wibox.my_first_widget.my_second_widget` style access -* Using JavaScript like `my_wibox:get_children_by_id("my_second_widget")[1]` - -The first method mixes the imperative and declarative syntax, and makes the code -less readable. The second is a little verbose and only works if every node in -the chain has a valid identifier. The last one doesn't require long paths, -but it is not easy to get a specific instance if multiple widgets have the -same identifier. - -WARNING: The widget identifier must not use a reserved name. This includes all -method names, existing widget attributes, `layout` and `widget`. Names should -also respect the Lua variable conventions (case-sensitive, alphanumeric, -underscore characters and non-numeric first character). - -Code: - - s.mywibox : setup { - { - id = "second", - widget = wibox.widget.textbox - }, - { - id = "third", - widget = wibox.widget.textbox - }, - id = "first", - layout = wibox.layout.fixed.horizontal, - } - - s.mywibox.first.second:set_markup("changed!") - s.mywibox:get_children_by_id("third")[1]:set_markup("Also changed!") - - - ### Extending the system This system is very flexible. Each section attribute (the entries with string @@ -460,3 +416,99 @@ necessary for three reasons: at a later time (by the parent widget). * The code is highly redundant and some of the logic is delegated to the parent widget to simplify everything. + +## Accessing and updating widgets + +There is 3 main ways to update the widgets. Each is best suited for it's own niche. +Choose the one that better suites the style of your code. + +### Imperative + +For each widget or container, it is possible to add an `identifier` attribute +so that it can be accessed later. + +Widgets defined using `setup` can be accessed using these methods: + +* Avoiding the issue by using externally created widgets +* Using `my_wibox.my_first_widget.my_second_widget` style access +* Using JavaScript like `my_wibox:get_children_by_id("my_second_widget")[1]` + +The first method mixes the imperative and declarative syntax, and makes the code +less readable. The second is a little verbose and only works if every node in +the chain has a valid identifier. The last one doesn't require long paths, +but it is not easy to get a specific instance if multiple widgets have the +same identifier. + +WARNING: The widget identifier must not use a reserved name. This includes all +method names, existing widget attributes, `layout` and `widget`. Names should +also respect the Lua variable conventions (case-sensitive, alphanumeric, +underscore characters and non-numeric first character). + +Code: + + s.mywibox : setup { + { + id = "second", + widget = wibox.widget.textbox + }, + { + id = "third", + widget = wibox.widget.textbox + }, + id = "first", + layout = wibox.layout.fixed.horizontal, + } + + s.mywibox.first.second:set_markup("changed!") + s.mywibox:get_children_by_id("third")[1]:set_markup("Also changed!") + +### Reactive + +![Reactive update](../images/gears_reactive.svg) + +Think of reactive programming like Excel/Spreadsheets. You define rules or even +business logic that will automatically be re-evaluated every time the data it +uses changes. In the background, this rewrites the function and automatically +creates all the signal connections. Lua is not a reactive language, so it has it's +limits and you should read the rules defined in the `gears.reactive` documentation +carefully. However, when it works, it is by far the simplest way to update a +widget defined using the declarative syntax. + +#### Reactive expressions + +A reactive expression is just a function wrapped by a `gears.reactive` object. This +will introspect the content and write all the boilerplate. Note that this *only* works +in declarative trees. It *cannot* be mixed with the imperative API. + +@DOC_wibox_decl_doc_reactive_expr1_EXAMPLE@ + +Unlike QML, AwesomeWM also support extracting reactive blocks out of the tree: + +@DOC_wibox_decl_doc_reactive_expr2_EXAMPLE@ + +#### Watcher objects + +`gears.watcher` objects poll files or command at an interval. They do so using +background threads, so it wont affect performance much. They can be attached +directly to a widget or used with a `gears.connection` as demonstrated below. +Using `gears.connection` is preferred when the value is needed by multiple +widgets (see the CPU graph example later in this document). + +@DOC_wibox_widget_progressbar_watcher_EXAMPLE@ + +### Declarative + +The other way to interact with widgets is by creating `gears.connection` objects. +They can be added in your declarative widget tree like this: + +@DOC_wibox_decl_doc_connection_EXAMPLE@ + +It is also possible to use them to bind non-widget objects with widgets: + +@DOC_wibox_decl_doc_connection3_EXAMPLE@ + +One useful feature is that when the `gears.connection` has a callback, it has +direct access to all `id`s as variables or using `get_children_by_id`: + +@DOC_wibox_decl_doc_connection5_EXAMPLE@ + diff --git a/docs/images/gears_reactive.svg b/docs/images/gears_reactive.svg new file mode 100644 index 00000000..6f74c86f --- /dev/null +++ b/docs/images/gears_reactive.svg @@ -0,0 +1,2106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + Magic + + Auto-updated widget(s) + + + + + + + + Hub object(s) + + ??? + ??? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { } + + + + + Code + { } + + + + + Code + { } + + + + + Code + { } + + + + + Code + + + + + + + + + + diff --git a/tests/examples/wibox/widget/declarative-pattern/imagebox.lua b/tests/examples/wibox/widget/declarative/imagebox.lua similarity index 100% rename from tests/examples/wibox/widget/declarative-pattern/imagebox.lua rename to tests/examples/wibox/widget/declarative/imagebox.lua