Merge pull request #1487 from Elv13/fix_1482

Add an awful.rules chain of responsibility
This commit is contained in:
Emmanuel Lepage Vallée 2018-03-04 16:42:05 -05:00 committed by GitHub
commit aabff6dac7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 544 additions and 13 deletions

View File

@ -66,6 +66,8 @@ new_type("legacylayout", "Layout related functions", false, "param")
new_type("clientlayout", "Client layouts", false, "param")
-- Document some callback prototypes
new_type("callback", "Callback functions prototype", false, "Parameters")
-- awful.rules sources
new_type("rulesources", "Rule sources", false, "param")
-- More fitting section names
kind_names={topic='Documentation', module='Libraries', script='Sample files'}

View File

@ -37,6 +37,10 @@ local atag = require("awful.tag")
local gtable = require("gears.table")
local a_place = require("awful.placement")
local protected_call = require("gears.protected_call")
local aspawn = require("awful.spawn")
local gsort = require("gears.sort")
local gdebug = require("gears.debug")
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local rules = {}
@ -207,23 +211,176 @@ function rules.matches_list(c, _rules)
return false
end
--- Apply awful.rules.rules to a client.
-- @client c The client.
function rules.apply(c)
local props = {}
-- Contains the sources.
-- The elements are ordered "first in, first executed". Thus, the higher the
-- index, the higher the priority. Each entry is a table with a `name` and a
-- `callback` field. This table is exposed for debugging purpose. The API
-- is private and should be modified using the public accessors.
local rule_sources = {}
local rule_source_sort = gsort.topological()
--- Add a new rule source.
--
-- A rule source is a provider called when a client is managed (started). It
-- allows to configure the client by providing properties that should be applied.
-- By default, Awesome provides 2 sources:
--
-- * `awful.rules`: A declarative matcher
-- * `awful.spawn`: Launch clients with pre-defined properties
--
-- It is possible to register new callbacks to modify the properties table
-- before it is applied. Each provider is executed sequentially and modifies the
-- same table. If the first provider set a property, then the second can
-- override it, then the third, etc. Once the providers are exhausted, the
-- properties are applied on the client.
--
-- It is important to note that properties themselves have their own
-- dependencies. For example, a `tag` property implies a `screen`. Therefor, if
-- a `screen` is already specified, then it will be ignored when the rule is
-- executed. Properties also have their own priorities. For example, the
-- `titlebar` and `border_width` need to be applied before the `x` and `y`
-- positions are set. Otherwise, it will be off or the client will shift
-- upward everytime Awesome is restarted. A rule source *cannot* change this.
-- It is up to the callback to be aware of the dependencies and avoid to
-- introduce issues. For example, if the source wants to set a `screen`, it has
-- to check if the `tag`, `tags` or `new_tag` are on that `screen` or remove
-- those properties. Otherwise, they will be ignored once the rule is applied.
--
-- @tparam string name The provider name. It must be unique.
-- @tparam function callback The callback that is called to produce properties.
-- @tparam client callback.c The client
-- @tparam table callback.properties The current properties. The callback should
-- add to and overwrite properties in this table
-- @tparam table callback.callbacks A table of all callbacks scheduled to be
-- executed after the main properties are applied.
-- @tparam[opt={}] table depends_on A list of names of sources this source depends on
-- (sources that must be executed *before* `name`.
-- @tparam[opt={}] table precede A list of names of sources this source have a
-- priority over.
-- @treturn boolean Returns false if a dependency conflict was found.
function rules.add_rule_source(name, callback, depends_on, precede)
depends_on = depends_on or {}
precede = precede or {}
assert(type( depends_on ) == "table")
assert(type( precede ) == "table")
for _, v in ipairs(rule_sources) do
-- Names must be unique
assert(
v.name ~= name,
"Name must be unique, but '" .. name .. "' was already registered."
)
end
local new_sources = rule_source_sort:clone()
new_sources:prepend(name, precede )
new_sources:append (name, depends_on )
local res, err = new_sources:sort()
if err then
gdebug.print_warning("Failed to add the rule source: "..err)
return false
end
-- Only replace the source once the additions have been proven safe
rule_source_sort = new_sources
local callbacks = {}
for _, entry in ipairs(rules.matching_rules(c, rules.rules)) do
if entry.properties then
for property, value in pairs(entry.properties) do
props[property] = value
end
-- Get all callbacks for *existing* sources.
-- It is important to remember that names can be used in the sorting even
-- if the source itself doesn't (yet) exists.
for _, v in ipairs(rule_sources) do
callbacks[v.name] = v.callback
end
rule_sources = {}
callbacks[name] = callback
for _, v in ipairs(res) do
if callbacks[v] then
table.insert(rule_sources, 1, {
callback = callbacks[v],
name = v
})
end
end
return true
end
--- Remove a source.
-- @tparam string name The source name.
-- @treturn boolean If the source was removed
function rules.remove_rule_source(name)
rule_source_sort:remove(name)
for k, v in ipairs(rule_sources) do
if v.name == name then
table.remove(rule_sources, k)
return true
end
end
return false
end
-- Add the rules properties
local function apply_awful_rules(c, props, callbacks)
for _, entry in ipairs(rules.matching_rules(c, rules.rules)) do
gtable.crush(props,entry.properties or {})
if entry.callback then
table.insert(callbacks, entry.callback)
end
end
end
--- The default `awful.rules` source.
--
-- **Has priority over:**
--
-- *nothing*
--
-- @rulesources awful.rules
rules.add_rule_source("awful.rules", apply_awful_rules, {"awful.spawn"}, {})
-- Add startup_id overridden properties
local function apply_spawn_rules(c, props, callbacks)
if c.startup_id and aspawn.snid_buffer[c.startup_id] then
local snprops, sncb = unpack(aspawn.snid_buffer[c.startup_id])
-- The SNID tag(s) always have precedence over the rules one(s)
if snprops.tag or snprops.tags or snprops.new_tag then
props.tag, props.tags, props.new_tag = nil, nil, nil
end
gtable.crush(props, snprops)
gtable.merge(callbacks, sncb)
end
end
--- The rule source for clients spawned by `awful.spawn`.
--
-- **Has priority over:**
--
-- * `awful.rules`
--
-- @rulesources awful.spawn
rules.add_rule_source("awful.spawn", apply_spawn_rules, {}, {"awful.rules"})
--- Apply awful.rules.rules to a client.
-- @client c The client.
function rules.apply(c)
local callbacks, props = {}, {}
for _, v in ipairs(rule_sources) do
v.callback(c, props, callbacks)
end
rules.execute(c, props, callbacks)
end
@ -531,12 +688,11 @@ function rules.execute(c, props, callbacks)
end
end
-- TODO v5 deprecate this
function rules.completed_with_payload_callback(c, props, callbacks)
rules.execute(c, props, callbacks)
end
client.connect_signal("spawn::completed_with_payload", rules.completed_with_payload_callback)
client.connect_signal("manage", rules.apply)
return rules

View File

@ -165,6 +165,7 @@ local lgi = require("lgi")
local Gio = lgi.Gio
local GLib = lgi.GLib
local util = require("awful.util")
local timer = require("gears.timer")
local protected_call = require("gears.protected_call")
local spawn = {}
@ -216,8 +217,12 @@ function spawn.on_snid_callback(c)
if entry then
local props = entry[1]
local callback = entry[2]
--TODO v5: Remove this signal
c:emit_signal("spawn::completed_with_payload", props, callback)
spawn.snid_buffer[c.startup_id] = nil
timer.delayed_call(function()
spawn.snid_buffer[c.startup_id] = nil
end)
end
end

View File

@ -21,6 +21,7 @@ return
math = require("gears.math");
table = require("gears.table");
string = require("gears.string");
sort = require("gears.sort");
filesystem = require("gears.filesystem");
}

