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) } }