Basic text-based console UI implemented in F#
This is a brief exploration of some of the capabilities of the F# language to define a small set of classes that can be leveraged to provide a simple text-based UI for an F# program. Example code is provided, which is a great introduction to the syntax, but a gentler and more thorough introduction to F# can be found at MSDN Visual F#.
After learning more about the language from a series of F# videos, I decided to explore the language and tinker with it for a while. One of the first challenges a developer will face working with a new language is settling on a simple means to display results to the screen and get input from the user. F# can import .NET assemblies, which are certainly sufficient, but working on a basic text console it turned out to be an interesting way to explore the language. Admittedly, this is not a problem domain specifically suited for F#, but it exercises some of the fundamental capabilities of the language and is grounded in the familiar territory of the command line console.
Box Console Text UI
Here’s a screenshot of what the final text console test program looks like, which renders its interface using the F# BoxConsole module.
Each of the boxes shown represents an instance of one of the classes defined in the BoxConsole module using structures to define the box location, size and style. The menu box in the center is an example of a DosMenu class which combines all of the building block classes to implement an F# application menu.
F# Enumerations & Simple Data Structures
One of the fundamental affordances a programming language can provide is a mechanism to map enumerated integer values to a set of constant symbols, a.k.a. an Enumeration. F# employs it’s pattern matching syntax to define an enumeration and here are some examples from the BoxConsole module:
BoxConsole.fs
- ///////////////////////////////////////////////////////////////////////////////
- // Enumerates the four corners of a box.
- ///////////////////////////////////////////////////////////////////////////////
- type BoxCorner =
- | UL = 0
- | UR = 1
- | LR = 2
- | LL = 3
- ///////////////////////////////////////////////////////////////////////////////
- // Enumerates possible text alignments.
- ///////////////////////////////////////////////////////////////////////////////
- type TextAlignment =
- | Left = 0
- | Center = 1
- | Right = 2
The first enumeration, BoxCorner expresses a shorthand to refer to the upper left, upper right, lower right and lower left corners of a box and the TextAlignment enumeration allows the users of the BoxConsole module to specify a left, center or right text justification.
Another fundamental programming language expression is the data record or structure that contains a collections of related values. The BoxLocation type defines a F# data structure to collect the data describing the size and position of a box.
BoxConsole.fs
- ///////////////////////////////////////////////////////////////////////////////
- // Contains box top, left, width and height information.
- ///////////////////////////////////////////////////////////////////////////////
- type BoxLocation =
- { Top : int;
- Left : int;
- Width : int;
- Height : int; }
The BoxStyle type describes the style of a DosBox, including members that describe the border style, margin around the text along with foreground and background colors of the box. In the case of the BoxStyle type, the Border member refers to an object and not a simple data type.
BoxConsole.fs
- ///////////////////////////////////////////////////////////////////////////////
- // Structure describing the syle of a DOS box.
- ///////////////////////////////////////////////////////////////////////////////
- type BoxStyle =
- { Border : BorderStyle;
- Margin : int;
- Foreground : System.ConsoleColor;
- Background : System.ConsoleColor; }
Mutable Values and Classes
The “immutable by default” nature of F# encourages developers to craft code that the complier can optimize much more aggressively and runs faster in a parallel execution context. However, at the end of the day, the program must have a reasonable way to express it’s state. The mutable keyword is the signal to F# that the variable being declared will be used in the more traditional manner: its value should be stored and can be expected to change during program execution. These mutable variables can then be used as fields to store values for the implementation of a class.
BoxConsole.fs
- ///////////////////////////////////////////////////////////////////////////////
- // Implements various styles for drawing boxes.
- ///////////////////////////////////////////////////////////////////////////////
- type BorderStyle = class
- ///////////////////////////////////////////////////////////////////////////////
- // Defines a number of text patterns used to style the boxes. Each pattern is
- // nine chracters in length and define the corner, edge and background characters.
- ///////////////////////////////////////////////////////////////////////////////
- static member private boxPatterns =
- @" /-\\-/|.| " +
- @"*-**-*| |+-++-+| | +++++++ +******* *////////////////\/ " +
- @" " +
- @"====== vvv^^^ ~~~~~~) (XXXXXXX X + + + + " +
- @" oooo uuu "
- ///////////////////////////////////////////////////////////////////////////////
- // Returns the specified set of nine-character box styles.
- ///////////////////////////////////////////////////////////////////////////////
- static member private getPattern n =
- new String [| for i in 0 .. 8 -> BorderStyle.boxPatterns.[9*n + i] |]
- ///////////////////////////////////////////////////////////////////////////////
- // Determine start, middle or end style character for line of a specific size.
- ///////////////////////////////////////////////////////////////////////////////
- static member private boxPart size n =
- match n with
- | 1 -> 0
- | n when n = size -> 2
- | _ -> 1
- ///////////////////////////////////////////////////////////////////////////////
- // Given a box style pattern, render a box of the specified size.
- ///////////////////////////////////////////////////////////////////////////////
- static member BoxLines(width : int, height : int, pattern : string) =
- [ for h in [1..height] ->
- new System.String
- [| for w in 1 .. width ->
- match h, (BorderStyle.boxPart width w) with
- | 1, i -> pattern.[i]
- | _, i when h = height -> pattern.[i + 3]
- | _, i -> pattern.[i + 6] |] ]
- ///////////////////////////////////////////////////////////////////////////////
- // Pattern in use for this border style.
- ///////////////////////////////////////////////////////////////////////////////
- val mutable Pattern : string;
- ///////////////////////////////////////////////////////////////////////////////
- // Constructor to use one of the predefined style patterns.
- ///////////////////////////////////////////////////////////////////////////////
- new (style: int) as border =
- { Pattern = " "; }
- then
- if style <= BorderStyle.boxPatterns.Length / 9
- then border.Pattern <- (BorderStyle.getPattern style)
- ///////////////////////////////////////////////////////////////////////////////
- // Constructor to use a custom nine-character style pattern.
- ///////////////////////////////////////////////////////////////////////////////
- new (pattern : string) as border =
- { Pattern = " "; }
- then
- if pattern.Length >= 9 then border.Pattern <- pattern
- end
(Note: Due to encoding issues, the contents of boxPatterns are not correctly rendered above. To see the complete set of character used, please download the project source code .)
Those new to F# but familiar with today’s object-oriented patterns should be able to recognize those patterns peeking through this alien code if they “squint” just a little bit. The static keyword is used to express functionality within the class scope, and is omitted for instance members of a class. Creation of objects is achieved via the new keyword, which also used to declare class constructors. Also notice the use of the private keyword allowing the developer to control the visibility of members in order to help maintain encapsulation.
The BorderStyle class knows how to render a nine character array as a the corners and edges of a box. One constructor uses a predefined style and the other allows the caller to specify a custom set of nine characters to define a border style. This class is effectively implemented with a couple pattern matching methods.
The first, boxPart, matches determines which offset to use to (0-2) depending on if it’s the first item in the list (yields 0), the last item in the list (2), or any other item in the list (1). Similar matching rules are employed in the BoxLinesfunction, but that pattern is wrapped in an array [| for w in 1 .. width –> |] generated from a list [ for h in [1..height] –> ] . Together, these two functions provide the character patterns for the box’s border style given a width and height.
Using the System.Console class
Class ConsoleUI wraps the System.Console class for the BoxConsole module, making the console easier to use from the F# implementation. F# uses the open keyword to import other type libraries into the current program scope. In this case we import the System .NET module on line six, right after we explicitly declare the name of our module on line four.
BoxConsole.fs
- ///////////////////////////////////////////////////////////////////////////////
- // BoxConsole.fs - Basic text-based console UI implemented in F#
- ///////////////////////////////////////////////////////////////////////////////
- module BoxConsole
- open System
Taking a closer look at one of the ConsoleUI methods reveals how .NET methods and properties can be called from an F# application. The WriteAt function will write a message on the console in the requested location and will trim the message to prevent the console screen from scrolling.
BoxConsole.fs
- ///////////////////////////////////////////////////////////////////////////////
- // Writes the message at the given location. This method will trim the message
- // to pervent scrolling of the console window.
- ///////////////////////////////////////////////////////////////////////////////
- static member WriteAt message left top =
- let oleft = System.Console.CursorLeft
- let otop = System.Console.CursorTop
- let message = ConsoleUI.trimToPreventOverflow(message, left, top)
- Console.CursorLeft <- left
- Console.CursorTop <- top
- Console.Write(message : string)
- Console.CursorLeft <- oleft
- Console.CursorTop <- otop
Note the use of the variables oleft and otop which are used to store the current cursor position so it can be restored once the message is written. These variables are written to once and never modified. They are given their values using the let keyword and the equals (=) sign and they are immutable, the value is bound that that name. Even in the case of message, the value is is not mutable: the old value of message is lost and a new one (possibly shorter) is created.
On the other hand, the .NET system Console.CursorLeft property is not immutable and must be modified to change the cursor position. In this case F# considers the variable mutable and allows its value to be changed using the <- operator. Finally, the WriteAt function provides an example of calling a method on a .NET class, writing the message to the console using the Console.Write(message: string) syntax to invoke the method.
Example Program
Program.fs is provided as a practical example of how to use the BoxConsole module to display and process user selections using the DosMenu class, and allows the user to cycle through some of the various styles and options available.
Program.fs
- ///////////////////////////////////////////////////////////////////////////////
- // Program.fs - Console UI Test Application
- ///////////////////////////////////////////////////////////////////////////////
- module Program
- open BoxConsole
- open System
- // Define a menu to drive the program
- let m = DosMenu(["Color"; "Style"; "Singleline"; "Multiline"; "Align"; "Margin"; "Quit"], 3)
- // Collections of colors to use for foreground and background testing
- let bgcs = [ ConsoleColor.DarkBlue; ConsoleColor.DarkMagenta; ConsoleColor.DarkRed; ConsoleColor.DarkGray; ConsoleColor.Black ]
- let fgcs = [ ConsoleColor.Yellow; ConsoleColor.Magenta; ConsoleColor.Red; ConsoleColor.White; ConsoleColor.White ]
- Console.Clear()
- let mutable color = 0 // Currently selected color
- let mutable style = 0 // Currently selected style
- let mutable singleline = true // Single line/multi-line text
- let mutable displayText = [] // Currently displayed text
- let mutable align = 0 // Currently selected text alignment
- let mutable margin = 1 // Currently selected style margin
- let mutable selection = ""// User menu selection
- // Create a text box to contain current date / time, center horizontally.
- let timeBox = new DosBox([System.DateTime.Now.ToLongTimeString(); System.DateTime.Now.ToLongDateString(); ])
- timeBox.Location <- { Top = Console.WindowHeight - 5; Left = timeBox.Location.Left; Width = timeBox.Location.Width + 2; Height = timeBox.Location.Height }
- timeBox.BoxText.Align <- TextAlignment.Center
- timeBox.BoxText.Foreground <- ConsoleColor.Green;
- timeBox.BoxText.Background <- ConsoleColor.DarkGreen;
- timeBox.Style <- { Border = new BorderStyle(3); Foreground = ConsoleColor.Green; Background = ConsoleColor.DarkGreen; Margin = 1; }
- // Main program loop
- while selection <> "Quit" do
- // Check if user made a valid selection
- if selection <> "" then
- Console.Clear()
- // Update current state based on the user's selection
- match selection with
- | "Color" -> color <- color + 1
- | "Style" -> style <- style + 1
- | "Singleline" -> singleline <- true
- | "Multiline" -> singleline <- false
- | "Align" -> align <- align + 1
- | "Margin" -> margin <- margin + 1
- | _ -> ()
- // Update each of the corner boxes
- for x in (int BoxCorner.UL)..(int BoxCorner.LL) do
- let k = (x+color)%5
- // Choose single line or multi-line text
- let cornerDisplay =
- if singleline then
- displayText <- [selection]
- else
- displayText <- ["---------------"; selection; "---------------";]
- // Create the box in the appropriate corner
- let box = new DosCornerBox(displayText,enum<BoxCorner>(x))
- box.Style <- { Border = new BorderStyle (style%35); Foreground = fgcs.[k]; Background = bgcs.[k]; Margin = (margin % 4 + 1); }
- box.BoxText.Foreground <- fgcs.[k]
- box.BoxText.Background <- bgcs.[k]
- box.BoxText.Align <- enum<TextAlignment>((int align) % 3)
- box.RecalcBox()
- box.Draw()
- // Select current foreground/background color combination
- let v = (color+4)%5
- m.Style <- { Border = new BorderStyle (style%35); Foreground = fgcs.[v]; Background = bgcs.[v]; Margin = (margin % 5); }
- m.Box.BoxText.Foreground <- fgcs.[v]
- m.Box.BoxText.Background <- bgcs.[v]
- // Update the current date and time
- timeBox.BoxText.Text <- [System.DateTime.Now.ToLongTimeString(); System.DateTime.Now.ToLongDateString(); ]
- timeBox.Draw()
- // Wait for user selection, or timeout in 1s
- selection <- m.GetSelectionTimeout 1000
- ()
The example first declares the module name and imports the System and BoxConsole types. The program then creates the DosMenu containing a list of the box styles through which the user can cycle. Next the program declares a set of working variables. It’s interesting to note that like many other program there is a mix of immutable and mutable variables, but in F# it’s mutable variables that are the exception that require additional semantic notation.
Next the program creates a DosBox that displays the time and then enters the main program while loop where it will run until the user selects “Quit”. In the body of the main loop, if the user has made a selection then the program options are updated. Finally, the program will pause for 1000ms to wait for the user’s next selection from the DosMenu.
Download complete BoxConsole source code.