Building a text-based application using Swift and ncurses Aug 6 2020

The ncurses(3) library powers many popular text-based applications, for example, emacs(1) and htop(1). The use of ncurses is not required to build text-based applications. We could use escape sequences. And for small command-line tools escape sequences are enough, but sometimes it's nice to rely on a library that handles edge cases. In this post, we'll build a text-based clock that uses SwiftCursesTerm, a wrapper library I created for using ncurses in Swift.

The SwiftCursesTerm library can be found here, it provides the basic building blocks for using ncurses but is far from a complete wrapper, and I also took some liberties on some default behaviour. If SwiftCursesTerm doesn't support a feature you need, let me know, or you can fork the project, implement the functionality you need, and then create a pull-request so everyone can use it.

Ok, let's begin by reviewing some basic ncurses basic concepts.

* You can check the full code in the GitHub Repository

The ncurses workflow

This post's focus is not on learning ncurses(3). I expect you have some familiarity with it, but if you need a refresher or just a quick intro, this section will help.

The ncurses(3) library was built for C, so it shows its procedural inheritance. You will notice it's not that Swifty, but you'll get used to it. If you follow some design pattern like MVC, it shouldn't interfere with any of your code. It should only be in charge of the View layer.

In a text-based application, we work with the terminal screen. We deal with lines and columns. And we are restricted by the space we have in our terminal. Our canvas is a window object, where we can display text. A sequence of characters represents the text. Each character is represented by a chtype. The chtype includes the text and text attributes, e.g. bold, underscore, colour, etcetera.

A window can have sub-windows that help us organise the whole screen.

From the procedural nature of ncurses, the workflow feels linear, one instruction after the other.

To illustrate the workflow, imagine the following code (calling the C ncurses functions directly from Swift, not using the SwiftCursesTerm library):

1
2
3
4
5
6
7
8
9
10
initscr()

