Skip to content

Instantly share code, notes, and snippets.

@shift
Created December 7, 2025 11:23
Show Gist options
  • Select an option

  • Save shift/2f08399fca76d34c189915b8e93372e4 to your computer and use it in GitHub Desktop.

Select an option

Save shift/2f08399fca76d34c189915b8e93372e4 to your computer and use it in GitHub Desktop.
Virtual HID keyboard macro for Linux
package main
import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"gopkg.in/yaml.v3"
)
// --- HID Constants & Structures ---
const (
UHID_CREATE2 = 11
UHID_INPUT2 = 12
)
var keyboardReportDesc = []byte{
0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, 0x01,
0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, 0x75, 0x01,
0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01, 0x95, 0x06,
0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00, 0xc0,
}
type Create2Req struct {
Name [128]byte
Phys [64]byte
Uniq [64]byte
RdSize uint16
Bus uint16
Vendor uint32
Product uint32
Version uint32
Country uint32
RdData [4096]byte
}
// --- Configuration Structures ---
type Config struct {
Name string `yaml:"name"`
Steps []Step `yaml:"steps"`
}
type Step struct {
Action string `yaml:"action"` // type, press, combo, wait
Text string `yaml:"text,omitempty"` // For "type"
Key string `yaml:"key,omitempty"` // For "press"/"combo"
Modifiers []string `yaml:"modifiers,omitempty"` // For "combo"
Duration string `yaml:"duration,omitempty"` // For "wait"
}
// --- Key Mappings ---
var hidMap = map[string]byte{
"A": 0x04, "B": 0x05, "C": 0x06, "D": 0x07, "E": 0x08, "F": 0x09, "G": 0x0a, "H": 0x0b,
"I": 0x0c, "J": 0x0d, "K": 0x0e, "L": 0x0f, "M": 0x10, "N": 0x11, "O": 0x12, "P": 0x13,
"Q": 0x14, "R": 0x15, "S": 0x16, "T": 0x17, "U": 0x18, "V": 0x19, "W": 0x1a, "X": 0x1b,
"Y": 0x1c, "Z": 0x1d, "1": 0x1e, "2": 0x1f, "3": 0x20, "4": 0x21, "5": 0x22, "6": 0x23,
"7": 0x24, "8": 0x25, "9": 0x26, "0": 0x27,
"ENTER": 0x28, "ESC": 0x29, "BACKSPACE": 0x2a, "TAB": 0x2b, "SPACE": 0x2c,
"-": 0x2d, "=": 0x2e, "[": 0x2f, "]": 0x30, "\\": 0x31, ";": 0x33, "'": 0x34,
",": 0x36, ".": 0x37, "/": 0x38,
"F1": 0x3a, "F2": 0x3b, "F3": 0x3c, "F4": 0x3d, "F5": 0x3e, "F6": 0x3f,
"F7": 0x40, "F8": 0x41, "F9": 0x42, "F10": 0x43, "F11": 0x44, "F12": 0x45,
"RIGHT": 0x4f, "LEFT": 0x50, "DOWN": 0x51, "UP": 0x52,
}
var modMap = map[string]byte{
"CTRL": 0x01, "LCTRL": 0x01,
"SHIFT": 0x02, "LSHIFT": 0x02,
"ALT": 0x04, "LALT": 0x04,
"GUI": 0x08, "SUPER": 0x08, "META": 0x08,
"RCTRL": 0x10, "RSHIFT": 0x20, "RALT": 0x40, "RGUI": 0x80,
}
// --- Logic ---
func main() {
configFile := flag.String("c", "macro.yaml", "Path to YAML configuration file")
flag.Parse()
// 1. Load Config
cfgData, err := ioutil.ReadFile(*configFile)
if err != nil {
log.Fatalf("Error reading config: %v", err)
}
var config Config
if err := yaml.Unmarshal(cfgData, &config); err != nil {
log.Fatalf("Error parsing YAML: %v", err)
}
// 2. Init UHID
f, err := os.OpenFile("/dev/uhid", os.O_RDWR, 0)
if err != nil {
log.Fatalf("Failed to open /dev/uhid: %v", err)
}
defer f.Close()
if err := createKeyboard(f); err != nil {
log.Fatalf("Failed to create keyboard: %v", err)
}
// Keep alive reader
go func() {
buf := make([]byte, 4096)
for {
f.Read(buf)
}
}()
fmt.Printf("Device '%s' active. Executing sequence '%s' in 2 seconds...\n", "Go Automator", config.Name)
time.Sleep(2 * time.Second)
// 3. Execute Steps
for i, step := range config.Steps {
fmt.Printf("[%d] Executing %s...\n", i, step.Action)
if err := executeStep(f, step); err != nil {
log.Printf("Step failed: %v", err)
}
}
fmt.Println("Sequence Complete. Exiting.")
destroyDevice(f)
}
func executeStep(f *os.File, step Step) error {
switch step.Action {
case "wait":
d, err := time.ParseDuration(step.Duration)
if err != nil {
return err
}
time.Sleep(d)
case "type":
for _, char := range step.Text {
pressChar(f, char)
time.Sleep(15 * time.Millisecond) // Typing speed
}
case "press":
code, ok := resolveKey(step.Key)
if !ok {
return fmt.Errorf("unknown key: %s", step.Key)
}
sendInput(f, 0, code)
time.Sleep(50 * time.Millisecond)
sendInput(f, 0, 0)
case "combo":
var mods byte
for _, m := range step.Modifiers {
if val, ok := modMap[strings.ToUpper(m)]; ok {
mods |= val
}
}
code, ok := resolveKey(step.Key)
if !ok {
return fmt.Errorf("unknown key in combo: %s", step.Key)
}
sendInput(f, mods, code)
time.Sleep(50 * time.Millisecond)
sendInput(f, 0, 0)
}
return nil
}
func resolveKey(k string) (byte, bool) {
k = strings.ToUpper(k)
if code, ok := hidMap[k]; ok {
return code, true
}
// Fallback for single characters if user put "a" instead of "A"
if len(k) == 1 {
if code, ok := hidMap[string(k[0])]; ok {
return code, true
}
}
return 0, false
}
func pressChar(f *os.File, char rune) {
// Simple ASCII mapping logic
// Note: This needs a more robust map for special symbols like !, @, #
// For brevity, we handle a-z, A-Z, 0-9 and space.
s := string(char)
upper := strings.ToUpper(s)
code, ok := hidMap[upper]
if !ok {
return // Skip unknown
}
mods := byte(0)
// If original was uppercase, hold shift
if strings.ContainsAny(s, "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+{}|:\"<>?") {
mods = 0x02 // Left Shift
}
sendInput(f, mods, code)
time.Sleep(20 * time.Millisecond)
sendInput(f, 0, 0)
}
func createKeyboard(f *os.File) error {
req := Create2Req{
RdSize: uint16(len(keyboardReportDesc)),
Bus: 0x03,
Vendor: 0x1235,
Product: 0x5679,
Version: 1,
}
copy(req.Name[:], "Go Automator")
copy(req.RdData[:], keyboardReportDesc)
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(UHID_CREATE2))
binary.Write(buf, binary.LittleEndian, req)
_, err := f.Write(buf.Bytes())
return err
}
func destroyDevice(f *os.File) {
// Standard close handles it, but we can send UHID_DESTROY (1) if strict
}
func sendInput(f *os.File, mod byte, key byte) {
// 8 byte report: [Mod, Reserved, Key1, 0, 0, 0, 0, 0]
data := []byte{mod, 0, key, 0, 0, 0, 0, 0}
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(UHID_INPUT2))
binary.Write(buf, binary.LittleEndian, uint16(len(data)))
buf.Write(data)
padding := make([]byte, 4096-len(data))
buf.Write(padding)
f.Write(buf.Bytes())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment