mirror of
https://github.com/taigrr/bubbletea.git
synced 2026-04-02 02:59:09 -07:00
Add basic mouse support
This commit is contained in:
60
examples/mouse/main.go
Normal file
60
examples/mouse/main.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func main() {
|
||||
p := tea.NewProgram(initialize, update, view)
|
||||
|
||||
p.EnterAltScreen()
|
||||
defer p.ExitAltScreen()
|
||||
p.EnableMouseAllMotion()
|
||||
defer p.DisableMouseAllMotion()
|
||||
|
||||
if err := p.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type model struct {
|
||||
init bool
|
||||
mouseEvent tea.MouseEvent
|
||||
}
|
||||
|
||||
func initialize() (tea.Model, tea.Cmd) {
|
||||
return model{}, nil
|
||||
}
|
||||
|
||||
func update(msg tea.Msg, mdl tea.Model) (tea.Model, tea.Cmd) {
|
||||
m, _ := mdl.(model)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if msg.Type == tea.KeyCtrlC || (msg.Type == tea.KeyRune && msg.Rune == 'q') {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case tea.MouseMsg:
|
||||
m.init = true
|
||||
m.mouseEvent = tea.MouseEvent(msg)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func view(mdl tea.Model) string {
|
||||
m, _ := mdl.(model)
|
||||
|
||||
s := "Do mouse stuff. When you're done press q to quit.\n\n"
|
||||
|
||||
if m.init {
|
||||
e := m.mouseEvent
|
||||
s += fmt.Sprintf("(X: %d, Y: %d) %s", e.X, e.Y, e)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
27
key.go
27
key.go
@@ -227,15 +227,22 @@ var hexes = map[string]Key{
|
||||
"1b5b313b3344": {Type: KeyLeft, Alt: true},
|
||||
}
|
||||
|
||||
// ReadKey reads keypress input from a TTY and returns a string representation
|
||||
// of a key.
|
||||
func ReadKey(r io.Reader) (Key, error) {
|
||||
// ReadInput reads keypress and mouse input from a TTY and returns a message
|
||||
// containing information about the key or mouse event accordingly
|
||||
func ReadInput(r io.Reader) (Msg, error) {
|
||||
var buf [256]byte
|
||||
|
||||
// Read and block
|
||||
numBytes, err := r.Read(buf[:])
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See if it's a mouse event. For now we're parsing X10-type mouse events
|
||||
// only.
|
||||
mouseEvent, err := parseX10MouseEvent(buf[:numBytes])
|
||||
if err == nil {
|
||||
return MouseMsg(mouseEvent), nil
|
||||
}
|
||||
|
||||
hex := fmt.Sprintf("%x", buf[:numBytes])
|
||||
@@ -248,17 +255,17 @@ func ReadKey(r io.Reader) (Key, error) {
|
||||
// Get unicode value
|
||||
char, _ := utf8.DecodeRune(buf[:])
|
||||
if char == utf8.RuneError {
|
||||
return Key{}, errors.New("could not decode rune")
|
||||
return nil, errors.New("could not decode rune")
|
||||
}
|
||||
|
||||
// Is it a control character?
|
||||
if numBytes == 1 && char <= keyUS || char == keyDEL {
|
||||
return Key{Type: KeyType(char)}, nil
|
||||
return KeyMsg(Key{Type: KeyType(char)}), nil
|
||||
}
|
||||
|
||||
// Is it a special sequence, like an arrow key?
|
||||
if k, ok := sequences[string(buf[:numBytes])]; ok {
|
||||
return Key{Type: k}, nil
|
||||
return KeyMsg(Key{Type: k}), nil
|
||||
}
|
||||
|
||||
// Is the alt key pressed? The buffer will be prefixed with an escape
|
||||
@@ -268,11 +275,11 @@ func ReadKey(r io.Reader) (Key, error) {
|
||||
// character.
|
||||
c, _ := utf8.DecodeRune(buf[1:])
|
||||
if c == utf8.RuneError {
|
||||
return Key{}, errors.New("could not decode rune after removing initial escape")
|
||||
return nil, errors.New("could not decode rune after removing initial escape")
|
||||
}
|
||||
return Key{Alt: true, Type: KeyRune, Rune: c}, nil
|
||||
return KeyMsg(Key{Alt: true, Type: KeyRune, Rune: c}), nil
|
||||
}
|
||||
|
||||
// Just a regular, ol' rune
|
||||
return Key{Type: KeyRune, Rune: char}, nil
|
||||
return KeyMsg(Key{Type: KeyRune, Rune: char}), nil
|
||||
}
|
||||
|
||||
89
mouse.go
Normal file
89
mouse.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package tea
|
||||
|
||||
import "errors"
|
||||
|
||||
type MouseMsg MouseEvent
|
||||
|
||||
type MouseEvent struct {
|
||||
X int
|
||||
Y int
|
||||
Button MouseButton
|
||||
Alt bool
|
||||
Ctrl bool
|
||||
}
|
||||
|
||||
func (m MouseEvent) String() (s string) {
|
||||
if m.Ctrl {
|
||||
s += "ctrl+"
|
||||
}
|
||||
if m.Alt {
|
||||
s += "alt+"
|
||||
}
|
||||
s += mouseButtonNames[m.Button]
|
||||
return s
|
||||
}
|
||||
|
||||
type MouseButton int
|
||||
|
||||
const (
|
||||
MouseLeft MouseButton = iota
|
||||
MouseRight
|
||||
MouseMiddle
|
||||
MouseRelease
|
||||
MouseWheelUp
|
||||
MouseWheelDown
|
||||
MouseMotion
|
||||
)
|
||||
|
||||
var mouseButtonNames = map[MouseButton]string{
|
||||
MouseLeft: "left",
|
||||
MouseRight: "right",
|
||||
MouseMiddle: "middle",
|
||||
MouseRelease: "release",
|
||||
MouseWheelUp: "wheel up",
|
||||
MouseWheelDown: "wheel down",
|
||||
MouseMotion: "motion",
|
||||
}
|
||||
|
||||
// Parse an X10-encoded mouse event. The simplest kind. The last release of
|
||||
// X10 was December 1986, by the way.
|
||||
func parseX10MouseEvent(buf []byte) (m MouseEvent, err error) {
|
||||
if len(buf) != 6 || string(buf[:3]) != "\x1b[M" {
|
||||
return m, errors.New("not an X10 mouse event")
|
||||
}
|
||||
|
||||
e := buf[3] - 32
|
||||
|
||||
switch e {
|
||||
case 35:
|
||||
m.Button = MouseMotion
|
||||
case 64:
|
||||
m.Button = MouseWheelUp
|
||||
case 65:
|
||||
m.Button = MouseWheelDown
|
||||
default:
|
||||
switch e & 3 {
|
||||
case 0:
|
||||
m.Button = MouseLeft
|
||||
case 1:
|
||||
m.Button = MouseMiddle
|
||||
case 2:
|
||||
m.Button = MouseRight
|
||||
case 3:
|
||||
m.Button = MouseRelease
|
||||
}
|
||||
}
|
||||
|
||||
if e&8 != 0 {
|
||||
m.Alt = true
|
||||
}
|
||||
if e&16 != 0 {
|
||||
m.Ctrl = true
|
||||
}
|
||||
|
||||
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
|
||||
m.X = int(buf[4]) - 32 - 1
|
||||
m.Y = int(buf[5]) - 32 - 1
|
||||
|
||||
return m, nil
|
||||
}
|
||||
28
tea.go
28
tea.go
@@ -119,11 +119,11 @@ func (p *Program) Start() error {
|
||||
// Subscribe to user input
|
||||
go func() {
|
||||
for {
|
||||
msg, err := ReadKey(os.Stdin)
|
||||
msg, err := ReadInput(os.Stdin)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
}
|
||||
msgs <- KeyMsg(msg)
|
||||
msgs <- msg
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -201,3 +201,27 @@ func (p *Program) ExitAltScreen() {
|
||||
defer p.mtx.Unlock()
|
||||
fmt.Fprintf(p.output, te.CSI+te.ExitAltScreenSeq)
|
||||
}
|
||||
|
||||
func (p *Program) EnableMouseCellMotion() {
|
||||
p.mtx.Lock()
|
||||
defer p.mtx.Unlock()
|
||||
fmt.Fprintf(p.output, te.CSI+"?1002h")
|
||||
}
|
||||
|
||||
func (p *Program) DisableMouseCellMotion() {
|
||||
p.mtx.Lock()
|
||||
defer p.mtx.Unlock()
|
||||
fmt.Fprintf(p.output, te.CSI+"?1002l")
|
||||
}
|
||||
|
||||
func (p *Program) EnableMouseAllMotion() {
|
||||
p.mtx.Lock()
|
||||
defer p.mtx.Unlock()
|
||||
fmt.Fprintf(p.output, te.CSI+"?1003h")
|
||||
}
|
||||
|
||||
func (p *Program) DisableMouseAllMotion() {
|
||||
p.mtx.Lock()
|
||||
defer p.mtx.Unlock()
|
||||
fmt.Fprintf(p.output, te.CSI+"?1003l")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user