From 96402e216b13665e6fcae2254a566f820cd0a644 Mon Sep 17 00:00:00 2001 From: ardnew Date: Tue, 16 Jan 2024 18:48:31 -0600 Subject: [PATCH] unexport Field, add type converter AsField --- field.go | 127 +++++++++++++++++++++++++++--------------------------- option.go | 2 +- walk.go | 43 ++++++++++++++++++ 3 files changed, 107 insertions(+), 65 deletions(-) diff --git a/field.go b/field.go index 9201783..ac609db 100644 --- a/field.go +++ b/field.go @@ -9,7 +9,7 @@ import ( "github.com/charmbracelet/huh" ) -type Field struct { +type field struct { *Model value *FilePath @@ -39,163 +39,162 @@ type Field struct { // KeyMap(keys *huh.KeyMap) huh.Field // Width(width int) huh.Field -func Value(value string) Option[*Field] { - return func(f *Field) *Field { return f.WithValue(value) } +// Value returns an Option that sets the value of a field. +func Value(value string) Option[*field] { + return func(f *field) *field { return f.WithValue(value) } } -func Key(key string) Option[*Field] { - return func(f *Field) *Field { return f.WithKey(key) } +// Key returns an Option that sets the key of a field. +func Key(key string) Option[*field] { + return func(f *field) *field { return f.WithKey(key) } } -func Heading(heading string) Option[*Field] { - return func(f *Field) *Field { return f.WithHeading(heading) } +// Heading returns an Option that sets the heading of a field. +func Heading(heading string) Option[*field] { + return func(f *field) *field { return f.WithHeading(heading) } } -func Caption(caption string) Option[*Field] { - return func(f *Field) *Field { return f.WithCaption(caption) } +// Caption returns an Option that sets the caption of a field. +func Caption(caption string) Option[*field] { + return func(f *field) *field { return f.WithCaption(caption) } } -func Validate(validate func(FilePath) error) Option[*Field] { - return func(f *Field) *Field { return f.WithValidate(validate) } +// Validate returns an Option that sets the validation function of a field. +func Validate(validate func(FilePath) error) Option[*field] { + return func(f *field) *field { return f.WithValidate(validate) } } -// Field returns a new Field of the receiver Model. -// -// Field implements both its epynomous interface and the Model interface used in -// the Bubble Tea framework (module packages "huh" & "bubbletea", respectively). -func (m *Model) Field(options ...Option[*Field]) *Field { - filter := textinput.New() - filter.Prompt = "/" - - return &Field{ - Model: m, - value: new(FilePath), - validate: func(FilePath) error { return nil }, - filter: filter, - } +// Prompt returns an Option that sets the prompt of a field. +func Prompt(prompt string) Option[*field] { + return func(f *field) *field { return f.WithPrompt(prompt) } } -// Init initializes the Field. -func (f *Field) Init() tea.Cmd { +// Init initializes the field. +func (f *field) Init() tea.Cmd { return f.Model.Init() } -func (f *Field) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (f *field) Update(msg tea.Msg) (tea.Model, tea.Cmd) { _, cmd := f.Model.Update(msg) return f, cmd } -func (f *Field) View() string { +func (f *field) View() string { return f.Model.View() } -// Blur blurs the Field. -func (f *Field) Blur() tea.Cmd { +// Blur blurs the field. +func (f *field) Blur() tea.Cmd { f.isFocused = false f.err = f.validate(*f.value) return nil } -// Focus focuses the Field. -func (f *Field) Focus() tea.Cmd { +// Focus focuses the field. +func (f *field) Focus() tea.Cmd { f.isFocused = true return nil } -// Error returns the error of the Field. -func (f *Field) Error() error { +// Error returns the error of the field. +func (f *field) Error() error { return f.err } -// Run runs the Field. -func (f *Field) Run() error { +// Run runs the field. +func (f *field) Run() error { if f.accessible { return f.runAccessible() } return newRunError(huh.Run(f)) } -// KeyBinds returns the keybindings for the Field. -func (f *Field) KeyBinds() []key.Binding { +// KeyBinds returns the keybindings for the field. +func (f *field) KeyBinds() []key.Binding { return []key.Binding{} // f.keys.bindings() } // With returns the receiver with the given options applied. -func (f *Field) With(options ...Option[*Field]) *Field { +func (f *field) With(options ...Option[*field]) *field { for _, option := range options { f = option(f) } return f } -// WithTheme sets the theme of the Field. -func (f *Field) WithTheme(theme *huh.Theme) huh.Field { +// WithTheme sets the theme of the field. +func (f *field) WithTheme(theme *huh.Theme) huh.Field { f.theme = theme f.filter.Cursor.Style = f.theme.Focused.TextInput.Cursor f.filter.PromptStyle = f.theme.Focused.TextInput.Prompt return f } -// WithAccessible sets the accessible mode of the Field. -func (f *Field) WithAccessible(accessible bool) huh.Field { +// WithAccessible sets the accessible mode of the field. +func (f *field) WithAccessible(accessible bool) huh.Field { f.accessible = accessible return f } -// WithKeyMap sets the keymap on a Field. -func (f *Field) WithKeyMap(keys *huh.KeyMap) huh.Field { +// WithKeyMap sets the keymap on a field. +func (f *field) WithKeyMap(keys *huh.KeyMap) huh.Field { // TBD return f } -// WithWidth sets the width of the Field. -func (f *Field) WithWidth(width int) huh.Field { +// WithWidth sets the width of the field. +func (f *field) WithWidth(width int) huh.Field { f.width = width return f } // GetKey returns the key of the field. -func (f *Field) GetKey() string { +func (f *field) GetKey() string { return f.key } // GetValue returns the value of the field. -func (f *Field) GetValue() any { +func (f *field) GetValue() any { return f.value.path() } -// Value sets the value of the Field. -func (f *Field) WithValue(value string) *Field { +// WithValue sets the value of the field. +func (f *field) WithValue(value string) *field { f.value = f.value.init(value) return f } -// Key sets the key of the Field which can be used to retrieve the value -// after submission. -func (f *Field) WithKey(key string) *Field { +// WithKey sets the key of the field. +func (f *field) WithKey(key string) *field { f.key = key return f } -// Heading sets the heading of the Field. -func (f *Field) WithHeading(heading string) *Field { +// WithHeading sets the heading of the field. +func (f *field) WithHeading(heading string) *field { f.heading = heading return f } -// Caption sets the caption of the Field. -func (f *Field) WithCaption(caption string) *Field { +// WithCaption sets the caption of the field. +func (f *field) WithCaption(caption string) *field { f.caption = caption return f } -// Validate sets the validation function of the Field. -func (f *Field) WithValidate(validate func(FilePath) error) *Field { +// WithValidate sets the validation function of the field. +func (f *field) WithValidate(validate func(FilePath) error) *field { f.validate = validate return f } -func (f *Field) runAccessible() error { +// WithPrompt sets the prompt of the field. +func (f *field) WithPrompt(prompt string) *field { + f.filter.Prompt = prompt + return f +} + +func (f *field) runAccessible() error { var sb strings.Builder sb.WriteString(f.theme.Focused.Title.Render(f.heading) + "\n") @@ -221,11 +220,11 @@ func (f *Field) runAccessible() error { return nil } -func (f *Field) setIsFiltered(isFiltered bool) { +func (f *field) setIsFiltered(isFiltered bool) { f.isFiltered = isFiltered } -func (f *Field) filterFunc(option string) bool { +func (f *field) filterFunc(option string) bool { // XXX: remove diacritics or allow customization of filter function. return strings.Contains( strings.ToLower(option), diff --git a/option.go b/option.go index 40a18f8..4ef0d0d 100644 --- a/option.go +++ b/option.go @@ -1,6 +1,6 @@ package walk // Optional is a type with which Option arguments and methods can be applied. -type Optional interface{ *Model | *Field } +type Optional interface{ *Model | *field } type Option[O Optional] func(O) O diff --git a/walk.go b/walk.go index ac6ab47..66bb81a 100644 --- a/walk.go +++ b/walk.go @@ -14,6 +14,7 @@ import ( "github.com/antonmedv/clipboard" "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/sahilm/fuzzy" @@ -34,6 +35,7 @@ type Model struct { path string // Current dir path we are looking at. files []fs.DirEntry // Files we are looking at. err error // Error while listing files. + field *field // Bubble Tea Huh form field. keys *KeyMap // Key bindings. st *Styles // Rendering attributes. cmdline []string // Command line to open files. @@ -75,12 +77,16 @@ type ( func New(options ...Option[*Model]) *Model { m := (&Model{positions: make(map[string]position)}).With(options...) + // Use the default key bindings if none provided. if m.keys == nil { m.keys = m.keys.Default() } + + // Use the default style if none provided. if m.st == nil { m.st = m.st.Default() } + return m } @@ -114,6 +120,13 @@ func Keys(keys *KeyMap) Option[*Model] { return func(m *Model) *Model { return m.WithKeys(keys) } } +// Field returns an Option that configures Model as a form field. +// +// This Option must be provided to initialize Model as a form field. +func Field(options ...Option[*field]) Option[*Model] { + return func(m *Model) *Model { return m.withField(options...) } +} + // Kill exits the program with the given exit status. func Kill(status int) { os.Exit(status) } @@ -508,6 +521,9 @@ func (m *Model) View() string { // Exit exits the program with the receiver's current exit status. func (m *Model) Exit() { Kill(m.status) } +// Field returns the receiver's field used in a form. +func (m *Model) Field() *field { return m.field } + // Value returns the path of the currently selected file. func (m *Model) Value() string { path, _ := m.filePath() @@ -560,6 +576,33 @@ func (m *Model) WithKeys(keys *KeyMap) *Model { return m } +// withField returns the receiver with the given options applied to its field. +// +// This method must be called to initialize walk as a form field, and it must +// be called when initializing a new Model by providing a field Option. +// +// The types Model and field both implement different interfaces from the +// Bubble Tea framework. field is a specialization of Model that allows walk to +// be used as a discrete field in a Bubble Tea "huh" form application: +// +// | Interface | `field` | `Model` | +// |------------:|:-------:|:-------:| +// | `tea.Model` | ✓ | ✓ | +// | `huh.Field` | ✓ | | +func (m *Model) withField(options ...Option[*field]) *Model { + m.field = (&field{ + Model: m, + value: new(FilePath), + validate: func(FilePath) error { return nil }, + filter: textinput.New(), + }).With(options...) + return m +} + +func (m *Model) AsField(options ...Option[*field]) *field { + return m.withField(options...).field +} + func (m *Model) moveUp() { m.r-- if m.r < 0 {