Current section
Files
Jump to
Current section
Files
src/radiate.gleam
import filespy
import gleam/io
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/otp/actor
import gleam/string
import shellout
/// Phantom type to indicate no directories are added
pub type NoDirectories
/// Phantom type to indicate directories have been added
pub type HasDirectories
/// Phantom type to indicate builder has no initializer
pub type NoInitializer
/// Phantom type to indicate builder has initializer
pub type HasInitializer
/// Phantom type to indicate builder has no callback
pub type NoCallback
/// Phantom type to indicate builder has callback
pub type HasCallback
/// Opaque builder type for reloader. Create with `new`.
pub opaque type Builder(state, has_dirs, has_initializer, has_callback) {
Builder(
dirs: List(String),
callback: fn(state, String) -> state,
initializer: Option(fn() -> state),
)
}
/// Construct a new builder
pub fn new() -> Builder(Nil, NoDirectories, NoInitializer, NoCallback) {
fn(state, path) {
io.println("Change in " <> path <> ", reloading.")
state
}
|> Builder(dirs: [], callback: _, initializer: None)
}
/// Add a directory to watch. The path can be relative or absolute.
/// macOS users: always use `"."` or an absolute path, otherwise it won't work
/// properly!
pub fn add_dir(
builder: Builder(state, has_dirs, has_initializer, has_callback),
dir: String,
) -> Builder(state, HasDirectories, has_initializer, has_callback) {
Builder(
dirs: [dir, ..builder.dirs],
callback: builder.callback,
initializer: builder.initializer,
)
}
/// Add a callback to be run after reload
pub fn on_reload(
builder: Builder(state, has_dirs, has_initializer, NoCallback),
callback: fn(state, String) -> state,
) -> Builder(state, has_dirs, has_initializer, HasCallback) {
Builder(
dirs: builder.dirs,
callback: callback,
initializer: builder.initializer,
)
}
/// Add an initializer
///
/// This will be run in the file watcher process, so it can be useful to, for
/// example, add state to an actor
pub fn set_initializer(
builder: Builder(state, has_dirs, NoInitializer, has_callback),
initializer: fn() -> state,
) -> Builder(state, has_dirs, HasInitializer, has_callback) {
Builder(
dirs: builder.dirs,
callback: builder.callback,
initializer: Some(initializer),
)
}
type Module
type What
@external(erlang, "code", "modified_modules")
fn modified_modules() -> List(Module)
@external(erlang, "code", "purge")
fn purge(module: Module) -> Bool
@external(erlang, "code", "atomic_load")
fn atomic_load(modules: List(Module)) -> Result(Nil, List(#(Module, What)))
/// Start reloader, if no state has been set
pub fn start(builder: Builder(Nil, HasDirectories, NoInitializer, has_callback)) {
builder
|> set_initializer(fn() { Nil })
|> start_state()
}
/// Start reloader, ensuring that a state has been set
pub fn start_state(
builder: Builder(state, HasDirectories, HasInitializer, has_callback),
) {
filespy.new()
|> filespy.set_initializer(fn() {
let assert Some(init) = builder.initializer
init()
})
|> filespy.add_dirs(builder.dirs)
|> filespy.set_actor_handler(fn(msg, state) {
case msg {
filespy.Custom(_) -> actor.continue(state)
filespy.Change(path, events) -> {
// todo fold over state... gah!
list.fold(events, state, fn(state, event) {
case event {
filespy.Closed | filespy.Modified -> {
// Check if path ends in '.gleam'
case string.ends_with(path, ".gleam") {
True -> {
// Rebuild
let build_result =
shellout.command(["build"], run: "gleam", in: ".", opt: [])
case build_result {
Ok(_output) -> {
let mods = modified_modules()
list.each(mods, purge)
let _ = atomic_load(mods)
builder.callback(state, path)
}
Error(#(_status, message)) -> {
message
|> io.print_error
state
}
}
}
_ -> state
}
}
_ -> state
}
})
|> actor.continue
}
}
})
|> filespy.start()
}
// todo:
// - make it so only one callback can be set (maybe one per directory?)
// - default state is nil, default callback is printing reloaded
// - callback returns actor stuff