%%%------------------------------------------------------------------- %% @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 `' per %% visible entry (entries with `hidden = true' are dropped), sorted %% case-insensitively by name, each wrapping an `Execute' action. %% @end %%%------------------------------------------------------------------- -module(er_xdg_pipe_menu_renderer). -include("er_xdg_pipe_menu.hrl"). -export([render/1]). -spec render([#desktop_entry{}]) -> binary(). render(Entries) -> Visible = [E || E <- Entries, not E#desktop_entry.hidden], Sorted = lists:sort(fun by_name/2, Visible), iolist_to_binary([ <<"\n">>, <<"\n">>, [render_item(E) || E <- Sorted], <<"\n">> ]). by_name(#desktop_entry{name = A}, #desktop_entry{name = B}) -> string:lowercase(A) =< string:lowercase(B). render_item(#desktop_entry{name = Name, exec = Exec, icon = Icon}) -> [ <<" >, escape_attr(Name), <<"\"">>, icon_attr(Icon), <<">\n">>, <<" \n">>, <<" ">>, escape_text(Exec), <<"\n">>, <<" \n">>, <<" \n">> ]. icon_attr(undefined) -> <<>>; icon_attr(Icon) -> [<<" icon=\"">>, escape_attr(Icon), <<"\"">>]. %% Attribute values additionally need '"' escaped; text content doesn't. escape_attr(Bin) -> escape(Bin, [{<<"\"">>, <<""">>}]). escape_text(Bin) -> escape(Bin, []). escape(Bin, Extra) -> lists:foldl( fun({Pat, Repl}, Acc) -> binary:replace(Acc, Pat, Repl, [global]) end, Bin, [{<<"&">>, <<"&">>}, {<<"<">>, <<"<">>}, {<<">">>, <<">">>} | Extra] ).