--------------------------------------------------------------------------- --- Menubar module, which aims to provide a freedesktop menu alternative. -- -- List of menubar keybindings: -- --- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
KeybindingDescription
LeftC-j select an item on the left
RightC-k select an item on the right
Backspace exit the current category if we are in any
Escape exit the current directory or exit menubar
Home select the first item
End select the last
Return execute the entry
C-Return execute the command with awful.spawn
C-M-Return execute the command in a terminal
-- -- @author Alexander Yakushev <yakushev.alex@gmail.com> -- @copyright 2011-2012 Alexander Yakushev -- @popupmod menubar --------------------------------------------------------------------------- -- Grab environment we need local capi = { client = client, mouse = mouse, screen = screen } local gmath = require("gears.math") local awful = require("awful") local gfs = require("gears.filesystem") local common = require("awful.widget.common") local theme = require("beautiful") local wibox = require("wibox") local gcolor = require("gears.color") local gstring = require("gears.string") local gdebug = require("gears.debug") local function get_screen(s) return s and capi.screen[s] end --- Menubar normal text color. -- @beautiful beautiful.menubar_fg_normal -- @param color --- Menubar normal background color. -- @beautiful beautiful.menubar_bg_normal -- @param color --- Menubar border width. -- @beautiful beautiful.menubar_border_width -- @tparam[opt=0] number menubar_border_width --- Menubar border color. -- @beautiful beautiful.menubar_border_color -- @param color --- Menubar selected item text color. -- @beautiful beautiful.menubar_fg_focus -- @param color --- Menubar selected item background color. -- @beautiful beautiful.menubar_bg_focus -- @param color --- Menubar font. -- @beautiful beautiful.menubar_font -- @param[opt=beautiful.font] font -- menubar local menubar = { menu_entries = {} } menubar.menu_gen = require("menubar.menu_gen") menubar.utils = require("menubar.utils") local current_page = {} -- Options section --- When true the .desktop files will be reparsed only when the -- extension is initialized. Use this if menubar takes much time to -- open. -- @tfield[opt=true] boolean cache_entries menubar.cache_entries = true --- When true the categories will be shown alongside application -- entries. -- @tfield[opt=true] boolean show_categories menubar.show_categories = true --- When false will hide results if the current query is empty -- @tfield[opt=true] boolean match_empty menubar.match_empty = true --- Specifies the geometry of the menubar. This is a table with the keys -- x, y, width and height. Missing values are replaced via the screen's -- geometry. However, missing height is replaced by the font size. -- @table geometry -- @tfield number geometry.x A forced horizontal position -- @tfield number geometry.y A forced vertical position -- @tfield number geometry.width A forced width -- @tfield number geometry.height A forced height menubar.geometry = { width = nil, height = nil, x = nil, y = nil } --- Width of blank space left in the right side. -- @tfield number right_margin menubar.right_margin = theme.xresources.apply_dpi(8) --- Label used for "Next page", default "▶▶". -- @tfield[opt="▶▶"] string right_label menubar.right_label = "▶▶ " --- Label used for "Previous page", default "◀◀". -- @tfield[opt="◀◀"] string left_label menubar.left_label = "◀◀ " -- awful.widget.common.list_update adds spacing of dpi(4) between items. -- @tfield number list_spacing local list_spacing = theme.xresources.apply_dpi(4) --- Allows user to specify custom parameters for prompt.run function -- (like colors). This will merge with the default parameters, overriding affected values. -- @see awful.prompt menubar.prompt_args = {} -- Private section local current_item = 1 local previous_item = nil local current_category = nil local shownitems = nil local instance = nil local common_args = { w = wibox.layout.fixed.vertical(), data = setmetatable({}, { __mode = 'kv' }) } --- Wrap the text with the color span tag. -- @param s The text. -- @param c The desired text color. -- @return the text wrapped in a span tag. local function colortext(s, c) return "" .. s .. "" end --- Get how the menu item should be displayed. -- @param o The menu item. -- @return item name, item background color, background image, item icon, item args. local function label(o) local fg_color = theme.menubar_fg_normal or theme.menu_fg_normal or theme.fg_normal local bg_color = theme.menubar_tag_bg_normal or theme.menu_tag_bg_normal or theme.tag_bg_normal or "#00000000" -- option to set tag background color different than background color. Needed because the normal way would make tags darker than the background anyways -> was annoying if o.focused then fg_color = theme.menubar_fg_focus or theme.menu_fg_focus or theme.fg_focus bg_color = theme.menubar_bg_focus or theme.menu_bg_focus or theme.bg_focus end return colortext(gstring.xml_escape(o.name), fg_color), bg_color, nil, o.icon, o.icon and {icon_size=20} -- TODO: dirty fix end local function load_count_table() if instance.count_table then return instance.count_table end instance.count_table = {} local count_file_name = gfs.get_cache_dir() .. "/menu_count_file" local count_file = io.open (count_file_name, "r") if count_file then for line in count_file:lines() do local name, count = string.match(line, "([^;]+);([^;]+)") if name ~= nil and count ~= nil then instance.count_table[name] = count end end count_file:close() end return instance.count_table end local function write_count_table(count_table) count_table = count_table or instance.count_table local count_file_name = gfs.get_cache_dir() .. "/menu_count_file" local count_file = assert(io.open(count_file_name, "w")) for name, count in pairs(count_table) do local str = string.format("%s;%d\n", name, count) count_file:write(str) end count_file:close() end --- Perform an action for the given menu item. -- @param o The menu item. -- @return if the function processed the callback, new awful.prompt command, new awful.prompt prompt text. local function perform_action(o) if not o then return end if o.key then current_category = o.key local new_prompt = shownitems[current_item].name .. ": " previous_item = current_item current_item = 1 return true, "", new_prompt elseif shownitems[current_item].cmdline then awful.spawn(shownitems[current_item].cmdline) -- load count_table from cache file local count_table = load_count_table() -- increase count local curname = shownitems[current_item].name count_table[curname] = (count_table[curname] or 0) + 1 -- write updated count table to cache file write_count_table(count_table) -- Let awful.prompt execute dummy exec_callback and -- done_callback to stop the keygrabber properly. return false end end -- Cut item list to return only current page. -- @tparam table all_items All items list. -- @tparam str query Search query. -- @tparam number|screen scr Screen -- @return table List of items for current page. local function get_current_page(all_items, query, scr) local compute_text_width = function(text, s) return wibox.widget.textbox.get_markup_geometry(text, s, instance.font)['width'] end scr = get_screen(scr) if not instance.prompt.width then instance.prompt.width = compute_text_width(instance.prompt.prompt, scr) end if not menubar.left_label_width then menubar.left_label_width = compute_text_width(menubar.left_label, scr) end if not menubar.right_label_width then menubar.right_label_width = compute_text_width(menubar.right_label, scr) end local border_width = theme.menubar_border_width or theme.menu_border_width or 0 --local available_space = instance.geometry.width - menubar.right_margin - -- menubar.right_label_width - menubar.left_label_width - -- compute_text_width(query..' ', scr) - instance.prompt.width - border_width * 2 -- space character is added as input cursor placeholder local extra_width = menubar.left_label_width local subtracted = false local item_height = 24 local available_space = menubar.geometry.height - item_height * 2 -- TODO: dirty fix local width_sum = 0 current_page = {} for i, item in ipairs(all_items) do item.width = item.width or ( compute_text_width(label(item), scr) + (item.icon and (item_height + list_spacing) or 0) ) -- TODO: 20 = dirty fix local total_height = item_height * math.floor(item.width / menubar.geometry.width + 1) if width_sum + total_height > available_space then -- TODO: 20 = dirty fix if current_item < i then table.insert(current_page, { name = menubar.right_label, ncon = nil }) break end current_page = { { name = menubar.left_label, icon = nil }, item, } width_sum = total_height * 2 -- TODO: 20 = dirty fix else table.insert(current_page, item) width_sum = width_sum + total_height -- TODO: 20 = dirty fix end end return current_page end --- Update the menubar according to the command entered by user. -- @tparam number|screen scr Screen local function menulist_update(scr) local query = instance.query or "" shownitems = {} local pattern = gstring.query_to_pattern(query) -- All entries are added to a list that will be sorted -- according to the priority (first) and weight (second) of its -- entries. -- If categories are used in the menu, we add the entries matching -- the current query with high priority as to ensure they are -- displayed first. Afterwards the non-category entries are added. -- All entries are weighted according to the number of times they -- have been executed previously (stored in count_table). local count_table = load_count_table() local command_list = {} local PRIO_NONE = 0 local PRIO_CATEGORY_MATCH = 2 -- Add the categories if menubar.show_categories then for _, v in pairs(menubar.menu_gen.all_categories) do v.focused = false if not current_category and v.use then -- check if current query matches a category if string.match(v.name, pattern) then v.weight = 0 v.prio = PRIO_CATEGORY_MATCH -- get use count from count_table if present -- and use it as weight if string.len(pattern) > 0 and count_table[v.name] ~= nil then v.weight = tonumber(count_table[v.name]) end -- check for prefix match if string.match(v.name, "^" .. pattern) then -- increase default priority v.prio = PRIO_CATEGORY_MATCH + 1 else v.prio = PRIO_CATEGORY_MATCH end table.insert (command_list, v) end end end end -- Add the applications according to their name and cmdline local add_entry = function(entry) entry.focused = false if not current_category or entry.category == current_category then -- check if the query matches either the name or the commandline -- of some entry if string.match(entry.name, pattern) or string.match(entry.cmdline, pattern) then entry.weight = 0 entry.prio = PRIO_NONE -- get use count from count_table if present -- and use it as weight if string.len(pattern) > 0 and count_table[entry.name] ~= nil then entry.weight = tonumber(count_table[entry.name]) end -- check for prefix match if string.match(entry.name, "^" .. pattern) or string.match(entry.cmdline, "^" .. pattern) then -- increase default priority entry.prio = PRIO_NONE + 1 else entry.prio = PRIO_NONE end table.insert (command_list, entry) end end end -- Add entries if required if query ~= "" or menubar.match_empty then for _, v in ipairs(menubar.menu_entries) do add_entry(v) end end local function compare_counts(a, b) if a.prio == b.prio then return a.weight > b.weight end return a.prio > b.prio end -- sort command_list by weight (highest first) table.sort(command_list, compare_counts) -- copy into showitems shownitems = command_list if #shownitems > 0 then -- Insert a run item value as the last choice table.insert(shownitems, { name = "Exec: " .. query, cmdline = query, icon = nil }) if current_item > #shownitems then current_item = #shownitems end shownitems[current_item].focused = true else table.insert(shownitems, { name = "", cmdline = query, icon = nil }) end common.list_update(common_args.w, nil, label, common_args.data, get_current_page(shownitems, query, scr)) end --- Refresh menubar's cache by reloading .desktop files. -- @tparam[opt] screen scr Screen. -- @staticfct menubar.refresh function menubar.refresh(scr) scr = get_screen(scr or awful.screen.focused() or 1) menubar.menu_gen.generate(function(entries) menubar.menu_entries = entries if instance then menulist_update(scr) end end) end --- Awful.prompt keypressed callback to be used when the user presses a key. -- @param mod Table of key combination modifiers (Control, Shift). -- @param key The key that was pressed. -- @param comm The current command in the prompt. -- @return if the function processed the callback, new awful.prompt command, new awful.prompt prompt text. local function prompt_keypressed_callback(mod, key, comm) if key == "Up" or (mod.Control and key == "j") then current_item = math.max(current_item - 1, 1) return true elseif key == "Down" or (mod.Control and key == "k") then current_item = current_item + 1 return true elseif key == "Left" or key == "Right" then local tmp_sum = 0 local index = nil local index_gen = function(tbl) local idx = {} for k,v in pairs(tbl) do idx[v] = k end return idx end if current_page[1]["name"] == menubar.left_label then tmp_sum = 1 if key == "Left" then if not index then index = index_gen(current_page) end tmp_sum = #current_page - index[shownitems[current_item]] end elseif key == "Left" then tmp_sum = #current_page - current_item end tmp_sum = tmp_sum + 1 if current_page[#current_page]["name"] == menubar.right_label and key == "Right" then if not index then index = index_gen(current_page) end tmp_sum = index[shownitems[current_item]] end current_item = current_item + (#current_page - tmp_sum) * (key == "Right" and 1 or -1) return true elseif key == "BackSpace" then if comm == "" and current_category then current_category = nil current_item = previous_item return true, nil, "Run: " end elseif key == "Escape" then if current_category then current_category = nil current_item = previous_item return true, nil, "Run: " end elseif key == "Home" then current_item = 1 return true elseif key == "End" then current_item = #shownitems return true elseif key == "Return" or key == "KP_Enter" then if mod.Control then current_item = #shownitems if mod.Mod1 then -- add a terminal to the cmdline shownitems[current_item].cmdline = menubar.utils.terminal .. " -e " .. shownitems[current_item].cmdline end end return perform_action(shownitems[current_item]) end return false end --- Show the menubar on the given screen. -- @param[opt] scr Screen. -- @staticfct menubar.show function menubar.show(scr) scr = get_screen(scr or awful.screen.focused() or 1) local fg_color = theme.menubar_fg_normal or theme.menu_fg_normal or theme.fg_normal local bg_color = theme.menubar_bg_normal or theme.menu_bg_normal or theme.bg_normal local border_width = theme.menubar_border_width or theme.menu_border_width or 0 local border_color = theme.menubar_border_color or theme.menu_border_color local font = theme.menubar_font or theme.font or "Monospace 10" if not instance then -- Add to each category the name of its key in all_categories for k, v in pairs(menubar.menu_gen.all_categories) do v.key = k end if menubar.cache_entries then menubar.refresh(scr) end instance = { wibox = wibox{ ontop = true, bg = bg_color, fg = fg_color, border_width = border_width, border_color = border_color, font = font, }, widget = common_args.w, prompt = awful.widget.prompt(), query = nil, count_table = nil, font = font, } local layout = wibox.layout.fixed.vertical() layout:add(instance.prompt) layout:add(instance.widget) instance.wibox:set_widget(layout) end if instance.wibox.visible then -- Menu already shown, exit return elseif not menubar.cache_entries then menubar.refresh(scr) end -- Set position and size local scrgeom = scr.workarea local geometry = menubar.geometry instance.geometry = {x = geometry.x or scrgeom.x, y = geometry.y or scrgeom.y, height = geometry.height or gmath.round(theme.get_font_height(font) * 1.5), width = (geometry.width or scrgeom.width) - border_width * 2} instance.wibox:geometry(instance.geometry) current_item = 1 current_category = nil menulist_update(scr) local default_prompt_args = { prompt = "Run: ", textbox = instance.prompt.widget, completion_callback = awful.completion.shell, history_path = gfs.get_cache_dir() .. "/history_menu", done_callback = menubar.hide, changed_callback = function(query) instance.query = query menulist_update(scr) end, keypressed_callback = prompt_keypressed_callback } default_prompt_args.textbox.forced_height = 18 awful.prompt.run(setmetatable(menubar.prompt_args, {__index=default_prompt_args})) instance.wibox.visible = true end --- Hide the menubar. -- @staticfct menubar.hide function menubar.hide() if instance then instance.wibox.visible = false instance.query = nil end end --- Get a menubar wibox. -- @tparam[opt] screen scr Screen. -- @return menubar wibox. -- @deprecated get function menubar.get(scr) gdebug.deprecate("Use menubar.show() instead", { deprecated_in = 5 }) menubar.refresh(scr) -- Add to each category the name of its key in all_categories for k, v in pairs(menubar.menu_gen.all_categories) do v.key = k end return common_args.w end local mt = {} function mt.__call(_, ...) return menubar.get(...) end return setmetatable(menubar, mt) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80