%%%------------------------------------------------------------------- %% @doc Caches the rendered Openbox pipe menu in memory. %% %% Holds the last-rendered menu binary so socket clients get an %% instant response. `invalidate/0' synchronously rescans the XDG %% application directories, reparses every `.desktop' file and %% re-renders the menu, replacing the cached binary. %% @end %%%------------------------------------------------------------------- -module(xdg_pipe_menu_cache). -behaviour(gen_server). -export([start_link/0, get_menu/0, invalidate/0, build/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). -spec start_link() -> {ok, pid()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec get_menu() -> binary(). get_menu() -> gen_server:call(?MODULE, get_menu). -spec invalidate() -> ok. invalidate() -> gen_server:cast(?MODULE, invalidate). %% Pure pipeline: explicit dirs in, rendered menu binary out. Kept %% side-effect-free (besides reading the filesystem) and exported so %% it can be unit tested directly and reused by the CLI client's %% no-daemon fallback, without going through the gen_server at all. -spec build([file:filename_all()]) -> binary(). build(Dirs) -> Files = xdg_pipe_menu_scanner:find_desktop_files(Dirs), Entries = lists:filtermap(fun parse/1, Files), xdg_pipe_menu_renderer:render(Entries). parse(File) -> case xdg_pipe_menu_desktop_entry:parse_file(File) of {ok, Entry} -> {true, Entry}; {error, _} -> false end. init([]) -> {ok, build(xdg_pipe_menu_scanner:data_dirs())}. handle_call(get_menu, _From, Menu) -> {reply, Menu, Menu}. handle_cast(invalidate, _Menu) -> {noreply, build(xdg_pipe_menu_scanner:data_dirs())}. handle_info(_Info, State) -> {noreply, State}.