Add new options in multiselect
This commit is contained in:
@ -19,6 +19,8 @@ type Keymap struct {
|
||||
Right key.Binding
|
||||
Next key.Binding
|
||||
Prev key.Binding
|
||||
NextPage key.Binding
|
||||
PrevPage key.Binding
|
||||
SetReport key.Binding
|
||||
SetContext key.Binding
|
||||
SetProject key.Binding
|
||||
@ -98,6 +100,16 @@ func NewKeymap() *Keymap {
|
||||
key.WithHelp("shift+tab", "Previous"),
|
||||
),
|
||||
|
||||
NextPage: key.NewBinding(
|
||||
key.WithKeys("]"),
|
||||
key.WithHelp("[", "Next page"),
|
||||
),
|
||||
|
||||
PrevPage: key.NewBinding(
|
||||
key.WithKeys("["),
|
||||
key.WithHelp("]", "Previous page"),
|
||||
),
|
||||
|
||||
SetReport: key.NewBinding(
|
||||
key.WithKeys("r"),
|
||||
key.WithHelp("r", "Set report"),
|
||||
|
||||
666
components/input/multiselect.go
Normal file
666
components/input/multiselect.go
Normal file
@ -0,0 +1,666 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// MultiSelect is a form multi-select field.
|
||||
type MultiSelect struct {
|
||||
common *common.Common
|
||||
|
||||
value *[]string
|
||||
key string
|
||||
|
||||
// customization
|
||||
title string
|
||||
description string
|
||||
options []Option[string]
|
||||
filterable bool
|
||||
filteredOptions []Option[string]
|
||||
limit int
|
||||
height int
|
||||
|
||||
// error handling
|
||||
validate func([]string) error
|
||||
err error
|
||||
|
||||
// state
|
||||
cursor int
|
||||
focused bool
|
||||
filtering bool
|
||||
filter textinput.Model
|
||||
viewport viewport.Model
|
||||
|
||||
// options
|
||||
width int
|
||||
accessible bool
|
||||
theme *huh.Theme
|
||||
keymap huh.MultiSelectKeyMap
|
||||
|
||||
// new
|
||||
hasNewOption bool
|
||||
newInput textinput.Model
|
||||
newInputActive bool
|
||||
}
|
||||
|
||||
// NewMultiSelect returns a new multi-select field.
|
||||
func NewMultiSelect(common *common.Common) *MultiSelect {
|
||||
filter := textinput.New()
|
||||
filter.Prompt = "/"
|
||||
|
||||
newInput := textinput.New()
|
||||
newInput.Prompt = "New: "
|
||||
|
||||
return &MultiSelect{
|
||||
common: common,
|
||||
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 multi-select field.
|
||||
func (m *MultiSelect) Value(value *[]string) *MultiSelect {
|
||||
m.value = value
|
||||
for i, o := range m.options {
|
||||
for _, v := range *value {
|
||||
if o.Value == v {
|
||||
m.options[i].selected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Key sets the key of the select field which can be used to retrieve the value
|
||||
// after submission.
|
||||
func (m *MultiSelect) Key(key string) *MultiSelect {
|
||||
m.key = key
|
||||
return m
|
||||
}
|
||||
|
||||
// Title sets the title of the multi-select field.
|
||||
func (m *MultiSelect) Title(title string) *MultiSelect {
|
||||
m.title = title
|
||||
return m
|
||||
}
|
||||
|
||||
// Description sets the description of the multi-select field.
|
||||
func (m *MultiSelect) Description(description string) *MultiSelect {
|
||||
m.description = description
|
||||
return m
|
||||
}
|
||||
|
||||
// Options sets the options of the multi-select field.
|
||||
func (m *MultiSelect) Options(hasNewOption bool, options ...Option[string]) *MultiSelect {
|
||||
if len(options) <= 0 {
|
||||
return m
|
||||
}
|
||||
|
||||
m.hasNewOption = hasNewOption
|
||||
|
||||
if m.hasNewOption {
|
||||
newOption := []Option[string]{
|
||||
{Key: "(new)", Value: ""},
|
||||
}
|
||||
options = append(newOption, options...)
|
||||
}
|
||||
|
||||
for i, o := range options {
|
||||
for _, v := range *m.value {
|
||||
if o.Value == v {
|
||||
options[i].selected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
m.options = options
|
||||
m.filteredOptions = options
|
||||
m.updateViewportHeight()
|
||||
return m
|
||||
}
|
||||
|
||||
// Filterable sets the multi-select field as filterable.
|
||||
func (m *MultiSelect) Filterable(filterable bool) *MultiSelect {
|
||||
m.filterable = filterable
|
||||
return m
|
||||
}
|
||||
|
||||
// Limit sets the limit of the multi-select field.
|
||||
func (m *MultiSelect) Limit(limit int) *MultiSelect {
|
||||
m.limit = limit
|
||||
return m
|
||||
}
|
||||
|
||||
// Height sets the height of the multi-select field.
|
||||
func (m *MultiSelect) Height(height int) *MultiSelect {
|
||||
// What we really want to do is set the height of the viewport, but we
|
||||
// need a theme applied before we can calcualate its height.
|
||||
m.height = height
|
||||
m.updateViewportHeight()
|
||||
return m
|
||||
}
|
||||
|
||||
// Validate sets the validation function of the multi-select field.
|
||||
func (m *MultiSelect) Validate(validate func([]string) error) *MultiSelect {
|
||||
m.validate = validate
|
||||
return m
|
||||
}
|
||||
|
||||
// Error returns the error of the multi-select field.
|
||||
func (m *MultiSelect) Error() error {
|
||||
return m.err
|
||||
}
|
||||
|
||||
// Skip returns whether the multiselect should be skipped or should be blocking.
|
||||
func (*MultiSelect) Skip() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Zoom returns whether the multiselect should be zoomed.
|
||||
func (*MultiSelect) Zoom() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Focus focuses the multi-select field.
|
||||
func (m *MultiSelect) Focus() tea.Cmd {
|
||||
m.focused = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Blur blurs the multi-select field.
|
||||
func (m *MultiSelect) Blur() tea.Cmd {
|
||||
m.focused = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyBinds returns the help message for the multi-select field.
|
||||
func (m *MultiSelect) KeyBinds() []key.Binding {
|
||||
return []key.Binding{
|
||||
m.keymap.Toggle,
|
||||
m.keymap.Up,
|
||||
m.keymap.Down,
|
||||
m.keymap.Filter,
|
||||
m.keymap.SetFilter,
|
||||
m.keymap.ClearFilter,
|
||||
m.keymap.Prev,
|
||||
m.keymap.Submit,
|
||||
m.keymap.Next,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the multi-select field.
|
||||
func (m *MultiSelect) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates the multi-select field.
|
||||
func (m *MultiSelect) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Enforce height on the viewport during update as we need themes to
|
||||
// be applied before we can calculate the height.
|
||||
m.updateViewportHeight()
|
||||
|
||||
var cmd tea.Cmd
|
||||
if m.filtering {
|
||||
m.filter, cmd = m.filter.Update(msg)
|
||||
}
|
||||
|
||||
if m.newInputActive {
|
||||
m.newInput, cmd = m.newInput.Update(msg)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.common.Keymap.Ok):
|
||||
newOptions := []Option[string]{}
|
||||
for _, item := range strings.Split(m.newInput.Value(), " ") {
|
||||
newOptions = append(newOptions, Option[string]{
|
||||
Key: item,
|
||||
Value: item,
|
||||
selected: true,
|
||||
})
|
||||
}
|
||||
m.options = append(m.options, newOptions...)
|
||||
filteredNewOptions := []Option[string]{}
|
||||
for _, item := range newOptions {
|
||||
if m.filterFunc(item.Key) {
|
||||
filteredNewOptions = append(filteredNewOptions, item)
|
||||
}
|
||||
}
|
||||
m.filteredOptions = append(m.filteredOptions, filteredNewOptions...)
|
||||
m.newInputActive = false
|
||||
m.newInput.SetValue("")
|
||||
m.newInput.Blur()
|
||||
case key.Matches(msg, m.common.Keymap.Back):
|
||||
m.newInputActive = false
|
||||
m.newInput.Blur()
|
||||
return m, SuppressBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
|
||||
m.err = nil
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, m.keymap.Filter):
|
||||
m.setFilter(true)
|
||||
return m, m.filter.Focus()
|
||||
case key.Matches(msg, m.keymap.SetFilter) && m.filtering:
|
||||
if len(m.filteredOptions) <= 0 {
|
||||
m.filter.SetValue("")
|
||||
m.filteredOptions = m.options
|
||||
}
|
||||
m.setFilter(false)
|
||||
case key.Matches(msg, m.common.Keymap.Back) && m.filtering:
|
||||
m.filter.SetValue("")
|
||||
m.filteredOptions = m.options
|
||||
m.setFilter(false)
|
||||
case key.Matches(msg, m.keymap.ClearFilter):
|
||||
m.filter.SetValue("")
|
||||
m.filteredOptions = m.options
|
||||
m.setFilter(false)
|
||||
case key.Matches(msg, m.keymap.Up):
|
||||
if m.filtering && msg.String() == "k" {
|
||||
break
|
||||
}
|
||||
|
||||
m.cursor = max(m.cursor-1, 0)
|
||||
if m.cursor < m.viewport.YOffset {
|
||||
m.viewport.SetYOffset(m.cursor)
|
||||
}
|
||||
case key.Matches(msg, m.keymap.Down):
|
||||
if m.filtering && msg.String() == "j" {
|
||||
break
|
||||
}
|
||||
|
||||
m.cursor = min(m.cursor+1, len(m.filteredOptions)-1)
|
||||
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
|
||||
m.viewport.LineDown(1)
|
||||
}
|
||||
case key.Matches(msg, m.keymap.GotoTop):
|
||||
if m.filtering {
|
||||
break
|
||||
}
|
||||
m.cursor = 0
|
||||
m.viewport.GotoTop()
|
||||
case key.Matches(msg, m.keymap.GotoBottom):
|
||||
if m.filtering {
|
||||
break
|
||||
}
|
||||
m.cursor = len(m.filteredOptions) - 1
|
||||
m.viewport.GotoBottom()
|
||||
case key.Matches(msg, m.keymap.HalfPageUp):
|
||||
m.cursor = max(m.cursor-m.viewport.Height/2, 0)
|
||||
m.viewport.HalfViewUp()
|
||||
case key.Matches(msg, m.keymap.HalfPageDown):
|
||||
m.cursor = min(m.cursor+m.viewport.Height/2, len(m.filteredOptions)-1)
|
||||
m.viewport.HalfViewDown()
|
||||
case key.Matches(msg, m.keymap.Toggle) && !m.filtering:
|
||||
if m.hasNewOption && m.cursor == 0 {
|
||||
m.newInputActive = true
|
||||
m.newInput.Focus()
|
||||
} else {
|
||||
for i, option := range m.options {
|
||||
if option.Key == m.filteredOptions[m.cursor].Key {
|
||||
if !m.options[m.cursor].selected && m.limit > 0 && m.numSelected() >= m.limit {
|
||||
break
|
||||
}
|
||||
selected := m.options[i].selected
|
||||
m.options[i].selected = !selected
|
||||
m.filteredOptions[m.cursor].selected = !selected
|
||||
}
|
||||
}
|
||||
}
|
||||
case key.Matches(msg, m.keymap.Prev):
|
||||
m.finalize()
|
||||
if m.err != nil {
|
||||
return m, nil
|
||||
}
|
||||
return m, huh.PrevField
|
||||
case key.Matches(msg, m.keymap.Next, m.keymap.Submit):
|
||||
m.finalize()
|
||||
if m.err != nil {
|
||||
return m, nil
|
||||
}
|
||||
return m, huh.NextField
|
||||
}
|
||||
|
||||
if m.filtering {
|
||||
m.filteredOptions = m.options
|
||||
if m.filter.Value() != "" {
|
||||
m.filteredOptions = nil
|
||||
for _, option := range m.options {
|
||||
if m.filterFunc(option.Key) {
|
||||
m.filteredOptions = append(m.filteredOptions, option)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(m.filteredOptions) > 0 {
|
||||
m.cursor = min(m.cursor, len(m.filteredOptions)-1)
|
||||
m.viewport.SetYOffset(clamp(m.cursor, 0, len(m.filteredOptions)-m.viewport.Height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// updateViewportHeight updates the viewport size according to the Height setting
|
||||
// on this multi-select field.
|
||||
func (m *MultiSelect) updateViewportHeight() {
|
||||
// If no height is set size the viewport to the number of options.
|
||||
if m.height <= 0 {
|
||||
m.viewport.Height = len(m.options)
|
||||
return
|
||||
}
|
||||
|
||||
const minHeight = 1
|
||||
m.viewport.Height = max(minHeight, m.height-
|
||||
lipgloss.Height(m.titleView())-
|
||||
lipgloss.Height(m.descriptionView()))
|
||||
}
|
||||
|
||||
func (m *MultiSelect) numSelected() int {
|
||||
var count int
|
||||
for _, o := range m.options {
|
||||
if o.selected {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (m *MultiSelect) finalize() {
|
||||
*m.value = make([]string, 0)
|
||||
for _, option := range m.options {
|
||||
if option.selected {
|
||||
*m.value = append(*m.value, option.Value)
|
||||
}
|
||||
}
|
||||
m.err = m.validate(*m.value)
|
||||
}
|
||||
|
||||
func (m *MultiSelect) activeStyles() *huh.FieldStyles {
|
||||
theme := m.theme
|
||||
if theme == nil {
|
||||
theme = huh.ThemeCharm()
|
||||
}
|
||||
if m.focused {
|
||||
return &theme.Focused
|
||||
}
|
||||
return &theme.Blurred
|
||||
}
|
||||
|
||||
func (m *MultiSelect) titleView() string {
|
||||
if m.title == "" {
|
||||
return ""
|
||||
}
|
||||
var (
|
||||
styles = m.activeStyles()
|
||||
sb = strings.Builder{}
|
||||
)
|
||||
if m.filtering {
|
||||
sb.WriteString(m.filter.View())
|
||||
} else if m.filter.Value() != "" {
|
||||
sb.WriteString(styles.Title.Render(m.title) + styles.Description.Render("/"+m.filter.Value()))
|
||||
} else {
|
||||
sb.WriteString(styles.Title.Render(m.title))
|
||||
}
|
||||
if m.err != nil {
|
||||
sb.WriteString(styles.ErrorIndicator.String())
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (m *MultiSelect) descriptionView() string {
|
||||
return m.activeStyles().Description.Render(m.description)
|
||||
}
|
||||
|
||||
func (m *MultiSelect) choicesView() string {
|
||||
var (
|
||||
styles = m.activeStyles()
|
||||
c = styles.MultiSelectSelector.String()
|
||||
sb strings.Builder
|
||||
)
|
||||
for i, option := range m.filteredOptions {
|
||||
if m.newInputActive && i == 0 {
|
||||
sb.WriteString(c)
|
||||
sb.WriteString(m.newInput.View())
|
||||
sb.WriteString("\n")
|
||||
continue
|
||||
} else if m.cursor == i {
|
||||
sb.WriteString(c)
|
||||
} else {
|
||||
sb.WriteString(strings.Repeat(" ", lipgloss.Width(c)))
|
||||
}
|
||||
|
||||
if m.filteredOptions[i].selected {
|
||||
sb.WriteString(styles.SelectedPrefix.String())
|
||||
sb.WriteString(styles.SelectedOption.Render(option.Key))
|
||||
} else {
|
||||
sb.WriteString(styles.UnselectedPrefix.String())
|
||||
sb.WriteString(styles.UnselectedOption.Render(option.Key))
|
||||
}
|
||||
if i < len(m.options)-1 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
for i := len(m.filteredOptions); i < len(m.options)-1; i++ {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// View renders the multi-select field.
|
||||
func (m *MultiSelect) View() string {
|
||||
styles := m.activeStyles()
|
||||
m.viewport.SetContent(m.choicesView())
|
||||
|
||||
var sb strings.Builder
|
||||
if m.title != "" {
|
||||
sb.WriteString(m.titleView())
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
if m.description != "" {
|
||||
sb.WriteString(m.descriptionView() + "\n")
|
||||
}
|
||||
sb.WriteString(m.viewport.View())
|
||||
return styles.Base.Render(sb.String())
|
||||
}
|
||||
|
||||
func (m *MultiSelect) printOptions() {
|
||||
styles := m.activeStyles()
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(styles.Title.Render(m.title))
|
||||
sb.WriteString("\n")
|
||||
|
||||
for i, option := range m.options {
|
||||
if option.selected {
|
||||
sb.WriteString(styles.SelectedOption.Render(fmt.Sprintf("%d. %s %s", i+1, "✓", option.Key)))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%d. %s %s", i+1, " ", option.Key))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
fmt.Println(sb.String())
|
||||
}
|
||||
|
||||
// setFilter sets the filter of the select field.
|
||||
func (m *MultiSelect) setFilter(filter bool) {
|
||||
m.filtering = filter
|
||||
m.keymap.SetFilter.SetEnabled(filter)
|
||||
m.keymap.Filter.SetEnabled(!filter)
|
||||
m.keymap.Next.SetEnabled(!filter)
|
||||
m.keymap.Submit.SetEnabled(!filter)
|
||||
m.keymap.Prev.SetEnabled(!filter)
|
||||
m.keymap.ClearFilter.SetEnabled(!filter && m.filter.Value() != "")
|
||||
}
|
||||
|
||||
// filterFunc returns true if the option matches the filter.
|
||||
func (m *MultiSelect) filterFunc(option string) bool {
|
||||
// XXX: remove diacritics or allow customization of filter function.
|
||||
return strings.Contains(strings.ToLower(option), strings.ToLower(m.filter.Value()))
|
||||
}
|
||||
|
||||
// Run runs the multi-select field.
|
||||
func (m *MultiSelect) Run() error {
|
||||
if m.accessible {
|
||||
return m.runAccessible()
|
||||
}
|
||||
return huh.Run(m)
|
||||
}
|
||||
|
||||
// runAccessible() runs the multi-select field in accessible mode.
|
||||
func (m *MultiSelect) runAccessible() error {
|
||||
m.printOptions()
|
||||
styles := m.activeStyles()
|
||||
|
||||
var choice int
|
||||
for {
|
||||
fmt.Printf("Select up to %d options. 0 to continue.\n", m.limit)
|
||||
|
||||
choice = accessibility.PromptInt("Select: ", 0, len(m.options))
|
||||
if choice == 0 {
|
||||
m.finalize()
|
||||
err := m.validate(*m.value)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if !m.options[choice-1].selected && m.limit > 0 && m.numSelected() >= m.limit {
|
||||
fmt.Printf("You can't select more than %d options.\n", m.limit)
|
||||
continue
|
||||
}
|
||||
m.options[choice-1].selected = !m.options[choice-1].selected
|
||||
if m.options[choice-1].selected {
|
||||
fmt.Printf("Selected: %s\n\n", m.options[choice-1].Key)
|
||||
} else {
|
||||
fmt.Printf("Deselected: %s\n\n", m.options[choice-1].Key)
|
||||
}
|
||||
|
||||
m.printOptions()
|
||||
}
|
||||
|
||||
var values []string
|
||||
|
||||
for _, option := range m.options {
|
||||
if option.selected {
|
||||
*m.value = append(*m.value, option.Value)
|
||||
values = append(values, option.Key)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(styles.SelectedOption.Render("Selected:", strings.Join(values, ", ")+"\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithTheme sets the theme of the multi-select field.
|
||||
func (m *MultiSelect) WithTheme(theme *huh.Theme) huh.Field {
|
||||
if m.theme != nil {
|
||||
return m
|
||||
}
|
||||
m.theme = theme
|
||||
m.filter.Cursor.Style = m.theme.Focused.TextInput.Cursor
|
||||
m.filter.PromptStyle = m.theme.Focused.TextInput.Prompt
|
||||
m.updateViewportHeight()
|
||||
return m
|
||||
}
|
||||
|
||||
// WithKeyMap sets the keymap of the multi-select field.
|
||||
func (m *MultiSelect) WithKeyMap(k *huh.KeyMap) huh.Field {
|
||||
m.keymap = k.MultiSelect
|
||||
return m
|
||||
}
|
||||
|
||||
// WithAccessible sets the accessible mode of the multi-select field.
|
||||
func (m *MultiSelect) WithAccessible(accessible bool) huh.Field {
|
||||
m.accessible = accessible
|
||||
return m
|
||||
}
|
||||
|
||||
// WithWidth sets the width of the multi-select field.
|
||||
func (m *MultiSelect) WithWidth(width int) huh.Field {
|
||||
m.width = width
|
||||
return m
|
||||
}
|
||||
|
||||
// WithHeight sets the height of the multi-select field.
|
||||
func (m *MultiSelect) WithHeight(height int) huh.Field {
|
||||
m.height = height
|
||||
return m
|
||||
}
|
||||
|
||||
// WithPosition sets the position of the multi-select field.
|
||||
func (m *MultiSelect) WithPosition(p huh.FieldPosition) huh.Field {
|
||||
if m.filtering {
|
||||
return m
|
||||
}
|
||||
m.keymap.Prev.SetEnabled(!p.IsFirst())
|
||||
m.keymap.Next.SetEnabled(!p.IsLast())
|
||||
m.keymap.Submit.SetEnabled(p.IsLast())
|
||||
return m
|
||||
}
|
||||
|
||||
// GetKey returns the multi-select's key.
|
||||
func (m *MultiSelect) GetKey() string {
|
||||
return m.key
|
||||
}
|
||||
|
||||
// GetValue returns the multi-select's value.
|
||||
func (m *MultiSelect) GetValue() any {
|
||||
return *m.value
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func clamp(n, low, high int) int {
|
||||
if low > high {
|
||||
low, high = high, low
|
||||
}
|
||||
return min(high, max(low, n))
|
||||
}
|
||||
|
||||
type SuppressBackMsg struct{}
|
||||
|
||||
func SuppressBack() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return SuppressBackMsg{}
|
||||
}
|
||||
}
|
||||
38
components/input/option.go
Normal file
38
components/input/option.go
Normal file
@ -0,0 +1,38 @@
|
||||
package input
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Option is an option for select fields.
|
||||
type Option[T comparable] struct {
|
||||
Key string
|
||||
Value T
|
||||
selected bool
|
||||
}
|
||||
|
||||
// NewOptions returns new options from a list of values.
|
||||
func NewOptions[T comparable](values ...T) []Option[T] {
|
||||
options := make([]Option[T], len(values))
|
||||
for i, o := range values {
|
||||
options[i] = Option[T]{
|
||||
Key: fmt.Sprint(o),
|
||||
Value: o,
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// NewOption returns a new select option.
|
||||
func NewOption[T comparable](key string, value T) Option[T] {
|
||||
return Option[T]{Key: key, Value: value}
|
||||
}
|
||||
|
||||
// Selected sets whether the option is currently selected.
|
||||
func (o Option[T]) Selected(selected bool) Option[T] {
|
||||
o.selected = selected
|
||||
return o
|
||||
}
|
||||
|
||||
// String returns the key of the option.
|
||||
func (o Option[T]) String() string {
|
||||
return o.Key
|
||||
}
|
||||
33
go.mod
33
go.mod
@ -4,39 +4,36 @@ go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.26.1
|
||||
github.com/charmbracelet/glamour v0.7.0
|
||||
github.com/charmbracelet/huh v0.3.0
|
||||
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb
|
||||
github.com/charmbracelet/bubbletea v0.26.4
|
||||
github.com/charmbracelet/huh v0.4.2
|
||||
github.com/charmbracelet/lipgloss v0.11.0
|
||||
github.com/mattn/go-runewidth v0.0.15
|
||||
golang.org/x/term v0.20.0
|
||||
golang.org/x/term v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.2 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a // indirect
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240606154654-7c42867b53c7 // indirect
|
||||
github.com/charmbracelet/x/input v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.1.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
|
||||
github.com/yuin/goldmark v1.5.4 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.2 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
)
|
||||
|
||||
76
go.sum
76
go.sum
@ -1,37 +1,33 @@
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0=
|
||||
github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo=
|
||||
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
|
||||
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
|
||||
github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE=
|
||||
github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA=
|
||||
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb h1:Hs3xzxHuruNT2Iuo87iS40c0PhLqpnUKBI6Xw6Ad3wQ=
|
||||
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495 h1:+0U9qX8Pv8KiYgRxfBvORRjgBzLgHMjtElP4O0PyKYA=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495/go.mod h1:qeR6w1zITbkF7vEhcx0CqX5GfnIiQloJWQghN6HfP+c=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40=
|
||||
github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0=
|
||||
github.com/charmbracelet/huh v0.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM=
|
||||
github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0=
|
||||
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
|
||||
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
|
||||
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
|
||||
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a h1:lOpqe2UvPmlln41DGoii7wlSZ/q8qGIon5JJ8Biu46I=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240606154654-7c42867b53c7 h1:6Rw/MHf+Rm7wlUr+euFyaGw1fNKeZPKVh4gkFHUjFsY=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240606154654-7c42867b53c7/go.mod h1:UZyEz89i6+jVx2oW3lgmIsVDNr7qzaKuvPVoSdwqDR0=
|
||||
github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4=
|
||||
github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0=
|
||||
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
|
||||
github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
|
||||
github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
@ -40,12 +36,9 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
@ -54,28 +47,23 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"tasksquire/common"
|
||||
"time"
|
||||
|
||||
"tasksquire/components/input"
|
||||
"tasksquire/taskwarrior"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
@ -151,6 +152,10 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return nil, tea.Quit
|
||||
}
|
||||
return model, p.updateTasksCmd
|
||||
case key.Matches(msg, p.common.Keymap.PrevPage):
|
||||
return p, prevArea()
|
||||
case key.Matches(msg, p.common.Keymap.NextPage):
|
||||
return p, nextArea()
|
||||
case key.Matches(msg, p.common.Keymap.Left):
|
||||
return p, prevColumn()
|
||||
case key.Matches(msg, p.common.Keymap.Right):
|
||||
@ -192,6 +197,14 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, p.common.Keymap.Back):
|
||||
model, cmd := p.areas[p.area].Update(msg)
|
||||
p.areas[p.area] = model.(area)
|
||||
if cmd != nil {
|
||||
_, ok := cmd().(input.SuppressBackMsg)
|
||||
if ok {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return p, changeMode(modeNormal)
|
||||
case key.Matches(msg, p.common.Keymap.Prev):
|
||||
if p.columnCursor == 0 {
|
||||
@ -264,17 +277,33 @@ func (p *TaskEditorPage) View() string {
|
||||
|
||||
}
|
||||
|
||||
tabs := ""
|
||||
for i, a := range p.areas {
|
||||
if i == p.area {
|
||||
tabs += p.common.Styles.Base.Bold(true).Render(fmt.Sprintf(" %s ", a.GetName()))
|
||||
} else {
|
||||
tabs += p.common.Styles.Base.Render(fmt.Sprintf(" %s ", a.GetName()))
|
||||
}
|
||||
}
|
||||
|
||||
page := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
tabs,
|
||||
area,
|
||||
)
|
||||
|
||||
// return lipgloss.Place(p.common.Width(), p.common.Height(), lipgloss.Center, lipgloss.Center, lipgloss.JoinHorizontal(
|
||||
// lipgloss.Center,
|
||||
// picker,
|
||||
// area,
|
||||
// ))
|
||||
return lipgloss.Place(p.common.Width(), p.common.Height(), lipgloss.Center, lipgloss.Center, area)
|
||||
return lipgloss.Place(p.common.Width(), p.common.Height(), lipgloss.Center, lipgloss.Center, page)
|
||||
}
|
||||
|
||||
type area interface {
|
||||
tea.Model
|
||||
SetCursor(c int)
|
||||
GetName() string
|
||||
}
|
||||
|
||||
type areaPicker struct {
|
||||
@ -493,6 +522,10 @@ func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit {
|
||||
return &t
|
||||
}
|
||||
|
||||
func (t *taskEdit) GetName() string {
|
||||
return "Task"
|
||||
}
|
||||
|
||||
func (t *taskEdit) SetCursor(c int) {
|
||||
t.fields[t.cursor].Blur()
|
||||
if c < 0 {
|
||||
@ -565,8 +598,8 @@ func NewTagEdit(common *common.Common, selected *[]string, options []string) *ta
|
||||
t := tagEdit{
|
||||
common: common,
|
||||
fields: []huh.Field{
|
||||
huh.NewMultiSelect[string]().
|
||||
Options(huh.NewOptions(options...)...).
|
||||
input.NewMultiSelect(common).
|
||||
Options(true, input.NewOptions(options...)...).
|
||||
// Key("tags").
|
||||
Title("Tags").
|
||||
Value(selected).
|
||||
@ -586,6 +619,10 @@ func NewTagEdit(common *common.Common, selected *[]string, options []string) *ta
|
||||
return &t
|
||||
}
|
||||
|
||||
func (t *tagEdit) GetName() string {
|
||||
return "Tags"
|
||||
}
|
||||
|
||||
func (t *tagEdit) SetCursor(c int) {
|
||||
t.fields[t.cursor].Blur()
|
||||
if c < 0 {
|
||||
@ -683,6 +720,10 @@ func NewTimeEdit(common *common.Common, due *string, scheduled *string, wait *st
|
||||
return &t
|
||||
}
|
||||
|
||||
func (t *timeEdit) GetName() string {
|
||||
return "Dates"
|
||||
}
|
||||
|
||||
func (t *timeEdit) SetCursor(c int) {
|
||||
t.fields[t.cursor].Blur()
|
||||
if c < 0 {
|
||||
@ -772,6 +813,10 @@ func NewDetailsEdit(com *common.Common, task *taskwarrior.Task) *detailsEdit {
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *detailsEdit) GetName() string {
|
||||
return "Details"
|
||||
}
|
||||
|
||||
func (d *detailsEdit) SetCursor(c int) {
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user