-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", 159). -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", 166). ?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(olive@logging:logger(), gleam@dynamic:dynamic_()) -> internal_msg(). watch_decoder(Logger, 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, Decode_errors} -> Msg@1 = gleam@list:map( Decode_errors, fun(Error) -> {decode_error, Expected, Found, Path} = Error, <<<<<<<<<<"Expected "/utf8, Expected/binary>>/binary, " at ["/utf8>>/binary, (gleam@string:join(Path, <<","/utf8>>))/binary>>/binary, "] but found "/utf8>>/binary, Found/binary>> end ), olive@logging:error( Logger, <<"Error occured while watching files:\n"/utf8, (gleam@string:join(Msg@1, <<"\n"/utf8>>))/binary>> ), ignore_changes end. -file("src/olive/watcher.gleam", 104). -spec watch_folder(olive@logging:logger(), binary()) -> gleam@erlang@process:selector(internal_msg()). watch_folder(Logger, 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(_capture) -> watch_decoder(Logger, _capture) end ) end, Selectors. -file("src/olive/watcher.gleam", 69). -spec start_watchers(olive@config:config()) -> gleam@erlang@process:selector(internal_msg()). start_watchers(Config) -> _pipe = erlang:element(8, Config), _pipe@1 = gleam@list:map( _pipe, fun(_capture) -> watch_folder(erlang:element(2, Config), _capture) end ), 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(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} ).