summaryrefslogtreecommitdiff
path: root/lib/menubar/utils.lua
blob: 6f80e86013be2cbf3cf7dd6596b5cc0e351629c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
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