-module(filespy). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([new/0, add_dir/2, add_dirs/2, set_actor_handler/2, set_initial_state/2, set_handler/2, set_initializer/2, selector/0, spec/1, start/1]). -export_type([no_directories/0, has_directories/0, no_handler/0, has_handler/0, no_initial_state/0, has_initial_state/0, event/0, change/1, builder/5]). -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( " This module is a wrapper around the erlang [FS](https://fs.n2o.dev/)\n" " library. It allows you to create a listener for file change events for any\n" " operating system.\n" "\n" " Note: on Linux and BSD, you need `inotify-tools` installed.\n" ). -type no_directories() :: any(). -type has_directories() :: any(). -type no_handler() :: any(). -type has_handler() :: any(). -type no_initial_state() :: any(). -type has_initial_state() :: any(). -type event() :: created | modified | closed | deleted | renamed | attribute | {unknown, gleam@erlang@atom:atom_()}. -type change(HXB) :: {change, binary(), list(event())} | {custom, HXB}. -opaque builder(HXC, HXD, HXE, HXF, HXG) :: {builder, list(binary()), gleam@option:option(fun((change(HXG), HXC) -> gleam@otp@actor:next(change(HXG), HXC))), gleam@option:option(fun(() -> HXC))} | {gleam_phantom, HXD, HXE, HXF}. -file("src/filespy.gleam", 78). ?DOC( " Create a new builder\n" "\n" " Use this with the `add_dir` and `handler` functions to configure the\n" " watcher\n" ). -spec new() -> builder(any(), no_directories(), no_handler(), no_initial_state(), nil). new() -> {builder, [], none, none}. -file("src/filespy.gleam", 90). ?DOC( " Add a directory to watch\n" "\n" " # Examples\n" "\n" " ```gleam\n" " filespy.new()\n" " |> filespy.add_dir(\"./watched\")\n" " ```\n" ). -spec add_dir(builder(HXV, any(), HXX, HXY, HXZ), binary()) -> builder(HXV, has_directories(), HXX, HXY, HXZ). add_dir(Builder, Directory) -> {builder, [Directory | erlang:element(2, Builder)], erlang:element(3, Builder), erlang:element(4, Builder)}. -file("src/filespy.gleam", 101). ?DOC(" Add multiple directories at once\n"). -spec add_dirs(builder(HYK, any(), HYM, HYN, HYO), list(binary())) -> builder(HYK, has_directories(), HYM, HYN, HYO). add_dirs(Builder, Directories) -> {builder, lists:append(Directories, erlang:element(2, Builder)), erlang:element(3, Builder), erlang:element(4, Builder)}. -file("src/filespy.gleam", 154). ?DOC( " Set the actor handler\n" "\n" " Use this if you want to have a more thorough control over the generated\n" " actor. You will also need to set an initial state, with `set_initial_state`.\n" ). -spec set_actor_handler( builder(HZM, HZN, no_handler(), HZO, HZP), fun((change(HZP), HZM) -> gleam@otp@actor:next(change(HZP), HZM)) ) -> builder(HZM, HZN, has_handler(), HZO, HZP). set_actor_handler(Builder, Handler) -> {builder, erlang:element(2, Builder), {some, Handler}, erlang:element(4, Builder)}. -file("src/filespy.gleam", 170). ?DOC( " Set the initial state\n" "\n" " Use this if you want to have a more thorough control over the generated\n" " actor. You'll only have access to this state if you set your handler with\n" " `set_actor_handler`.\n" ). -spec set_initial_state(builder(IAC, IAD, IAE, no_initial_state(), IAF), IAC) -> builder(IAC, IAD, IAE, has_initial_state(), IAF). set_initial_state(Builder, State) -> {builder, erlang:element(2, Builder), erlang:element(3, Builder), {some, fun() -> State end}}. -file("src/filespy.gleam", 129). ?DOC( " Set the handler\n" "\n" " # Examples\n" "\n" " ```gleam\n" " filespy.new()\n" " |> filespy.add_dir(\"./watched\")\n" " |> filespy.set_handler(fn (path: String, event: filespy.Event) {\n" " case event {\n" " filespy.Created -> {\n" " io.println(\"File \" <> path <> \" created\")\n" " }\n" " _ -> {\n" " io.println(\"Something else happened to \" <> path)\n" " }\n" " }\n" " })\n" ). -spec set_handler( builder(nil, HZA, no_handler(), no_initial_state(), HZB), fun((binary(), event()) -> nil) ) -> builder(nil, HZA, has_handler(), has_initial_state(), HZB). set_handler(Builder, Handler) -> Wrapped_handler = fun(Event, _) -> case Event of {change, Path, Events} -> gleam@list:each(Events, fun(Ev) -> Handler(Path, Ev) end); _ -> nil end, gleam@otp@actor:continue(nil) end, _pipe = Builder, _pipe@1 = set_actor_handler(_pipe, Wrapped_handler), set_initial_state(_pipe@1, nil). -file("src/filespy.gleam", 186). ?DOC( " Set the initializer\n" "\n" " Use this if you want to, for example, return a subject in the state. The\n" " initializer will run in the actor's `on_init`, so it'll run under the\n" " watcher process\n" ). -spec set_initializer( builder(IAQ, IAR, IAS, no_initial_state(), IAT), fun(() -> IAQ) ) -> builder(IAQ, IAR, IAS, has_initial_state(), IAT). set_initializer(Builder, Initializer) -> {builder, erlang:element(2, Builder), erlang:element(3, Builder), {some, Initializer}}. -file("src/filespy.gleam", 204). ?DOC(" Decode an atom to an `Event`.\n"). -spec event_decoder() -> gleam@dynamic@decode:decoder(event()). event_decoder() -> gleam@dynamic@decode:then( {decoder, fun gleam@dynamic@decode:decode_dynamic/1}, fun(Content) -> case gleam_erlang_ffi:atom_from_dynamic(Content) of {error, _} -> gleam@dynamic@decode:failure(created, <<"Event"/utf8>>); {ok, Event} -> Created = erlang:binary_to_atom(<<"created"/utf8>>), Deleted = erlang:binary_to_atom(<<"deleted"/utf8>>), Modified = erlang:binary_to_atom(<<"modified"/utf8>>), Closed = erlang:binary_to_atom(<<"closed"/utf8>>), Renamed = erlang:binary_to_atom(<<"renamed"/utf8>>), Attrib = erlang:binary_to_atom(<<"attribute"/utf8>>), Removed = erlang:binary_to_atom(<<"removed"/utf8>>), case Event of Ev when Ev =:= Created -> gleam@dynamic@decode:success(created); Ev@1 when Ev@1 =:= Deleted -> gleam@dynamic@decode:success(deleted); Ev@2 when Ev@2 =:= Modified -> gleam@dynamic@decode:success(modified); Ev@3 when Ev@3 =:= Closed -> gleam@dynamic@decode:success(closed); Ev@4 when Ev@4 =:= Renamed -> gleam@dynamic@decode:success(renamed); Ev@5 when Ev@5 =:= Removed -> gleam@dynamic@decode:success(deleted); Ev@6 when Ev@6 =:= Attrib -> gleam@dynamic@decode:success(attribute); Other -> gleam@dynamic@decode:success({unknown, Other}) end end end ). -file("src/filespy.gleam", 232). ?DOC( " Decode a [`charlist`](https://hexdocs.pm/gleam_erlang/gleam/erlang/charlist.html#Charlist)\n" " from a `Dynamic` value, and convert it to `String`.\n" ). -spec charlist_decoder() -> gleam@dynamic@decode:decoder(binary()). charlist_decoder() -> gleam@dynamic@decode:map( {decoder, fun gleam@dynamic@decode:decode_dynamic/1}, fun(Chars) -> unicode:characters_to_binary(filespy_ffi:identity(Chars)) end ). -file("src/filespy.gleam", 238). ?DOC(" Decode `fs` events. Return type is `#(Pid, FileEvent, #(Path, List(Event)))`.\n"). -spec change_decoder() -> gleam@dynamic@decode:decoder({gleam@dynamic:dynamic_(), gleam@dynamic:dynamic_(), {binary(), list(event())}}). change_decoder() -> gleam@dynamic@decode:field( 0, {decoder, fun gleam@dynamic@decode:decode_dynamic/1}, fun(Pid) -> gleam@dynamic@decode:field( 1, {decoder, fun gleam@dynamic@decode:decode_dynamic/1}, fun(File_event) -> gleam@dynamic@decode:field( 2, begin gleam@dynamic@decode:field( 0, charlist_decoder(), fun(Path) -> gleam@dynamic@decode:field( 1, gleam@dynamic@decode:list( event_decoder() ), fun(Events) -> gleam@dynamic@decode:success( {Path, Events} ) end ) end ) end, fun(All_events) -> gleam@dynamic@decode:success( {Pid, File_event, All_events} ) end ) end ) end ). -file("src/filespy.gleam", 250). ?DOC(" Get a `Selector` which can be used to select for filesystem events.\n"). -spec selector() -> gleam@erlang@process:selector(change(any())). selector() -> gleam@erlang@process:selecting_anything( gleam_erlang_ffi:new_selector(), fun(Event) -> case gleam@dynamic@decode:run(Event, change_decoder()) of {ok, {_, _, {Path, Events}}} -> {change, Path, Events}; _ -> {change, <<""/utf8>>, []} end end ). -file("src/filespy.gleam", 259). ?DOC(" Get an actor `Spec` for the watcher\n"). -spec spec( builder(IBK, has_directories(), has_handler(), has_initial_state(), IBL) ) -> gleam@otp@actor:spec(IBK, change(IBL)). spec(Builder) -> _assert_subject = erlang:element(3, Builder), {some, Handler} = case _assert_subject of {some, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, value => _assert_fail, module => <<"filespy"/utf8>>, function => <<"spec"/utf8>>, line => 262}) end, _assert_subject@1 = erlang:element(4, Builder), {some, Initializer} = case _assert_subject@1 of {some, _} -> _assert_subject@1; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, value => _assert_fail@1, module => <<"filespy"/utf8>>, function => <<"spec"/utf8>>, line => 263}) end, {spec, fun() -> {Oks, Errs} = begin _pipe = erlang:element(2, Builder), _pipe@2 = gleam@list:map( _pipe, fun(Dir) -> Atom = erlang:binary_to_atom( <<"fs_watcher"/utf8, Dir/binary>> ), _pipe@1 = fs:start_link(Atom, Dir), gleam@result:map(_pipe@1, fun(Pid) -> {Pid, Atom} end) end ), gleam@result:partition(_pipe@2) end, case Errs of [] -> _pipe@3 = Oks, gleam@list:each( _pipe@3, fun(Res) -> {_, Atom@1} = Res, fs:subscribe(Atom@1) end ), {ready, Initializer(), selector()}; Errs@1 -> gleam@list:each( Oks, fun(Res@1) -> {Pid@1, _} = Res@1, gleam@erlang@process:kill(Pid@1) end ), {failed, <<"Failed to start watcher: "/utf8, (gleam@string:inspect(Errs@1))/binary>>} end end, 5000, Handler}. -file("src/filespy.gleam", 305). ?DOC( " Start an actor which will receive filesystem events\n" "\n" " In order for this to work, you'll need to have set some directories to be\n" " watched, with `add_dir`, and set a handler with either `set_handler` or\n" " `set_actor_handler` and `set_initial_state`.\n" ). -spec start( builder(any(), has_directories(), has_handler(), has_initial_state(), IBV) ) -> {ok, gleam@erlang@process:subject(change(IBV))} | {error, gleam@otp@actor:start_error()}. start(Builder) -> _pipe = spec(Builder), gleam@otp@actor:start_spec(_pipe).