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 }