Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for GPM Clicking #297

Open
TravisDart opened this issue Aug 1, 2019 · 18 comments
Open

Support for GPM Clicking #297

TravisDart opened this issue Aug 1, 2019 · 18 comments

Comments

@TravisDart
Copy link

Browsh is a console browser that depends on tcell. There's an issue in the browsh repository to support GPM clicking, and the conclusion there is that this is ultimately a tcell issue. I searched in this repo, but didn't see a corresponding issue, so I thought I would create one.

(GPM provides mouse/clicking support for tty terminals.)

@gdamore
Copy link
Owner

gdamore commented Aug 1, 2019

I'm aware of GPM, but I have no idea how it works. It seems like in an ideal world it would just cause additional escape sequences to sent to the terminal. That may or may not be what happens.

@gdamore
Copy link
Owner

gdamore commented Aug 1, 2019

Looking at the Linux docs for gpm, it appears that gpm is a daemon that injects the events into the terminal so that they are reported in an xterm compatible fashion.

So, it may just (from tcell's perspective) be a matter of the user running gpm in the background, and having the necessary terminfo description. It's possible that I can just do this easily myself -- I can try with a VM and see what happens.

@jackdoe
Copy link

jackdoe commented Mar 16, 2020

i couldnt find how gpm injects events into the terminal, besides when pasting or reading the selection for which it uses /dev/console (docs are quite sparse), seems like most of the terminal programs use /dev/gpmctl to interact with it (also ncurses uses /dev/gpmctl)

i wrote https://github.com/jackdoe/go-gpmctl by reading how daemon/do_client.c in gpm works, it is fresh out of the oven but it seems to work on my laptop

@jackdoe
Copy link

jackdoe commented Mar 17, 2020

since i am trying to go back to the tty; browsh is pretty much my only option, so i have a local fork of tcell that does this hack in tscreen.go:

import "github.com/jackdoe/go-gpmctl"
...
func (t *tScreen) mainLoop() {
	go func() {
		gpm, err := gpmctl.NewGPM(gpmctl.DefaultConf)
		if err != nil {
			panic(err)
		}
		for {
			event, err := gpm.Read()
			if err != nil {
				panic(err)
			}

			btn := 35
			mM := 'M'
			if event.Type&(gpmctl.DOWN|gpmctl.UP) > 0 {
				if event.Buttons&(gpmctl.B_LEFT) > 0 {
					btn = 0
				}

				if event.Buttons&(gpmctl.B_MIDDLE) > 0 {
					btn = 1
				}

				if event.Buttons&(gpmctl.B_RIGHT) > 0 {
					btn = 2
				}

				if event.Type&gpmctl.UP > 0 {
					mM = 'm'
				}
			}

			s := fmt.Sprintf("%d;%d;%d%c", btn, event.X, event.Y, mM)
			b := []byte{27, 91, 60}
			b = append(b, []byte(s)...)
			t.keychan <- b
		}
	}()
....

i am just playing with, but its working, at least i can follow links
browsh is quite nice in --monochrome, though i dont have standalone mouse atm and gpm is not detecting two-finger scrolling, so i have to use the arrows to scroll

@gdamore
Copy link
Owner

gdamore commented Jan 25, 2021

A possibly somewhat cleaner way might be to use screen.PostEvent() to post an actual EventMouse.

You basically construct one with NewEventMouse and then inject into the event stream with screen.PostEvent().

@00c0
Copy link

00c0 commented Mar 15, 2022

How can I implement it?

@00c0
Copy link

00c0 commented Mar 25, 2022

@gdamore I'm still looking for help. Please may you help me know to what file this needs to be added. Please may you give more detail than your previous comment.

@slukits
Copy link

slukits commented Dec 4, 2022

Below gpm package which is MIT licensed (https://opensource.org/license/mit/) adds gpm support to tcell. Usage:

// import "path/to/gpm"

func main() {
	scr, err := tcell.NewScreen()
	if err != nil {
		panic(fmt.Sprintf("can't obtain screen: %v", err))
	}
	if err := scr.Init(); err != nil {
		panic(fmt.Sprintf("can't init screen: %v", err))
	}
	scr, haveGPM := gpm.WarpGPMSupport(scr)
	if !haveGPM {
		scr.EnableMouse()
	}
	for {
		evt := scr.PollEvent()
		if evt == nil {
			return
		}
		switch evt := evt.(type) {
			// process events
		}
	}
}

A patch for tcell could be in screen.go

// NewScreen returns a default Screen suitable for the user's terminal
// environment.
func NewScreen() (Screen, error) {
	// Windows is happier if we try for a console screen first.
	if s, _ := NewConsoleScreen(); s != nil {
		return s, nil
	} else if s, e := NewTerminfoScreen(); s != nil {
		s, _ = gpm.WrapGPMSupport(s)
		return s, nil
	} else {
		return nil, e
	}
}
package gpm

import (
	"os"

	"github.com/gdamore/tcell/v2"
	"github.com/jackdoe/go-gpmctl"
)

// scr enables to embedded tcell.Screen privately
type scr tcell.Screen

type gpmReader interface {
	Read() (gpmctl.Event, error)
}

// WrapGPMSupport either returns given screen scr and false if we are
// not in a linux console or no gpm support is found.  Otherwise scr is
// wrapped by GPMScreen reporting gpm mouse events and providing a mouse
// cursor.  Optionally further environment TERM-strings which should be checked
// for gpm support can be provided.
func WarpGPMSupport(scr tcell.Screen, tt ...string) (
	_ tcell.Screen, haveGPM bool,
) {
	tt = append([]string{"linux"}, tt...)
	properTerminal, envterm := false, os.Getenv("TERM")
	for _, t := range tt {
		if t != envterm {
			continue
		}
		properTerminal = true
		break
	}
	if !properTerminal {
		return scr, false
	}
	gpm, err := gpmctl.NewGPM(gpmctl.GPMConnect{
		EventMask:   gpmctl.ANY,
		DefaultMask: ^gpmctl.HARD,
		MinMod:      0,
		MaxMod:      ^uint16(0),
	})
	if err != nil {
		return scr, false
	}
	scr = &GPMScreen{scr: scr, gpm: gpm}
	go gpmReporter(scr.(*GPMScreen))
	return scr, true
}

type GPMScreen struct {
	gpm gpmReader
	scr
	x, y int
}

func (gpm *GPMScreen) PollEvent() tcell.Event {
	evt := gpm.scr.PollEvent()
	switch evt := evt.(type) {
	case *tcell.EventMouse:
		// update the mouse cursor
		gpm.update(evt)
	case *tcell.EventKey:
		// remove mouse cursor on keyboard input
		gpm.reset()
	}
	return evt
}

func (gpm *GPMScreen) GetContent(x, y int) (
	primary rune, combining []rune, style tcell.Style, width int,
) {
	if x != gpm.x || y != gpm.y {
		return gpm.scr.GetContent(x, y)
	}

	primary, combining, style, width = gpm.scr.GetContent(x, y)
	return primary, combining, switchReverseAttribute(style), width
}

func (gpm *GPMScreen) SetContent(
	x, y int, primary rune, combining []rune, style tcell.Style,
) {
	if x != gpm.x || y != gpm.y {
		gpm.scr.SetContent(x, y, primary, combining, style)
		return
	}

	gpm.scr.SetContent(
		x, y, primary, combining, switchReverseAttribute(style))
}

func (gpm *GPMScreen)  EnableMouse() {}

func (gpm *GPMScreen) update(evt *tcell.EventMouse) {
	gpm.switchCursor(false)
	gpm.x, gpm.y = evt.Position()
	gpm.switchCursor(true)
}

func (gpm *GPMScreen) reset() {
	gpm.switchCursor(true)
	gpm.x, gpm.y = -1, -1
}

func (gpm *GPMScreen) switchCursor(show bool) {
	r, _, sty, _ := gpm.scr.GetContent(gpm.x, gpm.y)
	if r == 0 {
		return
	}
	sty = switchReverseAttribute(sty)
	gpm.scr.SetContent(gpm.x, gpm.y, r, nil, sty)
	if show {
		gpm.Show()
	}
}

func switchReverseAttribute(sty tcell.Style) tcell.Style {
	_, _, aa := sty.Decompose()
	if aa&tcell.AttrReverse != 0 {
		aa &^= tcell.AttrReverse
	} else {
		aa |= tcell.AttrReverse
	}
	return sty.Attributes(aa)
}

func gpmReporter(scr *GPMScreen) {
	baseTypes := (gpmctl.MOVE | gpmctl.DOWN | gpmctl.UP | gpmctl.DRAG)
	for {
		evt, err := scr.gpm.Read()
		if err != nil {
			continue
		}
		x, y := int(evt.X-1), int(evt.Y-1)
		switch evt.Type & baseTypes {
		case gpmctl.MOVE:
			scr.PostEvent(tcell.NewEventMouse(
				x, y,
				gpmButtonsToTcell(evt.Buttons),
				gpmModifiersToTcell(int(evt.Modifiers)),
			))
		case gpmctl.DOWN:
			scr.PostEvent(tcell.NewEventMouse(
				x, y,
				gpmButtonsToTcell(evt.Buttons),
				gpmModifiersToTcell(int(evt.Modifiers)),
			))
		case gpmctl.UP:
			scr.PostEvent(tcell.NewEventMouse(
				x, y,
				tcell.ButtonNone,
				gpmModifiersToTcell(int(evt.Modifiers)),
			))
		case gpmctl.DRAG:
			scr.PostEvent(tcell.NewEventMouse(
				x, y,
				gpmButtonsToTcell(evt.Buttons),
				gpmModifiersToTcell(int(evt.Modifiers)),
			))
		}
	}
}

var gpmTcellModifiers = map[int]tcell.ModMask{
	1: tcell.ModShift,
	4: tcell.ModCtrl,
	8: tcell.ModAlt,
}

func gpmModifiersToTcell(m int) (tm tcell.ModMask) {
	for _, _m := range []int{1, 4, 8} {
		if m&_m == 0 {
			continue
		}
		tm = tm | gpmTcellModifiers[_m]
	}
	return tm
}

var gpmTcellButtons = map[gpmctl.Buttons]tcell.ButtonMask{
	gpmctl.B_LEFT:   tcell.Button1,
	gpmctl.B_RIGHT:  tcell.Button2,
	gpmctl.B_MIDDLE: tcell.Button3,
	gpmctl.B_FOURTH: tcell.Button4,
	gpmctl.B_UP:     tcell.WheelUp,
	gpmctl.B_DOWN:   tcell.WheelDown,
}

var gpmButtons = []gpmctl.Buttons{gpmctl.B_LEFT, gpmctl.B_RIGHT, gpmctl.B_MIDDLE,
	gpmctl.B_FOURTH, gpmctl.B_UP, gpmctl.B_DOWN}

func gpmButtonsToTcell(b gpmctl.Buttons) (tb tcell.ButtonMask) {
	for _, _b := range gpmButtons {
		if b&_b != _b {
			continue
		}
		tb |= gpmTcellButtons[_b]
	}
	return
}

@literon36
Copy link

any updates regarding this issue?

@slukits
Copy link

slukits commented Nov 23, 2023

the gpm package works for me; it doesn't look like as if it gets patched into tcell.

@literon36
Copy link

I just came here from browsh, which is missing GPM support due to this dependency not implementing it.
Since browsh relies on interactions via mouse, it is currently impossible to interact with browsh in a tty.

I sadly dont know Go, so I dont think I can really help you fix it...
Just out of interest; why didnt you make a PR with your fix @slukits ?

@ryannoblett
Copy link

+1 on this, I have the same use case of wanting to use browsh on a TTY. @slukits code above appears well-thought-out, but it's not clear to me which files should be patched to implement it. Anyone able to put this into a pull request?

@gdamore
Copy link
Owner

gdamore commented Dec 3, 2023

I'll take a look at this -- it seems like the approach is sound, but maybe we only want to do it on linux.

I have questions about how gpmctl works if the user is not actually on the console though -- it would be unfortunate if this became incompatible with coexisting with X sessions from SSH or something like that.

@gdamore
Copy link
Owner

gdamore commented Dec 10, 2023

I was going to see if I could just import the file that @slukits wrote, but it lacks any kind of a license statement, so I cannot just import it. I have some ideas though, so stay tuned.

@gdamore
Copy link
Owner

gdamore commented Dec 10, 2023

I meant this one, that lacks a license but otherwise looks nice: https://github.com/jackdoe/go-gpmctl

@jackdoe
Copy link

jackdoe commented Dec 10, 2023

I meant this one, that lacks a license but otherwise looks nice: https://github.com/jackdoe/go-gpmctl

ah no problem, i added MIT license https://github.com/jackdoe/go-gpmctl/blob/master/LICENSE.md

@slukits
Copy link

slukits commented Dec 16, 2023 via email

@gdamore
Copy link
Owner

gdamore commented Dec 16, 2023

MIT license is fine. I will probably build something on this. I just have to find time amongst all my other priorities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants