From 60cd54967c9cdf8a60803f6ac503c0177a0ea792 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 10 Feb 2026 17:47:06 +0100 Subject: [PATCH 1/7] Clean up and remove obsolete code --- input_event.go | 33 --------------------------------- keylistener.go | 47 +++++++++++++---------------------------------- main.go | 3 ++- 3 files changed, 15 insertions(+), 68 deletions(-) diff --git a/input_event.go b/input_event.go index 6fadf2d..52a5085 100644 --- a/input_event.go +++ b/input_event.go @@ -8,45 +8,12 @@ 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 EventType Code uint16 Value int32 } diff --git a/keylistener.go b/keylistener.go index 6f966f9..3cb84c4 100644 --- a/keylistener.go +++ b/keylistener.go @@ -13,6 +13,15 @@ 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 @@ -30,9 +39,7 @@ 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) { @@ -48,41 +55,13 @@ 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 := 0; i < 255; i++ { - buff, err := os.ReadFile(fmt.Sprintf(path, i)) + for i := range 255 { + buff, err := os.ReadFile(fmt.Sprintf(SysDeviceNamePath, i)) // prevent from checking non-existant files if os.IsNotExist(err) { @@ -97,7 +76,7 @@ func FindAllKeyboardDevices() []string { if restrictedDevices.hasDevice(deviceName) { continue } else if allowedDevices.hasDevice(deviceName) { - valid = append(valid, fmt.Sprintf(resolved, i)) + valid = append(valid, fmt.Sprintf(DevInputEventPath, i)) } } return valid diff --git a/main.go b/main.go index 68ea863..85a6ee0 100644 --- a/main.go +++ b/main.go @@ -151,6 +151,7 @@ func main() { panic(err) } c := kl.Read() + defer kl.Close() defer close(c) go func () { for { @@ -165,5 +166,5 @@ func main() { for { // wait fmt.Scanln() } - + } From c22bb0bca0abf7b7947abfef368ace59210b41cd Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 10 Feb 2026 17:59:32 +0100 Subject: [PATCH 2/7] Split up code from main.go into multiple files --- config.go | 15 +++++++- libresplit.go | 49 ++++++++++++++++++++++++++ main.go | 95 ++++++++++++--------------------------------------- xdg.go | 20 +++++++++++ 4 files changed, 104 insertions(+), 75 deletions(-) create mode 100644 libresplit.go create mode 100644 xdg.go diff --git a/config.go b/config.go index 1fea73c..2b455cb 100644 --- a/config.go +++ b/config.go @@ -1,5 +1,10 @@ package main +import ( + "encoding/json" + "os" +) + // Copyright (c) 2026 Julian Müller (ChaoticByte) type KeyBindsConfig struct { @@ -11,6 +16,14 @@ type KeyBindsConfig struct { CloseLibreSplit uint16 `json:"close_libresplit"` } -type Config struct { +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) } +} diff --git a/libresplit.go b/libresplit.go new file mode 100644 index 0000000..3a2b4a1 --- /dev/null +++ b/libresplit.go @@ -0,0 +1,49 @@ +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 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", 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) + } +} diff --git a/main.go b/main.go index 85a6ee0..3e5eb3e 100644 --- a/main.go +++ b/main.go @@ -3,56 +3,15 @@ package main // Copyright (c) 2026 Julian Müller (ChaoticByte) import ( - "encoding/binary" - "encoding/json" - "errors" "fmt" - "net" "os" "strings" ) -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 +// handlers 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) @@ -83,24 +42,23 @@ func HandleControl(e InputEvent) { keybindFound = false } 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) - } + // send command to libresplit + SendCommand(cmd) +} + +// 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(" dumpkeys\n") + fmt.Print(" Print all keypresses to stdout\n\n") + fmt.Printf("keyvent %s\n", Version) } func main() { @@ -122,17 +80,8 @@ func main() { switch os.Args[1] { case "control": - // 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, "/") + ReadConfig() + DetectXdgRuntimeDir() // set handler handler = HandleControl case "dumpkeys": @@ -156,9 +105,7 @@ func main() { go func () { for { e := <- c - if e.KeyPress() { - handler(e) - } + if e.KeyPress() { handler(e) } } }() } diff --git a/xdg.go b/xdg.go new file mode 100644 index 0000000..3fffd75 --- /dev/null +++ b/xdg.go @@ -0,0 +1,20 @@ +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, "/") +} From 2581f682e05a5416ce5549a7052affb14aedcbaa Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 10 Feb 2026 18:05:45 +0100 Subject: [PATCH 3/7] Delete redundant newlines --- config.go | 4 ++-- keylistener.go | 3 --- main.go | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/config.go b/config.go index 2b455cb..cd890bb 100644 --- a/config.go +++ b/config.go @@ -1,12 +1,12 @@ package main +// Copyright (c) 2026 Julian Müller (ChaoticByte) + import ( "encoding/json" "os" ) -// Copyright (c) 2026 Julian Müller (ChaoticByte) - type KeyBindsConfig struct { StartOrSplit uint16 `json:"start_or_split"` StopOrReset uint16 `json:"stop_or_reset"` diff --git a/keylistener.go b/keylistener.go index 3cb84c4..eaccc17 100644 --- a/keylistener.go +++ b/keylistener.go @@ -21,7 +21,6 @@ const DevInputEventPath = "/dev/input/event%d" var restrictedDevices = devices{"mouse"} var allowedDevices = devices{"keyboard", "logitech mx keys"} - // KeyListener wrapper around file descriptior type KeyListener struct { fd *os.File @@ -39,8 +38,6 @@ func (d *devices) hasDevice(str string) bool { return false } - - // New creates a new keylogger for a device path func New(devPath string) (*KeyListener, error) { k := &KeyListener{} diff --git a/main.go b/main.go index 3e5eb3e..4269156 100644 --- a/main.go +++ b/main.go @@ -113,5 +113,4 @@ func main() { for { // wait fmt.Scanln() } - } From ba39573ee0c5c8493809ebd03f40a820e92cca37 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 10 Feb 2026 18:16:58 +0100 Subject: [PATCH 4/7] Remove InputEvent.KeyRelease() and InputEvent.TimeNano() --- input_event.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/input_event.go b/input_event.go index 52a5085..03c649f 100644 --- a/input_event.go +++ b/input_event.go @@ -29,16 +29,6 @@ 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 From 159359688a1c1a3ddc8f77a14d2fee0060503abb Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 10 Feb 2026 18:57:34 +0100 Subject: [PATCH 5/7] Add info command, fix KeyEvent struct --- config.go | 20 ++++++++++++++++++-- input_event.go | 1 + keymap.go | 1 + libresplit.go | 6 +++++- main.go | 47 +++++++++++++++++++++++++++++++++++++---------- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/config.go b/config.go index cd890bb..1ed03f2 100644 --- a/config.go +++ b/config.go @@ -7,7 +7,12 @@ import ( "os" ) -type KeyBindsConfig struct { +type KeybindFriendlyName struct { + FriendlyName string + Code uint16 +} + +type KeybindsConfig struct { StartOrSplit uint16 `json:"start_or_split"` StopOrReset uint16 `json:"stop_or_reset"` Cancel uint16 `json:"cancel"` @@ -16,8 +21,19 @@ 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"` + Keybinds KeybindsConfig `json:"keybinds"` } func ReadConfig() { diff --git a/input_event.go b/input_event.go index 03c649f..260116b 100644 --- a/input_event.go +++ b/input_event.go @@ -14,6 +14,7 @@ var eventsize = int(unsafe.Sizeof(InputEvent{})) // InputEvent is the keyboard event structure itself type InputEvent struct { Time syscall.Timeval + Type uint16 Code uint16 Value int32 } diff --git a/keymap.go b/keymap.go index 82d4e60..d057fcb 100644 --- a/keymap.go +++ b/keymap.go @@ -236,6 +236,7 @@ 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 index 3a2b4a1..20fb0ad 100644 --- a/libresplit.go +++ b/libresplit.go @@ -20,6 +20,10 @@ const ( 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 @@ -30,7 +34,7 @@ func EncodeCmd(cmd uint32) []byte { func SendCommand(cmd uint32) { // try opening socket var err error - sock, err := net.Dial("unix", xdg_runtime_dir + "/" + LibreSplitSocket) + sock, err := net.Dial("unix", LibreSplitSocketPath()) if err != nil { fmt.Println("Could not connect to libresplit: ", err) return diff --git a/main.go b/main.go index 4269156..7f36939 100644 --- a/main.go +++ b/main.go @@ -26,24 +26,24 @@ 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 { return } - // send command to libresplit - SendCommand(cmd) + if keybindFound { + SendCommand(cmd) + } } // cli @@ -56,6 +56,8 @@ func PrintHelp() { 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) @@ -72,7 +74,7 @@ func main() { PrintHelp() os.Exit(0) } - if os.Args[1] == "control" && len(os.Args) < 3 { + if (os.Args[1] == "control" || os.Args[1] == "info") && len(os.Args) < 3 { fmt.Print("Missing argument \n\n") PrintHelp() os.Exit(1) @@ -84,6 +86,31 @@ func main() { DetectXdgRuntimeDir() // 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: From 93f8dbf3718b1aa4e617c8c12665929430db1cfe Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 10 Feb 2026 19:00:54 +0100 Subject: [PATCH 6/7] Improve README --- README.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 38f8e13..0675d12 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# (WIP) keyVent - LibreSplit Global Hotkeys +# keyVent - LibreSplit Global Hotkeys > [!WARNING] -> This is a work in progress! +> Still in beta stage. Expect bugs! > [!NOTE] > Only single-key hotkeys are supported. @@ -58,14 +58,27 @@ Use `0` to disable the key bind. ## Usage -Example: +``` +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 ``` keyvent control ./path/to/config.json ``` - -Print Help Text: - -``` -keyvent help -``` From 99aa53a3370a99011625ac9a7c19913734567923 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 17 Feb 2026 17:18:45 +0100 Subject: [PATCH 7/7] Update Compatibility section in Readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0675d12..14e015d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ The program then controls LibreSplit through it's libresplit-ctl unix domain soc This program was tested on -- Debian 13 / Wayland +- Debian 13 / Gnome / Wayland +- Ubuntu 24.04 / Gnome / Wayland + X11 ## Permissions