Monday, 27 March 2017

Gambas cli programming: ncurses text based user interface

Many command-line programs are more than just one liners.


They often need to display data and allow users to interface by both viewing data and making selections.


In this post I look at the Gambas component gb.ncurses and attempt to make an interactive display.

There are almost 100 Gambas components available covering everything from scanner management to web development, from text editors to databases. Using components allows the Gambas developer to draw on the power of other technologies.

Unfortunately, component documentation sometimes lags behind the availability of the component itself. And since the creator of Gambas (Benoît Minisini) is not a native English speaker, I often struggle to understand what documentation there is. Although I should point out that the ncurses component is actually the work of Tobias Boege, and I often struggle with software documentation anyway, simply because the author is normally light-years ahead of me!

Curses! ...what is ncurses


Ncurses is a programming library which allows you to create programs which look like alsamixer. The Gambas component gb.ncurses wraps ncurses functionality.

To use it, create a new Gambas cli project and add gb.ncurses from the list at menu Project > Properties > Components.

Also go to Project > Properties > Options and set "Use a terminal emulator" to "Yes".

Here is a very basic "Hello World!" example:-

Public Sub Main()
 
  With Window
    .Border = Border.ACS          'continuous line border
    .Caption = "a window on the world"
    .Print("\nHello world!")      'line-feed + text
  End With
 
End


We can add some colour, underline the message and move it within the window;

Public Sub Main()
 
  With Window
    .Border = Border.ACS  
    .Background = Color.Green
    .Caption = "a window on the world"
    .Print("Hello world!", 5, 5, Attr.Underline, Pair[Color.Red, Color.Green])
  End With
 
End


In the print statement, the two numbers are x & y positions, and the Pair[ ] controls the foreground and background colours.




a terminal is a terminal, is a terminal


Light-weight operating systems like Lubuntu often include their own terminal programs. As Lubuntu uses LXDE, the default terminal is LXTerminal, and this does not seem to have full functionality when compared to alternatives such as Xterm.

This can be illustrated by modifying the Hello World! example to make the text blink.

Public Sub Main()
 
  With Window
    .Border = Border.ACS       
    .Background = Color.Green
    .Caption = "a window on the world"
    .Print("Hello world!", 5, 5, Attr.Blink, Pair[Color.Red, Color.Green])
  End With
 
End


By creating an executable of this code, it is then possible to run it in an LXTerminal (the text does not blink) and an Xterm (the text does blink). Differences like this may be important when you are writing code (especially if you distribute to others).

using events


As in an earlier Gambas cli post, we can use an event timer. In this case a timer is used to update the window containing a clock display.

Public hTimerScreen As Timer    'declare screen update timer

Public Sub Main()

  'this timer re-writes the screen at 1s intervals
  hTimerScreen = New Timer As "tmrScreenUpdate"
  hTimerScreen.Delay = 1000
  hTimerScreen.start

End

Public Sub tmrScreenUpdate_Timer()
 
  With Window
    .Border = Border.ACS
    .Background = Color.Green
    .Caption = "  clock \r"      '\r returns cursor to start of line
    .PrintCenter(Format(Now, "hh:mm:ss"), Attr.Bold, Pair[Color.Black, Color.Green])
  End With

End


As you can see, this program just keeps printing the time each time the window is updated by the 1 second timer...


...but the principle shows that you can display updating information (e.g. temperature, data rate & so on).


what about pressing buttons?


The final example illustrates user interaction;



This requires us to create a new instance of "window", give it a name ("qWindow"), and then use Read events to respond to key-presses.

Public hTimerScreen As Timer        'screen update timer
Public hUserInterface As Window     'our user interface window
Public lngCount As Long       'just a trivial counter
'
Public Sub Main()

  'this timer re-writes the screen at 1s intervals
  hTimerScreen = New Timer As "tmrScreenUpdate"
  hUserInterface = New Window(True, 0, 0, 200, 15) As "qWindow"
  hTimerScreen.Delay = 1000
  hTimerScreen.start

End

Public Sub tmrScreenUpdate_Timer()
 
  Inc lngCount
  Screen.Echo = False 'don't display key press
  With hUserInterface
    .Border = Border.ACS
    .Background = Color.Green
    .Caption = "clock\r"
    .Print("Counter: " & lngCount, 2, 2, Attr.Blink, Pair[Color.Blue, Color.Green])
    .PrintCenter(Format(Now, "hh:mm:ss"), Attr.Bold, Pair[Color.Black, Color.Green])
    .Print("Press to reset counter, key to quit", 20, 13, Attr.Underline, Pair[Color.Blue, Color.Green])
    .SetFocus()
  End With

End

Public Sub qWindow_Read()
Dim iKey As Integer = Window.Read()

  If iKey = Key["r"] Then
    lngCount = 0
  Endif
  If iKey = Key["q"] Then
    Quit
  Endif
  hUserInterface.Raise()
End


The .SetFocus method is required in order that our Window instance can raise the _Read event.

The .Raise method is required to keep the window on top (visible).

other weird things


The documentation does not give information on some of the Window methods including .Lower (probably the opposite of .Raise), .Drain (I've no idea) and .SetFullScreen.

Using \n (new-line) or \r (like carriage return) may over-write x/y arguments used in the same command (e.g. Print).

Leaving x/y argument blank generally defaults them to zero. Leaving height/width parameters blank normally defaults them to maximum.

Also have no idea at the moment regarding input constants Cooked, Raw & CBreak.

I've just worked out that you can hide the flashing cursor by adding:-

Screen.Cursor = Cursor.Hidden

...to the timer event in the code above.


I worked out most of this stuff from this:-
http://gambaswiki.org/wiki/comp/gb.ncurses
...and this: http://gambaswiki.org/wiki/tutorial/ncursestut


Hopefully the information in this post may get you started, and it will certainly remind me how it works 6 months from now!

5 comments:

  1. I tried your code but ran into strange error messages. As I used a general 'mess about with' project I had several components selected. These, I discovered, were causing the errors. So make sure you only have 'gb' and 'gb.ncurses' components selected if you get errors.

    ReplyDelete
    Replies
    1. Yes, I think the easy way is to start with the new project wizard and select "command-line application" as project type, then add gb.ncurses from the Components list.

      Delete
  2. Hi, nice tutorial, thank you! One question, how can i move the cursor?

    ReplyDelete
  3. Very sorry that its taken so long to get back to you.

    You can set the cursor position to (say) x = 5, y = 7 by using:-
    Window.cursorX = 5
    Window.cursorY = 7

    If you do this followed by a window.print command you will see that this has worked by the position of the printed text.

    However, I can't seem to display the actual cursor by setting:-
    Screen.Cursor = Cursor.Visible

    ReplyDelete
  4. Thanks, but i need move the current cursor. I can't move it.

    ReplyDelete