wip: filepicker, help, list, table, textarea, textinput, viewport

This commit is contained in:
Carlos Alexandro Becker
2024-02-26 13:31:26 -03:00
parent 8f8d5fc7d9
commit e9bbc869c0
11 changed files with 76 additions and 62 deletions
+3 -9
View File
@@ -29,7 +29,7 @@ func nextID() int {
} }
// New returns a new filepicker model with default styling and key bindings. // New returns a new filepicker model with default styling and key bindings.
func New() Model { func New(ctx *tea.Context) Model {
return Model{ return Model{
id: nextID(), id: nextID(),
CurrentDirectory: ".", CurrentDirectory: ".",
@@ -49,7 +49,7 @@ func New() Model {
minStack: newStack(), minStack: newStack(),
maxStack: newStack(), maxStack: newStack(),
KeyMap: DefaultKeyMap(), KeyMap: DefaultKeyMap(),
Styles: DefaultStyles(), Styles: DefaultStyles(ctx.Renderer),
} }
} }
@@ -112,13 +112,7 @@ type Styles struct {
} }
// DefaultStyles defines the default styling for the file picker. // DefaultStyles defines the default styling for the file picker.
func DefaultStyles() Styles { func DefaultStyles(r *lipgloss.Renderer) Styles {
return DefaultStylesWithRenderer(lipgloss.DefaultRenderer())
}
// DefaultStylesWithRenderer defines the default styling for the file picker,
// with a given Lip Gloss renderer.
func DefaultStylesWithRenderer(r *lipgloss.Renderer) Styles {
return Styles{ return Styles{
DisabledCursor: r.NewStyle().Foreground(lipgloss.Color("247")), DisabledCursor: r.NewStyle().Foreground(lipgloss.Color("247")),
Cursor: r.NewStyle().Foreground(lipgloss.Color("212")), Cursor: r.NewStyle().Foreground(lipgloss.Color("212")),
+6 -6
View File
@@ -15,7 +15,6 @@ import (
// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be // Note that if a key is disabled (via key.Binding.SetEnabled) it will not be
// rendered in the help view, so in theory generated help should self-manage. // rendered in the help view, so in theory generated help should self-manage.
type KeyMap interface { type KeyMap interface {
// ShortHelp returns a slice of bindings to be displayed in the short // ShortHelp returns a slice of bindings to be displayed in the short
// version of the help. The help bubble will render help in the order in // version of the help. The help bubble will render help in the order in
// which the help items are returned here. // which the help items are returned here.
@@ -58,18 +57,19 @@ type Model struct {
} }
// New creates a new help view with some useful defaults. // New creates a new help view with some useful defaults.
func New() Model { func New(ctx *tea.Context) Model {
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ r := ctx.Renderer
keyStyle := r.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#909090", Light: "#909090",
Dark: "#626262", Dark: "#626262",
}) })
descStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ descStyle := r.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#B2B2B2", Light: "#B2B2B2",
Dark: "#4A4A4A", Dark: "#4A4A4A",
}) })
sepStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ sepStyle := r.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#DDDADA", Light: "#DDDADA",
Dark: "#3C3C3C", Dark: "#3C3C3C",
}) })
@@ -118,7 +118,7 @@ func (m Model) ShortHelpView(bindings []key.Binding) string {
var b strings.Builder var b strings.Builder
var totalWidth int var totalWidth int
var separator = m.Styles.ShortSeparator.Inline(true).Render(m.ShortSeparator) separator := m.Styles.ShortSeparator.Inline(true).Render(m.ShortSeparator)
for i, kb := range bindings { for i, kb := range bindings {
if !kb.Enabled() { if !kb.Enabled() {
+4 -4
View File
@@ -195,14 +195,14 @@ type Model struct {
} }
// New returns a new model with sensible defaults. // New returns a new model with sensible defaults.
func New(items []Item, delegate ItemDelegate, width, height int) Model { func New(ctx *tea.Context, items []Item, delegate ItemDelegate, width, height int) Model {
styles := DefaultStyles() styles := DefaultStyles(ctx.Renderer)
sp := spinner.New() sp := spinner.New()
sp.Spinner = spinner.Line sp.Spinner = spinner.Line
sp.Style = styles.Spinner sp.Style = styles.Spinner
filterInput := textinput.New() filterInput := textinput.New(ctx)
filterInput.Prompt = "Filter: " filterInput.Prompt = "Filter: "
filterInput.PromptStyle = styles.FilterPrompt filterInput.PromptStyle = styles.FilterPrompt
filterInput.Cursor.Style = styles.FilterCursor filterInput.Cursor.Style = styles.FilterCursor
@@ -236,7 +236,7 @@ func New(items []Item, delegate ItemDelegate, width, height int) Model {
items: items, items: items,
Paginator: p, Paginator: p,
spinner: sp, spinner: sp,
Help: help.New(), Help: help.New(ctx),
} }
m.updatePagination() m.updatePagination()
+7 -3
View File
@@ -7,6 +7,7 @@ import (
"testing" "testing"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
) )
type item string type item string
@@ -29,7 +30,8 @@ func (d itemDelegate) Render(w io.Writer, m Model, index int, listItem Item) {
} }
func TestStatusBarItemName(t *testing.T) { func TestStatusBarItemName(t *testing.T) {
list := New([]Item{item("foo"), item("bar")}, itemDelegate{}, 10, 10) ctx := &tea.Context{Renderer: lipgloss.DefaultRenderer()}
list := New(ctx, []Item{item("foo"), item("bar")}, itemDelegate{}, 10, 10)
expected := "2 items" expected := "2 items"
if !strings.Contains(list.statusView(), expected) { if !strings.Contains(list.statusView(), expected) {
t.Fatalf("Error: expected view to contain %s", expected) t.Fatalf("Error: expected view to contain %s", expected)
@@ -43,7 +45,8 @@ func TestStatusBarItemName(t *testing.T) {
} }
func TestStatusBarWithoutItems(t *testing.T) { func TestStatusBarWithoutItems(t *testing.T) {
list := New([]Item{}, itemDelegate{}, 10, 10) ctx := &tea.Context{Renderer: lipgloss.DefaultRenderer()}
list := New(ctx, []Item{}, itemDelegate{}, 10, 10)
expected := "No items" expected := "No items"
if !strings.Contains(list.statusView(), expected) { if !strings.Contains(list.statusView(), expected) {
@@ -52,7 +55,8 @@ func TestStatusBarWithoutItems(t *testing.T) {
} }
func TestCustomStatusBarItemName(t *testing.T) { func TestCustomStatusBarItemName(t *testing.T) {
list := New([]Item{item("foo"), item("bar")}, itemDelegate{}, 10, 10) ctx := &tea.Context{Renderer: lipgloss.DefaultRenderer()}
list := New(ctx, []Item{item("foo"), item("bar")}, itemDelegate{}, 10, 10)
list.SetStatusBarItemName("connection", "connections") list.SetStatusBarItemName("connection", "connections")
expected := "2 connections" expected := "2 connections"
+18 -18
View File
@@ -41,57 +41,57 @@ type Styles struct {
// DefaultStyles returns a set of default style definitions for this list // DefaultStyles returns a set of default style definitions for this list
// component. // component.
func DefaultStyles() (s Styles) { func DefaultStyles(r *lipgloss.Renderer) (s Styles) {
verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"} verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"} subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}
s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 1, 2) s.TitleBar = r.NewStyle().Padding(0, 0, 1, 2)
s.Title = lipgloss.NewStyle(). s.Title = r.NewStyle().
Background(lipgloss.Color("62")). Background(lipgloss.Color("62")).
Foreground(lipgloss.Color("230")). Foreground(lipgloss.Color("230")).
Padding(0, 1) Padding(0, 1)
s.Spinner = lipgloss.NewStyle(). s.Spinner = r.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#8E8E8E", Dark: "#747373"}) Foreground(lipgloss.AdaptiveColor{Light: "#8E8E8E", Dark: "#747373"})
s.FilterPrompt = lipgloss.NewStyle(). s.FilterPrompt = r.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}) Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"})
s.FilterCursor = lipgloss.NewStyle(). s.FilterCursor = r.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}) Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"})
s.DefaultFilterCharacterMatch = lipgloss.NewStyle().Underline(true) s.DefaultFilterCharacterMatch = r.NewStyle().Underline(true)
s.StatusBar = lipgloss.NewStyle(). s.StatusBar = r.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}). Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
Padding(0, 0, 1, 2) Padding(0, 0, 1, 2)
s.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor) s.StatusEmpty = r.NewStyle().Foreground(subduedColor)
s.StatusBarActiveFilter = lipgloss.NewStyle(). s.StatusBarActiveFilter = r.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}) Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
s.StatusBarFilterCount = lipgloss.NewStyle().Foreground(verySubduedColor) s.StatusBarFilterCount = r.NewStyle().Foreground(verySubduedColor)
s.NoItems = lipgloss.NewStyle(). s.NoItems = r.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"}) Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"})
s.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor) s.ArabicPagination = r.NewStyle().Foreground(subduedColor)
s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:gomnd s.PaginationStyle = r.NewStyle().PaddingLeft(2) //nolint:gomnd
s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) s.HelpStyle = r.NewStyle().Padding(1, 0, 0, 2)
s.ActivePaginationDot = lipgloss.NewStyle(). s.ActivePaginationDot = r.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}). Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}).
SetString(bullet) SetString(bullet)
s.InactivePaginationDot = lipgloss.NewStyle(). s.InactivePaginationDot = r.NewStyle().
Foreground(verySubduedColor). Foreground(verySubduedColor).
SetString(bullet) SetString(bullet)
s.DividerDot = lipgloss.NewStyle(). s.DividerDot = r.NewStyle().
Foreground(verySubduedColor). Foreground(verySubduedColor).
SetString(" " + bullet + " ") SetString(" " + bullet + " ")
+4 -4
View File
@@ -115,10 +115,10 @@ func (m *Model) SetStyles(s Styles) {
type Option func(*Model) type Option func(*Model)
// New creates a new model for the table widget. // New creates a new model for the table widget.
func New(opts ...Option) Model { func New(ctx *tea.Context, opts ...Option) Model {
m := Model{ m := Model{
cursor: 0, cursor: 0,
viewport: viewport.New(0, 20), viewport: viewport.New(ctx, 0, 20),
KeyMap: DefaultKeyMap(), KeyMap: DefaultKeyMap(),
styles: DefaultStyles(), styles: DefaultStyles(),
@@ -380,7 +380,7 @@ func (m *Model) FromValues(value, separator string) {
} }
func (m Model) headersView() string { func (m Model) headersView() string {
var s = make([]string, 0, len(m.cols)) s := make([]string, 0, len(m.cols))
for _, col := range m.cols { for _, col := range m.cols {
style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true)
renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…")) renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…"))
@@ -390,7 +390,7 @@ func (m Model) headersView() string {
} }
func (m *Model) renderRow(rowID int) string { func (m *Model) renderRow(rowID int) string {
var s = make([]string, 0, len(m.cols)) s := make([]string, 0, len(m.cols))
for i, value := range m.rows[rowID] { for i, value := range m.rows[rowID] {
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true) style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
renderedCell := m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…"))) renderedCell := m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…")))
+10 -3
View File
@@ -1,10 +1,16 @@
package table package table
import "testing" import (
"testing"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
func TestFromValues(t *testing.T) { func TestFromValues(t *testing.T) {
input := "foo1,bar1\nfoo2,bar2\nfoo3,bar3" input := "foo1,bar1\nfoo2,bar2\nfoo3,bar3"
table := New(WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}})) ctx := &tea.Context{Renderer: lipgloss.DefaultRenderer()}
table := New(ctx, WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}}))
table.FromValues(input, ",") table.FromValues(input, ",")
if len(table.rows) != 3 { if len(table.rows) != 3 {
@@ -22,8 +28,9 @@ func TestFromValues(t *testing.T) {
} }
func TestFromValuesWithTabSeparator(t *testing.T) { func TestFromValuesWithTabSeparator(t *testing.T) {
ctx := &tea.Context{Renderer: lipgloss.DefaultRenderer()}
input := "foo1.\tbar1\nfoo,bar,baz\tbar,2" input := "foo1.\tbar1\nfoo,bar,baz\tbar,2"
table := New(WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}})) table := New(ctx, WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}}))
table.FromValues(input, "\t") table.FromValues(input, "\t")
if len(table.rows) != 2 { if len(table.rows) != 2 {
+8 -7
View File
@@ -29,8 +29,10 @@ const (
) )
// Internal messages for clipboard operations. // Internal messages for clipboard operations.
type pasteMsg string type (
type pasteErrMsg struct{ error } pasteMsg string
pasteErrMsg struct{ error }
)
// KeyMap is the key bindings for different actions within the textarea. // KeyMap is the key bindings for different actions within the textarea.
type KeyMap struct { type KeyMap struct {
@@ -244,12 +246,12 @@ type Model struct {
} }
// New creates a new model with default settings. // New creates a new model with default settings.
func New(r *tea.Context) Model { func New(ctx *tea.Context) Model {
vp := viewport.New(0, 0) vp := viewport.New(ctx, 0, 0)
vp.KeyMap = viewport.KeyMap{} vp.KeyMap = viewport.KeyMap{}
cur := cursor.New() cur := cursor.New()
focusedStyle, blurredStyle := DefaultStyles(r.Renderer) focusedStyle, blurredStyle := DefaultStyles(ctx.Renderer)
m := Model{ m := Model{
CharLimit: defaultCharLimit, CharLimit: defaultCharLimit,
@@ -605,8 +607,7 @@ func (m *Model) transposeLeft() {
if m.col >= len(m.value[m.row]) { if m.col >= len(m.value[m.row]) {
m.SetCursor(m.col - 1) m.SetCursor(m.col - 1)
} }
m.value[m.row][m.col-1], m.value[m.row][m.col] = m.value[m.row][m.col-1], m.value[m.row][m.col] = m.value[m.row][m.col], m.value[m.row][m.col-1]
m.value[m.row][m.col], m.value[m.row][m.col-1]
if m.col < len(m.value[m.row]) { if m.col < len(m.value[m.row]) {
m.SetCursor(m.col + 1) m.SetCursor(m.col + 1)
} }
+4 -1
View File
@@ -5,6 +5,7 @@ import (
"testing" "testing"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
) )
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
@@ -429,7 +430,9 @@ func TestRendersEndOfLineBuffer(t *testing.T) {
} }
func newTextArea() Model { func newTextArea() Model {
textarea := New() textarea := New(&tea.Context{
Renderer: lipgloss.DefaultRenderer(),
})
textarea.Prompt = "> " textarea.Prompt = "> "
textarea.Placeholder = "Hello, World!" textarea.Placeholder = "Hello, World!"
+8 -5
View File
@@ -17,8 +17,10 @@ import (
) )
// Internal messages for clipboard operations. // Internal messages for clipboard operations.
type pasteMsg string type (
type pasteErrMsg struct{ error } pasteMsg string
pasteErrMsg struct{ error }
)
// EchoMode sets the input behavior of the text input field. // EchoMode sets the input behavior of the text input field.
type EchoMode int type EchoMode int
@@ -153,14 +155,15 @@ type Model struct {
} }
// New creates a new model with default settings. // New creates a new model with default settings.
func New() Model { func New(ctx *tea.Context) Model {
r := ctx.Renderer
return Model{ return Model{
Prompt: "> ", Prompt: "> ",
EchoCharacter: '*', EchoCharacter: '*',
CharLimit: 0, CharLimit: 0,
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), PlaceholderStyle: r.NewStyle().Foreground(lipgloss.Color("240")),
ShowSuggestions: false, ShowSuggestions: false,
CompletionStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), CompletionStyle: r.NewStyle().Foreground(lipgloss.Color("240")),
Cursor: cursor.New(), Cursor: cursor.New(),
KeyMap: DefaultKeyMap, KeyMap: DefaultKeyMap,
+4 -2
View File
@@ -11,9 +11,10 @@ import (
// New returns a new model with the given width and height as well as default // New returns a new model with the given width and height as well as default
// key mappings. // key mappings.
func New(width, height int) (m Model) { func New(ctx *tea.Context, width, height int) (m Model) {
m.Width = width m.Width = width
m.Height = height m.Height = height
m.renderer = ctx.Renderer
m.setInitialValues() m.setInitialValues()
return m return m
} }
@@ -54,6 +55,7 @@ type Model struct {
initialized bool initialized bool
lines []string lines []string
renderer *lipgloss.Renderer
} }
func (m *Model) setInitialValues() { func (m *Model) setInitialValues() {
@@ -372,7 +374,7 @@ func (m Model) View() string {
} }
contentWidth := w - m.Style.GetHorizontalFrameSize() contentWidth := w - m.Style.GetHorizontalFrameSize()
contentHeight := h - m.Style.GetVerticalFrameSize() contentHeight := h - m.Style.GetVerticalFrameSize()
contents := lipgloss.NewStyle(). contents := m.renderer.NewStyle().
Width(contentWidth). // pad to width. Width(contentWidth). // pad to width.
Height(contentHeight). // pad to height. Height(contentHeight). // pad to height.
MaxHeight(contentHeight). // truncate height if taller. MaxHeight(contentHeight). // truncate height if taller.