Pronounced mak-see —
Mouse And Keyboard Control for Go.
Synthesize mouse and keyboard input on Windows, macOS, and Linux from a single
Go API. No cgo, no embedded DLL — just purego, x/sys/windows, and
x/sys/unix.
Use it for: desktop automation, RPA, accessibility tools, integration tests, macro keyboards, custom input devices, anything that needs to drive a UI as if a human were at the keyboard.
go get github.com/aiwaki/makcRequires Go 1.23+.
package main
import (
"context"
"log"
"github.com/aiwaki/makc"
)
func main() {
client, err := makc.Open()
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
if err := client.Mouse.Click(ctx, makc.ButtonLeft); err != nil {
log.Fatal(err)
}
}That's it. makc.Open() picks the right backend for the current OS, asks for
permissions if needed, and returns a *Client. Every method takes a context
so you can cancel mid-sequence.
// Move the cursor 100 pixels right and down.
client.Mouse.MoveBy(ctx, 100, 100)
// Type some text.
client.Keyboard.TypeText(ctx, "hello, world")
// Press Cmd+Tab (Windows/Linux: Ctrl+Tab).
client.Keyboard.Combo(ctx, makc.KeyLeftControl, makc.KeyTab)
// Drag from the current spot to (500, 500), curving like a human hand.
client.Mouse.Drag(ctx, makc.ButtonLeft, makc.Point{X: 500, Y: 500},
makc.NaturalMovement(40, 250*time.Millisecond, 42))listener, err := client.Listen(ctx, makc.ListenOptions{Mask: makc.ListenAll})
if err != nil { log.Fatal(err) }
defer listener.Close()
for event := range listener.Events {
if event.Kind == makc.InputEventMouseButton {
log.Printf("button %s state %s", event.Mouse.Button, event.Mouse.State)
}
}Listen is supported on Windows (low-level hooks and Raw Input), Linux
(evdev), and macOS (CGEventTap). listener.Stats() reports delivered/dropped
counters — bump ListenOptions.Buffer if Dropped is climbing.
Most setup is a one-time permission step. Once granted, makc.Open() Just
Works.
Windows. Nothing to install. The library picks InjectMouseInput /
InjectKeyboardInput when the running build of user32.dll exports them and
falls back to SendInput otherwise. No admin needed for normal injection;
some elevated targets reject input from non-elevated callers — same as any
SendInput-based tool.
macOS. Add your binary (or your terminal during dev) to System Settings
→ Privacy & Security → Accessibility. Without it, Open() succeeds but
injection returns an error pointing you at the missing permission. Listening
also requires this permission.
Linux (X11). Either run with access to /dev/uinput (uncommon for
non-root users) or run this once and re-login:
sudo bash scripts/linux-uinput-permissions.sh "$USER"That installs a udev rule giving members of input group write access to
/dev/uinput. Listening reads /dev/input/event* — same permission story.
Linux (Wayland). Use the XDG desktop portal backend:
client, err := makc.Open(
makc.WithMouseXDGPortal(),
makc.WithKeyboardXDGPortal(),
)The first call shows a one-time GNOME/KDE permission dialog asking the user
to grant remote-desktop input to your app. Subsequent runs reuse the granted
session for the lifetime of the Client.
makc.Open() defaults to the right thing per OS. Override only if you need to.
| Platform | Default | Alternatives |
|---|---|---|
| Windows | MouseInjectionAuto → InjectMouseInput if available, else SendInput |
WithMouseSendInput(), WithMouseInjectMouseInput() |
| macOS | CoreGraphics CGEvent |
(only one) |
| Linux | /dev/uinput |
WithMouseXDGPortal() for Wayland |
Symmetric Options exist for keyboard.
A few useful tuning hints:
// Disable Windows' mouse-move coalescing for high-frequency injection.
makc.WithMouseMotion(makc.MouseMotionNoCoalesce)
// Multi-monitor absolute coordinates against the virtual desktop.
makc.WithMouseMotion(makc.MouseMotionVirtualDesk)
// Tag injected events so listeners can identify your own input.
makc.WithInputTag(0xCAFE)Client.Mouse.Click is the simple path. For deterministic timing or
"human-like" curves, use a profile:
// One click with a 50ms hold.
client.Mouse.ClickWithProfile(ctx, makc.ButtonLeft,
makc.ClickWithHold(50*time.Millisecond))
// Type text with a randomized but reproducible cadence.
client.Keyboard.TypeTextWithProfile(ctx, "hello",
makc.VariableTyping(40*time.Millisecond, 120*time.Millisecond, 42))
// Bezier-curved movement instead of teleport.
client.Mouse.MoveToProfile(ctx, makc.Point{X: 500, Y: 500},
makc.NaturalMovement(60, 400*time.Millisecond, 42))For workflows that mix moves, clicks, pauses, and typing, build an
InputSequence:
seq := makc.NewInputSequence(
makc.MoveStep(makc.Abs(300, 200)),
makc.PauseStep(80*time.Millisecond),
makc.ClickStep(makc.ButtonLeft, makc.InstantClick),
makc.TextStep("makc", makc.InstantTyping),
)
client.Run(ctx, seq)Fast, Balanced, and Careful presets bundle interval timing for the most
common cases:
profile := makc.BalancedInputProfile(42) // seed
client.Keyboard.TypeTextWithProfile(ctx, "hello", profile.Typing)Common entry points:
makc.Open(opts...)→*ClientClient.Mouse.{Move, MoveTo, MoveBy, Click, DoubleClick, Wheel, HWheel, Drag, Position, State, SystemSpeed, Inject}Client.Keyboard.{Tap, TapWithHold, Combo, TypeText, ScanTap, State, Inject}Client.Listen(ctx, opts)→*ListenerwithEvents,Stats,Wait,CloseClient.Run(ctx, sequence)/Client.RunSteps(ctx, steps...)Client.RuntimeInfo(ctx)for diagnostics
Movement / timing primitives:
InstantMovement,LinearMovement,EaseInOutMovement,NaturalMovement,NaturalMovementWithJitterFixedInterval,VariableInterval,ClickProfile,TypingProfileInstantInputProfile,FastInputProfile,BalancedInputProfile,CarefulInputProfile
Parsing for CLIs / config:
ParseKey("ctrl+shift+a"),ParseMouseButton("left")
Full reference: pkg.go.dev/github.com/aiwaki/makc.
go run ./examples/mouse
go run ./examples/keyboard
go run ./examples/sequence # tiny relative move only by default
go run ./examples/sequence -click # opt in to clicking
go run ./examples/sequence -text "hi"makc-smoke is a small CLI that opens a backend and reports what it can do
without injecting anything unless asked:
go run ./cmd/makc-smoke -runtime-info
go run ./cmd/makc-smoke -capabilities
go run ./cmd/makc-smoke -inject -dx 1 -dy 1Linux portal handshake (no input until you pass -start):
go run ./cmd/makc-portal-handshake
go run ./cmd/makc-portal-handshake -select-devices -start -timeout 300sFor full local validation, hardware test scripts, and CI smoke runs (Windows
on Parallels, Linux VMs, etc.) see scripts/ and the comments in
each script.
makc deliberately does not scrub the operating system's "this event was
injected" markers (LLMHF_INJECTED on Windows,
kCGEventSourceUnixProcessID on macOS) before forwarding to other hooks
installed in the system. Those flags exist so accessibility software, security
tools, and yes — anti-cheat — can distinguish synthetic from real input.
Stripping them out of shared kernel structures to fool other software is out
of scope.
What makc does provide: WithInputTag so your own listener can identify
your own injection, plus Listener.NormalizeOwnInjected to clear the flags
on events you produced before your code sees them. That's a callback-private
operation; the kernel struct stays intact.
- Module path is currently
github.com/aiwaki/makc— no/v2yet. - Release notes: CHANGELOG.md.
- Security policy: SECURITY.md.
- Contributions welcome — see CONTRIBUTING.md.
The legacy pkg/types, pkg/types/buttons, pkg/types/keys packages are
deprecated compatibility shims. New code should import the root
github.com/aiwaki/makc package directly.
