diff options
| author | Christopher R. Nelson <christopher.nelson@languidnights.com> | 2026-07-20 23:03:25 -0400 |
|---|---|---|
| committer | Christopher R. Nelson <christopher.nelson@languidnights.com> | 2026-07-20 23:03:25 -0400 |
| commit | 6bbc784b4cf0717ddc7f5c3ad32c1d5e2d486877 (patch) | |
| tree | 3aba5c36f02e035866bc0b30fb2b57823ff3271e /src/er_xdg_pipe_menu_renderer.erl | |
| parent | 7db20cd2ac2c8b8774a8ae02a7b233541666c865 (diff) | |
Add Openbox pipe menu renderer
render/1 turns a list of #desktop_entry{} into the XML Openbox expects
on stdout from a pipe menu: hidden entries dropped, remaining ones
sorted case-insensitively by name, each as an <item> wrapping an
Execute action with its command and (if present) icon attribute.
XML-escapes label/command/icon values.
Icon values are passed through as-is (theme icon names, not resolved
paths) -- icon theme resolution is left for later, separate from
rendering. Service wiring (scan -> parse -> render, with caching and
invalidation) is also left for later.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'src/er_xdg_pipe_menu_renderer.erl')
| -rw-r--r-- | src/er_xdg_pipe_menu_renderer.erl | 58 |
1 files changed, 58 insertions, 0 deletions
diff --git a/src/er_xdg_pipe_menu_renderer.erl b/src/er_xdg_pipe_menu_renderer.erl new file mode 100644 index 0000000..fafed45 --- /dev/null +++ b/src/er_xdg_pipe_menu_renderer.erl @@ -0,0 +1,58 @@ +%%%------------------------------------------------------------------- +%% @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 `<item>' 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([ + <<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n">>, + <<"<openbox_pipe_menu>\n">>, + [render_item(E) || E <- Sorted], + <<"</openbox_pipe_menu>\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}) -> + [ + <<" <item label=\"">>, + escape_attr(Name), + <<"\"">>, + icon_attr(Icon), + <<">\n">>, + <<" <action name=\"Execute\">\n">>, + <<" <command>">>, + escape_text(Exec), + <<"</command>\n">>, + <<" </action>\n">>, + <<" </item>\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] + ). |
