Add X11 daemon with tray status

This commit is contained in:
Thales Maciel 2026-02-06 11:36:45 -03:00
parent 3506770d09
commit a7f50fed75
19 changed files with 1202 additions and 4 deletions

232
internal/x11/x11.go Normal file
View 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
}