OK, then here's the "first round". I've put the whole extension into the
separate lib/menubar directory and modified existing lib/awful/prompt.lua.
Things I changed inside prompt.lua:
- introduced two more arguments to run() function - change_callback and
keypressed_callback
* keypressed_callback is a way to catch key combinations before
awful.prompt processes them. It is necessary because I want to catch
certain combinations (like Left/Right arrows o) and do different actions
for them.
Keypressed callback also acts as a way to tell awful.prompt how
to change the current command and prompt.
* change_callback should be called any time when the key combination
was pressed, no matter if it was caught by keypressed_callback or not.
It is vital because I want to filter the entries list based on user
input each time user types in a new symbol.
- moved the update() function (inside keygrabber.run) higher in the
code. Nothing important.
On 02/15/2012 10:16 PM, Uli Schlachter wrote:
So let's just plan several rounds of "Wouldn't it be better if..." and pick what
you consider right on the first one.
>From 35bc9fe1b2e8075846d081f6cddb14385ce9b7f7 Mon Sep 17 00:00:00 2001
From: Alexander Yakushev <[email protected]>
Date: Wed, 15 Feb 2012 23:10:29 +0200
Subject: [PATCH] Introduce the menubar extension
Signed-off-by: Alexander Yakushev <[email protected]>
---
lib/awful/prompt.lua.in | 51 +++++++--
lib/menubar/init.lua.in | 262 +++++++++++++++++++++++++++++++++++++++++++
lib/menubar/menu_gen.lua.in | 113 +++++++++++++++++++
lib/menubar/utils.lua.in | 154 +++++++++++++++++++++++++
4 files changed, 569 insertions(+), 11 deletions(-)
create mode 100644 lib/menubar/init.lua.in
create mode 100644 lib/menubar/menu_gen.lua.in
create mode 100644 lib/menubar/utils.lua.in
diff --git a/lib/awful/prompt.lua.in b/lib/awful/prompt.lua.in
index c398fc9..c4f646c 100644
--- a/lib/awful/prompt.lua.in
+++ b/lib/awful/prompt.lua.in
@@ -163,7 +163,9 @@ end
-- @param history_path Optional parameter: file path where the history should be saved, set nil to disable history
-- @param history_max Optional parameter: set the maximum entries in history file, 50 by default
-- @param done_callback Optional parameter: the callback function to always call without arguments, regardless of whether the prompt was cancelled.
-function run(args, textbox, exe_callback, completion_callback, history_path, history_max, done_callback)
+-- @param change_callback Optional parameter: the callback function to call with command as argument when a command was changed.
+-- @param keypressed_callback Optional parameter: the callback function to call with mod table, key and command as arguments when a key was pressed.
+function run(args, textbox, exe_callback, completion_callback, history_path, history_max, done_callback, change_callback, keypressed_callback)
local theme = beautiful.get()
if not args then args = {} end
local command = args.text or ""
@@ -204,10 +206,44 @@ function run(args, textbox, exe_callback, completion_callback, history_path, his
capi.keygrabber.run(
function (modifiers, key, event)
+ -- Update textbox
+ local function update()
+ textbox:set_font(font)
+ textbox:set_markup(prompt_text_with_cursor{
+ text = command, text_color = inv_col, cursor_color = cur_col,
+ cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
+ prompt = prettyprompt })
+ end
+
if event ~= "press" then return true end
-- Convert index array to hash table
local mod = {}
for k, v in ipairs(modifiers) do mod[v] = true end
+
+ -- Call the user specified callback. If it returns true as
+ -- the first result then return from the function. Treat the
+ -- second and third results as a new command and new prompt
+ -- to be set (if provided)
+ if keypressed_callback then
+ local user_catched, new_command, new_prompt =
+ keypressed_callback(mod, key, command)
+ if new_command or new_prompt then
+ if new_command then
+ command = new_command
+ end
+ if new_prompt then
+ prettyprompt = new_prompt
+ end
+ update()
+ end
+ if user_catched then
+ if change_callback then
+ change_callback(command)
+ end
+ return true
+ end
+ end
+
-- Get out cases
if (mod.Control and (key == "c" or key == "g"))
or (not mod.Control and key == "Escape") then
@@ -405,15 +441,6 @@ function run(args, textbox, exe_callback, completion_callback, history_path, his
selectall = nil
end
- -- Update textbox
- local function update()
- textbox:set_font(font)
- textbox:set_markup(prompt_text_with_cursor{
- text = command, text_color = inv_col, cursor_color = cur_col,
- cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
- prompt = prettyprompt })
- end
-
local success = pcall(update)
while not success do
-- TODO UGLY HACK TODO
@@ -429,7 +456,9 @@ function run(args, textbox, exe_callback, completion_callback, history_path, his
cur_pos = cur_pos - 1
success = pcall(update)
end
-
+ if change_callback then
+ change_callback(command)
+ end
return true
end)
end
diff --git a/lib/menubar/init.lua.in b/lib/menubar/init.lua.in
new file mode 100644
index 0000000..fe18246
--- /dev/null
+++ b/lib/menubar/init.lua.in
@@ -0,0 +1,262 @@
+---------------------------------------------------------------------------
+-- @author Alexander Yakushev <[email protected]>
+-- @copyright 2011 Alexander Yakushev
+-- @release @AWESOME_VERSION@
+---------------------------------------------------------------------------
+
+-- Grab environment we need
+local capi = { client = client,
+ screen = screen }
+local setmetatable = setmetatable
+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 print = print
+local wibox = require("wibox")
+
+module("menubar")
+
+-- Options section
+cache_entries = true
+show_categories = true
+g = { width = nil,
+ height = 20,
+ x = nil,
+ y = nil }
+
+-- Private section
+current_item = 1
+previous_item = nil
+current_category = nil
+shownitems = nil
+
+instance = { prompt = nil,
+ widget = nil,
+ wibox = nil }
+
+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 "<span color='" .. c .. "'>" .. s .. "</span>"
+end
+
+-- Get how the menu item should be displayed.
+-- @param o The menu item.
+-- @return item name, item background color, nil, item icon.
+local function label(o)
+ if o.focused then
+ return
+ colortext(o.name, awful.util.color_strip_alpha(theme.fg_focus)) or o.name,
+ 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.cat_id then
+ current_category = o.cat_id
+ 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
+
+-- Create the menubar wibox and widgets.
+local function initialize()
+ instance.wibox = wibox({})
+ instance.widget = new()
+ 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.
+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(scr)
+ elseif instance.wibox.visible then -- Menu already shown, exit
+ return
+ elseif not cache_entries then
+ refresh()
+ end
+
+ -- Set position and size
+ local scrgeom = capi.screen[scr or 1].workarea
+ local x = g.x or scrgeom.x
+ local y = g.y or scrgeom.y
+ instance.wibox.height = g.height or 20
+ instance.wibox.width = g.width or scrgeom.width
+ instance.wibox:geometry({x = x, y = y})
+
+ current_item = 1
+ current_category = nil
+ menulist_update()
+ prompt.run({ prompt = "Run app: ", bg_cursor = "#222222" }, 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
+
+-- 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
+
+--- Update the menubar according to the command inputed by user.
+-- @param query The text to filter entries by.
+function menulist_update(query)
+ local query = query or ""
+ shownitems = {}
+ local match_inside = {}
+
+ -- We add entries that match from the beginning to the table
+ -- shownitems, and those that match in the middle to the table
+ -- match_inside.
+ if show_categories then
+ for i, v in ipairs(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
+
+ 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 a new menubar object.
+-- @return menubar object.
+function new()
+ if app_folders then
+ menu_gen.all_menu_dirs = app_folders
+ end
+ refresh()
+ -- Load categories icons and add IDs to them
+ for i, v in ipairs(menu_gen.all_categories) do
+ v.cat_id = i
+ end
+ return common_args.w
+end
+
+--- Set the current system icon theme.
+-- @param theme_name The icon theme's name.
+function set_icon_theme(theme_name)
+ utils.icon_theme = theme_name
+ menu_gen.lookup_category_icons()
+end
+
+setmetatable(_M, { __call = function(_, ...) return new(...) end })
\ No newline at end of file
diff --git a/lib/menubar/menu_gen.lua.in b/lib/menubar/menu_gen.lua.in
new file mode 100644
index 0000000..620145f
--- /dev/null
+++ b/lib/menubar/menu_gen.lua.in
@@ -0,0 +1,113 @@
+-- Originally written by Antonio Terceiro
+-- https://github.com/terceiro/awesome-freedesktop
+-- Hacked by Alex Y. <[email protected]>
+
+-- Grab environment
+local utils = require("menubar.utils")
+local ipairs = ipairs
+local string = string
+local table = table
+
+-- Menu generation module for menubar
+module("menubar.menu_gen")
+
+-- Options section
+all_menu_dirs = { '/usr/share/applications/' }
+
+all_categories = {
+ { app_type = "AudioVideo", name = "Multimedia",
+ icon_name = "applications-multimedia.png", use = true },
+ { app_type = "Development", name = "Development",
+ icon_name = "applications-development.png", use = true },
+ { app_type = "Education", name = "Education",
+ icon_name = "applications-science.png", use = false },
+ { app_type = "Game", name = "Games",
+ icon_name = "applications-games.png", use = true },
+ { app_type = "Graphics", name = "Graphics",
+ icon_name = "applications-graphics.png", use = true },
+ { app_type = "Office", name = "Office",
+ icon_name = "applications-office.png", use = true },
+ { app_type = "Network", name = "Internet",
+ icon_name = "applications-internet.png", use = true },
+ { app_type = "Settings", name = "Settings",
+ icon_name = "applications-utilities.png", use = false },
+ { app_type = "System", name = "System Tools",
+ icon_name = "applications-system.png", use = true },
+ { app_type = "Utility", name = "Accessories",
+ icon_name = "applications-accessories.png", use = true }
+}
+
+--- Find icons for category entries.
+function lookup_category_icons()
+ for i, v in ipairs(all_categories) do
+ v.icon = utils.lookup_icon(v.icon_name)
+ end
+end
+
+lookup_category_icons()
+
+-- Get a category and its number by the application type.
+-- @param app_type Application category as written in .desktop file.
+-- @return category, category number in all_categories array
+local function get_category_and_number_by_type(app_type)
+ for i, v in ipairs(all_categories) do
+ if app_type == v.app_type then
+ return i, v
+ end
+ end
+ return nil
+end
+
+-- Remove non-printable characters from the string and escape single quotes.
+-- @param s The string to protect.
+-- @return the protected string.
+local function esc_q(s)
+ if s then
+ -- Remove all non-printable characters
+ return string.gsub(string.gsub(s, "'" ,"\\'"),
+ "(.)", function(c)
+ if string.byte(c, 1) > 31 then
+ return c
+ else
+ return ""
+ end
+ end)
+ else
+ return ""
+ end
+end
+
+--- Generate an array of all visible menu entries.
+-- @return all menu entries.
+function generate()
+ local result = {}
+
+ for i, dir in ipairs(all_menu_dirs) do
+ local entries = utils.parse_dir(dir) do
+ for i, program in ipairs(entries) do
+ -- check whether to include in the menu
+ if program.show and program.Name and program.cmdline then
+ local target_category = nil
+ if program.categories then
+ for _, category in ipairs(program.categories) do
+ local category_id, cat =
+ get_category_and_number_by_type(category)
+ if category_id and cat.use then
+ target_category = category_id
+ break
+ end
+ end
+ end
+ if target_category then
+ table.insert(result, { name = esc_q(program.Name) or "",
+ cmdline = esc_q(program.cmdline) or "",
+ icon = utils.lookup_icon(esc_q(program.icon_path)) or nil,
+ category = target_category })
+ end
+ end
+ end
+ end
+ end
+
+ return result
+end
diff --git a/lib/menubar/utils.lua.in b/lib/menubar/utils.lua.in
new file mode 100644
index 0000000..d8c4cea
--- /dev/null
+++ b/lib/menubar/utils.lua.in
@@ -0,0 +1,154 @@
+---------------------------------------------------------------------------
+-- @author Antonio Terceiro
+-- @copyright 2009 Antonio Terceiro
+-- @release @AWESOME_VERSION@
+---------------------------------------------------------------------------
+
+-- Grab environment
+local io = io
+local table = table
+local ipairs = ipairs
+local string = string
+local awful_util = require("awful.util")
+
+-- Utility module for menubar
+module("menubar.utils")
+
+-- Options section
+terminal = 'xterm'
+default_icon = nil
+icon_theme = nil
+
+-- Private section
+all_icon_sizes = {
+ '128x128' ,
+ '96x96',
+ '72x72',
+ '64x64',
+ '48x48',
+ '36x36',
+ '32x32',
+ '24x24',
+ '22x22',
+ '16x16'
+}
+
+icon_sizes = {}
+
+--- 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 (icon_file:find('.+%.png') or icon_file:find('.+%.xpm')) then
+ -- icons with absolute path and supported (AFAICT) formats
+ return icon_file
+ else
+ local icon_path = {}
+ local icon_theme_paths = {}
+ 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 cf spec
+
+ local isizes = {}
+ for i, sz in ipairs(all_icon_sizes) do
+ table.insert(isizes, sz)
+ end
+
+ for i, icon_theme_directory in ipairs(icon_theme_paths) do
+ for j, size in ipairs(icon_file_sizes or isizes) 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 (icon_file:find('.+%.png') or icon_file:find('.+%.xpm')) and awful_util.file_readable(directory .. icon_file) then
+ return directory .. icon_file
+ elseif awful_util.file_readable(directory .. icon_file .. '.xpm') then
+ return directory .. icon_file .. '.xpm'
+ elseif awful_util.file_readable(directory .. icon_file .. '.png') then
+ return directory .. icon_file .. '.png'
+ end
+ end
+ return default_icon
+ end
+end
+
+--- Parse a .desktop file
+-- @param file The .desktop file
+-- @param requested_icon_sizes A list of icon sizes (optional). If this list is given, it will be used as a priority list for icon sizes when looking up for icons. If you want large icons, for example, you can put '128x128' as the first item in the list.
+-- @return A table with file entries.
+function parse(file, requested_icon_sizes)
+ 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 not OnlyShowIn attribute
+ -- or if it's equal to 'awesome'
+ if program.OnlyShowIn ~= nil and program.OnlyShowIn ~= "awesome" 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.
+ if program.Categories then
+ program.categories = {}
+ for category in program.Categories:gfind('[^;]+') do
+ table.insert(program.categories, category)
+ end
+ end
+
+ if program.Exec then
+ 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
--
1.7.9.1