From 0516203610a988c4bc42b6e32f9e02d57a8accdf Mon Sep 17 00:00:00 2001 From: actionless Date: Sun, 20 Sep 2015 16:42:24 +0200 Subject: [PATCH] feat(lib: awful: hotkeys_popup): widget to show awesome and third-party keybindings Closes https://github.com/awesomeWM/awesome/pull/421. --- awesomerc.lua | 4 + lib/awful/hotkeys_popup/init.lua | 16 + lib/awful/hotkeys_popup/keys/init.lua | 14 + lib/awful/hotkeys_popup/keys/vim.lua | 132 +++++++++ lib/awful/hotkeys_popup/widget.lua | 409 ++++++++++++++++++++++++++ lib/awful/menu.lua | 12 +- lib/awful/util.lua | 24 +- lib/menubar/utils.lua | 14 +- 8 files changed, 610 insertions(+), 15 deletions(-) create mode 100644 lib/awful/hotkeys_popup/init.lua create mode 100644 lib/awful/hotkeys_popup/keys/init.lua create mode 100644 lib/awful/hotkeys_popup/keys/vim.lua create mode 100644 lib/awful/hotkeys_popup/widget.lua diff --git a/awesomerc.lua b/awesomerc.lua index eed3dcbf..f5528a69 100755 --- a/awesomerc.lua +++ b/awesomerc.lua @@ -10,6 +10,7 @@ local beautiful = require("beautiful") -- Notification library local naughty = require("naughty") local menubar = require("menubar") +local hotkeys_popup = require("awful.hotkeys_popup.widget") -- {{{ Error handling -- Check if awesome encountered an error during startup and fell back to @@ -108,6 +109,7 @@ end -- {{{ Menu -- Create a laucher widget and a main menu myawesomemenu = { + { "hotkeys", function() return false, hotkeys_popup.show_help end}, { "manual", terminal .. " -e man awesome" }, { "edit config", editor_cmd .. " " .. awesome.conffile }, { "restart", awesome.restart }, @@ -227,6 +229,8 @@ root.buttons(awful.util.table.join( -- {{{ Key bindings globalkeys = awful.util.table.join( + awful.key({ modkey, }, "s", hotkeys_popup.show_help, + {description="show help", group="awesome"}), awful.key({ modkey, }, "Left", awful.tag.viewprev, {description = "view previous", group = "tag"}), awful.key({ modkey, }, "Right", awful.tag.viewnext, diff --git a/lib/awful/hotkeys_popup/init.lua b/lib/awful/hotkeys_popup/init.lua new file mode 100644 index 00000000..513dd76c --- /dev/null +++ b/lib/awful/hotkeys_popup/init.lua @@ -0,0 +1,16 @@ +--------------------------------------------------------------------------- +--- Popup widget which shows current hotkeys and their descriptions. +-- +-- @author Yauheni Kirylau <yawghen@gmail.com> +-- @copyright 2014-2015 Yauheni Kirylau +-- @release @AWESOME_VERSION@ +-- @module awful.hotkeys_popup +--------------------------------------------------------------------------- + + +local hotkeys_popup = { + widget = require("awful.hotkeys_popup.widget"), + keys = require("awful.hotkeys_popup.keys") +} +hotkeys_popup.show_help = hotkeys_popup.widget.show_help +return hotkeys_popup diff --git a/lib/awful/hotkeys_popup/keys/init.lua b/lib/awful/hotkeys_popup/keys/init.lua new file mode 100644 index 00000000..ce3411f3 --- /dev/null +++ b/lib/awful/hotkeys_popup/keys/init.lua @@ -0,0 +1,14 @@ +--------------------------------------------------------------------------- +--- Additional hotkeys for awful.hotkeys_widget +-- +-- @author Yauheni Kirylau <yawghen@gmail.com> +-- @copyright 2014-2015 Yauheni Kirylau +-- @release @AWESOME_VERSION@ +-- @module awful.hotkeys_popup.keys +--------------------------------------------------------------------------- + + +local keys = { + vim = require("awful.hotkeys_popup.keys.vim") +} +return keys diff --git a/lib/awful/hotkeys_popup/keys/vim.lua b/lib/awful/hotkeys_popup/keys/vim.lua new file mode 100644 index 00000000..43860a35 --- /dev/null +++ b/lib/awful/hotkeys_popup/keys/vim.lua @@ -0,0 +1,132 @@ +--------------------------------------------------------------------------- +--- VIM hotkeys for awful.hotkeys_widget +-- +-- @author Yauheni Kirylau <yawghen@gmail.com> +-- @copyright 2014-2015 Yauheni Kirylau +-- @release @AWESOME_VERSION@ +-- @module awful.hotkeys_popup.keys.vim +--------------------------------------------------------------------------- + +local hotkeys_popup = require("awful.hotkeys_popup.widget") + +local vim_rule = {name="vim"} +for group_name, group_data in pairs({ + vim_motion= { color="#009F00", rule=vim_rule }, + vim_command= { color="#aFaF00", rule=vim_rule }, + vim_command_insert= { color="#cF4F40", rule=vim_rule }, + vim_operator= { color="#aF6F00", rule=vim_rule }, +}) do + hotkeys_popup.group_rules[group_name] = group_data +end + + +local vim_keys = { + + vim_motion={{ + modifiers = {}, + keys = { + ['`']="goto mark", + ['0']='"hard" BOL', + ['-']="prev line", + w="next word", + e="end word", + t=". 'till", + ['[']=". misc", + [']']=". misc", + f=". find char", + [';']="repeat t/T/f/F", + ["'"]=". goto mk. BOL", + b="prev word", + n="next word", + [',']="reverse t/T/f/F", + ['/']=". find", + ['~']="toggle case", + ["#"]='prev indent', + ["$"]='EOL', + ["%"]='goto match bracket', + ["^"]='"soft" BOL', + ["*"]='next indent', + ["("]='begin sentence', + [")"]='end sentence', + ["_"]='"soft" BOL down', + ["+"]='next line', + W='next WORD', + E='end WORD', + T=". back 'till", + ['{']="begin parag.", + ['}']="end parag.", + F='. "back" find char', + G='EOF/goto line', + H='screen top', + L='screen bottom', + B='prev WORD', + N='prev (find)', + M='screen middle', + ['?']='. find(rev.)', + } + }}, + + vim_operator={{ + modifiers = {}, + keys = { + ['=']="auto format", + y="yank", + d="delete", + c="change", + ["!"]='external filter', + ['<']='unindent', + ['>']='indent', + } + }}, + + vim_command={{ + modifiers = {}, + keys = { + q=". record macro", + r=". replace char", + u="undo", + p="paste after", + g="gg: top of file, gf: open file here", + z="zt: cursor to top, zb: bottom, zz: center", + x="delete char", + v="visual mode", + m=". set mark", + ['.']="repeat command", + ["@"]='. play macro', + ["&"]='repeat :s', + Q='ex mode', + Y='yank line', + U='undo line', + P='paste before', + D='delete to EOL', + J='join lines', + K='help', + [':']='ex cmd line', + ['"']='. register spec', + ["|"]='BOL/goto col', + Z='quit and ZZ:save or ZQ:not', + X='back-delete', + V='visual lines', + } + }}, + + vim_command_insert={{ + modifiers = {}, + keys = { + i="insert mode", + o="open below", + a="append", + s="subst char", + R='replace mode', + I='insert at BOL', + O='open above', + A='append at EOL', + S='subst line', + C='change to EOL', + } + }}, +} + +hotkeys_popup.add_hotkeys(vim_keys) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/hotkeys_popup/widget.lua b/lib/awful/hotkeys_popup/widget.lua new file mode 100644 index 00000000..3785faaa --- /dev/null +++ b/lib/awful/hotkeys_popup/widget.lua @@ -0,0 +1,409 @@ +--------------------------------------------------------------------------- +--- Popup widget which shows current hotkeys and their descriptions. +-- +-- @author Yauheni Kirylau <yawghen@gmail.com> +-- @copyright 2014-2015 Yauheni Kirylau +-- @release @AWESOME_VERSION@ +-- @module awful.hotkeys_popup.widget +--------------------------------------------------------------------------- + +local capi = { + screen = screen, + client = client, + keygrabber = keygrabber, +} +local awful = require("awful") +local wibox = require("wibox") +local beautiful = require("beautiful") +local dpi = beautiful.xresources.apply_dpi +local compute_textbox_width = require("menubar").utils.compute_textbox_width + + +-- Stripped copy of this module https://github.com/copycat-killer/lain/blob/master/util/markup.lua: +local markup = {} +-- Set the font. +function markup.font(font, text) + return '' .. tostring(text) ..'' +end +-- Set the foreground. +function markup.fg(color, text) + return '' .. tostring(text) .. '' +end +-- Set the background. +function markup.bg(color, text) + return '' .. tostring(text) .. '' +end + + +local widget = { + hide_without_description = true, + title_font = "Monospace Bold 9", + description_font = "Monospace 8", + width = dpi(1200), + height = dpi(800), + border_width = beautiful.border_width or dpi(2), + modifiers_color = beautiful.bg_minimize or "#555555", + group_margin = dpi(6), + group_rules = {}, + additional_hotkeys = {}, + labels = { + Mod4="Super", + Mod1="Alt", + Escape="Esc", + Insert="Ins", + Delete="Del", + Backspace="BackSpc", + Return="Enter", + Next="PgDn", + Prior="PgUp", + ['#108']="Alt Gr", + Left='←', + Up='↑', + Right='→', + Down='↓', + ['#67']="F1", + ['#68']="F2", + ['#69']="F3", + ['#70']="F4", + ['#71']="F5", + ['#72']="F6", + ['#73']="F7", + ['#74']="F8", + ['#75']="F9", + ['#76']="F10", + ['#95']="F11", + ['#96']="F12", + ['#10']="1", + ['#11']="2", + ['#12']="3", + ['#13']="4", + ['#14']="5", + ['#15']="6", + ['#16']="7", + ['#17']="8", + ['#18']="9", + ['#19']="0", + ['#20']="-", + ['#21']="=", + Control="Ctrl" + }, +} + + +local cached_wiboxes = {{}} +local cached_awful_keys = nil +local colors_counter = {} +local colors = beautiful.xresources.get_current_theme() +local group_list = {} + + +local function get_next_color(id) + id = id or "default" + if colors_counter[id] then + colors_counter[id] = math.fmod(colors_counter[id] + 1, 15) + 1 + else + colors_counter[id] = 1 + end + return colors["color"..tostring(colors_counter[id], 15)] +end + + +local function join_plus_sort(modifiers) + if #modifiers<1 then return "none" end + table.sort(modifiers) + return table.concat(modifiers, '+') +end + + +local function add_hotkey(key, data, target) + if widget.hide_without_description and not data.description then return end + + local readable_mods = {} + for _, mod in ipairs(data.mod) do + table.insert(readable_mods, widget.labels[mod] or mod) + end + local joined_mods = join_plus_sort(readable_mods) + if joined_mods == "none" then + joined_mods = "" + else + joined_mods = markup.fg(widget.modifiers_color, joined_mods.."+") + end + + local group = data.group or "none" + group_list[group] = true + if not target[group] then target[group] = {} end + table.insert( + target[group], + {hotkey= joined_mods .. (widget.labels[key] or key), + description=data.description} + ) +end + + +local function sort_hotkeys(target) + -- @TODO: add sort by 12345qwertyasdf etc + for group, _ in pairs(group_list) do + if target[group] then + table.sort( + target[group], + function(a,b) return a.hotkey available_height_px then + local new_keys = {} + overlap_leftovers = {} + -- +1 for group title and +1 for possible hyphen (v): + local available_height_items = (available_height_px - group_label_height*2) / line_height + for i=1,#keys do + table.insert(((i max_label_width then + max_label_width = length + max_label_content = rendered_hotkey + end + joined_labels = joined_labels .. rendered_hotkey .. (i~=#_keys and "\n" or "") + end + current_column.layout:add(wibox.widget.textbox(joined_labels)) + local max_width = compute_textbox_width(wibox.widget.textbox(max_label_content), s) + + widget.group_margin + if not current_column.max_width or max_width > current_column.max_width then + current_column.max_width = max_width + end + -- +1 for group label: + current_column.height_px = (current_column.height_px or 0) + + awful.util.linecount(joined_labels)*line_height + group_label_height + if _add_new_column then + table.insert(column_layouts, current_column) + end + end + + insert_keys(keys, add_new_column) + if overlap_leftovers then + current_column = {layout=wibox.layout.fixed.vertical()} + insert_keys(overlap_leftovers, true) + end + end + + -- arrange columns into pages + local available_width_px = width + local pages = {} + local columns = wibox.layout.fixed.horizontal() + for _, item in ipairs(column_layouts) do + if item.max_width > available_width_px then + columns.widgets[#columns.widgets]['widget']:add( + group_label("PgDn - Next Page", beautiful.fg_normal) + ) + table.insert(pages, columns) + columns = wibox.layout.fixed.horizontal() + available_width_px = width - item.max_width + local old_widgets = item.layout.widgets + item.layout.widgets = {group_label("PgUp - Prev Page", beautiful.fg_normal)} + awful.util.table.merge(item.layout.widgets, old_widgets) + else + available_width_px = available_width_px - item.max_width + end + local column_margin = wibox.layout.margin() + column_margin:set_widget(item.layout) + column_margin:set_left(widget.group_margin) + columns:add(column_margin) + end + table.insert(pages, columns) + + local mywibox = wibox({ + ontop = true, + opacity = beautiful.notification_opacity or 1, + border_width = widget.border_width, + border_color = beautiful.fg_normal, + }) + mywibox:geometry({ + x = wa.x + math.floor((wa.width - width - widget.border_width*2) / 2), + y = wa.y + math.floor((wa.height - height - widget.border_width*2) / 2), + width = width, + height = height, + }) + mywibox:set_widget(pages[1]) + mywibox:buttons(awful.util.table.join( + awful.button({ }, 1, function () mywibox.visible=false end), + awful.button({ }, 3, function () mywibox.visible=false end) + )) + + local widget_obj = {} + widget_obj.current_page = 1 + widget_obj.wibox = mywibox + function widget_obj:page_next() + if self.current_page == #pages then return end + self.current_page = self.current_page + 1 + self.wibox:set_widget(pages[self.current_page]) + end + function widget_obj:page_prev() + if self.current_page == 1 then return end + self.current_page = self.current_page - 1 + self.wibox:set_widget(pages[self.current_page]) + end + function widget_obj:show() + self.wibox.visible = true + end + function widget_obj:hide() + self.wibox.visible = false + end + + return widget_obj +end + + +--- Show popup with hotkeys help. +-- @tparam[opt] client c Client. +-- @tparam[opt] screen s Screen. +function widget.show_help(c, s) + import_awful_keys() + c = c or capi.client.focus + s = s or (c and c.screen or awful.screen.focused()) + + local available_groups = {} + for group, _ in pairs(group_list) do + local need_match + for group_name, data in pairs(widget.group_rules) do + if group_name==group and data.rule then + if not c or not awful.rules.match(c, data.rule) then + need_match = true + break + end + end + end + if not need_match then table.insert(available_groups, group) end + end + + local joined_groups = join_plus_sort(available_groups) + if not cached_wiboxes[s][joined_groups] then + cached_wiboxes[s][joined_groups] = create_wibox(s, available_groups) + end + local help_wibox = cached_wiboxes[s][joined_groups] + help_wibox:show() + + return capi.keygrabber.run(function(_, key, event) + if event == "release" then return end + if key then + if key == "Next" then + help_wibox:page_next() + elseif key == "Prior" then + help_wibox:page_prev() + else + capi.keygrabber.stop() + help_wibox:hide() + end + end + end) +end + + +--- Add hotkey descriptions for third-party applications. +-- @tparam table hotkeys Table with bindings, +-- see `awful.hotkeys_popup.key.vim` as an example. +-- @tparam[opt] bool nosort Do not sort hotkeys alphabetically. +function widget.add_hotkeys(hotkeys, nosort) + for group, bindings in pairs(hotkeys) do + for _, binding in ipairs(bindings) do + local modifiers = binding.modifiers + local keys = binding.keys + for key, description in pairs(keys) do + add_hotkey(key, { + mod=modifiers, + description=description, + group=group}, + widget.additional_hotkeys + ) + end + end + end + if not nosort then + sort_hotkeys(widget.additional_hotkeys) + end +end + +return widget + +-- 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 d0efbe34..ea515e6c 100644 --- a/lib/awful/menu.lua +++ b/lib/awful/menu.lua @@ -47,12 +47,6 @@ local table_update = function (t, set) return t end -local table_merge = function (t, set) - for _, v in ipairs(set) do - table.insert(t, v) - end -end - --- Key bindings for menu navigation. -- Keys are: up, down, exec, enter, back, close. Value are table with a list of valid @@ -484,15 +478,15 @@ function menu.clients(args, item_args) c.icon } if item_args then if type(item_args) == "function" then - table_merge(cls_t[#cls_t], item_args(c)) + util.table.merge(cls_t[#cls_t], item_args(c)) else - table_merge(cls_t[#cls_t], item_args) + util.table.merge(cls_t[#cls_t], item_args) end end end args = args or {} args.items = args.items or {} - table_merge(args.items, cls_t) + util.table.merge(args.items, cls_t) local m = menu.new(args) m:show(args) diff --git a/lib/awful/util.lua b/lib/awful/util.lua index ca54037d..665953f1 100644 --- a/lib/awful/util.lua +++ b/lib/awful/util.lua @@ -385,9 +385,9 @@ end -- @param indent Number of spaces added before each wrapped line. Default: 0. -- @return The string with lines wrapped to width. function util.linewrap(text, width, indent) - local text = text or "" - local width = width or 72 - local indent = indent or 0 + text = text or "" + width = width or 72 + indent = indent or 0 local pos = 1 return text:gsub("(%s+)()(%S+)()", @@ -399,6 +399,13 @@ function util.linewrap(text, width, indent) end) end +--- Count number of lines in a string +-- @tparam string text Input string. +-- @treturn int Number of lines. +function util.linecount(text) + return select(2, text:gsub('\n', '\n')) + 1 +end + --- Get a sorted table with all integer keys from a table -- @param t the table for which the keys to get -- @return A table with keys @@ -489,6 +496,17 @@ function util.table.iterate(t, filter, start) end end + +--- Merge items from the one table to another one +-- @tparam table t the container table +-- @tparam table set the mixin table +function util.table.merge(t, set) + for _, v in ipairs(set) do + table.insert(t, v) + end +end + + -- Escape all special pattern-matching characters so that lua interprets them -- literally instead of as a character class. -- Source: http://stackoverflow.com/a/20778724/15690 diff --git a/lib/menubar/utils.lua b/lib/menubar/utils.lua index 5c9e1e08..8025fdb8 100644 --- a/lib/menubar/utils.lua +++ b/lib/menubar/utils.lua @@ -254,12 +254,20 @@ function utils.parse_dir(dir) return programs end +--- Compute textbox width. +-- @tparam wibox.widget.textbox textbox Textbox instance. +-- @treturn int Text width. +function utils.compute_textbox_width(textbox, s) + s = s or mouse.screen + local w, h = textbox:get_preferred_size(s) + return w +end + --- Compute text width. -- @tparam str text Text. -- @treturn int Text width. -function utils.compute_text_width(text) - local _, logical = wibox.widget.textbox(awful_util.escape(text))._layout:get_pixel_extents() - return logical.width +function utils.compute_text_width(text, s) + return utils.compute_textbox_width(wibox.widget.textbox(awful_util.escape(text)), s) end return utils