vendor/github.com/charmbracelet/bubbletea/v2/.golangci.yml 🔗
@@ -7,7 +7,6 @@ linters:
- exhaustive
- goconst
- godot
- - godox
- gomoddirectives
- goprintffuncname
- gosec
Ayman Bagabas created
vendor/github.com/charmbracelet/bubbletea/v2/.golangci.yml | 1
vendor/github.com/charmbracelet/bubbletea/v2/README.md | 1
vendor/github.com/charmbracelet/bubbletea/v2/cursed_renderer.go | 15
vendor/github.com/charmbracelet/bubbletea/v2/exec.go | 4
vendor/github.com/charmbracelet/bubbletea/v2/nil_renderer.go | 3
vendor/github.com/charmbracelet/bubbletea/v2/renderer.go | 3
vendor/github.com/charmbracelet/bubbletea/v2/standard_renderer.go | 3
vendor/github.com/charmbracelet/bubbletea/v2/tea.go | 21
vendor/github.com/charmbracelet/ultraviolet/event.go | 36
vendor/github.com/charmbracelet/ultraviolet/parse.go | 8
vendor/github.com/charmbracelet/ultraviolet/terminal_reader.go | 43
vendor/github.com/charmbracelet/ultraviolet/terminal_reader_windows.go | 84
vendor/modules.txt | 10
13 files changed, 172 insertions(+), 60 deletions(-)
@@ -7,7 +7,6 @@ linters:
- exhaustive
- goconst
- godot
- - godox
- gomoddirectives
- goprintffuncname
- gosec
@@ -10,7 +10,6 @@
<a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/bubbletea?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg" alt="Build Status"></a>
- <a href="https://www.phorm.ai/query?projectId=a0e324b6-b706-4546-b951-6671ea60c13f"><img src="https://stuff.charm.sh/misc/phorm-badge.svg" alt="phorm.ai"></a>
</p>
The fun, functional and stateful way to build terminal apps. A Go framework
@@ -104,6 +104,21 @@ func (s *cursedRenderer) writeString(str string) (int, error) {
return s.scr.WriteString(str)
}
+// resetLinesRendered implements renderer.
+func (s *cursedRenderer) resetLinesRendered() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if !s.altScreen {
+ var frameHeight int
+ if s.lastFrame != nil {
+ frameHeight = strings.Count(*s.lastFrame, "\n") + 1
+ }
+
+ io.WriteString(s.w, strings.Repeat("\n", max(0, frameHeight-1))) //nolint:errcheck,gosec
+ }
+}
+
// flush implements renderer.
func (s *cursedRenderer) flush(p *Program) error {
s.mu.Lock()
@@ -114,6 +114,7 @@ func (p *Program) exec(c ExecCommand, fn ExecCallback) {
// Execute system command.
if err := c.Run(); err != nil {
+ p.renderer.resetLinesRendered()
_ = p.RestoreTerminal() // also try to restore the terminal.
if fn != nil {
go p.Send(fn(err))
@@ -121,6 +122,9 @@ func (p *Program) exec(c ExecCommand, fn ExecCallback) {
return
}
+ // Maintain the existing output from the command
+ p.renderer.resetLinesRendered()
+
// Have the program re-capture input.
err := p.RestoreTerminal()
if fn != nil {
@@ -67,4 +67,5 @@ func (n nilRenderer) setBackgroundColor(color.Color) {}
func (n nilRenderer) setWindowTitle(string) {}
// hit implements the Renderer interface.
-func (n nilRenderer) hit(MouseMsg) []Msg { return nil }
+func (n nilRenderer) hit(MouseMsg) []Msg { return nil }
+func (n nilRenderer) resetLinesRendered() {}
@@ -71,6 +71,9 @@ type renderer interface {
repaint()
writeString(string) (int, error)
+
+ // resetLinesRendered ensures exec output remains on screen on exit
+ resetLinesRendered()
}
// repaintMsg forces a full repaint.
@@ -382,3 +382,6 @@ func (r *standardRenderer) setForegroundColor(c color.Color) {}
func (r *standardRenderer) setBackgroundColor(c color.Color) {}
func (r *standardRenderer) setWindowTitle(s string) {}
func (r *standardRenderer) hit(MouseMsg) []Msg { return nil }
+func (r *standardRenderer) resetLinesRendered() {
+ r.linesRendered = 0
+}
@@ -1220,13 +1220,22 @@ func (p *Program) recoverFromPanic(r any) {
default:
}
p.cancel() // Just in case a previous shutdown has failed.
- s := strings.ReplaceAll(
- fmt.Sprintf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r),
- "\n", "\r\n")
- fmt.Fprintln(os.Stderr, s)
- stack := strings.ReplaceAll(fmt.Sprintf("%s", debug.Stack()), "\n", "\r\n")
- fmt.Fprintln(os.Stderr, stack)
p.shutdown(true)
+ // We use "\r\n" to ensure the output is formatted even when restoring the
+ // terminal does not work or when raw mode is still active.
+ rec := strings.ReplaceAll(fmt.Sprintf("%s", r), "\n", "\r\n")
+ fmt.Fprintf(os.Stderr, "Caught panic:\r\n\r\n%s\r\n\r\nRestoring terminal...\r\n\r\n", rec)
+ stack := strings.ReplaceAll(fmt.Sprintf("%s\n", debug.Stack()), "\n", "\r\n")
+ fmt.Fprint(os.Stderr, stack)
+ if v, err := strconv.ParseBool(os.Getenv("TEA_DEBUG")); err == nil && v {
+ f, err := os.Create(fmt.Sprintf("bubbletea-panic-%d.log", time.Now().Unix()))
+ if err == nil {
+ defer f.Close() //nolint:errcheck
+ fmt.Fprintln(f, rec) //nolint:errcheck
+ fmt.Fprintln(f) //nolint:errcheck
+ fmt.Fprintln(f, stack) //nolint:errcheck
+ }
+ }
}
// ReleaseTerminal restores the original terminal state and cancels the input
@@ -131,6 +131,24 @@ func (k KeyPressEvent) String() string {
return Key(k).String()
}
+// Keystroke returns the keystroke representation of the [Key]. While less type
+// safe than looking at the individual fields, it will usually be more
+// convenient and readable to use this method when matching against keys.
+//
+// Note that modifier keys are always printed in the following order:
+// - ctrl
+// - alt
+// - shift
+// - meta
+// - hyper
+// - super
+//
+// For example, you'll always see "ctrl+shift+alt+a" and never
+// "shift+ctrl+alt+a".
+func (k KeyPressEvent) Keystroke() string {
+ return Key(k).Keystroke()
+}
+
// Key returns the underlying key event. This is a syntactic sugar for casting
// the key event to a [Key].
func (k KeyPressEvent) Key() Key {
@@ -163,6 +181,24 @@ func (k KeyReleaseEvent) String() string {
return Key(k).String()
}
+// Keystroke returns the keystroke representation of the [Key]. While less type
+// safe than looking at the individual fields, it will usually be more
+// convenient and readable to use this method when matching against keys.
+//
+// Note that modifier keys are always printed in the following order:
+// - ctrl
+// - alt
+// - shift
+// - meta
+// - hyper
+// - super
+//
+// For example, you'll always see "ctrl+shift+alt+a" and never
+// "shift+ctrl+alt+a".
+func (k KeyReleaseEvent) Keystroke() string {
+ return Key(k).Keystroke()
+}
+
// Key returns the underlying key event. This is a convenience method and
// syntactic sugar to satisfy the [KeyEvent] interface, and cast the key event to
// [Key].
@@ -748,10 +748,12 @@ func (p *SequenceParser) parseSs3(b []byte) (int, Event) {
}
func (p *SequenceParser) parseOsc(b []byte) (int, Event) {
- defaultKey := KeyPressEvent{Code: rune(b[1]), Mod: ModAlt}
+ defaultKey := func() KeyPressEvent {
+ return KeyPressEvent{Code: rune(b[1]), Mod: ModAlt}
+ }
if len(b) == 2 && b[0] == ansi.ESC {
// short cut if this is an alt+] key
- return 2, defaultKey
+ return 2, defaultKey()
}
var i int
@@ -802,7 +804,7 @@ func (p *SequenceParser) parseOsc(b []byte) (int, Event) {
case ansi.ESC:
if i >= len(b) || b[i] != '\\' {
if cmd == -1 || (start == 0 && end == 2) {
- return 2, defaultKey
+ return 2, defaultKey()
}
// If we don't have a valid ST terminator, then this is a
@@ -55,14 +55,15 @@ type TerminalReader struct {
// Windows Console API.
MouseMode *MouseMode
- // Timeout is the escape character timeout duration. Most escape sequences
- // start with an escape character [ansi.ESC] and are followed by one or
- // more characters. If the next character is not received within this
- // timeout, the reader will assume that the escape sequence is complete and
- // will process the received characters as a complete escape sequence.
+ // EscTimeout is the escape character timeout duration. Most escape
+ // sequences start with an escape character [ansi.ESC] and are followed by
+ // one or more characters. If the next character is not received within
+ // this timeout, the reader will assume that the escape sequence is
+ // complete and will process the received characters as a complete escape
+ // sequence.
//
// By default, this is set to [DefaultEscTimeout] (50 milliseconds).
- Timeout time.Duration
+ EscTimeout time.Duration
r io.Reader
rd cancelreader.CancelReader
@@ -85,6 +86,7 @@ type TerminalReader struct {
// prevent multiple calls to the Close() method.
closed bool
started bool // started indicates whether the reader has been started.
+ runOnce sync.Once // runOnce is used to ensure that the reader is only started once.
close chan struct{} // close is a channel used to signal the reader to close.
closeOnce sync.Once
notify chan []byte // notify is a channel used to notify the reader of new input events.
@@ -113,10 +115,10 @@ type TerminalReader struct {
// }
func NewTerminalReader(r io.Reader, termType string) *TerminalReader {
return &TerminalReader{
- Timeout: DefaultEscTimeout,
- r: r,
- term: termType,
- lookup: true, // Use lookup table by default.
+ EscTimeout: DefaultEscTimeout,
+ r: r,
+ term: termType,
+ lookup: true, // Use lookup table by default.
}
}
@@ -131,22 +133,20 @@ func (d *TerminalReader) SetLogger(logger Logger) {
// lookup table for key sequences if it is not already set. This function
// should be called before reading input events.
func (d *TerminalReader) Start() (err error) {
- if d.rd == nil {
- d.rd, err = newCancelreader(d.r)
- if err != nil {
- return err
- }
+ d.rd, err = newCancelreader(d.r)
+ if err != nil {
+ return err
}
if d.table == nil {
d.table = buildKeysTable(d.Legacy, d.term, d.UseTerminfo)
}
d.started = true
d.esc.Store(false)
- d.timeout = time.NewTimer(d.Timeout)
+ d.timeout = time.NewTimer(d.EscTimeout)
d.notify = make(chan []byte)
d.close = make(chan struct{}, 1)
d.closeOnce = sync.Once{}
- go d.run()
+ d.runOnce = sync.Once{}
return nil
}
@@ -194,6 +194,11 @@ func (d *TerminalReader) receiveEvents(ctx context.Context, events chan<- Event)
return ErrReaderNotStarted
}
+ // Start the reader loop if it hasn't been started yet.
+ d.runOnce.Do(func() {
+ go d.run()
+ })
+
closingFunc := func() error {
// If we're closing, make sure to send any remaining events even if
// they are incomplete.
@@ -255,7 +260,7 @@ func (d *TerminalReader) run() {
esc := n > 0 && n <= 2 && readBuf[0] == ansi.ESC
if esc {
d.esc.Store(true)
- d.timeout.Reset(d.Timeout)
+ d.timeout.Reset(d.EscTimeout)
}
d.notify <- readBuf[:n]
@@ -305,7 +310,7 @@ LOOP:
ansi.ESC, ansi.CSI, ansi.OSC, ansi.DCS, ansi.APC, ansi.SOS, ansi.PM,
}, d.buf[0]) {
d.esc.Store(true)
- d.timeout.Reset(d.Timeout)
+ d.timeout.Reset(d.EscTimeout)
}
}
// If this is the entire buffer, we can break and assume this
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"strings"
+ "time"
"unicode"
"unicode/utf16"
"unicode/utf8"
@@ -22,7 +23,7 @@ import (
// given event channel.
func (d *TerminalReader) ReceiveEvents(ctx context.Context, events chan<- Event) error {
for {
- evs, err := d.handleConInput(readConsoleInput)
+ evs, err := d.handleConInput()
if errors.Is(err, errNotConInputReader) {
return d.receiveEvents(ctx, events)
}
@@ -41,31 +42,45 @@ func (d *TerminalReader) ReceiveEvents(ctx context.Context, events chan<- Event)
var errNotConInputReader = fmt.Errorf("handleConInput: not a conInputReader")
-func (d *TerminalReader) handleConInput(
- finput func(windows.Handle, []xwindows.InputRecord) (uint32, error),
-) ([]Event, error) {
+func (d *TerminalReader) handleConInput() ([]Event, error) {
cc, ok := d.rd.(*conInputReader)
if !ok {
return nil, errNotConInputReader
}
- // read up to 256 events, this is to allow for sequences events reported as
- // key events.
- var events [256]xwindows.InputRecord
- _, err := finput(cc.conin, events[:])
- if err != nil {
+ var (
+ events []xwindows.InputRecord
+ err error
+ )
+ for {
+ // Peek up to 256 events, this is to allow for sequences events reported as
+ // key events.
+ events, err = peekNConsoleInputs(cc.conin, 256)
if cc.isCanceled() {
return nil, cancelreader.ErrCanceled
}
+ if err != nil {
+ return nil, fmt.Errorf("peek coninput events: %w", err)
+ }
+ if len(events) > 0 {
+ break
+ }
+
+ // Sleep for a bit to avoid busy waiting.
+ time.Sleep(10 * time.Millisecond)
+ }
+
+ events, err = readNConsoleInputs(cc.conin, uint32(len(events)))
+ if cc.isCanceled() {
+ return nil, cancelreader.ErrCanceled
+ }
+ if err != nil {
return nil, fmt.Errorf("read coninput events: %w", err)
}
var evs []Event
for _, event := range events {
if e := d.SequenceParser.parseConInputEvent(event, &d.keyState, d.MouseMode, d.logger); e != nil {
- if e == nil {
- continue
- }
if multi, ok := e.(MultiEvent); ok {
if d.logger != nil {
for _, ev := range multi {
@@ -223,6 +238,16 @@ func highWord(data uint32) uint16 {
return uint16((data & 0xFFFF0000) >> 16) //nolint:gosec
}
+func readNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) {
+ if maxEvents == 0 {
+ return nil, fmt.Errorf("maxEvents cannot be zero")
+ }
+
+ records := make([]xwindows.InputRecord, maxEvents)
+ n, err := readConsoleInput(console, records)
+ return records[:n], err
+}
+
func readConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) {
if len(inputRecords) == 0 {
return 0, fmt.Errorf("size of input record buffer cannot be zero")
@@ -235,7 +260,6 @@ func readConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecor
return read, err //nolint:wrapcheck
}
-//nolint:unused
func peekConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) {
if len(inputRecords) == 0 {
return 0, fmt.Errorf("size of input record buffer cannot be zero")
@@ -248,6 +272,16 @@ func peekConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecor
return read, err //nolint:wrapcheck
}
+func peekNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) {
+ if maxEvents == 0 {
+ return nil, fmt.Errorf("maxEvents cannot be zero")
+ }
+
+ records := make([]xwindows.InputRecord, maxEvents)
+ n, err := peekConsoleInput(console, records)
+ return records[:n], err
+}
+
// parseWin32InputKeyEvent parses a single key event from either the Windows
// Console API or win32-input-mode events. When state is nil, it means this is
// an event from win32-input-mode. Otherwise, it's a key event from the Windows
@@ -506,7 +540,9 @@ func (p *SequenceParser) parseWin32InputKeyEvent(state *win32InputState, vkc uin
var text string
keyCode := baseCode
- if !unicode.IsControl(r) {
+ if unicode.IsControl(r) {
+ return p.parseControl(byte(r))
+ } else {
rw := utf8.EncodeRune(utf8Buf[:], r)
keyCode, _ = utf8.DecodeRune(utf8Buf[:rw])
if unicode.IsPrint(keyCode) && (cks == 0 ||
@@ -517,18 +553,18 @@ func (p *SequenceParser) parseWin32InputKeyEvent(state *win32InputState, vkc uin
// then the key event is a printable event i.e. [text] is not empty.
text = string(keyCode)
}
- }
- key.Code = keyCode
- key.Text = text
- key.Mod = translateControlKeyState(cks)
- key.BaseCode = baseCode
- key = ensureKeyCase(key, cks)
- if keyDown {
- return KeyPressEvent(key)
- }
+ key.Code = keyCode
+ key.Text = text
+ key.Mod = translateControlKeyState(cks)
+ key.BaseCode = baseCode
+ key = ensureKeyCase(key, cks)
+ if keyDown {
+ return KeyPressEvent(key)
+ }
- return KeyReleaseEvent(key)
+ return KeyReleaseEvent(key)
+ }
}
// ensureKeyCase ensures that the key's text is in the correct case based on the
@@ -254,7 +254,7 @@ github.com/charmbracelet/bubbles/v2/spinner
github.com/charmbracelet/bubbles/v2/textarea
github.com/charmbracelet/bubbles/v2/textinput
github.com/charmbracelet/bubbles/v2/viewport
-# github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250703182356-a42fb608faaf
+# github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250708145940-f4b2ad3636f9
## explicit; go 1.24.3
github.com/charmbracelet/bubbletea/v2
# github.com/charmbracelet/colorprofile v0.3.1
@@ -269,7 +269,7 @@ github.com/charmbracelet/glamour/v2
github.com/charmbracelet/glamour/v2/ansi
github.com/charmbracelet/glamour/v2/internal/autolink
github.com/charmbracelet/glamour/v2/styles
-# github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250703152138-ff346e83e819
+# github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250708150236-b6de769f3a51
## explicit; go 1.24.2
github.com/charmbracelet/lipgloss/v2
github.com/charmbracelet/lipgloss/v2/table
@@ -277,7 +277,7 @@ github.com/charmbracelet/lipgloss/v2/tree
# github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
## explicit; go 1.19
github.com/charmbracelet/log/v2
-# github.com/charmbracelet/ultraviolet v0.0.0-20250707134318-0fdaa64b8c5e
+# github.com/charmbracelet/ultraviolet v0.0.0-20250708144633-4a8e4329a1a0
## explicit; go 1.24.0
github.com/charmbracelet/ultraviolet
# github.com/charmbracelet/x/ansi v0.9.3
@@ -838,5 +838,5 @@ mvdan.cc/sh/v3/fileutil
mvdan.cc/sh/v3/interp
mvdan.cc/sh/v3/pattern
mvdan.cc/sh/v3/syntax
-# github.com/charmbracelet/bubbletea/v2 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250703182356-a42fb608faaf
-# github.com/charmbracelet/lipgloss/v2 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250703152138-ff346e83e819
+# github.com/charmbracelet/bubbletea/v2 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250708145940-f4b2ad3636f9
+# github.com/charmbracelet/lipgloss/v2 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250708150236-b6de769f3a51