166 lines
3.0 KiB
Go
166 lines
3.0 KiB
Go
package prompt
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"log/slog"
|
||
|
|
"unicode"
|
||
|
|
"unicode/utf8"
|
||
|
|
|
||
|
|
"code.chimeric.al/dwrz/wen/internal/editor/buffer/glyph"
|
||
|
|
"code.chimeric.al/dwrz/wen/internal/editor/buffer/line"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Prompt struct {
|
||
|
|
log *slog.Logger
|
||
|
|
|
||
|
|
err error
|
||
|
|
cb CB
|
||
|
|
cursor cursor
|
||
|
|
line *line.Line
|
||
|
|
text string
|
||
|
|
}
|
||
|
|
|
||
|
|
type CB func(l line.Line) error
|
||
|
|
|
||
|
|
// cursor tracks the position in the prompt input line.
|
||
|
|
// glyph is the 0-indexed terminal display column.
|
||
|
|
// index is the 0-indexed byte offset in line's UTF-8 string.
|
||
|
|
type cursor struct {
|
||
|
|
glyph int
|
||
|
|
index int
|
||
|
|
}
|
||
|
|
|
||
|
|
type Parameters struct {
|
||
|
|
Log *slog.Logger
|
||
|
|
}
|
||
|
|
|
||
|
|
func New(p Parameters) *Prompt {
|
||
|
|
return &Prompt{log: p.Log}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Complete runs the prompt callback with the current input line.
|
||
|
|
// If the callback returns an error, the prompt stays active and displays the
|
||
|
|
// error; otherwise the prompt state is cleared.
|
||
|
|
func (p *Prompt) Complete() {
|
||
|
|
if err := p.cb(*p.line); err != nil {
|
||
|
|
p.log.Error("callback error", slog.Any("error", err))
|
||
|
|
p.err = err
|
||
|
|
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// No error -- we should be done.
|
||
|
|
p.cb = nil
|
||
|
|
p.err = nil
|
||
|
|
p.line = nil
|
||
|
|
p.text = ""
|
||
|
|
p.cursor = cursor{}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) Cursor() int {
|
||
|
|
return p.cursor.glyph
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) Escape() {
|
||
|
|
p.cb = nil
|
||
|
|
p.err = nil
|
||
|
|
p.line = nil
|
||
|
|
p.text = ""
|
||
|
|
p.cursor = cursor{}
|
||
|
|
}
|
||
|
|
|
||
|
|
// IsPrompt reports whether an interactive prompt is active (as opposed to a
|
||
|
|
// passive status message).
|
||
|
|
func (p *Prompt) IsPrompt() bool {
|
||
|
|
return p.line != nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) Prompt(s string, cb CB) {
|
||
|
|
p.text = s
|
||
|
|
p.line = &line.Line{}
|
||
|
|
p.cb = cb
|
||
|
|
|
||
|
|
p.log.Debug("new prompt", slog.Any("prompt", p))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) SetText(s string) {
|
||
|
|
p.text = s
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) Text() string {
|
||
|
|
if p.err != nil {
|
||
|
|
return fmt.Sprintf("[%v] %s", p.err, p.text)
|
||
|
|
}
|
||
|
|
|
||
|
|
return p.text
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) Line(offset, width int) string {
|
||
|
|
if p.line == nil {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
return p.line.Render(offset, width)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) CursorLeft() {
|
||
|
|
if p.cursor.index > 0 {
|
||
|
|
var (
|
||
|
|
r rune
|
||
|
|
size int
|
||
|
|
)
|
||
|
|
// Reverse until we hit the start of a rune.
|
||
|
|
for i := p.cursor.index - 1; i >= 0; i-- {
|
||
|
|
r, size = p.line.DecodeRune(i)
|
||
|
|
if r != utf8.RuneError {
|
||
|
|
p.cursor.index -= size
|
||
|
|
p.cursor.glyph -= glyph.Width(r)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) CursorRight() {
|
||
|
|
if p.cursor.index < p.line.Length() {
|
||
|
|
r, size := p.line.DecodeRune(p.cursor.index)
|
||
|
|
if r == utf8.RuneError {
|
||
|
|
p.cursor.index++
|
||
|
|
p.cursor.glyph += glyph.Width(r)
|
||
|
|
} else {
|
||
|
|
p.cursor.index += size
|
||
|
|
p.cursor.glyph += glyph.Width(r)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) CursorHome() {
|
||
|
|
p.cursor.glyph = 0
|
||
|
|
p.cursor.index = 0
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) CursorEnd() {
|
||
|
|
p.cursor.index = p.line.Length()
|
||
|
|
p.cursor.glyph = p.line.Width()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) Backspace() {
|
||
|
|
if p.cursor.index > 0 {
|
||
|
|
p.CursorLeft()
|
||
|
|
p.line.DeleteRune(p.cursor.index)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Prompt) Insert(r rune) {
|
||
|
|
switch {
|
||
|
|
// Ignore all other non-printable characters.
|
||
|
|
case !unicode.IsPrint(r):
|
||
|
|
return
|
||
|
|
|
||
|
|
default:
|
||
|
|
p.line.Insert(p.cursor.index, r)
|
||
|
|
p.cursor.index += utf8.RuneLen(r)
|
||
|
|
p.cursor.glyph += glyph.Width(r)
|
||
|
|
}
|
||
|
|
}
|