aboutsummaryrefslogtreecommitdiff
path: root/src/er_xdg_pipe_menu_watcher.erl
blob: d8c407d43b04fc52dc70225a54fff5438eec3585 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
%%%-------------------------------------------------------------------
%% @doc Watches the XDG application directories for changes.
%%
%% Starts one `fs' (native inotify) watch per existing directory
%% returned by `er_xdg_pipe_menu_scanner:data_dirs/0' and subscribes
%% to its file events. On any event, a debounce timer is (re)started;
%% once it fires with no further events in the window, the menu cache
%% is invalidated so the next request gets a freshly rebuilt menu.
%%
%% `fs' watches recursively (it drives `inotifywait -r' on Linux), so
%% changes in vendor subdirectories (e.g. `applications/kde4/*') are
%% picked up without any extra handling here.
%% @end
%%%-------------------------------------------------------------------

-module(er_xdg_pipe_menu_watcher).

-behaviour(gen_server).

-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).

-define(DEFAULT_DEBOUNCE_MS, 500).

-spec start_link() -> {ok, pid()}.
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) ->
    warn_if_inotifywait_missing(),
    Dirs = [D || D <- er_xdg_pipe_menu_scanner:data_dirs(), filelib:is_dir(D)],
    lists:foreach(fun watch/1, lists:zip(lists:seq(1, length(Dirs)), Dirs)),
    {ok, #{timer => undefined}}.

watch({Index, Dir}) ->
    Name = list_to_atom("er_xdg_pipe_menu_fs_" ++ integer_to_list(Index)),
    case fs:start_link(Name, Dir) of
        {ok, _Pid} -> fs:subscribe(Name);
        {error, Reason} -> log_watch_failure(Dir, Reason)
    end.

%% `fs' on Linux shells out to the system `inotifywait' binary; if
%% it's missing, `fs:start_link/2' still returns `{ok, _}' but simply
%% never delivers events, which is otherwise silent.
warn_if_inotifywait_missing() ->
    case os:find_executable("inotifywait") of
        false ->
            error_logger:warning_msg(
                "er_xdg_pipe_menu_watcher: 'inotifywait' not found on PATH "
                "(install inotify-tools) -- desktop file changes won't be "
                "picked up until the daemon is restarted~n",
                []
            );
        _Path ->
            ok
    end.

log_watch_failure(Dir, Reason) ->
    error_logger:warning_msg(
        "er_xdg_pipe_menu_watcher: failed to watch ~p: ~p~n", [Dir, Reason]
    ).

handle_call(_Request, _From, State) ->
    {reply, ok, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info({_Pid, {fs, file_event}, {_Path, _Events}}, State) ->
    {noreply, reset_debounce(State)};
handle_info(debounce_fire, State) ->
    er_xdg_pipe_menu_cache:invalidate(),
    {noreply, State#{timer => undefined}};
handle_info(_Info, State) ->
    {noreply, State}.

reset_debounce(#{timer := Timer} = State) ->
    case Timer of
        undefined -> ok;
        Ref -> erlang:cancel_timer(Ref)
    end,
    State#{timer => erlang:send_after(debounce_ms(), self(), debounce_fire)}.

debounce_ms() ->
    application:get_env(er_xdg_pipe_menu, debounce_ms, ?DEFAULT_DEBOUNCE_MS).