diff --git a/README.md b/README.md index 14e015d..38f8e13 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# keyVent - LibreSplit Global Hotkeys +# (WIP) keyVent - LibreSplit Global Hotkeys > [!WARNING] -> Still in beta stage. Expect bugs! +> This is a work in progress! > [!NOTE] > Only single-key hotkeys are supported. @@ -20,8 +20,7 @@ The program then controls LibreSplit through it's libresplit-ctl unix domain soc This program was tested on -- Debian 13 / Gnome / Wayland -- Ubuntu 24.04 / Gnome / Wayland + X11 +- Debian 13 / Wayland ## Permissions @@ -59,27 +58,14 @@ Use `0` to disable the key bind. ## Usage -``` -keyvent [args...] - -Commands: - - help - Print this help text - - control - Read the -file and start listening for global hotkeys - - info - Show informations about the given config file and the environment - - dumpkeys - Print all keypresses to stdout - -``` - -### Example +Example: ``` keyvent control ./path/to/config.json ``` + +Print Help Text: + +``` +keyvent help +``` diff --git a/config.go b/config.go index 1ed03f2..1fea73c 100644 --- a/config.go +++ b/config.go @@ -2,17 +2,7 @@ package main // Copyright (c) 2026 Julian Müller (ChaoticByte) -import ( - "encoding/json" - "os" -) - -type KeybindFriendlyName struct { - FriendlyName string - Code uint16 -} - -type KeybindsConfig struct { +type KeyBindsConfig struct { StartOrSplit uint16 `json:"start_or_split"` StopOrReset uint16 `json:"stop_or_reset"` Cancel uint16 `json:"cancel"` @@ -21,25 +11,6 @@ type KeybindsConfig struct { CloseLibreSplit uint16 `json:"close_libresplit"` } -func (kbc *KeybindsConfig) FriendlyNames() []KeybindFriendlyName { - return []KeybindFriendlyName { - { "Start/Split", kbc.StartOrSplit}, - { "Stop/Reset", kbc.StopOrReset}, - { "Cancel", kbc.Cancel}, - { "Undo Split", kbc.Unsplit}, - { "Skip Split", kbc.SkipSplit}, - { "Close LibreSplit", kbc.CloseLibreSplit}, - } -} - -var config struct { - Keybinds KeybindsConfig `json:"keybinds"` -} - -func ReadConfig() { - // read config - configData, err := os.ReadFile(os.Args[2]) - if err != nil { panic(err) } - err = json.Unmarshal(configData, &config) - if err != nil { panic(err) } +type Config struct { + KeyBinds KeyBindsConfig `json:"keybinds"` } diff --git a/input_event.go b/input_event.go index 260116b..6fadf2d 100644 --- a/input_event.go +++ b/input_event.go @@ -8,13 +8,45 @@ import ( "unsafe" ) +const ( + // EvSyn is used as markers to separate events. Events may be separated in time or in space, such as with the multitouch protocol. + EvSyn EventType = 0x00 + // EvKey is used to describe state changes of keyboards, buttons, or other key-like devices. + EvKey EventType = 0x01 + // EvRel is used to describe relative axis value changes, e.g. moving the mouse 5 units to the left. + EvRel EventType = 0x02 + // EvAbs is used to describe absolute axis value changes, e.g. describing the coordinates of a touch on a touchscreen. + EvAbs EventType = 0x03 + // EvMsc is used to describe miscellaneous input data that do not fit into other types. + EvMsc EventType = 0x04 + // EvSw is used to describe binary state input switches. + EvSw EventType = 0x05 + // EvLed is used to turn LEDs on devices on and off. + EvLed EventType = 0x11 + // EvSnd is used to output sound to devices. + EvSnd EventType = 0x12 + // EvRep is used for autorepeating devices. + EvRep EventType = 0x14 + // EvFf is used to send force feedback commands to an input device. + EvFf EventType = 0x15 + // EvPwr is a special type for power button and switch input. + EvPwr EventType = 0x16 + // EvFfStatus is used to receive force feedback device status. + EvFfStatus EventType = 0x17 +) + +// EventType are groupings of codes under a logical input construct. +// Each type has a set of applicable codes to be used in generating events. +// See the Ev section for details on valid codes for each type +type EventType uint16 + // eventsize is size of structure of InputEvent var eventsize = int(unsafe.Sizeof(InputEvent{})) // InputEvent is the keyboard event structure itself type InputEvent struct { Time syscall.Timeval - Type uint16 + Type EventType Code uint16 Value int32 } @@ -30,6 +62,16 @@ func (i *InputEvent) KeyPress() bool { return i.Value == 1 } +// KeyRelease is the value when we release the key on keyboard +func (i *InputEvent) KeyRelease() bool { + return i.Value == 0 +} + +// Time of the keypress in nanoseconds +func (i *InputEvent) TimeNano() int64 { + return i.Time.Nano() +} + // KeyEvent is the keyboard event for up/down (press/release) type KeyEvent int32 diff --git a/keylistener.go b/keylistener.go index eaccc17..6f966f9 100644 --- a/keylistener.go +++ b/keylistener.go @@ -13,14 +13,6 @@ import ( "syscall" ) -// system paths -const SysDeviceNamePath = "/sys/class/input/event%d/device/name" -const DevInputEventPath = "/dev/input/event%d" - -// use lowercase names for devices, as we turn the device input name to lower case -var restrictedDevices = devices{"mouse"} -var allowedDevices = devices{"keyboard", "logitech mx keys"} - // KeyListener wrapper around file descriptior type KeyListener struct { fd *os.File @@ -38,6 +30,10 @@ func (d *devices) hasDevice(str string) bool { return false } +// use lowercase names for devices, as we turn the device input name to lower case +var restrictedDevices = devices{"mouse"} +var allowedDevices = devices{"keyboard", "logitech mx keys"} + // New creates a new keylogger for a device path func New(devPath string) (*KeyListener, error) { k := &KeyListener{} @@ -52,13 +48,41 @@ func New(devPath string) (*KeyListener, error) { return k, nil } +// FindKeyboardDevice by going through each device registered on OS +// Mostly it will contain keyword - keyboard +// Returns the file path which contains events +func FindKeyboardDevice() string { + path := "/sys/class/input/event%d/device/name" + resolved := "/dev/input/event%d" + + for i := range 255 { + buff, err := os.ReadFile(fmt.Sprintf(path, i)) + if err != nil { + continue + } + + deviceName := strings.ToLower(string(buff)) + + if restrictedDevices.hasDevice(deviceName) { + continue + } else if allowedDevices.hasDevice(deviceName) { + return fmt.Sprintf(resolved, i) + } + } + + return "" +} + // Like FindKeyboardDevice, but finds all devices which contain keyword 'keyboard' // Returns an array of file paths which contain keyboard events func FindAllKeyboardDevices() []string { + path := "/sys/class/input/event%d/device/name" + resolved := "/dev/input/event%d" + valid := make([]string, 0) - for i := range 255 { - buff, err := os.ReadFile(fmt.Sprintf(SysDeviceNamePath, i)) + for i := 0; i < 255; i++ { + buff, err := os.ReadFile(fmt.Sprintf(path, i)) // prevent from checking non-existant files if os.IsNotExist(err) { @@ -73,7 +97,7 @@ func FindAllKeyboardDevices() []string { if restrictedDevices.hasDevice(deviceName) { continue } else if allowedDevices.hasDevice(deviceName) { - valid = append(valid, fmt.Sprintf(DevInputEventPath, i)) + valid = append(valid, fmt.Sprintf(resolved, i)) } } return valid diff --git a/keymap.go b/keymap.go index d057fcb..82d4e60 100644 --- a/keymap.go +++ b/keymap.go @@ -236,7 +236,6 @@ const ( ) var keyCodeMap = map[uint16]string{ - // 0 must not exist! KEY_ESC: "ESC", KEY_1: "1", KEY_2: "2", diff --git a/libresplit.go b/libresplit.go deleted file mode 100644 index 20fb0ad..0000000 --- a/libresplit.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -// Copyright (c) 2026 Julian Müller (ChaoticByte) - -import ( - "encoding/binary" - "errors" - "fmt" - "net" -) - -const LibreSplitSocket = "libresplit.sock" - -const ( - CmdStartSplit uint32 = 0 - CmdStopReset uint32 = 1 - CmdCancel uint32 = 2 - CmdUnsplit uint32 = 3 - CmdSkip uint32 = 4 - CmdExit uint32 = 5 // close LibreSplit -) - -func LibreSplitSocketPath() string { - return xdg_runtime_dir + "/" + LibreSplitSocket -} - -func EncodeCmd(cmd uint32) []byte { - buf := make([]byte, 8) - binary.BigEndian.PutUint32(buf, 4) // encode length - binary.LittleEndian.PutUint32(buf[4:8], cmd) // encode cmd - return buf -} - -func SendCommand(cmd uint32) { - // try opening socket - var err error - sock, err := net.Dial("unix", LibreSplitSocketPath()) - if err != nil { - fmt.Println("Could not connect to libresplit: ", err) - return - } - // write to socket - msg := EncodeCmd(cmd) - n, err := sock.Write(msg) - // fmt.Println(msg, n, err) - sock.Close() - if err != nil || n != len(msg) { - if err == nil { - err = errors.New("wrong amout of bytes was written") - } - fmt.Println("Could not communicate with libresplit socket: ", err) - } -} diff --git a/main.go b/main.go index 7f36939..68ea863 100644 --- a/main.go +++ b/main.go @@ -3,15 +3,56 @@ package main // Copyright (c) 2026 Julian Müller (ChaoticByte) import ( + "encoding/binary" + "encoding/json" + "errors" "fmt" + "net" "os" "strings" ) -// handlers +const LibreSplitSocket = "libresplit.sock" + +const ( + CmdStartSplit uint32 = 0 + CmdStopReset uint32 = 1 + CmdCancel uint32 = 2 + CmdUnsplit uint32 = 3 + CmdSkip uint32 = 4 + CmdExit uint32 = 5 // close LibreSplit +) + +// global vars + +var config Config +var xdg_runtime_dir string var handler func(e InputEvent) +// + +func PrintHelp() { + arg1 := os.Args[0] + fmt.Print(arg1[strings.LastIndex(arg1, "/")+1:] + " [args...]\n\n") + fmt.Print("Commands:\n\n") + fmt.Print(" help\n") + fmt.Print(" Print this help text\n\n") + fmt.Print(" control \n") + fmt.Print(" Read the -file and start listening for global hotkeys\n\n") + fmt.Print(" dumpkeys\n") + fmt.Print(" Print all keypresses to stdout\n\n") + fmt.Printf("keyvent %s\n", Version) +} + + +func EncodeCmd(cmd uint32) []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint32(buf, 4) // encode length + binary.LittleEndian.PutUint32(buf[4:8], cmd) // encode cmd + return buf +} + func HandleDumpKey(e InputEvent) { if _, found := keyCodeMap[e.Code]; found { fmt.Printf("\nKey: %s, Code: %d\n", e.KeyString(), e.Code) @@ -26,41 +67,40 @@ func HandleControl(e InputEvent) { keybindFound := true var cmd uint32 switch e.Code { - case config.Keybinds.StartOrSplit: + case config.KeyBinds.StartOrSplit: cmd = CmdStartSplit - case config.Keybinds.StopOrReset: + case config.KeyBinds.StopOrReset: cmd = CmdStopReset - case config.Keybinds.Cancel: + case config.KeyBinds.Cancel: cmd = CmdCancel - case config.Keybinds.Unsplit: + case config.KeyBinds.Unsplit: cmd = CmdUnsplit - case config.Keybinds.SkipSplit: + case config.KeyBinds.SkipSplit: cmd = CmdSkip - case config.Keybinds.CloseLibreSplit: + case config.KeyBinds.CloseLibreSplit: cmd = CmdExit default: keybindFound = false } - if keybindFound { - SendCommand(cmd) + if !keybindFound { return } + // try opening socket + var err error + sock, err := net.Dial("unix", xdg_runtime_dir + "/" + LibreSplitSocket) + if err != nil { + fmt.Println("Could not connect to libresplit: ", err) + return + } + // write to socket + msg := EncodeCmd(cmd) + n, err := sock.Write(msg) + // fmt.Println(msg, n, err) + sock.Close() + if err != nil || n != len(msg) { + if err == nil { + err = errors.New("wrong amout of bytes was written") + } + fmt.Println("Could not communicate with libresplit socket: ", err) } -} - -// cli - -func PrintHelp() { - arg1 := os.Args[0] - fmt.Print(arg1[strings.LastIndex(arg1, "/")+1:] + " [args...]\n\n") - fmt.Print("Commands:\n\n") - fmt.Print(" help\n") - fmt.Print(" Print this help text\n\n") - fmt.Print(" control \n") - fmt.Print(" Read the -file and start listening for global hotkeys\n\n") - fmt.Print(" info \n") - fmt.Print(" Show informations about the given config file and the environment\n\n") - fmt.Print(" dumpkeys\n") - fmt.Print(" Print all keypresses to stdout\n\n") - fmt.Printf("keyvent %s\n", Version) } func main() { @@ -74,7 +114,7 @@ func main() { PrintHelp() os.Exit(0) } - if (os.Args[1] == "control" || os.Args[1] == "info") && len(os.Args) < 3 { + if os.Args[1] == "control" && len(os.Args) < 3 { fmt.Print("Missing argument \n\n") PrintHelp() os.Exit(1) @@ -82,35 +122,19 @@ func main() { switch os.Args[1] { case "control": - ReadConfig() - DetectXdgRuntimeDir() + // read config + configData, err := os.ReadFile(os.Args[2]) + if err != nil { panic(err) } + err = json.Unmarshal(configData, &config) + if err != nil { panic(err) } + // get xdg_runtime_dir + xdg_runtime_dir = os.Getenv("XDG_RUNTIME_DIR") + if xdg_runtime_dir == "" { + xdg_runtime_dir = fmt.Sprintf("/run/user/%d", os.Getuid()) + } + xdg_runtime_dir = strings.TrimRight(xdg_runtime_dir, "/") // set handler handler = HandleControl - case "info": - ReadConfig() - DetectXdgRuntimeDir() - fmt.Println("Keybinds:") - for _, fn := range config.Keybinds.FriendlyNames() { - keyname := "unset" - keyname_, found := keyCodeMap[fn.Code] - if found { - keyname = keyname_ - } - fmt.Printf(" %s: %s (%d)\n", fn.FriendlyName, keyname, fn.Code) - } - lsSockPath := LibreSplitSocketPath() - fmt.Printf("LibreSplit Socket: %s\n", lsSockPath) - lsStatus := "" - _, err := os.Stat(lsSockPath) - if os.IsNotExist(err) { - lsStatus = "not running" - } else if err != nil { - lsStatus = fmt.Sprintf("? (%s)", err.Error()) - } else { - lsStatus = "running" - } - fmt.Printf("LibreSplit: %s\n", lsStatus) - return case "dumpkeys": handler = HandleDumpKey default: @@ -127,12 +151,13 @@ func main() { panic(err) } c := kl.Read() - defer kl.Close() defer close(c) go func () { for { e := <- c - if e.KeyPress() { handler(e) } + if e.KeyPress() { + handler(e) + } } }() } @@ -140,4 +165,5 @@ func main() { for { // wait fmt.Scanln() } + } diff --git a/xdg.go b/xdg.go deleted file mode 100644 index 3fffd75..0000000 --- a/xdg.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -// Copyright (c) 2026 Julian Müller (ChaoticByte) - -import ( - "fmt" - "os" - "strings" -) - -var xdg_runtime_dir string - -func DetectXdgRuntimeDir() { - // get xdg_runtime_dir - xdg_runtime_dir = os.Getenv("XDG_RUNTIME_DIR") - if xdg_runtime_dir == "" { - xdg_runtime_dir = fmt.Sprintf("/run/user/%d", os.Getuid()) - } - xdg_runtime_dir = strings.TrimRight(xdg_runtime_dir, "/") -}