743 lines
28 KiB
Lua
743 lines
28 KiB
Lua
--luacheck: no max line length
|
|
|
|
---------------------------------------------------------------------------
|
|
--- Spawning of programs.
|
|
--
|
|
-- This module provides methods to start programs and supports startup
|
|
-- notifications, which allows for callbacks and applying properties to the
|
|
-- program after it has been launched. This requires currently that the
|
|
-- applicaton supports them.
|
|
--
|
|
-- Frequently asked questions
|
|
-- ===
|
|
--
|
|
-- **Rules of thumb when a shell is needed**:
|
|
--
|
|
-- * A shell is required when the commands contain `&&`, `;`, `||`, `&` or
|
|
-- any other unix shell language syntax
|
|
-- * When shell variables are defined as part of the command
|
|
-- * When the command is a shell alias
|
|
--
|
|
-- Note that a shell is **not** a terminal emulator. A terminal emulator is
|
|
-- something like XTerm, Gnome-terminal or Konsole. A shell is something like
|
|
-- `bash`, `zsh`, `busybox sh` or `Debian ash`.
|
|
--
|
|
-- If you wish to open a process in a terminal window, check that your terminal
|
|
-- emulator supports the common `-e` option. If it does, then something like
|
|
-- this should work:
|
|
--
|
|
-- awful.spawn(terminal.." -e my_command")
|
|
--
|
|
-- Note that some terminals, such as rxvt-unicode (urxvt) support full commands
|
|
-- using quotes, while other terminal emulators require to use quoting.
|
|
--
|
|
-- **Understanding clients versus PID versus commands versus class**:
|
|
--
|
|
-- A *process* has a *PID* (process identifier). It can have 0, 1 or many
|
|
-- *window*s.
|
|
--
|
|
-- A *command* is what is used to start *process*(es). It has no direct relation
|
|
-- with *process*, *client* or *window*. When a command is executed, it will
|
|
-- usually start a *process* which keeps running until it exits. This however is
|
|
-- not always the case as some applications use scripts as command and others
|
|
-- use various single-instance mechanisms (usually client/server) and merge
|
|
-- with an existing process.
|
|
--
|
|
-- A *client* corresponds to a *window*. It is owned by a process. It can have
|
|
-- both a parent and one or many children. A *client* has a *class*, an
|
|
-- *instance*, a *role*, and a *type*. See `client.class`, `client.instance`,
|
|
-- `client.role` and `client.type` for more information about these properties.
|
|
--
|
|
-- **Understanding blocking versus asynchronous execution**:
|
|
--
|
|
-- Awesome is single threaded, it means only one thing is executed at any time.
|
|
-- But Awesome isn't doomed to be slow. It may not have multiple threads, but
|
|
-- it has something called coroutine and also has callbacks. This means things
|
|
-- that take time to execute can still do so in the background (using C threads
|
|
-- or external process + sockets). When they are done, they can notify the
|
|
-- Awesome thread. This works perfectly and avoid blocking Awesome.
|
|
--
|
|
-- If you want to update the text of a `wibox.widget.textbox` with the output
|
|
-- of a shell command, you should use the `awful.spawn.easy_async_with_shell`
|
|
-- command. It is strongly recommanded not to use `io.popen` is explained in the
|
|
-- "Getting a command's output" section. Asynchronous execution is at first a
|
|
-- bit tricky to understand if you never used that before. The example below
|
|
-- should demonstrate how it works.
|
|
--
|
|
-- If we do (but *really*, don't do that):
|
|
--
|
|
-- -- **NEVER, EVER, DO THIS**: your computer will freeze
|
|
-- os.execute("sleep 1; echo foo > /tmp/foo.txt")
|
|
-- mylabel.text = io.popen("cat /tmp/foo.txt"):read("*all")
|
|
--
|
|
-- The label will display `foo`. But If we do:
|
|
--
|
|
-- -- Don't do this, it wont work.
|
|
-- -- Assumes /tmp/foo.txt does not exist
|
|
-- awful.spawn.with_shell("sleep 1; echo foo > /tmp/foo.txt")
|
|
-- mylabel.text = io.popen("cat /tmp/foo.txt"):read("*all")
|
|
--
|
|
-- Then the label will be **empty**. `awful.spawn` and `awful.spawn.with_shell`
|
|
-- will **not** block and thus the `io.popen` will be executed before
|
|
-- `sleep 1` finishes. This is why we have async functions to execute shell
|
|
-- commands. There are many variants with difference characteristics and
|
|
-- complexity. `awful.spawn.easy_async` is the most common as it is good enough
|
|
-- for the general "I want to execute a command and do something with the
|
|
-- output when it finishes".
|
|
--
|
|
-- -- This is the correct way
|
|
-- local command = "sleep 1; echo foo > /tmp/foo.txt"
|
|
--
|
|
-- awful.spawn.easy_async_with_shell(command, function()
|
|
-- awful.spawn.easy_async_with_shell("cat /tmp/foo.txt", function(out)
|
|
-- mylabel.text = out
|
|
-- end)
|
|
-- end)
|
|
--
|
|
-- In this variant, Awesome will not block. Again, like other spawn, you
|
|
-- cannot add code outside of the callback function to use the result of the
|
|
-- command. The code will be executed before the command is finished so the
|
|
-- result wont yet be available.
|
|
--
|
|
-- **Getting a command's output**:
|
|
--
|
|
-- First, do **not** use `io.popen` **ever**. It is synchronous. Synchronous
|
|
-- functions **block everything** until they are done. All visual applications
|
|
-- lock (as Awesome no longer responds), you will probably lose some keyboard
|
|
-- and mouse events and will have higher latency when playing games. This is
|
|
-- also true when reading files synchronously, but this is another topic.
|
|
--
|
|
-- Awesome provides a few ways to get output from commands. One is to use the
|
|
-- `Gio` libraries directly. This is usually very complicated, but gives a lot
|
|
-- of control on the command execution.
|
|
--
|
|
-- This modules provides `with_line_callback` and `easy_async` for convenience.
|
|
-- First, lets add this bash command to `rc.lua`:
|
|
--
|
|
-- local noisy = [[bash -c '
|
|
-- for I in $(seq 1 5); do
|
|
-- date
|
|
-- echo err >&2
|
|
-- sleep 2
|
|
-- done
|
|
-- ']]
|
|
--
|
|
-- It prints a bunch of junk on the standard output (*stdout*) and error
|
|
-- (*stderr*) streams. This command would block Awesome for 10 seconds if it
|
|
-- were executed synchronously, but will not block it at all using the
|
|
-- asynchronous functions.
|
|
--
|
|
-- `with_line_callback` will execute the callbacks every time a new line is
|
|
-- printed by the command:
|
|
--
|
|
-- awful.spawn.with_line_callback(noisy, {
|
|
-- stdout = function(line)
|
|
-- naughty.notification { message = "LINE:"..line }
|
|
-- end,
|
|
-- stderr = function(line)
|
|
-- naughty.notification { message = "ERR:"..line}
|
|
-- end,
|
|
-- })
|
|
--
|
|
-- If only the full output is needed, then `easy_async` is the right choice:
|
|
--
|
|
-- awful.spawn.easy_async(noisy, function(stdout, stderr, reason, exit_code)
|
|
-- naughty.notification { message = stdout }
|
|
-- end)
|
|
--
|
|
-- **Default applications**:
|
|
--
|
|
-- If the intent is to open a file/document, then it is recommended to use the
|
|
-- following standard command. The default application will be selected
|
|
-- according to the [Shared MIME-info Database](https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html)
|
|
-- specification. The `xdg-utils` package provided by most distributions
|
|
-- includes the `xdg-open` command:
|
|
--
|
|
-- awful.spawn({"xdg-open", "/path/to/file"})
|
|
--
|
|
-- Awesome **does not** manage, modify or otherwise influence the database
|
|
-- for default applications. For information about how to do this, consult the
|
|
-- [ARCH Linux Wiki](https://wiki.archlinux.org/index.php/default_applications).
|
|
--
|
|
-- If you wish to change how the default applications behave, then consult the
|
|
-- [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html)
|
|
-- specification.
|
|
--
|
|
-- Spawning applications with specific properties
|
|
-- ===
|
|
--
|
|
-- **The startup notification protocol**:
|
|
--
|
|
-- The startup notification protocol is an optional specification implemented
|
|
-- by X11 applications to bridge the chain of knowledge between the moment a
|
|
-- program is launched to the moment its window (client) is shown. It can be
|
|
-- found [on the FreeDesktop.org website](https://www.freedesktop.org/wiki/Specifications/startup-notification-spec/).
|
|
--
|
|
-- Awesome has support for the various events that are part of the protocol, but
|
|
-- the most useful is the identifier, usually identified by its `SNID` acronym in
|
|
-- the documentation. It isn't usually necessary to even know it exists, as it
|
|
-- is all done automatically. However, if more control is required, the
|
|
-- identifier can be specified by an environment variable called
|
|
-- `DESKTOP_STARTUP_ID`. For example, let us consider execution of the following
|
|
-- command:
|
|
--
|
|
-- DESKTOP_STARTUP_ID="something_TIME$(date '+%s')" my_command
|
|
--
|
|
-- This should (if the program correctly implements the protocol) result in
|
|
-- `c.startup_id` to at least match `something`.
|
|
-- This identifier can then be used in `awful.rules` to configure the client.
|
|
--
|
|
-- Awesome can automatically set the `DESKTOP_STARTUP_ID` variable. This is used
|
|
-- by `awful.spawn` to specify additional rules for the startup. For example:
|
|
--
|
|
-- awful.spawn("urxvt -e maxima -name CALCULATOR", {
|
|
-- floating = true,
|
|
-- tag = mouse.screen.selected_tag,
|
|
-- placement = awful.placement.bottom_right,
|
|
-- })
|
|
--
|
|
-- This can also be used from the command line:
|
|
--
|
|
-- awesome-client 'awful=require("awful");
|
|
-- awful.spawn("urxvt -e maxima -name CALCULATOR", {
|
|
-- floating = true,
|
|
-- tag = mouse.screen.selected_tag,
|
|
-- placement = awful.placement.bottom_right,
|
|
-- })'
|
|
--
|
|
-- This table contains the client properties that are valid when used the
|
|
-- `sn_rules` or `prop` function argument. They are the same as in `awful.rules`.
|
|
--
|
|
--@DOC_rules_index_COMMON@
|
|
--
|
|
-- @author Julien Danjou <julien@danjou.info>
|
|
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
|
|
-- @copyright 2008 Julien Danjou
|
|
-- @copyright 2014 Emmanuel Lepage Vallee
|
|
-- @module awful.spawn
|
|
---------------------------------------------------------------------------
|
|
|
|
local capi =
|
|
{
|
|
awesome = awesome,
|
|
mouse = mouse,
|
|
client = client,
|
|
}
|
|
local lgi = require("lgi")
|
|
local Gio = lgi.Gio
|
|
local GLib = lgi.GLib
|
|
local util = require("awful.util")
|
|
local gtable = require("gears.table")
|
|
local gtimer = require("gears.timer")
|
|
local aclient = require("awful.client")
|
|
local protected_call = require("gears.protected_call")
|
|
|
|
local spawn = {}
|
|
|
|
|
|
local end_of_file
|
|
do
|
|
-- API changes, bug fixes and lots of fun. Figure out how a EOF is signalled.
|
|
local input
|
|
if not pcall(function()
|
|
-- No idea when this API changed, but some versions expect a string,
|
|
-- others a table with some special(?) entries
|
|
input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data(""))
|
|
end) then
|
|
input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data({}))
|
|
end
|
|
local line, length = input:read_line()
|
|
if not line then
|
|
-- Fixed in 2016: NULL on the C side is transformed to nil in Lua
|
|
end_of_file = function(arg)
|
|
return not arg
|
|
end
|
|
elseif tostring(line) == "" and #line ~= length then
|
|
-- "Historic" behaviour for end-of-file:
|
|
-- - NULL is turned into an empty string
|
|
-- - The length variable is not initialized
|
|
-- It's highly unlikely that the uninitialized variable has value zero.
|
|
-- Use this hack to detect EOF.
|
|
end_of_file = function(arg1, arg2)
|
|
return #arg1 ~= arg2
|
|
end
|
|
else
|
|
assert(tostring(line) == "", "Cannot determine how to detect EOF")
|
|
-- The above uninitialized variable was fixed and thus length is
|
|
-- always 0 when line is NULL in C. We cannot tell apart an empty line and
|
|
-- EOF in this case.
|
|
require("gears.debug").print_warning("Cannot reliably detect EOF on an "
|
|
.. "GIOInputStream with this LGI version")
|
|
end_of_file = function(arg)
|
|
return tostring(arg) == ""
|
|
end
|
|
end
|
|
end
|
|
|
|
local function hash_command(cmd, rules)
|
|
rules = rules or {}
|
|
cmd = type(cmd) == "string" and cmd or table.concat(cmd, ';')
|
|
|
|
-- 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)
|
|
local entry = spawn.snid_buffer[c.startup_id]
|
|
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)
|
|
|
|
gtimer.delayed_call(function()
|
|
spawn.snid_buffer[c.startup_id] = nil
|
|
end)
|
|
end
|
|
end
|
|
|
|
function spawn.on_snid_cancel(id)
|
|
if spawn.snid_buffer[id] then
|
|
spawn.snid_buffer[id] = nil
|
|
end
|
|
end
|
|
|
|
--- Spawn a program, and optionally apply properties and/or run a callback.
|
|
--
|
|
-- Applying properties or running a callback requires the program/client to
|
|
-- support startup notifications.
|
|
--
|
|
-- See `awful.rules.execute` for more details about the format of `sn_rules`.
|
|
--
|
|
-- @tparam string|table cmd The command.
|
|
-- @tparam[opt=true] table|boolean sn_rules A table of properties to be applied
|
|
-- after startup; `false` to disable startup notifications.
|
|
-- @tparam[opt] function callback A callback function to be run after startup.
|
|
-- @treturn[1] integer The forked PID.
|
|
-- @treturn[1] ?string The startup notification ID, if `sn` is not false, or
|
|
-- a `callback` is provided.
|
|
-- @treturn[2] string Error message.
|
|
-- @staticfct awful.spawn
|
|
function spawn.spawn(cmd, sn_rules, callback)
|
|
if cmd and cmd ~= "" then
|
|
local enable_sn = (sn_rules ~= false or callback)
|
|
enable_sn = not not enable_sn -- Force into a boolean.
|
|
local pid, snid = capi.awesome.spawn(cmd, enable_sn)
|
|
-- The snid will be nil in case of failure
|
|
if snid then
|
|
sn_rules = type(sn_rules) ~= "boolean" and sn_rules or {}
|
|
spawn.snid_buffer[snid] = { sn_rules, { callback } }
|
|
end
|
|
return pid, snid
|
|
end
|
|
-- For consistency
|
|
return "Error: No command to execute"
|
|
end
|
|
|
|
--- Spawn a program using the shell.
|
|
-- This calls `cmd` with `$SHELL -c` (via `awful.util.shell`).
|
|
-- @tparam string cmd The command.
|
|
-- @staticfct awful.spawn.with_shell
|
|
function spawn.with_shell(cmd)
|
|
if cmd and cmd ~= "" then
|
|
cmd = { util.shell, "-c", cmd }
|
|
return capi.awesome.spawn(cmd, false)
|
|
end
|
|
end
|
|
|
|
--- Spawn a program and asynchronously capture its output line by line.
|
|
-- @tparam string|table cmd The command.
|
|
-- @tab callbacks Table containing callbacks that should be invoked on
|
|
-- various conditions.
|
|
-- @tparam[opt] function callbacks.stdout Function that is called with each
|
|
-- line of output on stdout, e.g. `stdout(line)`.
|
|
-- @tparam[opt] function callbacks.stderr Function that is called with each
|
|
-- line of output on stderr, e.g. `stderr(line)`.
|
|
-- @tparam[opt] function callbacks.output_done Function to call when no more
|
|
-- output is produced.
|
|
-- @tparam[opt] function callbacks.exit Function to call when the spawned
|
|
-- process exits. This function gets the exit reason and code as its
|
|
-- arguments.
|
|
-- The reason can be "exit" or "signal".
|
|
-- For "exit", the second argument is the exit code.
|
|
-- For "signal", the second argument is the signal causing process
|
|
-- termination.
|
|
-- @treturn[1] Integer the PID of the forked process.
|
|
-- @treturn[2] string Error message.
|
|
-- @staticfct awful.spawn.with_line_callback
|
|
function spawn.with_line_callback(cmd, callbacks)
|
|
local stdout_callback, stderr_callback, done_callback, exit_callback =
|
|
callbacks.stdout, callbacks.stderr, callbacks.output_done, callbacks.exit
|
|
local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil
|
|
local pid, _, stdin, stdout, stderr = capi.awesome.spawn(cmd,
|
|
false, false, have_stdout, have_stderr, exit_callback)
|
|
if type(pid) == "string" then
|
|
-- Error
|
|
return pid
|
|
end
|
|
|
|
local done_before = false
|
|
local function step_done()
|
|
if have_stdout and have_stderr and not done_before then
|
|
done_before = true
|
|
return
|
|
end
|
|
if done_callback then
|
|
done_callback()
|
|
end
|
|
end
|
|
if have_stdout then
|
|
spawn.read_lines(Gio.UnixInputStream.new(stdout, true),
|
|
stdout_callback, step_done, true)
|
|
end
|
|
if have_stderr then
|
|
spawn.read_lines(Gio.UnixInputStream.new(stderr, true),
|
|
stderr_callback, step_done, true)
|
|
end
|
|
assert(stdin == nil)
|
|
return pid
|
|
end
|
|
|
|
--- Asynchronously spawn a program and capture its output.
|
|
-- (wraps `spawn.with_line_callback`).
|
|
-- @tparam string|table cmd The command.
|
|
-- @tab callback Function with the following arguments
|
|
-- @tparam string callback.stdout Output on stdout.
|
|
-- @tparam string callback.stderr Output on stderr.
|
|
-- @tparam string callback.exitreason Exit reason ("exit" or "signal").
|
|
-- @tparam integer callback.exitcode Exit code (exit code or signal number,
|
|
-- depending on "exitreason").
|
|
-- @treturn[1] Integer the PID of the forked process.
|
|
-- @treturn[2] string Error message.
|
|
-- @see spawn.with_line_callback
|
|
-- @staticfct awful.spawn.easy_async
|
|
function spawn.easy_async(cmd, callback)
|
|
local stdout = ''
|
|
local stderr = ''
|
|
local exitcode, exitreason
|
|
local function parse_stdout(str)
|
|
stdout = stdout .. str .. "\n"
|
|
end
|
|
local function parse_stderr(str)
|
|
stderr = stderr .. str .. "\n"
|
|
end
|
|
local function done_callback()
|
|
return callback(stdout, stderr, exitreason, exitcode)
|
|
end
|
|
local exit_callback_fired = false
|
|
local output_done_callback_fired = false
|
|
local function exit_callback(reason, code)
|
|
exitcode = code
|
|
exitreason = reason
|
|
exit_callback_fired = true
|
|
if output_done_callback_fired then
|
|
return done_callback()
|
|
end
|
|
end
|
|
local function output_done_callback()
|
|
output_done_callback_fired = true
|
|
if exit_callback_fired then
|
|
return done_callback()
|
|
end
|
|
end
|
|
return spawn.with_line_callback(
|
|
cmd, {
|
|
stdout=parse_stdout,
|
|
stderr=parse_stderr,
|
|
exit=exit_callback,
|
|
output_done=output_done_callback
|
|
})
|
|
end
|
|
|
|
--- Call `spawn.easy_async` with a shell.
|
|
-- This calls `cmd` with `$SHELL -c` (via `awful.util.shell`).
|
|
-- @tparam string cmd The command.
|
|
-- @tab callback Function with the following arguments
|
|
-- @tparam string callback.stdout Output on stdout.
|
|
-- @tparam string callback.stderr Output on stderr.
|
|
-- @tparam string callback.exitreason Exit reason ("exit" or "signal").
|
|
-- @tparam integer callback.exitcode Exit code (exit code or signal number,
|
|
-- depending on "exitreason").
|
|
-- @treturn[1] Integer the PID of the forked process.
|
|
-- @treturn[2] string Error message.
|
|
-- @see spawn.with_line_callback
|
|
-- @staticfct awful.spawn.easy_async_with_shell
|
|
function spawn.easy_async_with_shell(cmd, callback)
|
|
return spawn.easy_async({ util.shell, "-c", cmd or "" }, callback)
|
|
end
|
|
|
|
--- Read lines from a Gio input stream
|
|
-- @tparam Gio.InputStream input_stream The input stream to read from.
|
|
-- @tparam function line_callback Function that is called with each line
|
|
-- read, e.g. `line_callback(line_from_stream)`.
|
|
-- @tparam[opt] function done_callback Function that is called when the
|
|
-- operation finishes (e.g. due to end of file).
|
|
-- @tparam[opt=false] boolean close Should the stream be closed after end-of-file?
|
|
-- @staticfct awful.spawn.read_lines
|
|
function spawn.read_lines(input_stream, line_callback, done_callback, close)
|
|
local stream = Gio.DataInputStream.new(input_stream)
|
|
local function done()
|
|
if close then
|
|
stream:close()
|
|
end
|
|
stream:set_buffer_size(0)
|
|
if done_callback then
|
|
protected_call(done_callback)
|
|
end
|
|
end
|
|
local start_read, finish_read
|
|
start_read = function()
|
|
stream:read_line_async(GLib.PRIORITY_DEFAULT, nil, finish_read)
|
|
end
|
|
finish_read = function(obj, res)
|
|
local line, length = obj:read_line_finish(res)
|
|
if type(length) ~= "number" then
|
|
-- Error
|
|
print("Error in awful.spawn.read_lines:", tostring(length))
|
|
done()
|
|
elseif end_of_file(line, length) then
|
|
-- End of file
|
|
done()
|
|
else
|
|
-- Read a line
|
|
-- This needs tostring() for older lgi versions which returned
|
|
-- "GLib.Bytes" instead of Lua strings (I guess)
|
|
protected_call(line_callback, tostring(line))
|
|
|
|
-- Read the next line
|
|
start_read()
|
|
end
|
|
end
|
|
start_read()
|
|
end
|
|
|
|
-- 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 matcher then
|
|
for _, c in ipairs(client.get()) do
|
|
if matcher(c) then return true end
|
|
end
|
|
end
|
|
|
|
if #status.instances == 0 then return false end
|
|
|
|
for _, c in ipairs(status.instances) do
|
|
if c.valid then return true 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[opt] 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
|
|
-- @staticfct awful.spawn.once
|
|
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[opt] 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
|
|
-- @staticfct awful.spawn.single_instance
|
|
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.
|
|
-- @staticfct awful.spawn.raise_or_spawn
|
|
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 )
|
|
|
|
return setmetatable(spawn, { __call = function(_, ...) return spawn.spawn(...) end })
|
|
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
|