Rename to Bubbles + copy over components from Bubble Tea + remove examples

This commit is contained in:
Christian Rocha 2020-05-25 19:57:58 -04:00
parent a72bf9128d
commit 87c7cd778f
No known key found for this signature in database
GPG Key ID: D6CC7A16E5878018
11 changed files with 505 additions and 377 deletions

1
bubbles.go Normal file
View File

@ -0,0 +1 @@
package bubbles

View File

@ -1,11 +0,0 @@
module examples
go 1.13
replace github.com/charmbracelet/teaparty => ../
require (
github.com/charmbracelet/tea v0.3.0
github.com/charmbracelet/teaparty v0.0.0-20200212224515-b4d35fd52906
github.com/muesli/termenv v0.5.2
)

View File

@ -1,32 +0,0 @@
github.com/charmbracelet/tea v0.0.0-20200220032354-23432f30dd46 h1:ZRxnZ9WA8+DX7IpIrUjaAzue12wCA8bHgDWSrqARvGY=
github.com/charmbracelet/tea v0.0.0-20200220032354-23432f30dd46/go.mod h1:96S8RyGdd7HfPcgDC8XqPKe+eH/1etFkw6lfhQCesgU=
github.com/charmbracelet/tea v0.2.0 h1:PvoNZa2N5HeHnUELfEmZxLO+gmDgjNcrDLNHUMsSuXM=
github.com/charmbracelet/tea v0.2.0/go.mod h1:lADjwO2mMub9qvXSCA9vAkabVWO0HeUrv4uO/lG3C+k=
github.com/charmbracelet/tea v0.3.0 h1:W5F1x/IYeSCKpZl3/hM3Mn5v2KAagckabDFhhzh5sIE=
github.com/charmbracelet/tea v0.3.0/go.mod h1:uA/DUzCuyIZ1NFyAdCz6k+gF8lspujo6ZvoavcSsLCM=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/muesli/termenv v0.4.1-0.20200131124310-936567584c3e h1:6exaizu60WAeTP998SgBox6eaSs6rT36fR/0ebsrnOQ=
github.com/muesli/termenv v0.4.1-0.20200131124310-936567584c3e/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
github.com/muesli/termenv v0.4.1-0.20200225030536-aeb2025df636 h1:E8Ak48QMa0KO1r/IaPrpUxD+mEcOXB+4Q1ySBx04P6k=
github.com/muesli/termenv v0.4.1-0.20200225030536-aeb2025df636/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
github.com/muesli/termenv v0.5.0 h1:7uYkBUJ2EE+CdWlBoYzO70I/cq/r3bOm4a40/lvmiAU=
github.com/muesli/termenv v0.5.0/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
github.com/muesli/termenv v0.5.2 h1:N1Y1dHRtx6OizOgaIQXd8SkJl4T/cCOV+YyWXiuLUEA=
github.com/muesli/termenv v0.5.2/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479 h1:LhLiKguPgZL+Tglay4GhVtfF0kb8cvOJ0dHTCBO8YNI=
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200430202703-d923437fa56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -1,202 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/charmbracelet/tea"
input "github.com/charmbracelet/teaparty/textinput"
te "github.com/muesli/termenv"
)
var (
color = te.ColorProfile().Color
focusedText = "205"
focusedPrompt = te.String("> ").Foreground(color("205")).String()
blurredPrompt = "> "
focusedSubmitButton = "[ " + te.String("Submit").Foreground(color("205")).String() + " ]"
blurredSubmitButton = "[ " + te.String("Submit").Foreground(color("240")).String() + " ]"
)
func main() {
if err := tea.NewProgram(
initialize,
update,
view,
subscriptions,
).Start(); err != nil {
fmt.Printf("could not start program: %s\n", err)
os.Exit(1)
}
}
type Model struct {
index int
nameInput input.Model
nickNameInput input.Model
emailInput input.Model
submitButton string
}
func initialize() (tea.Model, tea.Cmd) {
name := input.NewModel()
name.Placeholder = "Name"
name.Focus()
name.Prompt = focusedPrompt
name.TextColor = focusedText
nickName := input.NewModel()
nickName.Placeholder = "Nickname"
nickName.Prompt = blurredPrompt
email := input.NewModel()
email.Placeholder = "Email"
email.Prompt = blurredPrompt
return Model{0, name, nickName, email, blurredSubmitButton}, nil
}
func update(msg tea.Msg, model tea.Model) (tea.Model, tea.Cmd) {
m, ok := model.(Model)
if !ok {
panic("could not perform assertion on model")
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
// Cycle between inputs
case "tab":
fallthrough
case "shift+tab":
fallthrough
case "enter":
fallthrough
case "up":
fallthrough
case "down":
inputs := []input.Model{
m.nameInput,
m.nickNameInput,
m.emailInput,
}
s := msg.String()
// Did the user press enter while the submit button was focused?
// If so, exit.
if s == "enter" && m.index == len(inputs) {
return m, tea.Quit
}
// Cycle indexes
if s == "up" || s == "shift+tab" {
m.index--
} else {
m.index++
}
if m.index > len(inputs) {
m.index = 0
} else if m.index < 0 {
m.index = len(inputs)
}
for i := 0; i <= len(inputs)-1; i++ {
if i == m.index {
// Focused input
inputs[i].Focus()
inputs[i].Prompt = focusedPrompt
inputs[i].TextColor = focusedText
continue
}
// Blurred input
inputs[i].Blur()
inputs[i].Prompt = blurredPrompt
inputs[i].TextColor = ""
}
m.nameInput = inputs[0]
m.nickNameInput = inputs[1]
m.emailInput = inputs[2]
if m.index == len(inputs) {
m.submitButton = focusedSubmitButton
} else {
m.submitButton = blurredSubmitButton
}
return m, nil
default:
// Handle character input
m = updateInputs(msg, m)
return m, nil
}
default:
// Handle blinks
m = updateInputs(msg, m)
return m, nil
}
}
func updateInputs(msg tea.Msg, m Model) Model {
m.nameInput, _ = input.Update(msg, m.nameInput)
m.nickNameInput, _ = input.Update(msg, m.nickNameInput)
m.emailInput, _ = input.Update(msg, m.emailInput)
return m
}
func subscriptions(model tea.Model) tea.Subs {
m, ok := model.(Model)
if !ok {
return nil
}
// It's a little hacky, but we're using the subscription from one
// input element to handle the blinking for all elements. It doesn't
// have to be this way, we're just feeling a bit lazy at the moment.
inputSub, err := input.MakeSub(m.nameInput)
if err != nil {
return nil
}
return tea.Subs{
// It's a little hacky, but we're using the subscription from one
// input element to handle the blinking for all elements. It doesn't
// have to be this way, we're just feeling a bit lazy at the moment.
"blink": inputSub,
}
}
func view(model tea.Model) string {
m, ok := model.(Model)
if !ok {
return "[error] could not perform assertion on model"
}
s := "\n"
inputs := []string{
input.View(m.nameInput),
input.View(m.nickNameInput),
input.View(m.emailInput),
}
for i := 0; i < len(inputs); i++ {
s += inputs[i]
if i < len(inputs)-1 {
s += "\n"
}
}
s += "\n\n" + m.submitButton + "\n"
return s
}

