%%%------------------------------------------------------------------- %% @doc Renders parsed desktop entries as an Openbox pipe menu. %% %% Given a list of `#desktop_entry{}' records, produces the XML that %% Openbox expects on stdout from a pipe menu: one `' submenu %% per category, each containing an `' per visible entry in %% that category (entries with `hidden = true' are dropped). An entry %% belonging to several categories appears in each of their submenus; %% an entry with no categories is placed in a catch-all `Other' %% submenu. Categories are sorted case-insensitively, and entries %% within a submenu are sorted case-insensitively by name. %% @end %%%------------------------------------------------------------------- -module(er_xdg_pipe_menu_renderer). -include("er_xdg_pipe_menu.hrl"). -export([render/1]). -define(OTHER_CATEGORY, <<"Other">>). -spec render([#desktop_entry{}]) -> binary(). render(Entries) -> Visible = [E || E <- Entries, not E#desktop_entry.hidden], Grouped = group_by_category(Visible), Categories = lists:sort(fun category_lt/2, maps:keys(Grouped)), Menu = {openbox_pipe_menu, [], submenus_content(Categories, Grouped)}, Xml = xmerl:export_simple( [Menu], xmerl_xml, [{prolog, ["\n"]}] ), iolist_to_binary([Xml, "\n"]). group_by_category(Entries) -> lists:foldl(fun add_entry_to_categories/2, #{}, Entries). add_entry_to_categories(#desktop_entry{categories = []} = E, Acc) -> add_entry_to_category(?OTHER_CATEGORY, E, Acc); add_entry_to_categories(#desktop_entry{categories = Cats} = E, Acc) -> lists:foldl(fun(Cat, Acc0) -> add_entry_to_category(Cat, E, Acc0) end, Acc, Cats). add_entry_to_category(Cat, E, Acc) -> maps:update_with(Cat, fun(Es) -> [E | Es] end, [E], Acc). category_lt(A, B) -> string:lowercase(A) =< string:lowercase(B). by_name(#desktop_entry{name = A}, #desktop_entry{name = B}) -> string:lowercase(A) =< string:lowercase(B). submenus_content(Categories, Grouped) -> lists:foldr( fun(Cat, Acc) -> ["\n ", submenu_element(Cat, Grouped) | Acc] end, ["\n"], Categories ). submenu_element(Cat, Grouped) -> Entries = lists:sort(fun by_name/2, maps:get(Cat, Grouped)), {menu, [{id, Cat}, {label, Cat}], items_content(Entries)}. items_content(Entries) -> lists:foldr(fun(E, Acc) -> ["\n ", item_element(E) | Acc] end, ["\n "], Entries). item_element(#desktop_entry{name = Name, exec = Exec, icon = Icon}) -> {item, item_attrs(Name, Icon), [ "\n ", {action, [{name, <<"Execute">>}], [ "\n ", {command, [], [text(Exec)]}, "\n " ]}, "\n " ]}. item_attrs(Name, undefined) -> [{label, Name}]; item_attrs(Name, Icon) -> [{label, Name}, {icon, Icon}]. text(Bin) -> unicode:characters_to_list(Bin).