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.
Shared 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.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actual
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
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
Because of the environment constraint, confirm cannot be called from a workflow that has not been given a
console, and it cannot reach the real terminal behind your back.
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 }
FsLiveDocsGeneratedPage3_65D5665AADA9.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 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>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`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>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:
let withHiddenCursor (console: IConsole) (body: Flow<AppEnv, Never, 'value>) : Flow<AppEnv, Never, 'value> =
flow {
do! Flow.scopeFinalizer(fun _ ->
console.CursorVisible <- true
Task.CompletedTask)
do! Console.setCursorVisible false
return! body
}
|> Flow.scoped
withHiddenCursor: IConsole -> Flow<AppEnv,Never,'value> -> Flow<AppEnv,Never,'value>console: IConsoleAxial.Console.IConsoleProvides explicit access to standard console and terminal I/O.
body: Flow<AppEnv,Never,'value>Axial.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.
FsLiveDocsGeneratedPage3_65D5665AADA9.AppEnvAxial.NeverRepresents an error channel that cannot occur.
valueflow: FlowBuilderThe universal flow { } computation expression.
Axial.FlowscopeFinalizer: (CancellationToken -> Task) -> Flow<'env,'error,unit>Registers an asynchronous finalizer with the current runtime scope. The finalizer to run when the current scope closes. A flow that registers the finalizer. Use this when a resource acquired by a subflow should live until the surrounding runtime or layer scope closes, rather than only until the current expression ends.
CursorVisible: boolSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
CompletedTask: TaskGets a task that has already completed successfully. The successfully completed task.
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 any IConsole implementation. A recording console over StringWriter is usually enough, and it makes
assertions ordinary value comparisons:
IConsole is a wide interface, so implement it once in a test helper rather than in each test. This one reads its
input from a string, records what is written, and keeps terminal state in fields:
type RecordingConsole(input: string) =
let reader = new StringReader(input)
let output = new StringWriter()
let error = new StringWriter()
let mutable inputEncoding = Text.Encoding.UTF8
let mutable outputEncoding = Text.Encoding.UTF8
let mutable foreground = ConsoleColor.Gray
let mutable background = ConsoleColor.Black
let mutable cursorLeft = 0
let mutable cursorTop = 0
let mutable cursorVisible = true
let mutable title = ""
let mutable treatControlC = false
member _.Written = output.ToString()
member _.IsCursorVisible = cursorVisible
interface IConsole with
member _.In = reader
member _.Out = output
member _.Error = error
member _.InputEncoding with get () = inputEncoding and set value = inputEncoding <- value
member _.OutputEncoding with get () = outputEncoding and set value = outputEncoding <- value
member _.IsInputRedirected = true
member _.IsOutputRedirected = true
member _.IsErrorRedirected = true
member _.KeyAvailable = false
member _.Read() = reader.Read()
member _.ReadLine() = reader.ReadLine()
member _.ReadKey _ = ConsoleKeyInfo()
member _.Write value = output.Write value
member _.WriteLine value = output.WriteLine value
member _.WriteError value = error.Write value
member _.WriteErrorLine value = error.WriteLine value
member _.OpenStandardInput() = Stream.Null
member _.OpenStandardOutput() = Stream.Null
member _.OpenStandardError() = Stream.Null
member _.Clear() = ()
member _.Beep() = ()
member _.ResetColor() = ()
member _.ForegroundColor with get () = foreground and set value = foreground <- value
member _.BackgroundColor with get () = background and set value = background <- value
member _.CursorLeft with get () = cursorLeft and set value = cursorLeft <- value
member _.CursorTop with get () = cursorTop and set value = cursorTop <- value
member _.CursorVisible with get () = cursorVisible and set value = cursorVisible <- value
member _.SetCursorPosition(left, top) = cursorLeft <- left; cursorTop <- top
member _.Title with get () = title and set value = title <- value
member _.TreatControlCAsInput with get () = treatControlC and set value = treatControlC <- value
FsLiveDocsGeneratedPage3_65D5665AADA9.RecordingConsoleinput: stringstringAn abbreviation for the CLI type . Basic Types
reader: StringReaderSystem.IO.StringReaderImplements a that reads from a string.
output: StringWriterSystem.IO.StringWriterImplements a for writing information to a string. The information is stored in an underlying .
error: StringWriterinputEncoding: Text.EncodingTextUTF8: Text.EncodingGets an encoding for the UTF-8 format. An encoding for the UTF-8 format.
System.Text.EncodingRepresents a character encoding.
outputEncoding: Text.Encodingforeground: ConsoleColorSystem.ConsoleColorSpecifies constants that define foreground and background colors for the console.
Gray: ConsoleColorThe color gray.
background: ConsoleColorBlack: ConsoleColorThe color black.
cursorLeft: intcursorTop: intcursorVisible: booltitle: stringtreatControlC: bool_: RecordingConsoleWritten: RecordingConsole -> unit -> stringToString: unit -> stringReturns a string containing the characters written to the current so far. The string containing the characters written to the current .
IsCursorVisible: RecordingConsole -> unit -> boolAxial.Console.IConsoleProvides explicit access to standard console and terminal I/O.
In: RecordingConsole -> unit -> TextReaderOut: RecordingConsole -> unit -> TextWriterError: RecordingConsole -> unit -> TextWriterInputEncoding: RecordingConsole -> unit -> Text.Encodingvalue: Text.EncodingOutputEncoding: RecordingConsole -> unit -> Text.EncodingIsInputRedirected: RecordingConsole -> unit -> boolIsOutputRedirected: RecordingConsole -> unit -> boolIsErrorRedirected: RecordingConsole -> unit -> boolKeyAvailable: RecordingConsole -> unit -> boolRead: RecordingConsole -> unit -> intRead: unit -> intReads the next character from the input string and advances the character position by one character. The next character from the underlying string, or -1 if no more characters are available. The current reader is closed.
ReadLine: RecordingConsole -> unit -> stringReadLine: unit -> stringReads a line of characters from the current string and returns the data as a string. The next line from the current string, or if the end of the string is reached. The current reader is closed. There is insufficient memory to allocate a buffer for the returned string.
ReadKey: RecordingConsole -> bool -> ConsoleKeyInfo``.ctor``: unitWrite: RecordingConsole -> string -> unitvalue: stringWrite: string -> unitWrites a string to the current string. The string to write. The writer is closed.
WriteLine: RecordingConsole -> string -> unitWriteLine: string -> unitWrites a string to the text stream, followed by a line terminator. The string to write. If is , only the line terminator is written. The is closed. An I/O error occurs.
WriteError: RecordingConsole -> string -> unitWriteErrorLine: RecordingConsole -> string -> unitOpenStandardInput: RecordingConsole -> unit -> StreamSystem.IO.StreamProvides a generic view of a sequence of bytes. This is an abstract class.
Null: StreamA with no backing store.
OpenStandardOutput: RecordingConsole -> unit -> StreamOpenStandardError: RecordingConsole -> unit -> StreamClear: RecordingConsole -> unit -> unitBeep: RecordingConsole -> unit -> unitResetColor: RecordingConsole -> unit -> unitForegroundColor: RecordingConsole -> unit -> ConsoleColorvalue: ConsoleColorBackgroundColor: RecordingConsole -> unit -> ConsoleColorCursorLeft: RecordingConsole -> unit -> intvalue: intCursorTop: RecordingConsole -> unit -> intCursorVisible: RecordingConsole -> unit -> boolvalue: boolSetCursorPosition: RecordingConsole -> int * int -> unitleft: inttop: intTitle: RecordingConsole -> unit -> stringTreatControlCAsInput: RecordingConsole -> unit -> boolWith it, assertions are ordinary value comparisons:
let answeredYes = RecordingConsole "y\n"
confirm "Continue?" |> Flow.run { Console = answeredYes } |> shouldEqual (Exit.Success true)
answeredYes.Written |> shouldEqual "Continue? [y/N] "
let answeredBlank = RecordingConsole "\n"
confirm "Continue?" |> Flow.run { Console = answeredBlank } |> shouldEqual (Exit.Success false)
let terminal = RecordingConsole ""
withHiddenCursor terminal (Console.writeLine "working") |> Flow.run { Console = terminal } |> shouldEqual (Exit.Success())
terminal.IsCursorVisible |> shouldEqual true
answeredYes: RecordingConsole``.ctor``: string -> RecordingConsoleconfirm: '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.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
Console: IConsoleshouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Written: stringansweredBlank: RecordingConsoleterminal: RecordingConsolewithHiddenCursor: IConsole -> Flow<AppEnv,Never,'value> -> Flow<AppEnv,Never,'value>Axial.Console.ConsoleModulewriteLine: string -> Flow<'env,'error,unit>IsCursorVisible: boolThe cursor is visible again afterwards: the finalizer ran when the scope closed.
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.

