Skip to content

CLI prompts in Scala 3, available on JS, JVM, and Native platforms

License

Notifications You must be signed in to change notification settings

neandertech/cue4s

Repository files navigation

Cue4s

cue4s Scala version support

Scala 3 library for CLI prompts that works on JVM, JS, and Native.

The inspiration is taken from a JS library prompts, and the eventual goal is to have cue4s support all the same functionality.

CleanShot.2024-11-25.at.09.26.30.mp4

Installation

  • Scala CLI: //> using dep tech.neander::cue4s::<version>
  • SBT: libraryDependencies += "tech.neander" %%% "cue4s" % "<version>"

Usage

This example is runnable on both JVM and Native (note how we're using sync). In fact, if you put into a file named test.sc you can run it with Scala CLI.

//> using dep tech.neander::cue4s::latest.release

import cue4s.*

Prompts.sync.use: prompts =>
  val day = prompts
    .singleChoice("How was your day?", List("great", "okay"))
    .getOrThrow

  val work = prompts.text("Where do you work?").getOrThrow

  val letters = prompts.multiChoiceAllSelected(
      "What are your favourite letters?",
      ('A' to 'F').map(_.toString).toList
    ).getOrThrow

For this to work on JS, you need to use the Future-based methods, example for that is provided in examples folder.

Auto-derivation for case classes

cue4s includes an experimental auto-derivation for case classes (and only them, currently), allowing you to create prompt chains:

//> using tech.neander::cue4s::latest.release

import cue4s.*

val validateName: String => Option[PromptError] = s =>
    Option.when(s.trim.isEmpty)(PromptError("name cannot be empty!"))

case class Attributes(
  @cue(_.text("Your name").validate(validateName))
  name: String,
  @cue(_.text("Checklist").multi("Wake up" -> true, "Grub a brush" -> true, "Put a little makeup" -> false))
  doneToday: Set[String],
  @cue(_.text("What did you have for breakfast").options("eggs", "sadness"))
  breakfast: String,
  @cue(_.text("Do you want to build a snowman?"))
  snowman: Boolean,
  @cue(_.text("How old are you?"))
  age: Int,
  @cue(_.text("What is the value of PI?"))
  pi: Float
) derives PromptChain

val attributes: Attributes = 
  Prompts.sync.use: p =>
    p.run(PromptChain[Attributes]).getOrThrow

There is no generic mechanism to define how parameters of different types will be handled, just a set of rules that felt right at the time of writing this library:

  1. If the type is String, and .options(...) is present in annotation, the prompt will become SingleChoice
  2. If the type is F[String] where F is one of List, Vector, Set, and either .options(...) or .multi(...) are present, then the prompt will become MultipleChoice
  3. If the type is Option[String], then empty value will be turned into None (check for emptiness will be run before any validation)

In the future more combinations can be added.

Cats Effect integration

A simple Cats Effect integration is provided, which wraps the future-based implementation of terminal interactions.

The integration is available only for JVM and JS targets.

Installation

  • Scala CLI: //> using dep tech.neander::cue4s-cats-effect::<version>
  • SBT: libraryDependencies += "tech.neander" %%% "cue4s-cats-effect" % "<version>"

Usage

//> using dep tech.neander::cue4s-cats-effect::latest.release

import cue4s.catseffect.*

import cats.effect.*
import cats.syntax.all.*

case class Info(
    day: String,
    work: String,
    letters: List[String],
)

object ioExample extends IOApp.Simple:
  def run: IO[Unit] =
    PromptsIO.make.use: prompts =>
      for
        _ <- IO.println("let's go")

        day = prompts
          .singleChoice("How was your day?", List("great", "okay"))
          .map(_.toEither)
          .flatMap(IO.fromEither)

        work = prompts
          .text("Where do you work?")
          .map(_.toEither)
          .flatMap(IO.fromEither)

        letter = prompts
          .multiChoiceAllSelected(
            "What are your favourite letters?",
            ('A' to 'F').map(_.toString).toList,
          )
          .map(_.toEither)
          .flatMap(IO.fromEither)

        info <- (day, work, letter).mapN(Info.apply)

        _ <- IO.println(info)
      yield ()

end ioExample

Platform support

On JS, we can only execute the prompts asynchronously, so the minimal usable implementation of a prompt will always return Future[Completion[Result]].

On JVM and Native, we can execute prompts synchronously, so the simplest implementation returns Completion[Result] - but methods wrapping the result in Future are provided for convenience.

This is encoded in the availablity of sync and async methods on the Prompts object (e.g. only Prompts.async is available on JS).

This library is published for Scala.js 1.16.0+, Scala Native 0.5, and JVM.