csup - Crystal rewrite of sup email client

Ncurses
Login

Ncurses

Sup uses the Ruby ncursesw gem as the library for screen and keyboard access. Rather than attempting to port this gem to Crystal, I chose to use the SamualLB/ncurses shard on github, which provides similar functionality. In order to make it easier to port the existing Sup code that used ncursesw, I wrote an interface library that wraps the ncurses shard in a way that makes it look like the ncursesw gem. See the source file src/supcurses.cr for details.

Keyboard and Mouse

There are a few differences in the way Csup uses ncurses, as compared with Sup. The biggest difference is in how Csup handles keyboard and mouse events. In Sup, this is quite complicated, because regular keys have to be distinguished from function keys. Mouse events add a further complication.

In Csup, I chose to simplify the keyboard interface by providing an Ncurses.getkey method that handles all keyboard and mouse events, and returns a string for each event. In the case of ordinary keys, the string is a single character containing the ASCII key. For function keys, the string is the name of the key. For Ctrl key combos, the string is "C-x", where "x" is the key. For Alt+Ctrl key combos, the string is "M-C-x", where "x" is the key. For mouse events, the string is either "click" or "doubleclick"; commands that bind to these strings must call Ncurses.getmouse_y to get the line number.

This scheme simplifies the key binding tables, which can now refer to the key or mouse event with a string.

The Ncurses.getkey method takes an optional timeout parameter; if present, it specifies the number of seconds to wait for a keyboard or mouse event. If the timeout occurs, getkey returns the string "ERR". Csup uses this feature to handle the poll_interval option in ~/.csup/config.yaml. If Csup receives "ERR" at the main command prompt, it runs the poll command, which is also bound to "P".

Forms

Sup uses Ncurses forms to implement the "ask" buffer, where users type an input line in response to a question. Tab-initiated completions complicate this code even more. The result is a very confusing control flow that switches back and forth between the form handling in textfield.rb and the completion handling in buffer.rb. The following comment by William Morgan in textfield.rb expresses his feelings about this code:

writing this fucking sucked. if you thought ncurses was some 1970s before-people-knew-how-to-program bullshit, wait till you see ncurses forms.

Writing the code must have been bad enough, but reading it sucks, too. So in Csup I chose to eliminate the use of Ncurses forms. Instead, I implemented a line buffer editor, which was really not very difficult to write, and which keeps the control flow all in one place. See the do_ask method in src/buffer.cr for details.

The one feature of Sup's form handling that I didn't implement was history. I'm not sure how useful this would be, and I was not even aware of the feature until I started reading the code.