Fix pickers; Add new select option
This commit is contained in:
@ -126,8 +126,8 @@ func NewKeymap() *Keymap {
|
|||||||
),
|
),
|
||||||
|
|
||||||
Select: key.NewBinding(
|
Select: key.NewBinding(
|
||||||
key.WithKeys("enter"),
|
key.WithKeys(" "),
|
||||||
key.WithHelp("enter", "Select"),
|
key.WithHelp("space", "Select"),
|
||||||
),
|
),
|
||||||
|
|
||||||
Insert: key.NewBinding(
|
Insert: key.NewBinding(
|
||||||
|
|||||||
@ -108,10 +108,6 @@ func (m *MultiSelect) Description(description string) *MultiSelect {
|
|||||||
|
|
||||||
// Options sets the options of the multi-select field.
|
// Options sets the options of the multi-select field.
|
||||||
func (m *MultiSelect) Options(hasNewOption bool, options ...Option[string]) *MultiSelect {
|
func (m *MultiSelect) Options(hasNewOption bool, options ...Option[string]) *MultiSelect {
|
||||||
if len(options) <= 0 {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
m.hasNewOption = hasNewOption
|
m.hasNewOption = hasNewOption
|
||||||
|
|
||||||
if m.hasNewOption {
|
if m.hasNewOption {
|
||||||
@ -121,6 +117,10 @@ func (m *MultiSelect) Options(hasNewOption bool, options ...Option[string]) *Mul
|
|||||||
options = append(newOption, options...)
|
options = append(newOption, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(options) <= 0 {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
for i, o := range options {
|
for i, o := range options {
|
||||||
for _, v := range *m.value {
|
for _, v := range *m.value {
|
||||||
if o.Value == v {
|
if o.Value == v {
|
||||||
@ -326,6 +326,7 @@ func (m *MultiSelect) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
selected := m.options[i].selected
|
selected := m.options[i].selected
|
||||||
m.options[i].selected = !selected
|
m.options[i].selected = !selected
|
||||||
m.filteredOptions[m.cursor].selected = !selected
|
m.filteredOptions[m.cursor].selected = !selected
|
||||||
|
m.finalize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
618
components/input/select.go
Normal file
618
components/input/select.go
Normal file
@ -0,0 +1,618 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"tasksquire/common"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/huh/accessibility"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Select is a form select field.
|
||||||
|
type Select struct {
|
||||||
|
common *common.Common
|
||||||
|
|
||||||
|
value *string
|
||||||
|
key string
|
||||||
|
viewport viewport.Model
|
||||||
|
|
||||||
|
// customization
|
||||||
|
title string
|
||||||
|
description string
|
||||||
|
options []Option[string]
|
||||||
|
filteredOptions []Option[string]
|
||||||
|
height int
|
||||||
|
|
||||||
|
// error handling
|
||||||
|
validate func(string) error
|
||||||
|
err error
|
||||||
|
|
||||||
|
// state
|
||||||
|
selected int
|
||||||
|
focused bool
|
||||||
|
filtering bool
|
||||||
|
filter textinput.Model
|
||||||
|
|
||||||
|
// options
|
||||||
|
inline bool
|
||||||
|
width int
|
||||||
|
accessible bool
|
||||||
|
theme *huh.Theme
|
||||||
|
keymap huh.SelectKeyMap
|
||||||
|
|
||||||
|
// new
|
||||||
|
hasNewOption bool
|
||||||
|
newInput textinput.Model
|
||||||
|
newInputActive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSelect returns a new select field.
|
||||||
|
func NewSelect(com *common.Common) *Select {
|
||||||
|
filter := textinput.New()
|
||||||
|
filter.Prompt = "/"
|
||||||
|
|
||||||
|
newInput := textinput.New()
|
||||||
|
newInput.Prompt = "New: "
|
||||||
|
|
||||||
|
return &Select{
|
||||||
|
common: com,
|
||||||
|
options: []Option[string]{},
|
||||||
|
value: new(string),
|
||||||
|
validate: func(string) error { return nil },
|
||||||
|
filtering: false,
|
||||||
|
filter: filter,
|
||||||
|
newInput: newInput,
|
||||||
|
newInputActive: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value sets the value of the select field.
|
||||||
|
func (s *Select) Value(value *string) *Select {
|
||||||
|
s.value = value
|
||||||
|
s.selectValue(*value)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) selectValue(value string) {
|
||||||
|
for i, o := range s.options {
|
||||||
|
if o.Value == value {
|
||||||
|
s.selected = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key sets the key of the select field which can be used to retrieve the value
|
||||||
|
// after submission.
|
||||||
|
func (s *Select) Key(key string) *Select {
|
||||||
|
s.key = key
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title sets the title of the select field.
|
||||||
|
func (s *Select) Title(title string) *Select {
|
||||||
|
s.title = title
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description sets the description of the select field.
|
||||||
|
func (s *Select) Description(description string) *Select {
|
||||||
|
s.description = description
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options sets the options of the select field.
|
||||||
|
func (s *Select) Options(hasNewOption bool, options ...Option[string]) *Select {
|
||||||
|
s.hasNewOption = hasNewOption
|
||||||
|
|
||||||
|
if s.hasNewOption {
|
||||||
|
newOption := []Option[string]{
|
||||||
|
{Key: "(new)", Value: ""},
|
||||||
|
}
|
||||||
|
options = append(newOption, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(options) <= 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s.options = options
|
||||||
|
s.filteredOptions = options
|
||||||
|
|
||||||
|
// Set the cursor to the existing value or the last selected option.
|
||||||
|
for i, option := range options {
|
||||||
|
if option.Value == *s.value {
|
||||||
|
s.selected = i
|
||||||
|
break
|
||||||
|
} else if option.selected {
|
||||||
|
s.selected = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.updateViewportHeight()
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline sets whether the select input should be inline.
|
||||||
|
func (s *Select) Inline(v bool) *Select {
|
||||||
|
s.inline = v
|
||||||
|
if v {
|
||||||
|
s.Height(1)
|
||||||
|
}
|
||||||
|
s.keymap.Left.SetEnabled(v)
|
||||||
|
s.keymap.Right.SetEnabled(v)
|
||||||
|
s.keymap.Up.SetEnabled(!v)
|
||||||
|
s.keymap.Down.SetEnabled(!v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height sets the height of the select field. If the number of options
|
||||||
|
// exceeds the height, the select field will become scrollable.
|
||||||
|
func (s *Select) Height(height int) *Select {
|
||||||
|
s.height = height
|
||||||
|
s.updateViewportHeight()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate sets the validation function of the select field.
|
||||||
|
func (s *Select) Validate(validate func(string) error) *Select {
|
||||||
|
s.validate = validate
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the error of the select field.
|
||||||
|
func (s *Select) Error() error {
|
||||||
|
return s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip returns whether the select should be skipped or should be blocking.
|
||||||
|
func (*Select) Skip() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom returns whether the input should be zoomed.
|
||||||
|
func (*Select) Zoom() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus focuses the select field.
|
||||||
|
func (s *Select) Focus() tea.Cmd {
|
||||||
|
s.focused = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blur blurs the select field.
|
||||||
|
func (s *Select) Blur() tea.Cmd {
|
||||||
|
value := *s.value
|
||||||
|
if s.inline {
|
||||||
|
s.clearFilter()
|
||||||
|
s.selectValue(value)
|
||||||
|
}
|
||||||
|
s.focused = false
|
||||||
|
s.err = s.validate(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyBinds returns the help keybindings for the select field.
|
||||||
|
func (s *Select) KeyBinds() []key.Binding {
|
||||||
|
return []key.Binding{
|
||||||
|
s.keymap.Up,
|
||||||
|
s.keymap.Down,
|
||||||
|
s.keymap.Left,
|
||||||
|
s.keymap.Right,
|
||||||
|
s.keymap.Filter,
|
||||||
|
s.keymap.SetFilter,
|
||||||
|
s.keymap.ClearFilter,
|
||||||
|
s.keymap.Prev,
|
||||||
|
s.keymap.Next,
|
||||||
|
s.keymap.Submit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the select field.
|
||||||
|
func (s *Select) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the select field.
|
||||||
|
func (s *Select) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
s.updateViewportHeight()
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
if s.filtering {
|
||||||
|
s.filter, cmd = s.filter.Update(msg)
|
||||||
|
|
||||||
|
// Keep the selected item in view.
|
||||||
|
if s.selected < s.viewport.YOffset || s.selected >= s.viewport.YOffset+s.viewport.Height {
|
||||||
|
s.viewport.SetYOffset(s.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.newInputActive {
|
||||||
|
s.newInput, cmd = s.newInput.Update(msg)
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, s.common.Keymap.Ok):
|
||||||
|
if s.newInput.Value() != "" {
|
||||||
|
newOption := Option[string]{
|
||||||
|
Key: s.newInput.Value(),
|
||||||
|
Value: s.newInput.Value(),
|
||||||
|
selected: true,
|
||||||
|
}
|
||||||
|
s.options = append(s.options, newOption)
|
||||||
|
if s.filterFunc(newOption.Key) {
|
||||||
|
s.filteredOptions = append(s.filteredOptions, newOption)
|
||||||
|
}
|
||||||
|
s.selected = len(s.options) - 1
|
||||||
|
|
||||||
|
value := newOption.Value
|
||||||
|
s.setFiltering(false)
|
||||||
|
s.err = s.validate(value)
|
||||||
|
if s.err != nil {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
*s.value = value
|
||||||
|
}
|
||||||
|
s.newInputActive = false
|
||||||
|
s.newInput.SetValue("")
|
||||||
|
s.newInput.Blur()
|
||||||
|
return s, nil
|
||||||
|
case key.Matches(msg, s.common.Keymap.Back):
|
||||||
|
s.newInputActive = false
|
||||||
|
s.newInput.Blur()
|
||||||
|
return s, SuppressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
s.err = nil
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, s.keymap.Filter):
|
||||||
|
s.setFiltering(true)
|
||||||
|
return s, s.filter.Focus()
|
||||||
|
case key.Matches(msg, s.keymap.SetFilter):
|
||||||
|
if len(s.filteredOptions) <= 0 {
|
||||||
|
s.filter.SetValue("")
|
||||||
|
s.filteredOptions = s.options
|
||||||
|
}
|
||||||
|
s.setFiltering(false)
|
||||||
|
case key.Matches(msg, s.keymap.ClearFilter):
|
||||||
|
s.clearFilter()
|
||||||
|
case key.Matches(msg, s.keymap.Up, s.keymap.Left):
|
||||||
|
// When filtering we should ignore j/k keybindings
|
||||||
|
//
|
||||||
|
// XXX: Currently, the below check doesn't account for keymap
|
||||||
|
// changes. When making this fix it's worth considering ignoring
|
||||||
|
// whether to ignore all up/down keybindings as ignoring a-zA-Z0-9
|
||||||
|
// may not be enough when international keyboards are considered.
|
||||||
|
if s.filtering && (msg.String() == "k" || msg.String() == "h") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.selected = max(s.selected-1, 0)
|
||||||
|
if s.selected < s.viewport.YOffset {
|
||||||
|
s.viewport.SetYOffset(s.selected)
|
||||||
|
}
|
||||||
|
case key.Matches(msg, s.keymap.GotoTop):
|
||||||
|
if s.filtering {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.selected = 0
|
||||||
|
s.viewport.GotoTop()
|
||||||
|
case key.Matches(msg, s.keymap.GotoBottom):
|
||||||
|
if s.filtering {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.selected = len(s.filteredOptions) - 1
|
||||||
|
s.viewport.GotoBottom()
|
||||||
|
case key.Matches(msg, s.keymap.HalfPageUp):
|
||||||
|
s.selected = max(s.selected-s.viewport.Height/2, 0)
|
||||||
|
s.viewport.HalfViewUp()
|
||||||
|
case key.Matches(msg, s.keymap.HalfPageDown):
|
||||||
|
s.selected = min(s.selected+s.viewport.Height/2, len(s.filteredOptions)-1)
|
||||||
|
s.viewport.HalfViewDown()
|
||||||
|
case key.Matches(msg, s.keymap.Down, s.keymap.Right):
|
||||||
|
// When filtering we should ignore j/k keybindings
|
||||||
|
//
|
||||||
|
// XXX: See note in the previous case match.
|
||||||
|
if s.filtering && (msg.String() == "j" || msg.String() == "l") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.selected = min(s.selected+1, len(s.filteredOptions)-1)
|
||||||
|
if s.selected >= s.viewport.YOffset+s.viewport.Height {
|
||||||
|
s.viewport.LineDown(1)
|
||||||
|
}
|
||||||
|
case key.Matches(msg, s.keymap.Prev):
|
||||||
|
if s.selected >= len(s.filteredOptions) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
value := s.filteredOptions[s.selected].Value
|
||||||
|
s.err = s.validate(value)
|
||||||
|
if s.err != nil {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
*s.value = value
|
||||||
|
return s, huh.PrevField
|
||||||
|
case key.Matches(msg, s.common.Keymap.Select):
|
||||||
|
if s.hasNewOption && s.selected == 0 {
|
||||||
|
s.newInputActive = true
|
||||||
|
s.newInput.Focus()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
case key.Matches(msg, s.keymap.Next, s.keymap.Submit):
|
||||||
|
if s.selected >= len(s.filteredOptions) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
value := s.filteredOptions[s.selected].Value
|
||||||
|
s.setFiltering(false)
|
||||||
|
s.err = s.validate(value)
|
||||||
|
if s.err != nil {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
*s.value = value
|
||||||
|
return s, huh.NextField
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.filtering {
|
||||||
|
s.filteredOptions = s.options
|
||||||
|
if s.filter.Value() != "" {
|
||||||
|
s.filteredOptions = nil
|
||||||
|
for _, option := range s.options {
|
||||||
|
if s.filterFunc(option.Key) {
|
||||||
|
s.filteredOptions = append(s.filteredOptions, option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s.filteredOptions) > 0 {
|
||||||
|
s.selected = min(s.selected, len(s.filteredOptions)-1)
|
||||||
|
s.viewport.SetYOffset(clamp(s.selected, 0, len(s.filteredOptions)-s.viewport.Height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateViewportHeight updates the viewport size according to the Height setting
|
||||||
|
// on this select field.
|
||||||
|
func (s *Select) updateViewportHeight() {
|
||||||
|
// If no height is set size the viewport to the number of options.
|
||||||
|
if s.height <= 0 {
|
||||||
|
s.viewport.Height = len(s.options)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const minHeight = 1
|
||||||
|
s.viewport.Height = max(minHeight, s.height-
|
||||||
|
lipgloss.Height(s.titleView())-
|
||||||
|
lipgloss.Height(s.descriptionView()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) activeStyles() *huh.FieldStyles {
|
||||||
|
theme := s.theme
|
||||||
|
if theme == nil {
|
||||||
|
theme = huh.ThemeCharm()
|
||||||
|
}
|
||||||
|
if s.focused {
|
||||||
|
return &theme.Focused
|
||||||
|
}
|
||||||
|
return &theme.Blurred
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) titleView() string {
|
||||||
|
if s.title == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
styles = s.activeStyles()
|
||||||
|
sb = strings.Builder{}
|
||||||
|
)
|
||||||
|
if s.filtering {
|
||||||
|
sb.WriteString(styles.Title.Render(s.filter.View()))
|
||||||
|
} else if s.filter.Value() != "" && !s.inline {
|
||||||
|
sb.WriteString(styles.Title.Render(s.title) + styles.Description.Render("/"+s.filter.Value()))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(styles.Title.Render(s.title))
|
||||||
|
}
|
||||||
|
if s.err != nil {
|
||||||
|
sb.WriteString(styles.ErrorIndicator.String())
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) descriptionView() string {
|
||||||
|
return s.activeStyles().Description.Render(s.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) choicesView() string {
|
||||||
|
var (
|
||||||
|
styles = s.activeStyles()
|
||||||
|
c = styles.SelectSelector.String()
|
||||||
|
sb strings.Builder
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.inline {
|
||||||
|
sb.WriteString(styles.PrevIndicator.Faint(s.selected <= 0).String())
|
||||||
|
if len(s.filteredOptions) > 0 {
|
||||||
|
sb.WriteString(styles.SelectedOption.Render(s.filteredOptions[s.selected].Key))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(styles.TextInput.Placeholder.Render("No matches"))
|
||||||
|
}
|
||||||
|
sb.WriteString(styles.NextIndicator.Faint(s.selected == len(s.filteredOptions)-1).String())
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, option := range s.filteredOptions {
|
||||||
|
if s.newInputActive && i == 0 {
|
||||||
|
sb.WriteString(c)
|
||||||
|
sb.WriteString(s.newInput.View())
|
||||||
|
sb.WriteString("\n")
|
||||||
|
continue
|
||||||
|
} else if s.selected == i {
|
||||||
|
sb.WriteString(c + styles.SelectedOption.Render(option.Key))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(strings.Repeat(" ", lipgloss.Width(c)) + styles.Option.Render(option.Key))
|
||||||
|
}
|
||||||
|
if i < len(s.options)-1 {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(s.filteredOptions); i < len(s.options)-1; i++ {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// View renders the select field.
|
||||||
|
func (s *Select) View() string {
|
||||||
|
styles := s.activeStyles()
|
||||||
|
s.viewport.SetContent(s.choicesView())
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
if s.title != "" {
|
||||||
|
sb.WriteString(s.titleView())
|
||||||
|
if !s.inline {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.description != "" {
|
||||||
|
sb.WriteString(s.descriptionView())
|
||||||
|
if !s.inline {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(s.viewport.View())
|
||||||
|
return styles.Base.Render(sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearFilter clears the value of the filter.
|
||||||
|
func (s *Select) clearFilter() {
|
||||||
|
s.filter.SetValue("")
|
||||||
|
s.filteredOptions = s.options
|
||||||
|
s.setFiltering(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setFiltering sets the filter of the select field.
|
||||||
|
func (s *Select) setFiltering(filtering bool) {
|
||||||
|
if s.inline && filtering {
|
||||||
|
s.filter.Width = lipgloss.Width(s.titleView()) - 1 - 1
|
||||||
|
}
|
||||||
|
s.filtering = filtering
|
||||||
|
s.keymap.SetFilter.SetEnabled(filtering)
|
||||||
|
s.keymap.Filter.SetEnabled(!filtering)
|
||||||
|
s.keymap.ClearFilter.SetEnabled(!filtering && s.filter.Value() != "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterFunc returns true if the option matches the filter.
|
||||||
|
func (s *Select) filterFunc(option string) bool {
|
||||||
|
// XXX: remove diacritics or allow customization of filter function.
|
||||||
|
return strings.Contains(strings.ToLower(option), strings.ToLower(s.filter.Value()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs the select field.
|
||||||
|
func (s *Select) Run() error {
|
||||||
|
if s.accessible {
|
||||||
|
return s.runAccessible()
|
||||||
|
}
|
||||||
|
return huh.Run(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runAccessible runs an accessible select field.
|
||||||
|
func (s *Select) runAccessible() error {
|
||||||
|
var sb strings.Builder
|
||||||
|
styles := s.activeStyles()
|
||||||
|
|
||||||
|
sb.WriteString(styles.Title.Render(s.title) + "\n")
|
||||||
|
|
||||||
|
for i, option := range s.options {
|
||||||
|
sb.WriteString(fmt.Sprintf("%d. %s", i+1, option.Key))
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(sb.String())
|
||||||
|
|
||||||
|
for {
|
||||||
|
choice := accessibility.PromptInt("Choose: ", 1, len(s.options))
|
||||||
|
option := s.options[choice-1]
|
||||||
|
if err := s.validate(option.Value); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println(styles.SelectedOption.Render("Chose: " + option.Key + "\n"))
|
||||||
|
*s.value = option.Value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTheme sets the theme of the select field.
|
||||||
|
func (s *Select) WithTheme(theme *huh.Theme) huh.Field {
|
||||||
|
if s.theme != nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s.theme = theme
|
||||||
|
s.filter.Cursor.Style = s.theme.Focused.TextInput.Cursor
|
||||||
|
s.filter.PromptStyle = s.theme.Focused.TextInput.Prompt
|
||||||
|
s.updateViewportHeight()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeyMap sets the keymap on a select field.
|
||||||
|
func (s *Select) WithKeyMap(k *huh.KeyMap) huh.Field {
|
||||||
|
s.keymap = k.Select
|
||||||
|
s.keymap.Left.SetEnabled(s.inline)
|
||||||
|
s.keymap.Right.SetEnabled(s.inline)
|
||||||
|
s.keymap.Up.SetEnabled(!s.inline)
|
||||||
|
s.keymap.Down.SetEnabled(!s.inline)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAccessible sets the accessible mode of the select field.
|
||||||
|
func (s *Select) WithAccessible(accessible bool) huh.Field {
|
||||||
|
s.accessible = accessible
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithWidth sets the width of the select field.
|
||||||
|
func (s *Select) WithWidth(width int) huh.Field {
|
||||||
|
s.width = width
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeight sets the height of the select field.
|
||||||
|
func (s *Select) WithHeight(height int) huh.Field {
|
||||||
|
return s.Height(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPosition sets the position of the select field.
|
||||||
|
func (s *Select) WithPosition(p huh.FieldPosition) huh.Field {
|
||||||
|
if s.filtering {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s.keymap.Prev.SetEnabled(!p.IsFirst())
|
||||||
|
s.keymap.Next.SetEnabled(!p.IsLast())
|
||||||
|
s.keymap.Submit.SetEnabled(p.IsLast())
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKey returns the key of the field.
|
||||||
|
func (s *Select) GetKey() string {
|
||||||
|
return s.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the value of the field.
|
||||||
|
func (s *Select) GetValue() any {
|
||||||
|
return *s.value
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContextPickerPage struct {
|
type ContextPickerPage struct {
|
||||||
@ -40,18 +41,32 @@ func NewContextPickerPage(common *common.Common) *ContextPickerPage {
|
|||||||
Options(huh.NewOptions(options...)...).
|
Options(huh.NewOptions(options...)...).
|
||||||
Title("Contexts").
|
Title("Contexts").
|
||||||
Description("Choose a context").
|
Description("Choose a context").
|
||||||
Value(&selected),
|
Value(&selected).
|
||||||
|
WithTheme(common.Styles.Form),
|
||||||
),
|
),
|
||||||
).
|
).
|
||||||
WithShowHelp(false).
|
WithShowHelp(false).
|
||||||
WithShowErrors(true).
|
WithShowErrors(true)
|
||||||
WithTheme(p.common.Styles.Form)
|
|
||||||
|
p.SetSize(common.Width(), common.Height())
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ContextPickerPage) SetSize(width, height int) {
|
func (p *ContextPickerPage) SetSize(width, height int) {
|
||||||
p.common.SetSize(width, height)
|
p.common.SetSize(width, height)
|
||||||
|
|
||||||
|
if width >= 20 {
|
||||||
|
p.form = p.form.WithWidth(20)
|
||||||
|
} else {
|
||||||
|
p.form = p.form.WithWidth(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if height >= 30 {
|
||||||
|
p.form = p.form.WithHeight(30)
|
||||||
|
} else {
|
||||||
|
p.form = p.form.WithHeight(height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ContextPickerPage) Init() tea.Cmd {
|
func (p *ContextPickerPage) Init() tea.Cmd {
|
||||||
@ -96,7 +111,13 @@ func (p *ContextPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ContextPickerPage) View() string {
|
func (p *ContextPickerPage) View() string {
|
||||||
return p.common.Styles.Base.Render(p.form.View())
|
return lipgloss.Place(
|
||||||
|
p.common.Width(),
|
||||||
|
p.common.Height(),
|
||||||
|
lipgloss.Center,
|
||||||
|
lipgloss.Center,
|
||||||
|
p.common.Styles.Base.Render(p.form.View()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ContextPickerPage) updateContextCmd() tea.Msg {
|
func (p *ContextPickerPage) updateContextCmd() tea.Msg {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProjectPickerPage struct {
|
type ProjectPickerPage struct {
|
||||||
@ -37,17 +38,32 @@ func NewProjectPickerPage(common *common.Common, activeProject string) *ProjectP
|
|||||||
Options(huh.NewOptions(options...)...).
|
Options(huh.NewOptions(options...)...).
|
||||||
Title("Projects").
|
Title("Projects").
|
||||||
Description("Choose a project").
|
Description("Choose a project").
|
||||||
Value(&selected),
|
Value(&selected).
|
||||||
|
WithTheme(common.Styles.Form),
|
||||||
),
|
),
|
||||||
).
|
).
|
||||||
WithShowHelp(false).
|
WithShowHelp(false).
|
||||||
WithShowErrors(false)
|
WithShowErrors(false)
|
||||||
|
|
||||||
|
p.SetSize(common.Width(), common.Height())
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProjectPickerPage) SetSize(width, height int) {
|
func (p *ProjectPickerPage) SetSize(width, height int) {
|
||||||
p.common.SetSize(width, height)
|
p.common.SetSize(width, height)
|
||||||
|
|
||||||
|
if width >= 20 {
|
||||||
|
p.form = p.form.WithWidth(20)
|
||||||
|
} else {
|
||||||
|
p.form = p.form.WithWidth(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if height >= 30 {
|
||||||
|
p.form = p.form.WithHeight(30)
|
||||||
|
} else {
|
||||||
|
p.form = p.form.WithHeight(height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProjectPickerPage) Init() tea.Cmd {
|
func (p *ProjectPickerPage) Init() tea.Cmd {
|
||||||
@ -92,7 +108,13 @@ func (p *ProjectPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProjectPickerPage) View() string {
|
func (p *ProjectPickerPage) View() string {
|
||||||
return p.common.Styles.Base.Render(p.form.View())
|
return lipgloss.Place(
|
||||||
|
p.common.Width(),
|
||||||
|
p.common.Height(),
|
||||||
|
lipgloss.Center,
|
||||||
|
lipgloss.Center,
|
||||||
|
p.common.Styles.Base.Render(p.form.View()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProjectPickerPage) updateProjectCmd() tea.Msg {
|
func (p *ProjectPickerPage) updateProjectCmd() tea.Msg {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReportPickerPage struct {
|
type ReportPickerPage struct {
|
||||||
@ -38,17 +39,32 @@ func NewReportPickerPage(common *common.Common, activeReport *taskwarrior.Report
|
|||||||
Options(huh.NewOptions(options...)...).
|
Options(huh.NewOptions(options...)...).
|
||||||
Title("Reports").
|
Title("Reports").
|
||||||
Description("Choose a report").
|
Description("Choose a report").
|
||||||
Value(&selected),
|
Value(&selected).
|
||||||
|
WithTheme(common.Styles.Form),
|
||||||
),
|
),
|
||||||
).
|
).
|
||||||
WithShowHelp(false).
|
WithShowHelp(false).
|
||||||
WithShowErrors(false)
|
WithShowErrors(false)
|
||||||
|
|
||||||
|
p.SetSize(common.Width(), common.Height())
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReportPickerPage) SetSize(width, height int) {
|
func (p *ReportPickerPage) SetSize(width, height int) {
|
||||||
p.common.SetSize(width, height)
|
p.common.SetSize(width, height)
|
||||||
|
|
||||||
|
if width >= 20 {
|
||||||
|
p.form = p.form.WithWidth(20)
|
||||||
|
} else {
|
||||||
|
p.form = p.form.WithWidth(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if height >= 30 {
|
||||||
|
p.form = p.form.WithHeight(30)
|
||||||
|
} else {
|
||||||
|
p.form = p.form.WithHeight(height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReportPickerPage) Init() tea.Cmd {
|
func (p *ReportPickerPage) Init() tea.Cmd {
|
||||||
@ -93,7 +109,13 @@ func (p *ReportPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReportPickerPage) View() string {
|
func (p *ReportPickerPage) View() string {
|
||||||
return p.common.Styles.Base.Render(p.form.View())
|
return lipgloss.Place(
|
||||||
|
p.common.Width(),
|
||||||
|
p.common.Height(),
|
||||||
|
lipgloss.Center,
|
||||||
|
lipgloss.Center,
|
||||||
|
p.common.Styles.Base.Render(p.form.View()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReportPickerPage) updateReportCmd() tea.Msg {
|
func (p *ReportPickerPage) updateReportCmd() tea.Msg {
|
||||||
|
|||||||
@ -382,13 +382,13 @@ type taskEdit struct {
|
|||||||
fields []huh.Field
|
fields []huh.Field
|
||||||
cursor int
|
cursor int
|
||||||
|
|
||||||
newProjectName *string
|
// newProjectName *string
|
||||||
newAnnotation *string
|
newAnnotation *string
|
||||||
udaValues map[string]*string
|
udaValues map[string]*string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit {
|
func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit {
|
||||||
newProject := ""
|
// newProject := ""
|
||||||
projectOptions := append([]string{"(none)"}, com.TW.GetProjects()...)
|
projectOptions := append([]string{"(none)"}, com.TW.GetProjects()...)
|
||||||
if task.Project == "" {
|
if task.Project == "" {
|
||||||
task.Project = "(none)"
|
task.Project = "(none)"
|
||||||
@ -410,19 +410,19 @@ func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit {
|
|||||||
Prompt(": ").
|
Prompt(": ").
|
||||||
WithTheme(com.Styles.Form),
|
WithTheme(com.Styles.Form),
|
||||||
|
|
||||||
huh.NewSelect[string]().
|
input.NewSelect(com).
|
||||||
Options(huh.NewOptions(projectOptions...)...).
|
Options(true, input.NewOptions(projectOptions...)...).
|
||||||
Title("Project").
|
Title("Project").
|
||||||
Value(&task.Project).
|
Value(&task.Project).
|
||||||
WithKeyMap(defaultKeymap).
|
WithKeyMap(defaultKeymap).
|
||||||
WithTheme(com.Styles.Form),
|
WithTheme(com.Styles.Form),
|
||||||
|
|
||||||
huh.NewInput().
|
// huh.NewInput().
|
||||||
Title("New Project").
|
// Title("New Project").
|
||||||
Value(&newProject).
|
// Value(&newProject).
|
||||||
Inline(true).
|
// Inline(true).
|
||||||
Prompt(": ").
|
// Prompt(": ").
|
||||||
WithTheme(com.Styles.Form),
|
// WithTheme(com.Styles.Form),
|
||||||
}
|
}
|
||||||
|
|
||||||
udaValues := make(map[string]*string)
|
udaValues := make(map[string]*string)
|
||||||
@ -513,8 +513,8 @@ func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit {
|
|||||||
|
|
||||||
udaValues: udaValues,
|
udaValues: udaValues,
|
||||||
|
|
||||||
newProjectName: &newProject,
|
// newProjectName: &newProject,
|
||||||
newAnnotation: &newAnnotation,
|
newAnnotation: &newAnnotation,
|
||||||
}
|
}
|
||||||
|
|
||||||
t.fields[0].Focus()
|
t.fields[0].Focus()
|
||||||
@ -997,9 +997,9 @@ func (p *TaskEditorPage) updateTasksCmd() tea.Msg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if *(p.areas[0].(*taskEdit).newProjectName) != "" {
|
// if *(p.areas[0].(*taskEdit).newProjectName) != "" {
|
||||||
p.task.Project = *p.areas[0].(*taskEdit).newProjectName
|
// p.task.Project = *p.areas[0].(*taskEdit).newProjectName
|
||||||
}
|
// }
|
||||||
|
|
||||||
if *(p.areas[1].(*tagEdit).newTagsValue) != "" {
|
if *(p.areas[1].(*tagEdit).newTagsValue) != "" {
|
||||||
newTags := strings.Split(*p.areas[1].(*tagEdit).newTagsValue, " ")
|
newTags := strings.Split(*p.areas[1].(*tagEdit).newTagsValue, " ")
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user