Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ updates:
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "gomod"
directory: "/tutorials"
schedule:
interval: "daily"
time: "08:00"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,8 @@ For some Bubble Tea programs in production, see:
* [fork-cleaner](https://github.com/caarlos0/fork-cleaner): cleans up old and inactive forks in your GitHub account
* [gambit](https://github.com/maaslalani/gambit): play chess in the terminal
* [gembro](https://git.sr.ht/~rafael/gembro): a mouse-driven Gemini browser
* [gh-prs](https://www.github.com/dlvhdr/gh-prs): gh cli extension to display a dashboard of PRs
* [gh-b](https://github.com/joaom00/gh-b): GitHub CLI extension to easily manage your branches
* [gh-dash](https://www.github.com/dlvhdr/gh-dash): GitHub cli extension to display a dashboard of PRs and issues
* [gitflow-toolkit](https://github.com/mritd/gitflow-toolkit): a GitFlow submission tool
* [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser and online markdown stash
* [gocovsh](https://github.com/orlangure/gocovsh): explore Go coverage reports from the CLI
Expand Down
104 changes: 51 additions & 53 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,35 +492,7 @@ func readInputs(input io.Reader) ([]Msg, error) {
return m, nil
}

// Is it a sequence, like an arrow key?
if k, ok := sequences[string(buf[:numBytes])]; ok {
return []Msg{
KeyMsg(k),
}, nil
}

// Some of these need special handling.
hex := fmt.Sprintf("%x", buf[:numBytes])
if k, ok := hexes[hex]; ok {
return []Msg{
KeyMsg(k),
}, nil
}

// Is the alt key pressed? If so, the buffer will be prefixed with an
// escape.
if numBytes > 1 && buf[0] == 0x1b {
// Now remove the initial escape sequence and re-process to get the
// character being pressed in combination with alt.
c, _ := utf8.DecodeRune(buf[1:])
if c == utf8.RuneError {
return nil, errors.New("could not decode rune after removing initial escape")
}
return []Msg{
KeyMsg(Key{Alt: true, Type: KeyRunes, Runes: []rune{c}}),
}, nil
}

var runeSets [][]rune
var runes []rune
b := buf[:numBytes]

Expand All @@ -532,38 +504,64 @@ func readInputs(input io.Reader) ([]Msg, error) {
if r == utf8.RuneError {
return nil, errors.New("could not decode rune")
}

if r == '\x1b' && len(runes) > 1 {
// a new key sequence has started
runeSets = append(runeSets, runes)
runes = []rune{}
}

runes = append(runes, r)
w = width
}
// add the final set of runes we decoded
runeSets = append(runeSets, runes)

if len(runes) == 0 {
if len(runeSets) == 0 {
return nil, errors.New("received 0 runes from input")
} else if len(runes) > 1 {
// We received multiple runes, so we know this isn't a control
// character, sequence, and so on.
return []Msg{
KeyMsg(Key{Type: KeyRunes, Runes: runes}),
}, nil
}

// Is the first rune a control character?
r := KeyType(runes[0])
if numBytes == 1 && r <= keyUS || r == keyDEL {
return []Msg{
KeyMsg(Key{Type: r}),
}, nil
}
var msgs []Msg
for _, runes := range runeSets {
// Is it a sequence, like an arrow key?
if k, ok := sequences[string(runes)]; ok {
msgs = append(msgs, KeyMsg(k))
continue
}

// Some of these need special handling.
hex := fmt.Sprintf("%x", runes)
if k, ok := hexes[hex]; ok {
msgs = append(msgs, KeyMsg(k))
continue
}

// Is the alt key pressed? If so, the buffer will be prefixed with an
// escape.
if len(runes) > 1 && runes[0] == 0x1b {
msgs = append(msgs, KeyMsg(Key{Alt: true, Type: KeyRunes, Runes: runes[1:]}))
continue
}

// If it's a space, override the type with KeySpace (but still include the
// rune).
if runes[0] == ' ' {
return []Msg{
KeyMsg(Key{Type: KeySpace, Runes: runes}),
}, nil
for _, v := range runes {
// Is the first rune a control character?
r := KeyType(v)
if r <= keyUS || r == keyDEL {
msgs = append(msgs, KeyMsg(Key{Type: r}))
continue
}

// If it's a space, override the type with KeySpace (but still include
// the rune).
if r == ' ' {
msgs = append(msgs, KeyMsg(Key{Type: KeySpace, Runes: []rune{v}}))
continue
}

// Welp, just regular, ol' runes.
msgs = append(msgs, KeyMsg(Key{Type: KeyRunes, Runes: []rune{v}}))
}
}

// Welp, it's just a regular, ol' single rune.
return []Msg{
KeyMsg(Key{Type: KeyRunes, Runes: runes}),
}, nil
return msgs, nil
}
120 changes: 105 additions & 15 deletions key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,29 +48,119 @@ func TestKeyTypeString(t *testing.T) {
}

func TestReadInput(t *testing.T) {
for out, in := range map[string][]byte{
"a": {'a'},
"ctrl+a": {byte(keySOH)},
"alt+a": {0x1b, 'a'},
"abcd": {'a', 'b', 'c', 'd'},
"up": []byte("\x1b[A"),
"wheel up": {'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
"shift+tab": {'\x1b', '[', 'Z'},
type test struct {
in []byte
out []Msg
}
for out, td := range map[string]test{
"a": {
[]byte{'a'},
[]Msg{
KeyMsg{
Type: KeyRunes,
Runes: []rune{'a'},
},
},
},
" ": {
[]byte{' '},
[]Msg{
KeyMsg{
Type: KeySpace,
Runes: []rune{' '},
},
},
},
"ctrl+a": {
[]byte{byte(keySOH)},
[]Msg{
KeyMsg{
Type: KeyCtrlA,
},
},
},
"alt+a": {
[]byte{byte(0x1b), 'a'},
[]Msg{
KeyMsg{
Type: KeyRunes,
Alt: true,
Runes: []rune{'a'},
},
},
},
"abcd": {
[]byte{'a', 'b', 'c', 'd'},
[]Msg{
KeyMsg{
Type: KeyRunes,
Runes: []rune{'a'},
},
KeyMsg{
Type: KeyRunes,
Runes: []rune{'b'},
},
KeyMsg{
Type: KeyRunes,
Runes: []rune{'c'},
},
KeyMsg{
Type: KeyRunes,
Runes: []rune{'d'},
},
},
},
"up": {
[]byte("\x1b[A"),
[]Msg{
KeyMsg{
Type: KeyUp,
},
},
},
"wheel up": {
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
[]Msg{
MouseMsg{
Type: MouseWheelUp,
},
},
},
"shift+tab": {
[]byte{'\x1b', '[', 'Z'},
[]Msg{
KeyMsg{
Type: KeyShiftTab,
},
},
},
} {
t.Run(out, func(t *testing.T) {
msgs, err := readInputs(bytes.NewReader(in))
msgs, err := readInputs(bytes.NewReader(td.in))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(msgs) == 0 {
t.Fatalf("unexpected empty message list")
if len(msgs) != len(td.out) {
t.Fatalf("unexpected message list length")
}

if m, ok := msgs[0].(KeyMsg); ok && m.String() != out {
t.Fatalf(`expected a keymsg %q, got %q`, out, m)
if len(msgs) == 1 {
if m, ok := msgs[0].(KeyMsg); ok && m.String() != out {
t.Fatalf(`expected a keymsg %q, got %q`, out, m)
}
}
if m, ok := msgs[0].(MouseMsg); ok && mouseEventTypes[m.Type] != out {
t.Fatalf(`expected a mousemsg %q, got %q`, out, mouseEventTypes[m.Type])

for i, v := range msgs {
if m, ok := v.(KeyMsg); ok &&
m.String() != td.out[i].(KeyMsg).String() {
t.Fatalf(`expected a keymsg %q, got %q`, td.out[i].(KeyMsg), m)
}
if m, ok := v.(MouseMsg); ok &&
(mouseEventTypes[m.Type] != out || m.Type != td.out[i].(MouseMsg).Type) {
t.Fatalf(`expected a mousemsg %q, got %q`,
out,
mouseEventTypes[td.out[i].(MouseMsg).Type])
}
}
})
}
Expand Down
12 changes: 6 additions & 6 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ func (p *Program) shutdown(kill bool) {
// EnterAltScreen enters the alternate screen buffer, which consumes the entire
// terminal window. ExitAltScreen will return the terminal to its former state.
//
// Deprecated. Use the WithAltScreen ProgramOption instead.
// Deprecated: Use the WithAltScreen ProgramOption instead.
func (p *Program) EnterAltScreen() {
p.mtx.Lock()
defer p.mtx.Unlock()
Expand All @@ -616,7 +616,7 @@ func (p *Program) EnterAltScreen() {

// ExitAltScreen exits the alternate screen buffer.
//
// Deprecated. The altscreen will exited automatically when the program exits.
// Deprecated: The altscreen will exited automatically when the program exits.
func (p *Program) ExitAltScreen() {
p.mtx.Lock()
defer p.mtx.Unlock()
Expand All @@ -636,7 +636,7 @@ func (p *Program) ExitAltScreen() {
// EnableMouseCellMotion enables mouse click, release, wheel and motion events
// if a mouse button is pressed (i.e., drag events).
//
// Deprecated. Use the WithMouseCellMotion ProgramOption instead.
// Deprecated: Use the WithMouseCellMotion ProgramOption instead.
func (p *Program) EnableMouseCellMotion() {
p.mtx.Lock()
defer p.mtx.Unlock()
Expand All @@ -646,7 +646,7 @@ func (p *Program) EnableMouseCellMotion() {
// DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be
// called automatically when exiting a Bubble Tea program.
//
// Deprecated. The mouse will automatically be disabled when the program exits.
// Deprecated: The mouse will automatically be disabled when the program exits.
func (p *Program) DisableMouseCellMotion() {
p.mtx.Lock()
defer p.mtx.Unlock()
Expand All @@ -657,7 +657,7 @@ func (p *Program) DisableMouseCellMotion() {
// regardless of whether a mouse button is pressed. Many modern terminals
// support this, but not all.
//
// Deprecated. Use the WithMouseAllMotion ProgramOption instead.
// Deprecated: Use the WithMouseAllMotion ProgramOption instead.
func (p *Program) EnableMouseAllMotion() {
p.mtx.Lock()
defer p.mtx.Unlock()
Expand All @@ -667,7 +667,7 @@ func (p *Program) EnableMouseAllMotion() {
// DisableMouseAllMotion disables All Motion mouse tracking. This will be
// called automatically when exiting a Bubble Tea program.
//
// Deprecated. The mouse will automatically be disabled when the program exits.
// Deprecated: The mouse will automatically be disabled when the program exits.
func (p *Program) DisableMouseAllMotion() {
p.mtx.Lock()
defer p.mtx.Unlock()
Expand Down