/* * Copyright 2024 Magnus Madsen * * Use of this source code is governed by the Apache 2.0 license * that can be found in the LICENSE.md file. */////// Effect for interacting with the console, and its functions.///pubmod Sys.Console {use Sys.Consoleimport java.lang.Systemimport java.io.ByteArrayOutputStream////// Effect for interacting with the console.////// The operations are the interface that a handler implements.////// Programs normally use `print`, `println`, `eprint`, and `eprintln`/// which accept any value with a `ToString` instance.///pubeffConsole {////// Reads a single line from the console.////// Returns `None` if the end of input has been reached.///defreadln(): Option[String]////// Writes the given string `s` to the standard out.///defwrite(s: String): Unit////// Writes the given string `s` to the standard out followed by a new line.///defwriteLine(s: String): Unit////// Writes the given string `s` to the standard err.///defwriteErr(s: String): Unit////// Writes the given string `s` to the standard err followed by a new line.///defwriteErrLine(s: String): Unit }////// Prints the string representation of `x` to the standard out.///pubdefprint(x: a): Unit \ ConsolewithToString[a] =Console.write(ToString.toString(x))////// Prints the string representation of `x` to the standard out followed by a new line.///pubdefprintln(x: a): Unit \ ConsolewithToString[a] =Console.writeLine(ToString.toString(x))////// Prints the string representation of `x` to the standard err.///pubdefeprint(x: a): Unit \ ConsolewithToString[a] =Console.writeErr(ToString.toString(x))////// Prints the string representation of `x` to the standard err followed by a new line.///pubdefeprintln(x: a): Unit \ ConsolewithToString[a] =Console.writeErrLine(ToString.toString(x))////// Prints prompt `p`, reads a line, and applies `f` to the input.////// If `f` returns `Err(msg)`, prints `msg` to stderr and re-prompts./// If `f` returns `Ok(v)`, returns `Some(v)`.////// Returns `None` if the end of input has been reached.///pubdefreadlnWith(p: a, f: String -> Result[String, b]): Option[b] \ ConsolewithToString[a] =Console.print(p);matchConsole.readln() {case None => Nonecase Some(input) => matchf(input) {case Ok(v) => Some(v)case Err(msg) => Console.eprintln(msg); readlnWith(p, f) } }////// Prints prompt `p`, reads a line, and parses it as a value of type `b`.////// Leading and trailing whitespace is ignored.////// If the input cannot be parsed, prints an error to stderr and re-prompts.////// Returns `None` if the end of input has been reached.///pubdefreadlnAs(p: a): Option[b] \ ConsolewithToString[a], FromString[b] =readlnWith(p, s -> matchFromString.fromString(String.trim(s)) {case Some(v) => Ok(v)case None => Err("Invalid input. Please try again.") })////// Prints prompt `p` with a yes/no hint and reads a boolean answer.////// Appends `[Y/n]` if `default#default` is `true`, or `[y/N]` if `false`./// Empty input returns `default#default`. Recognizes "y", "yes", "n", "no" (case-insensitive)./// Any other input prints an error to stderr and re-prompts.////// Returns `default#default` if the end of input has been reached.///pubdefconfirm(p: a, default: {default = Bool}): Bool \ ConsolewithToString[a] =lethint = if (default#default) "[Y/n]"else"[y/N]";letparse = s -> matchString.toLowerCase(String.trim(s)) {case "" => Ok(default#default)case "y" => Ok(true)case "yes" => Ok(true)case "n" => Ok(false)case "no" => Ok(false)case_ => Err("Please answer 'y' or 'n'.") };readlnWith("${p} ${hint} ", parse) |> Option.getWithDefault(default#default)////// Handles the `Console` effect of the given function `f`.////// In other words, re-interprets the `Console` effect using the `IO` effect.///pubdefhandle(f: a -> b \ ef): a -> b \ (ef - Console) + IO = x ->// NB: System.console() returns null when there is no interactive terminal (e.g. IDE, piped input).letreader = matchObject.toOption(System.console()) {case Some(c) => () -> Object.toOption(c.readLine())case None => () -> readLineFromStdIn() };run {f(x) } withhandlerConsole {defreadln(k) = k(reader())defwrite(s, k) = { System.out.print(s); System.out.flush(); k() }defwriteLine(s, k) = { System.out.println(s); System.out.flush(); k() }defwriteErr(s, k) = { System.err.print(s); System.err.flush(); k() }defwriteErrLine(s, k) = { System.err.println(s); System.err.flush(); k() } }////// Runs the `Console` effect of the given function `f`.////// In other words, re-interprets the `Console` effect using the `IO` effect.///@DefaultHandlerpubdefrunWithIO(f: Unit -> a \ ef): a \ (ef - Console) + IO = handle(f)()////// Reads a line from `System.in` without reading past the end of the line.////// Returns `None` if the end of input has been reached before any byte was read.////// A `BufferedReader` reads ahead and would swallow lines meant for a later/// reader, e.g. when the `Console` effect is handled more than once./// Reading one byte at a time is cheap because `System.in` is itself buffered.///defreadLineFromStdIn(): Option[String] \ IO =letbytes = new ByteArrayOutputStream();// Returns `true` if a newline was consumed and `false` if the end of input was reached.defloop() = match System.in.read() {case -1 => false// end of inputcase 10 => true// '\n'caseb => bytes.write(b); loop() };letsawNewline = loop();if (notsawNewlineandbytes.size() ==0) Noneelse {lets = String.fromBytes(Array.toVector(bytes.toByteArray())); Some(Option.getWithDefault(s, String.stripSuffix(substr = "\r", s))) }}