forked from Mirrors/bubbletea
Move components over and update examples
This commit is contained in:
parent
4bf40fbe62
commit
82ddbb8e12
|
@ -6,8 +6,8 @@ replace github.com/charmbracelet/boba => ../
|
|||
|
||||
require (
|
||||
github.com/charmbracelet/boba v0.0.0-00010101000000-000000000000
|
||||
github.com/charmbracelet/tea v0.3.0
|
||||
github.com/charmbracelet/teaparty v0.0.0-20200511213328-a72bf9128d83
|
||||
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
|
||||
github.com/muesli/termenv v0.5.2
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f // indirect
|
||||
)
|
||||
|
|
|
@ -1,11 +1,3 @@
|
|||
github.com/charmbracelet/tea v0.0.0-20200130023737-bb06373836b4 h1:O8IGyYrKQuCwZZ98JP3DvzQCoPiXM5Y2zjwlDY7mOFM=
|
||||
github.com/charmbracelet/tea v0.0.0-20200130023737-bb06373836b4/go.mod h1:UsFFdg04MNbcYi1r2FBtdDEFY07bObaYDKHhE1xZUaQ=
|
||||
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/charmbracelet/teaparty v0.0.0-20200212224515-b4d35fd52906 h1:kcvv+hjb0dJiqhtMXkql5tczxalWMPvKIqwWo7cyhiQ=
|
||||
github.com/charmbracelet/teaparty v0.0.0-20200212224515-b4d35fd52906/go.mod h1:BG6oiwNZL9hB739ZOifRi3ePRGv0nT+kRTfxYLcZj/Y=
|
||||
github.com/charmbracelet/teaparty v0.0.0-20200511213328-a72bf9128d83 h1:ivIS4ze0LLG9yl9L8cnYerh6dWnqhmBqh1ohmrA3I/I=
|
||||
github.com/charmbracelet/teaparty v0.0.0-20200511213328-a72bf9128d83/go.mod h1:rlnGPwUokLHs2rQBiIYg6fpLM5m4DvtE2LwzdvS1wsk=
|
||||
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA=
|
||||
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs=
|
||||
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
|
||||
|
@ -14,16 +6,19 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW
|
|||
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.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/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-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200430202703-d923437fa56d h1:xmcims+WSpFuY56YEzkKF6IMDxYAVDRipkQRJfXUBZk=
|
||||
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=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
|
@ -74,7 +74,7 @@ func view(model boba.Model) string {
|
|||
} else if m.status != 0 {
|
||||
s += fmt.Sprintf("%d %s", m.status, http.StatusText(m.status))
|
||||
}
|
||||
return s
|
||||
return s + "\n"
|
||||
}
|
||||
|
||||
func checkServer() boba.Msg {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
package main
|
||||
|
||||
// A simple program that counts down from 5 and then exits.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/charmbracelet/boba"
|
||||
"github.com/charmbracelet/teaparty/input"
|
||||
input "github.com/charmbracelet/boba/textinput"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
@ -33,8 +31,9 @@ func main() {
|
|||
}
|
||||
|
||||
func initialize() (boba.Model, boba.Cmd) {
|
||||
inputModel := input.DefaultModel()
|
||||
inputModel := input.NewModel()
|
||||
inputModel.Placeholder = "Pikachu"
|
||||
inputModel.Focus()
|
||||
|
||||
return Model{
|
||||
textInput: inputModel,
|
||||
|
@ -60,6 +59,8 @@ func update(msg boba.Msg, model boba.Model) (boba.Model, boba.Cmd) {
|
|||
case boba.KeyCtrlC:
|
||||
fallthrough
|
||||
case boba.KeyEsc:
|
||||
fallthrough
|
||||
case boba.KeyEnter:
|
||||
return m, boba.Quit
|
||||
}
|
||||
|
||||
|
@ -74,13 +75,16 @@ func update(msg boba.Msg, model boba.Model) (boba.Model, boba.Cmd) {
|
|||
}
|
||||
|
||||
func subscriptions(model boba.Model) boba.Subs {
|
||||
m, ok := model.(Model)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
sub, err := input.MakeSub(m.textInput)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return boba.Subs{
|
||||
// We just hand off the subscription to the input component, giving
|
||||
// it the model it expects.
|
||||
"input": func(model boba.Model) boba.Msg {
|
||||
m, _ := model.(Model)
|
||||
return input.Blink(m.textInput)
|
||||
},
|
||||
"input": sub,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,5 +99,5 @@ func view(model boba.Model) string {
|
|||
"What’s your favorite Pokémon?\n\n%s\n\n%s",
|
||||
input.View(m.textInput),
|
||||
"(esc to quit)",
|
||||
)
|
||||
) + "\n"
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/charmbracelet/boba"
|
||||
"github.com/charmbracelet/teaparty/pager"
|
||||
"github.com/charmbracelet/boba/pager"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -54,7 +54,7 @@ func update(msg boba.Msg, model boba.Model) (boba.Model, boba.Cmd) {
|
|||
// to the terminal.
|
||||
func view(model boba.Model) string {
|
||||
m, _ := model.(Model)
|
||||
return fmt.Sprintf("Hi. This program will exit in %d seconds. To quit sooner press any key.", m)
|
||||
return fmt.Sprintf("Hi. This program will exit in %d seconds. To quit sooner press any key.\n", m)
|
||||
}
|
||||
|
||||
// This is a subscription which we setup in NewProgram(). It waits for one
|
||||
|
|
|
@ -2,10 +2,10 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/boba"
|
||||
"github.com/charmbracelet/teaparty/spinner"
|
||||
"github.com/charmbracelet/boba/spinner"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
|
@ -15,6 +15,7 @@ var (
|
|||
|
||||
type Model struct {
|
||||
spinner spinner.Model
|
||||
quitting bool
|
||||
err error
|
||||
}
|
||||
|
||||
|
@ -23,7 +24,8 @@ type errMsg error
|
|||
func main() {
|
||||
p := boba.NewProgram(initialize, update, view, subscriptions)
|
||||
if err := p.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +53,7 @@ func update(msg boba.Msg, model boba.Model) (boba.Model, boba.Cmd) {
|
|||
case "esc":
|
||||
fallthrough
|
||||
case "ctrl+c":
|
||||
m.quitting = true
|
||||
return m, boba.Quit
|
||||
default:
|
||||
return m, nil
|
||||
|
@ -70,7 +73,7 @@ func update(msg boba.Msg, model boba.Model) (boba.Model, boba.Cmd) {
|
|||
func view(model boba.Model) string {
|
||||
m, ok := model.(Model)
|
||||
if !ok {
|
||||
return "could not perform assertion on model in view"
|
||||
return "could not perform assertion on model in view\n"
|
||||
}
|
||||
if m.err != nil {
|
||||
return m.err.Error()
|
||||
|
@ -79,7 +82,11 @@ func view(model boba.Model) string {
|
|||
String(spinner.View(m.spinner)).
|
||||
Foreground(color.Color("205")).
|
||||
String()
|
||||
return fmt.Sprintf("\n\n %s Loading forever...press q to quit\n\n", s)
|
||||
str := fmt.Sprintf("\n\n %s Loading forever...press q to quit\n\n", s)
|
||||
if m.quitting {
|
||||
return str + "\n"
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func subscriptions(model boba.Model) boba.Subs {
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/boba"
|
||||
input "github.com/charmbracelet/boba/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 := boba.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() (boba.Model, boba.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 boba.Msg, model boba.Model) (boba.Model, boba.Cmd) {
|
||||
m, ok := model.(Model)
|
||||
if !ok {
|
||||
panic("could not perform assertion on model")
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case boba.KeyMsg:
|
||||
switch msg.String() {
|
||||
|
||||
case "ctrl+c":
|
||||
return m, boba.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, boba.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 boba.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 boba.Model) boba.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 boba.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 boba.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
|
||||
}
|
|
@ -8,12 +8,12 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/tea"
|
||||
"github.com/charmbracelet/boba"
|
||||
"github.com/fogleman/ease"
|
||||
)
|
||||
|
||||
func main() {
|
||||
p := tea.NewProgram(
|
||||
p := boba.NewProgram(
|
||||
initialize,
|
||||
update,
|
||||
view,
|
||||
|
@ -28,13 +28,13 @@ func main() {
|
|||
|
||||
type tickMsg time.Time
|
||||
|
||||
func newTickMsg(t time.Time) tea.Msg {
|
||||
func newTickMsg(t time.Time) boba.Msg {
|
||||
return tickMsg(t)
|
||||
}
|
||||
|
||||
type frameMsg time.Time
|
||||
|
||||
func newFrameMsg(t time.Time) tea.Msg {
|
||||
func newFrameMsg(t time.Time) boba.Msg {
|
||||
return frameMsg(t)
|
||||
}
|
||||
|
||||
|
@ -52,27 +52,27 @@ type Model struct {
|
|||
|
||||
// INIT
|
||||
|
||||
func initialize() (tea.Model, tea.Cmd) {
|
||||
func initialize() (boba.Model, boba.Cmd) {
|
||||
return Model{0, false, 10, 0, 0, false}, nil
|
||||
}
|
||||
|
||||
// SUBSCRIPTIONS
|
||||
|
||||
func subscriptions(model tea.Model) tea.Subs {
|
||||
func subscriptions(model boba.Model) boba.Subs {
|
||||
m, _ := model.(Model)
|
||||
if !m.Chosen || m.Loaded {
|
||||
return tea.Subs{
|
||||
"tick": tea.Every(time.Second, newTickMsg),
|
||||
return boba.Subs{
|
||||
"tick": boba.Every(time.Second, newTickMsg),
|
||||
}
|
||||
}
|
||||
return tea.Subs{
|
||||
"frame": tea.Every(time.Second/60, newFrameMsg),
|
||||
return boba.Subs{
|
||||
"frame": boba.Every(time.Second/60, newFrameMsg),
|
||||
}
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
|
||||
func update(msg tea.Msg, model tea.Model) (tea.Model, tea.Cmd) {
|
||||
func update(msg boba.Msg, model boba.Model) (boba.Model, boba.Cmd) {
|
||||
m, _ := model.(Model)
|
||||
|
||||
if !m.Chosen {
|
||||
|
@ -81,10 +81,10 @@ func update(msg tea.Msg, model tea.Model) (tea.Model, tea.Cmd) {
|
|||
return updateChosen(msg, m)
|
||||
}
|
||||
|
||||
func updateChoices(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
|
||||
func updateChoices(msg boba.Msg, m Model) (boba.Model, boba.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.KeyMsg:
|
||||
case boba.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "j":
|
||||
fallthrough
|
||||
|
@ -108,12 +108,12 @@ func updateChoices(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
|
|||
case "esc":
|
||||
fallthrough
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
return m, boba.Quit
|
||||
}
|
||||
|
||||
case tickMsg:
|
||||
if m.Ticks == 0 {
|
||||
return m, tea.Quit
|
||||
return m, boba.Quit
|
||||
}
|
||||
m.Ticks -= 1
|
||||
}
|
||||
|
@ -121,17 +121,17 @@ func updateChoices(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
|
|||
return m, nil
|
||||
}
|
||||
|
||||
func updateChosen(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
|
||||
func updateChosen(msg boba.Msg, m Model) (boba.Model, boba.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.KeyMsg:
|
||||
case boba.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "q":
|
||||
fallthrough
|
||||
case "esc":
|
||||
fallthrough
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
return m, boba.Quit
|
||||
}
|
||||
|
||||
case frameMsg:
|
||||
|
@ -148,7 +148,7 @@ func updateChosen(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
|
|||
case tickMsg:
|
||||
if m.Loaded {
|
||||
if m.Ticks == 0 {
|
||||
return m, tea.Quit
|
||||
return m, boba.Quit
|
||||
}
|
||||
m.Ticks -= 1
|
||||
}
|
||||
|
@ -159,12 +159,12 @@ func updateChosen(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
|
|||
|
||||
// VIEW
|
||||
|
||||
func view(model tea.Model) string {
|
||||
func view(model boba.Model) string {
|
||||
m, _ := model.(Model)
|
||||
if !m.Chosen {
|
||||
return choicesView(m)
|
||||
return choicesView(m) + "\n"
|
||||
}
|
||||
return chosenView(m)
|
||||
return chosenView(m) + "\n"
|
||||
}
|
||||
|
||||
const choicesTpl = `What to do today?
|
||||
|
|
1
go.mod
1
go.mod
|
@ -5,5 +5,6 @@ go 1.13
|
|||
require (
|
||||
github.com/muesli/termenv v0.5.2
|
||||
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||
golang.org/x/sys v0.0.0-20200430202703-d923437fa56d // indirect
|
||||
)
|
||||
|
|
7
go.sum
7
go.sum
|
@ -8,6 +8,13 @@ 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/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-20200430202703-d923437fa56d h1:xmcims+WSpFuY56YEzkKF6IMDxYAVDRipkQRJfXUBZk=
|
||||
golang.org/x/sys v0.0.0-20200430202703-d923437fa56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
package pager
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/boba"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func NewProgram(initialContent string) *boba.Program {
|
||||
return boba.NewProgram(
|
||||
Init(initialContent),
|
||||
Update,
|
||||
View,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// MSG
|
||||
|
||||
type terminalSizeMsg struct {
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
type errMsg error
|
||||
|
||||
// MODEL
|
||||
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateInit State = iota
|
||||
StateReady
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Err error
|
||||
Standalone bool
|
||||
State State
|
||||
Width int
|
||||
Height int
|
||||
Y int
|
||||
|
||||
lines []string
|
||||
}
|
||||
|
||||
// Content adds text content to the model
|
||||
func (m *Model) Content(s string) {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.Replace(s, "\r\n", "\n", -1) // normalize line endings
|
||||
m.lines = strings.Split(s, "\n")
|
||||
}
|
||||
|
||||
func NewModel() Model {
|
||||
return Model{
|
||||
State: StateInit,
|
||||
}
|
||||
}
|
||||
|
||||
// INIT
|
||||
|
||||
func Init(initialContent string) func() (boba.Model, boba.Cmd) {
|
||||
m := NewModel()
|
||||
m.Standalone = true
|
||||
m.Content(initialContent)
|
||||
return func() (boba.Model, boba.Cmd) {
|
||||
return m, getTerminalSize
|
||||
}
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
|
||||
func Update(msg boba.Msg, model boba.Model) (boba.Model, boba.Cmd) {
|
||||
m, ok := model.(Model)
|
||||
if !ok {
|
||||
return Model{
|
||||
Err: errors.New("could not perform assertion on model in update in pager; are you sure you passed the correct model?"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case boba.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "q":
|
||||
fallthrough
|
||||
case "ctrl+c":
|
||||
if m.Standalone {
|
||||
return m, boba.Quit
|
||||
}
|
||||
|
||||
// Up one page
|
||||
case "b":
|
||||
m.Y = max(0, m.Y-m.Height)
|
||||
return m, nil
|
||||
|
||||
// Down one page
|
||||
case "space":
|
||||
fallthrough
|
||||
case "f":
|
||||
m.Y = min(len(m.lines)-m.Height, m.Y+m.Height)
|
||||
return m, nil
|
||||
|
||||
// Up half page
|
||||
case "u":
|
||||
m.Y = max(0, m.Y-m.Height/2)
|
||||
return m, nil
|
||||
|
||||
// Down half page
|
||||
case "d":
|
||||
m.Y = min(len(m.lines)-m.Height, m.Y+m.Height/2)
|
||||
return m, nil
|
||||
|
||||
// Up one line
|
||||
case "up":
|
||||
fallthrough
|
||||
case "k":
|
||||
m.Y = max(0, m.Y-1)
|
||||
return m, nil
|
||||
|
||||
// Down one line
|
||||
case "down":
|
||||
fallthrough
|
||||
case "j":
|
||||
m.Y = min(len(m.lines)-m.Height, m.Y+1)
|
||||
return m, nil
|
||||
|
||||
// Re-render
|
||||
case "ctrl+l":
|
||||
return m, getTerminalSize
|
||||
|
||||
}
|
||||
|
||||
case errMsg:
|
||||
m.Err = msg
|
||||
return m, nil
|
||||
|
||||
case terminalSizeMsg:
|
||||
m.Width = msg.width
|
||||
m.Height = msg.height
|
||||
m.State = StateReady
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// VIEW
|
||||
|
||||
func View(model boba.Model) string {
|
||||
m, ok := model.(Model)
|
||||
if !ok {
|
||||
return "could not perform assertion on model in view in pager; are you sure you passed the correct model?"
|
||||
}
|
||||
|
||||
if m.Err != nil {
|
||||
return m.Err.Error()
|
||||
}
|
||||
|
||||
if len(m.lines) == 0 {
|
||||
return "(Buffer empty)"
|
||||
}
|
||||
|
||||
if m.State == StateReady {
|
||||
// Render viewport
|
||||
top := max(0, m.Y)
|
||||
bottom := min(len(m.lines), m.Y+m.Height)
|
||||
lines := m.lines[top:bottom]
|
||||
return "\n" + strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// CMD
|
||||
|
||||
func getTerminalSize() boba.Msg {
|
||||
w, h, err := terminal.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
return terminalSizeMsg{w, h}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
// package paginator provides a Boba package for calulating pagination and
|
||||
// rendering pagination info. Note that this package does not render actual
|
||||
// pages: it's purely for handling keystrokes related to pagination, and
|
||||
// rendering pagination status.
|
||||
package paginator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/boba"
|
||||
)
|
||||
|
||||
// PaginatorType specifies the way we render pagination
|
||||
type PaginatorType int
|
||||
|
||||
// Pagination rendering options
|
||||
const (
|
||||
Arabic PaginatorType = iota
|
||||
Dots
|
||||
)
|
||||
|
||||
// Model is the Boba model for this user interface
|
||||
type Model struct {
|
||||
Type PaginatorType
|
||||
Page int
|
||||
PerPage int
|
||||
TotalPages int
|
||||
ActiveDot string
|
||||
InactiveDot string
|
||||
ArabicFormat string
|
||||
UseLeftRightKeys bool
|
||||
UseUpDownKeys bool
|
||||
UseHLKeys bool
|
||||
UseJKKeys bool
|
||||
}
|
||||
|
||||
// SetTotalPages is a helper function for calculatng the total number of pages
|
||||
// from a given number of items. It's use is optional since this pager can be
|
||||
// used for other things beyond navigating sets. Note that it both returns the
|
||||
// number of total pages and alters the model.
|
||||
func (m *Model) SetTotalPages(items int) int {
|
||||
if items == 0 {
|
||||
return 0
|
||||
}
|
||||
n := items / m.PerPage
|
||||
if items%m.PerPage > 0 {
|
||||
n += 1
|
||||
}
|
||||
m.TotalPages = n
|
||||
return n
|
||||
}
|
||||
|
||||
// ItemsOnPage is a helper function fro returning the numer of items on the
|
||||
// current page given the total numer of items passed as an argument.
|
||||
func (m Model) ItemsOnPage(totalItems int) int {
|
||||
start, end := m.GetSliceBounds(totalItems)
|
||||
return end - start
|
||||
}
|
||||
|
||||
// GetSliceBounds is a helper function for paginating slices. Pass the length
|
||||
// of the slice you're rendering and you'll receive the start and end bounds
|
||||
// corresponding the to pagination. For example:
|
||||
//
|
||||
// bunchOfStuff := []stuff{...}
|
||||
// start, end := model.GetSliceBounds(len(bunchOfStuff))
|
||||
// sliceToRender := bunchOfStuff[start:end]
|
||||
//
|
||||
func (m *Model) GetSliceBounds(length int) (start int, end int) {
|
||||
start = m.Page * m.PerPage
|
||||
end = min(m.Page*m.PerPage+m.PerPage, length)
|
||||
return start, end
|
||||
}
|
||||
|
||||
// PrevPage is a number function for navigating one page backward. It will not
|
||||
// page beyond the first page (i.e. page 0).
|
||||
func (m *Model) PrevPage() {
|
||||
if m.Page > 0 {
|
||||
m.Page--
|
||||
}
|
||||
}
|
||||
|
||||
// NextPage is a helper function for navigating one page forward. It will not
|
||||
// page beyond the last page (i.e. totalPages - 1).
|
||||
func (m *Model) NextPage() {
|
||||
if m.Page < m.TotalPages-1 {
|
||||
m.Page++
|
||||
}
|
||||
}
|
||||
|
||||
// NewModel creates a new model with defaults
|
||||
func NewModel() Model {
|
||||
return Model{
|
||||
Type: Arabic,
|
||||
Page: 0,
|
||||
PerPage: 1,
|
||||
TotalPages: 1,
|
||||
ActiveDot: "•",
|
||||
InactiveDot: "○",
|
||||
ArabicFormat: "%d/%d",
|
||||
UseLeftRightKeys: true,
|
||||
UseUpDownKeys: false,
|
||||
UseHLKeys: true,
|
||||
UseJKKeys: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Update is the Boba update function which binds keystrokes to pagination
|
||||
func Update(msg boba.Msg, m Model) (Model, boba.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case boba.KeyMsg:
|
||||
if m.UseLeftRightKeys {
|
||||
switch msg.String() {
|
||||
case "left":
|
||||
m.PrevPage()
|
||||
case "right":
|
||||
m.NextPage()
|
||||
}
|
||||
}
|
||||
if m.UseUpDownKeys {
|
||||
switch msg.String() {
|
||||
case "up":
|
||||
m.PrevPage()
|
||||
case "down":
|
||||
m.NextPage()
|
||||
}
|
||||
}
|
||||
if m.UseHLKeys {
|
||||
switch msg.String() {
|
||||
case "h":
|
||||
m.PrevPage()
|
||||
case "l":
|
||||
m.NextPage()
|
||||
}
|
||||
}
|
||||
if m.UseJKKeys {
|
||||
switch msg.String() {
|
||||
case "j":
|
||||
m.PrevPage()
|
||||
case "k":
|
||||
m.NextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// View renders the pagination to a string
|
||||
func View(model boba.Model) string {
|
||||
m, ok := model.(Model)
|
||||
if !ok {
|
||||
return "could not perform assertion on model"
|
||||
}
|
||||
switch m.Type {
|
||||
case Dots:
|
||||
return dotsView(m)
|
||||
default:
|
||||
return arabicView(m)
|
||||
}
|
||||
}
|
||||
|
||||
func dotsView(m Model) string {
|
||||
var s string
|
||||
for i := 0; i < m.TotalPages; i++ {
|
||||
if i == m.Page {
|
||||
s += m.ActiveDot
|
||||
continue
|
||||
}
|
||||
s += m.InactiveDot
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func arabicView(m Model) string {
|
||||
return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages)
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package spinner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/boba"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
// Spinner denotes a type of spinner
|
||||
type Spinner = int
|
||||
|
||||
// Available types of spinners
|
||||
const (
|
||||
Line Spinner = iota
|
||||
Dot
|
||||
)
|
||||
|
||||
var (
|
||||
// Spinner frames
|
||||
spinners = map[Spinner][]string{
|
||||
Line: {"|", "/", "-", "\\"},
|
||||
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
|
||||
)
|
||||
|
||||
// Model contains the state for the spinner. Use NewModel to create new models
|
||||
// rather than using Model as a struct literal.
|
||||
type Model struct {
|
||||
Type Spinner
|
||||
FPS int
|
||||
ForegroundColor string
|
||||
BackgroundColor string
|
||||
|
||||
frame int
|
||||
}
|
||||
|
||||
// NewModel returns a model with default values
|
||||
func NewModel() Model {
|
||||
return Model{
|
||||
Type: Line,
|
||||
FPS: 9,
|
||||
frame: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// TickMsg indicates that the timer has ticked and we should render a frame
|
||||
type TickMsg time.Time
|
||||
|
||||
// Update is the Boba update function
|
||||
func Update(msg boba.Msg, m Model) (Model, boba.Cmd) {
|
||||
switch msg.(type) {
|
||||
case TickMsg:
|
||||
m.frame++
|
||||
if m.frame >= len(spinners[m.Type]) {
|
||||
m.frame = 0
|
||||
}
|
||||
return m, nil
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
// View renders the model's view
|
||||
func View(model Model) string {
|
||||
s := spinners[model.Type]
|
||||
if model.frame >= len(s) {
|
||||
return "[error]"
|
||||
}
|
||||
|
||||
str := s[model.frame]
|
||||
|
||||
if model.ForegroundColor != "" || model.BackgroundColor != "" {
|
||||
return termenv.
|
||||
String(str).
|
||||
Foreground(color(model.ForegroundColor)).
|
||||
Background(color(model.BackgroundColor)).
|
||||
String()
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// GetSub creates the subscription that allows the spinner to spin. Remember
|
||||
// that you need to execute this function in order to get the subscription
|
||||
// you'll need.
|
||||
func MakeSub(model boba.Model) (boba.Sub, error) {
|
||||
m, ok := model.(Model)
|
||||
if !ok {
|
||||
return nil, assertionErr
|
||||
}
|
||||
return boba.Tick(time.Second/time.Duration(m.FPS), func(t time.Time) boba.Msg {
|
||||
return TickMsg(t)
|
||||
}), nil
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
package textinput
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/boba"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
var (
|
||||
// color is a helper for returning colors
|
||||
color func(s string) termenv.Color = termenv.ColorProfile().Color
|
||||
)
|
||||
|
||||
// ErrMsg indicates there's been an error. We don't handle errors in the this
|
||||
// package; we're expecting errors to be handle in the program that implements
|
||||
// this text input.
|
||||
type ErrMsg error
|
||||
|
||||
// Model is the Boba model for this text input element
|
||||
type Model struct {
|
||||
Err error
|
||||
Prompt string
|
||||
Value string
|
||||
Cursor string
|
||||
BlinkSpeed time.Duration
|
||||
Placeholder string
|
||||
TextColor string
|
||||
PlaceholderColor string
|
||||
CursorColor string
|
||||
|
||||
// CharLimit is the maximum amount of characters this input element will
|
||||
// accept. If 0 or less, there's no limit.
|
||||
CharLimit int
|
||||
|
||||
// Focus indicates whether user input focus should be on this input
|
||||
// component. When false, don't blink and ignore keyboard input.
|
||||
focus bool
|
||||
|
||||
blink bool
|
||||
pos int
|
||||
}
|
||||
|
||||
// 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.blink = false
|
||||
}
|
||||
|
||||
// Blur removes the focus state on the model
|
||||
func (m *Model) Blur() {
|
||||
m.focus = false
|
||||
m.blink = true
|
||||
}
|
||||
|
||||
// colorText colorizes a given string according to the TextColor value of the
|
||||
// model
|
||||
func (m *Model) colorText(s string) string {
|
||||
return termenv.
|
||||
String(s).
|
||||
Foreground(color(m.TextColor)).
|
||||
String()
|
||||
}
|
||||
|
||||
// colorPlaceholder colorizes a given string according to the TextColor value
|
||||
// of the model
|
||||
func (m *Model) colorPlaceholder(s string) string {
|
||||
return termenv.
|
||||
String(s).
|
||||
Foreground(color(m.PlaceholderColor)).
|
||||
String()
|
||||
}
|
||||
|
||||
// CursorBlinkMsg is sent when the cursor should alternate it's blinking state
|
||||
type CursorBlinkMsg struct{}
|
||||
|
||||
// NewModel creates a new model with default settings
|
||||
func NewModel() Model {
|
||||
return Model{
|
||||
Prompt: "> ",
|
||||
Value: "",
|
||||
BlinkSpeed: time.Millisecond * 600,
|
||||
Placeholder: "",
|
||||
TextColor: "",
|
||||
PlaceholderColor: "240",
|
||||
CursorColor: "",
|
||||
CharLimit: 0,
|
||||
|
||||
focus: false,
|
||||
blink: true,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Update is the Boba update loop
|
||||
func Update(msg boba.Msg, m Model) (Model, boba.Cmd) {
|
||||
if !m.focus {
|
||||
m.blink = true
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case boba.KeyMsg:
|
||||
switch msg.Type {
|
||||
case boba.KeyBackspace:
|
||||
fallthrough
|
||||
case boba.KeyDelete:
|
||||
if len(m.Value) > 0 {
|
||||
m.Value = m.Value[:m.pos-1] + m.Value[m.pos:]
|
||||
m.pos--
|
||||
}
|
||||
return m, nil
|
||||
case boba.KeyLeft:
|
||||
if m.pos > 0 {
|
||||
m.pos--
|
||||
}
|
||||
return m, nil
|
||||
case boba.KeyRight:
|
||||
if m.pos < len(m.Value) {
|
||||
m.pos++
|
||||
}
|
||||
return m, nil
|
||||
case boba.KeyCtrlF: // ^F, forward one character
|
||||
fallthrough
|
||||
case boba.KeyCtrlB: // ^B, back one charcter
|
||||
fallthrough
|
||||
case boba.KeyCtrlA: // ^A, go to beginning
|
||||
m.pos = 0
|
||||
return m, nil
|
||||
case boba.KeyCtrlD: // ^D, delete char under cursor
|
||||
if len(m.Value) > 0 && m.pos < len(m.Value) {
|
||||
m.Value = m.Value[:m.pos] + m.Value[m.pos+1:]
|
||||
}
|
||||
return m, nil
|
||||
case boba.KeyCtrlE: // ^E, go to end
|
||||
m.pos = len(m.Value)
|
||||
return m, nil
|
||||
case boba.KeyCtrlK: // ^K, kill text after cursor
|
||||
m.Value = m.Value[:m.pos]
|
||||
m.pos = len(m.Value)
|
||||
return m, nil
|
||||
case boba.KeyCtrlU: // ^U, kill text before cursor
|
||||
m.Value = m.Value[m.pos:]
|
||||
m.pos = 0
|
||||
return m, nil
|
||||
case boba.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:]
|
||||
m.pos++
|
||||
}
|
||||
return m, nil
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
|
||||
case ErrMsg:
|
||||
m.Err = msg
|
||||
return m, nil
|
||||
|
||||
case CursorBlinkMsg:
|
||||
m.blink = !m.blink
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
// View renders the textinput in its current state
|
||||
func View(model boba.Model) string {
|
||||
m, ok := model.(Model)
|
||||
if !ok {
|
||||
return "could not perform assertion on model"
|
||||
}
|
||||
|
||||
// Placeholder text
|
||||
if m.Value == "" && m.Placeholder != "" {
|
||||
return placeholderView(m)
|
||||
}
|
||||
|
||||
v := m.colorText(m.Value[:m.pos])
|
||||
|
||||
if m.pos < len(m.Value) {
|
||||
v += cursorView(string(m.Value[m.pos]), m)
|
||||
v += m.colorText(m.Value[m.pos+1:])
|
||||
} else {
|
||||
v += cursorView(" ", m)
|
||||
}
|
||||
return m.Prompt + v
|
||||
}
|
||||
|
||||
// placeholderView
|
||||
func placeholderView(m Model) string {
|
||||
var (
|
||||
v string
|
||||
p = m.Placeholder
|
||||
)
|
||||
|
||||
// Cursor
|
||||
if m.blink && m.PlaceholderColor != "" {
|
||||
v += cursorView(
|
||||
m.colorPlaceholder(p[:1]),
|
||||
m,
|
||||
)
|
||||
} else {
|
||||
v += cursorView(p[:1], m)
|
||||
}
|
||||
|
||||
// The rest of the placeholder text
|
||||
v += m.colorPlaceholder(p[1:])
|
||||
|
||||
return m.Prompt + v
|
||||
}
|
||||
|
||||
// cursorView style the cursor
|
||||
func cursorView(s string, m Model) string {
|
||||
if m.blink {
|
||||
return s
|
||||
}
|
||||
return termenv.String(s).
|
||||
Foreground(color(m.CursorColor)).
|
||||
Reverse().
|
||||
String()
|
||||
}
|
||||
|
||||
// MakeSub return a subscription that lets us know when to alternate the
|
||||
// blinking of the cursor.
|
||||
func MakeSub(model boba.Model) (boba.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() boba.Msg {
|
||||
time.Sleep(m.BlinkSpeed)
|
||||
return CursorBlinkMsg{}
|
||||
}, nil
|
||||
}
|
Loading…
Reference in New Issue