diff --git a/awesomerc.lua.in b/awesomerc.lua.in index ceac430e..db1f0742 100644 --- a/awesomerc.lua.in +++ b/awesomerc.lua.in @@ -8,6 +8,7 @@ require("wibox") require("beautiful") -- Notification library require("naughty") +require("menubar") -- {{{ Error handling -- Check if awesome encountered an error during startup and fell back to @@ -93,6 +94,9 @@ mymainmenu = awful.menu({ items = { { "awesome", myawesomemenu, beautiful.awesom mylauncher = awful.widget.launcher({ image = beautiful.awesome_icon, menu = mymainmenu }) + +-- Menubar configuration +menubar.utils.terminal = terminal -- Set the terminal for applications that require it -- }}} -- {{{ Wibox @@ -251,7 +255,9 @@ globalkeys = awful.util.table.join( mypromptbox[mouse.screen].widget, awful.util.eval, nil, awful.util.getdir("cache") .. "/history_eval") - end) + end), + -- Menubar + awful.key({ modkey }, "p", function() menubar.show() end) ) clientkeys = awful.util.table.join( diff --git a/lib/menubar/init.lua.in b/lib/menubar/init.lua.in new file mode 100644 index 00000000..46942bc8 --- /dev/null +++ b/lib/menubar/init.lua.in @@ -0,0 +1,269 @@ +--------------------------------------------------------------------------- +-- @author Alexander Yakushev <yakushev.alex@gmail.com> +-- @copyright 2011-2012 Alexander Yakushev +-- @release @AWESOME_VERSION@ +--------------------------------------------------------------------------- + +-- Grab environment we need +local capi = { client = client, + screen = screen } +local setmetatable = setmetatable +local pairs = pairs +local ipairs = ipairs +local table = table +local theme = require("beautiful") +local menu_gen = require("menubar.menu_gen") +local prompt = require("awful.prompt") +local awful = require("awful") +local common = require("awful.widget.common") +local tonumber = tonumber +local string = string +local mouse = mouse +local math = math +local keygrabber = keygrabber +local wibox = require("wibox") + +module("menubar") + +-- 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. +cache_entries = true + +-- When true the categories will be shown alongside application +-- entries. +show_categories = 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. +geometry = { width = nil, + height = nil, + x = nil, + y = nil } + +-- Private section +local current_item = 1 +local previous_item = nil +local current_category = nil +local shownitems = nil +local instance = { prompt = nil, + widget = nil, + wibox = nil } + +local common_args = { w = wibox.layout.fixed.horizontal(), + 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 + +-- Generate a pattern matching expression that ignores case. +-- @param s Original pattern matching expresion. +local function nocase (s) + s = string.gsub(s, "%a", + function (c) + return string.format("[%s%s]", string.lower(c), + string.upper(c)) + end) + 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. +local function label(o) + if o.focused then + local color = awful.util.color_strip_alpha(theme.fg_focus) + return colortext(o.name, color), theme.bg_focus, nil, o.icon + else + return o.name, theme.bg_normal, nil, o.icon + end +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 or o.empty then + return true + 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.util.spawn(shownitems[current_item].cmdline) + hide() + return true + end +end + +-- Update the menubar according to the command entered by user. +-- @param query The text to filter entries by. +local function menulist_update(query) + local query = query or "" + shownitems = {} + local match_inside = {} + + -- First we add entries which names match the command from the + -- beginning to the table shownitems, and the ones that contain + -- command in the middle to the table match_inside. + + -- Add the categories + if show_categories then + for _, v in pairs(menu_gen.all_categories) do + v.focused = false + if not current_category and v.use then + if string.match(v.name, nocase(query)) then + if string.match(v.name, "^" .. nocase(query)) then + table.insert(shownitems, v) + else + table.insert(match_inside, v) + end + end + end + end + end + + -- Add the applications + for i, v in ipairs(menu_entries) do + v.focused = false + if not current_category or v.category == current_category then + if string.match(v.name, nocase(query)) then + if string.match(v.name, "^" .. nocase(query)) then + table.insert(shownitems, v) + else + table.insert(match_inside, v) + end + end + end + end + + -- Now add items from match_inside to shownitems + for i, v in ipairs(match_inside) do + table.insert(shownitems, v) + end + + if #shownitems > 0 then + if current_item > #shownitems then + current_item = #shownitems + end + shownitems[current_item].focused = true + else + table.insert(shownitems, { name = "<no matches>", icon = nil, + empty = true }) + end + + common.list_update(common_args.w, nil, label, + common_args.data, + shownitems) +end + +-- Create the menubar wibox and widgets. +local function initialize() + instance.wibox = wibox({}) + instance.widget = get() + instance.wibox.ontop = true + instance.prompt = awful.widget.prompt() + local layout = wibox.layout.fixed.horizontal() + layout:add(instance.prompt) + layout:add(instance.widget) + instance.wibox:set_widget(layout) +end + +--- Refresh menubar's cache by reloading .desktop files. +function refresh() + menu_entries = menu_gen.generate() +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 == "Left" or (mod.Control and key == "j") then + current_item = math.max(current_item - 1, 1) + return true + elseif key == "Right" or (mod.Control and key == "k") then + current_item = current_item + 1 + return true + elseif key == "BackSpace" then + if comm == "" and current_category then + current_category = nil + current_item = previous_item + return true, nil, "Run app: " + end + elseif key == "Escape" then + if current_category then + current_category = nil + current_item = previous_item + return true, nil, "Run app: " + end + elseif key == "Return" then + return perform_action(shownitems[current_item]) + end + return false +end + +--- Show the menubar on the given screen. +-- @param scr Screen number. +function show(scr) + if not instance.wibox then + initialize() + elseif instance.wibox.visible then -- Menu already shown, exit + return + elseif not cache_entries then + refresh() + end + + -- Set position and size + scr = scr or mouse.screen or 1 + local scrgeom = capi.screen[scr].workarea + instance.wibox:geometry({x = geometry.x or scrgeom.x, + y = geometry.y or scrgeom.y, + height = geometry.height or theme.get_font_height() * 1.5, + width = geometry.width or scrgeom.width}) + + current_item = 1 + current_category = nil + menulist_update() + prompt.run({ prompt = "Run app: " }, instance.prompt.widget, function(s) end, + nil, awful.util.getdir("cache") .. "/history_menu", nil, hide, + menulist_update, + prompt_keypressed_callback) + instance.wibox.visible = true +end + +--- Hide the menubar. +function hide() + keygrabber.stop() + instance.wibox.visible = false +end + +--- Get a menubar wibox. +-- @return menubar wibox. +function get() + if app_folders then + menu_gen.all_menu_dirs = app_folders + end + refresh() + -- Add to each category the name of its key in all_categories + for k, v in pairs(menu_gen.all_categories) do + v.key = k + end + return common_args.w +end + +setmetatable(_M, { __call = function(_, ...) return get(...) end }) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/menu_gen.lua.in b/lib/menubar/menu_gen.lua.in new file mode 100644 index 00000000..52bdba0c --- /dev/null +++ b/lib/menubar/menu_gen.lua.in @@ -0,0 +1,119 @@ +--------------------------------------------------------------------------- +-- @author Antonio Terceiro +-- @copyright 2009, 2011-2012 Antonio Terceiro, Alexander Yakushev +-- @release @AWESOME_VERSION@ +--------------------------------------------------------------------------- + +-- Grab environment +local utils = require("menubar.utils") +local pairs = pairs +local ipairs = ipairs +local string = string +local table = table + +-- Menu generation module for menubar +module("menubar.menu_gen") + +-- Options section + +-- Specifies all directories where menubar should look for .desktop +-- files. The search is not recursive. +all_menu_dirs = { '/usr/share/applications/' } + +-- Specify the mapping of .desktop Categories section to the +-- categories in the menubar. If "use" flag is set to false then any of +-- the applications that fall only to this category will not be shown. +all_categories = { + multimedia = { app_type = "AudioVideo", name = "Multimedia", + icon_name = "applications-multimedia.png", use = true }, + development = { app_type = "Development", name = "Development", + icon_name = "applications-development.png", use = true }, + education = { app_type = "Education", name = "Education", + icon_name = "applications-science.png", use = true }, + games = { app_type = "Game", name = "Games", + icon_name = "applications-games.png", use = true }, + graphics = { app_type = "Graphics", name = "Graphics", + icon_name = "applications-graphics.png", use = true }, + office = { app_type = "Office", name = "Office", + icon_name = "applications-office.png", use = true }, + internet = { app_type = "Network", name = "Internet", + icon_name = "applications-internet.png", use = true }, + settings = { app_type = "Settings", name = "Settings", + icon_name = "applications-utilities.png", use = true }, + tools = { app_type = "System", name = "System Tools", + icon_name = "applications-system.png", use = true }, + utility = { app_type = "Utility", name = "Accessories", + icon_name = "applications-accessories.png", use = true } +} + +--- Find icons for category entries. +function lookup_category_icons() + for _, v in pairs(all_categories) do + v.icon = utils.lookup_icon(v.icon_name) + end +end + +-- Get category key name and whether it is used by its app_type. +-- @param app_type Application category as written in .desktop file. +-- @return category key name in all_categories, whether the category is used +local function get_category_name_and_usage_by_type(app_type) + for k, v in pairs(all_categories) do + if app_type == v.app_type then + return k, v.use + end + end +end + +-- Remove CR\LF newline from the end of the string. +-- @param s string to trim +local function trim(s) + if not s then return end + if string.byte(s, #s) == 13 then + return string.sub(s, 1, #s - 1) + end + return s +end + +--- Generate an array of all visible menu entries. +-- @return all menu entries. +function generate() + -- Update icons for category entries + lookup_category_icons() + + local result = {} + + for _, dir in ipairs(all_menu_dirs) do + local entries = utils.parse_dir(dir) + for _, program in ipairs(entries) do + -- Check whether to include program in the menu + if program.show and program.Name and program.cmdline then + local target_category = nil + -- Check if the program falls at least to one of the + -- usable categories. Set target_category to be the id + -- of the first category it finds. + if program.categories then + for _, category in pairs(program.categories) do + local cat_key, cat_use = + get_category_name_and_usage_by_type(category) + if cat_key and cat_use then + target_category = cat_key + break + end + end + end + if target_category then + local name = trim(program.Name) or "" + local cmdline = trim(program.cmdline) or "" + local icon = utils.lookup_icon(trim(program.icon_path)) or nil + table.insert(result, { name = name, + cmdline = cmdline, + icon = icon, + category = target_category }) + end + end + end + end + return result +end + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/utils.lua.in b/lib/menubar/utils.lua.in new file mode 100644 index 00000000..3c6aa1e7 --- /dev/null +++ b/lib/menubar/utils.lua.in @@ -0,0 +1,190 @@ +--------------------------------------------------------------------------- +-- @author Antonio Terceiro +-- @copyright 2009, 2011-2012 Antonio Terceiro, Alexander Yakushev +-- @release @AWESOME_VERSION@ +--------------------------------------------------------------------------- + +-- Grab environment +local io = io +local table = table +local ipairs = ipairs +local string = string +local awful_util = require("awful.util") +local theme = require("beautiful") + +-- Utility module for menubar +module("menubar.utils") + +-- NOTE: This icons/desktop files module was written according to the +-- following freedesktop.org specifications: +-- Icons: http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-0.11.html +-- Desktop files: http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html + +-- Options section + +-- Terminal which applications that need terminal would open in. +terminal = 'xterm' + +-- The default icon for applications that don't provide any icon in +-- their .desktop files. +default_icon = nil + +-- Name of the WM for the OnlyShownIn entry in the .desktop file. +wm_name = "awesome" + +-- Private section + +local all_icon_sizes = { + '128x128' , + '96x96', + '72x72', + '64x64', + '48x48', + '36x36', + '32x32', + '24x24', + '22x22', + '16x16' +} + +-- List of supported icon formats. Ignore SVG because Awesome doesn't +-- support it. +local icon_formats = { "png", "xpm" } + +-- Check whether the icon format is supported. +-- @param icon_file Filename of the icon. +-- @return true if format is supported, false otherwise. +local function is_format_supported(icon_file) + for _, f in ipairs(icon_formats) do + if icon_file:match('%.' .. f) then + return true + end + end + return false +end + +--- Lookup an icon in different folders of the filesystem. +-- @param icon_file Short or full name of the icon. +-- @return full name of the icon. +function lookup_icon(icon_file) + if not icon_file or icon_file == "" then + return default_icon + end + + if icon_file:sub(1, 1) == '/' and is_format_supported(icon_file) then + -- If the path to the icon is absolute and its format is + -- supported, do not perform a lookup. + return icon_file + else + local icon_path = {} + local icon_theme_paths = {} + local icon_theme = theme.icon_theme + if icon_theme then + table.insert(icon_theme_paths, '/usr/share/icons/' .. icon_theme .. '/') + -- TODO also look in parent icon themes, as in freedesktop.org specification + end + table.insert(icon_theme_paths, '/usr/share/icons/hicolor/') -- fallback theme + + for i, icon_theme_directory in ipairs(icon_theme_paths) do + for j, size in ipairs(all_icon_sizes) do + table.insert(icon_path, icon_theme_directory .. size .. '/apps/') + table.insert(icon_path, icon_theme_directory .. size .. '/actions/') + table.insert(icon_path, icon_theme_directory .. size .. '/devices/') + table.insert(icon_path, icon_theme_directory .. size .. '/places/') + table.insert(icon_path, icon_theme_directory .. size .. '/categories/') + table.insert(icon_path, icon_theme_directory .. size .. '/status/') + end + end + -- lowest priority fallbacks + table.insert(icon_path, '/usr/share/pixmaps/') + table.insert(icon_path, '/usr/share/icons/') + + for i, directory in ipairs(icon_path) do + if is_format_supported(icon_file) and awful_util.file_readable(directory .. icon_file) then + return directory .. icon_file + else + -- Icon is probably specified without path and format, + -- like 'firefox'. Try to add supported extensions to + -- it and see if such file exists. + for _, format in ipairs(icon_formats) do + local possible_file = directory .. icon_file .. "." .. format + if awful_util.file_readable(possible_file) then + return possible_file + end + end + end + end + return default_icon + end +end + +--- Parse a .desktop file. +-- @param file The .desktop file. +-- @return A table with file entries. +function parse(file) + local program = { show = true, file = file } + for line in io.lines(file) do + for key, value in line:gmatch("(%w+)=(.+)") do + program[key] = value + end + end + + -- Don't show program if NoDisplay attribute is false + if program.NoDisplay and string.lower(program.NoDisplay) == "true" then + program.show = false + end + + -- Only show the program if there is no OnlyShowIn attribute + -- or if it's equal to 'awesome' + if program.OnlyShowIn ~= nil and program.OnlyShowIn ~= wm_name then + program.show = false + end + + -- Look up for a icon. + if program.Icon then + program.icon_path = lookup_icon(program.Icon) + end + + -- Split categories into a table. Categories are written in one + -- line separated by semicolon. + if program.Categories then + program.categories = {} + for category in program.Categories:gfind('[^;]+') do + table.insert(program.categories, category) + end + end + + if program.Exec then + -- Substitute Exec special codes as specified in + -- http://standards.freedesktop.org/desktop-entry-spec/1.1/ar01s06.html + local cmdline = program.Exec:gsub('%%c', program.Name) + cmdline = cmdline:gsub('%%[fuFU]', '') + cmdline = cmdline:gsub('%%k', program.file) + if program.icon_path then + cmdline = cmdline:gsub('%%i', '--icon ' .. program.icon_path) + else + cmdline = cmdline:gsub('%%i', '') + end + if program.Terminal == "true" then + cmdline = terminal .. ' -e ' .. cmdline + end + program.cmdline = cmdline + end + + return program +end + +--- Parse a directory with .desktop files +-- @param dir The directory. +-- @param icons_size, The icons sizes, optional. +-- @return A table with all .desktop entries. +function parse_dir(dir) + local programs = {} + local files = io.popen('find '.. dir ..' -maxdepth 1 -name "*.desktop"'):lines() + for file in files do + table.insert(programs, parse(file)) + end + return programs +end + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/themes/default/theme.lua.in b/themes/default/theme.lua.in index a4f2ed0b..6699acdf 100644 --- a/themes/default/theme.lua.in +++ b/themes/default/theme.lua.in @@ -91,5 +91,9 @@ theme.layout_dwindle = "@AWESOME_THEMES_PATH@/default/layouts/dwindlew.png" theme.awesome_icon = "@AWESOME_ICON_PATH@/awesome16.png" +-- Define the icon theme for application icons. If not set then the icons +-- from /usr/share/icons and /usr/share/icons/hicolor will be used. +theme.icon_theme = nil + return theme -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80