232 lines
4.9 KiB
Go
232 lines
4.9 KiB
Go
package x11
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/xgb"
|
|
"github.com/BurntSushi/xgb/xproto"
|
|
"github.com/BurntSushi/xgb/xtest"
|
|
)
|
|
|
|
type Conn struct {
|
|
X *xgb.Conn
|
|
Root xproto.Window
|
|
minKC xproto.Keycode
|
|
maxKC xproto.Keycode
|
|
}
|
|
|
|
func New() (*Conn, error) {
|
|
c, err := xgb.NewConn()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := xtest.Init(c); err != nil {
|
|
c.Close()
|
|
return nil, err
|
|
}
|
|
setup := xproto.Setup(c)
|
|
if setup == nil || len(setup.Roots) == 0 {
|
|
c.Close()
|
|
return nil, errors.New("no X11 screen setup found")
|
|
}
|
|
root := setup.Roots[0].Root
|
|
return &Conn{X: c, Root: root, minKC: setup.MinKeycode, maxKC: setup.MaxKeycode}, nil
|
|
}
|
|
|
|
func (c *Conn) Close() error {
|
|
if c.X == nil {
|
|
return nil
|
|
}
|
|
c.X.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Conn) KeysymToKeycode(target uint32) (xproto.Keycode, error) {
|
|
count := int(c.maxKC-c.minKC) + 1
|
|
if count <= 0 {
|
|
return 0, errors.New("invalid keycode range")
|
|
}
|
|
|
|
reply, err := xproto.GetKeyboardMapping(c.X, c.minKC, byte(count)).Reply()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if reply == nil || reply.KeysymsPerKeycode == 0 {
|
|
return 0, errors.New("no keyboard mapping")
|
|
}
|
|
|
|
per := int(reply.KeysymsPerKeycode)
|
|
targetKS := xproto.Keysym(target)
|
|
for i := 0; i < count; i++ {
|
|
start := i * per
|
|
end := start + per
|
|
for _, ks := range reply.Keysyms[start:end] {
|
|
if ks == targetKS {
|
|
return xproto.Keycode(int(c.minKC) + i), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0, fmt.Errorf("keysym 0x%x not found", target)
|
|
}
|
|
|
|
func (c *Conn) ParseHotkey(keystr string) (uint16, xproto.Keycode, error) {
|
|
parts := strings.Split(keystr, "+")
|
|
if len(parts) == 0 {
|
|
return 0, 0, errors.New("invalid hotkey")
|
|
}
|
|
|
|
var mods uint16
|
|
keyPart := ""
|
|
for _, raw := range parts {
|
|
p := strings.TrimSpace(raw)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
switch strings.ToLower(p) {
|
|
case "shift":
|
|
mods |= xproto.ModMaskShift
|
|
case "ctrl", "control":
|
|
mods |= xproto.ModMaskControl
|
|
case "alt", "mod1":
|
|
mods |= xproto.ModMask1
|
|
case "super", "mod4", "cmd", "command":
|
|
mods |= xproto.ModMask4
|
|
case "mod2":
|
|
mods |= xproto.ModMask2
|
|
case "mod3":
|
|
mods |= xproto.ModMask3
|
|
case "mod5":
|
|
mods |= xproto.ModMask5
|
|
case "lock":
|
|
mods |= xproto.ModMaskLock
|
|
default:
|
|
keyPart = p
|
|
}
|
|
}
|
|
|
|
if keyPart == "" {
|
|
return 0, 0, errors.New("hotkey missing key")
|
|
}
|
|
|
|
ks, ok := keysymFor(keyPart)
|
|
if !ok {
|
|
return 0, 0, fmt.Errorf("unsupported key: %s", keyPart)
|
|
}
|
|
|
|
kc, err := c.KeysymToKeycode(ks)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
return mods, kc, nil
|
|
}
|
|
|
|
func (c *Conn) GrabHotkey(mods uint16, keycode xproto.Keycode) error {
|
|
combos := modifierCombos(mods)
|
|
for _, m := range combos {
|
|
if err := xproto.GrabKeyChecked(c.X, true, c.Root, m, keycode, xproto.GrabModeAsync, xproto.GrabModeAsync).Check(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Conn) UngrabHotkey(mods uint16, keycode xproto.Keycode) {
|
|
combos := modifierCombos(mods)
|
|
for _, m := range combos {
|
|
_ = xproto.UngrabKeyChecked(c.X, keycode, c.Root, m).Check()
|
|
}
|
|
}
|
|
|
|
func (c *Conn) PasteCtrlV() error {
|
|
ctrl, err := c.KeysymToKeycode(0xffe3) // Control_L
|
|
if err != nil {
|
|
return err
|
|
}
|
|
vkey, err := c.KeysymToKeycode(0x76) // 'v'
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := xtest.FakeInputChecked(c.X, xproto.KeyPress, byte(ctrl), 0, xproto.WindowNone, 0, 0, 0).Check(); err != nil {
|
|
return err
|
|
}
|
|
if err := xtest.FakeInputChecked(c.X, xproto.KeyPress, byte(vkey), 0, xproto.WindowNone, 0, 0, 0).Check(); err != nil {
|
|
return err
|
|
}
|
|
if err := xtest.FakeInputChecked(c.X, xproto.KeyRelease, byte(vkey), 0, xproto.WindowNone, 0, 0, 0).Check(); err != nil {
|
|
return err
|
|
}
|
|
if err := xtest.FakeInputChecked(c.X, xproto.KeyRelease, byte(ctrl), 0, xproto.WindowNone, 0, 0, 0).Check(); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = xproto.GetInputFocus(c.X).Reply()
|
|
return err
|
|
}
|
|
|
|
func modifierCombos(base uint16) []uint16 {
|
|
combos := []uint16{base, base | xproto.ModMaskLock, base | xproto.ModMask2, base | xproto.ModMaskLock | xproto.ModMask2}
|
|
return combos
|
|
}
|
|
|
|
func keysymFor(key string) (uint32, bool) {
|
|
k := strings.ToLower(key)
|
|
switch k {
|
|
case "space":
|
|
return 0x20, true
|
|
case "tab":
|
|
return 0xff09, true
|
|
case "return", "enter":
|
|
return 0xff0d, true
|
|
case "escape", "esc":
|
|
return 0xff1b, true
|
|
case "backspace":
|
|
return 0xff08, true
|
|
}
|
|
|
|
if len(k) == 1 {
|
|
ch := k[0]
|
|
if ch >= 'a' && ch <= 'z' {
|
|
return uint32(ch), true
|
|
}
|
|
if ch >= '0' && ch <= '9' {
|
|
return uint32(ch), true
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(k, "f") {
|
|
num := strings.TrimPrefix(k, "f")
|
|
switch num {
|
|
case "1":
|
|
return 0xffbe, true
|
|
case "2":
|
|
return 0xffbf, true
|
|
case "3":
|
|
return 0xffc0, true
|
|
case "4":
|
|
return 0xffc1, true
|
|
case "5":
|
|
return 0xffc2, true
|
|
case "6":
|
|
return 0xffc3, true
|
|
case "7":
|
|
return 0xffc4, true
|
|
case "8":
|
|
return 0xffc5, true
|
|
case "9":
|
|
return 0xffc6, true
|
|
case "10":
|
|
return 0xffc7, true
|
|
case "11":
|
|
return 0xffc8, true
|
|
case "12":
|
|
return 0xffc9, true
|
|
}
|
|
}
|
|
|
|
return 0, false
|
|
}
|