-module(polly). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([add_dir/2, add_file/2, max_depth/2, interval/2, filter/2, default_filter/2, new/0, stop/1, watch_with/3, watch/2]). -export_type([event/0, options/1, no_watched_dirs/0, has_watched_dirs/0, watcher/0, vfs/0]). -type event() :: {created, binary()} | {changed, binary()} | {deleted, binary()} | {error, binary(), simplifile:file_error()}. -opaque options(GGC) :: {options, integer(), list(binary()), integer(), fun((simplifile:file_type(), binary()) -> boolean())} | {gleam_phantom, GGC}. -type no_watched_dirs() :: any(). -type has_watched_dirs() :: any(). -opaque watcher() :: {watcher, fun(() -> nil)}. -type vfs() :: {file, binary(), integer()} | {folder, binary(), integer(), list(vfs())}. -spec add_dir(options(any()), binary()) -> options(has_watched_dirs()). add_dir(Options, Path) -> {options, Interval, Paths, Max_depth, Filter} = Options, {options, Interval, [Path | Paths], Max_depth, Filter}. -spec add_file(options(any()), binary()) -> options(has_watched_dirs()). add_file(Options, Path) -> add_dir(Options, Path). -spec max_depth(options(GGN), integer()) -> options(GGN). max_depth(Options, Max_depth) -> case {erlang:element(4, Options) < 0, erlang:element(4, Options) > Max_depth} of {true, _} -> erlang:setelement(4, Options, Max_depth); {false, true} -> erlang:setelement(4, Options, Max_depth); {false, false} -> Options end. -spec interval(options(GGQ), integer()) -> options(GGQ). interval(Options, Interval) -> case Interval > 0 of true -> erlang:setelement(2, Options, Interval); false -> Options end. -spec filter(options(GGT), fun((simplifile:file_type(), binary()) -> boolean())) -> options(GGT). filter(Options, Filter) -> erlang:setelement(5, Options, Filter). -spec default_filter(simplifile:file_type(), binary()) -> boolean(). default_filter(_, Path) -> case filepath:base_name(Path) of <<"."/utf8>> -> true; <<".."/utf8>> -> true; Basename -> not gleam@string:starts_with(Basename, <<"."/utf8>>) end. -spec new() -> options(no_watched_dirs()). new() -> {options, 1000, [], -1, fun default_filter/2}. -spec collect_errors(list({binary(), simplifile:file_error()}), event()) -> list({binary(), simplifile:file_error()}). collect_errors(Errors, Event) -> case Event of {error, Path, Reason} -> [{Path, Reason} | Errors]; _ -> Errors end. -spec stop(watcher()) -> nil. stop(Watcher) -> (erlang:element(2, Watcher))(). -spec delete(binary(), vfs(), GHX, fun((GHX, event()) -> GHX)) -> GHX. delete(Path, Vfs, State, Emit) -> Full_path = filepath:join(Path, erlang:element(2, Vfs)), case Vfs of {file, _, _} -> Emit(State, {deleted, Full_path}); {folder, _, _, Children} -> State@2 = gleam@list:fold( Children, State, fun(State@1, Child) -> delete(Full_path, Child, State@1, Emit) end ), Emit(State@2, {deleted, Full_path}) end. -spec get_modkey(simplifile:file_info()) -> integer(). get_modkey(Stat) -> gleam@int:max(erlang:element(10, Stat), erlang:element(11, Stat)). -spec readdir(binary()) -> {ok, list(binary())} | {error, simplifile:file_error()}. readdir(Path) -> _pipe = simplifile_erl:read_directory(Path), gleam@result:map( _pipe, fun(_capture) -> gleam@list:sort(_capture, fun gleam@string:compare/2) end ). -spec diff_children( fun((simplifile:file_type(), binary()) -> boolean()), integer(), binary(), list(vfs()), list(binary()), list(vfs()), GHV, fun((GHV, event()) -> GHV) ) -> {list(vfs()), GHV}. diff_children( Filter, Depth, Path, Old_children, New_entries, New_children, State, Emit ) -> case {Old_children, New_entries} of {[], []} -> {lists:reverse(New_children), State}; {[First_old | Rest_old], [First_new | Rest_new]} -> case gleam@string:compare(erlang:element(2, First_old), First_new) of eq -> case diff(Filter, Depth, Path, First_old, State, Emit) of {{some, New_vfs}, State@1} -> diff_children( Filter, Depth, Path, Rest_old, Rest_new, [New_vfs | New_children], State@1, Emit ); {none, State@2} -> diff_children( Filter, Depth, Path, Rest_old, Rest_new, New_children, State@2, Emit ) end; gt -> {New_vfs@1, State@3} = create( Filter, Depth, Path, First_new, State, Emit ), New_children@1 = case New_vfs@1 of {some, New_vfs@2} -> [New_vfs@2 | New_children]; none -> New_children end, diff_children( Filter, Depth, Path, Old_children, Rest_new, New_children@1, State@3, Emit ); lt -> diff_children( Filter, Depth, Path, Rest_old, New_entries, New_children, delete(Path, First_old, State, Emit), Emit ) end; {[], [First_new@1 | Rest_new@1]} -> {New_vfs@3, State@4} = create( Filter, Depth, Path, First_new@1, State, Emit ), New_children@2 = case New_vfs@3 of {some, New_vfs@4} -> [New_vfs@4 | New_children]; none -> New_children end, diff_children( Filter, Depth, Path, Old_children, Rest_new@1, New_children@2, State@4, Emit ); {[First_old@1 | Rest_old@1], []} -> diff_children( Filter, Depth, Path, Rest_old@1, New_entries, New_children, delete(Path, First_old@1, State, Emit), Emit ) end. -spec diff( fun((simplifile:file_type(), binary()) -> boolean()), integer(), binary(), vfs(), GHQ, fun((GHQ, event()) -> GHQ) ) -> {gleam@option:option(vfs()), GHQ}. diff(Filter, Depth, Path, Vfs, State, Emit) -> Full_path = filepath:join(Path, erlang:element(2, Vfs)), case simplifile_erl:link_info(Full_path) of {ok, Stat} -> Type_ = simplifile:file_info_type(Stat), case Filter(Type_, Full_path) of true -> case {Type_, Vfs} of {file, {file, Name, Old_key}} -> New_key = get_modkey(Stat), case New_key =:= Old_key of true -> {{some, Vfs}, State}; false -> {{some, {file, Name, New_key}}, Emit(State, {changed, Full_path})} end; {directory, {folder, _, _, _}} when Depth =:= 0 -> {{some, Vfs}, State}; {directory, {folder, Name@1, _, Old_children}} when Depth =/= 0 -> case readdir(Full_path) of {ok, New_entries} -> {Children, State@1} = diff_children( Filter, Depth - 1, Full_path, Old_children, New_entries, [], State, Emit ), {{some, {folder, Name@1, get_modkey(Stat), Children}}, State@1}; {error, enoent} -> {none, delete(Path, Vfs, State, Emit)}; {error, eacces} -> {none, delete(Path, Vfs, State, Emit)}; {error, Reason} -> {{some, Vfs}, Emit(State, {error, Full_path, Reason})} end; {_, _} -> create_stat( Filter, Depth, erlang:element(2, Vfs), Full_path, Stat, delete(Path, Vfs, State, Emit), Emit ) end; false -> {none, delete(Path, Vfs, State, Emit)} end; {error, eacces} -> {none, delete(Path, Vfs, State, Emit)}; {error, enoent} -> {none, delete(Path, Vfs, State, Emit)}; {error, Reason@1} -> {{some, Vfs}, Emit(State, {error, Path, Reason@1})} end. -spec create_children( fun((simplifile:file_type(), binary()) -> boolean()), integer(), binary(), list(binary()), list(vfs()), GHO, fun((GHO, event()) -> GHO) ) -> {list(vfs()), GHO}. create_children(Filter, Depth, Path, Children, Oks, State, Emit) -> case Children of [] -> {lists:reverse(Oks), State}; [First | Rest] -> case create(Filter, Depth, Path, First, State, Emit) of {{some, Vfs}, State@1} -> create_children( Filter, Depth, Path, Rest, [Vfs | Oks], State@1, Emit ); {none, State@2} -> create_children( Filter, Depth, Path, Rest, Oks, State@2, Emit ) end end. -spec create( fun((simplifile:file_type(), binary()) -> boolean()), integer(), binary(), binary(), GHI, fun((GHI, event()) -> GHI) ) -> {gleam@option:option(vfs()), GHI}. create(Filter, Depth, Path, Name, State, Emit) -> Full_path = filepath:join(Path, Name), case simplifile_erl:link_info(Full_path) of {ok, Stat} -> create_stat(Filter, Depth, Name, Full_path, Stat, State, Emit); {error, enoent} -> {none, State}; {error, eacces} -> {none, State}; {error, Reason} -> {none, Emit(State, {error, Full_path, Reason})} end. -spec create_stat( fun((simplifile:file_type(), binary()) -> boolean()), integer(), binary(), binary(), simplifile:file_info(), GHK, fun((GHK, event()) -> GHK) ) -> {gleam@option:option(vfs()), GHK}. create_stat(Filter, Depth, Name, Full_path, Stat, State, Emit) -> Type_ = simplifile:file_info_type(Stat), gleam@bool:guard( not Filter(Type_, Full_path), {none, State}, fun() -> case Type_ of file -> {{some, {file, Name, get_modkey(Stat)}}, Emit(State, {created, Full_path})}; directory when Depth =:= 0 -> {{some, {folder, Name, get_modkey(Stat), []}}, Emit(State, {created, Full_path})}; directory when Depth =/= 0 -> case readdir(Full_path) of {ok, Entries} -> Depth@1 = Depth - 1, State@1 = Emit(State, {created, Full_path}), {Children, State@2} = create_children( Filter, Depth@1, Full_path, Entries, [], State@1, Emit ), {{some, {folder, Name, get_modkey(Stat), Children}}, State@2}; {error, enoent} -> {none, State}; {error, eacces} -> {none, State}; {error, Reason} -> {none, Emit(State, {error, Full_path, Reason})} end; _ -> {none, State} end end ). -spec watch_with(options(has_watched_dirs()), GHC, fun((GHC, event()) -> GHC)) -> {ok, watcher()} | {error, list({binary(), simplifile:file_error()})}. watch_with(Options, Initial, Emit) -> {options, Interval, Paths, Max_depth, Filter} = Options, gleam@result:'try'( (gleam@list:try_map( Paths, fun(Path) -> case create( Filter, Max_depth, Path, <<""/utf8>>, [], fun collect_errors/2 ) of {{some, Vfs}, []} -> {ok, {Path, {some, Vfs}}}; {none, []} -> {error, [{Path, enoent}]}; {_, Errors} -> {error, Errors} end end )), fun(Roots) -> State = {Roots, Initial}, Stop = polly_ffi:repeatedly( Interval, State, fun(State@1) -> {Roots@1, State@2} = State@1, gleam@list:fold( Roots@1, {[], State@2}, fun(_use0, _use1) -> {Roots@2, State@3} = _use0, {Path@1, Vfs@1} = _use1, case Vfs@1 of {some, Vfs@2} -> {New_vfs, State@4} = diff( Filter, Max_depth, Path@1, Vfs@2, State@3, Emit ), {[{Path@1, New_vfs} | Roots@2], State@4}; none -> {New_vfs@1, State@5} = create( Filter, Max_depth, Path@1, <<""/utf8>>, State@3, Emit ), {[{Path@1, New_vfs@1} | Roots@2], State@5} end end ) end ), {ok, {watcher, Stop}} end ). -spec watch(options(has_watched_dirs()), fun((event()) -> any())) -> {ok, watcher()} | {error, list({binary(), simplifile:file_error()})}. watch(Options, Emit) -> watch_with( Options, nil, fun(_, Event) -> Emit(Event), nil end ).