Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.HttpClient
open Axial.PlatformService
open Axial.Process
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptAxial.Console replaces System.Console with a service a workflow must declare. IConsole covers the three
standard streams, the redirection and encoding state around them, and interactive terminal control — cursor, colour,
title, and key reads.
open Axial
open Axial.Console
AxialConsolelet confirm question : Flow<#IHasConsole, Never, bool> =
flow {
do! Console.write $"{question} [y/N] "
let! answer = Console.readLine
return answer.Trim().ToLowerInvariant() = "y"
}
confirm: 'a -> Flow<'b,Never,bool>question: 'aAxial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.Console.IHasConsoleDeclares that an environment supplies the console service. Implement this on the environment supplied at the host edge. A workflow that reads or writes the console constrains its environment with 'env :> IHasConsole.
Axial.NeverRepresents an error channel that cannot occur.
boolAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
Axial.Console.ConsoleModulewrite: string -> Flow<'env,'error,unit>answer: stringreadLine: Flow<'env,'error,string>Trim: unit -> stringRemoves all leading and trailing white-space characters from the current object. The string that remains after all white-space characters are removed from the start and end of the current string. If no characters can be trimmed from the current instance, the method returns the current instance unchanged.
ToLowerInvariant: unit -> stringReturns a copy of this object converted to lowercase using the casing rules of the invariant culture. The lowercase equivalent of the current string.
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
Supplying the service
Implement IHasConsole on the application environment and supply Console.live at the host edge:
type AppEnv =
{ Console: IConsole }
interface IHasConsole with
member this.Console = this.Console
let! exit = confirm "Continue?" |> Flow.startTask { Console = Console.live }Reading and writing
Line-oriented operations cover the common cases. Each returns a flow with an unconstrained error channel, so they compose into a workflow with any failure type:
Console.write "partial" // stdout, no newline
Console.writeLine "done" // stdout
Console.writeError "partial" // stderr, no newline
Console.writeErrorLine "failed" // stderr
Console.read // next character as an int, -1 at end of input
Console.readLine // next line
Axial.Console.ConsoleModulewrite: string -> Flow<'env,'error,unit>writeLine: string -> Flow<'env,'error,unit>writeError: string -> Flow<'env,'error,unit>writeErrorLine: string -> Flow<'env,'error,unit>read: Flow<'env,'error,int>readLine: Flow<'env,'error,string>These operations do not produce typed failures. A console write that throws — a closed pipe, for instance — is a defect, not an expected error. Handle it as described in defects if the workflow should survive it.
Redirection and encoding
Check redirection before using anything interactive. A program whose output is piped into another process has no cursor to move:
let report line : Flow<#IHasConsole, Never, unit> =
flow {
let! redirected = Console.isOutputRedirected
if redirected then
return! Console.writeLine line
else
do! Console.setForegroundColor ConsoleColor.Green
do! Console.writeLine line
return! Console.resetColor
}
report: string -> Flow<'a,Never,unit>line: stringAxial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.Console.IHasConsoleDeclares that an environment supplies the console service. Implement this on the environment supplied at the host edge. A workflow that reads or writes the console constrains its environment with 'env :> IHasConsole.
Axial.NeverRepresents an error channel that cannot occur.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
redirected: boolAxial.Console.ConsoleModuleisOutputRedirected: Flow<'env,'error,bool>writeLine: string -> Flow<'env,'error,unit>setForegroundColor: ConsoleColor -> Flow<'env,'error,unit>System.ConsoleColorSpecifies constants that define foreground and background colors for the console.
Green: ConsoleColorThe color green.
resetColor: Flow<'env,'error,unit>Terminal control
For interactive programs the service exposes the terminal surface directly: clear, beep, foregroundColor /
setForegroundColor, backgroundColor / setBackgroundColor, resetColor, cursorPosition /
setCursorPosition, cursorVisible / setCursorVisible, title / setTitle, and keyAvailable / readKey.
Console.setTreatControlCAsInput true delivers Ctrl+C to readKey instead of signalling the process, which is what
a full-screen terminal application wants.
Every one of these is mutable terminal state that outlives the workflow that set it. Restore what you change through a finalizer, so an interrupted or failed workflow cannot leave the user with an invisible cursor or a green prompt:
let withHiddenCursor (console: IConsole) body =
flow {
do! Flow.addFinalizer(fun _ ->
console.CursorVisible <- true
Task.CompletedTask)
do! Console.setCursorVisible false
return! body
}Substitute any IConsole implementation. A recording console over StringWriter is usually enough, and it makes
assertions ordinary value comparisons:
let recorded = StringWriter()
let testConsole =
{ new IConsole with
member _.Out = recorded
member _.WriteLine(value) = recorded.WriteLine value
// remaining members raise or return defaults
}
let! exit = report "ready" |> Flow.startTask { Console = testConsole }
test <@ recorded.ToString().Trim() = "ready" @>Fable
Console.live is not compiled for Fable, and Layer.succeed Console.live fails with PlatformNotSupportedException there. A
workflow that must run on both .NET and Fable should depend on its own narrow output contract and adapt it to
IConsole only in the .NET host. See packages and platforms.
Related
- Service contracts — why the dependency is in the type.
- Processes — the process service uses a console for stream wiring.

