aboutsummaryrefslogtreecommitdiff
path: root/src/xdg_pipe_menu_renderer.erl
diff options
context:
space:
mode:
authorChristopher R. Nelson <christopher.nelson@languidnights.com>2026-07-21 11:44:41 -0400
committerChristopher R. Nelson <christopher.nelson@languidnights.com>2026-07-21 11:44:41 -0400
commit5ff1fbe175af17fbb808c5ea942f465a932da3af (patch)
treeb6a26b4b7e896e5ed5d85a5003c4d176ed5c8e9e /src/xdg_pipe_menu_renderer.erl
parent7e8472e1e2a0b39746ef86c041d56b92bab5a6b7 (diff)
Rename app from er_xdg_pipe_menu to xdg_pipe_menuHEADmain
Drop the "er-" prefix throughout: module/atom names, filenames, socket paths, and docs. It was an implementation detail, not part of the app's identity. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'src/xdg_pipe_menu_renderer.erl')
-rw-r--r--src/xdg_pipe_menu_renderer.erl78
1 files changed, 78 insertions, 0 deletions
diff --git a/src/xdg_pipe_menu_renderer.erl b/src/xdg_pipe_menu_renderer.erl
new file mode 100644
index 0000000..c94fe83
--- /dev/null
+++ b/src/xdg_pipe_menu_renderer.erl
@@ -0,0 +1,78 @@
+%%%-------------------------------------------------------------------
+%% @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 `<menu>' submenu
+%% per category, each containing an `<item>' 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(xdg_pipe_menu_renderer).
+
+-include("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, ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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).