Ncurses with Swift on Linux

| | 0 Comments| 11:22 AM
Categories:

Swift 2.2 ISC Linux

Ncurses is a toolkit for developing “GUI-like” application software that runs under a terminal emulator. If you aren’t familiar with ncurses, there are a number of resources for learning about its history and capabilities. Here are some of my favorites:

This tutorial will walk you through how to create ncurses applications on Linux with Apple’s Swift programming language. It is surprisingly easy with most of the complexity coming from learning ncurses itself.

Using With Swift

To use ncurses in Swift applications you can add a Package dependency in your application’s Package.swift file like so:

See our Swift Package Manager tutorial for more information on how this dependency works; it is a very simple modulemap around the ncurses header and library.

ncurses primitives

Ncurses is a “true” C API. Each API call is a C function with set of arguments (or what looks like a function call; many ncurses routines are in fact, C macros). Internal library structures and state variables keep track of the state of the screen.

This isn’t a tutorial on ncurses itself (see this for one), but we will show how to perform basic routines and point out areas we cannot use in Swift (and how to get around it!)

Let’s look at three functions we call to set up the screen:

initscr(): initscr is typically the first curses routine to call when initializing a program.
noecho(): noecho turns echoing of characters typed off. One might think character echoing would be desirable, but in most cases if we are dealing with receiving input from the terminal we want a fine control over what is displayed and what isn’t.
curs_set(int): curs_set controls the visual representation of the cursor. The integer value can be 0, 1, or 2 for invisible cursor, normal cursor, or prominent cursor. If you are painting an ASCII art masterpiece the last thing you want left on the screen in a chunky cursor.

Now that our screen is initialized we can use the several ncurses primitives to detect the size of the screen and where the cursor is currently located.

If you Google ncurses get size of screen it will invariably lead you to the “function” getmaxyx, which is actually a macro, not a function. That is an issue, because not only is getmaxyx a macro, it is what Swift defines as a complex macro. From the Swift Programming Language reference: Complex macros are macros that do not define constants, including parenthesized, function-like macros. The reference manual continues: complex macros that are in C and Objective-C source files are not made available to your Swift code. (emphasis mine).

So what’s a developer to do if they want to use getmaxyx? Turn to what the definition of the macro is in /usr/include/curses.h which is:

Using this definition we can use getmaxy and getmaxx together:

stdscr is a global ncurses variable of type WINDOW* and is a handle to the main window.

Alternatively we can create an analogous Swift function to the macro like this:

In this case use of the function would appear as:

Another common macro to use with ncurses is getcuryx which gets the current position of the cursor. Again, we either use getcurx and getcury together, or write a getcuryx function:

Now that we know how to determine the dimensions of our terminal screen and where the cursor is currently located, let’s look at how to move the cursor around, and more importantly, write text to the screen.

ncurses coordinates are frequently given in (y,x) format where y is the line number, and x is the column number. The upper-leftmost coordinate is (0,0). On a “standard” terminal screen defaulted to 24 lines and 80 columns, the lower-rightmost coordinate would be (23,79). Let’s look at writing UL (upper left) and LR (lower right) to the screen in their expected positions.

move is used to place the cursor at a given set of coordinates. Writing UL in the upper-left we’ll use move(0, 0) followed by addstr to add a string.

Note: refresh is used to tell ncurses to update what is visible on the screen. If you move the cursor and addstr without a refresh you will not see a visible change. Get used to calling refresh!

To place LR in the lower-right we want to move the cursor to (23,78) and use addstr("LR").

Our entire application would look like this (make sure and add the signal handling routine unless you don’t mind your terminal window getting trashed when exiting the application):

Write an appropriate Package.swift and place the above code in a main.swift and then build it with swift build.

Try this: Add the strings “LL” and “UR” in their appropriate location on the screen. Add the text “CENTER” centered in the window. Scroll down for a hint on the centerText routine.

One Character at a Time

addch is another primitive that provides us with the capability to add a single character to the screen. The function prototype is addch(const chtype ch) where chtype is a typedef in /usr/include/curses.h, typically of an unsigned. With Swift on Linux the function requires a UInt, so to add a character with addch one would use addch(UInt("*")).

