Add X11 daemon with tray status
This commit is contained in:
parent
3506770d09
commit
a7f50fed75
19 changed files with 1202 additions and 4 deletions
232
internal/x11/x11.go
Normal file
232
internal/x11/x11.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue