-module(olive@watcher). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([start/2]). -export_type([watcher_error/0, message/0, state/0, internal_msg/0, watch_events/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " The `watcher` module watches a list of dir and triggers a `FilesChanged` message\n" " to be handled by calling code.\n" ). -type watcher_error() :: no_file_watcher_supported_for_os | {no_file_watcher_installed, gleam@dynamic:dynamic_()}. -type message() :: {files_changed, binary()}. -type state() :: {state, gleam@option:option(gleam@erlang@process:timer()), gleam@erlang@process:subject(message())}. -opaque internal_msg() :: {trigger_files_changed, binary()} | ignore_changes. -type watch_events() :: event_needing_rebuild | other_events. -file("src/olive/watcher.gleam", 57). -spec check_watcher_install() -> {ok, nil} | {error, binary()}. check_watcher_install() -> case olive_ffi:check_watcher_installed() of {error, no_file_watcher_supported_for_os} -> {error, <<"No file watcher supported for your OS"/utf8>>}; {error, {no_file_watcher_installed, Watcher}} -> {error, <<"No file watcher installed, please install: "/utf8, (gleam@string:inspect(Watcher))/binary>>}; {ok, _} -> {ok, nil} end. -file("src/olive/watcher.gleam", 94). -spec maybe_cancel_timer(gleam@option:option(gleam@erlang@process:timer())) -> nil. maybe_cancel_timer(Timer) -> case Timer of none -> nil; {some, Timer@1} -> gleam@erlang@process:cancel_timer(Timer@1), nil end. -file("src/olive/watcher.gleam", 75). -spec do_loop(internal_msg(), state()) -> gleam@otp@actor:next(any(), state()). do_loop(Msg, State) -> case Msg of ignore_changes -> gleam@otp@actor:continue(State); {trigger_files_changed, File_name} -> maybe_cancel_timer(erlang:element(2, State)), Timer = gleam@erlang@process:send_after( erlang:element(3, State), 50, {files_changed, File_name} ), gleam@otp@actor:continue( begin _record = State, {state, {some, Timer}, erlang:element(3, _record)} end ) end. -file("src/olive/watcher.gleam", 146). -spec erlang_string_to_string_decoder() -> gleam@dynamic@decode:decoder(binary()). erlang_string_to_string_decoder() -> gleam@dynamic@decode:new_primitive_decoder( <<"ErlangString"/utf8>>, fun(Data) -> _pipe = olive_ffi:coerce(Data), _pipe@1 = unicode:characters_to_binary(_pipe), {ok, _pipe@1} end ). -file("src/olive/watcher.gleam", 153). ?DOC(" Converts an atom to an event\n"). -spec atom_to_watch_events() -> gleam@dynamic@decode:decoder(watch_events()). atom_to_watch_events() -> Modified = erlang:binary_to_atom(<<"modified"/utf8>>), Renamed = erlang:binary_to_atom(<<"renamed"/utf8>>), gleam@dynamic@decode:new_primitive_decoder( <<"Atom"/utf8>>, fun(Data) -> case gleam_erlang_ffi:atom_from_dynamic(Data) of {ok, Ev} when Ev =:= Modified -> {ok, event_needing_rebuild}; {ok, Ev@1} when Ev@1 =:= Renamed -> {ok, event_needing_rebuild}; _ -> {ok, other_events} end end ). -file("src/olive/watcher.gleam", 115). -spec watch_decoder(gleam@dynamic:dynamic_()) -> internal_msg(). watch_decoder(Msg) -> Decoder = begin gleam@dynamic@decode:subfield( [2, 0], erlang_string_to_string_decoder(), fun(File_name) -> gleam@dynamic@decode:subfield( [2, 1], gleam@dynamic@decode:list(atom_to_watch_events()), fun(Events) -> gleam@dynamic@decode:success({File_name, Events}) end ) end ) end, case gleam@dynamic@decode:run(Msg, Decoder) of {ok, {File_name@1, Events@1}} -> case {filepath:extension(File_name@1), gleam@list:contains(Events@1, event_needing_rebuild)} of {{ok, <<"gleam"/utf8>>}, true} -> {trigger_files_changed, File_name@1}; {_, _} -> ignore_changes end; {error, _} -> olive@logging:error(<<"Error occured while watching files"/utf8>>), ignore_changes end. -file("src/olive/watcher.gleam", 104). -spec watch_folder(binary()) -> gleam@erlang@process:selector(internal_msg()). watch_folder(Dir) -> Atom = erlang:binary_to_atom(<<"fs_watcher_"/utf8, Dir/binary>>), _assert_subject = fs:start_link(Atom, Dir), {ok, _} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, value => _assert_fail, module => <<"olive/watcher"/utf8>>, function => <<"watch_folder"/utf8>>, line => 107}) end, fs:subscribe(Atom), Selectors = begin _pipe = gleam_erlang_ffi:new_selector(), gleam@erlang@process:selecting_anything(_pipe, fun watch_decoder/1) end, Selectors. -file("src/olive/watcher.gleam", 69). -spec start_watchers(list(binary())) -> gleam@erlang@process:selector(internal_msg()). start_watchers(Dirs) -> _pipe = Dirs, _pipe@1 = gleam@list:map(_pipe, fun watch_folder/1), gleam@list:fold( _pipe@1, gleam_erlang_ffi:new_selector(), fun gleam_erlang_ffi:merge_selector/2 ). -file("src/olive/watcher.gleam", 47). -spec init_watcher( olive@config:config(), gleam@erlang@process:subject(message()) ) -> gleam@otp@actor:init_result(state(), internal_msg()). init_watcher(Config, Watch_subject) -> case check_watcher_install() of {error, Err} -> {failed, Err}; {ok, _} -> Selectors = start_watchers(erlang:element(7, Config)), {ready, {state, none, Watch_subject}, Selectors} end. -file("src/olive/watcher.gleam", 39). -spec start(olive@config:config(), gleam@erlang@process:subject(message())) -> {ok, gleam@erlang@process:subject(internal_msg())} | {error, gleam@otp@actor:start_error()}. start(Config, Watch_subject) -> gleam@otp@actor:start_spec( {spec, fun() -> init_watcher(Config, Watch_subject) end, 5000, fun do_loop/2} ).