In this quick example we use addch to draw a box using * characters around the screen followed by using addstr to add “Hello world!” in the middle. Try resizing the terminal window. select exits and the application ends. We’ll handle resizing further down.

Handling SIGWINCH

Modern terminals can change size. In the days of VT100s they didn’t, at least not dynamically; the original DEC manual indicates the VT100 had 24 lines by 80 characters or you could switch it to 14 lines by 132 characters. Of course today’s Linux terminal sessions usually start off with 24 lines and 80 columns unless you’ve customized it, but once they start up you can resize them to your heart’s content.

We want our ncurses applications to respond to changes in the size of the window. For example, if our window has a border drawn around it with a drawbox routine we will have to redraw that border if the screen size changes.

Your application process is notified of window size changes via the SIGWINCH signal.

SIGWINCH – Window size change. This is generated on some systems (including GNU) when the terminal driver’s record of the number of rows and columns on the screen is changed. The default action is to ignore it. If a program does full-screen display, it should handle SIGWINCH. When the signal arrives, it should fetch the new screen size and reformat its display accordingly. –GNU Documentation

We’re going to use the mechanism defined in our previous post to handle the signal.

When receiving a SIGWINCH it is very important to run endwin(), refresh(), initscr(), and clear() in that order to “flush” the screen and get it ready for redrawing. Once this sequence is executed we then get our new max (y,x) and redraw our box. For good measure Hello world! is centered in the box.

One last thing on handling signals: you will always want to handle SIGINT and ensure that a call to endwin() is made prior to exiting the application. Failing to do so will leave the terminal you return to (i.e., your shell) in a screwed up state. Try it; leave out the SIGINT handler below and hit CTRL-C to end the application.

main.swift for drawing a box that dynamically refreshes after resizing the terminal now looks like this:

Getting Input with getch

Handling input from a terminal that is under ncurses control is a bit of a pain. Once you see what’s going on it’s not too bad but the first time through is annoying. Let’s take a look at a routine that can intercept input characters, display them on the screen, handle backspaces, and then pass back the contents of the input when the user hits return. I’ve annotated the code with // 1, // 2, etc. to highlight what’s going on.

Note: This code is lifted from a class and is not intended to be copy-paste. The full class is given below.

1. Our input string is a buffer. It’s declared as a static class variable and is used in the getInput routine and a separate redrawInput routine which we’ll cover later. delim and backspace are constants for newline and the DEL character, respectively.

2. getInput returns a string. It is a class method due to the way we interact with ncurses itself (in particular, calling back into an object with a C callback causes issues when handling signals).

3. Clear the input buffer.

4. Move the cursor to the inputLine which is defined as “the last line” in the terminal window. curx will be 0 to start off the routine.

5. Swift is “funny” (nay, advanced) with what it defines as a character (a UnicodeScalar). Ncurses is old school; character routines expect 32-bit integers. Because some character handling routines expect Int32, and some expect UInt32, and our String object wants Character objects to be added, we sort of keep multiple representations of a character hanging around. ic is a UInt32 representation, c is a Character representation.

6. Our switch statement handles what to do with a character when the user types it. If it’s a backspace we ensure that we aren’t backing up over the beginning of the line (the guard statement), and if not, move back one column, delete the character that was just typed, refresh the screen, and then update our input String by using input.characters.dropLast() to get rid of the last character typed.

7. When delim is encountered (Enter/Return key hit), the input line on the screen is cleared out and the input buffer is returned.

8. If the character typed is neither a backspace or a newline it is evaluated as to whether it is printable, and if so addch is called to put the character on the screen. Our cursor is advanced (curx += 1), the screen refreshed, and the character c added to our input buffer. Try taking out the check for isprint and then scroll your mouse wheel. Hilarity ensues.

That in a nutshell is our routine to collect characters into a buffer and display on the screen! We will use this in our next example.

Our Translator App Updated

Starting with this post and continuing with Command Line Utilities in Swift we’ve been building upon an example application that uses a public REST API to translate text from one language to another. We’ve updated the application here to utilize an ncurses interface.