addstr("hello,")
init_pair(1, COLOR_WHITE, COLOR_GREEN)
attrset(COLOR_PAIR(1)
addstr(" world!)
refresh()
napms(3000)

endwin()

What we are doing is the following:

  1. We initialise our screen to use ncurses
  2. Add the string "hello," to the standard window structure that is created when you initialise ncurses
  3. Create a colour-pair. With white colour as the foreground and green as the background
  4. Set the attribute for the screen to use the colour pair we just created
  5. Add the string " world!", that will be displayed on white letters over green background. At this point, the program doesn't show anything on the screen. Only when we "flush" the screen with refresh, we get to see the state of the window actually on screen.
  6. Call refresh to show the window state on screen
  7. Wait for three seconds so you can see the screen
  8. And finally, call endwin to free the window structure

You can see how procedural this feels. It's not a bad thing, just different. As you can see, it is easy to follow, one instruction after the other, so I don't think you'll have any trouble working with it.

Ok, that should be enough to get you going. Let's get started with our project.

Design of our text-based clock application

Our text based application will look like this:

Clock App Demo

If you want to look at the whole code for the project, you can get the code from the Clock - Repository in GitHub

We are going to represent each segment of the clock as a 7-segment display. If you have built a 7-segment display clock before, you'll feel right at home. If you haven't worked with 7-segment displays, the idea is simple. You have a display with 7 "segments" (LEDs nowadays) in the following configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                     A
             +----------------+
             |                |
             |                |
             |                |
          F  |                |  B
             |                |
             |                |
             |        G       |
             +----------------+
             |                |
             |                |
             |                |
          E  |                |  C
             |                |
             |                |
             |                |
             +----------------+

                     D

With that, you can represent the numbers from 0–9. If we want to display 1, we turn on segment B and C. If we're going to display 2, we turn on Segments A, B, G, E, and D. You get the idea. That is the same idea we'll use. We are going to use a Model View Presenter (MVP) design pattern for our clock. So we'll separate our view from our model and logic.

Our model will be a struct with the name SevenSegmentDisplay. We'll use the SwiftCursesTerm library to build our view. The name of our view struct is going to be ClockView. And finally, we'll have the simple logic of our clock inside ClockController struct.

Building the clock application

Ok so let's begin by creating the executable using the Swift Package Manager:

1
2
3
$ mkdir clock
$ cd clock
$ swift package init --type executable

Now, we need to add the SwiftCursesTerm as a dependency, in our manifest file (Package.swift):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// swift-tools-version:5.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "clock",
    dependencies: [
         .package(url: "https://github.com/rderik/SwiftCursesTerm.git", from: "0.1.2"),
    ],
    targets: [
        .target(
            name: "clock",
            dependencies: ["SwiftCursesTerm"]),
        .testTarget(
            name: "clockTests",
            dependencies: ["clock"]),
    ]
)

Let's create the files for our MVC application:

1
2
3
$ touch Sources/clock/ClockController.swift
$ touch Sources/clock/ClockView.swift
$ touch Sources/clock/SevenSegmentDisplay.swift

We are going to focus on our ClockView.swift where most of our text-based interface will be. So let's get all the other parts out of the way.

Application entry point

Our application's entry point is main.swift. Let's begin there. We don't know at the moment how everything will work, but we can start by defining how we would like it to work in the general sense. We want to create a clock that we can tell to run and display in the screen. So let's write some placeholder code that represents that.

1
2
3
4
5
import Foundation //To reference EXIT_SUCCESS

var clock = ClockController()
clock.run()
exit(EXIT_SUCCESS)

SevenSegmentDisplay model

Let's get the model out of the way. The seven-segment struct will be simple. It'll have a representation of the seven segments that can be on or off. Open your file SevenSegmentDisplay.swift.

1
2
3
4
5
6
7
8
9
10
import Foundation
struct SevenSegmentDisplay {
    var a = true
    var b = true
    var c = true
    var d = true
    var e = true
    var f = true
    var g = true
}

We need to define how the segments are going to look. I'll use simple Unicode characters, but you can change them after. We'll add them as properties.

1
2
  let dash = "🖱"
  let pipe = "🖱"

We also would like to know the height and width of the whole display. So let's add those properties with some default values:

1
2
  var height = 7
  var width = 6

Ok, our display will only show numbers from 0–9, so let's create an initialiser that receives the number and sets our segments to true or false in the correct sequence:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    init(number: UInt8){
      switch number {
        case 0:
          (a, b, c, d, e, f, g) = (true, true, true, true, true, true, false)
        case 1:
            (a, b, c, d, e, f, g) = (false, true, true, false, false, false, false)
        case 2:
              (a, b, c, d, e, f, g) = (true, true, false, true, true, false, true)
        case 3:
              (a, b, c, d, e, f, g) = (true, true, true, true, false, false, true)
        case 4:
              (a, b, c, d, e, f, g) = (false, true, true, false, false, true, true)
        case 5:
              (a, b, c, d, e, f, g) = (true, false, true, true, false, true, true)
        case 6:
              (a, b, c, d, e, f, g) = (true, false, true, true, true, true, true)
        case 7:
              (a, b, c, d, e, f, g) = (true, true, true, false, false, false, false)
        case 8:
              (a, b, c, d, e, f, g) = (true, true, true, true, true, true, true)
        case 9:
              (a, b, c, d, e, f, g) = (true, true, true, true, false, true, true)
        default:
              (a, b, c, d, e, f, g) = (false, false, false, false, false, false, false)

      }
  }

Let's create an initialiser that also sets the height and width:

1
2
3
4
5
  init(height: Int, width: Int, number: UInt8) {
      self.init(number: number)
      self.height = height
      self.width = width
  }

Alright, now with that in place, let's add a function that will transform our model to a string representation. Add the following function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  func toString() -> String {
    let maxCol = width - 1
    let maxLine = height - 1
    var result = [[String]](repeating: [String](repeating: " ", count: width), count: height )
      if a {
       for i in 0 ... maxCol{
         result[0][i] = dash
       }
      }
      if b {
       let begin = 0
       let end = begin + (maxLine / 2) 
       for i in begin ... end {
         result[i][maxCol] = pipe
       }
      }
      if c {
       let begin = maxLine / 2
       for i in begin ... maxLine {
         result[i][maxCol] = pipe
       }
      }
      if d {
       for i in 0 ... maxCol{
         result[maxLine][i] = dash
       }
      }
      if e {
       let begin = maxLine / 2
       for i in begin ... maxLine {
         result[i][0] = pipe
       }
      }
      if f {
       let begin = 0
       let end = begin + (maxLine / 2) 
       for i in begin ... end {
         result[i][0] = pipe
       }
      }
      if g {
       let mid = maxLine / 2
       for i in 0 ... maxCol{
         result[mid][i] = dash
       }
      }

      var strResult = ""
        for i in 0 ... maxLine{
            strResult += result[i].joined() + "\n"
        }
     return strResult 
  }