7
go.mod
View File

@ -1,11 +1,8 @@
module github.com/charmbracelet/teaparty module github.com/charmbracelet/bubbles
go 1.13 go 1.13
require ( require (
github.com/charmbracelet/tea v0.3.0 github.com/charmbracelet/bubbletea v0.6.4-0.20200525234836-3b8b011b5a26
github.com/muesli/termenv v0.5.2 github.com/muesli/termenv v0.5.2
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f // indirect
) )
replace github.com/charmbracelet/tea => ../tea

25
go.sum
View File

@ -1,21 +1,22 @@
github.com/charmbracelet/tea v0.2.0 h1:PvoNZa2N5HeHnUELfEmZxLO+gmDgjNcrDLNHUMsSuXM= github.com/charmbracelet/bubbletea v0.6.4-0.20200525234836-3b8b011b5a26 h1:CUqznYQaIkZzgsrBe7QKIudrr+Wvtbe8t+K6ygEKcPc=
github.com/charmbracelet/tea v0.2.0/go.mod h1:lADjwO2mMub9qvXSCA9vAkabVWO0HeUrv4uO/lG3C+k= github.com/charmbracelet/bubbletea v0.6.4-0.20200525234836-3b8b011b5a26/go.mod h1:BTzHOUvUlKecQz7ZB8NgPRWi2Z8NRCV04qwyFOfO1Kk=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
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/muesli/termenv v0.5.0 h1:7uYkBUJ2EE+CdWlBoYzO70I/cq/r3bOm4a40/lvmiAU=
github.com/muesli/termenv v0.5.0/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
github.com/muesli/termenv v0.5.2 h1:N1Y1dHRtx6OizOgaIQXd8SkJl4T/cCOV+YyWXiuLUEA= github.com/muesli/termenv v0.5.2 h1:N1Y1dHRtx6OizOgaIQXd8SkJl4T/cCOV+YyWXiuLUEA=
github.com/muesli/termenv v0.5.2/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI= github.com/muesli/termenv v0.5.2/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0= github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 h1:pd4YKIqCB0U7O2I4gWHgEUA2mCEOENmco0l/bM957bU=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCvdOWbLqTHhJiYV414CURZJba6L8qA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200430202703-d923437fa56d h1:xmcims+WSpFuY56YEzkKF6IMDxYAVDRipkQRJfXUBZk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20200430202703-d923437fa56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -1,27 +1,27 @@
// package pager provides a Tea package for calulating pagination and rendering // package paginator provides a Bubble Tea package for calulating pagination
// pagination info. Note that this package does not render actual pages: it's // and rendering pagination info. Note that this package does not render actual
// purely for handling keystrokes related to pagination, and rendering // pages: it's purely for handling keystrokes related to pagination, and
// pagination status. // rendering pagination status.
package pager package paginator
import ( import (
"fmt" "fmt"
"github.com/charmbracelet/tea" tea "github.com/charmbracelet/bubbletea"
) )
// PagerType specifies the way we render pagination // Type specifies the way we render pagination.
type PagerType int type Type int
// Pagination rendering options // Pagination rendering options
const ( const (
Arabic PagerType = iota Arabic Type = iota
Dots Dots
) )
// Model is the Tea model for this user interface // Model is the Tea model for this user interface.
type Model struct { type Model struct {
Type PagerType Type Type
Page int Page int
PerPage int PerPage int
TotalPages int TotalPages int
@ -44,13 +44,13 @@ func (m *Model) SetTotalPages(items int) int {
} }
n := items / m.PerPage n := items / m.PerPage
if items%m.PerPage > 0 { if items%m.PerPage > 0 {
n += 1 n++
} }
m.TotalPages = n m.TotalPages = n
return n return n
} }
// ItemsOnPage is a helper function fro returning the numer of items on the // ItemsOnPage is a helper function for returning the numer of items on the
// current page given the total numer of items passed as an argument. // current page given the total numer of items passed as an argument.
func (m Model) ItemsOnPage(totalItems int) int { func (m Model) ItemsOnPage(totalItems int) int {
start, end := m.GetSliceBounds(totalItems) start, end := m.GetSliceBounds(totalItems)
@ -82,12 +82,17 @@ func (m *Model) PrevPage() {
// NextPage is a helper function for navigating one page forward. It will not // NextPage is a helper function for navigating one page forward. It will not
// page beyond the last page (i.e. totalPages - 1). // page beyond the last page (i.e. totalPages - 1).
func (m *Model) NextPage() { func (m *Model) NextPage() {
if m.Page < m.TotalPages-1 { if !m.OnLastPage() {
m.Page++ m.Page++
} }
} }
// NewModel creates a new model with defaults // LastPage returns whether or not we're on the last page.
func (m Model) OnLastPage() bool {
return m.Page == m.TotalPages-1
}
// NewModel creates a new model with defaults.
func NewModel() Model { func NewModel() Model {
return Model{ return Model{
Type: Arabic, Type: Arabic,
@ -104,7 +109,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 Update(msg tea.Msg, m Model) (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:
@ -145,7 +150,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
return m, nil return m, nil
} }
// View renders the pagination to a string // View renders the pagination to a string.
func View(model tea.Model) string { func View(model tea.Model) string {
m, ok := model.(Model) m, ok := model.(Model)
if !ok { if !ok {

View File

@ -1,14 +1,13 @@
package spinner package spinner
import ( import (
"errors"
"time" "time"
"github.com/charmbracelet/tea" tea "github.com/charmbracelet/bubbletea"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
// Spinner denotes a type of spinner // Spinner is a set of frames used in animating the spinner.
type Spinner = int type Spinner = int
// Available types of spinners // Available types of spinners
@ -17,6 +16,10 @@ const (
Dot Dot
) )
const (
defaultFPS = 9
)
var ( var (
// Spinner frames // Spinner frames
spinners = map[Spinner][]string{ spinners = map[Spinner][]string{
@ -24,49 +27,66 @@ var (
Dot: {"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "}, Dot: {"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
} }
assertionErr = errors.New("could not perform assertion on model to what the spinner expects. are you sure you passed the right value?")
color = termenv.ColorProfile().Color color = termenv.ColorProfile().Color
) )
// Model contains the state for the spinner. Use NewModel to create new models // Model contains the state for the spinner. Use NewModel to create new models
// rather than using Model as a struct literal. // rather than using Model as a struct literal.
type Model struct { type Model struct {
Type Spinner
FPS int // Type is the set of frames to use. See Spinner.
Type Spinner
// FPS is the speed at which the ticker should tick
FPS int
// 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 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).
BackgroundColor string BackgroundColor string
// CustomMsgFunc can be used to a custom message on tick. This can be
// useful when you have spinners in different parts of your application and
// want to differentiate between the messages for clarity and simplicity.
// If nil, this setting is ignored.
CustomMsgFunc func() tea.Msg
frame int frame int
} }
// NewModel returns a model with default values // NewModel returns a model with default values.
func NewModel() Model { func NewModel() Model {
return Model{ return Model{
Type: Line, Type: Line,
FPS: 9, FPS: defaultFPS,
frame: 0,
} }
} }
// 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 time.Time type TickMsg struct{}
// Update is the Tea update function // 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
// is setup so as not to call this Update needlessly.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg.(type) { m.frame++
case TickMsg: if m.frame >= len(spinners[m.Type]) {
m.frame++ m.frame = 0
if m.frame >= len(spinners[m.Type]) {
m.frame = 0
}
return m, nil
default:
return m, nil
} }
if m.CustomMsgFunc != nil {
return m, Tick(m)
}
return m, Tick(m)
} }
// View renders the model's view // View renders the model's view.
func View(model Model) string { func View(model Model) string {
s := spinners[model.Type] s := spinners[model.Type]
if model.frame >= len(s) { if model.frame >= len(s) {
@ -86,15 +106,13 @@ func View(model Model) string {
return str return str
} }
// GetSub creates the subscription that allows the spinner to spin. Remember // Tick is the command used to advance the spinner one frame.
// that you need to execute this function in order to get the subscription func Tick(model Model) tea.Cmd {
// you'll need. return func() tea.Msg {
func MakeSub(model tea.Model) (tea.Sub, error) { time.Sleep(time.Second / time.Duration(model.FPS))
m, ok := model.(Model) if model.CustomMsgFunc != nil {
if !ok { return model.CustomMsgFunc()
return nil, assertionErr }
return TickMsg{}
} }
return tea.Tick(time.Second/time.Duration(m.FPS), func(t time.Time) tea.Msg {
return TickMsg(t)
}), nil
} }

View File

@ -1 +0,0 @@
package teaparty

View File

@ -1,13 +1,18 @@
package textinput package textinput
import ( import (
"errors" "strings"
"time" "time"
"unicode"
"github.com/charmbracelet/tea" tea "github.com/charmbracelet/bubbletea"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
const (
defaultBlinkSpeed = time.Millisecond * 600
)
var ( var (
// color is a helper for returning colors // color is a helper for returning colors
color func(s string) termenv.Color = termenv.ColorProfile().Color color func(s string) termenv.Color = termenv.ColorProfile().Color
@ -18,15 +23,15 @@ var (
// this text input. // this text input.
type ErrMsg error type ErrMsg error
// Model is the Tea model for this text input element // Model is the Tea model for this text input element.
type Model struct { type Model struct {
Err error Err error
Prompt string Prompt string
Value string
Cursor string Cursor string
BlinkSpeed time.Duration BlinkSpeed time.Duration
Placeholder string Placeholder string
TextColor string TextColor string
BackgroundColor string
PlaceholderColor string PlaceholderColor string
CursorColor string CursorColor string
@ -34,71 +39,199 @@ type Model struct {
// accept. If 0 or less, there's no limit. // accept. If 0 or less, there's no limit.
CharLimit int CharLimit int
// Width is the maximum number of characters that can be displayed at once.
// It essentially treats the text field like a horizontally scrolling
// viewport. If 0 or less this setting is ignored.
Width int
// Underlying text value
value string
// Focus indicates whether user input focus should be on this input // Focus indicates whether user input focus should be on this input
// component. When false, don't blink and ignore keyboard input. // component. When false, don't blink and ignore keyboard input.
focus bool focus bool
// Cursor blink state
blink bool blink bool
pos int
// Cursor position
pos int
// Used to emulate a viewport when width is set and the content is
// overflowing
offset int
} }
// Focused returns the focus state on the model // SetValue sets the value of the text input.
func (m *Model) SetValue(s string) {
if m.CharLimit > 0 && len(s) > m.CharLimit {
m.value = s[:m.CharLimit]
} else {
m.value = s
}
if m.pos > len(m.value) {
m.pos = len(m.value)
}
m.handleOverflow()
}
// Value returns the value of the text input.
func (m Model) Value() string {
return m.value
}
// Cursor start moves the cursor to the given position. If the position is out
// of bounds the cursor will be moved to the start or end accordingly.
func (m *Model) SetCursor(pos int) {
m.pos = max(0, min(len(m.value), pos))
m.handleOverflow()
}
// CursorStart moves the cursor to the start of the field.
func (m *Model) CursorStart() {
m.pos = 0
m.handleOverflow()
}
// CursorEnd moves the cursor to the end of the field.
func (m *Model) CursorEnd() {
m.pos = len(m.value)
m.handleOverflow()
}
// Focused returns the focus state on the model.
func (m Model) Focused() bool { func (m Model) Focused() bool {
return m.focus return m.focus
} }
// Focus sets the focus state on the model // Focus sets the focus state on the model.
func (m *Model) Focus() { func (m *Model) Focus() {
m.focus = true m.focus = true
m.blink = false m.blink = false
} }
// Blur removes the focus state on the model // Blur removes the focus state on the model.
func (m *Model) Blur() { func (m *Model) Blur() {
m.focus = false m.focus = false
m.blink = true m.blink = true
} }
// Reset sets the input to its default state with no input.
func (m *Model) Reset() {
m.value = ""
m.offset = 0
m.pos = 0
m.blink = false
}
// If a max width is defined, perform some logic to treat the visible area
// as a horizontally scrolling viewport.
func (m *Model) handleOverflow() {
if m.Width > 0 {
overflow := max(0, len(m.value)-m.Width)
if overflow > 0 && m.pos < m.offset {
m.offset = max(0, min(len(m.value), m.pos))
} else if overflow > 0 && m.pos >= m.offset+m.Width {
m.offset = max(0, m.pos-m.Width)
}
}
}
// colorText colorizes a given string according to the TextColor value of the // colorText colorizes a given string according to the TextColor value of the
// model // model.
func (m *Model) colorText(s string) string { func (m *Model) colorText(s string) string {
return termenv. return termenv.
String(s). String(s).
Foreground(color(m.TextColor)). Foreground(color(m.TextColor)).
Background(color(m.BackgroundColor)).
String() String()
} }
// colorPlaceholder colorizes a given string according to the TextColor value // colorPlaceholder colorizes a given string according to the TextColor value
// of the model // of the model.
func (m *Model) colorPlaceholder(s string) string { func (m *Model) colorPlaceholder(s string) string {
return termenv. return termenv.
String(s). String(s).
Foreground(color(m.PlaceholderColor)). Foreground(color(m.PlaceholderColor)).
Background(color(m.BackgroundColor)).
String() String()
} }
// CursorBlinkMsg is sent when the cursor should alternate it's blinking state func (m *Model) wordLeft() {
type CursorBlinkMsg struct{} if m.pos == 0 || len(m.value) == 0 {
return
}
// NewModel creates a new model with default settings i := m.pos - 1
for i >= 0 {
if unicode.IsSpace(rune(m.value[i])) {
m.pos--
i--
} else {
break
}
}
for i >= 0 {
if !unicode.IsSpace(rune(m.value[i])) {
m.pos--
i--
} else {
break
}
}
}
func (m *Model) wordRight() {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
}
i := m.pos
for i < len(m.value) {
if unicode.IsSpace(rune(m.value[i])) {
m.pos++
i++
} else {
break
}
}
for i < len(m.value) {
if !unicode.IsSpace(rune(m.value[i])) {
m.pos++
i++
} else {
break
}
}
}
// BlinkMsg is sent when the cursor should alternate it's blinking state.
type BlinkMsg struct{}
// NewModel creates a new model with default settings.
func NewModel() Model { func NewModel() Model {
return Model{ return Model{
Prompt: "> ", Prompt: "> ",
Value: "", BlinkSpeed: defaultBlinkSpeed,
BlinkSpeed: time.Millisecond * 600,
Placeholder: "", Placeholder: "",
TextColor: "", TextColor: "",
PlaceholderColor: "240", PlaceholderColor: "240",
CursorColor: "", CursorColor: "",
CharLimit: 0, CharLimit: 0,
value: "",
focus: false, focus: false,
blink: true, blink: true,
pos: 0, pos: 0,
} }
} }
// Update is the Tea update loop // Update is the Tea update loop.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
if !m.focus { if !m.focus {
m.blink = true m.blink = true
@ -106,74 +239,84 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
} }
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.Type { switch msg.Type {
case tea.KeyBackspace: case tea.KeyBackspace:
fallthrough fallthrough
case tea.KeyDelete: case tea.KeyDelete:
if len(m.Value) > 0 { if len(m.value) > 0 {
m.Value = m.Value[:m.pos-1] + m.Value[m.pos:] m.value = m.value[:m.pos-1] + m.value[m.pos:]
m.pos-- m.pos--
} }
return m, nil
case tea.KeyLeft: case tea.KeyLeft:
if msg.Alt { // alt+left arrow, back one word
m.wordLeft()
break
}
if m.pos > 0 { if m.pos > 0 {
m.pos-- m.pos--
} }
return m, nil
case tea.KeyRight: case tea.KeyRight:
if m.pos < len(m.Value) { if msg.Alt { // alt+right arrow, forward one word
m.wordRight()
break
}
if m.pos < len(m.value) {
m.pos++ m.pos++
} }
return m, nil
case tea.KeyCtrlF: // ^F, forward one character case tea.KeyCtrlF: // ^F, forward one character
fallthrough fallthrough
case tea.KeyCtrlB: // ^B, back one charcter case tea.KeyCtrlB: // ^B, back one charcter
fallthrough fallthrough
case tea.KeyCtrlA: // ^A, go to beginning case tea.KeyCtrlA: // ^A, go to beginning
m.pos = 0 m.CursorStart()
return m, nil
case tea.KeyCtrlD: // ^D, delete char under cursor case tea.KeyCtrlD: // ^D, delete char under cursor
if len(m.Value) > 0 && m.pos < len(m.Value) { if len(m.value) > 0 && m.pos < len(m.value) {
m.Value = m.Value[:m.pos] + m.Value[m.pos+1:] m.value = m.value[:m.pos] + m.value[m.pos+1:]
} }
return m, nil
case tea.KeyCtrlE: // ^E, go to end case tea.KeyCtrlE: // ^E, go to end
m.pos = len(m.Value) m.CursorEnd()
return m, nil
case tea.KeyCtrlK: // ^K, kill text after cursor case tea.KeyCtrlK: // ^K, kill text after cursor
m.Value = m.Value[:m.pos] m.value = m.value[:m.pos]
m.pos = len(m.Value) m.pos = len(m.value)
return m, nil
case tea.KeyCtrlU: // ^U, kill text before cursor case tea.KeyCtrlU: // ^U, kill text before cursor
m.Value = m.Value[m.pos:] m.value = m.value[m.pos:]
m.pos = 0 m.pos = 0
return m, nil m.offset = 0
case tea.KeyRune: // input a regular character case tea.KeyRune: // input a regular character
if m.CharLimit <= 0 || len(m.Value) < m.CharLimit {
m.Value = m.Value[:m.pos] + string(msg.Rune) + m.Value[m.pos:] if msg.Alt {
if msg.Rune == 'b' { // alt+b, back one word
m.wordLeft()
break
}
if msg.Rune == 'f' { // alt+f, forward one word
m.wordRight()
break
}
}
// Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
m.value = m.value[:m.pos] + string(msg.Rune) + m.value[m.pos:]
m.pos++ m.pos++
} }
return m, nil
default:
return m, nil
} }
case ErrMsg: case ErrMsg:
m.Err = msg m.Err = msg
return m, nil
case CursorBlinkMsg: case BlinkMsg:
m.blink = !m.blink m.blink = !m.blink
return m, nil return m, Blink(m)
default:
return m, nil
} }
m.handleOverflow()
return m, nil
} }
// View renders the textinput in its current state // View renders the textinput in its current state.
func View(model tea.Model) string { func View(model tea.Model) string {
m, ok := model.(Model) m, ok := model.(Model)
if !ok { if !ok {
@ -181,18 +324,42 @@ func View(model tea.Model) string {
} }
// Placeholder text // Placeholder text
if m.Value == "" && m.Placeholder != "" { if m.value == "" && m.Placeholder != "" {
return placeholderView(m) return placeholderView(m)
} }
v := m.colorText(m.Value[:m.pos]) left := m.offset
right := 0
if m.Width > 0 {
right = min(len(m.value), m.offset+m.Width+1)
} else {
right = len(m.value)
}
value := m.value[left:right]
pos := m.pos - m.offset
if m.pos < len(m.Value) { v := m.colorText(value[:pos])
v += cursorView(string(m.Value[m.pos]), m)
v += m.colorText(m.Value[m.pos+1:]) if pos < len(value) {
v += cursorView(string(value[pos]), m) // cursor and text under it
v += m.colorText(value[pos+1:]) // text after cursor
} else { } else {
v += cursorView(" ", m) v += cursorView(" ", m)
} }
// If a max width and background color were set fill the empty spaces with
// the background color.
if m.Width > 0 && len(m.BackgroundColor) > 0 && len(value) <= m.Width {
padding := m.Width - len(value)
if len(value)+padding <= m.Width && pos < len(value) {
padding++
}
v += strings.Repeat(
termenv.String(" ").Background(color(m.BackgroundColor)).String(),
padding,
)
}
return m.Prompt + v return m.Prompt + v
} }
@ -219,26 +386,42 @@ func placeholderView(m Model) string {
return m.Prompt + v return m.Prompt + v
} }
// cursorView style the cursor // cursorView styles the cursor.
func cursorView(s string, m Model) string { func cursorView(s string, m Model) string {
if m.blink { if m.blink {
if m.TextColor != "" || m.BackgroundColor != "" {
return termenv.String(s).
Foreground(color(m.TextColor)).
Background(color(m.BackgroundColor)).
String()
}
return s return s
} }
return termenv.String(s). return termenv.String(s).
Foreground(color(m.CursorColor)). Foreground(color(m.CursorColor)).
Background(color(m.BackgroundColor)).
Reverse(). Reverse().
String() String()
} }
// MakeSub return a subscription that lets us know when to alternate the // Blink is a command used to time the cursor blinking.
// blinking of the cursor. func Blink(model Model) tea.Cmd {
func MakeSub(model tea.Model) (tea.Sub, error) {
m, ok := model.(Model)
if !ok {
return nil, errors.New("could not assert given model to the model we expected; make sure you're passing as input model")
}
return func() tea.Msg { return func() tea.Msg {
time.Sleep(m.BlinkSpeed) time.Sleep(model.BlinkSpeed)
return CursorBlinkMsg{} return BlinkMsg{}
}, nil }
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
} }

169
viewport/viewport.go Normal file
View File

@ -0,0 +1,169 @@
package viewport
import (
"strings"
tea "github.com/charmbracelet/bubbletea"
)
// MODEL
type Model struct {
Err error
Width int
Height int
Y int
lines []string
}
// Scrollpercent returns the amount scrolled as a float between 0 and 1.
func (m Model) ScrollPercent() float64 {
if m.Height >= len(m.lines) {
return 1.0
}
y := float64(m.Y)
h := float64(m.Height)
t := float64(len(m.lines))
return y / (t - h)
}
// SetContent set the pager's text content.
func (m *Model) SetContent(s string) {
s = strings.Replace(s, "\r\n", "\n", -1) // normalize line endings
m.lines = strings.Split(s, "\n")
}
// NewModel creates a new pager model. Pass the dimensions of the pager.
func NewModel(width, height int) Model {
return Model{
Width: width,
Height: height,
}
}
// ViewDown moves the view down by the number of lines in the viewport.
// Basically, "page down".
func (m *Model) ViewDown() {
m.Y = min(len(m.lines)-m.Height, m.Y+m.Height)
}
// ViewUp moves the view up by one height of the viewport. Basically, "page up".
func (m *Model) ViewUp() {
m.Y = max(0, m.Y-m.Height)
}
// HalfViewUp moves the view up by half the height of the viewport.
func (m *Model) HalfViewUp() {
m.Y = max(0, m.Y-m.Height/2)
}
// HalfViewDown moves the view down by half the height of the viewport.
func (m *Model) HalfViewDown() {
m.Y = min(len(m.lines)-m.Height, m.Y+m.Height/2)
}
// LineDown moves the view up by the given number of lines.
func (m *Model) LineDown(n int) {
m.Y = min(len(m.lines)-m.Height, m.Y+n)
}
// LineDown moves the view down by the given number of lines.
func (m *Model) LineUp(n int) {
m.Y = max(0, m.Y-n)
}
// UPDATE
// Update runs the update loop with default keybindings. To define your own
// keybindings use the methods on Model.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
// Down one page
case "pgdown":
fallthrough
case " ": // spacebar
fallthrough
case "f":
m.ViewDown()
return m, nil
// Up one page
case "pgup":
fallthrough
case "b":
m.ViewUp()
return m, nil
// Down half page
case "d":
m.HalfViewDown()
return m, nil
// Up half page
case "u":
m.HalfViewUp()
return m, nil
// Down one line
case "down":
fallthrough
case "j":
m.LineDown(1)
return m, nil
// Up one line
case "up":
fallthrough
case "k":
m.LineUp(1)
return m, nil
}
}
return m, nil
}
// VIEW
// View renders the viewport into a string.
func View(m Model) string {
if m.Err != nil {
return m.Err.Error()
}
var lines []string
if len(m.lines) > 0 {
top := max(0, m.Y)
bottom := min(len(m.lines), m.Y+m.Height)
lines = m.lines[top:bottom]
}
// Fill empty space with newlines
extraLines := ""
if len(lines) < m.Height {
extraLines = strings.Repeat("\n", m.Height-len(lines))
}
return strings.Join(lines, "\n") + extraLines
}
// ETC
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}