diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e43381c77c..c622d76ceb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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: diff --git a/README.md b/README.md index 9cc4e61773..4ee6a9f076 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/key.go b/key.go index e2415a994f..c21037677d 100644 --- a/key.go +++ b/key.go @@ -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] @@ -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 } diff --git a/key_test.go b/key_test.go index 511ae75b76..b74f06b53b 100644 --- a/key_test.go +++ b/key_test.go @@ -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]) + } } }) } diff --git a/tea.go b/tea.go index 73be9bdc2b..fcbf822af4 100644 --- a/tea.go +++ b/tea.go @@ -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() @@ -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() @@ -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() @@ -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() @@ -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() @@ -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()