curses is a well known set of libraries (with mostly common API) for creating TUIs. What is significantly less known though is that ncurses implementation comes with C++ wrappers for its C API.
The wrappers seem to not see an update for many years, but they still work and
are quite usable. The obvious benefits over usual C API are more
object-oriented interface, taking advantage of function grouping via classes,
more strict type checking, function overloading and default values of arguments.
The downsides are lack of documentation (although there are comments in headers
and a demo project demo.cc
in sources) and some implementation flaws.
So I found these wrappers wanting to do some quick prototyping of TUI and decided to try using them. Lack of documentation and above mentioned flaws made this harder than it should be, hence this post which tries to give a few pointers on how to and how not to use some of these wrappers. The description here is nowhere near complete and touches only on several topics I struggled with.
NCursesApplication
Using predefined main()
No need to write main()
if you setup creation of an object derived from
NCursesApplication
somewhere in your code. For example:
#include <cursesapp.h> class App : public NCursesApplication { // ... }; static App app;
Or writing your own main()
In practice however you might prefer to write main()
anyway as the one
provided by ncurses++ catches exceptions by reference (correct thing to do), but
at least NCursesMenuItem
throws pointers (wrong thing to do). Example:
int main(int argc, char *argv[]) { setlocale(LC_ALL, ""); int res; try { App app; app.handleArgs(argc, argv); res = app(); endwin(); } catch (const NCursesException *e) { endwin(); std::cerr << e->message << std::endl; res = e->errorno; } catch (const NCursesException &e) { endwin(); std::cerr << e.message << std::endl; res = e.errorno; } catch (const std::exception &e) { endwin(); std::cerr << "Exception: " << e.what() << std::endl; res = EXIT_FAILURE; } return res; }
Handling arguments
Arguments can be handled by overriding handleArgs()
method of
NCursesApplication
:
virtual void handleArgs(int argc, char* argv[]) { // ... }
Setting up title
Title either appears always or never, so size should be constant and non-zero if
title is needed, otherwise corresponding window just isn’t created inside
NCursesApplication
.
Menus
Construction and item ownership
Menus can be constructed either by passing them list of items (which are
pointers to objects of types derived from NCursesMenuItem
) via constructors
or by invoking different special constructor and using InitMenu()
function
afterward.
NCursesMenu
class supports two item management schemes:
- Lifetimes of the list of items and items are managed by the client.
- The list of items along with items are transfered to
NCursesMenu
class.
The scheme is picked via autoDelete_Items
parameter of
constructor/InitMenu()
. Beware that menu items should be detached from menu
before they can be destroyed, this means that when not using heap special care
must be taken to delete NCursesMenu
before deleting any of its items.
If you don’t do that, the program is likely to be terminated because of nested
exception thrown from item destructors (yeah, they do this kind of thing…).
Example below shows how to work around this via Base-from-Member idiom.
Constraints on list of menu items
No duplicates. List of items shouldn’t contain duplicates or weird effects
will arise. The library will use same low-level item for both items (plus
leaking N - 1
if there are N
duplicates), which won’t do any good.
Terminating null-element. List should be terminated with a pointer to an
empty NCursesMenuItem
object (the one constructed by calling
NCursesMenuItem()
). Such empty menu items can be removed freely as they don’t
own resources.
Not empty. There must be at least one non-null item or exception is thrown.
Customizing controls
Special menu keys can be customized by overwriting virtual function
NCursesMenu::virtualize()
, which should return codes suitable for passing into
menu_driver()
. For example, j and k keys can be
configured to move up and down by one item, just like in Vim:
virtual int virtualize(int c) { switch (c) { case 'j': return REQ_DOWN_ITEM; case 'k': return REQ_UP_ITEM; default: return NCursesMenu::virtualize(c); } }
Menu size
Menu doesn’t fill all available space automatically, use set_format()
method.
E.g.:
set_format(lines() - 1, 1);
Broken setItems()
setItems()
doesn’t seem to work in all cases. It always tries to free memory,
even the one it doesn’t own. So if you thought that you could use it to work
around issue with destroying items before the menu, the answer is no, you
can’t.
Multiple selection
First, it should be enabled:
options_off(O_ONEVALUE); // disable single-selection mode set_mark("*"); // setup selected item marker (not directory related)
Second, whether each item is selected or not can be determined via
NCursesMenuItem::value()
method:
if (item->value()) { // ... }
Menu item strings ownership
Just as new_item()
function, NCursesMenuItem
doesn’t copy its arguments!
This means that one shouldn’t pass in temporary strings or strings that won’t
outlive the menu item. Better way of ensuring this is to keep a copy in
std::string
and bound its lifetime to NCursesMenuItem
object.
Panels
If you want to display something and keep it there, use panels (NCursesPanel
)
and not just windows (NCursesWindow
; panels are windows with additional
layering functionality), which will be automatically updated as needed.
Accessing stdscr
To access state of the screen use one of these:
NCursesPanel screen;
NCursesWindow screen(::stdscr)
and then do screen.cols()
, screen.lines()
, etc.
Linking against the wrappers
Linking with ncurses++ requires the following libraries (even if you don’t use, say, forms out of this list):
-lncurses++w -lformw -lmenuw -lpanelw -lncursesw
Integrating readline library into the application
Thanks to flexibility of readline library, it’s easy to plug it into curses application. Here’s the code (no header/source separation for brevity):
// Compilation: g++ cmdline.cpp -c -std=c++11 #include <cursesp.h> #include <readline/history.h> #include <readline/readline.h> #include <cstddef> #include <cstdlib> #include <memory> #include <string> static inline std::wstring to_wide(const std::string &s); class CmdLine { public: CmdLine() { rl_redisplay_function = &readlineRedisplayThunk; // Prevent displaying completion menu, which could mess up output. rl_completion_display_matches_hook = [](char *[], int, int) {}; NCursesWindow screen(::stdscr); cmdline.reset(new NCursesPanel(1, screen.cols(), screen.lines() - 1, 0)); } ~CmdLine() { cmdline->hide(); NCursesWindow(::stdscr).refresh(); } public: std::string prompt(const std::string &prompt) { instance = this; const int visibility = curs_set(1); std::unique_ptr<char, decltype(&std::free)> line { readline(prompt.c_str()), &std::free }; curs_set(visibility); add_history(line.get()); return line.get(); } private: static void readlineRedisplayThunk() { instance->readlineRedisplay(); } private: void readlineRedisplay() { std::wstring prompt = to_wide(rl_display_prompt); std::wstring linebuf = to_wide(rl_line_buffer); std::size_t prompt_width = wcswidth(prompt.c_str(), static_cast<std::size_t>(-1)); std::size_t cursor_col = prompt_width + wcswidth(linebuf.c_str(), rl_point); cmdline->erase(); cmdline->printw(0, 0, "%s%s", rl_display_prompt, rl_line_buffer); if (static_cast<int>(cursor_col) >= NCursesWindow(::stdscr).cols()) { // Hide the cursor if it lies outside the window. Otherwise it'll // appear on the very right. curs_set(0); } else { cmdline->move(0, cursor_col); curs_set(2); } cmdline->refresh(); } private: std::unique_ptr<NCursesPanel> cmdline; private: // XXX: NEITHER thread safe NOR re-enterable. static CmdLine *instance; }; CmdLine *CmdLine::instance; static inline std::wstring to_wide(const std::string &s) { const std::size_t len = mbstowcs(NULL, s.c_str(), 0); if (len == static_cast<std::size_t>(-1)) { return std::wstring(); } std::wstring result(len + 1U, L'\0'); static_cast<void>(mbstowcs(&result[0], s.c_str(), len + 1U)); return result; }
And usage example:
const std::string &input = CmdLine().prompt(":"); doSomeProcessing(input);
readline adds this to linker flags:
-lreadline -ltermcap
Example
As an exercise let’s write simple filter-like tool that allows picking one or more elements from the list. It will accept list items either via arguments or from standard input.
The code above was C++11, this one is C++98. It’s also bigger in size so posted elsewhere. There are comments which restate some details of using ncurses++ from above.
I didn’t bother handling resizing, so it might not work.
Further information
There is almost no documentation at all, so read the sources (in source package
it’s under c++/
directory), README-first
file there or just headers
(cursesapp.h
, cursesf.h
, cursesm.h
, cursesp.h
, cursesw.h
,
cursslk.h
).
Couldn’t find any articles either, but with some patience one can figure out details of the code. It’s small enough and it’s a set of wrappers, so nothing sophisticated.