diff --git a/examples/mouse/main.go b/examples/mouse/main.go new file mode 100644 index 0000000..ca5152d --- /dev/null +++ b/examples/mouse/main.go @@ -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 +} diff --git a/key.go b/key.go index f15bc25..c16dcb4 100644 --- a/key.go +++ b/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 } diff --git a/mouse.go b/mouse.go new file mode 100644 index 0000000..89ce32e --- /dev/null +++ b/mouse.go @@ -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 +} diff --git a/tea.go b/tea.go index b60e933..5c4df51 100644 --- a/tea.go +++ b/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") +}