It builds a matrix the represents our display in characters (height x width) and adds the characters depending if the segment is on or off.

Ok here is the whole SevenSegmentDisplay.swift:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import Foundation
struct SevenSegmentDisplay {
  var height = 7
  var width = 6
  let dash = "🖱"
  let pipe = "🖱"
  var a = true
    var b = true
    var c = true
    var d = true
    var e = true
    var f = true
    var g = true

    init(number: UInt8){
      switch number {
        case 0:
          (a, b, c, d, e, f, g) = (true, true, true, true, true, true, false)
        case 1:
            (a, b, c, d, e, f, g) = (false, true, true, false, false, false, false)
        case 2:
              (a, b, c, d, e, f, g) = (true, true, false, true, true, false, true)
        case 3:
              (a, b, c, d, e, f, g) = (true, true, true, true, false, false, true)
        case 4:
              (a, b, c, d, e, f, g) = (false, true, true, false, false, true, true)
        case 5:
              (a, b, c, d, e, f, g) = (true, false, true, true, false, true, true)
        case 6:
              (a, b, c, d, e, f, g) = (true, false, true, true, true, true, true)
        case 7:
              (a, b, c, d, e, f, g) = (true, true, true, false, false, false, false)
        case 8:
              (a, b, c, d, e, f, g) = (true, true, true, true, true, true, true)
        case 9:
              (a, b, c, d, e, f, g) = (true, true, true, true, false, true, true)
        default:
              (a, b, c, d, e, f, g) = (false, false, false, false, false, false, false)

      }
  }

  init(height: Int, width: Int, number: UInt8) {
      self.init(number: number)
      self.height = height
      self.width = width
  }

  func toString() -> String {
    let maxCol = width - 1
    let maxLine = height - 1
    var result = [[String]](repeating: [String](repeating: " ", count: width), count: height )
      if a {
       for i in 0 ... maxCol{
         result[0][i] = dash
       }
      }
      if b {
       let begin = 0
       let end = begin + (maxLine / 2) 
       for i in begin ... end {
         result[i][maxCol] = pipe
       }
      }
      if c {
       let begin = maxLine / 2
       for i in begin ... maxLine {
         result[i][maxCol] = pipe
       }
      }
      if d {
       for i in 0 ... maxCol{
         result[maxLine][i] = dash
       }
      }
      if e {
       let begin = maxLine / 2
       for i in begin ... maxLine {
         result[i][0] = pipe
       }
      }
      if f {
       let begin = 0
       let end = begin + (maxLine / 2) 
       for i in begin ... end {
         result[i][0] = pipe
       }
      }
      if g {
       let mid = maxLine / 2
       for i in 0 ... maxCol{
         result[mid][i] = dash
       }
      }

      var strResult = ""
        for i in 0 ... maxLine{
            strResult += result[i].joined() + "\n"
        }
     return strResult 
  }
}

Let's move to our controller.

Creating the Controller

Our controller has the logic for our clock, which is not complicated. We need to get the current time, call our view mode and update it every second. The following is how our ClockController.swift will look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Foundation
import SwiftCursesTerm

struct ClockController {
    var clockView: ClockView
    var term = SwiftCursesTerm()

    init(segmentHeight: Int, segmentWidth: Int, colonPad: Int) {
        clockView = ClockView(term: term, segmentHeight: segmentHeight, segmentWidth: segmentWidth, colonPad: colonPad)
    }

    func run() {
        defer { term.shutdown() }
        term.noDelay(true)
        while (getch() == ERR) {
            let now = Date()
            let calendar = Calendar.current

            let hours = calendar.component(.hour, from: now)
            let minutes = calendar.component(.minute, from: now)
            let seconds = calendar.component(.second, from: now)
            clockView.display(hours: hours, minutes: minutes, seconds: seconds)
            napms(1000)
        }
    }
} 

