標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

go-readline-ny でマルチライン編集

go-readline-ny

  • Enterで改行挿入だけ
  • Ctrl-Enter or Ctrl-J で入力内容確定(送信)

が出来たらいいなぁと思ったので、トライしてみました。

当初はパッケージの改造がいるかなぁと思ってたんですが、キーマップへのコールバック関数のアサインだけでなんとかなってしまいました。

で、作っているうちに欲が出てきて「前の行が間違っていた時 Ctrl-C で全部破棄して入力しなおし(ヒストリは使えるものの…)」というのも芸がないので、Ctrl-P で前の行に戻れるようにもしてみました。 (ここまでパッケージ改造なし)

package main

import (
    "context"
    "errors"
    "fmt"
    "os"

    "github.com/nyaosorg/go-readline-ny"
)

func readMultiLine(ctx context.Context, editor *readline.Editor) ([]string, error) {
    submit := false
    editor.BindKeyClosure(readline.K_CTRL_J, func(_ context.Context, B *readline.Buffer) readline.Result {
        submit = true
        return readline.ENTER
    })

    csrline := 0
    upper := false
    upperFunc := func(_ context.Context, B *readline.Buffer) readline.Result {
        if csrline <= 0 {
            return readline.CONTINUE
        }
        upper = true
        return readline.ENTER
    }
    editor.BindKeyClosure(readline.K_CTRL_P, upperFunc)
    editor.BindKeyClosure(readline.K_UP, upperFunc)

    editor.LineFeed = func(rc readline.Result) {
        if rc == readline.ENTER && upper {
            return
        }
        fmt.Fprintln(editor.Out)
    }

    enterFunc, err := readline.GetFunc(readline.F_ACCEPT_LINE)
    if err != nil {
        return nil, err
    }
    editor.BindKeyFunc(readline.K_DOWN, enterFunc)
    editor.BindKeyFunc(readline.K_CTRL_N, enterFunc)

    lines := []string{}

    editor.Prompt = func() (int, error) {
        return fmt.Printf("%d> ", csrline)
    }
    for {
        if csrline < len(lines) {
            editor.Default = lines[csrline]
        } else {
            editor.Default = ""
        }
        line, err := editor.ReadLine(ctx)
        if err != nil {
            if errors.Is(err, readline.CtrlC) {
                lines = lines[:0]
                fmt.Println("^C")
                continue
            }
            return nil, err
        }
        if csrline >= len(lines) {
            lines = append(lines, line)
        } else {
            lines[csrline] = line
        }
        if submit {
            for i := csrline + 1; i < len(lines); i++ {
                fmt.Println()
            }
            return lines, nil
        } else if upper {
            upper = false
            csrline--
            fmt.Printf("\r\x1B[A")
        } else {
            csrline++
        }
    }
}

func mains() error {
    editor := &readline.Editor{}
    ctx := context.Background()
    fmt.Println("Enter, DOWN or Ctrl-N: New line or move to the next line")
    fmt.Println("UP or Ctrl-P: Move to the previous line.")
    fmt.Println("Ctrl-Enter: Sumbit")
    fmt.Println("Ctrl-C: Cancel lines.")
    fmt.Println("Ctrl-D: Quit.")
    for {
        lines, err := readMultiLine(ctx, editor)
        if err != nil {
            return err
        }
        fmt.Println("-----")
        for _, s := range lines {
            fmt.Println(s)
        }
        fmt.Println("-----")
    }
}

func main() {
    if err := mains(); err != nil {
        fmt.Fprintln(os.Stderr, err.Error())
        os.Exit(1)
    }
}

ここまで作ったのなら、readMultiLine のところをパッケージ化してもいいんちゃうとも思いますが、汎用化するなら、名前とか仕様とかをちゃんと考えないといけないので、今回のところはとりあえずここまで。

追記

パッケージ化しました。