ncurses
ncurses provides Scala Native bindings for the GNU Ncurses C library.
Overview
The goal of this project is to provide an easy-to-use Scala Native facade for the entire Ncurses C library, including the panel, menu and form extension libraries, which are bundled with the main ncurses library. This project is active and will be kept up-to-date with respect to new Scala Native releases.
The more “programmer friendly” part of this library is found in the io.github.edadma.ncurses
package. That’s the only package you need to import from, as seen in the examples below. The other package in the library is io.github.edadma.ncurses.extern
which provides for interaction with the Ncurses C library using Scala Native interoperability elements from the so-call unsafe
namespace. There are no public declarations in the io.github.edadma.ncurses
package that use unsafe
types in their parameter or return types, making it a pure Scala Consequently, you never have to worry about memory allocation or type conversions.
Efficient screen output
All facade classes that relate to screen output are Scala value classes which means that we can have a nice “Scala-esque” facade without sacrificing efficiency. Thanks to Scala’s value classes, we can wrap native pointers to Ncurses data structures without the overhead of object instantiation.
There is one facade class relating to responding to mouse events that is not a value class, but it only gets instantiated when there’s a mouse click and therefore does not affect screen output efficiency.
Library coverage and usability
Currently, a large part of the basic Ncurses library is covered, as well as most of the “panel” library. Each subsequent release will see more ncurses functions and variables added, and each minor release will include support for another extension library (support for menus is next).
As it stands now, this library is very usable and can be used to implement a wide variety of terminal applications in Scala. Of course, as with any terminal application, the terminal emulator that you run it on can make a difference. The terminal inside IntelliJ, for instance doesn’t do mouse events, so to really test the library, I’ve been using Tilix which has been working great.
Usage
To use this library, libncurses-dev
needs to be installed:
sudo apt install libncurses-dev
Include the following in your build.sbt
:
Include the following in your project/plugins.sbt
:
addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.3")
Include the following in your build.sbt
:
resolvers += Resolver.githubPackages("edadma")
libraryDependencies += "io.github.edadma" %%% "ncurses" % "0.2.6"
Use the following import
in your code:
import io.github.edadma.ncurses._
Examples
The following examples are taken from the well-known NCURSES Programming HOWTO, with only slight changes where needed.
Example 1
The obligatory “Hello World” example.
import io.github.edadma.ncurses._
object Main extends App {
initscr /* Start curses mode */
printw("Hello World !!!"); /* Print Hello World */
refresh /* Print it on to the real screen */
getch /* Wait for user input */
endwin /* End curses mode */
}
Example 4
Basic keyboard input example.
import io.github.edadma.ncurses._
object Main extends App {
try {
initscr
val mesg = "Enter a string: "
val (rows, cols) = stdscr.getmaxyx
printw(rows / 2, (cols - mesg.length) / 2, "%s", mesg)
val (_, str) = getstr(20)
noecho
curs_set(0)
addstr(LINES - 1, 0, s"You entered: $str")
getch
} catch {
case e: Exception =>
endwin
e.printStackTrace()
sys.exit(1)
}
endwin
}
Example 11
Basic mouse input example.
import io.github.edadma.ncurses._
object Main extends App {
val WIDTH = 30
val HEIGHT = 10
val choices = Vector("Choice 1", "Choice 2", "Choice 3", "Choice 4", "Exit")
val n_choices = choices.length
var choice = 0
/* Initialize curses */
initscr
clear
noecho
cbreak //Line buffering disabled. pass on everything
/* Try to put the window in the middle of screen */
val startx = (COLS - WIDTH) / 2
val starty = (LINES - HEIGHT) / 2
attron(A_REVERSE)
printw(LINES - 1, 1, "Click on Exit to quit (Works best in a virtual console)")
refresh
attroff(A_REVERSE)
/* Print the menu for the first time */
val menu_win = newwin(HEIGHT, WIDTH, starty, startx)
menu_win.keypad(true)
print_menu(menu_win, 1)
/* Get all the mouse events */
mousemask(ALL_MOUSE_EVENTS)
mouseinterval(0)
while (true) {
menu_win.getch match {
case `KEY_MOUSE` =>
val (res, event) = getmouse
if (res == OK)
/* When the user clicks left mouse button */
if ((event.bstate & BUTTON1_PRESSED) != 0) {
choice = report_choice(event.x + 1, event.y + 1).getOrElse(choice)
if (choice == -1) { //Exit chosen
endwin
sys.exit()
}
printw(LINES - 2, 1, "Choice made is : %d String Chosen is \"%10s\"", choice, choices(choice - 1))
refresh
}
print_menu(menu_win, choice)
case _ =>
}
}
def print_menu(menu_win: Window, highlight: Int): Unit = {
val x = 2
var y = 2
menu_win.box(0, 0)
for (i <- 0 until n_choices) {
if (highlight == i + 1) {
menu_win.attron(A_REVERSE);
menu_win.printw(y, x, "%s", choices(i));
menu_win.attroff(A_REVERSE);
} else
menu_win.printw(y, x, "%s", choices(i));
y += 1
}
menu_win.refresh
}
/* Report the choice according to mouse position */
def report_choice(mouse_x: Int, mouse_y: Int): Option[Int] = {
val i = startx + 2
val j = starty + 3
for (choice <- 0 until n_choices)
if (mouse_y == j + choice && mouse_x >= i && mouse_x <= i + choices(choice).length)
if (choice == n_choices - 1)
return Some(-1)
else
return Some(choice + 1)
None
}
}
Example 15
Basic panels (with colors) example.
import io.github.edadma.ncurses._
object Main extends App {
val NLINES = 10
val NCOLS = 40
/* Initialize curses */
initscr
start_color
cbreak
noecho
stdscr.keypad(true)
/* Initialize all the colors */
init_pair(1, COLOR_RED, COLOR_BLACK)
init_pair(2, COLOR_GREEN, COLOR_BLACK)
init_pair(3, COLOR_BLUE, COLOR_BLACK)
init_pair(4, COLOR_CYAN, COLOR_BLACK)
val my_wins = init_wins(3)
/* Attach a panel to each window */
/* Order is bottom up */
val my_panels = my_wins map new_panel
/* Set up the user pointers to the next panel */
for (i <- my_panels.indices)
my_panels(i).set_panel_userptr(my_panels((i + 1) % my_panels.length))
/* Update the stacking order. 2nd panel will be on top */
update_panels()
/* Show it on the screen */
attron(COLOR_PAIR(4))
printw(LINES - 2, 0, "Use tab to browse through the windows (F1 to Exit)")
attroff(COLOR_PAIR(4))
doupdate
var top = my_panels(2)
var ch: Int = _
while ({ ch = getch; ch != KEY_F1 }) {
ch match {
case '\t' =>
top = top.panel_userptr
top.top_panel
case _ =>
}
update_panels()
doupdate
}
endwin
/* Put all the windows */
def init_wins(n: Int): Seq[Window] = {
var y = 2
var x = 10
for (i <- 0 until n)
yield {
val win = newwin(NLINES, NCOLS, y, x)
win_show(win, s"Window Number ${i + 1}", i + 1)
y += 3
x += 7
win
}
}
/* Show the window with a border and a label */
def win_show(win: Window, label: String, label_color: Int): Unit = {
val width = win.getmaxx
win.box(0, 0)
win.addch(2, 0, ACS_LTEE)
win.hline(2, 1, ACS_HLINE, width - 2)
win.addch(2, width - 1, ACS_RTEE)
print_in_middle(win, 1, 0, width, label, COLOR_PAIR(label_color))
}
def print_in_middle(win: Window, starty: Int, startx: Int, width: Int, string: String, color: Int): Unit = {
val y = if (starty != 0) starty else win.getcury
val x = startx + (width - string.length) / 2
win.attron(color)
win.printw(y, x, "%s", string)
win.attroff(color)
refresh
}
}
Ncurses C library documentation
The official documentation for the Ncurses library can be found at ncurses. The official documentation for the extension libraries can be found at panel, menu, and form.
Guidelines
In these guidelines, the phrase library function/variable refers to the GNU Ncurses C library function/variable that has a counterpart in this library. Likewise, the phrase facade method/variable/constant/declaration refers to a method/variable/constant in this library that has a counterpart in the GNU Ncurses library.
There are hundreds of functions in the GNU Ncurses C library. Therefore, in order to be able to find library documentation corresponding to a given facade declaration or, vice versa, in order to know which facade declaration corresponds to a given library function or variable, there needs to be clear method naming guidelines. Also, in the case where facade methods have a different number of parameters or return values (in the cases where facade methods return a tuple) from their library function counterparts, there needs to be clear method parameter and return type guidelines.
The following are the guidelines being adhered to.
Naming
In general, the names of nearly all facade declarations shall be exactly the same as their library counterparts. This guideline will make it easier to lookup documentation for a given library function or variable. And, vice versa, this will make it easy to know the name for a facade counterpart to a given library function or variable. There are a number of exceptions to this guideline arising from the “Value class method names” and “Overloaded method variants” guidelines, however because of how the GNU Ncurses library documentation is organized, those exceptions won’t cause a problem in finding relevant documentation.
Methods that return a tuple
There are a few facade methods that return a tuple. If the library function has a return value, then the first component of that tuple shall be the library function return value. The remaining components shall be values that are returned via pointer arguments to the library function.
Value class method names
The Ncurses library already follows a certain naming convention fairly consistently:
- function names prefixed with “w” are window operations
- function names prefixed with “mv” are output operations that also move the cursor to a new position first
- function names prefixed with “mvw” are a combination of the above
- in many, but not all cases function names prefixed with “w” and “mvw” have corresponding
stdscr
functions without the “w” in the name prefix.
The guideline being followed here is that window operation functions with a “w” in their prefix that have a corresponding stdscr
version of the function (without the “w”), shall have a corresponding Window
facade method that drops the “w” in the name prefix. However, window operation functions with a “w” in their prefix that do not have a corresponding stdscr
version of the function at all shall keep the “w” in the name prefix. This rule makes it easier to look up documentation for corresponding library functions.
Overloaded method variants
As mentioned above, the Ncurses library tends to follow a function naming convention for variants of the same operation. In cases where these variants can be overloaded in Scala, the base name (i.e. without the extra prefix or infix letters) shall be used throughout. An example of this is the large number of functions such as addstr
/mvaddstr
that have variants with and without the ‘mv’ prefix. There are also pairs of functions with infix naming variants such as addstr
/addnstr
that can be overloaded.
This guideline shall not apply in cases where the resulting name doesn’t appear in the official Ncurses C library documentation.
Value class method parameters
All value class methods correspond to library functions whose first parameter has a pointer type corresponding to the underlying runtime pointer type of the value class. Therefore, the facade method doesn’t have that first parameter.