Skip to content

Instantly share code, notes, and snippets.

@MartinRamm
Last active July 12, 2025 05:46
Show Gist options
  • Select an option

  • Save MartinRamm/d2c9762fe87c9ed0427ff2a36cc7b74a to your computer and use it in GitHub Desktop.

Select an option

Save MartinRamm/d2c9762fe87c9ed0427ff2a36cc7b74a to your computer and use it in GitHub Desktop.
Dynamic Menu Entries for Awesome WM

Dynamic Menu Entries for Awesome WM

Features:

  • Change icon on selection / unselection of a menu element (including root entries)
  • Dynamic names and icons of submenu entries (i.e. NOT root entries). In other words, the elements you see when you open the menu are still hardcoded. But the entries of submenus ( the menus that opens if you hover over an element) are re-generated on each hover.
  • The posibility to add icons both left and right of the menu element
  • Have different icons for the submenu indicator: if not selected, if selected but not expanded and if selected and expanded
  • The posibility to have multiple, different submenu indicators for different menu elements

How can I do all this?

  • This is described in the code example. Just start reading from the --example usage` comment.
  • You will need to copy the whole file into your rc.lua file. Make sure you remove the original variable mymainmenu.
  • Tip: If you have the same logic for all icons (and you want to change the icon on selection), use a small helper function like this for arg3/4 for menu entries:
local function menuImage(imageName)
  return function(isSelected)
    local path = iconPath .. '/' .. imageName .. (isSelected and '_selected' or '') .. '.png'
    return gears.surface.load(path)
  end
end
  • You proably also want to cache the icons, instead of creating new surfaces every time:
local menuImageCache = {}
local function menuImage(imageName)
  return function(isSelected)
    local path = iconPath .. '/' .. imageName .. (isSelected and '_selected' or '') .. '.png'
    if not menuImageCache[path] then
      menuImageCache[path] = gears.surface.load(path)
    end
    return menuImageCache[path]
  end
end

Compatibility:

  • Tested with AwesomeWM Version 4.3.
  • As this is using non-public APIs, ymmv with other versions
--as seen on https://gist.github.com/MartinRamm/d2c9762fe87c9ed0427ff2a36cc7b74a
--we extend the awful menu class here
DynamicMenu = awful.menu
--there is no "super" function in LUA. So we need to keep a reference of the original method like this
local original_new_fn = DynamicMenu.new
--now override the super method. In this case this is the method that is
function DynamicMenu.new(args, parent)
--if required, set iconRight to the fallbackSubmenuIcon
if args.fallbackSubmenuIcon and args.items then
local function addIconRightArg(table)
for _, v in pairs(table) do
local cmd = v[2] or v.cmd
if type(cmd) == "table" then --has submenu
local iconRight = v[4] or v.iconRight
if not iconRight then --use fallback submenu icon if no custom one is provided
v.iconRight = args.fallbackSubmenuIcon
end
addIconRightArg(cmd) --recursive call for sub-sub-menu
end
end
end
addIconRightArg(args.items)
end
return original_new_fn(args, parent)
end
--function used to actually create the menu entries
local original_entry_fn = DynamicMenu.entry
function DynamicMenu.entry(parent, args)
local iconFn = nil
local iconRightFn = nil
local newArgs = {} --args given to original_entry
newArgs.text = args[1] or args.text or ""
if type(newArgs.text) == "function" then
newArgs.text = newArgs.text()
end
newArgs.cmd = args[2] or args.cmd
newArgs.icon = args[3] or args.icon
if type(newArgs.icon) == "function" then
iconFn = newArgs.icon
newArgs.icon = newArgs.icon(false)
end
newArgs.iconRight = args[4] or args.iconRight --the super method won't add this icon!
if type(newArgs.iconRight) == "function" then
iconRightFn = newArgs.iconRight
newArgs.iconRight = newArgs.iconRight(false)
end
newArgs.theme = args.theme
--call super method
local entry = original_entry_fn(parent, newArgs)
--add custom right icon
if newArgs.iconRight then
entry.sep = wibox.widget.imagebox()
entry.sep:set_image(newArgs.iconRight)
entry.widget:set_right(entry.sep)
end
--here, we add the functions to the menu entry to be able to access it in item_enter and item_leave
entry.____customIconFn____ = iconFn
entry.____customIconRightFn____ = iconRightFn
return entry
end
--function executed when an element is selected
local original_item_enter_fn = DynamicMenu.item_enter
function DynamicMenu:item_enter(num, opts)
local item = self.items[num] --this is the actual entry (as returned by the above function)
--lets change the icon!
if item.____customIconFn____ then
local newIcon = item.____customIconFn____(true)
item.icon:set_image(newIcon)
end
--and the right one
if item.____customIconRightFn____ then
--if you use the keyboard in the menu, the element will be selected but not expanded.
--if you use the mouse, the menu is automatically expanded
local isExpanded = type(item.cmd) == "table"
and self.active_child
and self.active_child == self.child[num]
and self.active_child.wibox.visible
local newIcon = item.____customIconRightFn____(true, isExpanded)
item.sep:set_image(newIcon)
end
--this is required for the logic in hide
item._____customIsSelected____ = true
return original_item_enter_fn(self, num, opts)
end
--function executed when an element is deselected
local original_item_leave_fn = DynamicMenu.item_leave
function DynamicMenu:item_leave(num)
local item = self.items[num]
--back to unfocused...
if item.____customIconFn____ then
local newIcon = item.____customIconFn____(false)
item.icon:set_image(newIcon)
end
if item.____customIconRightFn____ then
local newIcon = item.____customIconRightFn____(false, false)
item.sep:set_image(newIcon)
end
--this is required for the logic in hide
item._____customIsSelected____ = false
return original_item_leave_fn(self, num, opts)
end
--function executed when a submenu is opened (does other things as well)
local original_exec_fn = DynamicMenu.exec
function DynamicMenu:exec(num, opts)
local item = self.items[num]
if item.____customIconRightFn____ then
local newIcon = item.____customIconRightFn____(true, true)
item.sep:set_image(newIcon)
end
original_exec_fn(self, num, opts)
end
--function executed when a menu is closed (this includes the root menu, and also submenus)
local original_hide_fn = DynamicMenu.hide
function DynamicMenu:hide()
--unlike before, self is in this function is the (sub)menu itself.
original_hide_fn(self)
--the root menu can be simply identified because self.parent is nil.
if not self.parent or not self.parent.sel then
return
end
local item = self.parent.items[self.parent.sel]
--if the item does not contain a submenu, save some CPU cycles and return early.
if type(item.cmd) ~= "table" then
return
end
--there are two cases that we need to distinguish here: 1) the submenu is closed. 2) the whole menu is closed.
--if the whole menu is being closed, the original implementation of this function will have called `item_leave` for
--the selected item.
if item._____customIsSelected____ and item.____customIconRightFn____ then
local newIcon = item.____customIconRightFn____(true, false)
item.sep:set_image(newIcon)
end
--if the element has a submenu, remove the cached menu. this forces a re-generation of the menu on the next hovering
self.parent.child[self.parent.sel] = nil
self.parent.active_child = nil
end
--example usage: (you don't have to use functions. you can also continue providing hard-coded strings/icons)
local submenu = {
{
--arg 1 = title. can be hardcoded (a string) or dynamic as shown here
function()
--this function is executed every time the submenu is displayed, making the name of the submenu element dynamic.
--remember that os.execute freezes awesome until the command exits! So don't use it for long lookups or similar...
return "Random Number: " .. os.execute([[shuf -i 0-100 -n 1)']])
end,
-- arg 2 = action on click, in this case open the terminal
terminal,
-- [optional] arg 3 = left icon. it can be a dynamic (a function receiving a "isSelected" arguemnt that
-- is true if the user currently selected the menu entry) or sipmly a hardcoded icon
function(isSelected)
local path = iconPath .. '/test' .. (isSelected and '_selected' or '') .. '.png'
return gears.surface.load(path)
end,
--[optional] arg 4 - right icon. same as above
gears.surface.load(iconPath .. '/test/image.png')
}
}
mymainmenu = DynamicMenu({
items = {
{
function()
--while this works, the function is only executed once (when awesome is starting).
--Dynamic names on root entries haven't been implemented, as stated in the README.
--Therefore, it makes more sense to actually provide a static string as an argument here...
return "Random Number: " .. os.execute([[shuf -i 0-100 -n 1]])
end,
submenu,
function(isSelected)
--while this works, the function is only executed once and then on selecting the menu
--entry. Dynamic icons (that are re-evaluated once the menu opens) on root entries hasn't
--been implemented, as stated in the README.
local path = iconPath .. '/test' .. (isSelected and '_selected' or '') .. '.png'
return gears.surface.load(path)
end,
--the fourth argument here is the submenu indicator. if you don't want to provide one per entry, you
--can use the `fallbackSubmenuIcon` arument as shown below. The two arguments are booleans.
--The only values this function will receive are: `true,false`,`true,true` or `false,false`.
--this doesn't need to be a function - you can also provide a static image if you want.
--if you provide none at all, the default is used from your loaded theme.
function(isSelected, isExpanded)
local path = iconPath .. '/submenu' .. (isSelected and '_selected' or '').. (isExpanded and '_expanded' or '') .. '.png'
return gears.surface.load(path)
end
}
},
--optional default arg4 for all submenus (including nested).
fallbackSubmenuIcon = function(isSelected, isExpanded)
local path = iconPath .. '/submenu' .. (isSelected and '_selected' or '').. (isExpanded and '_expanded' or '') .. '.png'
return gears.surface.load(path)
end,
})
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment