summaryrefslogtreecommitdiff
path: root/lib/menubar/icon_theme.lua
blob: f76252ffc429e1bcb0e4368fa4c15c5202d4f3a7 (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
---------------------------------------------------------------------------
--- 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