1 Commits

Author SHA1 Message Date
Christian Muehlhaeuser
2e2a7eccb7 Add vertical layout 2020-10-30 15:59:45 +01:00
9 changed files with 212 additions and 374 deletions

View File

@@ -1,10 +1,6 @@
Bubbles Bubbles
======= =======
<p>
<img src="https://stuff.charm.sh/bubbles/bubbles-github.png" width="233" alt="The Bubbles Logo">
</p>
[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases) [![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles) [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions) [![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
@@ -40,17 +36,6 @@ the common, and many customization options.
* [Example code, many fields](https://github.com/charmbracelet/tea/tree/master/examples/textinputs/main.go) * [Example code, many fields](https://github.com/charmbracelet/tea/tree/master/examples/textinputs/main.go)
## Progress
<img src="https://stuff.charm.sh/bubbles-examples/progress.gif" width="800" alt="Progressbar Example">
A simple, customizable progress meter. Supports solid and gradient fills. The
empty and filled runes can be set to whatever you'd like. The percentage readout
is customizable and can also be omitted entirely.
* [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/progress/main.go)
## Paginator ## Paginator
<img src="https://stuff.charm.sh/bubbles-examples/pagination.gif" width="200" alt="Paginator Example"> <img src="https://stuff.charm.sh/bubbles-examples/pagination.gif" width="200" alt="Paginator Example">
@@ -70,7 +55,7 @@ browse SSH keys.
A viewport for vertically scrolling content. Optionally includes standard A viewport for vertically scrolling content. Optionally includes standard
pager keybindings and mouse wheel support. A high performance mode is available pager keybindings and mouse wheel support. A high performance mode is available
for applications which make use of the alternate screen buffer. for applications which make use of the alterate screen buffer.
* [Example code](https://github.com/charmbracelet/tea/tree/master/examples/pager/main.go) * [Example code](https://github.com/charmbracelet/tea/tree/master/examples/pager/main.go)
@@ -89,6 +74,7 @@ indenting and text wrapping.
Part of [Charm](https://charm.sh). Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a> <a href="https://charm.sh/"><img alt="the Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
Charm热爱开源! / Charm loves open source! Charm热爱开源! / Charm loves open source!

4
go.mod
View File

@@ -4,10 +4,8 @@ go 1.13
require ( require (
github.com/atotto/clipboard v0.1.2 github.com/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.12.2 github.com/charmbracelet/bubbletea v0.12.1
github.com/lucasb-eyer/go-colorful v1.0.3
github.com/mattn/go-runewidth v0.0.9 github.com/mattn/go-runewidth v0.0.9
github.com/muesli/reflow v0.2.0
github.com/muesli/termenv v0.7.4 github.com/muesli/termenv v0.7.4
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a // indirect golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a // indirect

6
go.sum
View File

@@ -1,7 +1,7 @@
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.12.2 h1:y9Yo2Pv8tcm3mAJsWONGsmHhzrbNxJVxpVtemikxE9A= github.com/charmbracelet/bubbletea v0.12.1 h1:t21pkG2IDBRduPbt2J64Dx5yt8yIidAkXwhhrc11SzY=
github.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg= github.com/charmbracelet/bubbletea v0.12.1/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc= github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
@@ -12,8 +12,6 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/muesli/reflow v0.2.0 h1:2o0UBJPHHH4fa2GCXU4Rg4DwOtWPMekCeyc5EWbAQp0=
github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8=
github.com/muesli/termenv v0.7.2 h1:r1raklL3uKE7rOvWgSenmEm2px+dnc33OTisZ8YR1fw= github.com/muesli/termenv v0.7.2 h1:r1raklL3uKE7rOvWgSenmEm2px+dnc33OTisZ8YR1fw=
github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8= github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8= github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8=

118
layouts/vertical.go Normal file
View File

@@ -0,0 +1,118 @@
package layouts
import (
tea "github.com/charmbracelet/bubbletea"
)
// Model is the Bubble Tea model for a vertical layout element.
type Model struct {
Index int
Items []tea.Model
// Focus indicates whether user focus should be on this component
focus bool
}
type FocusItem interface {
Focus() tea.Model
Blur() tea.Model
}
// NewModel creates a new model with default settings.
func NewModel() Model {
return Model{}
}
// Update is the Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if !m.focus {
return m, nil
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "shift+tab", "up":
m.Index--
if m.Index < 0 {
m.Index = len(m.Items) - 1
}
m.updateFocus()
case "tab", "down":
m.Index++
if m.Index >= len(m.Items) {
m.Index = 0
}
m.updateFocus()
}
}
cmd := m.updateItems(msg)
return m, cmd
}
// View renders the layout in its current state.
func (m Model) View() string {
var view string
for _, v := range m.Items {
if mi, ok := v.(tea.Model); ok {
view += mi.View() + "\n"
}
}
return view
}
func (m *Model) updateFocus() {
for i, v := range m.Items {
if m.Index == i {
if fi, ok := v.(FocusItem); ok {
// new focused item
m.Items[i] = fi.Focus()
}
} else {
if fi, ok := v.(FocusItem); ok {
m.Items[i] = fi.Blur()
}
}
}
}
// Pass messages and models through to text input components. Only text inputs
// with Focus() set will respond, so it's safe to simply update all of them
// here without any further logic.
func (m *Model) updateItems(msg tea.Msg) tea.Cmd {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
for i, v := range m.Items {
if mi, ok := v.(tea.Model); ok {
m.Items[i], cmd = mi.Update(msg)
if cmd != nil {
cmds = append(cmds, cmd)
}
}
}
return tea.Batch(cmds...)
}
// Focused returns the focus state on the model.
func (m Model) Focused() bool {
return m.focus
}
// Focus sets the focus state on the model.
func (m *Model) Focus() {
m.focus = true
m.updateFocus()
}
// Blur removes the focus state on the model.
func (m *Model) Blur() {
m.focus = false
}

View File

@@ -19,7 +19,7 @@ const (
Dots Dots
) )
// Model is the Bubble Tea model for this user interface. // Model is the Tea model for this user interface.
type Model struct { type Model struct {
Type Type Type Type
Page int Page int
@@ -115,7 +115,7 @@ func NewModel() Model {
} }
// Update is the Tea update function which binds keystrokes to pagination. // Update is the Tea update function which binds keystrokes to pagination.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
if m.UsePgUpPgDownKeys { if m.UsePgUpPgDownKeys {
@@ -164,16 +164,16 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
} }
// View renders the pagination to a string. // View renders the pagination to a string.
func (m Model) View() string { func View(m Model) string {
switch m.Type { switch m.Type {
case Dots: case Dots:
return m.dotsView() return dotsView(m)
default: default:
return m.arabicView() return arabicView(m)
} }
} }
func (m Model) dotsView() string { func dotsView(m Model) string {
var s string var s string
for i := 0; i < m.TotalPages; i++ { for i := 0; i < m.TotalPages; i++ {
if i == m.Page { if i == m.Page {
@@ -185,7 +185,7 @@ func (m Model) dotsView() string {
return s return s
} }
func (m Model) arabicView() string { func arabicView(m Model) string {
return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages) return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages)
} }

View File

@@ -1,192 +0,0 @@
package progress
import (
"fmt"
"strings"
"github.com/lucasb-eyer/go-colorful"
"github.com/muesli/reflow/ansi"
"github.com/muesli/termenv"
)
const defaultWidth = 40
var color func(string) termenv.Color = termenv.ColorProfile().Color
// Option is used to set options in NewModel. For example:
//
// progress := NewModel(
// WithRamp("#ff0000", "#0000ff"),
// WithoutPercentage(),
// )
type Option func(*Model) error
// WithDefaultGradient sets a gradient fill with default colors.
func WithDefaultGradient() Option {
return WithGradient("#5A56E0", "#EE6FF8")
}
// WithGradient sets a gradient fill blending between two colors.
func WithGradient(colorA, colorB string) Option {
return func(m *Model) error {
return m.setRamp(colorA, colorB, false)
}
}
// WithDefaultScaledGradient sets a gradient with default colors, and scales the
// gradient to fit the filled portion of the ramp.
func WithDefaultScaledGradient() Option {
return WithScaledGradient("#5A56E0", "#EE6FF8")
}
// WithScaledGradient scales the gradient to fit the width of the filled portion of
// the progress bar.
func WithScaledGradient(colorA, colorB string) Option {
return func(m *Model) error {
return m.setRamp(colorA, colorB, true)
}
}
// WithSolidFill sets the progress to use a solid fill with the given color.
func WithSolidFill(color string) Option {
return func(m *Model) error {
m.FullColor = color
m.useRamp = false
return nil
}
}
// WithoutPercentage hides the numeric percentage.
func WithoutPercentage() Option {
return func(m *Model) error {
m.ShowPercentage = false
return nil
}
}
// WithWidth sets the initial width of the progress bar. Note that you can also
// set the width via the Width property, which can come in handy if you're
// waiting for a tea.WindowSizeMsg.
func WithWidth(w int) Option {
return func(m *Model) error {
m.Width = w
return nil
}
}
// Model stores values we'll use when rendering the progress bar.
type Model struct {
// Total width of the progress bar, including percentage, if set.
Width int
// "Filled" sections of the progress bar
Full rune
FullColor string
// "Empty" sections of progress bar
Empty rune
EmptyColor string
// Settings for rendering the numeric percentage
ShowPercentage bool
PercentFormat string // a fmt string for a float
PercentageStyle *termenv.Style
useRamp bool
rampColorA colorful.Color
rampColorB colorful.Color
// When true, we scale the gradient to fit the width of the filled section
// of the progress bar. When false, the width of the gradient will be set
// to the full width of the progress bar.
scaleRamp bool
}
// NewModel returns a model with default values.
func NewModel(opts ...Option) (*Model, error) {
m := &Model{
Width: defaultWidth,
Full: '█',
FullColor: "#7571F9",
Empty: '░',
EmptyColor: "#606060",
ShowPercentage: true,
PercentFormat: " %3.0f%%",
}
for _, opt := range opts {
if err := opt(m); err != nil {
return nil, err
}
}
return m, nil
}
// View renders the progress bar as a given percentage.
func (m Model) View(percent float64) string {
b := strings.Builder{}
if m.ShowPercentage {
percentage := fmt.Sprintf(m.PercentFormat, percent*100)
if m.PercentageStyle != nil {
percentage = m.PercentageStyle.Styled(percentage)
}
m.bar(&b, percent, ansi.PrintableRuneWidth(percentage))
b.WriteString(percentage)
} else {
m.bar(&b, percent, 0)
}
return b.String()
}
func (m Model) bar(b *strings.Builder, percent float64, textWidth int) {
var (
tw = m.Width - textWidth // total width
fw = int(float64(tw) * percent) // filled width
p float64
)
if m.useRamp {
// Gradient fill
for i := 0; i < fw; i++ {
if m.scaleRamp {
p = float64(i) / float64(fw)
} else {
p = float64(i) / float64(tw)
}
c := m.rampColorA.BlendLuv(m.rampColorB, p).Hex()
b.WriteString(termenv.
String(string(m.Full)).
Foreground(color(c)).
String(),
)
}
} else {
// Solid fill
s := termenv.String(string(m.Full)).Foreground(color(m.FullColor)).String()
b.WriteString(strings.Repeat(s, fw))
}
// Empty fill
e := termenv.String(string(m.Empty)).Foreground(color(m.EmptyColor)).String()
b.WriteString(strings.Repeat(e, tw-fw))
}
func (m *Model) setRamp(colorA, colorB string, scaled bool) error {
a, err := colorful.Hex(colorA)
if err != nil {
return err
}
b, err := colorful.Hex(colorB)
if err != nil {
return err
}
m.useRamp = true
m.scaleRamp = scaled
m.rampColorA = a
m.rampColorB = b
return nil
}

View File

@@ -1,60 +1,23 @@
package spinner package spinner
import ( import (
"strings"
"time" "time"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/muesli/reflow/ansi"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
const defaultFPS = time.Second / 10 const (
defaultFPS = time.Second / 10
)
// Spinner is a set of frames used in animating the spinner. // Spinner is a set of frames used in animating the spinner.
type Spinner struct { type Spinner = []string
Frames []string
FPS time.Duration
}
var ( var (
// Some spinners to choose from. You could also make your own. // Some spinners to choose from. You could also make your own.
Line = Spinner{ Line = Spinner([]string{"|", "/", "-", "\\"})
Frames: []string{"|", "/", "-", "\\"}, Dot = Spinner([]string{"⣾ ", "⣽ ", "⣻ ", "", "", "", "⣯ ", "⣷ "})
FPS: time.Second / 10,
}
Dot = Spinner{
Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
FPS: time.Second / 10,
}
MiniDot = Spinner{
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
FPS: time.Second / 12,
}
Jump = Spinner{
Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
FPS: time.Second / 10,
}
Pulse = Spinner{
Frames: []string{"█", "▓", "▒", "░"},
FPS: time.Second / 8,
}
Points = Spinner{
Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
FPS: time.Second / 7,
}
Globe = Spinner{
Frames: []string{"🌍", "🌎", "🌏"},
FPS: time.Second / 4,
}
Moon = Spinner{
Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
FPS: time.Second / 8,
}
Monkey = Spinner{
Frames: []string{"🙈", "🙉", "🙊"},
FPS: time.Second / 3,
}
color = termenv.ColorProfile().Color color = termenv.ColorProfile().Color
) )
@@ -63,19 +26,22 @@ var (
// rather than using Model as a struct literal. // rather than using Model as a struct literal.
type Model struct { type Model struct {
// Spinner settings to use. See type Spinner. // Type is the set of frames to use. See Spinner.
Spinner Spinner Frames Spinner
// ForegroundColor sets the background color of the spinner. It can be // FPS is the speed at which the ticker should tick.
// a hex code or one of the 256 ANSI colors. If the terminal emulator can't FPS time.Duration
// support the color specified it will automatically degrade (per
// github.com/muesli/termenv). // ForegroundColor sets the background color of the spinner. It can be a
// hex code or one of the 256 ANSI colors. If the terminal emulator can't
// doesn't support the color specified it will automatically degrade
// (per github.com/muesli/termenv).
ForegroundColor string ForegroundColor string
// BackgroundColor sets the background color of the spinner. It can be // BackgroundColor sets the background color of the spinner. It can be a
// a hex code or one of the 256 ANSI colors. If the terminal emulator can't // hex code or one of the 256 ANSI colors. If the terminal emulator can't
// support the color specified it will automatically degrade (per // doesn't support the color specified it will automatically degrade
// github.com/muesli/termenv). // (per github.com/muesli/termenv).
BackgroundColor string BackgroundColor string
// MinimumLifetime is the minimum amount of time the spinner can run. Any // MinimumLifetime is the minimum amount of time the spinner can run. Any
@@ -101,49 +67,17 @@ type Model struct {
frame int frame int
startTime time.Time startTime time.Time
tag int
} }
// Start resets resets the spinner start time. For use with MinimumLifetime and // Start resets resets the spinner start time. For use with MinimumLifetime and
// MinimumStartTime. Optional. // MinimumStartTime. Optional.
// //
// This function is optional and generally considered for advanced use only.
// Most of the time your application logic will obviate the need for this
// method.
//
// This is considered experimental and may not appear in future versions of // This is considered experimental and may not appear in future versions of
// this library. // this library.
func (m *Model) Start() { func (m *Model) Start() {
m.startTime = time.Now() m.startTime = time.Now()
} }
// Finish sets the internal timer to a completed state so long as the spinner
// isn't flagged to be showing. If it is showing, finish has no effect. The
// idea here is that you call Finish if your operation has completed and, if
// the spinner isn't showing yet (by virtue of HideFor) then Visible() doesn't
// show the spinner at all.
//
// This is intended to work in conjunction with MinimumLifetime and
// MinimumStartTime, is completely optional.
//
// This function is optional and generally considered for advanced use only.
// Most of the time your application logic will obviate the need for this
// method.
//
// This is considered experimental and may not appear in future versions of
// this library.
func (m *Model) Finish() {
if m.hidden() {
m.startTime = time.Time{}
}
}
// advancedMode returns whether or not the user is making use of HideFor and
// MinimumLifetime properties.
func (m Model) advancedMode() bool {
return m.HideFor > 0 && m.MinimumLifetime > 0
}
// hidden returns whether or not Model.HideFor is in effect. // hidden returns whether or not Model.HideFor is in effect.
func (m Model) hidden() bool { func (m Model) hidden() bool {
if m.startTime.IsZero() { if m.startTime.IsZero() {
@@ -155,10 +89,12 @@ func (m Model) hidden() bool {
return m.startTime.Add(m.HideFor).After(time.Now()) return m.startTime.Add(m.HideFor).After(time.Now())
} }
// finished returns whether the minimum lifetime of this spinner has been // finished returns whether Model.MinimumLifetimeReached has been met.
// exceeded.
func (m Model) finished() bool { func (m Model) finished() bool {
if m.startTime.IsZero() || m.MinimumLifetime == 0 { if m.startTime.IsZero() {
return true
}
if m.MinimumLifetime == 0 {
return true return true
} }
return m.startTime.Add(m.HideFor).Add(m.MinimumLifetime).Before(time.Now()) return m.startTime.Add(m.HideFor).Add(m.MinimumLifetime).Before(time.Now())
@@ -166,13 +102,13 @@ func (m Model) finished() bool {
// Visible returns whether or not the view should be rendered. Works in // Visible returns whether or not the view should be rendered. Works in
// conjunction with Model.HideFor and Model.MinimumLifetimeReached. You should // conjunction with Model.HideFor and Model.MinimumLifetimeReached. You should
// use this method directly to determine whether or not to render this view in // use this message directly to determine whether or not to render this view in
// the parent view and whether to continue sending spin messaging in the // the parent view and whether to continue sending spin messaging in the
// parent update function. // parent update function.
// //
// This function is optional and generally considered for advanced use only. // Also note that using this function is optional and generally considered for
// Most of the time your application logic will obviate the need for this // advanced use only. Most of the time your application logic will determine
// method. // whether or not this view should be used.
// //
// This is considered experimental and may not appear in future versions of // This is considered experimental and may not appear in future versions of
// this library. // this library.
@@ -182,78 +118,55 @@ func (m Model) Visible() bool {
// NewModel returns a model with default values. // NewModel returns a model with default values.
func NewModel() Model { func NewModel() Model {
return Model{Spinner: Line} return Model{
Frames: Line,
FPS: defaultFPS,
}
} }
// TickMsg indicates that the timer has ticked and we should render a frame. // TickMsg indicates that the timer has ticked and we should render a frame.
type TickMsg struct { type TickMsg struct {
Time time.Time Time time.Time
tag int
} }
// Update is the Tea update function. This will advance the spinner one frame // Update is the Tea update function. This will advance the spinner one frame
// every time it's called, regardless the message passed, so be sure the logic // every time it's called, regardless the message passed, so be sure the logic
// is setup so as not to call this Update needlessly. // is setup so as not to call this Update needlessly.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg := msg.(type) { if _, ok := msg.(TickMsg); ok {
case TickMsg:
// If a tag is set, and it's not the one we expect, reject the message.
// This prevents the spinner from receiving too many messages and
// this spinning too fast.
if msg.tag > 0 && msg.tag != m.tag {
return m, nil
}
m.frame++ m.frame++
if m.frame >= len(m.Spinner.Frames) { if m.frame >= len(m.Frames) {
m.frame = 0 m.frame = 0
} }
return m, Tick(m)
m.tag++
return m, m.tick(m.tag)
default:
return m, nil
} }
return m, nil
} }
// View renders the model's view. // View renders the model's view.
func (m Model) View() string { func View(model Model) string {
if m.frame >= len(m.Spinner.Frames) { if model.frame >= len(model.Frames) {
return "(error)" return "error"
} }
frame := m.Spinner.Frames[m.frame] frame := model.Frames[model.frame]
// If we're using the fine-grained hide/show spinner rules and those rules if model.ForegroundColor != "" || model.BackgroundColor != "" {
// deem that the spinner should be hidden, draw an empty space in place of
// the spinner.
if m.advancedMode() && !m.Visible() {
frame = strings.Repeat(" ", ansi.PrintableRuneWidth(frame))
}
if m.ForegroundColor != "" || m.BackgroundColor != "" {
return termenv. return termenv.
String(frame). String(frame).
Foreground(color(m.ForegroundColor)). Foreground(color(model.ForegroundColor)).
Background(color(m.BackgroundColor)). Background(color(model.BackgroundColor)).
String() String()
} }
return frame return frame
} }
// Tick is the command used to advance the spinner one frame. Use this command // Tick is the command used to advance the spinner one frame.
// to effectively start the spinner. func Tick(m Model) tea.Cmd {
func Tick() tea.Msg { return tea.Tick(m.FPS, func(t time.Time) tea.Msg {
return TickMsg{Time: time.Now()}
}
func (m Model) tick(tag int) tea.Cmd {
return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {
return TickMsg{ return TickMsg{
Time: t, Time: t,
tag: tag,
} }
}) })
} }

View File

@@ -67,6 +67,7 @@ type Model struct {
Cursor string Cursor string
BlinkSpeed time.Duration BlinkSpeed time.Duration
TextColor string TextColor string
FocusedTextColor string
BackgroundColor string BackgroundColor string
PlaceholderColor string PlaceholderColor string
CursorColor string CursorColor string
@@ -114,6 +115,7 @@ func NewModel() Model {
Placeholder: "", Placeholder: "",
BlinkSpeed: defaultBlinkSpeed, BlinkSpeed: defaultBlinkSpeed,
TextColor: "", TextColor: "",
FocusedTextColor: "205",
PlaceholderColor: "240", PlaceholderColor: "240",
CursorColor: "", CursorColor: "",
EchoCharacter: '*', EchoCharacter: '*',
@@ -182,15 +184,19 @@ func (m Model) Focused() bool {
} }
// Focus sets the focus state on the model. // Focus sets the focus state on the model.
func (m *Model) Focus() { func (m Model) Focus() tea.Model {
m.focus = true m.focus = true
m.blink = m.cursorMode == cursorHide // show the cursor unless we've explicitly hidden it m.blink = m.cursorMode == cursorHide // show the cursor unless we've explicitly hidden it
return m
} }
// Blur removes the focus state on the model. // Blur removes the focus state on the model.
func (m *Model) Blur() { func (m Model) Blur() tea.Model {
m.focus = false m.focus = false
m.blink = true m.blink = true
return m
} }
// Reset sets the input to its default state with no input. Returns whether // Reset sets the input to its default state with no input. Returns whether
@@ -450,8 +456,12 @@ func (m Model) echoTransform(v string) string {
} }
} }
func (m Model) Init() tea.Cmd {
return Blink
}
// Update is the Bubble Tea update loop. // Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if !m.focus { if !m.focus {
m.blink = true m.blink = true
return m, nil return m, nil
@@ -508,17 +518,17 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.offset = 0 m.offset = 0
case tea.KeyCtrlV: // ^V paste case tea.KeyCtrlV: // ^V paste
return m, Paste return m, Paste
case tea.KeyRunes: // input regular characters case tea.KeyRune: // input a regular character
if msg.Alt && len(msg.Runes) == 1 { if msg.Alt {
if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor if msg.Rune == 'd' { // alt+d, delete word right of cursor
resetBlink = m.deleteWordRight() resetBlink = m.deleteWordRight()
break break
} }
if msg.Runes[0] == 'b' { // alt+b, back one word if msg.Rune == 'b' { // alt+b, back one word
resetBlink = m.wordLeft() resetBlink = m.wordLeft()
break break
} }
if msg.Runes[0] == 'f' { // alt+f, forward one word if msg.Rune == 'f' { // alt+f, forward one word
resetBlink = m.wordRight() resetBlink = m.wordRight()
break break
} }
@@ -526,8 +536,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// Input a regular character // Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit { if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
m.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...) m.value = append(m.value[:m.pos], append([]rune{msg.Rune}, m.value[m.pos:]...)...)
resetBlink = m.SetCursor(m.pos + len(msg.Runes)) resetBlink = m.SetCursor(m.pos + 1)
} }
} }
@@ -560,9 +570,14 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// View renders the textinput in its current state. // View renders the textinput in its current state.
func (m Model) View() string { func (m Model) View() string {
prompt := termenv.String(m.Prompt)
if m.focus {
prompt = prompt.Foreground(color(m.FocusedTextColor))
}
// Placeholder text // Placeholder text
if len(m.value) == 0 && m.Placeholder != "" { if len(m.value) == 0 && m.Placeholder != "" {
return m.placeholderView() return prompt.String() + m.placeholderView()
} }
value := m.value[m.offset:m.offsetRight] value := m.value[m.offset:m.offsetRight]
@@ -590,7 +605,7 @@ func (m Model) View() string {
) )
} }
return m.Prompt + v return prompt.String() + v
} }
// placeholderView returns the prompt and placeholder view, if any. // placeholderView returns the prompt and placeholder view, if any.
@@ -610,7 +625,7 @@ func (m Model) placeholderView() string {
// The rest of the placeholder text // The rest of the placeholder text
v += m.colorPlaceholder(p[1:]) v += m.colorPlaceholder(p[1:])
return m.Prompt + v return v
} }
// cursorView styles the cursor. // cursorView styles the cursor.

