1 Commits

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

View File

@@ -14,7 +14,6 @@ linters:
- godot
- godox
- goimports
- golint
- gomnd
- goprintffuncname
- gosec

View File

@@ -1,10 +1,6 @@
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)
[![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)
@@ -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)
## 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
<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
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)
@@ -89,6 +74,7 @@ indenting and text wrapping.
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!

10
go.mod
View File

@@ -4,11 +4,9 @@ go 1.13
require (
github.com/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.13.1
github.com/charmbracelet/lipgloss v0.1.2
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.12
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
github.com/muesli/termenv v0.8.1
github.com/charmbracelet/bubbletea v0.12.1
github.com/mattn/go-runewidth v0.0.9
github.com/muesli/termenv v0.7.4
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a // indirect
)

30
go.sum
View File

@@ -1,32 +1,25 @@
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.13.1 h1:huvX8mPaeMZ8DLulT50iEWRF+iitY5FNEDqDVLu69nM=
github.com/charmbracelet/bubbletea v0.13.1/go.mod h1:tp9tr9Dadh0PLhgiwchE5zZJXm5543JYjHG9oY+5qSg=
github.com/charmbracelet/lipgloss v0.1.2 h1:D+LUMg34W7n2pkuMrevKVxT7HXqnoRHm7IoomkX3/ZU=
github.com/charmbracelet/lipgloss v0.1.2/go.mod h1:5D8zradw52m7QmxRF6QgwbwJi9je84g8MkWiGN07uKg=
github.com/charmbracelet/bubbletea v0.12.1 h1:t21pkG2IDBRduPbt2J64Dx5yt8yIidAkXwhhrc11SzY=
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/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/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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/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/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.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
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.8.1 h1:9q230czSP3DHVpkaPDXGp0TOfAwyjyYwXlUCQxQSaBk=
github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8=
github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -35,7 +28,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
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-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a h1:e3IU37lwO4aq3uoRKINC7JikojFmE5gO7xhfxs8VC34=
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

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

