A flexible task runner companion for the Gleam build manager.
Rad has a variety of builtin features. Some of the more powerful include serving
documentation and watching the file system. With
rad docs serve
,
you can build docs for your project and all of its dependencies, and then serve
them together over HTTP
, like a small-scale hexdocs
service specific to your project. Plus, with
rad watch
you get
live reloading of any clients connected to the docs server in addition to
automated testing when saving files, for rapid feedback when coding and writing
docs.
Try this rad
one-liner:
$ # Press `Ctrl+Z` and use the `fg` command as needed
$ rad docs serve --all --host=0.0.0.0 &; rad watch
Then, visit localhost:7000
, or open
[your machine's LAN IP]:7000
from another device on your network, and watch
what happens when you edit and save one of your project's files!
Rad provides a helpful framework for automating repetitive actions, reducing mental burden, and lowering the potential for maintenance errors when developing your Gleam projects.
Make rad
your own by customizing it to suit your projects and workflows!
$ rad help # Print help information
$ rad shell # Start an Erlang shell
$ rad shell iex # Start an Elixir shell
$ rad shell deno # Start a JavaScript shell
$ rad shell node # Start a JavaScript shell
$ rad tree # Print the file structure
$ rad docs serve # Serve HTML documentation
$ rad watch # Automate project tasks
Either Node.js (>= v17.5
) or
Deno (>= v1.30
) is required to run
rad
. Although most rad
commands can be executed with the
Erlang runtime, rad
always initializes via a
JavaScript runtime
(Node.js, unless Deno is specified
as the default runtime in your project's gleam.toml
config).
$ gleam add rad
You must run rad
from your project's base directory (where gleam.toml
resides).
$ ./build/packages/rad/priv/rad <subcommand> [flags]
$ # or
$ gleam run --target=javascript --module=rad -- <subcommand> [flags]
Note: gleam run --target=erlang --module=rad ...
is currently unsupported!
For convenience when invoking rad
, first perform one of the following
operations in a manner consistent with your shell of choice. The goal is to get
priv/rad
or priv/rad.ps1
somewhere in your $PATH
; there are many ways to
accomplish this, these are merely some suggestions.
Alias priv/rad
$ alias rad='./build/packages/rad/priv/rad'
$ # To persist across sessions, add it to your .bashrc or an analogous file
Copy priv/rad
into your $PATH
$ sudo cp ./build/packages/rad/priv/rad /usr/local/bin/
Link priv/rad
into your $PATH
$ sudo git clone https://github.com/tynanbe/rad.git /usr/local/share/rad
$ sudo ln -s ../share/rad/priv/rad /usr/local/bin/
Alias priv/rad.ps1
PS> function rad { ./build/packages/rad/priv/rad.ps1 @Args }
PS> # To persist across sessions, add it to your $profile file
Copy priv/rad.ps1
into your $env:PATH
PS> # Create "${HOME}/bin"
PS> New-Item -Type Directory -Force "${HOME}/bin"
PS> # Add "${HOME}/bin" to $env:PATH
PS> $path = "${HOME}/bin"
PS> $sep = ";" # Use ":" for *nix
PS> $paths = $env:PATH -split $sep
PS> if ($paths -notcontains $path) {
$env:PATH = (@($path) + $paths | where { $_ }) -join $sep
}
PS> # To persist across sessions, add the previous lines to your $profile file
PS> # Copy rad.ps1
PS> Copy-Item "./build/packages/rad/priv/rad.ps1" -Destination "${HOME}/bin/"
Link priv/rad.ps1
into your $env:PATH
PS> # Create "${HOME}/bin"
PS> New-Item -Type Directory -Force "${HOME}/bin"
PS> # Add "${HOME}/bin" to $env:PATH
PS> $path = "${HOME}/bin"
PS> $sep = ";" # Use ":" for *nix
PS> $paths = $env:PATH -split $sep
PS> if ($paths -notcontains $path) {
$env:PATH = (@($path) + $paths | where { $_ }) -join $sep
}
PS> # To persist across sessions, add the previous lines to your $profile file
PS> # Create "${HOME}/src"
PS> New-Item -Type Directory -Force "${HOME}/src"
PS> # Clone the rad repository
PS> git clone https://github.com/tynanbe/rad.git "${HOME}/src/rad"
PS> # Link rad.ps1
PS> New-Item -ItemType SymbolicLink -Target "../src/rad/priv/rad.ps1" -Path "${HOME}/bin/rad.ps1"
After completing one of the previous operations, you should be able to invoke
rad
as follows.
$ rad <subcommand> [flags]
More information about rad
's standard subcommands can be found in
rad
hexdocs or with
rad help
.
You can extend rad
with your project's gleam.toml
configuration file.
[rad]
workbook = "my/workbook"
targets = ["erlang", "javascript"]
with = "javascript"
[[rad.formatters]]
name = "erlang"
check = ["erlfmt", "--check"]
run = ["erlfmt", "--write", "src/rad_ffi.erl"]
[[rad.formatters]]
name = "javascript"
check = ["deno", "fmt", "--check"]
run = ["deno", "fmt"]
[[rad.tasks]]
path = ["purple", "heart"]
run = ["echo", "💜 The dream you'll have here is a dream within a dream."]
shortdoc = "💜 The dream you'll have here is a dream within a dream."
[[rad.tasks]]
path = ["sparkles"]
run = ["echo", "✨ It's been a long road getting here..."]
[[rad.tasks]]
path = ["sparkling", "heart"]
run = ["sh", "-euc", """
echo \
💖 I was staring out the window and there it was, just fluttering there... \
$(rad version)!
"""]
In the base rad
table, you can define a custom
workbook
(see
Advanced Usage), a default array of compilation targets
that rad
tasks like build
and test
will cover, and a default runtime for
rad
to run all tasks with
(some tasks, like
shell
, will not
succeed with
the erlang
runtime; javascript
is the default).
The rad format
task runs the gleam
formatter along with any formatters defined in your
gleam.toml
config via the rad.formatters
table array. The name
, check
,
and run
fields are all mandatory for each formatter you define.
You can define your own basic tasks via the rad.tasks
table array. Few
assumptions are made about your environment, so rad
won't run your commands
through any shell interpreter on its own; however, the scope of your commands is
virtually unlimited, and you're free to specify your shell interpreter of
choice. The path
and run
fields are mandatory for each task you define,
while the shortdoc
field is optional. Both path
and run
must be formatted
as arrays of strings; the strings will generally be single words corresponding
to command line arguments. If your task has a shortdoc
, it will appear in
rad help
information as long as it has a visible parent path
.
The standard rad
workbook module exemplifies how to create a custom
workbook.gleam
module for your own project.
By providing main
and workbook
functions in your project's workbook.gleam
file, you can extend rad
's
standard
workbook
with
your own or write one entirely from scratch, optionally making it and your
Runner
s available for any
dependent projects!
// src/my/workbook.gleam
import gleam/dynamic
import gleam/json
import gleam/result
import glint.{type CommandInput}
import glint/flag
import rad
import rad/task.{type Result, type Task}
import rad/util
import rad/workbook.{type Workbook}
import rad/workbook/standard
import snag
pub fn main() -> Nil {
workbook()
|> rad.do_main
}
pub fn workbook() -> Workbook {
let standard_workbook = standard.workbook()
let assert Ok(root_task) =
[]
|> workbook.get(from: standard_workbook)
let assert Ok(help_task) =
["help"]
|> workbook.get(from: standard_workbook)
standard_workbook
|> workbook.task(
add: root
|> task.runner(into: root_task),
)
|> workbook.task(
add: workbook
|> workbook.help
|> task.runner(into: help_task),
)
|> workbook.task(
add: ["commit"]
|> task.new(run: commit)
|> task.shortdoc("Generate a questionable commit message"),
)
}
pub fn root(input: CommandInput, task: Task(Result)) -> Result {
let ver =
"version"
|> flag.get_bool(from: input.flags)
|> result.unwrap(or: False)
case ver {
True -> standard.root(input, task)
False -> workbook.help(from: workbook)(input, task)
}
}
pub fn commit(_input: CommandInput, _task: Task(Result)) -> Result {
let script =
"
fetch('http://whatthecommit.com/index.txt')
.then(async (response) => [response.status, await response.text()])
.then(
([status, text]) =>
console.log(JSON.stringify({ status: status, text: text.trim() }))
);
"
use output <- result.try(
util.javascript_run(deno: ["eval", script], or: ["--eval", script], opt: []),
)
let snag = snag.new("service unreachable")
use status <- result.try(
output
|> json.decode(using: dynamic.field(named: "status", of: dynamic.int))
|> result.replace_error(snag),
)
case status < 400 {
True ->
output
|> json.decode(using: dynamic.field(named: "text", of: dynamic.string))
|> result.replace_error(snag)
False -> Error(snag)
}
}
$ rad commit
Chuck Norris Emailed Me This Patch... I'm Not Going To Question It
For more information on all things rad
, read the
hexdocs.