v0.2.0 16 colors, default colors, /dev/tty, bracketed paste, themed form controls
This commit is contained in:
32 files changed
+1447
-424
No files matched your search
@@ -0,0 +1,116 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/lixenwraith/terminal"
|
||||
)
|
||||
|
||||
// Option is one entry of an option list
|
||||
type Option struct {
|
||||
Name string // what is chosen
|
||||
Hint string // a line about it
|
||||
}
|
||||
|
||||
// OptionListState filters options by the text typed into it, matching name
|
||||
// or hint regardless of case, and keeps a cursor on the matches
|
||||
type OptionListState struct {
|
||||
Options []Option
|
||||
Filter TextFieldState
|
||||
Cursor int // index into Matches
|
||||
Scroll int // first match shown
|
||||
}
|
||||
|
||||
// NewOptionListState creates a list showing every option
|
||||
func NewOptionListState(options []Option) *OptionListState {
|
||||
return &OptionListState{Options: options}
|
||||
}
|
||||
|
||||
// Matches returns the indices of the options the filter matches, in order,
|
||||
// keeping the cursor on one of them
|
||||
func (s *OptionListState) Matches() []int {
|
||||
filter := strings.ToLower(s.Filter.Value())
|
||||
var m []int
|
||||
for i, o := range s.Options {
|
||||
if strings.Contains(strings.ToLower(o.Name), filter) || strings.Contains(strings.ToLower(o.Hint), filter) {
|
||||
m = append(m, i)
|
||||
}
|
||||
}
|
||||
s.Cursor = max(0, min(s.Cursor, len(m)-1))
|
||||
return m
|
||||
}
|
||||
|
||||
// Chosen returns the index in Options of the option under the cursor, and
|
||||
// false when nothing matches
|
||||
func (s *OptionListState) Chosen() (int, bool) {
|
||||
m := s.Matches()
|
||||
if len(m) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return m[s.Cursor], true
|
||||
}
|
||||
|
||||
// HandleKey moves the cursor on Up and Down, or edits the filter, and
|
||||
// reports whether anything changed
|
||||
func (s *OptionListState) HandleKey(key terminal.Key, r rune, mod terminal.Modifier) bool {
|
||||
filter := s.Filter.Value()
|
||||
switch {
|
||||
case key == terminal.KeyUp:
|
||||
s.Cursor--
|
||||
case key == terminal.KeyDown:
|
||||
s.Cursor++
|
||||
case !s.Filter.HandleKey(key, r, mod):
|
||||
return false
|
||||
case s.Filter.Value() != filter:
|
||||
s.Cursor = 0
|
||||
}
|
||||
s.Matches() // keeps the cursor on a match
|
||||
return true
|
||||
}
|
||||
|
||||
// Paste adds pasted text to the filter
|
||||
func (s *OptionListState) Paste(text string) bool {
|
||||
if !s.Filter.Paste(text) {
|
||||
return false
|
||||
}
|
||||
s.Cursor = 0
|
||||
return true
|
||||
}
|
||||
|
||||
// OptionList draws the filter on the first row and the matches below it,
|
||||
// the cursor row marked with the pointer on the Selected background, each
|
||||
// hint muted in a column after the names
|
||||
func (r Region) OptionList(s *OptionListState, th Theme) {
|
||||
if r.W < 4 || r.H < 2 {
|
||||
return
|
||||
}
|
||||
r.TextStyled(0, 0, "/", th.Muted)
|
||||
r.Sub(2, 0, r.W-2, 1).TextInput(&s.Filter, "type to filter", true, th)
|
||||
|
||||
m := s.Matches()
|
||||
rows := r.H - 1
|
||||
if len(m) == 0 {
|
||||
r.TextStyled(2, 1, "no match", th.Muted)
|
||||
return
|
||||
}
|
||||
s.Scroll = max(0, min(s.Scroll, s.Cursor, len(m)-rows))
|
||||
s.Scroll = max(s.Scroll, s.Cursor-rows+1)
|
||||
|
||||
nameW := 0
|
||||
for _, i := range m {
|
||||
nameW = max(nameW, RuneLen(s.Options[i].Name))
|
||||
}
|
||||
nameW = min(nameW, r.W/2)
|
||||
for y := 0; y < rows && s.Scroll+y < len(m); y++ {
|
||||
o := s.Options[m[s.Scroll+y]]
|
||||
row := r.Sub(0, 1+y, r.W, 1)
|
||||
var bg Style
|
||||
if s.Scroll+y == s.Cursor {
|
||||
bg = th.Selected
|
||||
row.FillStyle(th.Text.On(bg))
|
||||
row.TextStyled(0, 0, string(th.Glyphs.Pointer), th.Accent.On(bg))
|
||||
}
|
||||
row.TextStyled(2, 0, Truncate(o.Name, nameW), th.Text.On(bg))
|
||||
row.TextStyled(2+nameW+2, 0, Truncate(o.Hint, r.W-nameW-4), th.Muted.On(bg))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user