diff options
author | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
---|---|---|
committer | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
commit | 22d656903563f75678f3634964731ccf93355dfd (patch) | |
tree | e3cb6279d95c9764093072d5e946566ea6533799 /lib/menubar |
Init commit
Diffstat (limited to 'lib/menubar')
-rw-r--r-- | lib/menubar/icon_theme.lua | 251 | ||||
-rw-r--r-- | lib/menubar/index_theme.lua | 164 | ||||
-rw-r--r-- | lib/menubar/init.lua | 480 | ||||
-rw-r--r-- | lib/menubar/menu_gen.lua | 141 | ||||
-rw-r--r-- | lib/menubar/utils.lua | 316 |
5 files changed, 1352 insertions, 0 deletions
diff --git a/lib/menubar/icon_theme.lua b/lib/menubar/icon_theme.lua new file mode 100644 index 0000000..f76252f --- /dev/null +++ b/lib/menubar/icon_theme.lua @@ -0,0 +1,251 @@ +--------------------------------------------------------------------------- +--- Class module for icon lookup for menubar +-- +-- @author Kazunobu Kuriyama +-- @copyright 2015 Kazunobu Kuriyama +-- @classmod menubar.icon_theme +--------------------------------------------------------------------------- + +-- This implementation is based on the specifications: +-- Icon Theme Specification 0.12 +-- http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-0.12.html + +local beautiful = require("beautiful") +local awful_util = require("awful.util") +local GLib = require("lgi").GLib +local index_theme = require("menubar.index_theme") + +local ipairs = ipairs +local setmetatable = setmetatable +local string = string +local table = table +local math = math + +local get_pragmatic_base_directories = function() + local dirs = {} + + local dir = GLib.build_filenamev({GLib.get_home_dir(), ".icons"}) + if awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + + dir = GLib.build_filenamev({GLib.get_user_data_dir(), "icons"}) + if awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + + for _, v in ipairs(GLib.get_system_data_dirs()) do + dir = GLib.build_filenamev({v, "icons"}) + if awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + end + + local need_usr_share_pixmaps = true + for _, v in ipairs(GLib.get_system_data_dirs()) do + dir = GLib.build_filenamev({v, "pixmaps"}) + if awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + if dir == "/usr/share/pixmaps" then + need_usr_share_pixmaps = false + end + end + + dir = "/usr/share/pixmaps" + if need_usr_share_pixmaps and awful_util.dir_readable(dir) then + table.insert(dirs, dir) + end + + return dirs +end + +local get_default_icon_theme_name = function() + local icon_theme_names = { "Adwaita", "gnome", "hicolor" } + for _, dir in ipairs(get_pragmatic_base_directories()) do + for _, icon_theme_name in ipairs(icon_theme_names) do + local filename = string.format("%s/%s/index.theme", dir, icon_theme_name) + if awful_util.file_readable(filename) then + return icon_theme_name + end + end + end + return nil +end + +local icon_theme = { mt = {} } + +local index_theme_cache = {} + +--- Class constructor of `icon_theme` +-- @tparam string icon_theme_name Internal name of icon theme +-- @tparam table base_directories Paths used for lookup +-- @treturn table An instance of the class `icon_theme` +icon_theme.new = function(icon_theme_name, base_directories) + icon_theme_name = icon_theme_name or beautiful.icon_theme or get_default_icon_theme_name() + base_directories = base_directories or get_pragmatic_base_directories() + + local self = {} + self.icon_theme_name = icon_theme_name + self.base_directories = base_directories + self.extensions = { "png", "svg", "xpm" } + + -- Instantiate index_theme (cached). + if not index_theme_cache[self.icon_theme_name] then + index_theme_cache[self.icon_theme_name] = {} + end + local cache_key = table.concat(self.base_directories, ':') + if not index_theme_cache[self.icon_theme_name][cache_key] then + index_theme_cache[self.icon_theme_name][cache_key] = index_theme( + self.icon_theme_name, + self.base_directories) + end + self.index_theme = index_theme_cache[self.icon_theme_name][cache_key] + + return setmetatable(self, { __index = icon_theme }) +end + +local directory_matches_size = function(self, subdirectory, icon_size) + local kind, size, min_size, max_size, threshold = self.index_theme:get_per_directory_keys(subdirectory) + + if kind == "Fixed" then + return icon_size == size + elseif kind == "Scalable" then + return icon_size >= min_size and icon_size <= max_size + elseif kind == "Threshold" then + return icon_size >= size - threshold and icon_size <= size + threshold + end + + return false +end + +local directory_size_distance = function(self, subdirectory, icon_size) + local kind, size, min_size, max_size, threshold = self.index_theme:get_per_directory_keys(subdirectory) + + if kind == "Fixed" then + return math.abs(icon_size - size) + elseif kind == "Scalable" then + if icon_size < min_size then + return min_size - icon_size + elseif icon_size > max_size then + return icon_size - max_size + end + return 0 + elseif kind == "Threshold" then + if icon_size < size - threshold then + return min_size - icon_size + elseif icon_size > size + threshold then + return icon_size - max_size + end + return 0 + end + + return 0xffffffff -- Any large number will do. +end + +local lookup_icon = function(self, icon_name, icon_size) + local checked_already = {} + for _, subdir in ipairs(self.index_theme:get_subdirectories()) do + for _, basedir in ipairs(self.base_directories) do + for _, ext in ipairs(self.extensions) do + if directory_matches_size(self, subdir, icon_size) then + local filename = string.format("%s/%s/%s/%s.%s", + basedir, self.icon_theme_name, subdir, + icon_name, ext) + if awful_util.file_readable(filename) then + return filename + else + checked_already[filename] = true + end + end + end + end + end + + local minimal_size = 0xffffffff -- Any large number will do. + local closest_filename = nil + for _, subdir in ipairs(self.index_theme:get_subdirectories()) do + local dist = directory_size_distance(self, subdir, icon_size) + if dist < minimal_size then + for _, basedir in ipairs(self.base_directories) do + for _, ext in ipairs(self.extensions) do + local filename = string.format("%s/%s/%s/%s.%s", + basedir, self.icon_theme_name, subdir, + icon_name, ext) + if not checked_already[filename] then + if awful_util.file_readable(filename) then + closest_filename = filename + minimal_size = dist + end + end + end + end + end + end + return closest_filename +end + +local find_icon_path_helper -- Gets called recursively. +find_icon_path_helper = function(self, icon_name, icon_size) + local filename = lookup_icon(self, icon_name, icon_size) + if filename then + return filename + end + + for _, parent in ipairs(self.index_theme:get_inherits()) do + local parent_icon_theme = icon_theme(parent, self.base_directories) + filename = find_icon_path_helper(parent_icon_theme, icon_name, icon_size) + if filename then + return filename + end + end + + return nil +end + +local lookup_fallback_icon = function(self, icon_name) + for _, dir in ipairs(self.base_directories) do + for _, ext in ipairs(self.extensions) do + local filename = string.format("%s/%s.%s", + dir, + icon_name, ext) + if awful_util.file_readable(filename) then + return filename + end + end + end + return nil +end + +--- Look up an image file based on a given icon name and/or a preferable size. +-- @tparam string icon_name Icon name to be looked up +-- @tparam number icon_size Prefereable icon size +-- @treturn string Absolute path to the icon file, or nil if not found +function icon_theme:find_icon_path(icon_name, icon_size) + icon_size = icon_size or 16 + if not icon_name or icon_name == "" then + return nil + end + + local filename = find_icon_path_helper(self, icon_name, icon_size) + if filename then + return filename + end + + if self.icon_theme_name ~= "hicolor" then + filename = find_icon_path_helper(icon_theme("hicolor", self.base_directories), icon_name, icon_size) + if filename then + return filename + end + end + + return lookup_fallback_icon(self, icon_name) +end + +icon_theme.mt.__call = function(_, ...) + return icon_theme.new(...) +end + +return setmetatable(icon_theme, icon_theme.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/index_theme.lua b/lib/menubar/index_theme.lua new file mode 100644 index 0000000..633964a --- /dev/null +++ b/lib/menubar/index_theme.lua @@ -0,0 +1,164 @@ +--------------------------------------------------------------------------- +--- Class module for parsing an index.theme file +-- +-- @author Kazunobu Kuriyama +-- @copyright 2015 Kazunobu Kuriyama +-- @classmod menubar.index_theme +--------------------------------------------------------------------------- + +-- This implementation is based on the specifications: +-- Icon Theme Specification 0.12 +-- http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-0.12.html + +local ipairs = ipairs +local setmetatable = setmetatable +local string = string +local table = table +local io = io + +-- index.theme groups +local ICON_THEME = "Icon Theme" +-- index.theme keys +local DIRECTORIES = "Directories" +local INHERITS = "Inherits" +-- per-directory subkeys +local TYPE = "Type" +local SIZE = "Size" +local MINSIZE = "MinSize" +local MAXSIZE = "MaxSize" +local THRESHOLD = "Threshold" + +local index_theme = { mt = {} } + +--- Class constructor of `index_theme` +-- @tparam table cls Metatable that will be used. Should always be `index_theme.mt`. +-- @tparam string icon_theme_name Internal name of icon theme +-- @tparam table base_directories Paths used for lookup +-- @treturn table An instance of the class `index_theme` +index_theme.new = function(cls, icon_theme_name, base_directories) + local self = {} + setmetatable(self, { __index = cls }) + + -- Initialize the fields + self.icon_theme_name = icon_theme_name + self.base_directory = nil + self[DIRECTORIES] = {} + self[INHERITS] = {} + self.per_directory_keys = {} + + -- base_directory + local basedir = nil + local handler = nil + for _, dir in ipairs(base_directories) do + basedir = dir .. "/" .. self.icon_theme_name + handler = io.open(basedir .. "/index.theme", "r") + if handler then + -- Use the index.theme which is found first. + break + end + end + if not handler then + return self + end + self.base_directory = basedir + + -- Parse index.theme. + while true do + local line = handler:read() + if not line then + break + end + + local group_header = "^%[(.+)%]$" + local group = line:match(group_header) + if group then + if group == ICON_THEME then + while true do + local item = handler:read() + if not item then + break + end + if item:match(group_header) then + handler:seek("cur", -string.len(item) - 1) + break + end + + local k, v = item:match("^(%w+)=(.*)$") + if k == DIRECTORIES or k == INHERITS then + string.gsub(v, "([^,]+),?", function(match) + table.insert(self[k], match) + end) + end + end + else + -- This must be a 'per-directory keys' group + local keys = {} + + while true do + local item = handler:read() + if not item then + break + end + if item:match(group_header) then + handler:seek("cur", -string.len(item) - 1) + break + end + + local k, v = item:match("^(%w+)=(%w+)$") + if k == SIZE or k == MINSIZE or k == MAXSIZE or k == THRESHOLD then + keys[k] = tonumber(v) + elseif k == TYPE then + keys[k] = v + end + end + + -- Size is a must. Other keys are optional. + if keys[SIZE] then + -- Set unset keys to the default values. + if not keys[TYPE] then keys[TYPE] = THRESHOLD end + if not keys[MINSIZE] then keys[MINSIZE] = keys[SIZE] end + if not keys[MAXSIZE] then keys[MAXSIZE] = keys[SIZE] end + if not keys[THRESHOLD] then keys[THRESHOLD] = 2 end + + self.per_directory_keys[group] = keys + end + end + end + end + + handler:close() + + return self +end + +--- Table of the values of the `Directories` key +-- @treturn table Values of the `Directories` key +index_theme.get_subdirectories = function(self) + return self[DIRECTORIES] +end + +--- Table of the values of the `Inherits` key +-- @treturn table Values of the `Inherits` key +index_theme.get_inherits = function(self) + return self[INHERITS] +end + +--- Query (part of) per-directory keys of a given subdirectory name. +-- @tparam table subdirectory Icon theme's subdirectory +-- @treturn[1] string Value of the `Type` key +-- @treturn[2] number Value of the `Size` key +-- @treturn[3] number VAlue of the `MinSize` key +-- @treturn[4] number Value of the `MaxSize` key +-- @treturn[5] number Value of the `Threshold` key +function index_theme:get_per_directory_keys(subdirectory) + local keys = self.per_directory_keys[subdirectory] + return keys[TYPE], keys[SIZE], keys[MINSIZE], keys[MAXSIZE], keys[THRESHOLD] +end + +index_theme.mt.__call = function(cls, icon_theme_name, base_directories) + return index_theme.new(cls, icon_theme_name, base_directories) +end + +return setmetatable(index_theme, index_theme.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/init.lua b/lib/menubar/init.lua new file mode 100644 index 0000000..10ad65c --- /dev/null +++ b/lib/menubar/init.lua @@ -0,0 +1,480 @@ +--------------------------------------------------------------------------- +--- Menubar module, which aims to provide a freedesktop menu alternative +-- +-- List of menubar keybindings: +-- --- +-- +-- * "Left" | "C-j" select an item on the left +-- * "Right" | "C-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 +-- @module menubar +--------------------------------------------------------------------------- + +-- Grab environment we need +local capi = { + client = client, + mouse = mouse, + screen = screen +} +local awful = require("awful") +local common = require("awful.widget.common") +local theme = require("beautiful") +local wibox = require("wibox") + +local function get_screen(s) + return s and capi.screen[s] +end + +-- menubar +local menubar = { mt = {}, menu_entries = {} } +menubar.menu_gen = require("menubar.menu_gen") +menubar.utils = require("menubar.utils") +local compute_text_width = menubar.utils.compute_text_width + +-- 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 + +--- 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 three times a margin of dpi(4) +-- for each item: +-- @tfield number list_interspace +local list_interspace = theme.xresources.apply_dpi(4) * 3 + +--- Allows user to specify custom parameters for prompt.run function +-- (like colors). +-- @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 = { 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 "<span color='" .. awful.util.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. +local function label(o) + if o.focused then + return colortext(o.name, (theme.menu_fg_focus or theme.fg_focus)), (theme.menu_bg_focus or theme.bg_focus), nil, o.icon + else + return o.name, (theme.menu_bg_normal or theme.bg_normal), nil, o.icon + end +end + +local function load_count_table() + local count_file_name = awful.util.getdir("cache") .. "/menu_count_file" + + local count_file = io.open (count_file_name, "r") + local count_table = {} + + -- read weight file + if count_file then + io.input (count_file) + for line in io.lines() do + local name, count = string.match(line, "([^;]+);([^;]+)") + if name ~= nil and count ~= nil then + count_table[name] = count + end + end + end + + return count_table +end + +local function write_count_table(count_table) + local count_file_name = awful.util.getdir("cache") .. "/menu_count_file" + + local count_file = io.open (count_file_name, "w") + + if count_file then + io.output (count_file) + + for name, count in pairs(count_table) do + local str = string.format("%s;%d\n", name, count) + io.write(str) + end + io.flush() + 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 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 + if count_table[curname] ~= nil then + count_table[curname] = count_table[curname] + 1 + else + count_table[curname] = 1 + end + + -- 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) + 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 available_space = instance.geometry.width - menubar.right_margin - + menubar.right_label_width - menubar.left_label_width - + compute_text_width(query, scr) - instance.prompt.width + + local width_sum = 0 + local current_page = {} + for i, item in ipairs(all_items) do + item.width = item.width or + compute_text_width(item.name, scr) + + (item.icon and instance.geometry.height or 0) + list_interspace + if width_sum + item.width > available_space then + if current_item < i then + table.insert(current_page, { name = menubar.right_label, icon = nil }) + break + end + current_page = { { name = menubar.left_label, icon = nil }, item, } + width_sum = item.width + else + table.insert(current_page, item) + width_sum = width_sum + item.width + end + end + return current_page +end + +--- Update the menubar according to the command entered by user. +-- @tparam str query Search query. +-- @tparam number|screen scr Screen +local function menulist_update(query, scr) + query = query or "" + shownitems = {} + local pattern = awful.util.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 + for _, v in ipairs(menubar.menu_entries) do + v.focused = false + if not current_category or v.category == current_category then + + -- check if the query matches either the name or the commandline + -- of some entry + if string.match(v.name, pattern) + or string.match(v.cmdline, pattern) then + + v.weight = 0 + v.prio = PRIO_NONE + + -- 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) + or string.match(v.cmdline, "^" .. pattern) then + -- increase default priority + v.prio = PRIO_NONE + 1 + else + v.prio = PRIO_NONE + end + + table.insert (command_list, v) + end + 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 + +--- Create the menubar wibox and widgets. +-- @tparam[opt] screen scr Screen. +local function initialize(scr) + instance.wibox = wibox({}) + instance.widget = menubar.get(scr) + 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. +-- @tparam[opt] screen scr Screen. +function menubar.refresh(scr) + menubar.menu_gen.generate(function(entries) + menubar.menu_entries = entries + menulist_update(nil, scr) + 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 == "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: " + 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 scr Screen. +function menubar.show(scr) + if not instance.wibox then + initialize(scr) + elseif instance.wibox.visible then -- Menu already shown, exit + return + elseif not menubar.cache_entries then + menubar.refresh(scr) + end + + -- Set position and size + scr = scr or awful.screen.focused() or 1 + scr = get_screen(scr) + 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 awful.util.round(theme.get_font_height() * 1.5), + width = geometry.width or scrgeom.width} + instance.wibox:geometry(instance.geometry) + + current_item = 1 + current_category = nil + menulist_update(nil, scr) + + local prompt_args = menubar.prompt_args or {} + + awful.prompt.run(setmetatable({ + prompt = "Run: ", + textbox = instance.prompt.widget, + completion_callback = awful.completion.shell, + history_path = awful.util.get_cache_dir() .. "/history_menu", + done_callback = menubar.hide, + changed_callback = function(query) menulist_update(query, scr) end, + keypressed_callback = prompt_keypressed_callback + }, {__index=prompt_args})) + + instance.wibox.visible = true +end + +--- Hide the menubar. +function menubar.hide() + instance.wibox.visible = false +end + +--- Get a menubar wibox. +-- @tparam[opt] screen scr Screen. +-- @return menubar wibox. +function menubar.get(scr) + 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 + +function menubar.mt.__call(_, ...) + return menubar.get(...) +end + +return setmetatable(menubar, menubar.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/menu_gen.lua b/lib/menubar/menu_gen.lua new file mode 100644 index 0000000..ed2aa14 --- /dev/null +++ b/lib/menubar/menu_gen.lua @@ -0,0 +1,141 @@ +--------------------------------------------------------------------------- +--- Menu generation module for menubar +-- +-- @author Antonio Terceiro +-- @copyright 2009, 2011-2012 Antonio Terceiro, Alexander Yakushev +-- @module menubar.menu_gen +--------------------------------------------------------------------------- + +-- Grab environment +local utils = require("menubar.utils") +local icon_theme = require("menubar.icon_theme") +local pairs = pairs +local ipairs = ipairs +local string = string +local table = table + +local menu_gen = {} + +-- Options section + +local data_dir = os.getenv("XDG_DATA_HOME") +if not data_dir then + data_dir = os.getenv("HOME") .. '/.local/share/' +end + +--- Specifies all directories where menubar should look for .desktop +-- files. The search is recursive. +menu_gen.all_menu_dirs = { data_dir .. 'applications/', '/usr/share/applications/', '/usr/local/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. +menu_gen.all_categories = { + multimedia = { app_type = "AudioVideo", name = "Multimedia", + icon_name = "applications-multimedia", use = true }, + development = { app_type = "Development", name = "Development", + icon_name = "applications-development", use = true }, + education = { app_type = "Education", name = "Education", + icon_name = "applications-science", use = true }, + games = { app_type = "Game", name = "Games", + icon_name = "applications-games", use = true }, + graphics = { app_type = "Graphics", name = "Graphics", + icon_name = "applications-graphics", use = true }, + office = { app_type = "Office", name = "Office", + icon_name = "applications-office", use = true }, + internet = { app_type = "Network", name = "Internet", + icon_name = "applications-internet", use = true }, + settings = { app_type = "Settings", name = "Settings", + icon_name = "applications-utilities", use = true }, + tools = { app_type = "System", name = "System Tools", + icon_name = "applications-system", use = true }, + utility = { app_type = "Utility", name = "Accessories", + icon_name = "applications-accessories", use = true } +} + +--- Find icons for category entries. +function menu_gen.lookup_category_icons() + for _, v in pairs(menu_gen.all_categories) do + v.icon = icon_theme():find_icon_path(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(menu_gen.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. +-- @tparam function callback Will be fired when all menu entries were parsed +-- with the resulting list of menu entries as argument. +-- @tparam table callback.entries All menu entries. +function menu_gen.generate(callback) + -- Update icons for category entries + menu_gen.lookup_category_icons() + + local result = {} + local unique_entries = {} + local dirs_parsed = 0 + + for _, dir in ipairs(menu_gen.all_menu_dirs) do + utils.parse_dir(dir, function(entries) + entries = entries or {} + for _, entry in ipairs(entries) do + -- Check whether to include program in the menu + if entry.show and entry.Name and entry.cmdline then + local unique_key = entry.Name .. '\0' .. entry.cmdline + if not unique_entries[unique_key] then + local target_category = nil + -- Check if the program falls into at least one of the + -- usable categories. Set target_category to be the id + -- of the first category it finds. + if entry.categories then + for _, category in pairs(entry.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(entry.Name) or "" + local cmdline = trim(entry.cmdline) or "" + local icon = entry.icon_path or nil + table.insert(result, { name = name, + cmdline = cmdline, + icon = icon, + category = target_category }) + unique_entries[unique_key] = true + end + end + end + end + dirs_parsed = dirs_parsed + 1 + if dirs_parsed == #menu_gen.all_menu_dirs then + callback(result) + end + end) + end +end + +return menu_gen + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/menubar/utils.lua b/lib/menubar/utils.lua new file mode 100644 index 0000000..6f80e86 --- /dev/null +++ b/lib/menubar/utils.lua @@ -0,0 +1,316 @@ +--------------------------------------------------------------------------- +--- Utility module for menubar +-- +-- @author Antonio Terceiro +-- @copyright 2009, 2011-2012 Antonio Terceiro, Alexander Yakushev +-- @module menubar.utils +--------------------------------------------------------------------------- + +-- Grab environment +local io = io +local table = table +local ipairs = ipairs +local string = string +local screen = screen +local awful_util = require("awful.util") +local theme = require("beautiful") +local lgi = require("lgi") +local gio = lgi.Gio +local glib = lgi.GLib +local wibox = require("wibox") +local debug = require("gears.debug") +local protected_call = require("gears.protected_call") + +local 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. +utils.terminal = 'xterm' + +--- The default icon for applications that don't provide any icon in +-- their .desktop files. +local default_icon = nil + +--- Name of the WM for the OnlyShownIn entry in the .desktop file. +utils.wm_name = "awesome" + +-- Private section + +local all_icon_sizes = { + '128x128' , + '96x96', + '72x72', + '64x64', + '48x48', + '36x36', + '32x32', + '24x24', + '22x22', + '16x16' +} + +--- List of supported icon formats. +local icon_formats = { "png", "xpm", "svg" } + +--- 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 + +local icon_lookup_path = nil +--- Get a list of icon lookup paths. +-- @treturn table A list of directories, without trailing slash. +local function get_icon_lookup_path() + if not icon_lookup_path then + local add_if_readable = function(t, path) + if awful_util.dir_readable(path) then + table.insert(t, path) + end + end + icon_lookup_path = {} + local icon_theme_paths = {} + local icon_theme = theme.icon_theme + local paths = glib.get_system_data_dirs() + table.insert(paths, 1, glib.get_user_data_dir()) + table.insert(paths, 1, glib.build_filenamev({glib.get_home_dir(), + '.icons'})) + for _,dir in ipairs(paths) do + local icons_dir = glib.build_filenamev({dir, 'icons'}) + if awful_util.dir_readable(icons_dir) then + if icon_theme then + add_if_readable(icon_theme_paths, + glib.build_filenamev({icons_dir, + icon_theme})) + end + -- Fallback theme. + add_if_readable(icon_theme_paths, + glib.build_filenamev({icons_dir, 'hicolor'})) + end + end + for _, icon_theme_directory in ipairs(icon_theme_paths) do + for _, size in ipairs(all_icon_sizes) do + add_if_readable(icon_lookup_path, + glib.build_filenamev({icon_theme_directory, + size, 'apps'})) + end + end + for _,dir in ipairs(paths)do + -- lowest priority fallbacks + add_if_readable(icon_lookup_path, + glib.build_filenamev({dir, 'pixmaps'})) + add_if_readable(icon_lookup_path, + glib.build_filenamev({dir, 'icons'})) + end + end + return icon_lookup_path +end + +--- Lookup an icon in different folders of the filesystem. +-- @tparam string icon_file Short or full name of the icon. +-- @treturn string|boolean Full name of the icon, or false on failure. +function utils.lookup_icon_uncached(icon_file) + if not icon_file or icon_file == "" then + return false + 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 awful_util.file_readable(icon_file) and icon_file or nil + else + for _, directory in ipairs(get_icon_lookup_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 false + end +end + +local lookup_icon_cache = {} +--- Lookup an icon in different folders of the filesystem (cached). +-- @param icon Short or full name of the icon. +-- @return full name of the icon. +function utils.lookup_icon(icon) + if not lookup_icon_cache[icon] and lookup_icon_cache[icon] ~= false then + lookup_icon_cache[icon] = utils.lookup_icon_uncached(icon) + end + return lookup_icon_cache[icon] or default_icon +end + +--- Parse a .desktop file. +-- @param file The .desktop file. +-- @return A table with file entries. +function utils.parse_desktop_file(file) + local program = { show = true, file = file } + local desktop_entry = false + + -- Parse the .desktop file. + -- We are interested in [Desktop Entry] group only. + for line in io.lines(file) do + if line:find("^%s*#") then + -- Skip comments. + (function() end)() -- I haven't found a nice way to silence luacheck here + elseif not desktop_entry and line == "[Desktop Entry]" then + desktop_entry = true + else + if line:sub(1, 1) == "[" and line:sub(-1) == "]" then + -- A declaration of new group - stop parsing + break + end + + -- Grab the values + for key, value in line:gmatch("(%w+)%s*=%s*(.+)") do + program[key] = value + end + end + end + + -- In case [Desktop Entry] was not found + if not desktop_entry then return nil end + + -- In case the (required) 'Name' entry was not found + if not program.Name or program.Name == '' then return nil 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 utils.wm_name + if program.OnlyShowIn ~= nil and not program.OnlyShowIn:match(utils.wm_name) then + program.show = false + end + + -- Look up for a icon. + if program.Icon then + program.icon_path = utils.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:gmatch('[^;]+') 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 + if program.Name == nil then + program.Name = '['.. file:match("([^/]+)%.desktop$") ..']' + end + 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 = utils.terminal .. ' -e ' .. cmdline + end + program.cmdline = cmdline + end + + return program +end + +--- Parse a directory with .desktop files recursively. +-- @tparam string dir_path The directory path. +-- @tparam function callback Will be fired when all the files were parsed +-- with the resulting list of menu entries as argument. +-- @tparam table callback.programs Paths of found .desktop files. +function utils.parse_dir(dir_path, callback) + + local function parser(dir, programs) + local f = gio.File.new_for_path(dir) + -- Except for "NONE" there is also NOFOLLOW_SYMLINKS + local query = gio.FILE_ATTRIBUTE_STANDARD_NAME .. "," .. gio.FILE_ATTRIBUTE_STANDARD_TYPE + local enum, err = f:async_enumerate_children(query, gio.FileQueryInfoFlags.NONE) + if not enum then + debug.print_error(err) + return + end + local files_per_call = 100 -- Actual value is not that important + while true do + local list, enum_err = enum:async_next_files(files_per_call) + if enum_err then + debug.print_error(enum_err) + return + end + for _, info in ipairs(list) do + local file_type = info:get_file_type() + local file_path = enum:get_child(info):get_path() + if file_type == 'REGULAR' then + local program = utils.parse_desktop_file(file_path) + if program then + table.insert(programs, program) + end + elseif file_type == 'DIRECTORY' then + parser(file_path, programs) + end + end + if #list == 0 then + break + end + end + enum:async_close() + end + + gio.Async.start(function() + local result = {} + parser(dir_path, result) + protected_call.call(callback, result) + end)() +end + +--- Compute textbox width. +-- @tparam wibox.widget.textbox textbox Textbox instance. +-- @tparam number|screen s Screen +-- @treturn int Text width. +function utils.compute_textbox_width(textbox, s) + s = screen[s or mouse.screen] + local w, _ = textbox:get_preferred_size(s) + return w +end + +--- Compute text width. +-- @tparam str text Text. +-- @tparam number|screen s Screen +-- @treturn int Text width. +function utils.compute_text_width(text, s) + return utils.compute_textbox_width(wibox.widget.textbox(awful_util.escape(text)), s) +end + +return utils + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |