Merge remote-tracking branch 'elv/reactive'

This commit is contained in:
Aire-One 2021-03-28 20:09:07 +02:00
commit 32827126cc
39 changed files with 5611 additions and 222 deletions

View File

@ -63,6 +63,7 @@ local allowed_deps = {
}, },
-- TODO: Get rid of these -- TODO: Get rid of these
["gears.surface"] = { ["wibox.hierarchy"] = true }, ["gears.surface"] = { ["wibox.hierarchy"] = true },
["gears.connection"] = { ["*"] = true },
} }
-- Turn "foo.bar.baz" into "foo.bar". Returns nil if there is nothing more to -- Turn "foo.bar.baz" into "foo.bar". Returns nil if there is nothing more to

View File

@ -373,50 +373,6 @@ Code:
layout = wibox.layout.fixed.horizontal, 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 ### Extending the system
This system is very flexible. Each section attribute (the entries with string 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). at a later time (by the parent widget).
* The code is highly redundant and some of the logic is delegated to the parent * The code is highly redundant and some of the logic is delegated to the parent
widget to simplify everything. 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@

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -224,56 +224,15 @@ local capi =
client = client, client = client,
} }
local lgi = require("lgi") local lgi = require("lgi")
local Gio = lgi.Gio
local GLib = lgi.GLib local GLib = lgi.GLib
local util = require("awful.util") local util = require("awful.util")
local gtable = require("gears.table") local gtable = require("gears.table")
local gtimer = require("gears.timer") local gtimer = require("gears.timer")
local aclient = require("awful.client") local aclient = require("awful.client")
local protected_call = require("gears.protected_call") local watcher = require("gears.watcher")
local spawn = {} 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) local function hash_command(cmd, rules)
rules = rules or {} rules = rules or {}
cmd = type(cmd) == "string" and cmd or table.concat(cmd, ';') 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[1] Integer the PID of the forked process.
-- @treturn[2] string Error message. -- @treturn[2] string Error message.
-- @staticfct awful.spawn.with_line_callback -- @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 -- It is still "officially" in `awful.spawn`, but since `gears.watcher`
local function step_done() -- depends on it, the implementation lives there.
if have_stdout and have_stderr and not done_before then spawn.with_line_callback = watcher._with_line_callback
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
--- Asynchronously spawn a program and capture its output. --- Asynchronously spawn a program and capture its output.
-- (wraps `spawn.with_line_callback`). -- (wraps `spawn.with_line_callback`).
@ -438,43 +369,10 @@ end
-- @treturn[2] string Error message. -- @treturn[2] string Error message.
-- @see spawn.with_line_callback -- @see spawn.with_line_callback
-- @staticfct awful.spawn.easy_async -- @staticfct awful.spawn.easy_async
function spawn.easy_async(cmd, callback)
local stdout = '' -- It is still "officially" in `awful.spawn`, but since `gears.watcher`
local stderr = '' -- depends on it, the implementation lives there.
local exitcode, exitreason spawn.easy_async = watcher._easy_async
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
--- Call `spawn.easy_async` with a shell. --- Call `spawn.easy_async` with a shell.
-- This calls `cmd` with `$SHELL -c` (via `awful.util.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). -- operation finishes (e.g. due to end of file).
-- @tparam[opt=false] boolean close Should the stream be closed after end-of-file? -- @tparam[opt=false] boolean close Should the stream be closed after end-of-file?
-- @staticfct awful.spawn.read_lines -- @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 spawn.read_lines = watcher._read_lines
start_read()
end
end
start_read()
end
-- When a command should only be executed once or only have a single instance, -- When a command should only be executed once or only have a single instance,
-- track the SNID set on them to prevent multiple execution. -- track the SNID set on them to prevent multiple execution.

View File

@ -7,7 +7,6 @@
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
-- Grab environment we need -- Grab environment we need
local os = os
local assert = assert local assert = assert
local load = loadstring or load -- luacheck: globals loadstring (compatibility with Lua 5.1) local load = loadstring or load -- luacheck: globals loadstring (compatibility with Lua 5.1)
local loadfile = loadfile local loadfile = loadfile
@ -19,6 +18,7 @@ local gstring = require("gears.string")
local grect = require("gears.geometry").rectangle local grect = require("gears.geometry").rectangle
local gcolor = require("gears.color") local gcolor = require("gears.color")
local gfs = require("gears.filesystem") local gfs = require("gears.filesystem")
local watcher = require("gears.watcher")
local capi = local capi =
{ {
awesome = awesome, awesome = awesome,
@ -31,8 +31,7 @@ local util = {}
util.table = {} util.table = {}
--- The default shell used when spawing processes. --- The default shell used when spawing processes.
-- @param string -- @tfield string awful.util.shell
util.shell = os.getenv("SHELL") or "/bin/sh"
--- Execute a system command and road the output. --- Execute a system command and road the output.
-- This function implementation **has been removed** and no longer -- This function implementation **has been removed** and no longer
@ -484,6 +483,19 @@ function util.round(x)
return gmath.round(x) return gmath.round(x)
end 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 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

628
lib/gears/connection.lua Normal file
View File

@ -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})

View File

@ -23,6 +23,9 @@ return
string = require("gears.string"); string = require("gears.string");
sort = require("gears.sort"); sort = require("gears.sort");
filesystem = require("gears.filesystem"); 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 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

656
lib/gears/reactive.lua Normal file
View File

@ -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 &lt;elv1313@gmail.com&gt;
-- @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 &lt;elv1313@gmail.com&gt;
--
-- 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})

View File

@ -59,6 +59,7 @@ local traceback = debug.traceback
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local glib = require("lgi").GLib local glib = require("lgi").GLib
local object = require("gears.object") local object = require("gears.object")
local gtable = require("gears.table")
local protected_call = require("gears.protected_call") local protected_call = require("gears.protected_call")
local gdebug = require("gears.debug") local gdebug = require("gears.debug")
@ -87,11 +88,11 @@ local timer = { mt = {} }
-- @method start -- @method start
-- @emits start -- @emits start
function timer: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")) gdebug.print_error(traceback("timer already started"))
return return
end 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") protected_call(self.emit_signal, self, "timeout")
return true return true
end) end)
@ -102,12 +103,12 @@ end
-- @method stop -- @method stop
-- @emits stop -- @emits stop
function timer: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")) gdebug.print_error(traceback("timer not started"))
return return
end end
glib.source_remove(self.data.source_id) glib.source_remove(self._private.source_id)
self.data.source_id = nil self._private.source_id = nil
self:emit_signal("stop") self:emit_signal("stop")
end end
@ -118,7 +119,7 @@ end
-- @emits start -- @emits start
-- @emits stop -- @emits stop
function timer:again() function timer:again()
if self.data.source_id ~= nil then if self._private.source_id ~= nil then
self:stop() self:stop()
end end
self:start() self:start()
@ -134,24 +135,28 @@ end
-- @param number -- @param number
-- @propemits true false -- @propemits true false
local timer_instance_mt = { function timer:get_timeout()
__index = function(self, property) return self._private.timeout
if property == "timeout" then end
return self.data.timeout
elseif property == "started" then
return self.data.source_id ~= nil
end
return timer[property] function timer:get_started()
end, return self._private.source_id ~= nil
end
__newindex = function(self, property, value) function timer:set_started(value)
if property == "timeout" then if value == self:get_started() then return end
self.data.timeout = tonumber(value)
self:emit_signal("property::timeout", value) if value then
end self:start()
else
self:stop()
end end
} end
function timer:set_timeout(value)
self._private.timeout = tonumber(value)
self:emit_signal("property::timeout", value)
end
--- Create a new timer object. --- Create a new timer object.
-- @tparam table args Arguments. -- @tparam table args Arguments.
@ -165,10 +170,33 @@ local timer_instance_mt = {
-- @constructorfct gears.timer -- @constructorfct gears.timer
function timer.new(args) function timer.new(args)
args = args or {} 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 gtable.crush(ret, timer, true)
setmetatable(ret, timer_instance_mt)
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 for k, v in pairs(args) do
ret[k] = v ret[k] = v

733
lib/gears/watcher.lua Normal file
View File

@ -0,0 +1,733 @@
--- Fetch information at a specific interval.
--
-- @author Emmanuel Lepage-Vallee &lt;elv1313@gmail.com&gt;
-- @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})

View File

@ -471,7 +471,8 @@ end
-- Read the table, separate attributes from widgets. -- Read the table, separate attributes from widgets.
local function parse_table(t, leave_empty) local function parse_table(t, leave_empty)
local max = 0 local max = 0
local attributes, widgets = {}, {} local attributes, widgets, with_handlers = {}, {}, {}
for k,v in pairs(t) do for k,v in pairs(t) do
if type(k) == "number" then if type(k) == "number" then
if v then if v then
@ -481,20 +482,35 @@ local function parse_table(t, leave_empty)
max = k max = k
end 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 end
elseif type(v) == "table" and rawget(v, "_set_declarative_handler") then
with_handlers[k] = v
else else
attributes[k] = v attributes[k] = v
end end
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. -- Pack the sparse table, if the container doesn't support sparse tables.
if not leave_empty then if not leave_empty then
widgets = gtable.from_sparse(widgets) widgets = gtable.from_sparse(widgets)
max = #widgets max = #widgets
end end
return max, attributes, widgets return max, attributes, widgets, with_handlers
end end
-- Recursively build a container from a declarative table. -- Recursively build a container from a declarative table.
@ -510,8 +526,19 @@ local function drill(ids, content)
-- Create layouts based on metatable's __call. -- Create layouts based on metatable's __call.
local l = layout.is_widget and layout or layout() 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). -- 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 -- Get the optional identifier to create a virtual widget tree to place
-- in an "access table" to be able to retrieve the widget. -- in an "access table" to be able to retrieve the widget.
@ -533,6 +560,14 @@ local function drill(ids, content)
end end
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. -- Add all widgets.
for k = 1, max do for k = 1, max do
-- ipairs cannot be used on sparse tables. -- ipairs cannot be used on sparse tables.
@ -555,10 +590,18 @@ local function drill(ids, content)
end end
end end
end end
-- Replace all children (if any) with the new ones. -- Replace all children (if any) with the new ones.
if widgets then if widgets then
l:set_children(widgets) l:set_children(widgets)
end 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 return l, id
end end

View File

@ -148,7 +148,7 @@ describe("gears.object", function()
assert.is.equal(obj2.foo, 42) assert.is.equal(obj2.foo, 42)
end) end)
it("dynamic property disabled", function() it("dynamic property enabled", function()
local class = {} local class = {}
function class:get_foo() return "bar" end function class:get_foo() return "bar" end

View File

@ -0,0 +1,213 @@
---------------------------------------------------------------------------
-- @author Emmanuel Lepage-Vallee
-- @copyright 2020 Emmanuel Lepage-Vallee &lt;elv1313@gmail.com&gt;
---------------------------------------------------------------------------
_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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1 @@
one two three

View File

@ -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)

View File

@ -0,0 +1 @@
three

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 '<i>' .. (my_hub.some_property / 100) .. '</i>'
end),
widget = wibox.widget.textbox
}
--DOC_NEWLINE
-- This will update the widget text to '<i>13.37</i>'
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 == "<i>13.37</i>") --DOC_HIDE
parent:add(w) --DOC_HIDE

View File

@ -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 "<span fgcolor='#ffff00'>The world is fine</span>"
else
return "<span fgcolor='#ff0000'>The world is on fire</span>"
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 == "<span fgcolor='#ff0000'>The world is on fire</span>") --DOC_HIDE
parent:add(w) --DOC_HIDE

View File

@ -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 '<i>' .. (my_hub.some_property / 100) .. '</i>'
end),
widget = wibox.widget.textbox
}
--DOC_NEWLINE
-- This will update the widget text to '<i>13.37</i>'
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 == "<i>13.37</i>") --DOC_HIDE
parent:add(w) --DOC_HIDE

View File

@ -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

View File

@ -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

View File

@ -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