Skip to content

Commit

Permalink
feat(textarea): Add multi-line text input
Browse files Browse the repository at this point in the history
  • Loading branch information
maaslalani committed Jun 28, 2022
1 parent 42f85b4 commit 2fd583c
Show file tree
Hide file tree
Showing 4 changed files with 1,785 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Expand Up @@ -41,6 +41,18 @@ the common, and many customization options.
* [Example code, one field](https://github.com/charmbracelet/tea/tree/master/examples/textinput/main.go)
* [Example code, many fields](https://github.com/charmbracelet/tea/tree/master/examples/textinputs/main.go)

## Text Area

<img src="https://stuff.charm.sh/bubbles-examples/textarea.gif" width="400" alt="Text Area Example">

A text area field, akin to an `<textarea />` in HTML. Allows for input that
spans multiple lines. Supports unicode, pasting, vertical scrolling when the
value exceeds the width and height of the element, and many customization
options.

* [Example code, chat input](https://github.com/charmbracelet/tea/tree/master/examples/chat/main.go)
* [Example code, story time input](https://github.com/charmbracelet/tea/tree/master/examples/textarea/main.go)


## Progress

Expand Down
207 changes: 207 additions & 0 deletions cursor/cursor.go
@@ -0,0 +1,207 @@
package cursor

import (
"context"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

const defaultBlinkSpeed = time.Millisecond * 530

// initialBlinkMsg initializes cursor blinking.
type initialBlinkMsg struct{}

// BlinkMsg signals that the cursor should blink. It contains metadata that
// allows us to tell if the blink message is the one we're expecting.
type BlinkMsg struct {
id int
tag int
}

// blinkCanceled is sent when a blink operation is canceled.
type blinkCanceled struct{}

// blinkCtx manages cursor blinking.
type blinkCtx struct {
ctx context.Context
cancel context.CancelFunc
}

// Mode describes the behavior of the cursor.
type Mode int

// Available cursor modes.
const (
CursorBlink Mode = iota
CursorStatic
CursorHide
)

// String returns the cursor mode in a human-readable format. This method is
// provisional and for informational purposes only.
func (c Mode) String() string {
return [...]string{
"blink",
"static",
"hidden",
}[c]
}

// Model is the Bubble Tea model for this cursor element.
type Model struct {
BlinkSpeed time.Duration
// Style for styling the cursor block.
Style lipgloss.Style
// TextStyle is the style used for the cursor when it is hidden (when blinking).
// I.e. displaying normal text.
TextStyle lipgloss.Style

// char is the character under the cursor
char string
// The ID of this Model as it relates to other cursors
id int
// focus indicates whether the containing input is focused
focus bool
// Cursor Blink state.
Blink bool
// Used to manage cursor blink
blinkCtx *blinkCtx
// The ID of the blink message we're expecting to receive.
blinkTag int
// cursorMode determines the behavior of the cursor
cursorMode Mode
}

// New creates a new model with default settings.
func New() Model {
return Model{
BlinkSpeed: defaultBlinkSpeed,

Blink: true,
cursorMode: CursorBlink,

blinkCtx: &blinkCtx{
ctx: context.Background(),
},
}
}

// Update updates the cursor.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case initialBlinkMsg:
// We accept all initialBlinkMsgs generated by the Blink command.

if m.cursorMode != CursorBlink || !m.focus {
return m, nil
}

cmd := m.BlinkCmd()
return m, cmd

case BlinkMsg:
// We're choosy about whether to accept blinkMsgs so that our cursor
// only exactly when it should.

// Is this model blink-able?
if m.cursorMode != CursorBlink || !m.focus {
return m, nil
}

// Were we expecting this blink message?
if msg.id != m.id || msg.tag != m.blinkTag {
return m, nil
}

var cmd tea.Cmd
if m.cursorMode == CursorBlink {
m.Blink = !m.Blink
cmd = m.BlinkCmd()
}
return m, cmd

case blinkCanceled: // no-op
return m, nil
}
return m, nil
}

// CursorMode returns the model's cursor mode. For available cursor modes, see
// type CursorMode.
func (m Model) CursorMode() Mode {
return m.cursorMode
}

// SetCursorMode sets the model's cursor mode. This method returns a command.
//
// For available cursor modes, see type CursorMode.
func (m *Model) SetCursorMode(mode Mode) tea.Cmd {
m.cursorMode = mode
m.Blink = m.cursorMode == CursorHide || !m.focus
if mode == CursorBlink {
return Blink
}
return nil
}

// BlinkCmd is an command used to manage cursor blinking.
func (m *Model) BlinkCmd() tea.Cmd {
if m.cursorMode != CursorBlink {
return nil
}

if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
m.blinkCtx.cancel()
}

ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
m.blinkCtx.cancel = cancel

m.blinkTag++

return func() tea.Msg {
defer cancel()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return BlinkMsg{id: m.id, tag: m.blinkTag}
}
return blinkCanceled{}
}
}

// Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg {
return initialBlinkMsg{}
}

// Focus focuses the cursor to allow it to blink if desired.
func (m *Model) Focus() tea.Cmd {
m.focus = true
m.Blink = m.cursorMode == CursorHide // show the cursor unless we've explicitly hidden it

if m.cursorMode == CursorBlink && m.focus {
return m.BlinkCmd()
}
return nil
}

// Blur blurs the cursor.
func (m *Model) Blur() {
m.focus = false
m.Blink = true
}

// SetChar sets the character under the cursor.
func (m *Model) SetChar(char string) {
m.char = char
}

// View displays the cursor.
func (m Model) View() string {
if m.Blink {
return m.TextStyle.Render(m.char)
}
return m.Style.Inline(true).Reverse(true).Render(m.char)
}

0 comments on commit 2fd583c

Please sign in to comment.