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/docs/03-declarative-layout.md b/docs/03-declarative-layout.md
index 12143b54..05dc67da 100644
--- a/docs/03-declarative-layout.md
+++ b/docs/03-declarative-layout.md
@@ -373,50 +373,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
@@ -518,3 +474,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 @@
+
+
+
+
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 1d817406..8c16e344 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/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})
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/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})
diff --git a/lib/gears/timer.lua b/lib/gears/timer.lua
index ee1778d7..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")
@@ -87,11 +88,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 +103,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 +119,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()
@@ -134,24 +135,28 @@ end
-- @param number
-- @propemits true false
-local timer_instance_mt = {
- __index = function(self, property)
- if property == "timeout" then
- return self.data.timeout
- elseif property == "started" then
- return self.data.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.data.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,10 +170,33 @@ 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,
+ }
- ret.data = { timeout = 0 } --TODO v5 rename to ._private
- setmetatable(ret, timer_instance_mt)
+ gtable.crush(ret, timer, true)
+
+ 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
+ }))
for k, v in pairs(args) do
ret[k] = v
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/lib/wibox/widget/base.lua b/lib/wibox/widget/base.lua
index 904f5adc..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.
@@ -510,8 +526,19 @@ 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)
+ 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.
@@ -533,6 +560,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,10 +590,18 @@ local function drill(ids, content)
end
end
end
+
-- Replace all children (if any) with the new ones.
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
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/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/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/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.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/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
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/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
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-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
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
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