Our first update is to the CommandInterpreter.swift class in that we take out all of the IO as it will now be handled by a new singleton-style class CursesInterface.swift. Here is the revamped CommandInterpreter.swift:

If you compare this routine to our previous versions there is no display of the prompt, no collecting characters, and our .Quit command now doesn’t call exit(0) but rather posts a QUIT_NOTIFICATION.

The main.swift file of the translator has also changed. Again, we will mark up the code with // 1, // 2:

1. Like the previous translator applications we still have a command interpreter and a translator.

2. We register an observer for the INPUT_NOTIFICATION NSNotification. When the trailing closure is called upon receipt of the event it will take the translationCommand and feed it to the translator routine which, in turn, will call the translation REST API service. You can see here our use of class methods of CursesInterface to update the status bar that we are translating as well as post the translation text.

3. It is very important to not simply call exit(0) anywhere lest we want a wonky terminal window left in our wake. Rather than exit out immediately upon the user typing /quit a notification is posted which is handled by CursesInterface.end() (a wrapper around the ncurses endwin() followed by exit(0)). To be prudent we should also catch a lot more signals than SIGINT in the CursesInterface class.

4. The workhorse routine. Start the interface and then enter a while true loop which, on each turn:

  • updates the prompt (which is always from->to)
  • displays the status bar
  • collects input
  • parses the input
  • executes the result of the input

Finally, we get to CursesInterface.swift. Here is where input and output to the application is handled. Nothing gets drawn on the screen elsewhere in the application except through here. CursesInterface is a sort of a singleton-style class, except that rather creating a singleton instance we leverage class methods. I explicitly chose this approach to handle catching the SIGWINCH signal and call back into the class methods in the trailing closure. Think of CursesInterface as a namespace for global state variables and routines that act on those variables. A bit like ncurses itself.

Everything in the above code with the exception of attron(), attroff(), and clrtoeol() has already been covered in some fashion. The attr functions allow for the application of attributes to the character cell. In our case we turn on reverse video to provide a “status bar”. The value of A_REVERSE is provided by ncurses by means of macros, so again, we had to reverse-engineer /usr/include/curses.h to determine it. It’s an exercise to the reader to reason out why it is Int32(1 << 18).

Putting together all of our classes we get a nice UI for translating strings!

Ncurses Translator
Ncurses Translator

Don’t worry about typing or pasting all that code in, links to everything are provided below!

Some Restrictions Apply

Unfortunately with Swift on Linux we cannot access C variadic functions, which means we cannot use, directly, many of the ncurses routines. This includes routines such as:

  • printw
  • wprintw
  • mvprintw

As with the macros that aren’t available, one can work around by providing non-variadic counterparts. As an example, mvprintw has the signature int mvprintw(int y, int x, char *fmt, ...); This can be rewritten as:

and used like this: mvprintw(y:inputLine, x:0, str:input)

Since the signature of the function is different from that of the imported C mvprintw there is no conflict and we can use our Swift version easily.

Getting the Code

We’ve covered a lot of ground in this post and there are a lot of snippets of code to reason through, so we’ve posted all of the examples in our moreswift repository on Github.

git clone https://github.com/iachievedit/moreswift

Take a look at these directories:

  • ncurses_basic – sets up a screen and writes UL and LR in the upper-left and lower-right of a 24×80 terminal. Update this app to detect the actual screen size!
  • ncurses_box_simple – draws a box around the screen with Hello world! written to the center. Resizing the terminal ends the app, fix that!
  • ncurses_box – a working example of being able to resize the box and recenter the text
  • ncurses_translator – Our translator application, complete with handling resizing of the screen, except when the screen is resized any translations already provided are lost. Fix that!

Each directory can be built with swift build. Have fun!

Postscript

If you’re enjoying reading these Swift on Linux tutorials, follow us on Twitter at @iachievedit. This blog post was not written on an iPhone, so any misspellings, grammar errors, or other word crimes are unacceptable. If you find one, drop us a line at feedback@iachieved.it.

Leave a Reply

Your email address will not be published. Required fields are marked *