We are going to use the SwiftCursesTerm library, so we begin by creating the term that will serve as the canvas for our ClockView. We'll define the size of each segment and how much space to leave for the colon between HH:MM:SS.

Then we implement the run function, that is the central part where we have the clock logic.

When we are done with our windows, we need to free up the window structures. The SwiftCursesTerm frees the window, and it's sub-windows when it is deinitialised, but we are calling the term.shutdown() function manually to make sure everything ends up properly and our shell doesn't end up in a weird state.

Note: while testing your terminal might end up in a state, where you can't see what you are typing or with some weird behaviour. If your term ends up in that odd state just type reset, and it'll get back to normal (most of the time).

We are using a little trick here by using the noDelay feature of ncurses. This will set the getch() function to non-blocking, so if there is nothing to read it'll just return ERR, and it'll keep running, but if there is something to read it'll read it and getch() will be different than ERR. That means that we are going to run that while loop until a key is pressed.

The loop logic is simple, obtain the current time, and call our ClockView to display it.

We need to correct our main.swift to pass the dimensions we are going to use for each segment. So update your main.swift with the following content:

1
2
3
4
5
import Foundation //To reference EXIT_SUCCESS

var clock = ClockController(segmentHeight: 9, segmentWidth: 5, colonPad: 1)
clock.run()
exit(EXIT_SUCCESS)

Ok, time to work on our view.

Building our View structure

We will use a struct to represent our view. It contains a SwiftCursesTerm object that we'll use to display all of its elements. We will also need the size of the segments(height and width), and the padding for the colon. And lastly, we are going to use sub-windows to wrap each segment, that means that we'll need a SCTWindowId for each segment.

1
2
3
4
5
6
7
struct ClockView {
    let colonPad: Int
    var segmentHeight: Int
    var segmentWidth: Int
    var term: SwiftCursesTerm
    var hourSeg1, hourSeg2, minSeg1, minSeg2, secSeg1, secSeg2: SCTWindowId
}

The hardest part is the initialisation, where we define the structure of our text-based interface. Let's begin there. Begin by assigning the values to our struct properties.

1
2
3
4
5
6
    init(term: SwiftCursesTerm, segmentHeight: Int, segmentWidth: Int, colonPad: Int = 1) {
        self.term = term
        self.segmentHeight = segmentHeight
        self.segmentWidth = segmentWidth
        self.colonPad = colonPad
        //More set up to come

Now we need to add a window for each segment. Let's begin with the hour segments:

1
2
        hourSeg1 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: 0)
        hourSeg2 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: segmentWidth)

Easy enough, we just created two windows one after the other. Now let's add the colon separator between the hour segment sub-windows and the minute segments:

1
2
3
        //Leaving space for colon
        term.addStrTo(content: "🀆", line: segmentHeight / 4, column: (segmentWidth * 2) + (colonPad / 2), refresh: true)
        term.addStrTo(content: "🀆", line: segmentHeight * 3 / 4, column: (segmentWidth * 2) + (colonPad / 2 ))

Now we can create the windows for the minutes segments:

1
2
3
        minSeg1 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: (2 * segmentWidth) + colonPad )
        minSeg2 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: (3 * segmentWidth) + colonPad )

Again add colon separators between minutes and seconds:

1
2
3
        //Leaving space for colon
        term.addStrTo(content: "🀆", line: segmentHeight / 4, column: (segmentWidth * 4) + colonPad + (colonPad / 2))
        term.addStrTo(content: "🀆", line: segmentHeight * 3 / 4, column: (segmentWidth * 4) + colonPad + (colonPad / 2 ))

Now we can create the windows for the seconds segments:

1
2
        secSeg1 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: (4 * segmentWidth) + (colonPad * 2))
        secSeg2 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: (5 * segmentWidth) + (colonPad * 2))

Let's make our ClockView display 88:88:88 as it's initial state. We need to create the segments:

1
2
3
4
5
6
7
8
        let segmentHeightDisplay = segmentHeight - 1
        let segmentWidthDisplay = segmentWidth - 1
        let h1 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let h2 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let m1 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let m2 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let s1 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let s2 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )

