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:
1 2 3 4 5 6 7 8 |
import PackageDescription let package = Package( name: "ncurses_example", dependencies: [ .Package(url: "https://github.com/iachievedit/CNCURSES", majorVersion: 1), ] ) |
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:
1 |
#define getmaxyx(win,y,x) (y = getmaxy(win), x = getmaxx(win)) |
Using this definition we can use getmaxy
and getmaxx
together:
1 |
maxx = getmaxx(stdscr); maxy = getmaxy(stdscr) |
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:
1 2 3 4 |
func getmaxyx(window:UnsafeMutablePointer<WINDOW>, inout y:Int32, inout x:Int32) { x = getmaxx(window) y = getmaxy(window) } |
In this case use of the function would appear as:
1 |
getmaxyx(stdscr, y:&maxy, x:&maxx) |
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:
1 2 3 4 |
func getcuryx(window:UnsafeMutablePointer<WINDOW>, inout y:Int32, inout x:Int32) { x = getcurx(window) y = getcury(window) } |
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.
1 2 3 |
move(0, 0) addstr("UL") refresh() // This is required to update the screen |
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")
.
1 2 3 |
move(23,78) addstr("LR") refresh() |
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):
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 |
import Foundation import CNCURSES import Glibc enum Signal:Int32 { case INT = 2 case WINCH = 28 } typealias SignalHandler = __sighandler_t func trap(signum:Signal, action:SignalHandler) { signal(signum.rawValue, action) } trap(.INT) { signal in endwin() exit(0) } initscr() noecho() // Turn on noecho, though it doesn't matter in this example curs_set(0) // 0 is invisible, 1 is visible, 2 is very visible move(0, 0) addstr("UL") refresh() // This is required to update the screen move(23,78) addstr("LR") refresh() select(0, nil, nil, nil, nil) // Wait |
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.
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 |
import Foundation import CNCURSES import Glibc enum Signal:Int32 { case INT = 2 } typealias SignalHandler = __sighandler_t func trap(signum:Signal, action:SignalHandler) { signal(signum.rawValue, action) } func getmaxyx(window:UnsafeMutablePointer<WINDOW>, inout y:Int32, inout x:Int32) { x = getmaxx(window) y = getmaxy(window) } func getcuryx(window:UnsafeMutablePointer<WINDOW>, inout y:Int32, inout x:Int32) { x = getcurx(window) y = getcury(window) } func drawbox(numlines:Int32, numcols:Int32) { for y in 0...numlines-1 { for x in 0...numcols { move(y, x) if y == 0 || y == numlines-1 { addch(UInt("*")) } else { if x == 0 || x == numcols { addch(UInt("*")) } } } } refresh() } func centerText(text:String, numlines:Int32, numcols:Int32) { let cy:Int32 = numlines/2 let cx:Int32 = (numcols - Int32(text.characters.count))/2 move(cy,cx) addstr(text) refresh() } trap(.INT) { signal in endwin() exit(0) } var maxy:Int32 = 0 var maxx:Int32 = 0 initscr() noecho() curs_set(0) getmaxyx(stdscr, y:&maxy, x:&maxx) drawbox(maxy, numcols:maxx) centerText("Hello world!", numlines:maxy, numcols:maxx) select(0, nil, nil, nil, nil) endwin() |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func drawbox(numlines:Int32, numcols:Int32) { for y in 0...numlines-1 { for x in 0...numcols { move(y, x) if y == 0 || y == numlines-1 { addch(UInt("*")) } else { if x == 0 || x == numcols { addch(UInt("*")) } } } } refresh() } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
enum Signal:Int32 { case INT = 2 case WINCH = 28 } typealias SignalHandler = __sighandler_t func trap(signum:Signal, action:SignalHandler) { signal(signum.rawValue, action) } trap(.WINCH) { signal in endwin() refresh() initscr() clear() getmaxyx(stdscr, y:&maxy, x:&maxx) drawbox(maxy, numcols:maxx) centerText("Hello world!", numlines:maxy, numcols:maxx) } |
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:
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 |
import Foundation import CNCURSES import Glibc enum Signal:Int32 { case INT = 2 case WINCH = 28 } typealias SignalHandler = __sighandler_t func trap(signum:Signal, action:SignalHandler) { signal(signum.rawValue, action) } func getmaxyx(window:UnsafeMutablePointer<WINDOW>, inout y:Int32, inout x:Int32) { x = getmaxx(window) y = getmaxy(window) } func getcuryx(window:UnsafeMutablePointer<WINDOW>, inout y:Int32, inout x:Int32) { x = getcurx(window) y = getcury(window) } func drawbox(numlines:Int32, numcols:Int32) { for y in 0...numlines-1 { for x in 0...numcols { move(y, x) if y == 0 || y == numlines-1 { addch(UInt("*")) } else { if x == 0 || x == numcols { addch(UInt("*")) } } } } refresh() } func centerText(text:String, numlines:Int32, numcols:Int32) { let cy:Int32 = numlines/2 let cx:Int32 = (numcols - Int32(text.characters.count))/2 move(cy,cx) addstr(text) refresh() } trap(.INT) { signal in endwin() exit(0) } var maxy:Int32 = 0 var maxx:Int32 = 0 trap(.WINCH) { signal in endwin() refresh() initscr() clear() getmaxyx(stdscr, y:&maxy, x:&maxx) drawbox(maxy, numcols:maxx) centerText("Hello world!", numlines:maxy, numcols:maxx) } initscr() noecho() curs_set(0) getmaxyx(stdscr, y:&maxy, x:&maxx) drawbox(maxy, numcols:maxx) centerText("Hello world!", numlines:maxy, numcols:maxx) while true { select(0, nil, nil, nil, nil) } |
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 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 |
// 1 static var input:String = "" static let delim:Character = "\n" static let backspace:Character = Character(UnicodeScalar(127)) // 2 class func getInput() -> String { input = "" // 3 curx = inputCol move(inputLine, curx) // 4 refresh() while true { // 5 let ic = UInt32(getch()) let c = Character(UnicodeScalar(ic)) switch c { case backspace: // 6 guard curx != inputCol else { break } curx -= 1; move(inputLine, curx) delch() refresh() input = String(input.characters.dropLast()) case delim: // 7 clearline(inputLine) return input default: // 8 if isprint(Int32(ic)) != 0 { addch(UInt(ic)); curx += 1 refresh() input.append(c) } } } } |
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
:
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 |
import Foundation import Glibc enum CommandType { case None case Translate case SetFrom case SetTo case Quit } struct Command { var type:CommandType var data:String } // Classes class CommandInterpreter { var prompt:String { return "\(translationCommand.from)->\(translationCommand.to)" } init() { } func parseInput(input:String) -> Command { var commandType:CommandType var commandData:String = "" let tokens = input.characters.split{$0 == " "}.map(String.init) guard tokens.count > 0 else { return Command(type:CommandType.None, data:"") } switch tokens[0] { case "/quit": commandType = .Quit case "/from": guard tokens.count == 2 else { return Command(type:.None, data:"") } commandType = .SetFrom commandData = tokens[1] case "/to": guard tokens.count == 2 else { return Command(type:.None, data:"") } commandType = .SetTo commandData = tokens[1] default: commandType = .Translate commandData = input } return Command(type:commandType, data:commandData) } func doCommand(command:Command) { switch command.type { case .Quit: nc.postNotificationName(QUIT_NOTIFICATION, object:nil) case .SetFrom: translationCommand.from = command.data case .SetTo: translationCommand.to = command.data case .Translate: translationCommand.text = command.data nc.postNotificationName(INPUT_NOTIFICATION, object:nil) case .None: break } } } |
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 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 |
import Foundation import Glibc // 1 let interpreter = CommandInterpreter() let translator = Translator() // 2 nc.addObserverForName(INPUT_NOTIFICATION, object:nil, queue:nil) { (_) in let tc = translationCommand CursesInterface.displayStatusBar("Translating") translator.translate(tc.text, from:tc.from, to:tc.to){ translation, error in guard error == nil && translation != nil else { CursesInterface.displayTranslation("Translation failure: \(error!.code)") return } CursesInterface.displayTranslation(translation!) } } // 3 nc.addObserverForName(QUIT_NOTIFICATION, object:nil, queue:nil) { (_) in CursesInterface.end() } // 4 CursesInterface.start() while true { CursesInterface.setPrompt(interpreter.prompt) CursesInterface.displayStatusBar() let input = CursesInterface.getInput() let command = interpreter.parseInput(input) interpreter.doCommand(command) } |
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.
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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
import Foundation import CNCURSES import Glibc enum Signal:Int32 { case INT = 2 case WINCH = 28 } typealias SignalHandler = __sighandler_t func trap(signum:Signal, action:SignalHandler) { signal(signum.rawValue, action) } func mvprintw(y y:Int32, x:Int32, str:String) -> Int32 { move(y,x) return addstr(str) } class CursesInterface { // Constants/Attributes static let delim:Character = "\n" static let backspace:Character = Character(UnicodeScalar(127)) static let A_REVERSE = Int32(1 << 18) // Ncurses screen positions static var maxy:Int32 = 0 static var maxx:Int32 = 0 static var cury:Int32 = 0 static var curx:Int32 = 0 static var liny:Int32 = 0 static let inputCol:Int32 = 0 class var statusLine:Int32 { get { return maxy - 2 } } class var inputLine:Int32 { get { return maxy - 1 } } // Prompt static var prompt:String = "" class func start() { trap(.INT) { signal in CursesInterface.end() } trap(.WINCH) { signal in CursesInterface.reset() CursesInterface.getDisplaySize() CursesInterface.displayStatusBar(CursesInterface.prompt) CursesInterface.displayInput() } CursesInterface.reset() CursesInterface.getDisplaySize() CursesInterface.displayStatusBar(CursesInterface.prompt) } class func reset() { endwin() refresh() initscr() clear() noecho() curs_set(1) liny = 0 } class func getDisplaySize() { maxx = getmaxx(stdscr) maxy = getmaxy(stdscr) } class func setPrompt(prompt:String) { CursesInterface.prompt = prompt } class func displayStatusBar(status:String = CursesInterface.prompt) { let cols = maxx let pad = cols - status.characters.count - 1 var paddedStatus = status for _ in 1...pad { paddedStatus += " " } move(CursesInterface.statusLine, 0) attron(A_REVERSE) addstr(paddedStatus) addch(UInt(" ")) attroff(A_REVERSE) refresh() } class func displayTranslation(translation:String) { move(liny, 0); liny += 1 addstr(translation) refresh() } class func end() { endwin() exit(0) } static var input:String = "" class func getInput() -> String { input = "" curx = inputCol move(inputLine, curx) refresh() while true { let ic = UInt32(getch()) let c = Character(UnicodeScalar(ic)) switch c { case backspace: guard curx != inputCol else { break } curx -= 1; move(inputLine, curx) delch() refresh() input = String(input.characters.dropLast()) case delim: clearline(inputLine) return input default: if isprint(Int32(ic)) != 0 { addch(UInt(ic)); curx += 1 refresh() input.append(c) } } } } class func displayInput() { move(inputLine, 0) addstr(input) refresh() } class func clearline(lineno:Int32) { move(lineno, 0) clrtoeol() refresh() } } |
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!
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:
1 2 3 4 |
func mvprintw(y y:Int32, x:Int32, str:String) -> Int32 { move(y,x) return addstr(str) } |
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 writesUL
andLR
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 withHello 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 textncurses_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.