9
lib/gears/sort/init.lua Normal file
View File

@ -0,0 +1,9 @@
---------------------------------------------------------------------------
--- Extra sorting algorithms.
--
-- @module gears.sort
---------------------------------------------------------------------------
return {
topological = require("gears.sort.topological")
}

View File

@ -0,0 +1,117 @@
---------------------------------------------------------------------------
--- Extra sorting algorithms.
--
-- @submodule gears.sort
---------------------------------------------------------------------------
local tsort = {}
local gtable = require("gears.table")
local mt = { __index = tsort }
local function add_node(self, node)
if not self._edges[node] then
self._edges[node] = {}
end
end
--- Ensure that `node` appears after all `dependencies`.
-- @param node The node that edges are added to.
-- @tparam table dependencies List of nodes that have to appear before `node`.
function tsort:append(node, dependencies)
add_node(self, node)
for _, dep in ipairs(dependencies) do
add_node(self, dep)
self._edges[node][dep] = true
end
end
--- Ensure that `node` appears before all `subordinates`.
-- @param node The node that edges are added to.
-- @tparam table subordinates List of nodes that have to appear after `node`.
function tsort:prepend(node, subordinates)
for _, dep in ipairs(subordinates) do
self:append(dep, { node })
end
end
local HANDLING, DONE = 1, 2
local function visit(result, self, state, node)
if state[node] == DONE then
-- This node is already in the output
return
end
if state[node] == HANDLING then
-- We are handling this node already and managed to visit it again
-- from itself. Thus, there must be a loop.
result.BAD = node
return true
end
state[node] = HANDLING
-- Before this node, all nodes that it depends on must appear
for dep in pairs(self._edges[node]) do
if visit(result, self, state, dep) then
return true
end
end
state[node] = DONE
table.insert(result, node)
end
--- Create a copy of this topological sort.
-- This is useful to backup it before adding elements that can potentially
-- have circular dependencies and thus render the original useless.
function tsort:clone()
local new = tsort.topological()
-- Disable deep copy as the sorted values may be objects or tables
new._edges = gtable.clone(self._edges, false)
return new
end
--- Remove a node from the topological map.
--
-- @param node The node
function tsort:remove(node)
self._edges[node] = nil
for _, deps in pairs(self._edges) do
deps[node] = nil
end
end
--- Try to sort the nodes.
-- @treturn[1] table A sorted list of nodes
-- @treturn[2] nil
-- @return[2] A node around which a loop exists
function tsort:sort()
local result, state = {}, {}
for node in pairs(self._edges) do
if visit(result, self, state, node) then
return nil, result.BAD
end
end
return result
end
--- A topological sorting class.
--
-- The object returned by this function allows to create simple dependency
-- graphs. It can be used for decision making or ordering of complex sequences.
--
--
--@DOC_text_gears_sort_topological_EXAMPLE@
--
-- @function gears.sort.topological
function tsort.topological()
return setmetatable({
_edges = {},
}, mt)
end
return setmetatable(tsort, {__call = function(_, ...)
return tsort.topological(...)
end})

