--------------------------------------------------------------------------- --- A keyboard grabbing and transaction object. -- -- This module allows to grab all keyboard inputs until stopped. It is used -- to gather input data (such as in `awful.prompt`), implement VI-like keybindings -- or multi key transactions such as emulating the Alt+Tab behavior. -- -- Note that this module has been redesigned in Awesome 4.3 to be object oriented -- and stateful. The use of the older global keygrabbing API is discouraged -- going forward since it had problem with recursive keygrabbers and required -- a lot of boiler plate code to get anything done. -- -- Using keygrabber as transactions -- -------------------------------- -- -- The transactional keybindings are keybindings that start with a normal -- keybindings, but only end when a (mod)key is released. In the classic -- "Alt+Tab" transaction, first pressing "Alt+Tab" will display a popup listing -- all windows (clients). This popup will only disappear when "Alt" is released. -- -- @DOC_text_awful_keygrabber_alttab_EXAMPLE@ -- -- In that example, because `export_keybindings` is set to `true`, pressing -- `alt+tab` or `alt+shift+tab` will start the transaction even if the -- keygrabber is not (yet) running. -- -- Using keygrabber for modal keybindings (VI like) -- ------------------------------------------------ -- -- VI-like modal keybindings are triggered by a key, like `Escape`, followed by -- either a number, an adjective (or noun) and closed by a verb. For example -- `+2+t+f` could mean "focus (f) the second (2) tag (t)". -- `+2+h+t+f` would "focus (f) two (2) tags (t) to the right (h)". -- -- Here is a basic implementation of such a system. Note that the action -- functions themselves are not implemented to keep the example size and -- complexity to a minimum. The implementation is just if/elseif of all action -- and the code can be found in the normal `rc.lua` keybindings section: -- -- @DOC_text_awful_keygrabber_vimode_EXAMPLE@ -- -- Using signals -- ------------- -- -- When the keygrabber is running, it will emit signals on each event. The -- format is "key_name".."::".."pressed_or_released". For example, to attach -- a callback to `Escape` being pressed, do: -- -- mykeygrabber:connect_signal("Escape::pressed", function(self, modifiers, modset) -- print("Escape called!") -- end) -- -- The `self` argument is the keygrabber instance. The `modifiers` is a list of -- all the currently pressed modifiers and the `modset` is the same table, but -- with the modifiers as keys instead of value. It allow to save time by -- checking `if modset.Mod4 then ... end` instead of looping into the `modifiers` -- table or using `gears.table`. -- -- @author dodo -- @author Emmanuel Lepage Vallee <elv1313@gmail.com> -- @copyright 2012 dodo -- @copyright 2017 Emmanuel Lepage Vallee -- @coreclassmod awful.keygrabber --------------------------------------------------------------------------- local ipairs = ipairs local table = table local gdebug = require('gears.debug') local akey = require("awful.key") local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) local gtable = require("gears.table") local gobject = require("gears.object") local gtimer = require("gears.timer") local akeyboard = require("awful.keyboard") local glib = require("lgi").GLib local capi = { keygrabber = keygrabber, root = root, awesome = awesome } local keygrab = {} -- Private data local grabbers = {} local keygrabbing = false local keygrabber = { object = {} } -- Instead of checking for every modifiers, check the key directly. local conversion = nil --BEGIN one day create a proper API to add and remove keybindings at runtime. -- Doing it this way is horrible. -- Read the modifiers name and map their keysyms to the modkeys local function generate_conversion_map() if conversion then return conversion end local mods = capi.awesome._modifiers assert(mods) conversion = {} for mod, keysyms in pairs(mods) do for _, keysym in ipairs(keysyms) do assert(keysym.keysym) conversion[keysym.keysym] = mod end end return conversion end capi.awesome.connect_signal("xkb::map_changed" , function() conversion = nil end) local function add_root_keybindings(self, list) assert( list, "`add_root_keybindings` needs to be called with a list of keybindings" ) for _, kb in ipairs(list) do if kb.on_press then local old_press = kb.on_press kb.on_press = function(...) self:start() old_press(...) end end if kb.on_release then local old_release = kb.on_release kb.on_release = function(...) self:start() old_release(...) end end akeyboard.append_global_keybinding(kb) end end --END hack local function grabber(mod, key, event) for _, keygrabber_function in ipairs(grabbers) do -- continue if the grabber explicitly returns false if keygrabber_function(mod, key, event) ~= false then break end end end local function runner(self, modifiers, key, event) local converted = generate_conversion_map()[key] -- Stop the keygrabber with the `stop_key` if (key == self.stop_key or (converted and converted == self.stop_key)) and event == self.stop_event and self.stop_key then self:stop(key, modifiers) return false end -- Stop when only a subset of keys are allowed and it isn't one of them. if self._private.allowed_keys and not self._private.allowed_keys[key] then self:stop(key, modifiers) return false end -- Support multiple stop keys if type(self.stop_key) == "table" and event == self.stop_event then for _, k in ipairs(self.stop_key) do if k == key then self:stop(k, modifiers) return false end end end local is_modifier = converted ~= nil -- Reset the inactivity timer on each events. if self._private.timer and self._private.timer.started then self._private.timer:again() end -- Lua strings are bytes, to handle UTF-8, use GLib local seq_len = glib.utf8_strlen(self.sequence, -1) -- Record the key sequence if key == "BackSpace" and seq_len > 0 and event == "release" then self.sequence = glib.utf8_substring(self.sequence, 0, seq_len - 1) elseif glib.utf8_strlen(key, -1) == 1 and event == "release" then self.sequence = self.sequence..key end -- Convert index array to hash table local mod = {} for _, v in ipairs(modifiers) do mod[v] = true end -- Emit some signals so the user can connect to a single type of event. self:emit_signal(key.."::"..event, modifiers, mod) local filtered_modifiers = {} -- User defined cases if self._private.keybindings[key] and event == "press" then -- Remove caps and num lock for _, m in ipairs(modifiers) do if not gtable.hasitem(akey.ignore_modifiers, m) then table.insert(filtered_modifiers, m) end end for _,v in ipairs(self._private.keybindings[key]) do if #filtered_modifiers == #v.modifiers then local match = true for _,v2 in ipairs(v.modifiers) do match = match and mod[v2] end if match and v.on_press then v.on_press(self) if self.mask_event_callback ~= false then return end end end end end -- Do not call the callbacks on modkeys if is_modifier and self.mask_modkeys then return end if self.keypressed_callback and event == "press" then self.keypressed_callback(self, modifiers, key, event) elseif self.keyreleased_callback and event == "release" then self.keyreleased_callback(self, modifiers, key, event) end end --- Stop grabbing the keyboard for the provided callback. -- -- When no callback is given, the last grabber gets removed (last one added to -- the stack). -- -- @param[opt] g The key grabber that must be removed. -- @deprecated awful.keygrabber.stop function keygrab.stop(g) -- If `run` is used directly and stop() is called with `g==nil`, get the -- first keygrabber. g = g or keygrab.current_instance and keygrab.current_instance.grabber or grabbers[#grabbers] for i, v in ipairs(grabbers) do if v == g then table.remove(grabbers, i) break end end if g and keygrab.current_instance and keygrab.current_instance.grabber == g then keygrab.current_instance = nil end -- Stop the global key grabber if the last grabber disappears from stack. if #grabbers == 0 then keygrabbing = false capi.keygrabber.stop() end end --- The keygrabber timeout. -- -- @DOC_text_awful_keygrabber_timeout_EXAMPLE@ -- -- @property timeout -- @param number -- @see gears.timer -- @see timeout_callback --- The key on which the keygrabber listen to terminate itself. -- -- When this is set, the running keygrabber will quit when [one of] the stop -- key event occurs. -- -- By default, the event is `press`. It is common for use case like the -- `awful.prompt` where `return` (enter) will terminate the keygrabbing. Using -- `release` as an event is more appropriate when the keygrabber is tied to a -- modifier key. For example, an Alt+Tab implementation stops when `mod1` (Alt) -- is released. -- -- It can also be a table containing many keys (as values). -- -- @DOC_text_awful_keygrabber_stop_keys_EXAMPLE@ -- -- @DOC_text_awful_keygrabber_stop_key_EXAMPLE@ -- -- Please note that modkeys are not accepted as `stop_key`s. You have to use -- their corresponding key names such as `Control_L`. -- -- @property stop_key -- @param[opt=nil] string|table stop_key -- @see stop_event --- The event on which the keygrabbing will be terminated. -- -- the valid values are: -- -- * "press" (default) -- * "release" -- -- @property stop_event -- @param string -- @see stop_key --- Whether or not to execute the key press/release callbacks when keybindings are called. -- -- When this property is set to false, the `keyreleased_callback` and -- `keypressed_callback` callbacks will be executed even when the event was -- consumed by a `keybinding`. -- -- By default, keybindings block those callbacks from being executed. -- -- @property mask_event_callback -- @param[opt=true] boolean -- @see keybindings -- @see keyreleased_callback -- @see keypressed_callback --- Do not call the callbacks on modifier keys (like `Control` or `Mod4`) events. -- @property mask_modkeys -- @param[opt=false] boolean -- @see mask_event_callback --- Export all keygrabber keybindings as root (global) keybindings. -- -- When this is enabled, calling all of the keygrabber object `keybinding`s will -- will create root `awful.key` and will automatically starts the grabbing. -- -- Use this with caution. If many keygrabber or "real" root keybindings are set -- on the same key combination, they are all executed and there is almost no -- safe way to undo that. Make sure the `keygrabber` that use this option -- have a single instance. -- -- @property export_keybindings -- @param[opt=false] boolean --- The root (global) keybinding to start this keygrabber. -- -- Instead of adding an entry to `root.keys` or `rc.lua` `globalkeys` section, -- this property can be used to take care of everything. This way, it becomes -- easier to package modules using keygrabbers. -- -- @DOC_text_awful_keygrabber_root_keybindings_EXAMPLE@ -- -- @property root_keybindings -- @param table -- @see export_keybindings -- @see keybindings --- The keybindings associated with this keygrabber. -- -- This property contains a table of `awful.key` objects. -- -- @property keybindings -- @param table -- @see export_keybindings -- @see root_keybindings -- @see awful.key --- If any key is pressed that is not in this list, the keygrabber is stopped. -- -- When defined, this property allows to define an implicit way to release the -- keygrabber. It helps save some boilerplate code in the handler callbacks. -- -- It is useful when a transaction only handle a limited number of keys. If -- a key unhandled by the transaction is triggered, the transaction is -- canceled. -- -- @DOC_text_awful_keygrabber_allowed_keys_EXAMPLE@ -- -- @property allowed_keys -- @tparam[opt=nil] table|nil allowed_keys The list of keys. --- The sequence of keys recorded since the start of the keygrabber. -- -- In this example, the `stop_callback` is used to retrieve the final key -- sequence. -- -- Please note that backspace will remove elements from the sequence and -- named keys and modifiers are ignored. -- -- @DOC_text_awful_keygrabber_autostart_EXAMPLE@ -- -- @property sequence -- @param string -- --- The current (running) instance of this keygrabber. -- -- The keygrabber instance is created when the keygrabber starts. It is an object -- mirroring this keygrabber object, but where extra properties can be added. It -- is useful to keep some contextual data part of the current transaction without -- polluting the original object of having extra boilerplate code. -- -- @tfield keygrabber current_instance -- @emits property::current_instance --- The global signal used to track the `current_instance`. -- -- @signal property::current_instance --- If a keygrabber is currently running. -- @tfield boolean is_running --- Start the keygrabber. -- -- Note that only a single keygrabber can be started at any one time. If another -- keygrabber (or this one) is currently running. This method returns false. -- -- @method start -- @treturn boolean If the keygrabber was successfully started. -- @emits started -- @emits property::current_instance function keygrabber:start() if self.grabber or keygrab.current_instance then return false end self.current_instance = setmetatable({}, { __index = self, __newindex = function(tab, key, value) if keygrabber["set_"..key] then self[key](self, value) else rawset(tab, key, value) end end }) self.sequence = "" if self.start_callback then self.start_callback(self.current_instance) end self.grabber = keygrab.run(function(...) return runner(self, ...) end) -- Ease making keygrabber that won't hang forever if no action is taken. if self.timeout and not self._private.timer then self._private.timer = gtimer { timeout = self.timeout, single_shot = true, callback = function() if self.timeout_callback then pcall(self.timeout_callback, self) end self:stop() end } end if self._private.timer then self._private.timer:start() end keygrab.current_instance = self keygrab.emit_signal("property::current_instance", keygrab.current_instance) self:emit_signal("started") end --- Stop the keygrabber. -- -- Also stops any `timeout`. -- -- @method stop -- @emits stopped -- @emits property::current_instance function keygrabber:stop(_stop_key, _stop_mods) -- (at)function disables ldoc params keygrab.stop(self.grabber) local timer = self._private.timer if timer and timer.started then timer:stop() end if self.stop_callback then self.stop_callback( self.current_instance, _stop_key, _stop_mods, self.sequence ) end keygrab.emit_signal("property::current_instance", nil) self.grabber = nil self:emit_signal("stopped") end --- Add a keybinding to this keygrabber. -- -- Those keybindings will automatically start the keygrabbing when hit. -- -- @method add_keybinding -- @tparam awful.key key The key. -- @tparam string description.group The keybinding group function keygrabber:add_keybinding(key, _keycode, _callback, _description) local mods = not key._is_awful_key and key or nil if mods then gdebug.deprecate(":add_keybinding now takes `awful.key` objects instead" .. " of multiple parameters", {deprecated_in=5} ) key = akey { modifiers = mods, key = _keycode, description = _description, on_press = _callback } end self._private.keybindings[key.key] = self._private.keybindings[key.key] or {} table.insert(self._private.keybindings[key.key], key) if self.export_keybindings then add_root_keybindings(self, {key}) end end function keygrabber:set_root_keybindings(keys) local real_keys = {} -- Handle the pre-object-oriented input structures. for _, key in ipairs(keys) do if key._is_awful_key then table.insert(real_keys, key) else gdebug.deprecate(":set_root_keybindings now takes `awful.key` " .. " objects instead of tables", {deprecated_in=5} ) local mods, keycode, press, release, data = unpack(key) table.insert(real_keys, akey { modifiers = mods, key = keycode, description = (data or {}).description, on_press = press, on_release = release, }) end end add_root_keybindings(self, real_keys) end -- Turn into a set. function keygrabber:set_allowed_keys(keys) self._private.allowed_keys = {} for _, v in ipairs(keys) do self._private.allowed_keys[v] = true end end --TODO v5 remove this function keygrabber:set_release_event(event) -- It has never been in a release, so it can be deprecated right away gdebug.deprecate("release_event has been renamed to stop_event") self.stop_event = event end --TODO v5 remove this function keygrabber:get_release_event() return self.stop_event end --- When the keygrabber starts. -- @signal started --- When the keygrabber stops. -- @signal stopped --- A function called when a keygrabber starts. -- @callback start_callback -- @tparam keygrabber keygrabber The keygrabber. --- The callback when the keygrabbing stops. -- -- @usage local function my_done_cb(self, stop_key, stop_mods, sequence) -- -- do something -- end -- @tparam table self The current transaction object. -- @tparam string stop_key The key(s) that stop the keygrabbing (if any) -- @tparam table stop_mods The modifiers key (if any) -- @tparam string sequence The recorded key sequence. -- @callback stop_callback -- @see sequence -- @see stop --- The function called when the keygrabber stops because of a `timeout`. -- -- Note that the `stop_callback` is also called when the keygrabbers timeout. -- -- @callback timeout_callback -- @see timeout --- The callback function to call with mod table, key and command as arguments -- when a key was pressed. -- -- @callback keypressed_callback -- @tparam table self The current transaction object. -- @tparam table mod The current modifiers (like "Control" or "Shift"). -- @tparam string key The key name. -- @tparam string event The event ("press" or "release"). -- @usage local function my_keypressed_cb(self, mod, key, command) -- -- do something -- end --- The callback function to call with mod table, key and command as arguments -- when a key was released. -- @usage local function my_keyreleased_cb(self, mod, key, command) -- -- do something -- end -- @callback keyreleased_callback -- @tparam table self The current transaction object. -- @tparam table mod The current modifiers (like "Control" or "Shift"). -- @tparam string key The key name. -- @tparam string event The event ("press" or "release") --- A wrapper around the keygrabber to make it easier to add keybindings. -- -- This is similar to the `awful.prompt`, but without an actual widget. It can -- be used to implement custom transactions such as alt+tab or keyboard menu -- navigation. -- -- Note that the object returned can be used as a client or global keybinding -- callback without modification. Make sure to set `stop_key` and `stop_event` -- to proper values to avoid permanently locking the keyboard. -- -- @tparam table args -- @tparam[opt="press"] string args.stop_event Release event ("press" or "release") -- @tparam[opt=nil] string|table args.stop_key The key that has to be kept pressed. -- @tparam table args.keybindings All keybindings. -- @tparam[opt=-1] number args.timeout The maximum inactivity delay. -- @tparam[opt=true] boolean args.mask_event_callback Do not call `keypressed_callback` -- or `keyreleased_callback` when a hook is called. -- @tparam[opt] function args.start_callback -- @tparam[opt] function args.stop_callback -- @tparam[opt] function args.timeout_callback -- @tparam[opt] function args.keypressed_callback -- @tparam[opt] function args.keyreleased_callback -- @tparam[opt=nil] table|nil args.allowed_keys A table with all keys that -- **won't** stop the keygrabber. -- @tparam[opt] table args.root_keybindings The root (global) keybindings. -- @tparam[opt=false] boolean args.export_keybindings Create root (global) keybindings. -- @tparam[opt=false] boolean args.autostart Start the grabbing immediately -- @tparam[opt=false] boolean args.mask_modkeys Do not call the callbacks on -- modifier keys (like `Control` or `Mod4`) events. -- @constructorfct awful.keygrabber function keygrab.run_with_keybindings(args) args = args or {} local ret = gobject { enable_properties = true, enable_auto_signals = true } rawset(ret, "_private", {}) -- Do not use `rawset` or `_private` because it uses the `gears.object` -- auto signals. ret.sequence = "" gtable.crush(ret, keygrabber, true) gtable.crush(ret, args) ret._private.keybindings = {} ret.stop_event = args.stop_event or "press" -- Build the hook map for _,v in ipairs(args.keybindings or {}) do if v._is_awful_key then ret._private.keybindings[v.key] = ret._private.keybindings[v.key] or {} table.insert(ret._private.keybindings[v.key], v) elseif #v >= 3 and #v <= 4 then gdebug.deprecate("keybindings now contains `awful.key` objects" .. "instead of multiple tables", {deprecated_in=5} ) local modifiers, key, callback = unpack(v) if type(callback) == "function" then local k = akey { modifiers = modifiers, key = key, on_press = callback, } ret._private.keybindings[key] = ret._private.keybindings[key] or {} table.insert(ret._private.keybindings[key], k) else gdebug.print_warning( "The hook's 3rd parameter has to be a function. " .. gdebug.dump_return(v or {}) ) end else gdebug.print_warning( "The keybindings should be awful.key objects" .. gdebug.dump_return(v or {}) ) end end if args.export_keybindings then ret:set_root_keybindings(args.keybindings) end local mt = getmetatable(ret) -- Add syntax-sugar to call `:start()`. -- This allows keygrabbers object to be used as callbacks "functions". function mt.__call() ret:start() end if args.autostart then ret:start() end return ret end --- A lower level API to interact with the keygrabber directly. -- -- Grab keyboard input and read pressed keys, calling the least callback -- function from the stack at each keypress, until the stack is empty. -- -- Calling run with the same callback again will bring the callback -- to the top of the stack. -- -- The callback function receives three arguments: -- -- * a table containing modifiers keys -- * a string with the pressed key -- * a string with either "press" or "release" to indicate the event type. -- -- Here is the content of the modifier table: -- -- -- -- -- -- -- -- -- -- -- --
Modifier name Key nameAlternate key name
Mod4 Super_L Super_R
Control Control_L Control_R
Shift Shift_L Shift_R
Mod1 Alt_L Alt_R
-- -- A callback can return `false` to pass the events to the next -- keygrabber in the stack. -- -- @param g The key grabber callback that will get the key events until it -- will be deleted or a new grabber is added. -- @return the given callback `g`. -- @usage -- -- The following function can be bound to a key, and be used to resize a -- -- client using the keyboard. -- -- function resize(c) -- local grabber -- grabber = awful.keygrabber.run(function(mod, key, event) -- if event == "release" then return end -- -- if key == 'Up' then c:relative_move(0, 0, 0, 5) -- elseif key == 'Down' then c:relative_move(0, 0, 0, -5) -- elseif key == 'Right' then c:relative_move(0, 0, 5, 0) -- elseif key == 'Left' then c:relative_move(0, 0, -5, 0) -- else awful.keygrabber.stop(grabber) -- end -- end) -- end -- @deprecated awful.keygrabber.run function keygrab.run(g) -- Remove the grabber if it is in the stack. keygrab.stop(g) -- Record the grabber that has been added most recently. table.insert(grabbers, 1, g) -- Start the keygrabber if it is not running already. if not keygrabbing then keygrabbing = true capi.keygrabber.run(grabber) end return g end -- Implement the signal system for the keygrabber. local signals = {} --- Connect to a signal for all keygrabbers at once. -- @staticfct awful.keygrabber.connect_signal -- @tparam string name The signal name. -- @tparam function callback The callback. function keygrab.connect_signal(name, callback) signals[name] = signals[name] or {} -- Use table.insert instead of signals[name][callback] = true to make -- the execution order stable across `emit_signal`. This avoids some -- heisenbugs. table.insert(signals[name], callback) end --- Disconnect to a signal for all keygrabbers at once. -- @staticfct awful.keygrabber.disconnect_signal -- @tparam string name The signal name. -- @tparam function callback The callback. function keygrab.disconnect_signal(name, callback) signals[name] = signals[name] or {} for k, v in ipairs(signals[name]) do if v == callback then table.remove(signals[name], k) break end end end --- Emit a signal on the keygrabber module itself. -- -- Warning, usually don't use this directly, use -- `my_keygrabber:emit_signal(name, ...)`. This function works on the whole -- keygrabber module, not one of its instance. -- -- @staticfct awful.keygrabber.emit_signal -- @tparam string name The signal name. -- @param ... Other arguments for the callbacks. function keygrab.emit_signal(name, ...) signals[name] = signals[name] or {} for _, cb in ipairs(signals[name]) do cb(...) end end function keygrab.get_is_running() return keygrab.current_instance ~= nil end --@DOC_object_COMMON@ return setmetatable(keygrab, { __call = function(_, args) return keygrab.run_with_keybindings(args) end }) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80