From 4095eb91a837efb69a82caca3218ebbbb74e76a9 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 29 Sep 2015 18:05:56 -0400 Subject: [PATCH] Move util.spawn to a new module, add ability to spawn with properties * This commit add a new module to avoid a (4 level) loop dependency * It is now possible to call awful.spawn() with a table of properties * awful.rules is used to execute the rules. * Everything is public to allow alternative workflow modules such as Tyrannical to use their own callback implementation. --- lib/awful/client.lua | 3 +- lib/awful/init.lua | 15 +++ lib/awful/menu.lua | 3 +- lib/awful/rules.lua | 9 +- lib/awful/spawn.lua | 187 ++++++++++++++++++++++++++++++++++ lib/awful/util.lua | 135 ------------------------ lib/awful/widget/launcher.lua | 3 +- lib/awful/widget/prompt.lua | 3 +- lib/menubar/init.lua | 4 +- tests/test-focus.lua | 4 +- tests/test-spawn.lua | 6 +- tests/test-urgent.lua | 6 +- 12 files changed, 228 insertions(+), 150 deletions(-) create mode 100644 lib/awful/spawn.lua diff --git a/lib/awful/client.lua b/lib/awful/client.lua index 54da91f9..ea7027c5 100644 --- a/lib/awful/client.lua +++ b/lib/awful/client.lua @@ -9,6 +9,7 @@ -- Grab environment we need local util = require("awful.util") +local spawn = require("awful.spawn") local tag = require("awful.tag") local pairs = pairs local type = type @@ -1035,7 +1036,7 @@ function client.run_or_raise(cmd, matcher, merge) end -- client not found, spawn it - util.spawn(cmd) + spawn(cmd) end --- Get a matching transient_for client (if any). diff --git a/lib/awful/init.lua b/lib/awful/init.lua index d35d2562..750c8af1 100644 --- a/lib/awful/init.lua +++ b/lib/awful/init.lua @@ -15,6 +15,20 @@ function timer(...) return gtimer(...) end +--TODO: This is a hack for backwards-compatibility with 3.5, remove! +-- Set awful.util.spawn* +local spawn = require("awful.spawn") + +util.spawn = function(...) + util.deprecate("awful.spawn") + spawn.spawn(...) +end + +util.spawn_with_shell = function(...) + util.deprecate("awful.spawn.with_shell") + spawn.spawn_with_shell(...) +end + return { client = require("awful.client"); @@ -37,6 +51,7 @@ return tooltip = require("awful.tooltip"); ewmh = require("awful.ewmh"); titlebar = require("awful.titlebar"); + spawn = spawn; } -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/menu.lua b/lib/awful/menu.lua index a4cee4fe..0de0aa34 100644 --- a/lib/awful/menu.lua +++ b/lib/awful/menu.lua @@ -12,6 +12,7 @@ local wibox = require("wibox") local button = require("awful.button") local util = require("awful.util") +local spawn = require("awful.spawn") local tags = require("awful.tag") local keygrabber = require("awful.keygrabber") local beautiful = require("beautiful") @@ -258,7 +259,7 @@ function menu:exec(num, opts) end elseif type(cmd) == "string" then menu.get_root(self):hide() - util.spawn(cmd) + spawn(cmd) elseif type(cmd) == "function" then local visible, action = cmd(item, self) if not visible then diff --git a/lib/awful/rules.lua b/lib/awful/rules.lua index 54af8ea8..23d3cd8c 100644 --- a/lib/awful/rules.lua +++ b/lib/awful/rules.lua @@ -49,7 +49,7 @@ can add: { rule = { class = "dosbox" }, callback = function(c) - awful.util.spawn('mpc pause') + awful.spawn('mpc pause') end } Note that all "rule" entries need to match. If any of the entry does not @@ -233,6 +233,13 @@ function rules.execute(c, props, callbacks) end end +function rules.completed_with_payload_callback(c, props) + rules.execute(c, props, type(props.callback) == "function" and + {props.callback} or props.callback ) +end + +client.connect_signal("spawn::completed_with_payload", rules.completed_with_payload_callback) + client.connect_signal("manage", rules.apply) return rules diff --git a/lib/awful/spawn.lua b/lib/awful/spawn.lua new file mode 100644 index 00000000..24b2fad5 --- /dev/null +++ b/lib/awful/spawn.lua @@ -0,0 +1,187 @@ +--------------------------------------------------------------------------- +-- @author Julien Danjou <julien@danjou.info> +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2008 Julien Danjou +-- @copyright 2014 Emmanuel Lepage Vallee +-- @release @AWESOME_VERSION@ +--------------------------------------------------------------------------- + +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 spawn = {} + + +spawn.snid_buffer = {} + +function spawn.on_snid_callback(c) + local props = spawn.snid_buffer[c.startup_id] + if props then + c:emit_signal("spawn::completed_with_payload", props) + spawn.snid_buffer[c.startup_id] = nil + end +end + + +function spawn.on_snid_cancel(id) + if spawn.snid_buffer[id] then + spawn.snid_buffer[id] = nil + end +end + +--- Spawn a program. +-- See awful.rules.execute for more details +-- @param cmd The command. +-- @param sn_rules a property table, false to disable startup-notification. +-- @param callback A callback function (if the client support startup notifications) +-- @return The forked PID or an error message +-- @return The startup notification UID, if the spawn was successful +function spawn.spawn(cmd, sn_rules, callback) + if cmd and cmd ~= "" then + local enable_sn = (sn_rules ~= false or callback) + if not sn_rules and callback then + sn_rules = {callback=callback} + elseif callback then + sn_rules.callback = callback + end + local pid, snid = capi.awesome.spawn(cmd, enable_sn) + -- The snid will be nil in case of failure + if snid and type(sn_rules) == "table" then + spawn.snid_buffer[snid] = sn_rules + end + return pid, snid + end + -- For consistency + return "Error: No command to execute" +end + +--- Spawn a program using the shell. +-- @param cmd The command. +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 and capture its output line by line. +-- @tparam string|table cmd The command. +-- @tparam[opt] function stdout_callback Function that is called with each line of +-- output on stdout, e.g. `stdout_callback(line)`. +-- @tparam[opt] function stderr_callback Function that is called with each line of +-- output on stderr, e.g. `stderr_callback(line)`. +-- @tparam[opt] function done_callback Function to call when no more output is +-- produced. +-- @treturn[1] Integer the PID of the forked process. +-- @treturn[2] string Error message. +function spawn.with_line_callback(cmd, stdout_callback, stderr_callback, done_callback) + local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil + local pid, sn_id, stdin, stdout, stderr = capi.awesome.spawn(cmd, false, false, have_stdout, have_stderr) + 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 + done_callback() + 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 + +--- 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? +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 + if done_callback then + xpcall(done_callback, function(err) + print(debug.traceback("Error while calling done_callback:" + .. tostring(err), 2)) + end) + 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 #line ~= length then + -- End of file + done() + else + -- Read a line + xpcall(function() + -- This needs tostring() for older lgi versions which returned + -- "GLib.Bytes" instead of Lua strings (I guess) + line_callback(tostring(line)) + end, function(err) + print(debug.traceback("Error while calling line_callback: " + .. tostring(err), 2)) + end) + + -- Read the next line + start_read() + end + end + start_read() +end + +--- Read a program output and returns its output as a string. +-- @param cmd The command to run. +-- @return A string with the program output, or the error if one occured. +function spawn.pread(cmd) + if cmd and cmd ~= "" then + local f, err = io.popen(cmd, 'r') + if f then + local s = f:read("*all") + f:close() + return s + else + return err + end + end +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 ) + +capi.client.add_signal ("spawn::completed_with_payload" ) + +return setmetatable(spawn, { __call = function(_, ...) return spawn.spawn(...) end }) +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/util.lua b/lib/awful/util.lua index 8ba24511..4fa9b743 100644 --- a/lib/awful/util.lua +++ b/lib/awful/util.lua @@ -22,8 +22,6 @@ local pairs = pairs local string = string local lgi = require("lgi") local Pango = lgi.Pango -local Gio = lgi.Gio -local GLib = lgi.GLib local capi = { awesome = awesome, @@ -85,139 +83,6 @@ function util.mkdir(dir) return os.execute("mkdir -p " .. dir) end ---- Spawn a program. --- The program gets started on the default screen. --- @tparam string|table cmd The command. --- @tparam boolean sn Enable startup-notification. --- @treturn[1] integer The forked PID. --- @treturn[1] string The startup notification ID, if `sn` is true. --- @treturn[2] string Error message. -function util.spawn(cmd, sn) - if cmd and cmd ~= "" then - if sn == nil then sn = true end - return capi.awesome.spawn(cmd, sn) - end -end - ---- Spawn a program using the shell. --- @tparam string cmd The command. --- @tparam[opt=false] boolean sn Enable startup-notification? --- @treturn[1] integer The forked PID. --- @treturn[1] string The startup notification ID, if `sn` is true. --- @treturn[2] string Error message. -function util.spawn_with_shell(cmd, sn) - sn = sn or false - if cmd and cmd ~= "" then - cmd = { util.shell, "-c", cmd } - return util.spawn(cmd, sn) - end -end - ---- Spawn a program and asynchronously and capture its output line by line. --- @tparam string|table cmd The command. --- @tparam[opt] function stdout_callback Function that is called with each line of --- output on stdout, e.g. `stdout_callback(line)`. --- @tparam[opt] function stderr_callback Function that is called with each line of --- output on stderr, e.g. `stderr_callback(line)`. --- @tparam[opt] function done_callback Function to call when no more output is --- produced. --- @treturn[1] Integer the PID of the forked process. --- @treturn[2] string Error message. -function util.spawn_with_line_callback(cmd, stdout_callback, stderr_callback, done_callback) - local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil - local pid, sn_id, stdin, stdout, stderr = capi.awesome.spawn(cmd, false, false, have_stdout, have_stderr) - 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 - done_callback() - end - if have_stdout then - util.read_lines(Gio.UnixInputStream.new(stdout, true), - stdout_callback, step_done, true) - end - if have_stderr then - util.read_lines(Gio.UnixInputStream.new(stderr, true), - stderr_callback, step_done, true) - end - assert(stdin == nil) - return pid -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? -function util.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 - if done_callback then - xpcall(done_callback, function(err) - print(debug.traceback("Error while calling done_callback:" - .. tostring(err), 2)) - end) - 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.util.read_lines:", tostring(length)) - done() - elseif #line ~= length then - -- End of file - done() - else - -- Read a line - xpcall(function() - -- This needs tostring() for older lgi versions which returned - -- "GLib.Bytes" instead of Lua strings (I guess) - line_callback(tostring(line)) - end, function(err) - print(debug.traceback("Error while calling line_callback: " - .. tostring(err), 2)) - end) - - -- Read the next line - start_read() - end - end - start_read() -end - ---- Read a program output and returns its output as a string. --- @param cmd The command to run. --- @return A string with the program output, or the error if one occured. -function util.pread(cmd) - if cmd and cmd ~= "" then - local f, err = io.popen(cmd, 'r') - if f then - local s = f:read("*all") - f:close() - return s - else - return err - end - end -end - --- Eval Lua code. -- @return The return value of Lua code. function util.eval(s) diff --git a/lib/awful/widget/launcher.lua b/lib/awful/widget/launcher.lua index 91833f26..af5fbf41 100644 --- a/lib/awful/widget/launcher.lua +++ b/lib/awful/widget/launcher.lua @@ -7,6 +7,7 @@ local setmetatable = setmetatable local util = require("awful.util") +local spawn = require("awful.spawn") local wbutton = require("awful.widget.button") local button = require("awful.button") @@ -23,7 +24,7 @@ function launcher.new(args) local b if args.command then - b = util.table.join(w:buttons(), button({}, 1, nil, function () util.spawn(args.command) end)) + b = util.table.join(w:buttons(), button({}, 1, nil, function () spawn(args.command) end)) elseif args.menu then b = util.table.join(w:buttons(), button({}, 1, nil, function () args.menu:toggle() end)) end diff --git a/lib/awful/widget/prompt.lua b/lib/awful/widget/prompt.lua index bbcde7b7..c8430ea6 100644 --- a/lib/awful/widget/prompt.lua +++ b/lib/awful/widget/prompt.lua @@ -9,6 +9,7 @@ local setmetatable = setmetatable local completion = require("awful.completion") local util = require("awful.util") +local spawn = require("awful.spawn") local prompt = require("awful.prompt") local widget_base = require("wibox.widget.base") local textbox = require("wibox.widget.textbox") @@ -23,7 +24,7 @@ local function run(promptbox) return prompt.run({ prompt = promptbox.prompt }, promptbox.widget, function (...) - local result = util.spawn(...) + local result = spawn(...) if type(result) == "string" then promptbox.widget:set_text(result) end diff --git a/lib/menubar/init.lua b/lib/menubar/init.lua index ff644fe0..20502948 100644 --- a/lib/menubar/init.lua +++ b/lib/menubar/init.lua @@ -11,7 +11,7 @@ -- * "Home" select the first item -- * "End" select the last -- * "Return" execute the entry --- * "C-Return" execute the command with awful.util.spawn +-- * "C-Return" execute the command with awful.spawn -- * "C-M-Return" execute the command in a terminal -- -- @author Alexander Yakushev <yakushev.alex@gmail.com> @@ -112,7 +112,7 @@ local function perform_action(o) current_item = 1 return true, "", new_prompt elseif shownitems[current_item].cmdline then - awful.util.spawn(shownitems[current_item].cmdline) + awful.spawn(shownitems[current_item].cmdline) -- Let awful.prompt execute dummy exec_callback and -- done_callback to stop the keygrabber properly. return false diff --git a/tests/test-focus.lua b/tests/test-focus.lua index 4aad162e..b8c11ecd 100644 --- a/tests/test-focus.lua +++ b/tests/test-focus.lua @@ -17,7 +17,7 @@ local steps = { -- border_color should get applied via focus signal for first client on tag. function(count) if count == 1 then - awful.util.spawn("xterm") + awful.spawn("xterm") else local c = client.get()[1] if c then @@ -30,7 +30,7 @@ local steps = { -- border_color should get applied via focus signal for second client on tag. function(count) if count == 1 then - awful.util.spawn("xterm") + awful.spawn("xterm") else if #client.get() == 2 then local c = client.get()[1] diff --git a/tests/test-spawn.lua b/tests/test-spawn.lua index 90ae535e..8341f653 100644 --- a/tests/test-spawn.lua +++ b/tests/test-spawn.lua @@ -1,6 +1,6 @@ --- Tests for spawn -local util = require("awful.util") +local spawn = require("awful.spawn") local spawns_done = 0 @@ -8,7 +8,7 @@ local steps = { function(count) if count == 1 then local steps_yay = 0 - util.spawn_with_line_callback("echo yay", function(line) + spawn.with_line_callback("echo yay", function(line) assert(line == "yay", "line == '" .. tostring(line) .. "'") assert(steps_yay == 0) steps_yay = steps_yay + 1 @@ -20,7 +20,7 @@ local steps = { local steps_count = 0 local err_count = 0 - util.spawn_with_line_callback({ "sh", "-c", "printf line1\\\\nline2\\\\nline3 ; echo err >&2" }, + spawn.with_line_callback({ "sh", "-c", "printf line1\\\\nline2\\\\nline3 ; echo err >&2" }, function(line) assert(steps_count < 3) steps_count = steps_count + 1 diff --git a/tests/test-urgent.lua b/tests/test-urgent.lua index 47c3497c..33b6d14b 100644 --- a/tests/test-urgent.lua +++ b/tests/test-urgent.lua @@ -33,7 +33,7 @@ local steps = { runner.add_to_default_rules({ rule = { class = "XTerm" }, properties = { tag = tags[1][2], focus = true } }) - awful.util.spawn("xterm") + awful.spawn("xterm") end if urgent_cb_done then assert(awful.tag.getproperty(tags[1][2], "urgent") == true) @@ -73,7 +73,7 @@ local steps = { runner.add_to_default_rules({ rule = { class = "XTerm" }, properties = { tag = tags[1][2], focus = true, switchtotag = true }}) - awful.util.spawn("xterm") + awful.spawn("xterm") elseif awful.tag.selectedlist()[1] == tags[1][2] then assert(urgent_cb_done) @@ -95,7 +95,7 @@ local steps = { runner.add_to_default_rules({rule = { class = "XTerm" }, properties = { tag = tags[1][2], focus = false }}) - awful.util.spawn("xterm") + awful.spawn("xterm") end if manage_cb_done then assert(client.get()[1].urgent == false)