Files
wen/internal/editor/prompt/prompt.go
T

166 lines
3.0 KiB
Go
Raw Normal View History

2026-10-26 02:32:03 +00:00
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)
}
}