Compare commits
No commits in common. "main" and "0.0.1" have entirely different histories.
8 changed files with 173 additions and 198 deletions
34
README.md
34
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 <command> [args...]
|
||||
|
||||
Commands:
|
||||
|
||||
help
|
||||
Print this help text
|
||||
|
||||
control <config>
|
||||
Read the <config>-file and start listening for global hotkeys
|
||||
|
||||
info <config>
|
||||
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
|
||||
```
|
||||
|
|
|
|||
35
config.go
35
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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -236,7 +236,6 @@ const (
|
|||
)
|
||||
|
||||
var keyCodeMap = map[uint16]string{
|
||||
// 0 must not exist!
|
||||
KEY_ESC: "ESC",
|
||||
KEY_1: "1",
|
||||
KEY_2: "2",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
138
main.go
138
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:] + " <command> [args...]\n\n")
|
||||
fmt.Print("Commands:\n\n")
|
||||
fmt.Print(" help\n")
|
||||
fmt.Print(" Print this help text\n\n")
|
||||
fmt.Print(" control <config>\n")
|
||||
fmt.Print(" Read the <config>-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:] + " <command> [args...]\n\n")
|
||||
fmt.Print("Commands:\n\n")
|
||||
fmt.Print(" help\n")
|
||||
fmt.Print(" Print this help text\n\n")
|
||||
fmt.Print(" control <config>\n")
|
||||
fmt.Print(" Read the <config>-file and start listening for global hotkeys\n\n")
|
||||
fmt.Print(" info <config>\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 <config>\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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
20
xdg.go
20
xdg.go
|
|
@ -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, "/")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue