diff --git a/examples/go.mod b/examples/go.mod index 2935ecd..1611667 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -5,7 +5,8 @@ go 1.13 replace github.com/charmbracelet/bubbletea => ../ require ( - github.com/charmbracelet/bubbletea v0.0.0-00010101000000-000000000000 + github.com/charmbracelet/bubbles v0.0.0-20200526000837-87c7cd778f80 + github.com/charmbracelet/bubbletea v0.6.4-0.20200525234836-3b8b011b5a26 github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 github.com/muesli/termenv v0.5.2 ) diff --git a/examples/go.sum b/examples/go.sum index fb00e06..34d12a7 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,3 +1,5 @@ +github.com/charmbracelet/bubbles v0.0.0-20200526000837-87c7cd778f80 h1:cfaoL1+tHPABTLEAg831PIFG96teW69Wamz9M025r5M= +github.com/charmbracelet/bubbles v0.0.0-20200526000837-87c7cd778f80/go.mod h1:/AeLRFlL2Uf4X7U5LjnswTII6u4maPzMm1+vZfeUJKc= 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= diff --git a/examples/pager/main.go b/examples/pager/main.go index 30b800b..1df3ef3 100644 --- a/examples/pager/main.go +++ b/examples/pager/main.go @@ -5,8 +5,8 @@ import ( "io/ioutil" "os" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/bubbletea/viewport" ) func main() { diff --git a/examples/simple/main.go b/examples/simple/main.go index 288bdb1..780046e 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -7,7 +7,7 @@ import ( "log" "time" - tea "github.com/charmbracelet/bubbletea" + tea "github.com/charmbracelet/bubbles" ) // A model can be more or less any type of data. It holds all the data for a diff --git a/examples/spinner/main.go b/examples/spinner/main.go index 95750c2..d9ea67c 100644 --- a/examples/spinner/main.go +++ b/examples/spinner/main.go @@ -4,8 +4,8 @@ import ( "fmt" "os" + "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/bubbletea/spinner" "github.com/muesli/termenv" ) diff --git a/examples/textinput/main.go b/examples/textinput/main.go index b226757..f4ac243 100644 --- a/examples/textinput/main.go +++ b/examples/textinput/main.go @@ -5,8 +5,8 @@ import ( "fmt" "log" + input "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - input "github.com/charmbracelet/bubbletea/textinput" ) type Model struct { diff --git a/examples/textinputs/main.go b/examples/textinputs/main.go index 1948862..0ad850c 100644 --- a/examples/textinputs/main.go +++ b/examples/textinputs/main.go @@ -4,8 +4,8 @@ import ( "fmt" "os" + input "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - input "github.com/charmbracelet/bubbletea/textinput" te "github.com/muesli/termenv" ) diff --git a/paginator/paginator.go b/paginator/paginator.go deleted file mode 100644 index ee6b9eb..0000000 --- a/paginator/paginator.go +++ /dev/null @@ -1,188 +0,0 @@ -// 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. -package paginator - -import ( - "fmt" - - tea "github.com/charmbracelet/bubbletea" -) - -// Type specifies the way we render pagination. -type Type int - -// Pagination rendering options -const ( - Arabic Type = iota - Dots -) - -// Model is the Tea model for this user interface. -type Model struct { - Type Type - 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++ - } - m.TotalPages = n - return n -} - -// 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. -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.OnLastPage() { - m.Page++ - } -} - -// 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 { - 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 Tea update function which binds keystrokes to pagination. -func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.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 tea.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 -} diff --git a/spinner/spinner.go b/spinner/spinner.go deleted file mode 100644 index 2f7bb73..0000000 --- a/spinner/spinner.go +++ /dev/null @@ -1,118 +0,0 @@ -package spinner - -import ( - "time" - - tea "github.com/charmbracelet/bubbletea" - "github.com/muesli/termenv" -) - -// Spinner is a set of frames used in animating the spinner. -type Spinner = int - -// Available types of spinners -const ( - Line Spinner = iota - Dot -) - -const ( - defaultFPS = 9 -) - -var ( - // Spinner frames - spinners = map[Spinner][]string{ - Line: {"|", "/", "-", "\\"}, - Dot: {"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "}, - } - - 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 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 - - // 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 - - // 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 -} - -// NewModel returns a model with default values. -func NewModel() Model { - return Model{ - Type: Line, - FPS: defaultFPS, - } -} - -// TickMsg indicates that the timer has ticked and we should render a frame. -type TickMsg struct{} - -// 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) { - m.frame++ - if m.frame >= len(spinners[m.Type]) { - m.frame = 0 - } - if m.CustomMsgFunc != nil { - return m, Tick(m) - } - return m, Tick(m) -} - -// 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 -} - -// Tick is the command used to advance the spinner one frame. -func Tick(model Model) tea.Cmd { - return func() tea.Msg { - time.Sleep(time.Second / time.Duration(model.FPS)) - if model.CustomMsgFunc != nil { - return model.CustomMsgFunc() - } - return TickMsg{} - } -} diff --git a/textinput/textinput.go b/textinput/textinput.go deleted file mode 100644 index 3d4a6c7..0000000 --- a/textinput/textinput.go +++ /dev/null @@ -1,427 +0,0 @@ -package textinput - -import ( - "strings" - "time" - "unicode" - - tea "github.com/charmbracelet/bubbletea" - "github.com/muesli/termenv" -) - -const ( - defaultBlinkSpeed = time.Millisecond * 600 -) - -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 Tea model for this text input element. -type Model struct { - Err error - Prompt string - Cursor string - BlinkSpeed time.Duration - Placeholder string - TextColor string - BackgroundColor 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 - - // 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 - // component. When false, don't blink and ignore keyboard input. - focus bool - - // Cursor blink state - blink bool - - // Cursor position - pos int - - // Used to emulate a viewport when width is set and the content is - // overflowing - offset int -} - -// 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 { - 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 -} - -// 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 -// model. -func (m *Model) colorText(s string) string { - return termenv. - String(s). - Foreground(color(m.TextColor)). - Background(color(m.BackgroundColor)). - 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)). - Background(color(m.BackgroundColor)). - String() -} - -func (m *Model) wordLeft() { - if m.pos == 0 || len(m.value) == 0 { - return - } - - 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 { - return Model{ - Prompt: "> ", - BlinkSpeed: defaultBlinkSpeed, - Placeholder: "", - TextColor: "", - PlaceholderColor: "240", - CursorColor: "", - CharLimit: 0, - - value: "", - focus: false, - blink: true, - pos: 0, - } -} - -// Update is the Tea update loop. -func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { - if !m.focus { - m.blink = true - return m, nil - } - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyBackspace: - fallthrough - case tea.KeyDelete: - if len(m.value) > 0 { - m.value = m.value[:m.pos-1] + m.value[m.pos:] - m.pos-- - } - case tea.KeyLeft: - if msg.Alt { // alt+left arrow, back one word - m.wordLeft() - break - } - if m.pos > 0 { - m.pos-- - } - case tea.KeyRight: - if msg.Alt { // alt+right arrow, forward one word - m.wordRight() - break - } - if m.pos < len(m.value) { - m.pos++ - } - case tea.KeyCtrlF: // ^F, forward one character - fallthrough - case tea.KeyCtrlB: // ^B, back one charcter - fallthrough - case tea.KeyCtrlA: // ^A, go to beginning - m.CursorStart() - case tea.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:] - } - case tea.KeyCtrlE: // ^E, go to end - m.CursorEnd() - case tea.KeyCtrlK: // ^K, kill text after cursor - m.value = m.value[:m.pos] - m.pos = len(m.value) - case tea.KeyCtrlU: // ^U, kill text before cursor - m.value = m.value[m.pos:] - m.pos = 0 - m.offset = 0 - case tea.KeyRune: // input a regular character - - 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++ - } - } - - case ErrMsg: - m.Err = msg - - case BlinkMsg: - m.blink = !m.blink - return m, Blink(m) - } - - m.handleOverflow() - - return m, nil -} - -// View renders the textinput in its current state. -func View(model tea.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) - } - - 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 - - v := m.colorText(value[:pos]) - - 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 { - 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 -} - -// 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 styles the cursor. -func cursorView(s string, m Model) string { - if m.blink { - if m.TextColor != "" || m.BackgroundColor != "" { - return termenv.String(s). - Foreground(color(m.TextColor)). - Background(color(m.BackgroundColor)). - String() - } - return s - } - return termenv.String(s). - Foreground(color(m.CursorColor)). - Background(color(m.BackgroundColor)). - Reverse(). - String() -} - -// Blink is a command used to time the cursor blinking. -func Blink(model Model) tea.Cmd { - return func() tea.Msg { - time.Sleep(model.BlinkSpeed) - return BlinkMsg{} - } -} - -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 -} diff --git a/viewport/viewport.go b/viewport/viewport.go deleted file mode 100644 index b56c251..0000000 --- a/viewport/viewport.go +++ /dev/null @@ -1,169 +0,0 @@ -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 -}