2021-08-09 01:38:19 +00:00
---------------------------------------------------------------------------
--- Menubar module, which aims to provide a freedesktop menu alternative.
--
-- List of menubar keybindings:
-- ---
--
-- <table class='widget_list' border=1>
-- <tr style='font-weight: bold;'>
-- <th align='center'>Keybinding</th>
-- <th align='center'>Description</th>
-- </tr> </td></tr>
-- <tr><td><kbd>Left</kbd><kbd>C-j</kbd></td><td> select an item on the left </td></tr>
-- <tr><td><kbd>Right</kbd><kbd>C-k</kbd></td><td> select an item on the right </td></tr>
-- <tr><td><kbd>Backspace </kbd></td><td> exit the current category if we are in any </td></tr>
-- <tr><td><kbd>Escape </kbd></td><td> exit the current directory or exit menubar </td></tr>
-- <tr><td><kbd>Home </kbd></td><td> select the first item </td></tr>
-- <tr><td><kbd>End </kbd></td><td> select the last </td></tr>
-- <tr><td><kbd>Return </kbd></td><td> execute the entry </td></tr>
-- <tr><td><kbd>C-Return </kbd></td><td> execute the command with awful.spawn </td></tr>
-- <tr><td><kbd>C-M-Return </kbd></td><td> execute the command in a terminal </td></tr>
-- </table>
--
-- @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 " <span color=' " .. gcolor.ensure_pango_color ( c ) .. " '> " .. s .. " </span> "
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
2021-08-09 02:23:37 +00:00
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
2021-08-09 01:38:19 +00:00
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
}
2021-08-09 02:23:37 +00:00
default_prompt_args.textbox . forced_height = 18
2021-08-09 01:38:19 +00:00
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