feat(lib: awful: hotkeys_popup): widget to show awesome and third-party keybindings

Closes https://github.com/awesomeWM/awesome/pull/421.
This commit is contained in:
actionless 2015-09-20 16:42:24 +02:00 committed by Daniel Hahler
parent ecf6b55b55
commit 0516203610
8 changed files with 610 additions and 15 deletions

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 '<span font="' .. tostring(font) .. '">' .. tostring(text) ..'</span>'
end
-- Set the foreground.
function markup.fg(color, text)
return '<span foreground="' .. tostring(color) .. '">' .. tostring(text) .. '</span>'
end
-- Set the background.
function markup.bg(color, text)
return '<span background="' .. tostring(color) .. '">' .. tostring(text) .. '</span>'
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<b.hotkey end
)
end
end
end
local function import_awful_keys()
if cached_awful_keys then
return
end
cached_awful_keys = {}
for _, data in pairs(awful.key.hotkeys) do
add_hotkey(data.key, data, cached_awful_keys)
end
sort_hotkeys(cached_awful_keys)
end
local function group_label(group, color)
local textbox = wibox.widget.textbox(
markup.font(widget.title_font,
markup.bg(
color or (widget.group_rules[group] and
widget.group_rules[group].color or get_next_color("group_title")
),
markup.fg(beautiful.bg_normal or "#000000", " "..group.." ")
)
)
)
local margin = wibox.layout.margin()
margin:set_widget(textbox)
margin:set_top(widget.group_margin)
return margin
end
local function create_wibox(s, available_groups)
local wa = capi.screen[s].workarea
local height = (widget.height < wa.height) and widget.height or
(wa.height - widget.border_width * 2)
local width = (widget.width < wa.width) and widget.width or
(wa.width - widget.border_width * 2)
-- arrange hotkey groups into columns
local line_height = beautiful.get_font_height(widget.title_font)
local group_label_height = line_height + widget.group_margin
-- -1 for possible pagination:
local max_height_px = height - group_label_height
local column_layouts = {}
for _, group in ipairs(available_groups) do
local keys = cached_awful_keys[group] or widget.additional_hotkeys[group]
local joined_descriptions = ""
for i, key in ipairs(keys) do
joined_descriptions = joined_descriptions .. key.description .. (i~=#keys and "\n" or "")
end
-- +1 for group label:
local items_height = awful.util.linecount(joined_descriptions) * line_height + group_label_height
local current_column
local available_height_px = max_height_px
local add_new_column = true
for i, column in ipairs(column_layouts) do
if ((column.height_px + items_height) < max_height_px) or
(i == #column_layouts and column.height_px < max_height_px / 2)
then
current_column = column
add_new_column = false
available_height_px = max_height_px - current_column.height_px
break
end
end
local overlap_leftovers
if items_height > 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<available_height_items) and new_keys or overlap_leftovers), keys[i])
end
keys = new_keys
table.insert(keys, {hotkey=markup.fg(widget.modifiers_color, ""), description=""})
end
if not current_column then
current_column = {layout=wibox.layout.fixed.vertical()}
end
current_column.layout:add(group_label(group))
local function insert_keys(_keys, _add_new_column)
local max_label_width = 0
local max_label_content = ""
local joined_labels = ""
for i, key in ipairs(_keys) do
local length = string.len(key.hotkey) + string.len(key.description)
local rendered_hotkey = markup.font(widget.title_font, key.hotkey.." ") ..
(markup.font(widget.description_font, key.description) or "")
if length > 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

View File

@ -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)

View File

@ -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

View File

@ -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