@@ -1,4 +1,4 @@
// Package paginator provides a Bubble Tea package for calulating pagination
// package paginator provides a Bubble Tea 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.
@@ -19,7 +19,7 @@ const (
Dots
)
// Model is the Bubble Tea model for this user interface.
// Model is the Tea model for this user interface.
type Model struct {
Type Type
Page int
@@ -91,7 +91,7 @@ func (m *Model) NextPage() {
}
}
// OnLastPage returns whether or not we're on the last page.
// LastPage returns whether or not we're on the last page.
func (m Model) OnLastPage() bool {
return m.Page == m.TotalPages-1
}
@@ -115,7 +115,7 @@ func NewModel() Model {
}
// 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) {
case tea.KeyMsg:
if m.UsePgUpPgDownKeys {
@@ -164,16 +164,16 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
// View renders the pagination to a string.
func (m Model) View() string {
func View(m Model) string {
switch m.Type {
case Dots:
return m.dotsView()
return dotsView(m)
default:
return m.arabicView()
return arabicView(m)
}
}
func (m Model) dotsView() string {
func dotsView(m Model) string {
var s string
for i := 0; i < m.TotalPages; i++ {
if i == m.Page {
@@ -185,7 +185,7 @@ func (m Model) dotsView() string {
return s
}
func (m Model) arabicView() string {
func arabicView(m Model) string {
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) //nolint:gomnd
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,73 +1,48 @@
package spinner
import (
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/ansi"
"github.com/muesli/termenv"
)
const (
defaultFPS = time.Second / 10
)
// Spinner is a set of frames used in animating the spinner.
type Spinner struct {
Frames []string
FPS time.Duration
}
type Spinner = []string
// Some spinners to choose from. You could also make your own.
var (
Line = Spinner{
Frames: []string{"|", "/", "-", "\\"},
FPS: time.Second / 10, //nolint:gomnd
}
Dot = Spinner{
Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
FPS: time.Second / 10, //nolint:gomnd
}
MiniDot = Spinner{
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
FPS: time.Second / 12, //nolint:gomnd
}
Jump = Spinner{
Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
FPS: time.Second / 10, //nolint:gomnd
}
Pulse = Spinner{
Frames: []string{"█", "▓", "▒", "░"},
FPS: time.Second / 8, //nolint:gomnd
}
Points = Spinner{
Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
FPS: time.Second / 7, //nolint:gomnd
}
Globe = Spinner{
Frames: []string{"🌍", "🌎", "🌏"},
FPS: time.Second / 4, //nolint:gomnd
}
Moon = Spinner{
Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
FPS: time.Second / 8, //nolint:gomnd
}
Monkey = Spinner{
Frames: []string{"🙈", "🙉", "🙊"},
FPS: time.Second / 3, //nolint:gomnd
}
// Some spinners to choose from. You could also make your own.
Line = Spinner([]string{"|", "/", "-", "\\"})
Dot = Spinner([]string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "})
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 {
// Spinner settings to use. See type Spinner.
Spinner Spinner
// Type is the set of frames to use. See Spinner.
Frames Spinner
// Style sets the styling for the spinner. Most of the time you'll just
// want foreground and background coloring, and potentially some padding.
//
// For an introduction to styling with Lip Gloss see:
// https://github.com/charmbracelet/lipgloss
Style lipgloss.Style
// FPS is the speed at which the ticker should tick.
FPS time.Duration
// 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
// 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
// MinimumLifetime is the minimum amount of time the spinner can run. Any
// logic around this can be implemented in view that implements this
@@ -92,49 +67,17 @@ type Model struct {
frame int
startTime time.Time
tag int
}
// Start resets resets the spinner start time. For use with MinimumLifetime and
// 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 library.
func (m *Model) Start() {
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.
func (m Model) hidden() bool {
if m.startTime.IsZero() {
@@ -146,10 +89,12 @@ func (m Model) hidden() bool {
return m.startTime.Add(m.HideFor).After(time.Now())
}
// finished returns whether the minimum lifetime of this spinner has been
// exceeded.
// finished returns whether Model.MinimumLifetimeReached has been met.
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 m.startTime.Add(m.HideFor).Add(m.MinimumLifetime).Before(time.Now())
@@ -157,13 +102,13 @@ func (m Model) finished() bool {
// Visible returns whether or not the view should be rendered. Works in
// 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
// parent update function.
//
// 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.
// Also note that using this function is optional and generally considered for
// advanced use only. Most of the time your application logic will determine
// whether or not this view should be used.
//
// This is considered experimental and may not appear in future versions of
// this library.
@@ -173,70 +118,55 @@ func (m Model) Visible() bool {
// NewModel returns a model with default values.
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.
type TickMsg struct {
Time time.Time
tag int
}
// 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 (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
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
}
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
if _, ok := msg.(TickMsg); ok {
m.frame++
if m.frame >= len(m.Spinner.Frames) {
if m.frame >= len(m.Frames) {
m.frame = 0
}
m.tag++
return m, m.tick(m.tag)
default:
return m, nil
return m, Tick(m)
}
return m, nil
}
// View renders the model's view.
func (m Model) View() string {
if m.frame >= len(m.Spinner.Frames) {
return "(error)"
func View(model Model) string {
if model.frame >= len(model.Frames) {
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
// 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 model.ForegroundColor != "" || model.BackgroundColor != "" {
return termenv.
String(frame).
Foreground(color(model.ForegroundColor)).
Background(color(model.BackgroundColor)).
String()
}
return m.Style.Render(frame)
return frame
}
// Tick is the command used to advance the spinner one frame. Use this command
// to effectively start the spinner.
func Tick() 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 {
// Tick is the command used to advance the spinner one frame.
func Tick(m Model) tea.Cmd {
return tea.Tick(m.FPS, func(t time.Time) tea.Msg {
return TickMsg{
Time: t,
tag: tag,
}
})
}

View File

@@ -8,12 +8,15 @@ import (
"github.com/atotto/clipboard"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
rw "github.com/mattn/go-runewidth"
"github.com/muesli/termenv"
)
const defaultBlinkSpeed = time.Millisecond * 530
// color is a helper for returning colors.
var color func(s string) termenv.Color = termenv.ColorProfile().Color
// blinkMsg and blinkCanceled are used to manage cursor blinking.
type blinkMsg struct{}
type blinkCanceled struct{}
@@ -58,22 +61,18 @@ const (
type Model struct {
Err error
// General settings.
Prompt string
Placeholder string
BlinkSpeed time.Duration
EchoMode EchoMode
EchoCharacter rune
// Styles. These will be applied as inline styles.
//
// For an introduction to styling with Lip Gloss see:
// https://github.com/charmbracelet/lipgloss
PromptStyle lipgloss.Style
TextStyle lipgloss.Style
BackgroundStyle lipgloss.Style
PlaceholderStyle lipgloss.Style
CursorStyle lipgloss.Style
// General settings
Prompt string
Placeholder string
Cursor string
BlinkSpeed time.Duration
TextColor string
FocusedTextColor string
BackgroundColor string
PlaceholderColor string
CursorColor string
EchoMode EchoMode
EchoCharacter rune
// CharLimit is the maximum amount of characters this input element will
// accept. If 0 or less, there's no limit.
@@ -113,10 +112,14 @@ type Model struct {
func NewModel() Model {
return Model{
Prompt: "> ",
Placeholder: "",
BlinkSpeed: defaultBlinkSpeed,
TextColor: "",
FocusedTextColor: "205",
PlaceholderColor: "240",
CursorColor: "",
EchoCharacter: '*',
CharLimit: 0,
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
value: nil,
focus: false,
@@ -138,8 +141,8 @@ func (m *Model) SetValue(s string) {
} else {
m.value = runes
}
if m.pos == 0 || m.pos > len(m.value) {
m.setCursor(len(m.value))
if m.pos > len(m.value) {
m.SetCursor(len(m.value))
}
m.handleOverflow()
}
@@ -149,21 +152,10 @@ func (m Model) Value() string {
return string(m.value)
}
// Cursor returns the cursor position.
func (m Model) Cursor() int {
return m.pos
}
// SetCursor moves the cursor to the given position. If the position is
// SetCursor 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.setCursor(pos)
}
// setCursor moves the cursor to the given position and returns whether or not
// the cursor blink should be reset. If the position is out of bounds the
// cursor will be moved to the start or end accordingly.
func (m *Model) setCursor(pos int) bool {
// Returns whether or nor the cursor timer should be reset.
func (m *Model) SetCursor(pos int) bool {
m.pos = clamp(pos, 0, len(m.value))
m.handleOverflow()
@@ -174,26 +166,16 @@ func (m *Model) setCursor(pos int) bool {
return m.cursorMode == cursorBlink
}
// CursorStart moves the cursor to the start of the input field.
func (m *Model) CursorStart() {
m.cursorStart()
// CursorStart moves the cursor to the start of the field. Returns whether or
// not the curosr blink should be reset.
func (m *Model) CursorStart() bool {
return m.SetCursor(0)
}
// cursorStart moves the cursor to the start of the input field and returns
// whether or not the curosr blink should be reset.
func (m *Model) cursorStart() bool {
return m.setCursor(0)
}
// CursorEnd moves the cursor to the end of the input field
func (m *Model) CursorEnd() {
m.cursorEnd()
}
// cursorEnd moves the cursor to the end of the input field and returns whether
// or not
func (m *Model) cursorEnd() bool {
return m.setCursor(len(m.value))
// CursorEnd moves the cursor to the end of the field. Returns whether or not
// the cursor blink should be reset.
func (m *Model) CursorEnd() bool {
return m.SetCursor(len(m.value))
}
// Focused returns the focus state on the model.
@@ -202,27 +184,31 @@ func (m Model) Focused() bool {
}
// Focus sets the focus state on the model.
func (m *Model) Focus() {
func (m Model) Focus() tea.Model {
m.focus = true
m.blink = m.cursorMode == cursorHide // show the cursor unless we've explicitly hidden it
return m
}
// Blur removes the focus state on the model.
func (m *Model) Blur() {
func (m Model) Blur() tea.Model {
m.focus = false
m.blink = true
return m
}
// Reset sets the input to its default state with no input. Returns whether
// or not the cursor blink should reset.
func (m *Model) Reset() bool {
m.value = nil
return m.setCursor(0)
return m.SetCursor(0)
}
// handle a clipboard paste event, if supported. Returns whether or not the
// cursor blink should be reset.
func (m *Model) handlePaste(v string) bool {
func (m *Model) handlePaste(v string) (blink bool) {
paste := []rune(v)
var availSpace int
@@ -232,7 +218,7 @@ func (m *Model) handlePaste(v string) bool {
// If the char limit's been reached cancel
if m.CharLimit > 0 && availSpace <= 0 {
return false
return
}
// If there's not enough space to paste the whole thing cut the pasted
@@ -263,7 +249,7 @@ func (m *Model) handlePaste(v string) bool {
m.value = append(head, tail...)
// Reset blink state if necessary and run overflow checks
return m.setCursor(m.pos)
return m.SetCursor(m.pos)
}
// If a max width is defined, perform some logic to treat the visible area
@@ -311,47 +297,47 @@ func (m *Model) handleOverflow() {
}
}
// deleteBeforeCursor deletes all text before the cursor. Returns whether or
// not the cursor blink should be reset.
func (m *Model) deleteBeforeCursor() bool {
m.value = m.value[m.pos:]
m.offset = 0
return m.setCursor(0)
// 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)).
Background(color(m.BackgroundColor)).
String()
}
// deleteAfterCursor deletes all text after the cursor. Returns whether or not
// the cursor blink should be reset. If input is masked delete everything after
// the cursor so as not to reveal word breaks in the masked input.
func (m *Model) deleteAfterCursor() bool {
m.value = m.value[:m.pos]
return m.setCursor(len(m.value))
// 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)).
Background(color(m.BackgroundColor)).
String()
}
// deleteWordLeft deletes the word left to the cursor. Returns whether or not
// the cursor blink should be reset.
func (m *Model) deleteWordLeft() bool {
func (m *Model) deleteWordLeft() (blink bool) {
if m.pos == 0 || len(m.value) == 0 {
return false
}
if m.EchoMode != EchoNormal {
return m.deleteBeforeCursor()
return
}
i := m.pos
blink := m.setCursor(m.pos - 1)
blink = m.SetCursor(m.pos - 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace before cursor
blink = m.setCursor(m.pos - 1)
blink = m.SetCursor(m.pos - 1)
}
for m.pos > 0 {
if !unicode.IsSpace(m.value[m.pos]) {
blink = m.setCursor(m.pos - 1)
blink = m.SetCursor(m.pos - 1)
} else {
if m.pos > 0 {
// keep the previous space
blink = m.setCursor(m.pos + 1)
blink = m.SetCursor(m.pos + 1)
}
break
}
@@ -363,31 +349,26 @@ func (m *Model) deleteWordLeft() bool {
m.value = append(m.value[:m.pos], m.value[i:]...)
}
return blink
return
}
// deleteWordRight deletes the word right to the cursor. Returns whether or not
// the cursor blink should be reset. If input is masked delete everything after
// the cursor so as not to reveal word breaks in the masked input.
func (m *Model) deleteWordRight() bool {
// the cursor blink should be reset.
func (m *Model) deleteWordRight() (blink bool) {
if m.pos >= len(m.value) || len(m.value) == 0 {
return false
}
if m.EchoMode != EchoNormal {
return m.deleteAfterCursor()
return
}
i := m.pos
m.setCursor(m.pos + 1)
blink = m.SetCursor(m.pos + 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace after cursor
m.setCursor(m.pos + 1)
blink = m.SetCursor(m.pos + 1)
}
for m.pos < len(m.value) {
if !unicode.IsSpace(m.value[m.pos]) {
m.setCursor(m.pos + 1)
blink = m.SetCursor(m.pos + 1)
} else {
break
}
@@ -398,27 +379,23 @@ func (m *Model) deleteWordRight() bool {
} else {
m.value = append(m.value[:i], m.value[m.pos:]...)
}
blink = m.SetCursor(i)
return m.setCursor(i)
return
}
// wordLeft moves the cursor one word to the left. Returns whether or not the
// cursor blink should be reset. If input is masked, move input to the start
// so as not to reveal word breaks in the masked input.
func (m *Model) wordLeft() bool {
// cursor blink should be reset.
func (m *Model) wordLeft() (blink bool) {
if m.pos == 0 || len(m.value) == 0 {
return false
return
}
if m.EchoMode != EchoNormal {
return m.cursorStart()
}
blink := false
i := m.pos - 1
for i >= 0 {
if unicode.IsSpace(m.value[i]) {
blink = m.setCursor(m.pos - 1)
blink = m.SetCursor(m.pos - 1)
i--
} else {
break
@@ -427,33 +404,28 @@ func (m *Model) wordLeft() bool {
for i >= 0 {
if !unicode.IsSpace(m.value[i]) {
blink = m.setCursor(m.pos - 1)
blink = m.SetCursor(m.pos - 1)
i--
} else {
break
}
}
return blink
return
}
// wordRight moves the cursor one word to the right. Returns whether or not the
// cursor blink should be reset. If the input is masked, move input to the end
// so as not to reveal word breaks in the masked input.
func (m *Model) wordRight() bool {
// cursor blink should be reset.
func (m *Model) wordRight() (blink bool) {
if m.pos >= len(m.value) || len(m.value) == 0 {
return false
return
}
if m.EchoMode != EchoNormal {
return m.cursorEnd()
}
blink := false
i := m.pos
for i < len(m.value) {
if unicode.IsSpace(m.value[i]) {
blink = m.setCursor(m.pos + 1)
blink = m.SetCursor(m.pos + 1)
i++
} else {
break
@@ -462,14 +434,14 @@ func (m *Model) wordRight() bool {
for i < len(m.value) {
if !unicode.IsSpace(m.value[i]) {
blink = m.setCursor(m.pos + 1)
blink = m.SetCursor(m.pos + 1)
i++
} else {
break
}
}
return blink
return
}
func (m Model) echoTransform(v string) string {
@@ -484,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.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if !m.focus {
m.blink = true
return m, nil
@@ -503,7 +479,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if len(m.value) > 0 {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 {
resetBlink = m.setCursor(m.pos - 1)
resetBlink = m.SetCursor(m.pos - 1)
}
}
}
@@ -513,43 +489,46 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
break
}
if m.pos > 0 { // left arrow, ^F, back one character
resetBlink = m.setCursor(m.pos - 1)
resetBlink = m.SetCursor(m.pos - 1)
}
case tea.KeyRight, tea.KeyCtrlF:
if msg.Alt { // alt+right arrow, forward one word
resetBlink = m.wordRight()
break
}
if m.pos < len(m.value) { // right arrow, ^F, forward one character
resetBlink = m.setCursor(m.pos + 1)
if m.pos < len(m.value) { // right arrow, ^F, forward one word
resetBlink = m.SetCursor(m.pos + 1)
}
case tea.KeyCtrlW: // ^W, delete word left of cursor
resetBlink = m.deleteWordLeft()
case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
resetBlink = m.cursorStart()
resetBlink = m.CursorStart()
case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor
if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
}
case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end
resetBlink = m.cursorEnd()
resetBlink = m.CursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor
resetBlink = m.deleteAfterCursor()
m.value = m.value[:m.pos]
resetBlink = m.SetCursor(len(m.value))
case tea.KeyCtrlU: // ^U, kill text before cursor
resetBlink = m.deleteBeforeCursor()
m.value = m.value[m.pos:]
resetBlink = m.SetCursor(0)
m.offset = 0
case tea.KeyCtrlV: // ^V paste
return m, Paste
case tea.KeyRunes: // input regular characters
if msg.Alt && len(msg.Runes) == 1 {
if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor
case tea.KeyRune: // input a regular character
if msg.Alt {
if msg.Rune == 'd' { // alt+d, delete word right of cursor
resetBlink = m.deleteWordRight()
break
}
if msg.Runes[0] == 'b' { // alt+b, back one word
if msg.Rune == 'b' { // alt+b, back one word
resetBlink = m.wordLeft()
break
}
if msg.Runes[0] == 'f' { // alt+f, forward one word
if msg.Rune == 'f' { // alt+f, forward one word
resetBlink = m.wordRight()
break
}
@@ -557,8 +536,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
m.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...)
resetBlink = m.setCursor(m.pos + len(msg.Runes))
m.value = append(m.value[:m.pos], append([]rune{msg.Rune}, m.value[m.pos:]...)...)
resetBlink = m.SetCursor(m.pos + 1)
}
}
@@ -591,20 +570,23 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// View renders the textinput in its current state.
func (m Model) View() string {
// Placeholder text
if len(m.value) == 0 && m.Placeholder != "" {
return m.placeholderView()
prompt := termenv.String(m.Prompt)
if m.focus {
prompt = prompt.Foreground(color(m.FocusedTextColor))
}
styleText := m.TextStyle.Inline(true).Render
// Placeholder text
if len(m.value) == 0 && m.Placeholder != "" {
return prompt.String() + m.placeholderView()
}
value := m.value[m.offset:m.offsetRight]
pos := max(0, m.pos-m.offset)
v := styleText(m.echoTransform(string(value[:pos])))
v := m.colorText(m.echoTransform(string(value[:pos])))
if pos < len(value) {
v += m.cursorView(m.echoTransform(string(value[pos]))) // cursor and text under it
v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor
v += m.cursorView(m.echoTransform(string(value[pos]))) // cursor and text under it
v += m.colorText(m.echoTransform(string(value[pos+1:]))) // text after cursor
} else {
v += m.cursorView(" ")
}
@@ -612,44 +594,56 @@ func (m Model) View() string {
// If a max width and background color were set fill the empty spaces with
// the background color.
valWidth := rw.StringWidth(string(value))
if m.Width > 0 && valWidth <= m.Width {
if m.Width > 0 && len(m.BackgroundColor) > 0 && valWidth <= m.Width {
padding := max(0, m.Width-valWidth)
if valWidth+padding <= m.Width && pos < len(value) {
padding++
}
v += styleText(strings.Repeat(" ", padding))
v += strings.Repeat(
termenv.String(" ").Background(color(m.BackgroundColor)).String(),
padding,
)
}
return m.PromptStyle.Render(m.Prompt) + v
return prompt.String() + v
}
// placeholderView returns the prompt and placeholder view, if any.
func (m Model) placeholderView() string {
var (
v string
p = m.Placeholder
style = m.PlaceholderStyle.Inline(true).Render
v string
p = m.Placeholder
)
// Cursor
if m.blink {
v += m.cursorView(style(p[:1]))
if m.blink && m.PlaceholderColor != "" {
v += m.cursorView(m.colorPlaceholder(p[:1]))
} else {
v += m.cursorView(p[:1])
}
// The rest of the placeholder text
v += style(p[1:])
v += m.colorPlaceholder(p[1:])
return m.PromptStyle.Render(m.Prompt) + v
return v
}
// cursorView styles the cursor.
func (m Model) cursorView(v string) string {
if m.blink {
return m.TextStyle.Render(v)
if m.TextColor != "" || m.BackgroundColor != "" {
return termenv.String(v).
Foreground(color(m.TextColor)).
Background(color(m.BackgroundColor)).
String()
}
return v
}
return m.CursorStyle.Inline(true).Reverse(true).Render(v)
return termenv.String(v).
Foreground(color(m.CursorColor)).
Background(color(m.BackgroundColor)).
Reverse().
String()
}
// blinkCmd is an internal command used to manage cursor blinking.

View File

@@ -8,11 +8,11 @@ import (
)
const (
spacebar = " "
mouseWheelDelta = 3
spacebar = " "
)
// Model is the Bubble Tea model for this viewport element.
// MODEL
type Model struct {
Width int
Height int
@@ -54,7 +54,7 @@ func (m Model) PastBottom() bool {
return m.YOffset > len(m.lines)-1-m.Height
}
// ScrollPercent returns the amount scrolled as a float between 0 and 1.
// 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
@@ -71,10 +71,6 @@ func (m Model) ScrollPercent() float64 {
func (m *Model) SetContent(s string) {
s = strings.Replace(s, "\r\n", "\n", -1) // normalize line endings
m.lines = strings.Split(s, "\n")
if m.YOffset > len(m.lines)-1 {
m.GotoBottom()
}
}
// Return the lines that should currently be visible in the viewport.
@@ -221,7 +217,7 @@ func (m *Model) GotoTop() (lines []string) {
return lines
}
// GotoBottom sets the viewport to the bottom position.
// GotoTop sets the viewport to the bottom position.
func (m *Model) GotoBottom() (lines []string) {
m.YOffset = max(len(m.lines)-1-m.Height, 0)
@@ -273,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
// 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.
func ViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
@@ -287,7 +283,7 @@ func ViewUp(m Model, lines []string) tea.Cmd {
// Update runs the update loop with default keybindings similar to popular
// pagers. To define your own keybindings use the methods on Model (i.e.
// 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
switch msg := msg.(type) {
@@ -339,13 +335,13 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case tea.MouseMsg:
switch msg.Type {
case tea.MouseWheelUp:
lines := m.LineUp(mouseWheelDelta)
lines := m.LineUp(3)
if m.HighPerformanceRendering {
cmd = ViewUp(m, lines)
}
case tea.MouseWheelDown:
lines := m.LineDown(mouseWheelDelta)
lines := m.LineDown(3)
if m.HighPerformanceRendering {
cmd = ViewDown(m, lines)
}
@@ -358,7 +354,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// VIEW
// View renders the viewport into a string.
func (m Model) View() string {
func View(m Model) string {
if m.HighPerformanceRendering {
// Just send newlines since we're doing to be rendering the actual
// content seprately. We still need send something that equals the