A modal input mapping library for Zig.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker f2a98d1b71 Add MIT license
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:00:17 +02:00
src Migrate to Zig 0.16 2026-04-30 11:43:35 +02:00
.gitignore Initial inputmap library: modal input mapping engine 2026-04-05 10:28:02 +02:00
build.zig Initial inputmap library: modal input mapping engine 2026-04-05 10:28:02 +02:00
build.zig.zon Migrate to Zig 0.16 2026-04-30 11:43:35 +02:00
LICENSE Add MIT license 2026-07-25 12:00:17 +02:00
README.md Migrate to Zig 0.16 2026-04-30 11:43:35 +02:00

inputmap

Modal input mapping for Zig. Maps keyboard keys, controller buttons, and analog axes to user-defined actions through a mode-aware state machine.

No dependencies on any UI toolkit, backend, or scripting engine.

Features

  • Unified Key enum -- keyboard, punctuation, symbols, navigation, function keys, controller buttons
  • Modifier support -- shift, ctrl, alt, cmd as a packed struct
  • Multi-key sequences -- g g, space f, ctrl-x ctrl-s
  • Mode-aware -- bindings scoped to a mode ID, or null for any-mode
  • Analog axes -- controller sticks and triggers
  • Static or dynamic -- comptime-friendly static bindings, or runtime addBinding with allocator
  • sendKeys parser -- feed key strings like "abc<esc><cmd-s>H<pad-a>" for scripting and testing

Usage

Static keymap (comptime-friendly)

const im = @import("inputmap");

const Action = enum(u16) { move_left = 1, move_right, save };
const normal: im.ModeId = 0;

const bindings = [_]im.Binding{
    .{ .mode = normal, .sequence = &.{.{ .key = .h }}, .action = @intFromEnum(Action.move_left) },
    .{ .mode = normal, .sequence = &.{.{ .key = .l }}, .action = @intFromEnum(Action.move_right) },
    .{ .mode = null,   .sequence = &.{.{ .key = .s, .mods = .{ .cmd = true } }}, .action = @intFromEnum(Action.save) },
};

var km = im.Keymap{ .bindings = &bindings };

switch (km.feed(normal, .{ .key = .h })) {
    .dispatched => |action| handleAction(action),
    .pending => {},    // prefix of a longer sequence, wait for more input
    .no_match => {},   // no binding matches
}

Dynamic keymap (runtime registration)

var km = im.Keymap.init(allocator);
defer km.deinit();

try km.addBinding(0, &.{.{ .key = .h }}, 1);
try km.addBinding(0, &.{ .{ .key = .g }, .{ .key = .g } }, 10);
try km.addBinding(null, &.{.{ .key = .s, .mods = .{ .cmd = true } }}, 20);

// Remove all bindings for an action
_ = km.removeAction(10);

Multi-key sequences

// First 'g' returns .pending, second 'g' returns .dispatched
switch (km.feed(mode, .{ .key = .g })) {
    .pending => {},  // show "g..." in status bar
    else => {},
}
switch (km.feed(mode, .{ .key = .g })) {
    .dispatched => |action| handleAction(action),
    else => {},
}

Analog axes

const analog = [_]im.AnalogBinding{
    .{ .mode = 0, .axis = .left_x, .action = 100 },
};
var km = im.Keymap{ .bindings = &.{}, .analog_bindings = &analog };

if (km.feedAnalog(mode, .{ .axis = .left_x, .value = 0.8 })) |action| {
    handleAction(action);
}

sendKeys (scripting/testing)

im.sendKeys("gg<esc><cmd-s>H<pad-a>", struct {
    fn f(input: im.Input) void {
        // called once per parsed input
    }
}.f);

Format: plain chars (abc), uppercase = shift (H), special keys (<esc>, <enter>, <tab>), modifiers (<cmd-s>, <ctrl-alt-x>), controller (<pad-a>, <pad-lb>), literal angle brackets (<lt>, <gt>).

Zig dependency

Requires Zig 0.16.0 or later.

// build.zig.zon
.inputmap = .{ .path = "../inputmap" },

// build.zig
const inputmap = b.dependency("inputmap", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("inputmap", inputmap.module("inputmap"));

License

MIT