rad ✨

Hex Package Hex Docs License Build

A flexible task runner companion for the Gleam build manager.

A rad screenshot.

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!

Quick Start

$ 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

Requirements

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).

Installation

Gleam

$ gleam add rad

Usage

Basic Usage

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.

POSIX (Bash-Like)

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/

PowerShell

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.

Configuration

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)!
"""]

[rad]

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).

[[rad.formatters]]

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.

[[rad.tasks]]

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.

Advanced Usage

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 Runners available for any dependent projects!

Examples

// 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)
  }
}

In the shell

$ rad commit
Chuck Norris Emailed Me This Patch... I'm Not Going To Question It

Further Reading

For more information on all things rad, read the hexdocs.

Search Document