View File

@ -0,0 +1,173 @@
local tsort = require("gears.sort.topological")
describe("gears.sort.topological", function()
describe("prepend_1", function()
local ts = tsort.topological()
ts:prepend(1, { 2, 3, 4 })
ts:prepend(2, { 3, 4 })
ts:prepend(3, { 4 })
local res = ts:sort()
assert.is.equal(type(res), "table")
assert(#res == 4)
for k, v in pairs(res) do
assert.is.equal(k, v)
end
end)
describe("append_1", function()
local ts = tsort.topological()
ts:append(4, { 1, 2, 3 })
ts:append(3, { 1, 2 })
ts:append(2, { 1 })
local res = ts:sort()
assert.is.equal(type(res), "table")
assert(#res == 4)
for k, v in pairs(res) do
assert.is.equal(k, v)
end
end)
describe("mixed_1", function()
local ts = tsort.topological()
ts:prepend(1, { 2, 3 })
ts:append(3, { 1, 2 })
ts:append(2, { 1 })
ts:prepend(2, { 3 })
local res, err = ts:sort()
assert.is.equal(res[1], 1 )
assert.is.equal(res[2], 2 )
assert.is.equal(res[3], 3 )
assert.is.equal(type(err), "nil")
end)
describe("mixed_2", function()
local ts = tsort.topological()
ts:append(11, { 2, 9, 3, 5, 7, 10 })
ts:append(8, { 7, 3 })
ts:prepend(8, { 9 })
ts:prepend(3, { 10 })
ts:prepend(3, { 7, 5 })
ts:prepend(2, { 3, 5 })
ts:append(5, { 3 })
ts:append(10, { 9 })
ts:prepend(5, { 7 })
local res, err = ts:sort()
assert.is.equal(type(res), "table")
assert.is.equal(type(err), "nil")
assert.is.equal(#res, 8 )
assert.is.equal(res[1], 2)
assert.is.equal(res[2], 3)
assert.is.equal(res[3], 5)
assert.is.equal(res[4], 7)
assert.is.equal(res[5], 8)
assert.is.equal(res[6], 9)
assert.is.equal(res[7], 10)
assert.is.equal(res[8], 11)
end)
describe("mixed_2_and_clone", function()
local ts = tsort.topological()
ts:append(11, { 2, 9, 3, 5, 7, 10 })
ts:append(8, { 7, 3 })
ts:prepend(8, { 9 })
ts:prepend(3, { 10 })
ts:prepend(3, { 7, 5 })
ts:prepend(2, { 3, 5 })
ts:append(5, { 3 })
ts:append(10, { 9 })
ts:prepend(5, { 7 })
local res, err = ts:sort()
assert.is.equal(type(res), "table")
assert.is.equal(type(err), "nil")
assert.is.equal(#res, 8 )
assert.is.equal(res[1], 2)
assert.is.equal(res[2], 3)
assert.is.equal(res[3], 5)
assert.is.equal(res[4], 7)
assert.is.equal(res[5], 8)
assert.is.equal(res[6], 9)
assert.is.equal(res[7], 10)
assert.is.equal(res[8], 11)
local ts2 = ts:clone()
local res2, err2 = ts2:sort()
assert.is.equal(type(res2), "table")
assert.is.equal(type(err2), "nil")
assert.is.equal(#res, #res2)
for i=1, #res do
assert.is.equal(res[i], res2[i])
end
end)
describe("simple_remove", function()
local ts = tsort.topological()
ts:prepend(1, { 2, 3 })
ts:append(3, { 1, 2 })
ts:append(2, { 1 })
ts:prepend(2, { 3 })
local res, err = ts:sort()
assert.is.equal(#res, 3)
assert.is.equal(type(err), "nil")
ts:remove(2)
res, err = ts:sort()
assert.is.equal(#res, 2)
assert.is.equal(type(err), "nil")
end)
describe("simple_error_1", function()
local ts = tsort.topological()
ts:append(2, { 2 })
local res, err = ts:sort()
assert.is.equal(type(err), "number")
assert.is.equal(type(res), "nil")
end)
describe("simple_error_2", function()
local ts = tsort.topological()
ts:append(2, { 2, 3 })
local res, err = ts:sort()
assert.is.equal(type(err), "number")
assert.is.equal(type(res), "nil")
end)
describe("simple_error_3", function()
local ts = tsort.topological()
ts:append(2, { 3 })
ts:append(3, { 2 })
local res, err = ts:sort()
assert.is.equal(type(err), "number")
assert.is.equal(type(res), "nil")
end)
describe("simple_error_4", function()
local ts = tsort.topological()
ts:prepend(2, { 3 })
ts:prepend(3, { 2 })
local res, err = ts:sort()
assert.is.equal(type(err), "number")
assert.is.equal(type(res), "nil")
end)
end)

View File

@ -0,0 +1,22 @@
local gears = {sort={topological = require("gears.sort.topological")}} --DOC_HIDE
local tsort = gears.sort.topological()
tsort:prepend('a', { 'b' })
tsort:prepend('b', { 'c' })
tsort:prepend('c', { 'd' })
tsort:append('e', { 'd' })
tsort:append('f', { 'e', 'd' })
local res = assert(tsort:sort())
for k, v in ipairs(res) do
print("The position #"..k.." is: "..v)
end
assert(#res == 6) --DOC_HIDE
assert(res[1] == 'a') --DOC_HIDE
assert(res[2] == 'b') --DOC_HIDE
assert(res[3] == 'c') --DOC_HIDE
assert(res[4] == 'd') --DOC_HIDE
assert(res[5] == 'e') --DOC_HIDE
assert(res[6] == 'f') --DOC_HIDE

View File

@ -296,6 +296,12 @@ table.insert(multi_screen_steps, function()
screen = screen[1],
})
-- Same as previous, but switched
test_client("test_tag2", nil, {
tag = screen[1].tags[5],
screen = screen[2],
})
-- Add a client with multiple tags on the same screen, but not c.screen
test_client("test_tags1", nil, {
tags = { screen[1].tags[3], screen[1].tags[4] },
@ -326,7 +332,7 @@ end)
table.insert(multi_screen_steps, function()
if screen.count() < 2 then return true end
if #client.get() ~= 5 then return end
if #client.get() ~= 6 then return end
local c_by_class = {}
@ -336,6 +342,11 @@ table.insert(multi_screen_steps, function()
assert(c_by_class["test_tag1"].screen == screen[2])
assert(#c_by_class["test_tag1"]:tags() == 1)
assert(c_by_class["test_tag1"]:tags()[1] == screen[2].tags[2])
assert(c_by_class["test_tag2"].screen == screen[1])
assert(#c_by_class["test_tag2"]:tags() == 1)
assert(c_by_class["test_tag2"]:tags()[1] == screen[1].tags[5])
assert(c_by_class["test_tags1"].screen == screen[1])
assert(#c_by_class["test_tags1"]:tags() == 2)

View File

@ -264,6 +264,34 @@ test_rule {
end
}
-- Test the custom sources
assert(awful.rules.add_rule_source("high_priority", function(c, props, _)
assert(type(c) == "client")
assert(props.random2)
props.random1 = true
end, {"awful.spawn"}, {"awful.rules", "low_priority"}))
assert(awful.rules.add_rule_source("before2", function()
error("This function should not be called")
end, {"awful.spawn"}, {"awful.rules"}))
assert(awful.rules.remove_rule_source("before2"))
assert(awful.rules.add_rule_source("low_priority", function(c, props, _)
assert(type(c) == "client")
assert(not props.random1)
props.random2 = true
end, {"awful.spawn", "high_priority"}, {"void", "awful.rules"}))
local temp = gears.debug.print_warning
gears.debug.print_warning = function() end
assert(not awful.rules.add_rule_source("invalid_source", function()
assert(false, "This cannot happen")
end, {"awful.rules"}, {"awful.spawn"}))
gears.debug.print_warning = temp
-- Test tag and switchtotag
test_rule {
properties = {
@ -295,10 +323,17 @@ test_rule {
assert(c.screen.selected_tag.name ~= "8")
-- Test the custom sources
assert(c.random1)
assert(c.random2)
assert(not c.random3)
return true
end
}
-- Wait until all the auto-generated clients are ready
local function spawn_clients()
if #client.get() >= #tests then