Merge remote-tracking branch 'elv/reactive'
This commit is contained in:
commit
32827126cc
|
@ -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
|
||||||
|
|
|
@ -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 |
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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})
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,656 @@
|
||||||
|
--- Utility module to convert functions to Excel like objects.
|
||||||
|
--
|
||||||
|
-- When converting a function into a `gears.reactive` object, all
|
||||||
|
-- properties accessed by the function are monitored for changes.
|
||||||
|
--
|
||||||
|
-- When such a change is detected, the `property::value` signal is emitted. When
|
||||||
|
-- used within a widget declarative construct, the property it is attached
|
||||||
|
-- to will also be automatically updated.
|
||||||
|
--
|
||||||
|
-- Theory
|
||||||
|
-- ======
|
||||||
|
--
|
||||||
|
-- ![Client geometry](../images/gears_reactive.svg)
|
||||||
|
--
|
||||||
|
-- To use this module and, more importantly, to understand how to write something
|
||||||
|
-- that actually works based on it, some background is required. Most AwesomeWM
|
||||||
|
-- objects are based on the `gears.object` module or it's `C` equivalent. Those
|
||||||
|
-- objects have "properties". Behind the scene, they have getters, setters and
|
||||||
|
-- a signal to notify the value changed.
|
||||||
|
--
|
||||||
|
-- `gears.reactive` adds a firewall like sandbox around the function. It
|
||||||
|
-- intercept any `gears.object` instance trying to cross the sandbox boundary
|
||||||
|
-- and replace them with proxy objects. Those proxies have built-in
|
||||||
|
-- introspection code to detect how they are used. This is then converted into
|
||||||
|
-- a list of objects and signals to monitor. Once one of the monitored object
|
||||||
|
-- emits one of the monitored signal, then the whole function is re-evaluated.
|
||||||
|
-- Each time the function is evaluated, the "target" properties are updated. The
|
||||||
|
-- reactive function result or any call to external function from within goes
|
||||||
|
-- through the firewall again and any proxies are removed.
|
||||||
|
--
|
||||||
|
-- That design has one big limitation. It cannot detect any changes which are
|
||||||
|
-- not directly part of `gears.object` instance. You cannot use random tables
|
||||||
|
-- and expect the function to be called when it's content change. To work
|
||||||
|
-- around this, it is recommanded to make "hub" objects to store the data used
|
||||||
|
-- within the reactive function.
|
||||||
|
--
|
||||||
|
-- Recommanded usage
|
||||||
|
-- =================
|
||||||
|
--
|
||||||
|
-- The best way to use `gears.reactive` is when the value used within the
|
||||||
|
-- expressions are part of other objects. It can be a `gears.watcher`, but
|
||||||
|
-- it can also be a custom object:
|
||||||
|
--
|
||||||
|
-- @DOC_wibox_widget_declarative_reactive_EXAMPLE@
|
||||||
|
--
|
||||||
|
-- Limitations
|
||||||
|
-- ===========
|
||||||
|
--
|
||||||
|
-- `gears.reactive` is pushing Lua way beyond what it has been designed for.
|
||||||
|
-- Because of this, there is some limitations.
|
||||||
|
--
|
||||||
|
-- * This module will **NOT** try to track the change of other
|
||||||
|
-- functions and methods called by the expression. It is **NOT** recursive
|
||||||
|
-- and only the top level properties are tracked. This is a feature, not a
|
||||||
|
-- bug. If it was recursive, this list of limitations or gotchas would be
|
||||||
|
-- endless.
|
||||||
|
-- * This only works with `gears.object` and Core API objects which implement
|
||||||
|
-- the `property::*****` signals correctly. If it is a regular Lua table
|
||||||
|
-- or the property signals are incorrectly used, the value changes cannot
|
||||||
|
-- be detected. If you find something that should work, but doesn't in
|
||||||
|
-- one of the AwesomeWM API, [please report a bug](https://github.com/awesomeWM/awesome/issues/new).
|
||||||
|
-- * More generally, when making a custom `gears.object` with custom setters,
|
||||||
|
-- it is the programmer responsibility to emit the signal. It is also
|
||||||
|
-- required to only emit those signals when the property actually changes to
|
||||||
|
-- avoid an unecessary re-evaluations.
|
||||||
|
-- * Changing the type of the variables accessed by the reactive function
|
||||||
|
-- (its "upvalues") after the reactive expression has been created wont
|
||||||
|
-- be detected. It will cause missed updates and, potentially, hard to debug
|
||||||
|
-- Lua errors within the proxy code itself.
|
||||||
|
-- * Internally, the engine tries its best to prevent the internal proxy objects
|
||||||
|
-- to leak out the sandbox. However this cannot be perfect, at least not
|
||||||
|
-- without adding limitation elsewhere. It is probably worth reporting a bug if
|
||||||
|
-- you encounter such an issue. But set your expectations, not all corner case
|
||||||
|
-- can be fixed.
|
||||||
|
-- * Don't use rawset in the expression.
|
||||||
|
-- * If the return value is a table, only the first 3 nesting levels are sanitized.
|
||||||
|
-- Avoid using nested tables in the returned value if you can. `gears.object`
|
||||||
|
-- instances *should* be fine.
|
||||||
|
-- * There is currently no way to disable a reactive expression once it's
|
||||||
|
-- been defined. This may change eventually.
|
||||||
|
-- * Rio Lua 5.1 (not LuaJIT 2.1) is currently not support. If you need it,
|
||||||
|
-- [please report a bug](https://github.com/awesomeWM/awesome/issues/new).
|
||||||
|
--
|
||||||
|
-- @author Emmanuel Lepage-Vallee <elv1313@gmail.com>
|
||||||
|
-- @copyright 2017-2020 Emmanuel Lepage-Vallee
|
||||||
|
-- @classmod gears.reactive
|
||||||
|
|
||||||
|
-- This file is provided under the BSD 2 clause license since it is better than
|
||||||
|
-- any existing implementation (most of which only support Lua 5.1).
|
||||||
|
--
|
||||||
|
-- Copyright 2020 Emmanuel Lepage-Vallee <elv1313@gmail.com>
|
||||||
|
--
|
||||||
|
-- Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
-- are permitted provided that the following conditions are met:
|
||||||
|
--
|
||||||
|
-- 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
-- list of conditions and the following disclaimer.
|
||||||
|
--
|
||||||
|
-- 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
-- this list of conditions and the following disclaimer in the documentation
|
||||||
|
-- and/or other materials provided with the distribution.
|
||||||
|
--
|
||||||
|
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
-- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||||
|
-- OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
||||||
|
-- IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
local gobject = require("gears.object")
|
||||||
|
local gtable = require("gears.table")
|
||||||
|
local gtimer = require("gears.timer")
|
||||||
|
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
|
||||||
|
|
||||||
|
-- It's only possible to know when something changes if it has signals.
|
||||||
|
local function is_gears_object(input)
|
||||||
|
return type(input) == "table" and rawget(input, "emit_signal")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If the sandbox proxies were to leave the reactive expressions, bad
|
||||||
|
-- things would happen. Catch functions and constructors to make sure
|
||||||
|
-- we strip away the proxy.
|
||||||
|
local function is_callable(object)
|
||||||
|
local t = type(object)
|
||||||
|
|
||||||
|
if t == "function" then return true end
|
||||||
|
|
||||||
|
if not (t == "table" or t == "userdata") then return false end
|
||||||
|
|
||||||
|
local mt = getmetatable(object)
|
||||||
|
|
||||||
|
return mt and mt.__call ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the upvalue current value or cached value.
|
||||||
|
local function get_upvalue(proxy_md)
|
||||||
|
if type(proxy_md) ~= "table" then return proxy_md end
|
||||||
|
|
||||||
|
return proxy_md._reactive.getter and
|
||||||
|
proxy_md._reactive.getter() or proxy_md._reactive.input
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Attempt to remove the proxy references. There might be loops
|
||||||
|
-- and stackoverflows, so it will only go 3 level deep. With some
|
||||||
|
-- luck, the result wont be a table. Even then, hopefully it wont
|
||||||
|
-- contain "raw" sub-tables. If it contains `gears.object`, it's
|
||||||
|
-- safe since they will be proxied.
|
||||||
|
local sanitize_return = nil
|
||||||
|
sanitize_return = function(input, depth)
|
||||||
|
if (depth or 0) > 3 then return input end
|
||||||
|
|
||||||
|
-- The first `input` is always a table, so it is safe to call `pairs`.
|
||||||
|
for k, v in pairs(input) do
|
||||||
|
local t = type(v)
|
||||||
|
|
||||||
|
if t == "table" then
|
||||||
|
if v._reactive then
|
||||||
|
-- No need to go deeper unless the user used rawset.
|
||||||
|
input[k] = get_upvalue(v)
|
||||||
|
else
|
||||||
|
-- It's an unproxied raw table, dig further.
|
||||||
|
sanitize_return(v, (depth or 1) + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return input
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The actual code that connects the gears.object signals.
|
||||||
|
local function add_key(self, key)
|
||||||
|
if type(key) ~= "string" or not is_gears_object(get_upvalue(self)) then return end
|
||||||
|
|
||||||
|
local input, objs = get_upvalue(self), self._reactive.origin.objects
|
||||||
|
|
||||||
|
objs[input] = objs[input] or {}
|
||||||
|
|
||||||
|
if objs[input][key] then return end
|
||||||
|
|
||||||
|
objs[input][key] = true
|
||||||
|
|
||||||
|
-- Prefer the `weak` version to avoid memory leaks. If nothing is holding a
|
||||||
|
-- reference, then the value wont change. Yes, there is some hairy corner cases.
|
||||||
|
-- Yes, the expression proxy itself is holding a strong reference so this wont
|
||||||
|
-- get used very often.
|
||||||
|
if input.weak_connect_signal then
|
||||||
|
input:weak_connect_signal("property::"..key, self._reactive.origin.callback)
|
||||||
|
else
|
||||||
|
input:connect_signal("property::"..key, self._reactive.origin.callback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gen_proxy_md(input, origin, getter)
|
||||||
|
assert(input and origin)
|
||||||
|
return {
|
||||||
|
input = (not getter) and input or nil,
|
||||||
|
getter = getter,
|
||||||
|
children = {},
|
||||||
|
properties = {},
|
||||||
|
origin = origin
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local create_proxy = nil
|
||||||
|
|
||||||
|
-- Un-proxy the function input and proxy its output.
|
||||||
|
local function proxy_call(self, ...)
|
||||||
|
local newargs, newrets = {}, {}
|
||||||
|
|
||||||
|
-- Remove the proxies before calling the function.
|
||||||
|
for _, v in ipairs{...} do
|
||||||
|
if type(v) == "table" and v._reactive then
|
||||||
|
v = get_upvalue(v)
|
||||||
|
end
|
||||||
|
table.insert(newargs, v)
|
||||||
|
end
|
||||||
|
|
||||||
|
local rets
|
||||||
|
|
||||||
|
if #newargs > 0 then
|
||||||
|
rets = {get_upvalue(self)(unpack(newargs))}
|
||||||
|
else
|
||||||
|
rets = {get_upvalue(self)(nil)}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add new proxies to make sure changes are detected if something
|
||||||
|
-- like `a_function().foo` is used. Since we still have legacy accessor
|
||||||
|
-- implemented as method (t:clients(), c:tags(), etc), this is actually
|
||||||
|
-- likely to happen.
|
||||||
|
for _, v in ipairs(rets) do
|
||||||
|
local ret, _ = create_proxy(v, self._reactive.origin)
|
||||||
|
table.insert(newrets, ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
return unpack(newrets)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build a tree of proxies or return the primitives.
|
||||||
|
local function proxy_index(self, key)
|
||||||
|
local up = get_upvalue(self)
|
||||||
|
|
||||||
|
-- Since it would otherwise print a cryptic error.
|
||||||
|
assert(
|
||||||
|
type(up) == "table",
|
||||||
|
"Trying to index a "..type(up).." value in a `gears.reactive` expression."
|
||||||
|
)
|
||||||
|
|
||||||
|
local upk = up[key]
|
||||||
|
|
||||||
|
-- Connect.
|
||||||
|
add_key(self, key)
|
||||||
|
|
||||||
|
local ret, is_proxy = create_proxy(upk, self._reactive.origin)
|
||||||
|
|
||||||
|
-- Always query the non-proxy upvalue. We cannot detect if they
|
||||||
|
-- change.
|
||||||
|
if is_proxy then
|
||||||
|
rawset(self, key, ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set valuers trough the proxy.
|
||||||
|
local function proxy_newindex(self, key, value)
|
||||||
|
rawset(self, key, create_proxy(value, self._reactive.origin))
|
||||||
|
|
||||||
|
-- Strip the proxy before setting the value on the original object.
|
||||||
|
if type(value) == "table" and value._reactive then
|
||||||
|
value = get_upvalue(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
local v = get_upvalue(self)
|
||||||
|
|
||||||
|
v[key] = value
|
||||||
|
|
||||||
|
-- Connect.
|
||||||
|
if is_gears_object(v) and not self._reactive.origin.objects[self][key] then
|
||||||
|
add_key(self, key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- It's possible that multiple proxy points to the same value.
|
||||||
|
-- While we could make a large map of all proxies, it's simpler
|
||||||
|
-- to just implement __eq/__lt/__le and be done.
|
||||||
|
local function proxy_equal(a, b)
|
||||||
|
a, b = get_upvalue(a), get_upvalue(b)
|
||||||
|
|
||||||
|
return a == b
|
||||||
|
end
|
||||||
|
|
||||||
|
local function proxy_lesser(a, b)
|
||||||
|
a, b = get_upvalue(a), get_upvalue(b)
|
||||||
|
|
||||||
|
return a < b
|
||||||
|
end
|
||||||
|
|
||||||
|
local function proxy_lesser_equal(a, b)
|
||||||
|
a, b = get_upvalue(a), get_upvalue(b)
|
||||||
|
|
||||||
|
return a <= b
|
||||||
|
end
|
||||||
|
|
||||||
|
local function proxy_tostring(o)
|
||||||
|
return tostring(get_upvalue(o))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Wrap tables and functions into a proxy object.
|
||||||
|
create_proxy = function(input, origin, getter)
|
||||||
|
local t = type(input)
|
||||||
|
local is_call = is_callable(input)
|
||||||
|
|
||||||
|
-- Everything but the tables are immutable.
|
||||||
|
if t ~= "table" and not is_call then return input, false end
|
||||||
|
|
||||||
|
-- Remove any foreign proxy.
|
||||||
|
if t ~= "function" and input._reactive and input._reactive.origin ~= origin then
|
||||||
|
input = get_upvalue(input)
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable({_reactive = gen_proxy_md(input, origin, getter)}, {
|
||||||
|
__index = proxy_index,
|
||||||
|
__newindex = proxy_newindex,
|
||||||
|
__call = proxy_call,
|
||||||
|
__eq = proxy_equal,
|
||||||
|
__lt = proxy_lesser,
|
||||||
|
__le = proxy_lesser_equal,
|
||||||
|
__tostring = proxy_tostring
|
||||||
|
}), true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create a "fake" upvalue ref because otherwise the sandbox could
|
||||||
|
-- "leak" and set upvalue in actual code outside of the function.
|
||||||
|
-- By "leak", I mean the proxy would become visible from outside
|
||||||
|
-- of the sandbox by the reactive expression "siblings" sharing the
|
||||||
|
-- same environment.
|
||||||
|
local function copy_upvalue_reference(loaded, idx, value)
|
||||||
|
-- Make sure it has a fresh upvalue reference. Something that
|
||||||
|
-- we are sure cannot be shared with something else.
|
||||||
|
local function fake_upvalue_env()
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
debug.upvaluejoin(loaded, idx, fake_upvalue_env, 1) --luacheck: ignore
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create a meta-getter for the original `fct` upvalue at index `idx`.
|
||||||
|
-- This will allow all the other code to dynamically get a "current" version
|
||||||
|
-- of the upvalue rather than something from the cache. It will also avoid
|
||||||
|
-- having to store a strong reference to the original value.
|
||||||
|
local function create_upvalue_getter(fct, idx)
|
||||||
|
local placeholder = nil
|
||||||
|
|
||||||
|
local function fake_upvalue_getter()
|
||||||
|
return placeholder
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Using this, each time `fake_upvalue_getter` is called, it
|
||||||
|
-- will return the current upvalue. This means we don't have to
|
||||||
|
-- cache it. Thus, the cache cannot get outdated. The drawback if
|
||||||
|
-- that if the type changes from an object to a primitive, it
|
||||||
|
-- will explode.
|
||||||
|
debug.upvaluejoin(fake_upvalue_getter, 1, fct, idx) --luacheck: ignore
|
||||||
|
|
||||||
|
return fake_upvalue_getter
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create a copy of the function and replace it's environment.
|
||||||
|
local function sandbox(fct, env, vars, values)
|
||||||
|
-- We must serialize the function for several reasons. First of all, it
|
||||||
|
-- might get wrapped into multiple `gears.reactive` objects (which we should
|
||||||
|
-- handle differently, but currently allow) or share the "upvalue environemnt"
|
||||||
|
-- (this variables from the upper execution context and stack frames).
|
||||||
|
--
|
||||||
|
-- For example, if 2 function both access the variables "foo" and "bar"
|
||||||
|
-- from the global context, they might end up with the same execution
|
||||||
|
-- environment. If we didn't create a copy, calling `debug.upvaluejoin`
|
||||||
|
-- woulc affect both functions.
|
||||||
|
local dump = string.dump(fct)
|
||||||
|
local loaded = load(dump, nil, nil, env)
|
||||||
|
|
||||||
|
-- It doesn't seem possible to "just remove" the upvalues. It's not possible
|
||||||
|
-- to have the catch-all in the metatable. It would have been nicer since this
|
||||||
|
-- code is redundant with the metatable (which are still needed for _G and _ENV
|
||||||
|
-- content).
|
||||||
|
for name, k in pairs(vars) do
|
||||||
|
if is_callable(values[name]) or type(values[name]) == "table" then
|
||||||
|
-- For table, functions and objects, use a proxy upvalue.
|
||||||
|
copy_upvalue_reference(
|
||||||
|
loaded,
|
||||||
|
k,
|
||||||
|
create_proxy(values[name], env.__origin, create_upvalue_getter(fct ,k))
|
||||||
|
)
|
||||||
|
else
|
||||||
|
-- For string, booleans, numbers and function, use the real upvalue.
|
||||||
|
-- This means if it is changed by something else, the sandboxed
|
||||||
|
-- copy sees the change.
|
||||||
|
debug.upvaluejoin(loaded, k, fct, k) --luacheck: ignore
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return loaded
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `getfenv` and `setfenv` would simplify this a lot, but are not
|
||||||
|
-- present in newer versions of Lua. Technically, we could have 3
|
||||||
|
-- implementation optimized for each Lua versions, but this one
|
||||||
|
-- seems to be portable (until now...). So while the performance
|
||||||
|
-- isn't as good as it could be, it's maintainable.
|
||||||
|
local function getfenv_compat(fct, root)
|
||||||
|
local vars, soft_env = {}, {}
|
||||||
|
|
||||||
|
local origin = {
|
||||||
|
objects = {},
|
||||||
|
callback = function()
|
||||||
|
root:emit_signal("property::value")
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
for i = 1, math.huge do
|
||||||
|
local name, val = debug.getupvalue(fct, i)
|
||||||
|
|
||||||
|
if name == nil then break end
|
||||||
|
|
||||||
|
soft_env[name] = val
|
||||||
|
|
||||||
|
if not vars[name] then
|
||||||
|
vars[name] = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create the sandbox.
|
||||||
|
local self = {__origin = origin}
|
||||||
|
local sandboxed = sandbox(fct, self, vars, soft_env)
|
||||||
|
|
||||||
|
return setmetatable(self, {
|
||||||
|
__index = function(_, key)
|
||||||
|
if _G[key] then
|
||||||
|
return create_proxy(_G[key], origin)
|
||||||
|
end
|
||||||
|
|
||||||
|
return soft_env[key]
|
||||||
|
end,
|
||||||
|
__newindex = function(_, key, value)
|
||||||
|
-- This `if` might be dead code.
|
||||||
|
if vars[key] then
|
||||||
|
debug.setupvalue(sandboxed, vars[key], value)
|
||||||
|
|
||||||
|
-- Do not try to disconnect the old one. It would make the code too complex.
|
||||||
|
if (not soft_env[key]) or get_upvalue(soft_env[key]) ~= value then
|
||||||
|
soft_env[key] = create_proxy(value, origin)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rawset(vars, key, create_proxy(value, origin))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__call = function()
|
||||||
|
return unpack(sanitize_return({sandboxed()}))
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local module = {}
|
||||||
|
|
||||||
|
local function real_set_value(self, force)
|
||||||
|
if self._private.delayed_started and not force then return end
|
||||||
|
|
||||||
|
local function value_transaction()
|
||||||
|
-- If `get_value` was called, then this transaction is no longer
|
||||||
|
-- pending.
|
||||||
|
if not self._private.delayed_started then return end
|
||||||
|
|
||||||
|
-- Reset the delay in case the setter causes the expression to
|
||||||
|
-- change.
|
||||||
|
self._private.delayed_started = false
|
||||||
|
|
||||||
|
self._private.value = self._private.origin()
|
||||||
|
self._private.evaluated = true
|
||||||
|
|
||||||
|
for _, target in pairs(self._private.targets) do
|
||||||
|
local obj, property = target[1], target[2]
|
||||||
|
obj[property] = self._private.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._private.delayed and not force then
|
||||||
|
gtimer.delayed_call(value_transaction)
|
||||||
|
self._private.delayed_started = true
|
||||||
|
else
|
||||||
|
self._private.delayed_started = true
|
||||||
|
value_transaction()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- A function which will be evaluated each time its content changes.
|
||||||
|
--
|
||||||
|
-- @property expression
|
||||||
|
-- @param function
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
|
--- The current value of the expression.
|
||||||
|
-- @property value
|
||||||
|
-- @propemits false false
|
||||||
|
|
||||||
|
--- Only evaluate once per event loop iteration.
|
||||||
|
--
|
||||||
|
-- In most case this is a simple performance win, but there is some
|
||||||
|
-- case where you might want the expression to be evaluated each time
|
||||||
|
-- one of the upvalue "really" change rather than batch them. This
|
||||||
|
-- option is enabled by default.
|
||||||
|
--
|
||||||
|
-- @property delayed
|
||||||
|
-- @tparam[opt=true] boolean delayed
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
|
function module:get_delayed()
|
||||||
|
return self._private.delayed
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_delayed(value)
|
||||||
|
if value == self._private.delayed then return end
|
||||||
|
|
||||||
|
self._private.delayed = value
|
||||||
|
self:emit_signal("property::delayed", value)
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_expression(value)
|
||||||
|
self:disconnect()
|
||||||
|
self._private.origin = getfenv_compat(value, self)
|
||||||
|
|
||||||
|
self:connect_signal("property::value", real_set_value)
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:get_value()
|
||||||
|
-- This will call `real_set_value`.
|
||||||
|
if (not self._private.evaluated) and self._private.origin then
|
||||||
|
self:refresh()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._private.delayed_started then
|
||||||
|
real_set_value(self, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
return self._private.value
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_value()
|
||||||
|
assert(false, "A value cannot be set on a `gears.reactive` instance.")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Disconnect all expression signals.
|
||||||
|
--
|
||||||
|
-- @method disconnect
|
||||||
|
|
||||||
|
function module:disconnect()
|
||||||
|
if self._private.origin then
|
||||||
|
for obj, properties in pairs(self._private.origin.__origin.objects) do
|
||||||
|
for property in pairs(properties) do
|
||||||
|
obj:disconnect_signal("property::"..property, self._private.origin.__origin.callback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Recompute the expression.
|
||||||
|
--
|
||||||
|
-- When the expression uses a non-object upvalue, the changes cannot
|
||||||
|
-- be auto-retected. Calling `:refresh()` will immediatly recompute the
|
||||||
|
-- expression.
|
||||||
|
--
|
||||||
|
-- @method refresh
|
||||||
|
|
||||||
|
function module:refresh()
|
||||||
|
if self._private.origin then
|
||||||
|
self:emit_signal("property::value")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add a new target property and object.
|
||||||
|
function module:_add_target(object, property)
|
||||||
|
local hash = tostring(object)..property
|
||||||
|
if self._private.targets[hash] then return end
|
||||||
|
|
||||||
|
self._private.targets[hash] = {object, property}
|
||||||
|
|
||||||
|
self:emit_signal("target_added", object, property)
|
||||||
|
|
||||||
|
if self._private.evaluated then
|
||||||
|
object[property] = self._private.value
|
||||||
|
else
|
||||||
|
real_set_value(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Emitted when a new property is attached to this reactive expression.
|
||||||
|
--
|
||||||
|
-- @signal target_added
|
||||||
|
-- @tparam gears.object object The object (often the widget).
|
||||||
|
-- @tparam string property The property name.
|
||||||
|
|
||||||
|
function module:_set_declarative_handler(parent, key)
|
||||||
|
-- Lua "properties", aka table.foo must be strings.
|
||||||
|
assert(
|
||||||
|
type(key) == "string",
|
||||||
|
"gears.reactive can only be used ob object properties"
|
||||||
|
)
|
||||||
|
|
||||||
|
self:_add_target(parent, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Create a new `gears.reactive` object.
|
||||||
|
-- @constructorfct gears.reactive
|
||||||
|
-- @tparam table args
|
||||||
|
-- @tparam function args.expression A function which accesses other `gears.object`.
|
||||||
|
-- @tparam gears.object args.object Any AwesomeWM object.
|
||||||
|
-- @tparam string args.property The name of the property to track.
|
||||||
|
|
||||||
|
local function new(_, args)
|
||||||
|
-- It *can* be done. Actually, it is much easier to implement this module
|
||||||
|
-- on 5.1 since `setfenv` is much simpler than `debug.upvaluejoin`. However,
|
||||||
|
-- unless someone asks, then why support 2 incompatible code paths. Luajit 2.1
|
||||||
|
-- supports `debug.upvaluejoin`.
|
||||||
|
assert(
|
||||||
|
debug.upvaluejoin, --luacheck: ignore
|
||||||
|
"Sorry, `gears.reactive` doesn't support Lua 5.1 at this time"
|
||||||
|
)
|
||||||
|
|
||||||
|
if type(args) == "function" then
|
||||||
|
args = {expression = args}
|
||||||
|
end
|
||||||
|
|
||||||
|
local self = gobject {
|
||||||
|
enable_properties = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
rawset(self, "_private", {
|
||||||
|
targets = {},
|
||||||
|
delayed = true
|
||||||
|
})
|
||||||
|
|
||||||
|
gtable.crush(self, module, true)
|
||||||
|
|
||||||
|
gtable.crush(self, args, false)
|
||||||
|
|
||||||
|
self:connect_signal("property::value", real_set_value)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--@DOC_object_COMMON@
|
||||||
|
|
||||||
|
return setmetatable(module, {__call = new})
|
|
@ -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
|
|
||||||
return self.data.timeout
|
|
||||||
elseif property == "started" then
|
|
||||||
return self.data.source_id ~= nil
|
|
||||||
end
|
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)
|
|
||||||
|
if value then
|
||||||
|
self:start()
|
||||||
|
else
|
||||||
|
self:stop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function timer:set_timeout(value)
|
||||||
|
self._private.timeout = tonumber(value)
|
||||||
self:emit_signal("property::timeout", value)
|
self:emit_signal("property::timeout", value)
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,733 @@
|
||||||
|
--- Fetch information at a specific interval.
|
||||||
|
--
|
||||||
|
-- @author Emmanuel Lepage-Vallee <elv1313@gmail.com>
|
||||||
|
-- @copyright 2020 Emmanuel Lepage-Vallee
|
||||||
|
-- @classmod gears.watcher
|
||||||
|
|
||||||
|
local capi = {awesome = awesome}
|
||||||
|
local protected_call = require("gears.protected_call")
|
||||||
|
local gtimer = require("gears.timer")
|
||||||
|
local gtable = require("gears.table")
|
||||||
|
local lgi = require("lgi")
|
||||||
|
local Gio = lgi.Gio
|
||||||
|
local GLib = lgi.GLib
|
||||||
|
|
||||||
|
local module = {}
|
||||||
|
|
||||||
|
-- This is awful.util.shell
|
||||||
|
module._shell = os.getenv("SHELL") or "/bin/sh"
|
||||||
|
|
||||||
|
local end_of_file
|
||||||
|
do
|
||||||
|
-- API changes, bug fixes and lots of fun. Figure out how a EOF is signalled.
|
||||||
|
local input
|
||||||
|
if not pcall(function()
|
||||||
|
-- No idea when this API changed, but some versions expect a string,
|
||||||
|
-- others a table with some special(?) entries
|
||||||
|
input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data(""))
|
||||||
|
end) then
|
||||||
|
input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data({}))
|
||||||
|
end
|
||||||
|
local line, length = input:read_line()
|
||||||
|
if not line then
|
||||||
|
-- Fixed in 2016: NULL on the C side is transformed to nil in Lua
|
||||||
|
end_of_file = function(arg)
|
||||||
|
return not arg
|
||||||
|
end
|
||||||
|
elseif tostring(line) == "" and #line ~= length then
|
||||||
|
-- "Historic" behaviour for end-of-file:
|
||||||
|
-- - NULL is turned into an empty string
|
||||||
|
-- - The length variable is not initialized
|
||||||
|
-- It's highly unlikely that the uninitialized variable has value zero.
|
||||||
|
-- Use this hack to detect EOF.
|
||||||
|
end_of_file = function(arg1, arg2)
|
||||||
|
return #arg1 ~= arg2
|
||||||
|
end
|
||||||
|
else
|
||||||
|
assert(tostring(line) == "", "Cannot determine how to detect EOF")
|
||||||
|
-- The above uninitialized variable was fixed and thus length is
|
||||||
|
-- always 0 when line is NULL in C. We cannot tell apart an empty line and
|
||||||
|
-- EOF in this case.
|
||||||
|
require("gears.debug").print_warning("Cannot reliably detect EOF on an "
|
||||||
|
.. "GIOInputStream with this LGI version")
|
||||||
|
end_of_file = function(arg)
|
||||||
|
return tostring(arg) == ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module._end_of_file = end_of_file
|
||||||
|
|
||||||
|
function module._read_lines(input_stream, line_callback, done_callback, close)
|
||||||
|
local stream = Gio.DataInputStream.new(input_stream)
|
||||||
|
local function done()
|
||||||
|
if close then
|
||||||
|
stream:close()
|
||||||
|
end
|
||||||
|
stream:set_buffer_size(0)
|
||||||
|
if done_callback then
|
||||||
|
protected_call(done_callback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local start_read, finish_read
|
||||||
|
start_read = function()
|
||||||
|
stream:read_line_async(GLib.PRIORITY_DEFAULT, nil, finish_read)
|
||||||
|
end
|
||||||
|
finish_read = function(obj, res)
|
||||||
|
local line, length = obj:read_line_finish(res)
|
||||||
|
if type(length) ~= "number" then
|
||||||
|
-- Error
|
||||||
|
print("Error in awful.spawn.read_lines:", tostring(length))
|
||||||
|
done()
|
||||||
|
elseif end_of_file(line, length) then
|
||||||
|
-- End of file
|
||||||
|
done()
|
||||||
|
else
|
||||||
|
-- Read a line
|
||||||
|
-- This needs tostring() for older lgi versions which returned
|
||||||
|
-- "GLib.Bytes" instead of Lua strings (I guess)
|
||||||
|
protected_call(line_callback, tostring(line))
|
||||||
|
|
||||||
|
-- Read the next line
|
||||||
|
start_read()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
start_read()
|
||||||
|
end
|
||||||
|
|
||||||
|
function module._with_line_callback(cmd, callbacks)
|
||||||
|
local stdout_callback, stderr_callback, done_callback, exit_callback =
|
||||||
|
callbacks.stdout, callbacks.stderr, callbacks.output_done, callbacks.exit
|
||||||
|
local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil
|
||||||
|
local pid, _, stdin, stdout, stderr = capi.awesome.spawn(cmd,
|
||||||
|
false, false, have_stdout, have_stderr, exit_callback)
|
||||||
|
if type(pid) == "string" then
|
||||||
|
-- Error
|
||||||
|
return pid
|
||||||
|
end
|
||||||
|
|
||||||
|
local done_before = false
|
||||||
|
local function step_done()
|
||||||
|
if have_stdout and have_stderr and not done_before then
|
||||||
|
done_before = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if done_callback then
|
||||||
|
done_callback()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if have_stdout then
|
||||||
|
module._read_lines(Gio.UnixInputStream.new(stdout, true),
|
||||||
|
stdout_callback, step_done, true)
|
||||||
|
end
|
||||||
|
if have_stderr then
|
||||||
|
module._read_lines(Gio.UnixInputStream.new(stderr, true),
|
||||||
|
stderr_callback, step_done, true)
|
||||||
|
end
|
||||||
|
assert(stdin == nil)
|
||||||
|
return pid
|
||||||
|
end
|
||||||
|
|
||||||
|
function module._easy_async(cmd, callback)
|
||||||
|
local stdout = ''
|
||||||
|
local stderr = ''
|
||||||
|
local exitcode, exitreason
|
||||||
|
local function parse_stdout(str)
|
||||||
|
stdout = stdout .. str .. "\n"
|
||||||
|
end
|
||||||
|
local function parse_stderr(str)
|
||||||
|
stderr = stderr .. str .. "\n"
|
||||||
|
end
|
||||||
|
local function done_callback()
|
||||||
|
return callback(stdout, stderr, exitreason, exitcode)
|
||||||
|
end
|
||||||
|
local exit_callback_fired = false
|
||||||
|
local output_done_callback_fired = false
|
||||||
|
local function exit_callback(reason, code)
|
||||||
|
exitcode = code
|
||||||
|
exitreason = reason
|
||||||
|
exit_callback_fired = true
|
||||||
|
if output_done_callback_fired then
|
||||||
|
return done_callback()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function output_done_callback()
|
||||||
|
output_done_callback_fired = true
|
||||||
|
if exit_callback_fired then
|
||||||
|
return done_callback()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return module._with_line_callback(
|
||||||
|
cmd, {
|
||||||
|
stdout=parse_stdout,
|
||||||
|
stderr=parse_stderr,
|
||||||
|
exit=exit_callback,
|
||||||
|
output_done=output_done_callback
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function module._read_async(path, callback, fail_callback)
|
||||||
|
local cancel = Gio.Cancellable()
|
||||||
|
|
||||||
|
Gio.File.new_for_path(path):load_contents_async(cancel, function(file, task)
|
||||||
|
local content = file:load_contents_finish(task)
|
||||||
|
if content then
|
||||||
|
callback(path, content)
|
||||||
|
elseif fail_callback then
|
||||||
|
fail_callback(path)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return cancel
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Posix files and commands always end with a newline.
|
||||||
|
-- It is nearly always unwanted, so we strip it by default.
|
||||||
|
local function remove_posix_extra_newline(content)
|
||||||
|
-- Remove the trailing `\n`
|
||||||
|
if content:sub(-1) == '\n' then
|
||||||
|
content = content:sub(1, -2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return content
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Make sure we sort transactions in a way obsolete ones are
|
||||||
|
-- not used.
|
||||||
|
|
||||||
|
local function add_transaction(self, transaction)
|
||||||
|
table.insert(self._private.transactions, transaction)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_transaction(self, transaction)
|
||||||
|
|
||||||
|
-- Too late, abort.
|
||||||
|
for _, cancel in pairs(transaction.pending) do
|
||||||
|
cancel:cancel()
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, t in ipairs(self._private.transactions) do
|
||||||
|
if t == transaction then
|
||||||
|
table.remove(self._private.transactions, k)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Keys can also be labels or object, # wont work.
|
||||||
|
local function count_files(files)
|
||||||
|
local ret = 0
|
||||||
|
|
||||||
|
for _ in pairs(files) do
|
||||||
|
ret = ret + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
-- When there is multiple files, we need to wait
|
||||||
|
-- until are of them are read.
|
||||||
|
local function gen_file_transaction(self)
|
||||||
|
if count_files(self.files) == 0 then return nil end
|
||||||
|
|
||||||
|
local ret = {
|
||||||
|
counter = count_files(self.files),
|
||||||
|
files = gtable.clone(self.files, false),
|
||||||
|
filter = self.filter,
|
||||||
|
labels = {},
|
||||||
|
content = {},
|
||||||
|
failed = {},
|
||||||
|
pending = {},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
local function finished(file, content)
|
||||||
|
assert(not ret.content[file])
|
||||||
|
self:emit_signal("file::acquired", file, content)
|
||||||
|
ret.pending[file] = nil
|
||||||
|
|
||||||
|
ret.counter = ret.counter - 1
|
||||||
|
ret.content[file] = content
|
||||||
|
|
||||||
|
if ret.counter > 0 then return end
|
||||||
|
|
||||||
|
self:emit_signal("files::acquired", ret.content, ret.failed)
|
||||||
|
|
||||||
|
-- Make the final table using the stored keys.
|
||||||
|
local contents = {}
|
||||||
|
|
||||||
|
for path, ctn in pairs(ret.content) do
|
||||||
|
if self.strip_newline then
|
||||||
|
ctn = remove_posix_extra_newline(ctn)
|
||||||
|
end
|
||||||
|
|
||||||
|
contents[self._private.file_keys[path]] = ctn
|
||||||
|
|
||||||
|
if self.labels_as_properties and type(ret.labels[path]) == "string" then
|
||||||
|
local val
|
||||||
|
|
||||||
|
if ret.filter then
|
||||||
|
val = ret.filter(ret.labels[path], ctn)
|
||||||
|
else
|
||||||
|
val = ctn
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make sure the signals are not fired for nothing.
|
||||||
|
if val ~= self[ret.labels[path]] then
|
||||||
|
self[ret.labels[path]] = val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ctn = count_files(ret.files) == 1 and contents[next(contents)] or contents
|
||||||
|
|
||||||
|
if ret.filter and not self.labels_as_properties then
|
||||||
|
self._private.value = ret.filter(ctn, ret.failed)
|
||||||
|
else
|
||||||
|
self._private.value = ctn
|
||||||
|
end
|
||||||
|
|
||||||
|
self:emit_signal("property::value", self._private.value)
|
||||||
|
|
||||||
|
remove_transaction(self, ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_error(file)
|
||||||
|
ret.pending[file] = nil
|
||||||
|
table.insert(ret.failed, file)
|
||||||
|
self:emit_signal("file::failed", file)
|
||||||
|
end
|
||||||
|
|
||||||
|
for label, file in pairs(ret.files) do
|
||||||
|
ret.labels[file] = label
|
||||||
|
|
||||||
|
local cancel = module._read_async(file, finished, read_error)
|
||||||
|
|
||||||
|
ret.pending[file] = cancel
|
||||||
|
end
|
||||||
|
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
local modes_start, modes_abort = {}, {}
|
||||||
|
|
||||||
|
modes_start["none"] = function() --[[nop]] end
|
||||||
|
modes_abort["none"] = function() --[[nop]] end
|
||||||
|
|
||||||
|
modes_start["files"] = function(self)
|
||||||
|
if not self._private.init then return end
|
||||||
|
|
||||||
|
local t = gen_file_transaction(self)
|
||||||
|
|
||||||
|
add_transaction(self, t)
|
||||||
|
end
|
||||||
|
|
||||||
|
modes_abort["files"] = function(self)
|
||||||
|
for _, t in ipairs(self._private.transactions) do
|
||||||
|
remove_transaction(self, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
modes_start["command"] = function(self)
|
||||||
|
if not self._private.init then return end
|
||||||
|
|
||||||
|
local com = self._private.command
|
||||||
|
|
||||||
|
if self._private.shell then
|
||||||
|
assert(
|
||||||
|
type(com) == "string",
|
||||||
|
"When using `gears.watcher` with `shell = true`, "..
|
||||||
|
"the command must be a string"
|
||||||
|
)
|
||||||
|
|
||||||
|
com = {module._shell, '-c', com}
|
||||||
|
end
|
||||||
|
|
||||||
|
module._easy_async(com, function(lines, stderr, err, errcode)
|
||||||
|
if self.strip_newline then
|
||||||
|
lines = remove_posix_extra_newline(lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.filter then
|
||||||
|
self._private.value = self.filter(lines, stderr, err, errcode)
|
||||||
|
else
|
||||||
|
self._private.value = lines
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
modes_abort["command"] = function()
|
||||||
|
--TODO
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:_set_declarative_handler(parent, key, ids)
|
||||||
|
assert(type(key) == "string", "A watcher can only be attached to properties")
|
||||||
|
|
||||||
|
table.insert(self._private.targets, {
|
||||||
|
ids = ids,
|
||||||
|
parent = parent,
|
||||||
|
property = key
|
||||||
|
})
|
||||||
|
|
||||||
|
if self._private.value then
|
||||||
|
parent[key] = self._private.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Abort the current transactions.
|
||||||
|
--
|
||||||
|
-- If files are currently being read or commands executed,
|
||||||
|
-- abort it. This does prevent new transaction from being
|
||||||
|
-- started. Use `:stop()` for that.
|
||||||
|
--
|
||||||
|
-- @method abort
|
||||||
|
-- @see stop
|
||||||
|
|
||||||
|
function module:abort()
|
||||||
|
modes_abort[self._private.mode](self)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Emitted when a file is read.
|
||||||
|
--
|
||||||
|
-- @signal file::acquired
|
||||||
|
-- @tparam string path The path.
|
||||||
|
-- @tparam string content The file content.
|
||||||
|
-- @see files::acquired
|
||||||
|
-- @see file::failed
|
||||||
|
|
||||||
|
--- When reading a file failed.
|
||||||
|
-- @signal file::failed
|
||||||
|
-- @tparam string path The path.
|
||||||
|
|
||||||
|
--- Emitted when all files are read.
|
||||||
|
--
|
||||||
|
-- @signal files::acquired
|
||||||
|
-- @tparam table contents Path as keys and content as values.
|
||||||
|
-- @tparam table failed The list of files which could not be read.
|
||||||
|
|
||||||
|
--- A file path.
|
||||||
|
--
|
||||||
|
-- @DOC_text_gears_watcher_simple_EXAMPLE@
|
||||||
|
--
|
||||||
|
-- @property file
|
||||||
|
-- @tparam string file
|
||||||
|
-- @propemits true false
|
||||||
|
-- @see files
|
||||||
|
|
||||||
|
--- A list or map of files.
|
||||||
|
--
|
||||||
|
-- It is often necessary to query multiple files. When reading from `proc`,
|
||||||
|
-- some data is split across multiple files, such as the battery charge.
|
||||||
|
--
|
||||||
|
-- This property accepts 2 format. One uses is a plain table of paths while
|
||||||
|
-- the other is a label->path map. Depending on what is being read, both make
|
||||||
|
-- sense.
|
||||||
|
--
|
||||||
|
-- **Simple path list:**
|
||||||
|
--
|
||||||
|
-- @DOC_text_gears_watcher_files1_EXAMPLE@
|
||||||
|
--
|
||||||
|
-- **With labels:**
|
||||||
|
--
|
||||||
|
-- @DOC_text_gears_watcher_files2_EXAMPLE@
|
||||||
|
--
|
||||||
|
-- @property files
|
||||||
|
-- @tparam table files
|
||||||
|
-- @propemits true false
|
||||||
|
-- @see labels_as_properties
|
||||||
|
|
||||||
|
function module:get_file()
|
||||||
|
return self._private.files[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_file(path)
|
||||||
|
self:set_files({path})
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:get_files()
|
||||||
|
return self._private.files
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_files(paths)
|
||||||
|
self:abort()
|
||||||
|
|
||||||
|
self._private.files = paths or {}
|
||||||
|
self._private.mode = "files"
|
||||||
|
|
||||||
|
self:emit_signal("property::files", self._private.files )
|
||||||
|
self:emit_signal("property::file" , select(2, next(self._private.files)))
|
||||||
|
|
||||||
|
-- It is possible to give names to each files. For modules like
|
||||||
|
-- battery widgets, which require reading multiple long paths from
|
||||||
|
-- `proc`, it makes the user code more readable.
|
||||||
|
self._private.file_keys = {}
|
||||||
|
|
||||||
|
for k, v in pairs(self._private.files) do
|
||||||
|
self._private.file_keys[v] = type(k) == "number" and v or k
|
||||||
|
end
|
||||||
|
|
||||||
|
modes_start["files"](self)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Add a file to the files list.
|
||||||
|
--
|
||||||
|
-- @method append_file
|
||||||
|
-- @tparam string path The path.
|
||||||
|
-- @tparam[opt] string The key.
|
||||||
|
|
||||||
|
function module:append_file(path, key)
|
||||||
|
self:abort()
|
||||||
|
|
||||||
|
if self._private.files[path] then return end
|
||||||
|
|
||||||
|
key = key or (#self._private.files + 1)
|
||||||
|
|
||||||
|
self._private.mode = "files"
|
||||||
|
|
||||||
|
self._private.files[key] = path
|
||||||
|
self._private.file_keys[path] = key
|
||||||
|
|
||||||
|
self:emit_signal("property::files", self._private.files )
|
||||||
|
self:emit_signal("property::file" , select(2, next(self._private.files)))
|
||||||
|
|
||||||
|
modes_start["files"](self)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Remove a file to the files list.
|
||||||
|
--
|
||||||
|
-- @method remove_file
|
||||||
|
-- @tparam string path The path or the key.
|
||||||
|
|
||||||
|
--- A filter to post-process the file or command.
|
||||||
|
--
|
||||||
|
-- It can be used, for example, to convert the string to a number or turn
|
||||||
|
-- the various file content into the final value. The filter argument
|
||||||
|
-- depend on various `gears.watcher` properties (as documented below).
|
||||||
|
--
|
||||||
|
-- **The callback parameters for a single file:**
|
||||||
|
-- (1) The file content (string)
|
||||||
|
--
|
||||||
|
-- **The callback parameters for a multiple files (paths):**
|
||||||
|
-- (1) Tables with the paths as keys and string content as value.
|
||||||
|
--
|
||||||
|
-- **The callback parameters for a multiple files (labels):**
|
||||||
|
-- (1) Tables with the keys as keys and string content as value.
|
||||||
|
--
|
||||||
|
-- **The callback when `labels_as_properties` is true:
|
||||||
|
-- (1) The label name
|
||||||
|
-- (2) The content
|
||||||
|
--
|
||||||
|
-- **The callback parameters for a command:**
|
||||||
|
-- (1) Stdout as first parameter
|
||||||
|
-- (2) Stderr as second parameter
|
||||||
|
-- (3) Exitreason
|
||||||
|
-- (4) Exitcode
|
||||||
|
--
|
||||||
|
-- @property filter
|
||||||
|
-- @tparam function filter
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
|
function module:get_filter()
|
||||||
|
return self._private.filter
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_filter(filter)
|
||||||
|
self:abort()
|
||||||
|
|
||||||
|
self._private.filter = filter
|
||||||
|
|
||||||
|
self:emit_signal("property::filter", filter)
|
||||||
|
|
||||||
|
modes_start[self._private.mode](self)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- A command.
|
||||||
|
--
|
||||||
|
-- If you plan to use pipes or any shell features, do not
|
||||||
|
-- forget to also set `shell` to `true`.
|
||||||
|
--
|
||||||
|
-- @DOC_text_gears_watcher_command1_EXAMPLE@
|
||||||
|
--
|
||||||
|
-- @property command
|
||||||
|
-- @tparam table|string command
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
|
function module:get_command()
|
||||||
|
return self._private.command
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_command(command)
|
||||||
|
self._private.command = command
|
||||||
|
self._private.mode = "command"
|
||||||
|
self:emit_signal("property::command")
|
||||||
|
modes_start["command"](self)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Use a shell when calling the command.
|
||||||
|
--
|
||||||
|
-- This means you can use `|`, `&&`, `$?` in your command.
|
||||||
|
--
|
||||||
|
-- @DOC_text_gears_watcher_command2_EXAMPLE@
|
||||||
|
---
|
||||||
|
-- @property shell
|
||||||
|
-- @tparam[opt=false] boolean|string shell
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
|
function module:get_shell()
|
||||||
|
return self._private.shell or false
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_shell(shell)
|
||||||
|
self._private.shell = shell
|
||||||
|
self:emit_signal("property::shell")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- In files mode, when paths have labels, apply them as properties.
|
||||||
|
--
|
||||||
|
-- @DOC_wibox_widget_declarative_watcher_EXAMPLE@
|
||||||
|
--
|
||||||
|
-- @property labels_as_properties
|
||||||
|
-- @tparam[opt=false] boolean labels_as_properties
|
||||||
|
-- @see files
|
||||||
|
|
||||||
|
--- The interval between the content refresh.
|
||||||
|
--
|
||||||
|
-- (in seconds)
|
||||||
|
--
|
||||||
|
-- @property interval
|
||||||
|
-- @tparam number interval
|
||||||
|
-- @see gears.timer.timeout
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
|
-- There is not get_timeout/set_timeout, so we can't make aliases.
|
||||||
|
module.get_interval = gtimer.get_timeout
|
||||||
|
module.set_interval = gtimer.set_timeout
|
||||||
|
|
||||||
|
--- The current value of the watcher.
|
||||||
|
--
|
||||||
|
-- If there is no filter, this will be a string. If a filter is used,
|
||||||
|
-- then it is whatever it returns.
|
||||||
|
--
|
||||||
|
-- @property value
|
||||||
|
-- @propemits false false
|
||||||
|
|
||||||
|
function module:get_value()
|
||||||
|
return self._private.value
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Strip the extra trailing newline.
|
||||||
|
--
|
||||||
|
-- All posix compliant text file and commands end with a newline.
|
||||||
|
-- Most of the time, this is inconvinient, so `gears.watcher` removes
|
||||||
|
-- them by default. Set this to `false` if this isn't the desired
|
||||||
|
-- behavior.
|
||||||
|
--
|
||||||
|
-- @property strip_newline
|
||||||
|
-- @tparam[opt=true] boolean strip_newline
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
|
function module:get_strip_newline()
|
||||||
|
return self._private.newline
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_strip_newline(value)
|
||||||
|
self._private.newline = value
|
||||||
|
self:emit_signal("property::strip_newline", value)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Start the timer.
|
||||||
|
-- @method start
|
||||||
|
-- @emits start
|
||||||
|
-- @baseclass gears.timer
|
||||||
|
-- @see stop
|
||||||
|
|
||||||
|
--- Stop the timer.
|
||||||
|
-- @method stop
|
||||||
|
-- @emits stop
|
||||||
|
-- @baseclass gears.timer
|
||||||
|
-- @see abort
|
||||||
|
-- @see start
|
||||||
|
|
||||||
|
--- The timer is started.
|
||||||
|
-- @property started
|
||||||
|
-- @tparam boolean started
|
||||||
|
-- @propemits false false
|
||||||
|
-- @baseclass gears.timer
|
||||||
|
|
||||||
|
--- Restart the timer.
|
||||||
|
-- This is equivalent to stopping the timer if it is running and then starting
|
||||||
|
-- it.
|
||||||
|
-- @method again
|
||||||
|
-- @baseclass gears.timer
|
||||||
|
-- @emits start
|
||||||
|
-- @emits stop
|
||||||
|
|
||||||
|
--- Create a new `gears.watcher` object.
|
||||||
|
--
|
||||||
|
-- @constructorfct gears.watcher
|
||||||
|
-- @tparam table args
|
||||||
|
-- @tparam string args.file A file path.
|
||||||
|
-- @tparam table args.files A list or map of files.
|
||||||
|
-- @tparam function args.filter A filter to post-process the file or command.
|
||||||
|
-- @tparam table|string args.command A command (without a shell).
|
||||||
|
-- @tparam[opt=false] boolean args.shell Use a shell when calling the command.
|
||||||
|
-- @param args.initial_value The value to use before the first "real" value is acquired.
|
||||||
|
-- @tparam number args.interval The interval between the content refresh.
|
||||||
|
-- @tparam boolean args.labels_as_properties Set the file labels as properties.
|
||||||
|
-- @tparam boolean args.started The timer is started.
|
||||||
|
|
||||||
|
local function new(_, args)
|
||||||
|
local ret = gtimer()
|
||||||
|
ret.timeout = 5000
|
||||||
|
|
||||||
|
local newargs = gtable.clone(args or {}, false)
|
||||||
|
|
||||||
|
ret._private.mode = "none"
|
||||||
|
ret._private.transactions = {}
|
||||||
|
ret._private.targets = {}
|
||||||
|
ret._private.files = {}
|
||||||
|
|
||||||
|
if newargs.autostart == nil then
|
||||||
|
newargs.autostart = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if newargs.strip_newline == nil then
|
||||||
|
newargs.strip_newline = true
|
||||||
|
end
|
||||||
|
|
||||||
|
gtable.crush(ret, module , true )
|
||||||
|
gtable.crush(ret, newargs, false)
|
||||||
|
|
||||||
|
ret.shell = args.shell
|
||||||
|
-- ret:set_shell(args.shell)
|
||||||
|
|
||||||
|
local function value_callback()
|
||||||
|
for _, target in ipairs(ret._private.targets) do
|
||||||
|
target.parent[target.property] = ret.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_callback()
|
||||||
|
modes_start[ret._private.mode](ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
ret:connect_signal("property::value", value_callback)
|
||||||
|
ret:connect_signal("timeout", update_callback)
|
||||||
|
|
||||||
|
if args.initial_value then
|
||||||
|
ret._private.value = args.initial_value
|
||||||
|
end
|
||||||
|
|
||||||
|
ret._private.init = true
|
||||||
|
|
||||||
|
if newargs.autostart then
|
||||||
|
ret:start()
|
||||||
|
modes_start[ret._private.mode](ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
--@DOC_object_COMMON@
|
||||||
|
|
||||||
|
return setmetatable(module, {__call = new})
|
|
@ -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
|
||||||
|
|
||||||
|
-- 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
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
-- @author Emmanuel Lepage-Vallee
|
||||||
|
-- @copyright 2020 Emmanuel Lepage-Vallee <elv1313@gmail.com>
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
_G.awesome.connect_signal = function() end
|
||||||
|
|
||||||
|
local reactive = require("gears.reactive")
|
||||||
|
local gobject = require("gears.object")
|
||||||
|
|
||||||
|
-- Keep track of the number of time the value changed.
|
||||||
|
local change_counter, last_counter = 0, 0
|
||||||
|
|
||||||
|
local function has_changed()
|
||||||
|
local ret = change_counter > last_counter
|
||||||
|
last_counter = change_counter
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
describe("gears.reactive", function()
|
||||||
|
-- Unsupported.
|
||||||
|
if not debug.upvaluejoin then return end -- luacheck: globals debug.upvaluejoin
|
||||||
|
|
||||||
|
local myobject1 = gobject {
|
||||||
|
enable_properties = true,
|
||||||
|
enable_auto_signals = true
|
||||||
|
}
|
||||||
|
|
||||||
|
local myobject2 = gobject {
|
||||||
|
enable_properties = true,
|
||||||
|
enable_auto_signals = true
|
||||||
|
}
|
||||||
|
|
||||||
|
local myobject3 = gobject {
|
||||||
|
enable_properties = true,
|
||||||
|
enable_auto_signals = true
|
||||||
|
}
|
||||||
|
|
||||||
|
-- This will create a property with a signal, we will need that later.
|
||||||
|
myobject3.bar = "baz"
|
||||||
|
myobject1.foo = 0
|
||||||
|
|
||||||
|
-- Using rawset wont add a signal. It means the change isn't visible to the
|
||||||
|
-- `gears.reactive` expression. However, we still want to make sure it can
|
||||||
|
-- use the raw property even without change detection.
|
||||||
|
rawset(myobject2, "obj3", myobject3)
|
||||||
|
|
||||||
|
-- Use a string to compare the address. We can't use `==` since
|
||||||
|
-- `gears.reactive` re-implement it to emulate the `==` of the source
|
||||||
|
-- objects.
|
||||||
|
local hash, hash2, hash3 = tostring(myobject1), tostring(myobject2), tostring(print)
|
||||||
|
|
||||||
|
-- Make sure the proxy wrapper isn't passed to the called functions.
|
||||||
|
local function check_no_proxy(obj)
|
||||||
|
assert.is.equal(rawget(obj, "_reactive"), nil)
|
||||||
|
assert.is.equal(hash, tostring(obj))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- With args.
|
||||||
|
function myobject1:method1(a, b, obj)
|
||||||
|
-- Make sure the proxy isn't propagated.
|
||||||
|
assert.is.equal(hash, tostring(obj))
|
||||||
|
assert.is.equal(hash, tostring(self))
|
||||||
|
assert.is.falsy(obj._reactive)
|
||||||
|
assert.is.falsy(self._reactive)
|
||||||
|
|
||||||
|
-- Check the arguments.
|
||||||
|
assert.is.equal(a, 1)
|
||||||
|
assert.is.equal(b, 2)
|
||||||
|
|
||||||
|
return myobject2, 42
|
||||||
|
end
|
||||||
|
|
||||||
|
-- With no args.
|
||||||
|
function myobject1:method2(a)
|
||||||
|
assert(a == nil)
|
||||||
|
assert(not self._reactive)
|
||||||
|
assert(hash == tostring(self))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create some _ENV variables. `gears.reactive` cannot detect the changes,
|
||||||
|
-- at least for now. This is to test if they can be used regardless.
|
||||||
|
local i, r = 1337, nil
|
||||||
|
|
||||||
|
it("basic creation", function()
|
||||||
|
r = reactive(function()
|
||||||
|
-- Skip busted, it uses its own debug magic which collide with
|
||||||
|
-- gears.reactive sandboxes.
|
||||||
|
local assert, tostring = rawget(_G, "assert"), rawget(_G, "tostring")
|
||||||
|
|
||||||
|
-- Using _G directly should bypass the proxy. It least until more
|
||||||
|
-- magic is implemented to stop it. So better test it too.
|
||||||
|
local realprint = _G.print
|
||||||
|
assert(tostring(realprint) == hash3)
|
||||||
|
|
||||||
|
-- But the "local" one should be proxy-ed to prevent the internal
|
||||||
|
-- proxy objects from leaking when calling a function outside of the
|
||||||
|
-- sandbox.
|
||||||
|
assert(tostring(print) == hash3)
|
||||||
|
|
||||||
|
-- Make sure we got a proxy.
|
||||||
|
assert(myobject1._reactive)
|
||||||
|
|
||||||
|
assert(not myobject1:method2())
|
||||||
|
|
||||||
|
local newobject, other = myobject1:method1(1,2, myobject1)
|
||||||
|
|
||||||
|
-- Make sure the returned objects are proxied properly.
|
||||||
|
assert(type(other) == "number")
|
||||||
|
assert(other == 42)
|
||||||
|
assert(newobject._reactive)
|
||||||
|
assert(tostring(newobject) == tostring(myobject2))
|
||||||
|
assert(tostring(newobject) == hash2)
|
||||||
|
|
||||||
|
-- Now call an upvalue local function
|
||||||
|
check_no_proxy(myobject1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
not_object = i,
|
||||||
|
object_expression = (myobject1.foo + 42),
|
||||||
|
nested_object_tree = myobject2.obj3.bar,
|
||||||
|
original_obj = myobject1
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
r:connect_signal("property::value", function()
|
||||||
|
change_counter = change_counter + 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert.is_false(has_changed())
|
||||||
|
|
||||||
|
-- Make sure that the reactive proxy didn't override the original value.
|
||||||
|
-- And yes, it's actually possible and there is explicit code to avoid
|
||||||
|
-- it.
|
||||||
|
assert.is.equal(hash, tostring(myobject1))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("basic_changes", function()
|
||||||
|
local val = r.value
|
||||||
|
|
||||||
|
-- The delayed magic should be transparent. It will never work
|
||||||
|
-- in the unit test, but it should not cause any visible behavior
|
||||||
|
-- change. It would not be magic if it was.
|
||||||
|
assert(val)
|
||||||
|
|
||||||
|
-- Disable delayed.
|
||||||
|
r._private.value = nil
|
||||||
|
r._private.evaluated = false
|
||||||
|
assert.is_true(r.delayed)
|
||||||
|
r.delayed = false
|
||||||
|
assert.is.falsy(r.delayed)
|
||||||
|
|
||||||
|
val = r.value
|
||||||
|
assert(val)
|
||||||
|
|
||||||
|
-- Make sure the proxy didn't leak into the return value
|
||||||
|
assert.is.falsy(rawget(val, "_reactive"))
|
||||||
|
assert.is.falsy(rawget(val.original_obj, "_reactive"))
|
||||||
|
|
||||||
|
assert.is_true(has_changed())
|
||||||
|
|
||||||
|
assert.is.equal(r._private.value.object_expression, 42)
|
||||||
|
assert.is.equal(r._private.value.not_object, 1337)
|
||||||
|
|
||||||
|
myobject1.foo = 1
|
||||||
|
|
||||||
|
assert.is_true(has_changed())
|
||||||
|
|
||||||
|
assert.is.equal(r._private.value.object_expression, 43)
|
||||||
|
|
||||||
|
-- Known limitation.
|
||||||
|
i = 1338
|
||||||
|
assert.is.equal(r._private.value.not_object, 1337)
|
||||||
|
r:refresh()
|
||||||
|
assert.is.equal(r._private.value.not_object, 1338)
|
||||||
|
|
||||||
|
-- Ensure that nested (and raw-setted) object property changes
|
||||||
|
-- are detected.
|
||||||
|
assert.is.equal(r._private.value.nested_object_tree, "baz")
|
||||||
|
myobject3.bar = "bazz"
|
||||||
|
assert.is_true(has_changed())
|
||||||
|
assert.is.equal(r._private.value.nested_object_tree, "bazz")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- gears.reactive play with the metatable operators a lot.
|
||||||
|
-- Make sure one of them work.
|
||||||
|
it("test tostring", function()
|
||||||
|
local myobject4 = gobject {
|
||||||
|
enable_properties = true,
|
||||||
|
enable_auto_signals = true
|
||||||
|
}
|
||||||
|
|
||||||
|
local mt = getmetatable(myobject4)
|
||||||
|
|
||||||
|
mt.__tostring = function() return "lol" end
|
||||||
|
|
||||||
|
local react = reactive(function()
|
||||||
|
_G.assert(myobject4._reactive)
|
||||||
|
_G.assert(tostring(myobject4) == "lol")
|
||||||
|
|
||||||
|
return tostring(myobject4)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local val = react.value
|
||||||
|
|
||||||
|
assert.is.equal(val, "lol")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("test disconnect", function()
|
||||||
|
r:disconnect()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
one two three
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
three
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue