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.PlatformService
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.
Show page setup
// Setup for the checked examples on this page.
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.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript
/// Fails the docs test when an example's result differs from the value shown.open Axial
open Axial.Console
AxialConsolelet confirm question : Flow<#IHasConsole, Never, bool> =
flow {
do! Console.write $"{question} [y/N] "
let! answer = Console.readLine
return
match answer with
| null -> false
| value -> value.Trim().ToLowerInvariant() = "y"
}
confirm: 'a -> Flow<'b,Never,bool>question: 'aAxial.Flow`3Axial.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>value: StringTrim: 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
Console.readLine returns null at end of input, so confirm treats EOF the same as a blank line: a non-affirmative answer, and it never calls Trim on a null value. The environment constraint requires callers of confirm to supply a console; the workflow cannot access the terminal without one.
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 askToContinue () : Task<Exit<bool, Never>> =
confirm "Continue?" |> Flow.startTask { Console = Console.live }
FsLiveDocsGeneratedPage4_F9357565CFEE.AppEnvConsole: IConsoleAxial.Console.IConsoleProvides explicit access to standard console and terminal I/O.
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.
this: AppEnvConsole: AppEnv -> unit -> IConsoleaskToContinue: unit -> Task<Exit<bool,Never>>System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Axial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
boolAn abbreviation for the CLI type . Basic Types
Axial.NeverRepresents an error channel that cannot occur.
confirm: 'a -> Flow<'b,Never,bool>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.FlowstartTask: 'env -> Flow<'env,'error,'value> -> Task<Exit<'value,'error>>Starts the workflow immediately and returns a task handle for its final exit. The work is already in flight when this returns. Use Flow.toAsync for a cold handle. The environment used by the workflow. The workflow to start. A task that completes with the workflow exit. let running = workflow |> Flow.startTask environment
Axial.Console.ConsoleModulelive: IConsoleCreates a live console service backed by .
For a runtime assembled with layers, wrap it: Layer.succeed Console.live. See
building a base runtime.
Reading and writing
Line-oriented operations read and write standard streams. 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>Console.input, Console.output, and Console.error return the underlying TextReader and TextWriter values when
you need to hand a stream to another API. Console.openStandardInput, openStandardOutput, and openStandardError
return raw Stream values for binary work.
These operations do not produce typed failures. A console write that throws, for instance on a closed pipe, 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`3Axial.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>Console.isInputRedirected, isOutputRedirected, and isErrorRedirected report the state of each stream.
Console.inputEncoding and Console.outputEncoding read the current encodings; setInputEncoding and
setOutputEncoding change them.
Terminal control
For interactive programs the service exposes terminal control 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.
The release is a flow, so it uses the same Console operation from the environment:
let withHiddenCursor (body: Flow<AppEnv, Never, 'value>) : Flow<AppEnv, Never, 'value> =
flow {
do! Flow.scopeFinalizer (Console.setCursorVisible true)
do! Console.setCursorVisible false
return! body
}
|> Flow.scoped
withHiddenCursor: Flow<AppEnv,Never,'value> -> Flow<AppEnv,Never,'value>body: Flow<AppEnv,Never,'value>Axial.Flow`3FsLiveDocsGeneratedPage4_F9357565CFEE.AppEnvAxial.NeverRepresents an error channel that cannot occur.
valueflow: FlowBuilderThe universal flow { } computation expression.
Axial.FlowscopeFinalizer: Flow<'env,Never,unit> -> Flow<'env,'error,unit>Registers a release flow with the current runtime scope. Use this when cleanup should run when the surrounding runtime, fiber, or layer scope closes, rather than when the current expression ends (Flow.ensuring). The release runs without interruption and with the current environment; a defect it raises becomes a defect of the flow that closes the scope. The release flow run when the current scope closes. A flow that registers the release.
Axial.Console.ConsoleModulesetCursorVisible: bool -> Flow<'env,'error,unit>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
Testing
Substitute an IConsole implementation. The interface is wide, so implement the recording double once in a test
helper rather than in each test. The test fakes recipe provides a complete RecordingConsole
— one that reads its input from a string, records what is written, and keeps terminal state in fields — with checked
assertions against the recorded output and cursor visibility.
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.

