-module(polly). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([add_dir/2, max_depth/2, interval/2, filter/2, default_filter/1, new/0, stop/1, watch_with/3, watch/2]). -export_type([event/0, fs_error/0, options/0, watcher/1, vfs/0, hydration_result/0, stat/0, file_type/0, file_access/0]). -type event() :: {created, binary()} | {changed, binary()} | {deleted, binary()}. -type fs_error() :: eacces | enoent | enotdir | {e_unknown, binary()}. -opaque options() :: {options, integer(), list(binary()), integer(), fun((binary()) -> boolean())}. -opaque watcher(GLX) :: {watcher, fun(() -> nil)} | {gleam_phantom, GLX}. -type vfs() :: {file, binary(), integer()} | {folder, binary(), integer(), list(vfs())}. -type hydration_result() :: {success, vfs(), list(event())} | skipped | {errored, binary(), fs_error()}. -type stat() :: {stat, integer(), file_type(), file_access(), integer(), integer(), integer(), integer(), integer(), integer(), integer(), integer(), integer(), integer()}. -type file_type() :: device | directory | symlink | other | regular. -type file_access() :: read | write | read_write | none. -spec add_dir(options(), binary()) -> options(). add_dir(Options, Path) -> erlang:setelement(3, Options, [Path | erlang:element(3, Options)]). -spec max_depth(options(), integer()) -> options(). 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(), integer()) -> options(). interval(Options, Interval) -> case Interval > 0 of true -> erlang:setelement(2, Options, Interval); false -> Options end. -spec filter(options(), fun((binary()) -> boolean())) -> options(). filter(Options, Filter) -> erlang:setelement(5, Options, Filter). -spec default_filter(binary()) -> boolean(). default_filter(Path) -> Is_hidden = begin _pipe = filepath:base_name(Path), gleam@string:starts_with(_pipe, <<"."/utf8>>) end, Is_hidden =:= false. -spec new() -> options(). new() -> {options, 1000, [], -1, fun default_filter/1}. -spec stop(watcher(any())) -> nil. stop(Watcher) -> (erlang:element(2, Watcher))(). -spec 'try'( binary(), {ok, GML} | {error, fs_error()}, fun((GML) -> hydration_result()) ) -> hydration_result(). 'try'(Path, Result, Then) -> case Result of {ok, Value} -> Then(Value); {error, Err} -> {errored, Path, Err} end. -spec do_created(binary(), vfs(), list(event())) -> list(event()). do_created(Path, Vfs, Acc) -> Full_path = filepath:join(Path, erlang:element(2, Vfs)), Acc@1 = [{created, Full_path} | Acc], case Vfs of {file, _, _} -> Acc@1; {folder, _, _, Children} -> gleam@list:fold( Children, Acc@1, fun(Acc@2, Child) -> do_created(Full_path, Child, Acc@2) end ) end. -spec created(binary(), vfs()) -> list(event()). created(Path, Vfs) -> lists:reverse(do_created(Path, Vfs, [])). -spec do_deleted(binary(), vfs(), list(event())) -> list(event()). do_deleted(Path, Vfs, Acc) -> Full_path = filepath:join(Path, erlang:element(2, Vfs)), Acc@1 = [{deleted, Full_path} | Acc], case Vfs of {file, _, _} -> Acc@1; {folder, _, _, Children} -> gleam@list:fold( Children, Acc@1, fun(Acc@2, Child) -> do_deleted(Full_path, Child, Acc@2) end ) end. -spec deleted(binary(), vfs()) -> list(event()). deleted(Path, Vfs) -> do_deleted(Path, Vfs, []). -spec get_modkey(stat()) -> integer(). get_modkey(Stat) -> gleam@int:max(erlang:element(6, Stat), erlang:element(7, Stat)). -spec map_error({ok, GNE} | {error, fs_error()}) -> {ok, GNE} | {error, fs_error()}. map_error(Result) -> gleam@result:map_error(Result, fun(Err) -> case Err of eacces -> Err; enotdir -> Err; enoent -> Err; _ -> {e_unknown, gleam@string:inspect(Err)} end end). -spec readdir(binary()) -> {ok, list(binary())} | {error, fs_error()}. readdir(Path) -> _pipe = polly_ffi:readdir(Path), _pipe@1 = map_error(_pipe), gleam@result:map( _pipe@1, fun(_capture) -> gleam@list:sort(_capture, fun gleam@string:compare/2) end ). -spec lstat(binary()) -> {ok, stat()} | {error, fs_error()}. lstat(Path) -> map_error(polly_ffi:lstat(Path)). -spec diff_children( fun((binary()) -> boolean()), integer(), binary(), binary(), integer(), list(vfs()), list(binary()), list(vfs()), list(event()) ) -> hydration_result(). diff_children( Filter, Depth, Path, Name, Modkey, Old_children, New_entries, New_children, Events ) -> case {Old_children, New_entries} of {[], []} -> {success, {folder, Name, Modkey, lists:reverse(New_children)}, Events}; {[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, Events) of {success, New_vfs, Events@1} -> New_children@1 = [New_vfs | New_children], diff_children( Filter, Depth, Path, Name, Modkey, Rest_old, Rest_new, New_children@1, Events@1 ); skipped -> Events@2 = lists:append( deleted(Path, First_old), Events ), diff_children( Filter, Depth, Path, Name, Modkey, Rest_old, Rest_new, New_children, Events@2 ); {errored, _, _} = Err -> Err end; gt -> case hydrate(Filter, Depth, Path, First_new) of {success, New_vfs@1, _} -> Events@3 = lists:append( created(Path, New_vfs@1), Events ), New_children@2 = [New_vfs@1 | New_children], diff_children( Filter, Depth, Path, Name, Modkey, Old_children, Rest_new, New_children@2, Events@3 ); skipped -> diff_children( Filter, Depth, Path, Name, Modkey, Old_children, Rest_new, New_children, Events ); {errored, _, _} = Err@1 -> Err@1 end; lt -> Events@4 = lists:append(deleted(Path, First_old), Events), diff_children( Filter, Depth, Path, Name, Modkey, Rest_old, New_entries, New_children, Events@4 ) end; {[], [First_new@1 | Rest_new@1]} -> case hydrate(Filter, Depth, Path, First_new@1) of {success, New_vfs@2, _} -> Events@5 = lists:append(created(Path, New_vfs@2), Events), New_children@3 = [New_vfs@2 | New_children], diff_children( Filter, Depth, Path, Name, Modkey, Old_children, Rest_new@1, New_children@3, Events@5 ); skipped -> diff_children( Filter, Depth, Path, Name, Modkey, Old_children, Rest_new@1, New_children, Events ); {errored, _, _} = Err@2 -> Err@2 end; {[First_old@1 | Rest_old@1], []} -> Events@6 = lists:append(deleted(Path, First_old@1), Events), diff_children( Filter, Depth, Path, Name, Modkey, Rest_old@1, New_entries, New_children, Events@6 ) end. -spec diff( fun((binary()) -> boolean()), integer(), binary(), vfs(), list(event()) ) -> hydration_result(). diff(Filter, Depth, Path, Vfs, Events) -> Full_path = filepath:join(Path, erlang:element(2, Vfs)), gleam@bool:guard( not Filter(Full_path), skipped, fun() -> case lstat(Full_path) of {ok, Stat} -> gleam@bool:guard( erlang:element(4, Stat) /= read_write, skipped, fun() -> case {erlang:element(3, Stat), Vfs} of {regular, {file, Name, Old_key}} -> New_key = get_modkey(Stat), case New_key =:= Old_key of true -> {success, Vfs, Events}; false -> {success, {file, Name, New_key}, [{changed, Full_path} | Events]} end; {directory, {folder, _, _, _}} when Depth =:= 0 -> {success, Vfs, Events}; {directory, {folder, Name@1, _, Old_children}} when Depth =/= 0 -> New_key@1 = get_modkey(Stat), 'try'( Full_path, readdir(Full_path), fun(New_entries) -> diff_children( Filter, Depth - 1, Full_path, Name@1, New_key@1, Old_children, New_entries, [], Events ) end ); {_, _} -> case hydrate_stat( Filter, Depth, erlang:element(2, Vfs), Full_path, Stat ) of {success, New_vfs, _} -> Events@1 = lists:append( deleted(Path, Vfs), created(Path, New_vfs) ), {success, New_vfs, Events@1}; _ = Res -> Res end end end ); {error, eacces} -> skipped; {error, enoent} -> skipped; {error, E} -> {errored, Path, E} end end ). -spec hydrate_children( fun((binary()) -> boolean()), integer(), binary(), list(binary()), list(vfs()) ) -> {ok, list(vfs())} | {error, {binary(), fs_error()}}. hydrate_children(Filter, Depth, Path, Children, Acc) -> case Children of [] -> {ok, lists:reverse(Acc)}; [First | Rest] -> case hydrate(Filter, Depth, Path, First) of {success, Vfs, _} -> hydrate_children(Filter, Depth, Path, Rest, [Vfs | Acc]); skipped -> hydrate_children(Filter, Depth, Path, Rest, Acc); {errored, Path@1, Err} -> {error, {Path@1, Err}} end end. -spec hydrate(fun((binary()) -> boolean()), integer(), binary(), binary()) -> hydration_result(). hydrate(Filter, Depth, Path, Name) -> Full_path = filepath:join(Path, Name), gleam@bool:guard( not Filter(Full_path), skipped, fun() -> case lstat(Full_path) of {ok, Stat} -> hydrate_stat(Filter, Depth, Name, Full_path, Stat); {error, eacces} -> skipped; {error, enoent} -> skipped; {error, E} -> {errored, Full_path, E} end end ). -spec hydrate_stat( fun((binary()) -> boolean()), integer(), binary(), binary(), stat() ) -> hydration_result(). hydrate_stat(Filter, Depth, Name, Full_path, Stat) -> gleam@bool:guard( erlang:element(4, Stat) /= read_write, skipped, fun() -> case erlang:element(3, Stat) of regular -> {success, {file, Name, get_modkey(Stat)}, []}; directory when Depth =:= 0 -> {success, {folder, Name, get_modkey(Stat), []}, []}; directory when Depth =/= 0 -> 'try'( Full_path, readdir(Full_path), fun(Entries) -> case hydrate_children( Filter, Depth - 1, Full_path, Entries, [] ) of {ok, Children} -> {success, {folder, Name, get_modkey(Stat), Children}, []}; {error, {Path, Reason}} -> {errored, Path, Reason} end end ); _ -> skipped end end ). -spec watch_with(options(), GMF, fun((GMF, event()) -> GMF)) -> {ok, watcher(GMF)} | {error, {binary(), fs_error()}}. watch_with(Options, Initial, Callback) -> gleam@result:'try'( (gleam@list:try_map( erlang:element(3, Options), fun(Path) -> case hydrate( erlang:element(5, Options), erlang:element(4, Options), Path, <<""/utf8>> ) of {success, Vfs, _} -> {ok, {Path, {some, Vfs}}}; skipped -> {error, {Path, enoent}}; {errored, Path@1, Err} -> {error, {Path@1, Err}} end end )), fun(Roots) -> State = {Roots, Initial}, Stop = polly_ffi:repeatedly( erlang:element(2, Options), State, fun(State@1) -> {Roots@1, User_state} = State@1, gleam@list:fold( Roots@1, {[], User_state}, fun(_use0, _use1) -> {New_roots, User_state@1} = _use0, {Path@2, Vfs@1} = _use1, case Vfs@1 of {some, Vfs@2} -> case diff( erlang:element(5, Options), erlang:element(4, Options), Path@2, Vfs@2, [] ) of {success, Vfs@3, Events} -> User_state@2 = gleam@list:fold( Events, User_state@1, Callback ), {[{Path@2, {some, Vfs@3}} | New_roots], User_state@2}; _ -> Events@1 = deleted(Path@2, Vfs@2), User_state@3 = gleam@list:fold( Events@1, User_state@1, Callback ), {[{Path@2, none} | New_roots], User_state@3} end; none -> case hydrate( erlang:element(5, Options), erlang:element(4, Options), Path@2, <<""/utf8>> ) of {success, Vfs@4, _} -> Events@2 = created(Path@2, Vfs@4), User_state@4 = gleam@list:fold( Events@2, User_state@1, Callback ), {[{Path@2, {some, Vfs@4}} | New_roots], User_state@4}; _ -> {[{Path@2, Vfs@1} | New_roots], User_state@1} end end end ) end ), {ok, {watcher, Stop}} end ). -spec watch(options(), fun((event()) -> any())) -> {ok, watcher(nil)} | {error, {binary(), fs_error()}}. watch(Options, Callback) -> watch_with( Options, nil, fun(_, Event) -> Callback(Event), nil end ).