diff --git a/examples/chat/main.go b/examples/chat/main.go new file mode 100644 index 0000000..99fd0c6 --- /dev/null +++ b/examples/chat/main.go @@ -0,0 +1,110 @@ +package main + +// A simple program demonstrating the text area component from the Bubbles +// component library. + +import ( + "fmt" + "log" + "strings" + + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +func main() { + p := tea.NewProgram(initialModel()) + + if err := p.Start(); err != nil { + log.Fatal(err) + } +} + +type tickMsg struct{} +type errMsg error + +type model struct { + viewport viewport.Model + messages []string + textarea textarea.Model + senderStyle lipgloss.Style + err error +} + +func initialModel() model { + ta := textarea.New() + ta.Placeholder = "Send a message..." + ta.Focus() + + ta.Prompt = "┃ " + ta.CharLimit = 280 + + ta.SetWidth(30) + ta.SetHeight(3) + + // Remove cursor line styling + ta.FocusedStyle.CursorLine = lipgloss.NewStyle() + + ta.ShowLineNumbers = false + + vp := viewport.New(30, 10) + vp.SetContent(`Welcome to the Bubbles multi-line text input! +Try typing any message and pressing ENTER. +If you write a long message, it will automatically wrap :D + `) + + ta.KeyMap.InsertNewline.SetEnabled(false) + + return model{ + textarea: ta, + messages: []string{}, + viewport: vp, + senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")), + err: nil, + } +} + +func (m model) Init() tea.Cmd { + return textarea.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + tiCmd tea.Cmd + vpCmd tea.Cmd + ) + + m.textarea, tiCmd = m.textarea.Update(msg) + m.viewport, vpCmd = m.viewport.Update(msg) + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + fmt.Println(m.textarea.Value()) + return m, tea.Quit + case tea.KeyEnter: + m.messages = append(m.messages, m.senderStyle.Render("You: ")+m.textarea.Value()) + m.viewport.SetContent(strings.Join(m.messages, "\n")) + m.textarea.Reset() + m.viewport.GotoBottom() + } + + // We handle errors just like any other message + case errMsg: + m.err = msg + return m, nil + } + + return m, tea.Batch(tiCmd, vpCmd) +} + +func (m model) View() string { + return fmt.Sprintf( + "%s\n\n%s", + m.viewport.View(), + m.textarea.View(), + ) + "\n\n" +} diff --git a/examples/go.mod b/examples/go.mod index 54d3cc6..022856d 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -3,15 +3,15 @@ module examples go 1.13 require ( - github.com/charmbracelet/bubbles v0.11.1-0.20220610161724-e57fd292cc68 - github.com/charmbracelet/bubbletea v0.21.0 + github.com/charmbracelet/bubbles v0.12.1-0.20220701153126-7cc578698457 + github.com/charmbracelet/bubbletea v0.22.0 github.com/charmbracelet/glamour v0.5.0 github.com/charmbracelet/lipgloss v0.5.0 github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-isatty v0.0.14 github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 + github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898 ) replace github.com/charmbracelet/bubbletea => ../ diff --git a/examples/go.sum b/examples/go.sum index ef03829..edbb7dd 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -2,16 +2,28 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/charmbracelet/bubbles v0.11.1-0.20220610161724-e57fd292cc68 h1:oDxdCcM/JreVa7RTt2NQLdp06PwkApSL3huTwrOl/ww= -github.com/charmbracelet/bubbles v0.11.1-0.20220610161724-e57fd292cc68/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= +github.com/charmbracelet/bubbles v0.11.1-0.20220622203329-a207867ff1b6 h1:xoDdrxsnmEt9SUVX8t1YBZ6cse426wNzXkqyXXOGzXg= +github.com/charmbracelet/bubbles v0.11.1-0.20220622203329-a207867ff1b6/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= +github.com/charmbracelet/bubbles v0.12.0 h1:fxb9U9yI60Hek3tcPmMTFya5NhvPrqpkpyMaNngFh7A= +github.com/charmbracelet/bubbles v0.12.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= +github.com/charmbracelet/bubbles v0.12.1-0.20220628162924-2fd583c8ef43 h1:phpOTjLqJCSm9q2mW312y+i+iD9Tyfv8v+2eqrZ7h8E= +github.com/charmbracelet/bubbles v0.12.1-0.20220628162924-2fd583c8ef43/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= +github.com/charmbracelet/bubbles v0.12.1-0.20220701153126-7cc578698457 h1:rroce7neSYjLMDTzMCYOYkUxHAFYY7I383UF0hORBo0= +github.com/charmbracelet/bubbles v0.12.1-0.20220701153126-7cc578698457/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g= github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43 h1:xO5Bh21Ii+0p3EYp1GdFEF/Iax7VhBgMbBVCOFBZ2/Q= +github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/charmbracelet/lipgloss v0.5.1-0.20220615005615-2e17a8a06096 h1:ai19sA3Zyg3DARevWCbdLOWt+MfWiE3e8voBqzFOgP8= +github.com/charmbracelet/lipgloss v0.5.1-0.20220615005615-2e17a8a06096/go.mod h1:D7uPgcyfB9T1Ug2mfJOnES17o47nz5oqIzSSVrpcviU= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -48,6 +60,8 @@ github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtl github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898 h1:0j+cbZdhLgpNxjg0nWCasHUA82fgWOXxxGgWNVOLS1I= +github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898/go.mod h1:bN6sPNtkiahdhHv2Xm6RGU16LSCxfbIZvMfqjOCfrR4= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/examples/split-editors/main.go b/examples/split-editors/main.go new file mode 100644 index 0000000..d2872c6 --- /dev/null +++ b/examples/split-editors/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "fmt" + "os" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const ( + initialInputs = 2 + maxInputs = 6 + minInputs = 1 +) + +var ( + cursorLineStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("57")). + Foreground(lipgloss.Color("230")) + + placeholderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("238")) + + focusedPlaceholderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("99")) +) + +type keymap = struct { + next, prev, add, remove, quit key.Binding +} + +func newTextarea() textarea.Model { + t := textarea.New() + t.SetHeight(20) + t.Prompt = "" + t.Placeholder = "Type something" + t.ShowLineNumbers = true + t.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) + t.FocusedStyle.Placeholder = focusedPlaceholderStyle + t.BlurredStyle.Placeholder = placeholderStyle + t.FocusedStyle.CursorLine = cursorLineStyle + t.KeyMap.DeleteWordBackward.SetEnabled(false) + t.KeyMap.LineNext = key.NewBinding(key.WithKeys("down")) + t.KeyMap.LinePrevious = key.NewBinding(key.WithKeys("up")) + t.Blur() + return t +} + +type model struct { + width int + keymap keymap + help help.Model + inputs []textarea.Model + focus int +} + +func newModel() model { + m := model{ + inputs: make([]textarea.Model, initialInputs), + help: help.New(), + keymap: keymap{ + next: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "next"), + ), + prev: key.NewBinding( + key.WithKeys("shift+tab"), + key.WithHelp("shift+tab", "prev"), + ), + add: key.NewBinding( + key.WithKeys("ctrl+n"), + key.WithHelp("ctrl+n", "add an editor"), + ), + remove: key.NewBinding( + key.WithKeys("ctrl+w"), + key.WithHelp("ctrl+w", "remove an editor"), + ), + quit: key.NewBinding( + key.WithKeys("esc", "ctrl+c"), + key.WithHelp("esc", "quit"), + ), + }, + } + for i := 0; i < initialInputs; i++ { + m.inputs[i] = newTextarea() + } + m.inputs[m.focus].Focus() + m.updateKeybindings() + return m +} + +func (m model) Init() tea.Cmd { + return textarea.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.keymap.quit): + for i := range m.inputs { + m.inputs[i].Blur() + } + return m, tea.Quit + case key.Matches(msg, m.keymap.next): + m.inputs[m.focus].Blur() + m.focus++ + if m.focus > len(m.inputs)-1 { + m.focus = 0 + } + cmd := m.inputs[m.focus].Focus() + cmds = append(cmds, cmd) + case key.Matches(msg, m.keymap.prev): + m.inputs[m.focus].Blur() + m.focus-- + if m.focus < 0 { + m.focus = len(m.inputs) - 1 + } + cmd := m.inputs[m.focus].Focus() + cmds = append(cmds, cmd) + case key.Matches(msg, m.keymap.add): + m.inputs = append(m.inputs, newTextarea()) + case key.Matches(msg, m.keymap.remove): + m.inputs = m.inputs[:len(m.inputs)-1] + if m.focus > len(m.inputs)-1 { + m.focus = len(m.inputs) - 1 + } + } + case tea.WindowSizeMsg: + m.width = msg.Width + } + + m.updateKeybindings() + m.sizeInputs() + + // Update all textareas + for i := range m.inputs { + newModel, cmd := m.inputs[i].Update(msg) + m.inputs[i] = newModel + cmds = append(cmds, cmd) + } + + return m, tea.Batch(cmds...) +} + +func (m *model) sizeInputs() { + for i := range m.inputs { + m.inputs[i].SetWidth(m.width / len(m.inputs)) + } +} + +func (m *model) updateKeybindings() { + m.keymap.add.SetEnabled(len(m.inputs) < maxInputs) + m.keymap.remove.SetEnabled(len(m.inputs) > minInputs) +} + +func (m model) View() string { + help := m.help.ShortHelpView([]key.Binding{ + m.keymap.next, + m.keymap.prev, + m.keymap.add, + m.keymap.remove, + m.keymap.quit, + }) + + var views []string + for i := range m.inputs { + views = append(views, m.inputs[i].View()) + } + + return lipgloss.JoinHorizontal(lipgloss.Top, views...) + "\n\n" + help +} + +func main() { + if err := tea.NewProgram(newModel()).Start(); err != nil { + fmt.Println("Error while running program:", err) + os.Exit(1) + } +} diff --git a/examples/textarea/main.go b/examples/textarea/main.go new file mode 100644 index 0000000..3180bfc --- /dev/null +++ b/examples/textarea/main.go @@ -0,0 +1,82 @@ +package main + +// A simple program demonstrating the text input component from the Bubbles +// component library. + +import ( + "fmt" + "log" + + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" +) + +func main() { + p := tea.NewProgram(initialModel()) + + if err := p.Start(); err != nil { + log.Fatal(err) + } +} + +type tickMsg struct{} +type errMsg error + +type model struct { + textarea textarea.Model + err error +} + +func initialModel() model { + ti := textarea.New() + ti.Placeholder = "Once upon a time..." + ti.Focus() + + return model{ + textarea: ti, + err: nil, + } +} + +func (m model) Init() tea.Cmd { + return textarea.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEsc: + if m.textarea.Focused() { + m.textarea.Blur() + } + case tea.KeyCtrlC: + return m, tea.Quit + default: + if !m.textarea.Focused() { + cmd = m.textarea.Focus() + cmds = append(cmds, cmd) + } + } + + // We handle errors just like any other message + case errMsg: + m.err = msg + return m, nil + } + + m.textarea, cmd = m.textarea.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} + +func (m model) View() string { + return fmt.Sprintf( + "Tell me a story.\n\n%s\n\n%s", + m.textarea.View(), + "(ctrl+c to quit)", + ) + "\n\n" +}