iTranslated by AI
Building a Simple CUI Prompt
I already wrote about this on my main blog, but at the beginning of the new year, I saw the article:
and updated my own tool. This time, I'd like to dive a bit deeper and introduce the zetamatta/go-readline-ny package that helped me out.
go-readline-ny is a package that allows for single-line editing and input on a CUI, and it has the following features:
- It was developed for NYAGOS, an extended shell for Windows.
- Emacs-like key bindings (you can use
C-w,C-y, etc.).- It seems you can also assign functions to function keys (apparently?).
- It correctly catches Ctrl+C and Ctrl+D and returns them as errors (readline.CtrlC and io.EOF) (no need for SIGNAL operations in the upper layer).
- Can be used in combination with mattn/go-colorable.
- A simple history function can be added.
- It supports the context standard package.
- Multi-platform support (probably). At least, it works fine on Windows and Ubuntu.
It's got everything you need (lol).
As a simple prompt, bufio.Scanner from the bufio standard package is famous. While its advantage is being agnostic to the input stream, for single-line editing/input, it only provides very basic functions like backspace.
Also, the aforementioned article "Command Line Shell??? Anyone can make one" introduces mattn/go-tty. While this allows for fairly primitive operations in RAW mode, it feels a bit heavy if you just want to "easily build a simple CUI prompt." With go-readline-ny:
text, err := (&readline.Editor{}).ReadLine(context.Background())
This works perfectly. Amazing!
So, let's try building a simple CUI prompt using go-readline-ny.
First, consider the following function.
func Reverse(r []rune) []rune {
if len(r) > 1 {
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
}
return r
}
It's a simple job of just reversing the order of a Rune array. We'll pass the string entered in the simple CUI prompt through this function to flip the string[1]. How about code like this?
func main() {
//input
text, err := (&readline.Editor{
Prompt: func() (int, error) { return fmt.Print("> ") },
}).ReadLine(context.Background())
if err != nil {
fmt.Fprintln(os.Stderr, errPrint(err))
return
}
//output
fmt.Println(string(Reverse([]rune(text))))
}
By the way, the errPrint() function is a function that checks for Ctrl+C and Ctrl+D like this:
func errPrint(err error) string {
if err == nil {
return ""
}
switch {
case errors.Is(err, readline.CtrlC):
return "Aborting process"
case errors.Is(err, io.EOF):
return "Terminating process"
default:
return err.Error()
}
}
Running this results in:
$ go run sample1.go
> あいうえお
おえういあ
It works! It reverses the string correctly.
Next, let's modify the code to run this prompt repeatedly. We'll also enable a simple history feature.
func main() {
history := simplehistory.New()
editor := readline.Editor{
Prompt: func() (int, error) { return fmt.Print("> ") },
History: history,
}
fmt.Println("Input Ctrl+D to stop.")
for {
//input
text, err := editor.ReadLine(context.Background())
if err != nil {
fmt.Fprintln(os.Stderr, errPrint(err))
return
}
//output
fmt.Println(string(Reverse([]rune(text))))
//add history
history.Add(text)
}
return
}
Note that I'm using the zetamatta/go-readline-ny/simplehistory package for history control. Running this gives us:
$ go run sample2.go
Input Ctrl+D to stop.
> あいうえお
おえういあ
> しんぶんし
しんぶんし
>
Terminating process
It looks like this (the last line was ended with Ctrl+D). By the way, the up and down arrow keys display the history. Bravo!
Actually, the readline.Editor type is defined as:
type Editor struct {
KeyMap
History IHistory
Writer io.Writer
Out *bufio.Writer
Prompt func() (int, error)
Default string
Cursor int
LineFeed func(Result)
OpenKeyGetter func() (KeyGetter, error)
}
And the IHistory type within it is an interface:
type IHistory interface {
Len() int
At(int) string
}
This means that you can use your own custom history type as long as it satisfies the readline.IHistory interface.
So, I thought of the following type and methods[2].
const (
max = 50
logfile = "history.log"
)
type History struct {
buffer []string
}
var _ readline.IHistory = (*History)(nil) //compiler hint
func New() (*History, error) {
history := &History{buffer: []string{}}
file, err := os.Open(logfile)
if err != nil {
return history, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
history.Add(scanner.Text())
}
return history, scanner.Err()
}
func (h *History) Len() int {
if h == nil {
return 0
}
return len(h.buffer)
}
func (h *History) At(n int) string {
if h == nil || h.Len() <= n {
return ""
}
return h.buffer[n]
}
func (h *History) Add(s string) {
if h == nil || len(s) == 0 {
return
}
if n := h.Len(); n < 1 {
h.buffer = append(h.buffer, s)
} else if h.buffer[n-1] != s {
h.buffer = append(h.buffer, s)
}
if n := h.Len(); n > max {
h.buffer = h.buffer[n-max:]
}
}
func (h *History) Save() error {
if h == nil {
return nil
}
file, err := os.Create(logfile)
if err != nil {
return err
}
defer file.Close()
for _, s := range h.buffer {
fmt.Fprintln(file, s)
}
return nil
}
If you do it this way:
func main() {
history, err := New()
if err != nil {
fmt.Fprintln(os.Stderr, err)
//continue
}
editor := readline.Editor{
Prompt: func() (int, error) { return fmt.Print("\u003e ") },
History: history,
}
fmt.Println("Input Ctrl+D to stop.")
for {
//input
text, err := editor.ReadLine(context.Background())
if err != nil {
fmt.Fprintln(os.Stderr, errPrint(err))
break
}
//output
fmt.Println(string(Reverse([]rune(text))))
//add history
history.Add(text)
}
if err := history.Save(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
return
}
then the history will be saved to history.log up to a maximum of 50 entries. Please compare this with the previous code in sample2.go.
I think I've covered the basic functions of go-readline-ny now. Let's play around with this a bit more.
Reference Books
-
Strictly speaking, the sorting is per rune (Unicode code point), not per "character." Since variant characters or emojis can be combinations of multiple codes, reversing them with this kind of code will probably lead to disastrous results (lol). ↩︎
-
Ideally, I should probably use something like a ring buffer, but I'm slacking off this time. Sorry. ↩︎
Discussion