diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/er_xdg_pipe_menu_desktop_entry.erl | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/src/er_xdg_pipe_menu_desktop_entry.erl b/src/er_xdg_pipe_menu_desktop_entry.erl new file mode 100644 index 0000000..bbd2594 --- /dev/null +++ b/src/er_xdg_pipe_menu_desktop_entry.erl @@ -0,0 +1,131 @@ +%%%------------------------------------------------------------------- +%% @doc Parser for XDG .desktop files. +%% +%% Reads the `[Desktop Entry]' group of a desktop entry file and +%% extracts the fields needed to build a menu item: `Name', `Exec' +%% and (optionally) `Icon'. Other groups (e.g. `[Desktop Action ...]') +%% and localized keys (e.g. `Name[fr]') are ignored. +%% @end +%%%------------------------------------------------------------------- + +-module(er_xdg_pipe_menu_desktop_entry). + +-include("er_xdg_pipe_menu.hrl"). + +-export([parse_file/1, parse/1]). + +-define(GROUP, <<"Desktop Entry">>). +-define(WANTED_KEYS, [ + <<"Name">>, <<"Exec">>, <<"Icon">>, <<"NoDisplay">>, <<"Hidden">>, <<"Type">> +]). +-define(FIELD_CODES, "fFuUdDnNickvm"). + +-spec parse_file(file:filename_all()) -> {ok, #desktop_entry{}} | {error, term()}. +parse_file(Path) -> + case file:read_file(Path) of + {ok, Bin} -> parse(Bin); + {error, Reason} -> {error, Reason} + end. + +-spec parse(binary()) -> {ok, #desktop_entry{}} | {error, missing_name | missing_exec}. +parse(Bin) -> + to_entry(scan(Bin)). + +%% Fold every line into a map of the fields we care about, tracking +%% which `[Group]' we're currently inside. +scan(Bin) -> + Lines = binary:split(Bin, [<<"\r\n">>, <<"\n">>], [global]), + {_Group, Fields} = lists:foldl(fun scan_line/2, {undefined, #{}}, Lines), + Fields. + +scan_line(Line, {Group, Fields}) -> + case classify(string:trim(Line)) of + skip -> + {Group, Fields}; + {section, Name} -> + {Name, Fields}; + {kv, Key, Value} -> + case Group =:= ?GROUP andalso lists:member(Key, ?WANTED_KEYS) of + true -> {Group, maps:put(Key, unescape(Value), Fields)}; + false -> {Group, Fields} + end + end. + +classify(<<>>) -> + skip; +classify(<<"#", _/binary>>) -> + skip; +classify(<<"[", Rest/binary>>) -> + Size = byte_size(Rest) - 1, + case Rest of + <<Name:Size/binary, "]">> -> {section, string:trim(Name)}; + _ -> skip + end; +classify(Line) -> + case binary:split(Line, <<"=">>) of + [Key, Value] -> {kv, string:trim(Key), string:trim(Value)}; + [_] -> skip + end. + +%% Desktop entry escape sequences: \\ \s \n \t \r +unescape(Bin) -> + unescape(Bin, <<>>). + +unescape(<<>>, Acc) -> + Acc; +unescape(<<$\\, $\\, Rest/binary>>, Acc) -> + unescape(Rest, <<Acc/binary, $\\>>); +unescape(<<$\\, $s, Rest/binary>>, Acc) -> + unescape(Rest, <<Acc/binary, $\s>>); +unescape(<<$\\, $n, Rest/binary>>, Acc) -> + unescape(Rest, <<Acc/binary, $\n>>); +unescape(<<$\\, $t, Rest/binary>>, Acc) -> + unescape(Rest, <<Acc/binary, $\t>>); +unescape(<<$\\, $r, Rest/binary>>, Acc) -> + unescape(Rest, <<Acc/binary, $\r>>); +unescape(<<C, Rest/binary>>, Acc) -> + unescape(Rest, <<Acc/binary, C>>). + +to_entry(Fields) -> + case maps:find(<<"Name">>, Fields) of + error -> + {error, missing_name}; + {ok, Name} -> + case maps:find(<<"Exec">>, Fields) of + error -> + {error, missing_exec}; + {ok, Exec} -> + {ok, #desktop_entry{ + name = Name, + exec = strip_field_codes(Exec), + icon = maps:get(<<"Icon">>, Fields, undefined), + hidden = is_hidden(Fields) + }} + end + end. + +%% Niche variations (e.g. a Type-less override dropped into +%% ~/.local/share/applications) are left to be special-cased there +%% rather than accounted for here. +is_hidden(Fields) -> + maps:get(<<"NoDisplay">>, Fields, <<"false">>) =:= <<"true">> orelse + maps:get(<<"Hidden">>, Fields, <<"false">>) =:= <<"true">> orelse + case maps:find(<<"Type">>, Fields) of + {ok, Type} -> Type =/= <<"Application">>; + error -> false + end. + +%% Field codes (%f, %u, %i, ...) are expanded by launchers that pass +%% files/URIs on the command line; a pipemenu has none to pass, so +%% each code is dropped along with the whitespace around it. `%%' is +%% then unescaped to a literal `%'. +strip_field_codes(Exec) -> + Tokens = binary:split(Exec, <<" ">>, [global]), + Kept = [T || T <- Tokens, T =/= <<>>, not is_field_code(T)], + Joined = iolist_to_binary(lists:join(<<" ">>, Kept)), + binary:replace(Joined, <<"%%">>, <<"%">>, [global]). + +is_field_code(<<"%", C>>) -> + lists:member(C, ?FIELD_CODES); +is_field_code(_) -> + false. |
