~evn/blog/

tilde-ing about

Curses in Fennel

August 22, 2022 — ~evn

Outside of the official documentation there aren’t a lot of examples on the web of how to use the Fennel programing language. Suppose you want to do some TUI stuff in Fennel. This will get you started.

You should have Fennel and Luarocks installed. Install the lcurses package with the command luarocks install lcurses.

Debugging curses applications can be tricky because the curses app itself prevents error messages being displayed in the terminal when an error occurs. To remedy this we wrap our application in a function that we will call “main”. We define an error handling function, and we use “xpcall”, which will call our main function, and then call our error handler if an error occurs. The error handler function will close curses, return to a normal terminal environment, and then display debugging information about the error.

(local curses (require :curses))

;; Make an object representing the whole window and clear it.
(local screen (curses.initscr))
(screen:clear)

(fn main []
  ;; Make a sub-window whose height is two rows less than the main
  ;; window height and whose width is four columns less than the main
  ;; window width. Put its top left coner at hte coordinates 1,1.
  (local win-height (- (curses.lines) 2))
  (local win-width (- (curses.cols) 4))
  (local win (screen:sub win-height
                         win-width
                         1
                         2))

  ;; Draw a border around the sub-window and add some text to it.
  (win:border)
  (win:move 1 1)
  (win:addstr "Press 'x' to exit.")
  (win:move 2 1)
  (win:addstr "Press 'e' to cause an error.")
  (win:move 3 1)
  ;; Call refresh to write the changes to the screen.
  (screen:refresh)

  ;; Enter a loop that waits for an 'x' or a 'e' keypress.
  (var k nil)
  (while true
    (set k (win:getch))
    ;; Check the value of the keypress. It will be nil if no key was
    ;; pressed.
    (if (= k 120) ;120 represents 'x' in ASCII
        ;; then close the program cleanly.
        (do
          (curses.endwin)
          (os.exit 0))
        ;; else if
        (= k 101) ;101 represents 'e' in ASCII
        ;; then call win:move without any arguments. This will cause
        ;; an error.
        (win:move))))


(fn error-handler [err]
  (curses.endwin) ;Close curses
  ;; Print debugging information to the terminal. The second argument
  ;; to debug.traceback is a debug level. Lower numbers show more
  ;; detail about the error. higher numbers show less detail.
  (print "---- Encountered an error ----.\n")
  (print (debug.traceback err 2))
  ;; Return a non-zero value to tell the OS that the program did not
  ;; complete successfully.
  (os.exit 3))

;; Call the main function. Handle errors with the error-handler
;; function.
(xpcall main error-handler)

This example was created with Lua v5.3.3, Fennel v1.0.0-1, and lcurses v9.0.0-1.

tags: fennel-lang, curses, terminal