View File

@@ -7,7 +7,9 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
const spacebar = " " const (
spacebar = " "
)
// MODEL // MODEL
@@ -267,7 +269,7 @@ func ViewDown(m Model, lines []string) tea.Cmd {
} }
// ViewUp is a high performance command the moves the viewport down by a given // ViewUp is a high performance command the moves the viewport down by a given
// number of lines height. Use Model.ViewUp to get the lines that should be // number of lines height. Use Model.ViewDown to get the lines that should be
// rendered. // rendered.
func ViewUp(m Model, lines []string) tea.Cmd { func ViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 { if len(lines) == 0 {
@@ -281,7 +283,7 @@ func ViewUp(m Model, lines []string) tea.Cmd {
// Update runs the update loop with default keybindings similar to popular // Update runs the update loop with default keybindings similar to popular
// pagers. To define your own keybindings use the methods on Model (i.e. // pagers. To define your own keybindings use the methods on Model (i.e.
// Model.LineDown()) and define your own update function. // Model.LineDown()) and define your own update function.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
@@ -352,7 +354,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// VIEW // VIEW
// View renders the viewport into a string. // View renders the viewport into a string.
func (m Model) View() string { func View(m Model) string {
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
// Just send newlines since we're doing to be rendering the actual // Just send newlines since we're doing to be rendering the actual
// content seprately. We still need send something that equals the // content seprately. We still need send something that equals the