mattermost-community-enterp.../vendor/github.com/isacikgoz/prompt/prompt.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

377 lines
7.9 KiB
Go

package prompt
import (
"context"
"os"
"os/signal"
"sync"
"syscall"
"time"
"unicode/utf8"
"github.com/fatih/color"
"github.com/isacikgoz/prompt/term"
)
type keyEvent struct {
ch rune
err error
}
// KeyBinding is used for mapping a key to a function
type KeyBinding struct {
Key rune
Display string
Handler func(interface{}) error
Desc string
}
type keyHandlerFunc func(rune) error
type selectionHandlerFunc func(interface{}) error
type itemRendererFunc func(interface{}, []int, bool) [][]term.Cell
type informationRendererFunc func(interface{}) [][]term.Cell
//OptionalFunc handles functional arguments of the prompt
type OptionalFunc func(*Prompt)
// Options is the common options for building a prompt
type Options struct {
LineSize int `default:"5"`
StartInSearch bool
DisableColor bool
VimKeys bool `default:"true"`
}
// State holds the changeable vars of the prompt
type State struct {
List List
SearchMode bool
SearchStr string
SearchLabel string
Cursor int
Scroll int
ListSize int
}
// Prompt is a interactive prompt for command-line
type Prompt struct {
list List
opts *Options
keyBindings []*KeyBinding
selectionHandler selectionHandlerFunc
itemRenderer itemRendererFunc
informationRenderer informationRendererFunc
exitMsg [][]term.Cell // to be set on runtime if required
inputMode bool
helpMode bool
itemsLabel string
input string
reader *term.RuneReader // initialized by prompt
writer *term.BufferedWriter // initialized by prompt
mx *sync.RWMutex
events chan keyEvent
quit chan struct{}
newItem chan struct{}
}
// Create returns a pointer to prompt that is ready to Run
func Create(label string, opts *Options, list List, fs ...OptionalFunc) *Prompt {
p := &Prompt{
opts: opts,
list: list,
itemsLabel: label,
itemRenderer: itemText,
reader: term.NewRuneReader(os.Stdin),
writer: term.NewBufferedWriter(os.Stdout),
mx: &sync.RWMutex{},
events: make(chan keyEvent, 20),
quit: make(chan struct{}, 1),
newItem: make(chan struct{}),
}
for _, f := range fs {
f(p)
}
return p
}
// WithSelectionHandler adds a selection handler to the prompt
func WithSelectionHandler(f selectionHandlerFunc) OptionalFunc {
return func(p *Prompt) {
p.selectionHandler = f
}
}
// WithItemRenderer to add your own implementation on rendering an Item
func WithItemRenderer(f itemRendererFunc) OptionalFunc {
return func(p *Prompt) {
p.itemRenderer = f
}
}
// WithInformation adds additional information below to the prompt
func WithInformation(f informationRendererFunc) OptionalFunc {
return func(p *Prompt) {
p.informationRenderer = f
}
}
// Run as name implies starts the prompt until it quits
func (p *Prompt) Run(ctx context.Context) error {
// disable echo and hide cursor
if err := term.Init(os.Stdin, os.Stdout); err != nil {
return err
}
defer term.Close()
if p.opts.DisableColor {
term.DisableColor()
}
if p.opts.StartInSearch {
p.inputMode = true
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// start input loop
go p.spawnEvents(ctx)
p.render() // start with an initial render
err := p.mainloop()
// reset cursor position and remove buffer
p.writer.Reset()
p.writer.ClearScreen()
if err != nil {
return err
}
for _, cells := range p.exitMsg {
p.writer.WriteCells(cells)
}
p.writer.Flush()
return nil
}
// Stop sends a quit signal to the main loop of the prompt
func (p *Prompt) Stop() {
p.quit <- struct{}{}
}
func (p *Prompt) spawnEvents(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-time.After(10 * time.Millisecond):
p.mx.Lock()
r, _, err := p.reader.ReadRune()
p.mx.Unlock()
p.events <- keyEvent{ch: r, err: err}
}
}
}
// this is the main loop for reading input channel
func (p *Prompt) mainloop() error {
sigwinch := make(chan os.Signal, 1)
defer close(sigwinch)
signal.Notify(sigwinch, syscall.SIGWINCH)
for {
select {
case <-p.quit:
return nil
case <-sigwinch:
p.render()
case <-p.list.Update():
p.render()
case ev := <-p.events:
if err := func() error {
p.mx.Lock()
defer p.mx.Unlock()
if err := ev.err; err != nil {
return err
}
switch r := ev.ch; r {
case rune(term.KeyCtrlC), rune(term.KeyCtrlD):
p.Stop()
return nil
case term.Enter, term.NewLine:
items, idx := p.list.Items()
if idx == NotFound {
break
}
if err := p.selectionHandler(items[idx]); err != nil {
return err
}
default:
if err := p.onKey(r); err != nil {
return err
}
}
p.render()
return nil
}(); err != nil {
return err
}
}
}
}
// render function draws screen's list to terminal
func (p *Prompt) render() {
defer func() {
p.writer.Flush()
}()
if p.helpMode {
for _, line := range genHelp(p.allControls()) {
p.writer.WriteCells(line)
}
return
}
items, idx := p.list.Items()
p.writer.WriteCells(renderSearch(p.itemsLabel, p.inputMode, p.input))
for i := range items {
output := p.itemRenderer(items[i], p.list.Matches(items[i]), (i == idx))
for _, l := range output {
p.writer.WriteCells(l)
}
}
p.writer.WriteCells(nil) // add an empty line
if idx != NotFound {
for _, line := range p.informationRenderer(items[idx]) {
p.writer.WriteCells(line)
}
} else {
p.writer.WriteCells(term.Cprint("Not found.", color.FgRed))
}
}
// AddKeyBinding adds a key-function map to prompt
func (p *Prompt) AddKeyBinding(b *KeyBinding) error {
p.keyBindings = append(p.keyBindings, b)
return nil
}
// default key handling function
func (p *Prompt) onKey(key rune) error {
if p.helpMode {
p.helpMode = false
return nil
}
switch key {
case term.ArrowUp:
p.list.Prev()
case term.ArrowDown:
p.list.Next()
case term.ArrowLeft:
p.list.PageDown()
case term.ArrowRight:
p.list.PageUp()
default:
if key == '/' {
p.inputMode = !p.inputMode
} else if p.inputMode {
switch key {
case term.Backspace, term.Backspace2:
if len(p.input) > 0 {
_, size := utf8.DecodeLastRuneInString(p.input)
p.input = p.input[0 : len(p.input)-size]
}
case rune(term.KeyCtrlU):
p.input = ""
default:
p.input += string(key)
}
p.list.Search(p.input)
} else if key == '?' {
p.helpMode = !p.helpMode
} else if p.opts.VimKeys && key == 'k' {
// refactor vim keys
p.list.Prev()
} else if p.opts.VimKeys && key == 'j' {
p.list.Next()
} else if p.opts.VimKeys && key == 'h' {
p.list.PageDown()
} else if p.opts.VimKeys && key == 'l' {
p.list.PageUp()
} else {
items, idx := p.list.Items()
if idx == NotFound {
return nil
}
for _, kb := range p.keyBindings {
if kb.Key == key {
return kb.Handler(items[idx])
}
}
}
}
return nil
}
func (p *Prompt) allControls() map[string]string {
controls := make(map[string]string)
controls["← ↓ ↑ → (h,j,k,l)"] = "navigation"
controls["/"] = "toggle search"
for _, kb := range p.keyBindings {
controls[kb.Display] = kb.Desc
}
return controls
}
// State return the current replace-able vars as a struct
func (p *Prompt) State() *State {
scroll := p.list.Start()
return &State{
List: p.list,
SearchMode: p.inputMode,
SearchStr: p.input,
SearchLabel: p.itemsLabel,
Cursor: p.list.Cursor(),
Scroll: scroll,
ListSize: p.list.Size(),
}
}
// SetState replaces the state of the prompt
func (p *Prompt) SetState(state *State) {
p.list = state.List
p.inputMode = state.SearchMode
p.input = state.SearchStr
p.itemsLabel = state.SearchLabel
p.list.SetCursor(state.Cursor)
p.list.SetStart(state.Scroll)
}
// ListSize returns the size of the items that is renderer each time
func (p *Prompt) ListSize() int {
return p.opts.LineSize
}
// SetExitMsg adds a rendered cell grid to be printed after prompt is finished
func (p *Prompt) SetExitMsg(grid [][]term.Cell) {
p.exitMsg = grid
}