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>
377 lines
7.9 KiB
Go
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
|
|
}
|