Terminus

Terminus is a Scala 3 library for working with the terminal. It currently supports JVM, Scala Native, and Javascript backends.

Setup

To use Terminus, add the following to your build.sbt

libraryDependencies += "org.creativescala" %% "terminus-core" % "0.1"

Usage

Import Terminus

import terminus.*

Now you can call methods on the Terminal object. The core methods are read and write, but there are also methods to change color, move the cursor, erase content, and so on. On most terminals you will need to call flush or your output won't appear. Wrap a call to run around your entire program. Here's a small example that prints green text.

Terminal.run {
  Terminal.display.bold {
    Terminal.foreground.green {
      Terminal.write("This is Terminus!")
      Terminal.flush()
    }
  }
}

This produces the following output.

See the Examples for more involved use cases.

Design

The API in the terminus package provides a functional API for working with the terminal. Methods consume and return context functions with type

type Program[A] = Terminal ?=> A

where Terminal is a backend specific implementation that handles interfacing with the terminal. A Program represents a function that, when run, will do something with the terminal.

Context functions are new to Scala 3, so many developers may not be familiar with how they work. There are three rules for working with them, described below.

The first rule is that if the compiler can tell that a context function is expected it will automatically create one. We can do this with a Program type annotation. Here are some examples

// Most of the methods on Terminal return programs
val aTerminalOperation: Program[Unit] = Terminal.write("Some text")

// Any expression can be a Program with a type annotation
val aProgram: Program[Int] = 1 + 1

The second rule is that context functions will be automatically applied if there is a given value of the appropriate type in scope. This is what allows us to write effectful code in so-called direct-style, which just means writing normal code without monads or other complications. Here's an example that mixes effectful code, using the terminal, with some normal code. Notice that the entire result is a Program. This type annotation means the compiler constructs a context function around the entire block.

val writeSomeStuff: Program[Int] = {
  Terminal.write("Some output")
  // We can mix normal code in
  val result = 1 + 1
  Terminal.write("More output")
  Terminal.flush()
  result
}

We can do the same thing with a method, by specifying the return type is a Program. Here's an example.

def doSomeStuff(): Program[Int] = {
  Terminal.write("Some output")
  val result = 1 + 1
  Terminal.write("More output")
  Terminal.flush()
  result
}

The final rule is if we don't tell the compiler we're expecting a context function, we may get an error when the compiler attempts to apply a context function to a given value that does not exist.

Terminal.write("Some output")
// error: 
// No given instance of type terminus.effect.Writer was found for parameter of (terminus.effect.Writer) ?=> Unit

We can solve this by either adding a context function type annotation

val ok: Program[Unit] = Terminal.write("Some output")

or by providing a given value of the required type, which is what Terminal.run does.

Terminal.run(Terminal.write("Some output"))

If you want to work directly with the terminal, without working with context functions, you can work with the types in terminus.effect.

Low-level Code

All the ANSI escape codes used by Terminus are defined in terminus.effect.AnsiCodes. This can be useful if you want to write escape codes directly to the terminal without the abstractions provided by the Terminus DSL. Here's a simple example.

import terminus.effect.AnsiCodes

AnsiCodes.foreground.red
// res6: String = "\u001b[31m"
AnsiCodes.erase.line
// res7: String = "\u001b[2K"

Notes

You won't be able to run terminal programs from sbt if the JVM forks when running. That is, if you have the setting

run / fork := true

the forked JVM won't have access to a terminal and therefore any terminal programs will not work as expected.

Fansi may be a better choice if your only interest in styling output printed to a terminal.

Cue4s provides higher level abstractions for building UIs in the terminal.

Further Reading

The terminal is much more evolved than designed. I haven't been able to find a single document that describes all the features. Here are a few resources that I've found useful for piecing together how things should work: