diff --git a/cursor/cursor.go b/cursor/cursor.go index a4f4793..dd93e5c 100644 --- a/cursor/cursor.go +++ b/cursor/cursor.go @@ -75,21 +75,24 @@ type Model struct { } // New creates a new model with default settings. -func New() Model { +func New(ctx tea.Context) Model { return Model{ + Style: ctx.NewStyle(), + TextStyle: ctx.NewStyle(), + BlinkSpeed: defaultBlinkSpeed, Blink: true, mode: CursorBlink, blinkCtx: &blinkCtx{ - ctx: context.Background(), + ctx: ctx, }, } } // Update updates the cursor. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(ctx tea.Context, msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case initialBlinkMsg: // We accept all initialBlinkMsgs generated by the Blink command. @@ -203,7 +206,7 @@ func (m *Model) SetChar(char string) { } // View displays the cursor. -func (m Model) View() string { +func (m Model) View(tea.Context) string { if m.Blink { return m.TextStyle.Inline(true).Render(m.char) } diff --git a/filepicker/filepicker.go b/filepicker/filepicker.go index 9a1235a..12cc483 100644 --- a/filepicker/filepicker.go +++ b/filepicker/filepicker.go @@ -29,7 +29,7 @@ func nextID() int { } // New returns a new filepicker model with default styling and key bindings. -func New() Model { +func New(ctx tea.Context) Model { return Model{ id: nextID(), CurrentDirectory: ".", @@ -49,7 +49,7 @@ func New() Model { minStack: newStack(), maxStack: newStack(), KeyMap: DefaultKeyMap(), - Styles: DefaultStyles(), + Styles: DefaultStyles(ctx), } } @@ -112,25 +112,19 @@ type Styles struct { } // DefaultStyles defines the default styling for the file picker. -func DefaultStyles() 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 { +func DefaultStyles(ctx tea.Context) Styles { return Styles{ - DisabledCursor: r.NewStyle().Foreground(lipgloss.Color("247")), - Cursor: r.NewStyle().Foreground(lipgloss.Color("212")), - Symlink: r.NewStyle().Foreground(lipgloss.Color("36")), - Directory: r.NewStyle().Foreground(lipgloss.Color("99")), - File: r.NewStyle(), - DisabledFile: r.NewStyle().Foreground(lipgloss.Color("243")), - DisabledSelected: r.NewStyle().Foreground(lipgloss.Color("247")), - Permission: r.NewStyle().Foreground(lipgloss.Color("244")), - Selected: r.NewStyle().Foreground(lipgloss.Color("212")).Bold(true), - FileSize: r.NewStyle().Foreground(lipgloss.Color("240")).Width(fileSizeWidth).Align(lipgloss.Right), - EmptyDirectory: r.NewStyle().Foreground(lipgloss.Color("240")).PaddingLeft(paddingLeft).SetString("Bummer. No Files Found."), + DisabledCursor: ctx.NewStyle().Foreground(lipgloss.Color("247")), + Cursor: ctx.NewStyle().Foreground(lipgloss.Color("212")), + Symlink: ctx.NewStyle().Foreground(lipgloss.Color("36")), + Directory: ctx.NewStyle().Foreground(lipgloss.Color("99")), + File: ctx.NewStyle(), + DisabledFile: ctx.NewStyle().Foreground(lipgloss.Color("243")), + DisabledSelected: ctx.NewStyle().Foreground(lipgloss.Color("247")), + Permission: ctx.NewStyle().Foreground(lipgloss.Color("244")), + Selected: ctx.NewStyle().Foreground(lipgloss.Color("212")).Bold(true), + FileSize: ctx.NewStyle().Foreground(lipgloss.Color("240")).Width(fileSizeWidth).Align(lipgloss.Right), + EmptyDirectory: ctx.NewStyle().Foreground(lipgloss.Color("240")).PaddingLeft(paddingLeft).SetString("Bummer. No Files Found."), } } @@ -236,12 +230,12 @@ func (m Model) readDir(path string, showHidden bool) tea.Cmd { } // Init initializes the file picker model. -func (m Model) Init() tea.Cmd { +func (m Model) Init(tea.Context) tea.Cmd { return m.readDir(m.CurrentDirectory, m.ShowHidden) } // Update handles user interactions within the file picker model. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(_ tea.Context, msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case readDirMsg: if msg.id != m.id { @@ -363,7 +357,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } // View returns the view of the file picker. -func (m Model) View() string { +func (m Model) View(tea.Context) string { if len(m.files) == 0 { return m.Styles.EmptyDirectory.Height(m.Height).MaxHeight(m.Height).String() } diff --git a/go.mod b/go.mod index 9ff29e8..5c01409 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,11 @@ module github.com/charmbracelet/bubbles go 1.18 -replace github.com/charmbracelet/bubbletea => ../bubbletea - require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/atotto/clipboard v0.1.4 - github.com/charmbracelet/bubbletea v0.25.1-0.20240306212323-3df8b37dba50 + github.com/charmbracelet/bubbletea v0.26.3-0.20240510203954-a434f25ff7d3 github.com/charmbracelet/harmonica v0.2.0 github.com/charmbracelet/lipgloss v0.10.1-0.20240510203806-a6dc2ab509a8 github.com/dustin/go-humanize v1.0.1 @@ -35,5 +33,3 @@ require ( golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect ) - -replace github.com/charmbracelet/x/exp/term => ../x/exp/term diff --git a/go.sum b/go.sum index 49c5f15..5c79b83 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z 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/charmbracelet/bubbletea v0.26.3-0.20240510203954-a434f25ff7d3 h1:Bg0eRBAgsWh84rpAq5X+e52TMAfloxoWeSJkYp9Pyw0= +github.com/charmbracelet/bubbletea v0.26.3-0.20240510203954-a434f25ff7d3/go.mod h1:jxN9TKmttdNsUKWSjjQQonHIY4nCuTBNdkDn3/Bpf7A= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.10.1-0.20240510203806-a6dc2ab509a8 h1:aOfL0f8fhgqKe+iOIQYAIazSwTO4J0axB5ztPxw5JHY= diff --git a/help/help.go b/help/help.go index 8e5f77f..c2f356e 100644 --- a/help/help.go +++ b/help/help.go @@ -15,7 +15,6 @@ import ( // 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. type KeyMap interface { - // 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 // which the help items are returned here. @@ -58,18 +57,18 @@ type Model struct { } // New creates a new help view with some useful defaults. -func New() Model { - keyStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ +func New(ctx tea.Context) Model { + keyStyle := ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{ Light: "#909090", Dark: "#626262", }) - descStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + descStyle := ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{ Light: "#B2B2B2", Dark: "#4A4A4A", }) - sepStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + sepStyle := ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{ Light: "#DDDADA", Dark: "#3C3C3C", }) @@ -118,7 +117,7 @@ func (m Model) ShortHelpView(bindings []key.Binding) string { var b strings.Builder 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 { if !kb.Enabled() { diff --git a/list/defaultitem.go b/list/defaultitem.go index 5474c00..ed9424f 100644 --- a/list/defaultitem.go +++ b/list/defaultitem.go @@ -32,15 +32,15 @@ type DefaultItemStyles struct { // NewDefaultItemStyles returns style definitions for a default item. See // DefaultItemView for when these come into play. -func NewDefaultItemStyles() (s DefaultItemStyles) { - s.NormalTitle = lipgloss.NewStyle(). +func NewDefaultItemStyles(ctx tea.Context) (s DefaultItemStyles) { + s.NormalTitle = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}). Padding(0, 0, 0, 2) s.NormalDesc = s.NormalTitle.Copy(). Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}) - s.SelectedTitle = lipgloss.NewStyle(). + s.SelectedTitle = ctx.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"}). Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}). @@ -49,14 +49,14 @@ func NewDefaultItemStyles() (s DefaultItemStyles) { s.SelectedDesc = s.SelectedTitle.Copy(). Foreground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"}) - s.DimmedTitle = lipgloss.NewStyle(). + s.DimmedTitle = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}). Padding(0, 0, 0, 2) s.DimmedDesc = s.DimmedTitle.Copy(). Foreground(lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"}) - s.FilterMatch = lipgloss.NewStyle().Underline(true) + s.FilterMatch = ctx.NewStyle().Underline(true) return s } @@ -92,10 +92,10 @@ type DefaultDelegate struct { } // NewDefaultDelegate creates a new delegate with default styles. -func NewDefaultDelegate() DefaultDelegate { +func NewDefaultDelegate(ctx tea.Context) DefaultDelegate { return DefaultDelegate{ ShowDescription: true, - Styles: NewDefaultItemStyles(), + Styles: NewDefaultItemStyles(ctx), height: 2, spacing: 1, } diff --git a/list/list.go b/list/list.go index af360d1..feac1b6 100644 --- a/list/list.go +++ b/list/list.go @@ -49,7 +49,7 @@ type ItemDelegate interface { // loop will pass through here except when the user is setting a filter. // Use this method to perform item-level updates appropriate to this // delegate. - Update(msg tea.Msg, m *Model) tea.Cmd + Update(ctx tea.Context, msg tea.Msg, m *Model) tea.Cmd } type filteredItem struct { @@ -192,24 +192,26 @@ type Model struct { filteredItems filteredItems delegate ItemDelegate + + ctx tea.Context } // New returns a new model with sensible defaults. -func New(items []Item, delegate ItemDelegate, width, height int) Model { - styles := DefaultStyles() +func New(ctx tea.Context, items []Item, delegate ItemDelegate, width, height int) Model { + styles := DefaultStyles(ctx) - sp := spinner.New() + sp := spinner.New(ctx) sp.Spinner = spinner.Line sp.Style = styles.Spinner - filterInput := textinput.New() + filterInput := textinput.New(ctx) filterInput.Prompt = "Filter: " filterInput.PromptStyle = styles.FilterPrompt filterInput.Cursor.Style = styles.FilterCursor filterInput.CharLimit = 64 filterInput.Focus() - p := paginator.New() + p := paginator.New(ctx) p.Type = paginator.Dots p.ActiveDot = styles.ActivePaginationDot.String() p.InactiveDot = styles.InactivePaginationDot.String() @@ -236,7 +238,8 @@ func New(items []Item, delegate ItemDelegate, width, height int) Model { items: items, Paginator: p, spinner: sp, - Help: help.New(), + Help: help.New(ctx), + ctx: ctx, } m.updatePagination() @@ -768,7 +771,7 @@ func (m *Model) hideStatusMessage() { } // Update is the Bubble Tea update loop. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(ctx tea.Context, msg tea.Msg) (Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { @@ -782,7 +785,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil case spinner.TickMsg: - newSpinnerModel, cmd := m.spinner.Update(msg) + newSpinnerModel, cmd := m.spinner.Update(ctx, msg) m.spinner = newSpinnerModel if m.showSpinner { cmds = append(cmds, cmd) @@ -859,7 +862,7 @@ func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd { } } - cmd := m.delegate.Update(msg, m) + cmd := m.delegate.Update(m.ctx, msg, m) cmds = append(cmds, cmd) // Keep the index in bounds when paginating @@ -909,7 +912,7 @@ func (m *Model) handleFiltering(msg tea.Msg) tea.Cmd { } // Update the filter text input component - newFilterInputModel, inputCmd := m.FilterInput.Update(msg) + newFilterInputModel, inputCmd := m.FilterInput.Update(m.ctx, msg) filterChanged := m.FilterInput.Value() != newFilterInputModel.Value() m.FilterInput = newFilterInputModel cmds = append(cmds, inputCmd) @@ -1003,7 +1006,7 @@ func (m Model) FullHelp() [][]key.Binding { } // View renders the component. -func (m Model) View() string { +func (m Model) View(ctx tea.Context) string { var ( sections []string availHeight = m.height @@ -1033,7 +1036,7 @@ func (m Model) View() string { availHeight -= lipgloss.Height(help) } - content := lipgloss.NewStyle().Height(availHeight).Render(m.populatedView()) + content := ctx.NewStyle().Height(availHeight).Render(m.populatedView()) sections = append(sections, content) if m.showPagination { @@ -1062,7 +1065,7 @@ func (m Model) titleView() string { // If the filter's showing, draw that. Otherwise draw the title. if m.showFilter && m.filterState == Filtering { - view += m.FilterInput.View() + view += m.FilterInput.View(m.ctx) } else if m.showTitle { if m.showSpinner && spinnerOnLeft { view += spinnerView + spinnerLeftGap @@ -1147,13 +1150,13 @@ func (m Model) paginationView() string { return "" } - s := m.Paginator.View() + s := m.Paginator.View(m.ctx) // If the dot pagination is wider than the width of the window // use the arabic paginator. if ansi.PrintableRuneWidth(s) > m.width { m.Paginator.Type = paginator.Arabic - s = m.Styles.ArabicPagination.Render(m.Paginator.View()) + s = m.Styles.ArabicPagination.Render(m.Paginator.View(m.ctx)) } style := m.Styles.PaginationStyle @@ -1209,7 +1212,7 @@ func (m Model) helpView() string { } func (m Model) spinnerView() string { - return m.spinner.View() + return m.spinner.View(m.ctx) } func filterItems(m Model) tea.Cmd { diff --git a/list/style.go b/list/style.go index e4451f8..2c37ed0 100644 --- a/list/style.go +++ b/list/style.go @@ -1,6 +1,7 @@ package list import ( + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -41,57 +42,57 @@ type Styles struct { // DefaultStyles returns a set of default style definitions for this list // component. -func DefaultStyles() (s Styles) { +func DefaultStyles(ctx tea.Context) (s Styles) { verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"} subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"} - s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 1, 2) + s.TitleBar = ctx.NewStyle().Padding(0, 0, 1, 2) - s.Title = lipgloss.NewStyle(). + s.Title = ctx.NewStyle(). Background(lipgloss.Color("62")). Foreground(lipgloss.Color("230")). Padding(0, 1) - s.Spinner = lipgloss.NewStyle(). + s.Spinner = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#8E8E8E", Dark: "#747373"}) - s.FilterPrompt = lipgloss.NewStyle(). + s.FilterPrompt = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}) - s.FilterCursor = lipgloss.NewStyle(). + s.FilterCursor = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}) - s.DefaultFilterCharacterMatch = lipgloss.NewStyle().Underline(true) + s.DefaultFilterCharacterMatch = ctx.NewStyle().Underline(true) - s.StatusBar = lipgloss.NewStyle(). + s.StatusBar = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}). Padding(0, 0, 1, 2) - s.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor) + s.StatusEmpty = ctx.NewStyle().Foreground(subduedColor) - s.StatusBarActiveFilter = lipgloss.NewStyle(). + s.StatusBarActiveFilter = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}) - s.StatusBarFilterCount = lipgloss.NewStyle().Foreground(verySubduedColor) + s.StatusBarFilterCount = ctx.NewStyle().Foreground(verySubduedColor) - s.NoItems = lipgloss.NewStyle(). + s.NoItems = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"}) - s.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor) + s.ArabicPagination = ctx.NewStyle().Foreground(subduedColor) - s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:gomnd + s.PaginationStyle = ctx.NewStyle().PaddingLeft(2) //nolint:gomnd - s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) + s.HelpStyle = ctx.NewStyle().Padding(1, 0, 0, 2) - s.ActivePaginationDot = lipgloss.NewStyle(). + s.ActivePaginationDot = ctx.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}). SetString(bullet) - s.InactivePaginationDot = lipgloss.NewStyle(). + s.InactivePaginationDot = ctx.NewStyle(). Foreground(verySubduedColor). SetString(bullet) - s.DividerDot = lipgloss.NewStyle(). + s.DividerDot = ctx.NewStyle(). Foreground(verySubduedColor). SetString(" " + bullet + " ") diff --git a/paginator/paginator.go b/paginator/paginator.go index 82dc3ed..aeaec74 100644 --- a/paginator/paginator.go +++ b/paginator/paginator.go @@ -131,7 +131,7 @@ func (m Model) OnFirstPage() bool { } // New creates a new model with defaults. -func New() Model { +func New(ctx tea.Context) Model { return Model{ Type: Arabic, Page: 0, @@ -150,7 +150,7 @@ func New() Model { var NewModel = New // Update is the Tea update function which binds keystrokes to pagination. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(ctx tea.Context, msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { @@ -165,7 +165,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } // View renders the pagination to a string. -func (m Model) View() string { +func (m Model) View(tea.Context) string { switch m.Type { case Dots: return m.dotsView() diff --git a/progress/progress.go b/progress/progress.go index d5a4c5a..e1b7c7e 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -12,7 +12,6 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/lucasb-eyer/go-colorful" "github.com/muesli/reflow/ansi" - "github.com/muesli/termenv" ) // Internal ID management. Used during animating to assure that frame messages @@ -115,13 +114,6 @@ func WithSpringOptions(frequency, damping float64) Option { } } -// WithColorProfile sets the color profile to use for the progress bar. -func WithColorProfile(p termenv.Profile) Option { - return func(m *Model) { - m.colorProfile = p - } -} - // FrameMsg indicates that an animation step should occur. type FrameMsg struct { id int @@ -169,13 +161,10 @@ type Model struct { // of the progress bar. When false, the width of the gradient will be set // to the full width of the progress bar. scaleRamp bool - - // Color profile for the progress bar. - colorProfile termenv.Profile } // New returns a model with default values. -func New(opts ...Option) Model { +func New(ctx tea.Context, opts ...Option) Model { m := Model{ id: nextID(), Width: defaultWidth, @@ -185,7 +174,6 @@ func New(opts ...Option) Model { EmptyColor: "#606060", ShowPercentage: true, PercentFormat: " %3.0f%%", - colorProfile: termenv.ColorProfile(), } if !m.springCustomized { m.SetSpringOptions(defaultFrequency, defaultDamping) @@ -203,15 +191,15 @@ func New(opts ...Option) Model { var NewModel = New // Init exists to satisfy the tea.Model interface. -func (m Model) Init() tea.Cmd { - return nil +func (m Model) Init(tea.Context) (tea.Model, tea.Cmd) { + return m, nil } // Update is used to animate the progress bar during transitions. Use // SetPercent to create the command you'll need to trigger the animation. // // If you're rendering with ViewAs you won't need this. -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m Model) Update(ctx tea.Context, msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case FrameMsg: if msg.id != m.id || msg.tag != m.tag { @@ -275,15 +263,15 @@ func (m *Model) DecrPercent(v float64) tea.Cmd { // View renders an animated progress bar in its current state. To render // a static progress bar based on your own calculations use ViewAs instead. -func (m Model) View() string { - return m.ViewAs(m.percentShown) +func (m Model) View(ctx tea.Context) string { + return m.ViewAs(ctx, m.percentShown) } // ViewAs renders the progress bar with a given percentage. -func (m Model) ViewAs(percent float64) string { +func (m Model) ViewAs(ctx tea.Context, percent float64) string { b := strings.Builder{} percentView := m.percentageView(percent) - m.barView(&b, percent, ansi.PrintableRuneWidth(percentView)) + m.barView(ctx, &b, percent, ansi.PrintableRuneWidth(percentView)) b.WriteString(percentView) return b.String() } @@ -294,7 +282,7 @@ func (m *Model) nextFrame() tea.Cmd { }) } -func (m Model) barView(b *strings.Builder, percent float64, textWidth int) { +func (m Model) barView(ctx tea.Context, b *strings.Builder, percent float64, textWidth int) { var ( tw = max(0, m.Width-textWidth) // total width fw = int(math.Round((float64(tw) * percent))) // filled width @@ -317,20 +305,19 @@ func (m Model) barView(b *strings.Builder, percent float64, textWidth int) { p = float64(i) / float64(tw-1) } c := m.rampColorA.BlendLuv(m.rampColorB, p).Hex() - b.WriteString(termenv. - String(string(m.Full)). - Foreground(m.color(c)). - String(), + b.WriteString(ctx.NewStyle(). + Foreground(lipgloss.Color(c)). + Render(string(m.Full)), ) } } else { // Solid fill - s := termenv.String(string(m.Full)).Foreground(m.color(m.FullColor)).String() + s := ctx.NewStyle().Foreground(lipgloss.Color(m.FullColor)).Render(string(m.Full)) b.WriteString(strings.Repeat(s, fw)) } // Empty fill - e := termenv.String(string(m.Empty)).Foreground(m.color(m.EmptyColor)).String() + e := ctx.NewStyle().Foreground(lipgloss.Color(m.EmptyColor)).Render(string(m.Empty)) n := max(0, tw-fw) b.WriteString(strings.Repeat(e, n)) } @@ -358,10 +345,6 @@ func (m *Model) setRamp(colorA, colorB string, scaled bool) { m.rampColorB = b } -func (m Model) color(c string) termenv.Color { - return m.colorProfile.Color(c) -} - func max(a, b int) int { if a > b { return a diff --git a/spinner/spinner.go b/spinner/spinner.go index bb53597..974791b 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -113,9 +113,10 @@ func (m Model) ID() int { } // New returns a model with default values. -func New(opts ...Option) Model { +func New(ctx tea.Context, opts ...Option) Model { m := Model{ Spinner: Line, + Style: ctx.NewStyle(), id: nextID(), } @@ -139,7 +140,7 @@ type TickMsg struct { } // Update is the Tea update function. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(ctx tea.Context, msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case TickMsg: // If an ID is set, and the ID doesn't belong to this spinner, reject @@ -168,7 +169,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } // View renders the model's view. -func (m Model) View() string { +func (m Model) View(ctx tea.Context) string { if m.frame >= len(m.Spinner.Frames) { return "(error)" } diff --git a/stopwatch/stopwatch.go b/stopwatch/stopwatch.go index 6b298f7..f8b3d01 100644 --- a/stopwatch/stopwatch.go +++ b/stopwatch/stopwatch.go @@ -55,7 +55,7 @@ type Model struct { // NewWithInterval creates a new stopwatch with the given timeout and tick // interval. -func NewWithInterval(interval time.Duration) Model { +func NewWithInterval(_ tea.Context, interval time.Duration) Model { return Model{ Interval: interval, id: nextID(), @@ -63,8 +63,8 @@ func NewWithInterval(interval time.Duration) Model { } // New creates a new stopwatch with 1s interval. -func New() Model { - return NewWithInterval(time.Second) +func New(ctx tea.Context) Model { + return NewWithInterval(ctx, time.Second) } // ID returns the unique ID of the model. @@ -73,7 +73,7 @@ func (m Model) ID() int { } // Init starts the stopwatch. -func (m Model) Init() tea.Cmd { +func (m Model) Init(tea.Context) tea.Cmd { return m.Start() } @@ -112,7 +112,7 @@ func (m Model) Running() bool { } // Update handles the timer tick. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(_ tea.Context, msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case StartStopMsg: if msg.ID != m.id { @@ -141,7 +141,7 @@ func (m Model) Elapsed() time.Duration { } // View of the timer component. -func (m Model) View() string { +func (m Model) View(tea.Context) string { return m.d.String() } diff --git a/table/table.go b/table/table.go index e355b23..fa8c62e 100644 --- a/table/table.go +++ b/table/table.go @@ -24,6 +24,8 @@ type Model struct { viewport viewport.Model start int end int + + ctx tea.Context } // Row represents one line in the table. @@ -109,11 +111,11 @@ type Styles struct { } // DefaultStyles returns a set of default style definitions for this table. -func DefaultStyles() Styles { +func DefaultStyles(ctx tea.Context) Styles { return Styles{ - Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")), - Header: lipgloss.NewStyle().Bold(true).Padding(0, 1), - Cell: lipgloss.NewStyle().Padding(0, 1), + Selected: ctx.NewStyle().Bold(true).Foreground(lipgloss.Color("212")), + Header: ctx.NewStyle().Bold(true).Padding(0, 1), + Cell: ctx.NewStyle().Padding(0, 1), } } @@ -129,13 +131,15 @@ func (m *Model) SetStyles(s Styles) { type Option func(*Model) // New creates a new model for the table widget. -func New(opts ...Option) Model { +func New(ctx tea.Context, opts ...Option) Model { m := Model{ cursor: 0, - viewport: viewport.New(0, 20), + viewport: viewport.New(ctx, 0, 20), KeyMap: DefaultKeyMap(), - styles: DefaultStyles(), + styles: DefaultStyles(ctx), + + ctx: ctx, } for _, opt := range opts { @@ -204,7 +208,7 @@ func WithKeyMap(km KeyMap) Option { } // Update is the Bubble Tea update loop. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(ctx tea.Context, msg tea.Msg) (Model, tea.Cmd) { if !m.focus { return m, nil } @@ -255,8 +259,8 @@ func (m *Model) Blur() { } // View renders the component. -func (m Model) View() string { - return m.headersView() + "\n" + m.viewport.View() +func (m Model) View(ctx tea.Context) string { + return m.headersView() + "\n" + m.viewport.View(ctx) } // UpdateViewport updates the list content based on the previously defined @@ -409,12 +413,12 @@ func (m *Model) FromValues(value, separator string) { type StyleFunc func(row, col int, value string) lipgloss.Style 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 { if col.Width <= 0 { continue } - style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) + style := m.ctx.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…")) s = append(s, m.styles.Header.Render(renderedCell)) } @@ -422,7 +426,7 @@ func (m Model) headersView() string { } func (m *Model) renderRow(r int) string { - var s = make([]string, 0, len(m.cols)) + s := make([]string, 0, len(m.cols)) for i, value := range m.rows[r] { if m.cols[i].Width <= 0 { continue @@ -437,7 +441,7 @@ func (m *Model) renderRow(r int) string { cellStyle = m.styles.Cell } - style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true) + style := m.ctx.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true) renderedCell := cellStyle.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…"))) s = append(s, renderedCell) } diff --git a/textarea/textarea.go b/textarea/textarea.go index 6a48520..772b326 100644 --- a/textarea/textarea.go +++ b/textarea/textarea.go @@ -245,12 +245,12 @@ type Model struct { } // New creates a new model with default settings. -func New() Model { - vp := viewport.New(0, 0) +func New(ctx tea.Context) Model { + vp := viewport.New(ctx, 0, 0) vp.KeyMap = viewport.KeyMap{} - cur := cursor.New() + cur := cursor.New(ctx) - focusedStyle, blurredStyle := DefaultStyles() + focusedStyle, blurredStyle := DefaultStyles(ctx) m := Model{ CharLimit: defaultCharLimit, @@ -283,26 +283,26 @@ func New() Model { // DefaultStyles returns the default styles for focused and blurred states for // the textarea. -func DefaultStyles() (Style, Style) { +func DefaultStyles(ctx tea.Context) (Style, Style) { focused := Style{ - Base: lipgloss.NewStyle(), - CursorLine: lipgloss.NewStyle().Background(lipgloss.AdaptiveColor{Light: "255", Dark: "0"}), - CursorLineNumber: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "240"}), - EndOfBuffer: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "254", Dark: "0"}), - LineNumber: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), - Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), - Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), - Text: lipgloss.NewStyle(), + Base: ctx.NewStyle(), + CursorLine: ctx.NewStyle().Background(lipgloss.AdaptiveColor{Light: "255", Dark: "0"}), + CursorLineNumber: ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "240"}), + EndOfBuffer: ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "254", Dark: "0"}), + LineNumber: ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), + Placeholder: ctx.NewStyle().Foreground(lipgloss.Color("240")), + Prompt: ctx.NewStyle().Foreground(lipgloss.Color("7")), + Text: ctx.NewStyle(), } blurred := Style{ - Base: lipgloss.NewStyle(), - CursorLine: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "7"}), - CursorLineNumber: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), - EndOfBuffer: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "254", Dark: "0"}), - LineNumber: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), - Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), - Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), - Text: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "7"}), + Base: ctx.NewStyle(), + CursorLine: ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "7"}), + CursorLineNumber: ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), + EndOfBuffer: ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "254", Dark: "0"}), + LineNumber: ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), + Placeholder: ctx.NewStyle().Foreground(lipgloss.Color("240")), + Prompt: ctx.NewStyle().Foreground(lipgloss.Color("7")), + Text: ctx.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "7"}), } return focused, blurred @@ -927,7 +927,7 @@ func (m *Model) SetHeight(h int) { } // Update is the Bubble Tea update loop. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(ctx tea.Context, msg tea.Msg) (Model, tea.Cmd) { if !m.focus { m.Cursor.Blur() return m, nil @@ -1047,12 +1047,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.Err = msg } - vp, cmd := m.viewport.Update(msg) + vp, cmd := m.viewport.Update(ctx, msg) m.viewport = &vp cmds = append(cmds, cmd) newRow, newCol := m.cursorLineNumber(), m.col - m.Cursor, cmd = m.Cursor.Update(msg) + m.Cursor, cmd = m.Cursor.Update(ctx, msg) if (newRow != oldRow || newCol != oldCol) && m.Cursor.Mode() == cursor.CursorBlink { m.Cursor.Blink = false cmd = m.Cursor.BlinkCmd() @@ -1065,9 +1065,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } // View renders the text area in its current state. -func (m Model) View() string { +func (m Model) View(ctx tea.Context) string { if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" { - return m.placeholderView() + return m.placeholderView(ctx) } m.Cursor.TextStyle = m.style.CursorLine @@ -1126,10 +1126,10 @@ func (m Model) View() string { s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset]))) if m.col >= len(line) && lineInfo.CharOffset >= m.width { m.Cursor.SetChar(" ") - s.WriteString(m.Cursor.View()) + s.WriteString(m.Cursor.View(ctx)) } else { m.Cursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset])) - s.WriteString(style.Render(m.Cursor.View())) + s.WriteString(style.Render(m.Cursor.View(ctx))) s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:]))) } } else { @@ -1154,7 +1154,7 @@ func (m Model) View() string { } m.viewport.SetContent(s.String()) - return m.style.Base.Render(m.viewport.View()) + return m.style.Base.Render(m.viewport.View(ctx)) } func (m Model) getPromptString(displayLine int) (prompt string) { @@ -1171,7 +1171,7 @@ func (m Model) getPromptString(displayLine int) (prompt string) { } // placeholderView returns the prompt and placeholder view, if any. -func (m Model) placeholderView() string { +func (m Model) placeholderView(ctx tea.Context) string { var ( s strings.Builder p = rw.Truncate(rw.Truncate(m.Placeholder, m.width, "..."), m.width, "") @@ -1188,7 +1188,7 @@ func (m Model) placeholderView() string { m.Cursor.TextStyle = m.style.Placeholder m.Cursor.SetChar(string(p[0])) - s.WriteString(m.style.CursorLine.Render(m.Cursor.View())) + s.WriteString(m.style.CursorLine.Render(m.Cursor.View(ctx))) // The rest of the placeholder text s.WriteString(m.style.CursorLine.Render(style.Render(p[1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(p)))))) @@ -1207,7 +1207,7 @@ func (m Model) placeholderView() string { } m.viewport.SetContent(s.String()) - return m.style.Base.Render(m.viewport.View()) + return m.style.Base.Render(m.viewport.View(ctx)) } // Blink returns the blink command for the cursor. diff --git a/textinput/textinput.go b/textinput/textinput.go index d20477d..5fc92c3 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -155,15 +155,18 @@ type Model struct { } // New creates a new model with default settings. -func New() Model { +func New(ctx tea.Context) Model { return Model{ Prompt: "> ", EchoCharacter: '*', CharLimit: 0, - PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + PromptStyle: ctx.NewStyle(), + TextStyle: ctx.NewStyle(), + CursorStyle: ctx.NewStyle(), + PlaceholderStyle: ctx.NewStyle().Foreground(lipgloss.Color("240")), ShowSuggestions: false, - CompletionStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), - Cursor: cursor.New(), + CompletionStyle: ctx.NewStyle().Foreground(lipgloss.Color("240")), + Cursor: cursor.New(ctx), KeyMap: DefaultKeyMap, suggestions: [][]rune{}, @@ -549,7 +552,7 @@ func (m Model) echoTransform(v string) string { } // Update is the Bubble Tea update loop. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(ctx tea.Context, msg tea.Msg) (Model, tea.Cmd) { if !m.focus { return m, nil } @@ -636,7 +639,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmds []tea.Cmd var cmd tea.Cmd - m.Cursor, cmd = m.Cursor.Update(msg) + m.Cursor, cmd = m.Cursor.Update(ctx, msg) cmds = append(cmds, cmd) if oldPos != m.pos && m.Cursor.Mode() == cursor.CursorBlink { @@ -649,10 +652,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } // View renders the textinput in its current state. -func (m Model) View() string { +func (m Model) View(ctx tea.Context) string { // Placeholder text if len(m.value) == 0 && m.Placeholder != "" { - return m.placeholderView() + return m.placeholderView(ctx) } styleText := m.TextStyle.Inline(true).Render @@ -664,7 +667,7 @@ func (m Model) View() string { if pos < len(value) { char := m.echoTransform(string(value[pos])) m.Cursor.SetChar(char) - v += m.Cursor.View() // cursor and text under it + v += m.Cursor.View(ctx) // cursor and text under it v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor v += m.completionView(0) // suggested completion } else { @@ -673,15 +676,15 @@ func (m Model) View() string { if len(value) < len(suggestion) { m.Cursor.TextStyle = m.CompletionStyle m.Cursor.SetChar(m.echoTransform(string(suggestion[pos]))) - v += m.Cursor.View() + v += m.Cursor.View(ctx) v += m.completionView(1) } else { m.Cursor.SetChar(" ") - v += m.Cursor.View() + v += m.Cursor.View(ctx) } } else { m.Cursor.SetChar(" ") - v += m.Cursor.View() + v += m.Cursor.View(ctx) } } @@ -700,7 +703,7 @@ func (m Model) View() string { } // placeholderView returns the prompt and placeholder view, if any. -func (m Model) placeholderView() string { +func (m Model) placeholderView(ctx tea.Context) string { var ( v string p = []rune(m.Placeholder) @@ -709,7 +712,7 @@ func (m Model) placeholderView() string { m.Cursor.TextStyle = m.PlaceholderStyle m.Cursor.SetChar(string(p[:1])) - v += m.Cursor.View() + v += m.Cursor.View(ctx) // If the entire placeholder is already set and no padding is needed, finish if m.Width < 1 && len(p) <= 1 { diff --git a/timer/timer.go b/timer/timer.go index eb085e0..494a66f 100644 --- a/timer/timer.go +++ b/timer/timer.go @@ -90,7 +90,7 @@ type Model struct { } // NewWithInterval creates a new timer with the given timeout and tick interval. -func NewWithInterval(timeout, interval time.Duration) Model { +func NewWithInterval(_ tea.Context, timeout, interval time.Duration) Model { return Model{ Timeout: timeout, Interval: interval, @@ -100,8 +100,8 @@ func NewWithInterval(timeout, interval time.Duration) Model { } // New creates a new timer with the given timeout and default 1s interval. -func New(timeout time.Duration) Model { - return NewWithInterval(timeout, time.Second) +func New(ctx tea.Context, timeout time.Duration) Model { + return NewWithInterval(ctx, timeout, time.Second) } // ID returns the model's identifier. This can be used to determine if messages @@ -125,12 +125,12 @@ func (m Model) Timedout() bool { } // Init starts the timer. -func (m Model) Init() tea.Cmd { +func (m Model) Init(tea.Context) tea.Cmd { return m.tick() } // Update handles the timer tick. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(_ tea.Context, msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case StartStopMsg: if msg.ID != 0 && msg.ID != m.id { @@ -151,7 +151,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } // View of the timer component. -func (m Model) View() string { +func (m Model) View(tea.Context) string { return m.Timeout.String() } diff --git a/viewport/viewport.go b/viewport/viewport.go index 747f67c..0053a6a 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -11,7 +11,7 @@ import ( // New returns a new model with the given width and height as well as default // key mappings. -func New(width, height int) (m Model) { +func New(ctx tea.Context, width, height int) (m Model) { m.Width = width m.Height = height m.setInitialValues() @@ -64,7 +64,7 @@ func (m *Model) setInitialValues() { } // Init exists to satisfy the tea.Model interface for composability purposes. -func (m Model) Init() tea.Cmd { +func (m Model) Init(tea.Context) tea.Cmd { return nil } @@ -276,7 +276,7 @@ func ViewUp(m Model, lines []string) tea.Cmd { } // Update handles standard message-based viewport updates. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m Model) Update(_ tea.Context, msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m, cmd = m.updateAsModel(msg) return m, cmd @@ -354,7 +354,7 @@ func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) { } // View renders the viewport into a string. -func (m Model) View() string { +func (m Model) View(ctx tea.Context) string { if m.HighPerformanceRendering { // Just send newlines since we're going to be rendering the actual // content separately. We still need to send something that equals the @@ -372,7 +372,7 @@ func (m Model) View() string { } contentWidth := w - m.Style.GetHorizontalFrameSize() contentHeight := h - m.Style.GetVerticalFrameSize() - contents := lipgloss.NewStyle(). + contents := ctx.NewStyle(). Width(contentWidth). // pad to width. Height(contentHeight). // pad to height. MaxHeight(contentHeight). // truncate height if taller.