Add basic mouse support

This commit is contained in:
Christian Rocha
2020-06-22 20:30:16 -04:00
parent 3a70ecdde1
commit d86cb562f0
4 changed files with 192 additions and 12 deletions

60
examples/mouse/main.go Normal file
View 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
View File

@@ -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
View 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
View File

@@ -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")
}