-module(filespy). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -define(FILEPATH, "src/filespy.gleam"). -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, 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(FSW) :: {change, binary(), list(event())} | {custom, FSW}. -opaque builder(FSX, FSY, FSZ, FTA, FTB) :: {builder, list(binary()), gleam@option:option(fun((FSX, change(FTB)) -> gleam@otp@actor:next(FSX, change(FTB)))), gleam@option:option(fun((gleam@erlang@process:subject(change(FTB))) -> {ok, gleam@otp@actor:initialised(FSX, change(FTB), gleam@erlang@process:subject(change(FTB)))} | {error, binary()}))} | {gleam_phantom, FSY, FSZ, FTA}. -file("src/filespy.gleam", 84). ?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", 96). ?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(FTQ, any(), FTS, FTT, FTU), binary()) -> builder(FTQ, has_directories(), FTS, FTT, FTU). add_dir(Builder, Directory) -> {builder, [Directory | erlang:element(2, Builder)], erlang:element(3, Builder), erlang:element(4, Builder)}. -file("src/filespy.gleam", 107). ?DOC(" Add multiple directories at once\n"). -spec add_dirs(builder(FUF, any(), FUH, FUI, FUJ), list(binary())) -> builder(FUF, has_directories(), FUH, FUI, FUJ). add_dirs(Builder, Directories) -> {builder, lists:append(Directories, erlang:element(2, Builder)), erlang:element(3, Builder), erlang:element(4, Builder)}. -file("src/filespy.gleam", 160). ?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(FVH, FVI, no_handler(), FVJ, FVK), fun((FVH, change(FVK)) -> gleam@otp@actor:next(FVH, change(FVK))) ) -> builder(FVH, FVI, has_handler(), FVJ, FVK). set_actor_handler(Builder, Handler) -> {builder, erlang:element(2, Builder), {some, Handler}, erlang:element(4, Builder)}. -file("src/filespy.gleam", 176). ?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(FVX, FVY, FVZ, no_initial_state(), FWA), FVX) -> builder(FVX, FVY, FVZ, has_initial_state(), FWA). set_initial_state(Builder, State) -> {builder, erlang:element(2, Builder), erlang:element(3, Builder), {some, fun(Self) -> {ok, begin _pipe = gleam@otp@actor:initialised(State), gleam@otp@actor:returning(_pipe, Self) end} end}}. -file("src/filespy.gleam", 135). ?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, FUV, no_handler(), no_initial_state(), FUW), fun((binary(), event()) -> nil) ) -> builder(nil, FUV, has_handler(), has_initial_state(), FUW). 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", 197). ?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(FWL, FWM, FWN, no_initial_state(), FWO), fun((gleam@erlang@process:subject(change(FWO))) -> {ok, gleam@otp@actor:initialised(FWL, change(FWO), gleam@erlang@process:subject(change(FWO)))} | {error, binary()}) ) -> builder(FWL, FWM, FWN, has_initial_state(), FWO). set_initializer(Builder, Initializer) -> {builder, erlang:element(2, Builder), erlang:element(3, Builder), {some, Initializer}}. -file("src/filespy.gleam", 219). ?DOC(" Decode an atom to an `Event`.\n"). -spec event_decoder() -> gleam@dynamic@decode:decoder(event()). event_decoder() -> gleam@dynamic@decode:map( gleam@erlang@atom:decoder(), fun(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 -> created; Ev@1 when Ev@1 =:= Deleted -> deleted; Ev@2 when Ev@2 =:= Modified -> modified; Ev@3 when Ev@3 =:= Closed -> closed; Ev@4 when Ev@4 =:= Renamed -> renamed; Ev@5 when Ev@5 =:= Removed -> deleted; Ev@6 when Ev@6 =:= Attrib -> attribute; Other -> {unknown, Other} end end ). -file("src/filespy.gleam", 242). ?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", 248). ?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", 260). ?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:select_other( 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", 273). ?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(), FXS) ) -> {ok, gleam@otp@actor:started(gleam@erlang@process:subject(change(FXS)))} | {error, gleam@otp@actor:start_error()}. start(Builder) -> Handler@1 = case erlang:element(3, Builder) of {some, Handler} -> Handler; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"filespy"/utf8>>, function => <<"start"/utf8>>, line => 276, value => _assert_fail, start => 7891, 'end' => 7933, pattern_start => 7902, pattern_end => 7915}) end, Initializer@1 = case erlang:element(4, Builder) of {some, Initializer} -> Initializer; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"filespy"/utf8>>, function => <<"start"/utf8>>, line => 277, value => _assert_fail@1, start => 7936, 'end' => 7986, pattern_start => 7947, pattern_end => 7964}) end, Composed_initializer = fun(Self) -> {Oks, Errs} = begin _pipe = erlang:element(2, Builder), _pipe@2 = gleam@list:map( _pipe, fun(Dir) -> Watcher = erlang:binary_to_atom( <<"fs_watcher"/utf8, Dir/binary>> ), _pipe@1 = fs:start_link(Watcher, Dir), gleam@result:map(_pipe@1, fun(Pid) -> {Pid, Watcher} end) end ), gleam@result:partition(_pipe@2) end, case Errs of [] -> _pipe@3 = Oks, gleam@list:each( _pipe@3, fun(Res) -> {_, Watcher@1} = Res, fs:subscribe(Watcher@1) end ), gleam@result:map(Initializer@1(Self), fun(A) -> _pipe@4 = A, gleam@otp@actor:selecting(_pipe@4, selector()) end); Errs@1 -> gleam@list:each( Oks, fun(Res@1) -> {Pid@1, _} = Res@1, gleam@erlang@process:kill(Pid@1) end ), {error, <<"Failed to start watcher: "/utf8, (gleam@string:inspect(Errs@1))/binary>>} end end, _pipe@5 = gleam@otp@actor:new_with_initialiser(5000, Composed_initializer), _pipe@6 = gleam@otp@actor:on_message(_pipe@5, Handler@1), gleam@otp@actor:start(_pipe@6).