How to access the whole ncurses API on Mac?

Hello folks,

I am porting my tiny console UI toolkit library that I wrote for C# to Swift which is built on top of ncurses [1]. The Darwin.ncurses library does not seem to surface enough of the API from ncurses (character input, and various definitions) and I have struggled to find a way of doing my own binding for ncurses so far as the information seems to have changed significantly over time.

There are a number of github projects called CNCurses with many variations [2] but none of them appear to compile anymore, with errors like:

error: the package does not contain a buildable target

I suspect the Darwin.ncurses is lacking methods purely by how the binding was produced - some macros are missing, some methods are missing -likely a missing define at compile time?

What I was hoping was to produce some sort of projection from the C API in ncurses.h to Swift that I could consume, and I could drive that binding by providing the proper compile time definitions, am I on the right path here?

[1] GitHub - gui-cs/Terminal.Gui: Cross Platform Terminal UI toolkit for .NET
[2] GitHub - iachievedit/CNCURSES: Modulemap for NCurses https://github.com/iachievedit/IgorMuzyka/Cncurses

8 Likes

Awesome! I love seeing more command-line packages.

There are two potential issues here.

Unfortunately, the Darwin ncurses is very old. The version in Xcode 10.2 is 5.7, which hails from 2008. Do you know if this old of a version is suitable for your needs? Would it have the declarations you expect?

The other potential issue is that the declarations are somehow un-importable into Swift. This is pretty rare, so it'd be interesting to see what's going on. We can often improve the import story through extra attributes via something like an apinotes file.

Can you share your progress so far? I'd like to take a look.

2 Likes

The problem with Darwin.ncurses is that the declarations are missing - perhaps because Swift was not able to import them? I suspect the real reason is that whoever did the import did not specify the _XOPEN_SOURCE_EXTENDED define, hence keeping a number of APIs from being bound.

I have seen lots of folks have that CNCurses variation, but none of them build. What I am looking for is for some help to get one of these working:

[2] https://github.com/iachievedit/CNCURSES https://github.com/iachievedit/IgorMuzyka/Cncurses

Last time I checked, the Darwin modulemap was missing the 'menu' and 'panel' headers, so you have to make your own. Refer to this post: Ncurses on Linux with Swift - #2 by Karl

That error message you posted makes it sound like a changes to the manifest format made it so SwiftPM could no longer able to understand the package, but I didn't investigate in depth.

Thanks Karl - I did try that post, it was my first attempts, but could not get anywhere. "swift build" fails with the error described at the top of this post. That is where I am stuck.

I am fine without menu and panels, my library rolls its own.

Could you provide an example of a specific declaration that is missing and I can see what happened to it?

_XOPEN_SOURCE_EXTENDED seems like a candidate, as it does guard many of the "wch" declarations. @rintaro or @Michael_Gottesman, do you know who would be familiar with the flags we pass for the Darwin module? Any idea why we might be defining or not defining it?

That claims to be Linux only, but I don't see any reason it must be so. @Aciid, if I try to build that it complains:

CNCURSES: error: package at 'CNCURSES' is using Swift tools version 3.1.0 which is no longer supported; use 4.0.0 or newer instead

How can the package update? Also, I see that it has a hard-coded header path, which doesn't correspond to where curses.h from Xcode (old 5.7) or a new one from home-brew (6.1) would live. How can this be made more flexible?

I haven't seen @migueldeicaza's error message, @Aciid, any idea what that means?

1 Like

You're getting this because you can't really build a system library target. They only act as a bridging layer to import C types into Swift. Once you setup a system library, you need some buildable target (library, executable, unit test) in order to use it. I don't think you can use the brew version of ncurses in Swift because that gets in a conflict with the one in SDK. Since you can already import ncurses using the Darwin module, there is no need of creating a system library on Mac. The examples you quoted are useful for linux since Glibc's modulemap probably doesn't include ncurses.h.

The one I was looking for was get_wch, or any of the variations for wide-character keyboard input. Those were the ones behind that _XOPEN_SOURCE_EXTENDED define.

Earlier I found that some of the macros were also missing, I just went and copied the values that I had from my C# binding (A_NORMAL, A_STANDOUT, and the rest of the attributes).

This is my intermediate workaround to unblock my work:

typealias get_wch_def = @convention(c) (UnsafeMutablePointer<Int32>) -> Int
var get_wch_fn : get_wch_def? = nil
let rtld_default = UnsafeMutableRawPointer(bitPattern: -2)

...
let get_wch_ptr = dlsym (rtld_default, "get_wch")
get_wch_fn = unsafeBitCast(get_wch_ptr, to: get_wch_def.self)

And then using get_wch_fn as a replacement.

Thanks for the guidance @Aciid - that sheds some light into the problem.

A newer version of ncurses would be convenient to bind, but it is not necessary for my UI toolkit.

My follow up question is: if I have a system-library "package" in this form, how can I consume it and the types it might bring in into Xcode? I have also struggled to find an answer to that, would love to reuse any of these CNCurses definition as they are - I believe they all rely on the compilation flags to know where to resolve ncurses.h from.

As I said, you can't use these ncurses system library packages (which are probably meant for curses installed with brew) on Mac because they get into a conflict with the one that's already installed in the SDK.

Is it possible to create a secondary binding, say "Miguel.ncurses" that would allow me to get access to those APIs?

Ah, I am able get access to those APIs by combining system library target (non-brew) with the recent build settings feature that landed in Swift 5. The trick is to define the _XOPEN_SOURCE_EXTENDED macro in cSettings. Here is a sample package that demonstrates this: https://github.com/aciidb0mb3r/swift-package-manager/tree/curses/Examples/ncurses

Earlier I found that some of the macros were also missing …

A common cause of this is that the macro is too complex for the Swift C importer to deal with. I’m not a curses expert, so let’s pick an example from something more generic, like stat. In this program:

print(S_IRUSR)
print(DEFFILEMODE)

the first line compiles and the second line doesn’t. That’s because the Swift C importer can deal with this:

#define S_IRUSR         0000400         /* [XSI] R for owner */

but not this:

#define DEFFILEMODE     (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)

I prefer not to hard-code macro constants in my source code, so I generally resolve this by writing C code that ‘exports’ the macro in a format that Swift can understand. For example:

// In my C header…

extern mode_t defaultFileMode;

// In my C implementation…

mode_t defaultFileMode = DEFFILEMODE;

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Thank you for putting together this sample, this is great!

So this means the next major release of Midnight Commander is written in Swift now? ;-)

1 Like

Follow up question, I hope you do not mind - but how can I consume this from an Xcode project? My Google skills are not working for me.

Use swift package generate-xcodeproj from within the project directory

1 Like

One can hope! I have not worked on it for decades, but still use it every day - the new maintainers keep it going :-)

2 Likes