Now let's add the string representation of each of those segments to it's respective window:

1
2
3
4
5
6
        term.addStrTo(window: hourSeg1, content: h1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: hourSeg2, content: h2.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: minSeg1, content: m1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: minSeg2, content: m2.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: secSeg1, content: s1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: secSeg2, content: s2.toString(), line: 0, column: 0, refresh: true)

As additional information, let's add the date to the bottom of our clock.

1
2
3
4
5
6
7
8
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .none

        let currDate = formatter.string(from: Date())
        let green = term.defineColorPair(foreground: CursesColor.clear, background: CursesColor.green)
        term.setAttributes([TextAttribute.bold, TextAttribute.underline, TextAttribute.reverse], colorPair: green)
        term.addStrTo(content: currDate, line: segmentHeight + 1, column: ((segmentWidth * 6) + (colonPad * 2)) / 2 - (currDate.count / 2), refresh: true)

And that's it for our initialiser. That was the hardest part. Now let's implement the display function where we display time. Create the function:

1
2
3
4
    func display(hours: Int, minutes: Int, seconds: Int) {
// Content to do
    }

Now, we are going to create segments with the data for each component of the time.

1
2
3
4
5
6
7
8
9
        let segmentHeightDisplay = segmentHeight - 1
        let segmentWidthDisplay = segmentWidth - 1

        let h1 = SevenSegmentDisplay(height: segmentHeight - 1 , width: segmentWidth - 1, number: UInt8(hours / 10) )
        let h2 = SevenSegmentDisplay(height: segmentHeight - 1 , width: segmentWidth - 1, number: UInt8(hours % 10))
        let m1 = SevenSegmentDisplay(height: segmentHeight - 1 , width: segmentWidth - 1, number: UInt8(minutes / 10) )
        let m2 = SevenSegmentDisplay(height: segmentHeight - 1 , width: segmentWidth - 1, number: UInt8(minutes % 10) )
        let s1 = SevenSegmentDisplay(height: segmentHeight - 1 , width: segmentWidth - 1, number: UInt8(seconds / 10) )
        let s2 = SevenSegmentDisplay(height: segmentHeight - 1 , width: segmentWidth - 1, number: UInt8(seconds % 10) )

We now have each component on a segment, is now just a matter of adding that representation on the specific window we created for each segment:

1
2
3
4
5
6
7
        term.addStrTo(window: hourSeg1, content: h1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: hourSeg2, content: h2.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: minSeg1, content: m1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: minSeg2, content: m2.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: secSeg1, content: s1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: secSeg2, content: s2.toString(), line: 0, column: 0, refresh: true)
} //end of init

Remember that when we are adding text to a window structure in ncurses, it doesn't automatically appear on the screen, we need to call refresh. That is the reason we pass the refresh: true parameter to the addStrTo function.

The following is the complete ClockView.swift file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import curses
import Foundation
import SwiftCursesTerm

struct ClockView {
    let colonPad: Int
    var segmentHeight: Int
    var segmentWidth: Int
    var term: SwiftCursesTerm
    var hourSeg1, hourSeg2, minSeg1, minSeg2, secSeg1, secSeg2: SCTWindowId

