Merge pull request #2409 from Elv13/spawn_once

A better run_or_raise/spawn.once/singleton API
This commit is contained in:
mergify[bot] 2018-10-07 11:35:11 +00:00 committed by GitHub
commit 2f70fd6cce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 529 additions and 19 deletions

View File

@ -8,7 +8,7 @@
-- Grab environment we need
local gdebug = require("gears.debug")
local spawn = require("awful.spawn")
local spawn = nil --TODO v5 deprecate
local set_shape = require("awful.client.shape").update.all
local object = require("gears.object")
local grect = require("gears.geometry").rectangle
@ -1170,8 +1170,11 @@ end
-- @tparam bool|function merge If true then merge tags (select the client's
-- first tag additionally) when the client is not visible.
-- If it is a function, it will be called with the client as argument.
-- @see awful.spawn.once
-- @see awful.spawn.single_instance
-- @see awful.spawn.raise_or_spawn
--
-- @function awful.client.run_or_raise
-- @deprecated awful.client.run_or_raise
-- @usage -- run or raise urxvt (perhaps, with tabs) on modkey + semicolon
-- awful.key({ modkey, }, 'semicolon', function ()
-- local matcher = function (c)
@ -1180,6 +1183,10 @@ end
-- awful.client.run_or_raise('urxvt', matcher)
-- end);
function client.run_or_raise(cmd, matcher, merge)
gdebug.deprecate("Use awful.spawn.single_instance instead of"..
"awful.client.run_or_raise", {deprecated_in=5})
spawn = spawn or require("awful.spawn")
local clients = capi.client.get()
local findex = gtable.hasitem(clients, capi.client.focus) or 1
local start = gmath.cycle(#clients, findex + 1)

View File

@ -16,6 +16,7 @@ local aplace = require("awful.placement")
local asuit = require("awful.layout.suit")
local beautiful = require("beautiful")
local alayout = require("awful.layout")
local atag = require("awful.tag")
local ewmh = {
generic_activate_filters = {},
@ -83,6 +84,10 @@ end
-- @tparam string context The context where this signal was used.
-- @tparam[opt] table hints A table with additional hints:
-- @tparam[opt=false] boolean hints.raise should the client be raised?
-- @tparam[opt=false] boolean hints.switch_to_tag should the client's first tag
-- be selected if none of the client's tags are selected?
-- @tparam[opt=false] boolean hints.switch_to_tags Select all tags associated
-- with the client.
function ewmh.activate(c, context, hints) -- luacheck: no unused args
hints = hints or {}
@ -121,12 +126,18 @@ function ewmh.activate(c, context, hints) -- luacheck: no unused args
return
end
if hints and hints.raise then
if hints.raise then
c:raise()
if not awesome.startup and not c:isvisible() then
c.urgent = true
end
end
-- The rules use `switchtotag`. For consistency and code re-use, support it,
-- but keep undocumented. --TODO v5 remove switchtotag
if hints.switchtotag or hints.switch_to_tag or hints.switch_to_tags then
atag.viewmore(c:tags(), c.screen, (not hints.switch_to_tags) and 0 or nil)
end
end
--- Add an activate (focus stealing) filter function.

View File

@ -15,7 +15,7 @@
-- * honor_workarea
-- * tag
-- * new_tag
-- * switchtotag
-- * switch_to_tags (also called switchtotag)
-- * focus
-- * titlebars_enabled
-- * callback
@ -83,7 +83,7 @@ If you want to put Emacs on a specific tag at startup, and immediately switch
to that tag you can add:
{ rule = { class = "Emacs" },
properties = { tag = mytagobject, switchtotag = true } }
properties = { tag = mytagobject, switch_to_tags = true } }
If you want to apply a custom callback to execute when a rule matched,
for example to pause playing music from mpd when you start dosbox, you
@ -374,6 +374,46 @@ end
rules.add_rule_source("awful.spawn", apply_spawn_rules, {}, {"awful.rules"})
local function apply_singleton_rules(c, props, callbacks)
local persis_id, info = c.single_instance_id, nil
-- This is a persistent property set by `awful.spawn`
if awesome.startup and persis_id then
info = aspawn.single_instance_manager.by_uid[persis_id]
elseif c.startup_id then
info = aspawn.single_instance_manager.by_snid[c.startup_id]
aspawn.single_instance_manager.by_snid[c.startup_id] = nil
elseif aspawn.single_instance_manager.by_pid[c.pid] then
info = aspawn.single_instance_manager.by_pid[c.pid].matcher(c) and
aspawn.single_instance_manager.by_pid[c.pid] or nil
end
if info then
c.single_instance_id = info.hash
gtable.crush(props, info.rules)
table.insert(callbacks, info.callback)
table.insert(info.instances, c)
-- Prevent apps with multiple clients from re-using this too often in
-- the first 30 seconds before the PID is cleared.
aspawn.single_instance_manager.by_pid[c.pid] = nil
end
end
--- The rule source for clients spawned by `awful.spawn.once` and `single_instance`.
--
-- **Has priority over:**
--
-- * `awful.rules`
--
-- **Depends on:**
--
-- * `awful.spawn`
--
-- @rulesources awful.spawn_once
rules.add_rule_source("awful.spawn_once", apply_singleton_rules, {"awful.spawn"}, {"awful.rules"})
--- Apply awful.rules.rules to a client.
-- @client c The client.
function rules.apply(c)
@ -437,7 +477,7 @@ rules.high_priority_properties = {}
-- @tfield table awful.rules.delayed_properties
-- By default, the table has the following functions:
--
-- * switchtotag
-- * switch_to_tags
rules.delayed_properties = {}
local force_ignore = {
@ -471,11 +511,17 @@ function rules.high_priority_properties.tag(c, value, props)
end
end
function rules.delayed_properties.switchtotag(c, value)
function rules.delayed_properties.switch_to_tags(c, value)
if not value then return end
atag.viewmore(c:tags(), c.screen)
end
function rules.delayed_properties.switchtotag(c, value)
gdebug.deprecate("Use switch_to_tags instead of switchtotag", {deprecated_in=5})
rules.delayed_properties.switch_to_tags(c, value)
end
function rules.extra_properties.geometry(c, _, props)
local cur_geo = c:geometry()

View File

@ -165,7 +165,9 @@ local lgi = require("lgi")
local Gio = lgi.Gio
local GLib = lgi.GLib
local util = require("awful.util")
local timer = require("gears.timer")
local gtable = require("gears.table")
local gtimer = require("gears.timer")
local aclient = require("awful.client")
local protected_call = require("gears.protected_call")
local spawn = {}
@ -210,6 +212,39 @@ do
end
end
local function hash_command(cmd, rules)
rules = rules or {}
cmd = type(cmd) == "string" and cmd or table.concat(cmd, ';')
-- Do its best at adding some entropy
local concat_rules = nil
concat_rules = function (r, depth)
if depth > 2 then return end
local keys = gtable.keys(rules)
for _, k in ipairs(keys) do
local v = r[k]
local t = type(v)
if t == "string" or t == "number" then
cmd = cmd..k..v
elseif t == "tag" then
cmd = cmd..k..v.name
elseif t == "table" and not t.connect_signal then
cmd = cmd .. k
concat_rules(v, depth + 1)
end
end
end
concat_rules(rules, 1)
local glibstr = GLib.String(cmd)
return string.format('%x', math.ceil(GLib.String.hash(glibstr)))
end
spawn.snid_buffer = {}
function spawn.on_snid_callback(c)
@ -220,7 +255,7 @@ function spawn.on_snid_callback(c)
--TODO v5: Remove this signal
c:emit_signal("spawn::completed_with_payload", props, callback)
timer.delayed_call(function()
gtimer.delayed_call(function()
spawn.snid_buffer[c.startup_id] = nil
end)
end
@ -435,6 +470,199 @@ function spawn.read_lines(input_stream, line_callback, done_callback, close)
start_read()
end
-- When a command should only be executed once or only have a single instance,
-- track the SNID set on them to prevent multiple execution.
spawn.single_instance_manager = {
by_snid = {},
by_pid = {},
by_uid = {},
}
aclient.property.persist("single_instance_id", "string")
-- Check if the client is running either using the rule source or the matcher.
local function is_running(hash, matcher)
local status = spawn.single_instance_manager.by_uid[hash]
if not status then return false end
if #status.instances == 0 then return false end
for _, c in ipairs(status.instances) do
if c.valid then return true end
end
if matcher then
for _, c in ipairs(client.get()) do
if matcher(c) then return true end
end
end
return false
end
-- Keep the data related to this hash.
local function register_common(hash, rules, matcher, callback)
local status = spawn.single_instance_manager.by_uid[hash]
if status then return status end
status = {
rules = rules,
callback = callback,
instances = setmetatable({}, {__mode = "v"}),
hash = hash,
exec = false,
matcher = matcher,
}
spawn.single_instance_manager.by_uid[hash] = status
return status
end
local function run_once_common(hash, cmd, keep_pid)
local pid, snid = spawn.spawn(cmd)
if type(pid) == "string" or not snid then return pid, snid end
assert(spawn.single_instance_manager.by_uid[hash])
local status = spawn.single_instance_manager.by_uid[hash]
status.exec = true
spawn.single_instance_manager.by_snid[snid] = status
if keep_pid then
spawn.single_instance_manager.by_pid[pid] = status
end
-- Prevent issues related to PID being re-used and a memory leak
gtimer {
single_shot = true,
autostart = true,
timeout = 30,
callback = function()
spawn.single_instance_manager.by_pid [pid ] = nil
spawn.single_instance_manager.by_snid[snid] = nil
end
}
return pid, snid
end
local function run_after_startup(f)
-- The clients are not yet managed during the first execution, so it will
-- miss existing instances.
if awesome.startup then
gtimer.delayed_call(f)
else
f()
end
end
--- Spawn a command if it has not been spawned before.
--
-- This function tries its best to preserve the state across `awesome.restart()`.
--
-- By default, when no `unique_id` is specified, this function will generate one by
-- hashing the command and its rules. If you have multiple instance of the same
-- command and rules, you need to specify an UID or only the first one will be
-- executed.
--
-- The `rules` are standard `awful.rules`.
--
-- This function depends on the startup notification protocol to be correctly
-- implemented by the command. See `client.startup_id` for more information.
-- Note that this also wont work with shell or terminal commands.
--
-- @tparam string|table cmd The command.
-- @tparam table rules The properties that need to be applied to the client.
-- @tparam[opt] function matcher A matching function to find the instance
-- among running clients.
-- @tparam[opt] string unique_id A string to identify the client so it isn't executed
-- multiple time.
-- @tparam[opt] function callback A callback function when the client is created.
-- @see awful.rules
function spawn.once(cmd, rules, matcher, unique_id, callback)
local hash = unique_id or hash_command(cmd, rules)
local status = register_common(hash, rules, matcher, callback)
run_after_startup(function()
if not status.exec and not is_running(hash, matcher) then
run_once_common(hash, cmd, matcher ~= nil)
end
end)
end
--- Spawn a command if an instance is not already running.
--
-- This is like `awful.spawn.once`, but will spawn new instances if the previous
-- has finished.
--
-- The `rules` are standard `awful.rules`.
--
-- This function depends on the startup notification protocol to be correctly
-- implemented by the command. See `client.startup_id` for more information.
-- Note that this also wont work with shell or terminal commands.
--
-- Note that multiple instances can still be spawned if the command is called
-- faster than the client has time to start.
--
-- @tparam string|table cmd The command.
-- @tparam table rules The properties that need to be applied to the client.
-- @tparam[opt] function matcher A matching function to find the instance
-- among running clients.
-- @tparam[opt] string unique_id A string to identify the client so it isn't executed
-- multiple time.
-- @tparam[opt] function callback A callback function when the client is created.
-- @see awful.rules
function spawn.single_instance(cmd, rules, matcher, unique_id, callback)
local hash = unique_id or hash_command(cmd, rules)
register_common(hash, rules, matcher, callback)
run_after_startup(function()
if not is_running(hash, matcher) then
return run_once_common(hash, cmd, matcher ~= nil)
end
end)
end
local raise_rules = {focus = true, switch_to_tags = true, raise = true}
--- Raise a client if it exists or spawn a new one then raise it.
--
-- This function depends on the startup notification protocol to be correctly
-- implemented by the command. See `client.startup_id` for more information.
-- Note that this also wont work with shell or terminal commands.
--
-- @tparam string|table cmd The command.
-- @tparam table rules The properties that need to be applied to the client.
-- @tparam[opt] function matcher A matching function to find the instance
-- among running clients.
-- @tparam[opt] string unique_id A string to identify the client so it isn't executed
-- multiple time.
-- @tparam[opt] function callback A callback function when the client is created.
-- @see awful.rules
-- @treturn client The client if it already exists.
function spawn.raise_or_spawn(cmd, rules, matcher, unique_id, callback)
local hash = unique_id or hash_command(cmd, rules)
local status = spawn.single_instance_manager.by_uid[hash]
if status then
for _, c in ipairs(status.instances) do
if c.valid then
c:emit_signal("request::activate", "spawn.raise_or_spawn", raise_rules)
return c
end
end
end
-- Do not modify the original. It also can't be a metatable.__index due to
-- its "broken" `pairs()` support.
local props = gtable.join(rules, raise_rules)
spawn.single_instance(cmd, props, matcher, unique_id, callback)
return nil
end
capi.awesome.connect_signal("spawn::canceled" , spawn.on_snid_cancel )
capi.awesome.connect_signal("spawn::timeout" , spawn.on_snid_cancel )
capi.client.connect_signal ("manage" , spawn.on_snid_callback )

View File

@ -1281,19 +1281,39 @@ function tag.viewonly(t)
end
--- View only a set of tags.
--
-- If `maximum` is set, there will be a limit on the number of new tag being
-- selected. The tags already selected do not count. To do nothing if one or
-- more of the tags are already selected, set `maximum` to zero.
--
-- @function awful.tag.viewmore
-- @param tags A table with tags to view only.
-- @param[opt] screen The screen of the tags.
function tag.viewmore(tags, screen)
-- @tparam[opt=#tags] number maximum The maximum number of tags to select.
function tag.viewmore(tags, screen, maximum)
maximum = maximum or #tags
local selected = 0
screen = get_screen(screen or ascreen.focused())
local screen_tags = screen.tags
for _, _tag in ipairs(screen_tags) do
if not gtable.hasitem(tags, _tag) then
_tag.selected = false
elseif _tag.selected then
selected = selected + 1
end
end
for _, _tag in ipairs(tags) do
if selected == 0 and maximum == 0 then
_tag.selected = true
break
end
if selected >= maximum then break end
if not _tag.selected then
selected = selected + 1
_tag.selected = true
end
end
screen:emit_signal("tag::history::update")
end

View File

@ -18,7 +18,7 @@ local gtable = {}
-- @return A new table containing all keys from the arguments.
function gtable.join(...)
local ret = {}
for _, t in pairs({...}) do
for _, t in ipairs({...}) do
if t then
for k, v in pairs(t) do
if type(k) == "number" then

View File

@ -292,11 +292,11 @@ assert(not awful.rules.add_rule_source("invalid_source", function()
end, {"awful.rules"}, {"awful.spawn"}))
gears.debug.print_warning = temp
-- Test tag and switchtotag
-- Test tag and switch_to_tags
test_rule {
properties = {
tag = "9",
switchtotag = true
switch_to_tags = true
},
test = function(class)
local c = get_client_by_class(class)
@ -312,7 +312,7 @@ test_rule {
test_rule {
properties = {
tag = "8",
switchtotag = false
switch_to_tags = false
},
test = function(class)
local c = get_client_by_class(class)

View File

@ -10,6 +10,35 @@ local exit_yay, exit_snd = nil, nil
-- * Using spawn with array is already covered by the test client.
-- * spawn with startup notification is covered by test-spawn-snid.lua
local tiny_client = function(class)
return {"lua", "-e", [[
local lgi = require 'lgi'
local Gtk = lgi.require('Gtk')
local class = ']]..class..[['
Gtk.init()
window = Gtk.Window {
default_width = 100,
default_height = 100,
title = 'title',
}
window:set_wmclass(class, class)
local app = Gtk.Application {}
function app:on_activate()
window.application = self
window:show_all()
end
app:run {''}
]]}
end
local matcher_called = false
local steps = {
function()
-- Test various error conditions. There are quite a number of them...
@ -119,6 +148,8 @@ local steps = {
exit_snd = code
end
})
spawn.once(tiny_client("client1"), {tag=screen[1].tags[2]})
end
if spawns_done == 3 then
assert(exit_yay == 0)
@ -126,7 +157,174 @@ local steps = {
assert(async_spawns_done == 2)
return true
end
end,
-- Test spawn_once
function()
if #client.get() ~= 1 then return end
assert(client.get()[1].class == "client1")
assert(client.get()[1]:tags()[1] == screen[1].tags[2])
spawn.once(tiny_client("client1"), {tag=screen[1].tags[2]})
spawn.once(tiny_client("client1"), {tag=screen[1].tags[2]})
return true
end,
function(count)
-- Limit the odds of a race condition
if count ~= 3 then return end
assert(#client.get() == 1)
assert(client.get()[1].class == "client1")
client.get()[1]:kill()
return true
end,
-- Test single_instance
function()
if #client.get() ~= 0 then return end
-- This should do nothing
spawn.once(tiny_client("client1"), {tag=screen[1].tags[2]})
spawn.single_instance(tiny_client("client2"), {tag=screen[1].tags[3]})
return true
end,
-- Test that no extra clients are created
function()
if #client.get() ~= 1 then return end
assert(client.get()[1].class == "client2")
assert(client.get()[1]:tags()[1] == screen[1].tags[3])
-- This should do nothing
spawn.single_instance(tiny_client("client2"), {tag=screen[1].tags[3]})
spawn.single_instance(tiny_client("client2"), {tag=screen[1].tags[3]})
return true
end,
function()
if #client.get() ~= 1 then return end
assert(client.get()[1].class == "client2")
assert(client.get()[1]:tags()[1] == screen[1].tags[3])
client.get()[1]:kill()
return true
end,
-- Test that new instances can be spawned
function()
if #client.get() ~= 0 then return end
spawn.single_instance(tiny_client("client2"), {tag=screen[1].tags[3]})
return true
end,
-- Test raise_or_spawn
function()
if #client.get() ~= 1 then return end
assert(client.get()[1].class == "client2")
assert(client.get()[1]:tags()[1] == screen[1].tags[3])
client.get()[1]:kill()
spawn.raise_or_spawn(tiny_client("client3"), {tag=screen[1].tags[3]})
return true
end,
-- Add more clients to test the focus
function()
if #client.get() ~= 1 then return end
-- In another iteration to make sure client4 has no focus
spawn(tiny_client("client4"), {tag = screen[1].tags[4]})
spawn(tiny_client("client4"), {tag = screen[1].tags[4]})
spawn(tiny_client("client4"), {
tag = screen[1].tags[4], switch_to_tags= true, focus = true,
})
return true
end,
function()
if #client.get() ~= 4 then return end
assert(screen[1].tags[3].selected == false)
for _, c in ipairs(client.get()) do
if c.class == "client4" then
assert(#c:tags() == 1)
assert(c:tags()[1] == screen[1].tags[4])
end
end
assert(screen[1].tags[4].selected == true)
spawn.raise_or_spawn(tiny_client("client3"), {tag=screen[1].tags[3]})
return true
end,
-- Test that the client can be raised
function()
if #client.get() ~= 4 then return false end
assert(client.focus.class == "client3")
assert(screen[1].tags[3].selected == true)
assert(screen[1].tags[4].selected == false)
for _, c in ipairs(client.get()) do
if c.class == "client4" then
c:tags()[1]:view_only()
client.focus = c
break
end
end
assert(screen[1].tags[3].selected == false)
assert(screen[1].tags[4].selected == true )
for _, c in ipairs(client.get()) do
if c.class == "client3" then
c:kill()
end
end
return true
end,
-- Test that a new instance can be spawned
function()
if #client.get() ~= 3 then return end
spawn.raise_or_spawn(tiny_client("client3"), {tag=screen[1].tags[3]})
return true
end,
function()
if #client.get() ~= 4 then return end
-- Cleanup
for _, c in ipairs(client.get()) do
c:kill()
end
return true
end,
-- Test the matcher
function()
if #client.get() ~= 0 then return end
spawn.single_instance("xterm", {tag=screen[1].tags[5]}, function(c)
matcher_called = true
return c.class == "xterm"
end)
return true
end,
function()
if #client.get() ~= 1 then return end
assert(matcher_called)
return true
end,
}
runner.run_steps(steps)

View File

@ -72,7 +72,7 @@ local steps = {
awful.screen.focused().tags[1]:view_only()
runner.add_to_default_rules({ rule = { class = "XTerm" },
properties = { tag = "2", focus = true, switchtotag = true }})
properties = { tag = "2", focus = true, switch_to_tags = true }})
awful.spawn("xterm")