    init(term: SwiftCursesTerm, segmentHeight: Int, segmentWidth: Int, colonPad: Int = 1) {
        self.term = term
        self.segmentHeight = segmentHeight
        self.segmentWidth = segmentWidth
        self.colonPad = colonPad

        hourSeg1 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: 0)
        hourSeg2 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: segmentWidth)

        //Leaving space for colon
        term.addStrTo(content: "🀆", line: segmentHeight / 4, column: (segmentWidth * 2) + (colonPad / 2), refresh: true)
        term.addStrTo(content: "🀆", line: segmentHeight * 3 / 4, column: (segmentWidth * 2) + (colonPad / 2 ))

        minSeg1 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: (2 * segmentWidth) + colonPad )
        minSeg2 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: (3 * segmentWidth) + colonPad )

        //Leaving space for colon
        term.addStrTo(content: "🀆", line: segmentHeight / 4, column: (segmentWidth * 4) + colonPad + (colonPad / 2))
        term.addStrTo(content: "🀆", line: segmentHeight * 3 / 4, column: (segmentWidth * 4) + colonPad + (colonPad / 2 ))

        secSeg1 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: (4 * segmentWidth) + (colonPad * 2))
        secSeg2 = term.newWindow(height: segmentHeight, width:  segmentWidth, line: 0, column: (5 * segmentWidth) + (colonPad * 2))

        let segmentHeightDisplay = segmentHeight - 1
        let segmentWidthDisplay = segmentWidth - 1
        let h1 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let h2 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let m1 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let m2 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let s1 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )
        let s2 = SevenSegmentDisplay(height: segmentHeightDisplay , width: segmentWidthDisplay, number: UInt8(8) )

        term.addStrTo(window: hourSeg1, content: h1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: hourSeg2, content: h2.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: minSeg1, content: m1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: minSeg2, content: m2.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: secSeg1, content: s1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: secSeg2, content: s2.toString(), line: 0, column: 0, refresh: true)

        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .none

        let currDate = formatter.string(from: Date())
        let green = term.defineColorPair(foreground: CursesColor.clear, background: CursesColor.green)
        term.setAttributes([TextAttribute.bold, TextAttribute.underline, TextAttribute.reverse], colorPair: green)
        term.addStrTo(content: currDate, line: segmentHeight + 1, column: ((segmentWidth * 6) + (colonPad * 2)) / 2 - (currDate.count / 2), refresh: true)
    }

    func display(hours: Int, minutes: Int, seconds: Int) {
        let segmentHeightDisplay = segmentHeight - 1
        let segmentWidthDisplay = segmentWidth - 1

        let h1 = SevenSegmentDisplay(height: segmentHeightDisplay, width: segmentWidthDisplay, number: UInt8(hours / 10) )
        let h2 = SevenSegmentDisplay(height: segmentHeightDisplay, width: segmentWidthDisplay, number: UInt8(hours % 10))
        let m1 = SevenSegmentDisplay(height: segmentHeightDisplay, width: segmentWidthDisplay, number: UInt8(minutes / 10) )
        let m2 = SevenSegmentDisplay(height: segmentHeightDisplay, width: segmentWidthDisplay, number: UInt8(minutes % 10) )
        let s1 = SevenSegmentDisplay(height: segmentHeightDisplay, width: segmentWidthDisplay, number: UInt8(seconds / 10) )
        let s2 = SevenSegmentDisplay(height: segmentHeightDisplay, width: segmentWidthDisplay, number: UInt8(seconds % 10) )

        term.addStrTo(window: hourSeg1, content: h1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: hourSeg2, content: h2.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: minSeg1, content: m1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: minSeg2, content: m2.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: secSeg1, content: s1.toString(), line: 0, column: 0, refresh: true)
        term.addStrTo(window: secSeg2, content: s2.toString(), line: 0, column: 0, refresh: true)
    }
}

And that's it.

Putting it all together

To recap what we did, we begin with our entry point creating a ClockController instance that makes uses SwiftCursesTerm to handle the interface of our application. The ClockView is in charge of adding all the text-based elements to the terminal window. And we created a basic model to represent the segments of the clock using the class SevenSegmentDisplay. A nice and simple MVC text-based application.

Now you can build and run it:

1
$ swift run

And you should see the clock ticking on your screen. Congratulations.

* You can check the full code in the GitHub Repository

Final Thoughts

I hope you see how easy it is to build text-based interfaces for your command-line tools. It requires a small change of mindset, but it's not complicated. I created the SwiftCursesTerm library to make the process simple, so if you like it, please use it and contribute to it whenever you can.

One thing I would like to point out to you is that make sure your application does the proper shutdown of the SwiftCursesTerm. I tried to make it as painless as possible by calling the shutdown sequence when the object is deinitialised. But you can always break things if you directly exit the process without calling the correct shutdown sequence, in those cases call the shutdown manually.

Ok, that's it, I hope you find it useful. And as always, feedback is welcomed.


** If you want to check what else I'm currently doing be sure to follow me on twitter @rderik or subscribe to the newsletter. If you want to send me a direct message you can send it to derik@rderik.com.