A => .gitignore +4 -0
@@ 1,4 @@
+.DS*
+*~
+zig-cache/
+zig-out/
A => build.zig +33 -0
@@ 1,33 @@
+const std = @import("std");
+
+pub fn build(b: *std.build.Builder) void {
+ // Standard release options allow the person running `zig build` to select
+ // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
+ const mode = b.standardReleaseOptions();
+
+ const lib = b.addStaticLibrary("kira", "src/main.zig");
+ lib.setBuildMode(mode);
+ lib.install();
+
+ const examples_step = b.step("examples", "Build all examples");
+ inline for (.{
+ "input_info",
+ "event_info",
+ "mouse_test",
+ "term_info",
+ "cursor_mover",
+ }) |example| {
+ const exe = b.addExecutable(example, "examples/" ++ example ++ ".zig");
+ exe.addPackagePath("kira", "src/main.zig");
+ exe.setBuildMode(mode);
+ const example_install_step = b.addInstallArtifact(exe);
+ examples_step.dependOn(&example_install_step.step);
+ examples_step.dependOn(&exe.step);
+ }
+
+ const main_tests = b.addTest("src/main.zig");
+ main_tests.setBuildMode(mode);
+
+ const test_step = b.step("test", "Run library tests");
+ test_step.dependOn(&main_tests.step);
+}
A => examples/cursor_mover.zig +37 -0
@@ 1,37 @@
+const std = @import("std");
+const kira = @import("kira");
+
+pub fn main() !void {
+ var terminal = try kira.RawTerminal.init(true);
+ defer terminal.deinit();
+
+ const size = try terminal.getWindowSize();
+ var cursor_position = kira.TerminalPosition{
+ .col = size.cols / 2,
+ .row = size.rows / 2,
+ };
+
+ while (true) {
+ try terminal.clear(.screen);
+ try terminal.cursorControl(.{ .goto = .{ .col = 0, .row = 0 } });
+ try terminal.print("terminal size: {} by {}\r\n", .{ size.cols, size.rows });
+ try terminal.print("cursor position: ({}, {})\r\n", .{ cursor_position.col, cursor_position.row });
+ try terminal.print("press 'q' to quit\r\n", .{});
+ try terminal.cursorControl(.{ .goto = cursor_position });
+ if (try terminal.nextEvent()) |event| {
+ switch (event) {
+ .key => |key| switch (key) {
+ .char => |char| if (char == 'q') break,
+ .up => cursor_position.row -|= 1,
+ .down => cursor_position.row = std.math.min(size.rows - 1, cursor_position.row + 1),
+ .left => cursor_position.col -|= 1,
+ .right => cursor_position.col = std.math.min(size.cols - 1, cursor_position.col + 1),
+ else => {},
+ },
+ else => {},
+ }
+ }
+ }
+ try terminal.clear(.screen);
+ try terminal.cursorControl(.{ .goto = .{ .col = 0, .row = 0 } });
+}
A => examples/event_info.zig +18 -0
@@ 1,18 @@
+const std = @import("std");
+const kira = @import("kira");
+
+pub fn main() !void {
+ var terminal = try kira.RawTerminal.init(true);
+ defer terminal.deinit();
+
+ while (try terminal.nextEvent()) |event| {
+ try terminal.print("{}\r\n", .{event});
+ switch (event) {
+ .key => |key| switch (key) {
+ .char => |char| if (char == 'q') break,
+ else => {},
+ },
+ else => {},
+ }
+ }
+}
A => examples/input_info.zig +18 -0
@@ 1,18 @@
+const std = @import("std");
+const kira = @import("kira");
+
+pub fn main() !void {
+ var terminal = try kira.RawTerminal.init(true);
+ defer terminal.deinit();
+
+ var read_buffer: [32]u8 = undefined;
+ while (true) {
+ const read = try terminal.read(&read_buffer);
+ if (std.ascii.isCntrl(read_buffer[0])) {
+ try terminal.print("{d}\r\n", .{read_buffer[0..read]});
+ } else {
+ try terminal.print("{0d} ('{0s}')\r\n", .{read_buffer[0..read]});
+ }
+ if (read_buffer[0] == 'q') break;
+ }
+}
A => examples/mouse_test.zig +21 -0
@@ 1,21 @@
+const std = @import("std");
+const kira = @import("kira");
+
+pub fn main() !void {
+ var terminal = try kira.RawTerminal.init(true);
+ defer terminal.deinit();
+
+ try terminal.enableMouse();
+ defer terminal.disableMouse() catch {};
+
+ while (try terminal.nextEvent()) |event| {
+ try terminal.print("{}\r\n", .{event});
+ switch (event) {
+ .key => |key| switch (key) {
+ .char => |char| if (char == 'q') break,
+ else => {},
+ },
+ else => {},
+ }
+ }
+}
A => examples/term_info.zig +16 -0
@@ 1,16 @@
+const std = @import("std");
+const kira = @import("kira");
+
+pub fn main() !void {
+ var terminal = try kira.RawTerminal.init(true);
+
+ const size = try terminal.getWindowSize();
+ const cursor = try terminal.getCursorPosition();
+
+ terminal.deinit();
+
+ std.debug.print(
+ "size: {}\ncursor position: {}\n",
+ .{ size, cursor },
+ );
+}
A => src/ByteIterator.zig +20 -0
@@ 1,20 @@
+const Self = @This();
+
+bytes: []const u8,
+index: usize,
+
+pub fn init(bytes: []const u8) Self {
+ return Self{
+ .bytes = bytes,
+ .index = 0,
+ };
+}
+
+pub fn next(self: *Self) ?u8 {
+ if (self.index < self.bytes.len) {
+ defer self.index += 1;
+ return self.bytes[self.index];
+ } else {
+ return null;
+ }
+}
A => src/RawTerminal.zig +200 -0
@@ 1,200 @@
+const std = @import("std");
+
+const Event = @import("events.zig").Event;
+const parseCsi = @import("events.zig").parseCsi;
+const ByteIterator = @import("ByteIterator.zig");
+const TerminalSize = @import("main.zig").TerminalSize;
+const TerminalPosition = @import("main.zig").TerminalPosition;
+
+const Self = @This();
+
+original_termios: std.os.termios,
+out: std.fs.File,
+in: std.fs.File,
+read_buffer: [32]u8 = undefined,
+
+pub fn init(blocking: bool) !Self {
+ var std_in = std.io.getStdIn();
+ const original_termios = try std.os.tcgetattr(std_in.handle);
+ var raw_termios = original_termios;
+ raw_termios.iflag &= ~(std.c.BRKINT | std.c.ICRNL | std.c.INPCK | std.c.ISTRIP | std.c.IXON);
+ raw_termios.oflag &= ~(std.c.OPOST);
+ raw_termios.cflag |= (std.c.CS8);
+ raw_termios.lflag &= ~(std.c.ECHO | std.c.ICANON | std.c.IEXTEN | std.c.ISIG);
+ if (!blocking) {
+ raw_termios.cc[std.c.V.MIN] = 0;
+ raw_termios.cc[std.c.V.TIME] = 1;
+ }
+ try std.os.tcsetattr(std_in.handle, .FLUSH, raw_termios);
+
+ var std_out = std.io.getStdOut();
+
+ return Self{
+ .original_termios = original_termios,
+ .in = std_in,
+ .out = std_out,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ std.os.tcsetattr(self.out.handle, .FLUSH, self.original_termios) catch {};
+}
+
+pub fn enableMouse(self: *Self) !void {
+ try self.writeAll("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
+}
+
+pub fn disableMouse(self: *Self) !void {
+ try self.writeAll("\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l");
+}
+
+pub fn write(self: *Self, bytes: []const u8) !usize {
+ return self.out.write(bytes);
+}
+
+pub fn writeAll(self: *Self, bytes: []const u8) !void {
+ return self.out.writeAll(bytes);
+}
+
+pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
+ return self.out.writer().print(format, args);
+}
+
+pub fn read(self: *Self, buffer: []u8) !usize {
+ return self.in.read(buffer);
+}
+
+/// If the event is of type unknown, it is
+/// valid only until the next nextEvent call
+// TODO: properly handle when more than one
+// event happens in the same 'read'
+pub fn nextEvent(self: *Self) !?Event {
+ self.read_buffer = undefined;
+ var count = try self.read(&self.read_buffer);
+ if (count == 0) {
+ return null;
+ }
+
+ const bytes = self.read_buffer[0..count];
+
+ if (bytes[0] == '\x1b') {
+ var iterator = ByteIterator.init(bytes[1..]);
+ if (iterator.next()) |next| {
+ switch (next) {
+ 'O' => {
+ if (iterator.next()) |next2| switch (next2) {
+ 'P' => return Event{ .key = .f1 },
+ 'Q' => return Event{ .key = .f2 },
+ 'R' => return Event{ .key = .f3 },
+ 'S' => return Event{ .key = .f4 },
+ else => {},
+ };
+ },
+ '[' => if (parseCsi(bytes[2..])) |csi_event| return csi_event,
+ else => if (std.unicode.utf8ValidateSlice(bytes[1..])) {
+ const codepoint = std.unicode.utf8Decode(bytes[1..]) catch unreachable;
+ return Event{ .key = .{ .alt = codepoint } };
+ },
+ }
+ }
+ } else if (bytes[0] == '\n' or bytes[0] == '\r')
+ return Event{ .key = .enter }
+ else if (bytes[0] == '\t')
+ return Event{ .key = .tab }
+ else if (bytes[0] == '\x7f')
+ return Event{ .key = .backspace }
+ else if (bytes[0] >= '\x01' and bytes[0] <= '\x1a')
+ return Event{ .key = .{ .ctrl = bytes[0] - 0x1 + 'a' } }
+ else if (bytes[0] >= '\x1c' and bytes[0] <= '\x1f')
+ return Event{ .key = .{ .ctrl = bytes[0] - 0x1c + '4' } }
+ else if (bytes[0] == 0) {
+ return Event{ .key = .{ .ctrl = ' ' } };
+ } else if (std.unicode.utf8ValidateSlice(bytes) and
+ try std.unicode.utf8CountCodepoints(bytes) == 1)
+ {
+ return Event{ .key = .{ .char = try std.unicode.utf8Decode(bytes) } };
+ }
+
+ return Event{ .unknown = bytes };
+}
+
+pub fn getWindowSize(self: *Self) !TerminalSize {
+ var size: std.c.winsize = undefined;
+ if (std.c.ioctl(self.out.handle, std.c.T.IOCGWINSZ, &size) == -1 or size.ws_col == 0) {
+ const old_cursor_position = try self.getCursorPosition();
+ try self.writeAll("\x1b[999C\x1b[999B");
+ const cursor_position = try self.getCursorPosition();
+ try self.print("\x1b[{d};{d}H", .{ old_cursor_position.row, old_cursor_position.col });
+ return TerminalSize{
+ .rows = cursor_position.row + 1,
+ .cols = cursor_position.col + 1,
+ };
+ } else {
+ return TerminalSize{
+ .rows = size.ws_row,
+ .cols = size.ws_col,
+ };
+ }
+}
+
+pub fn getCursorPosition(self: *Self) !TerminalPosition {
+ try self.writeAll("\x1b[6n");
+ var buffer: [32]u8 = undefined;
+ var count: usize = 0;
+ while (count < buffer.len) {
+ if ((try self.read(buffer[count .. count + 1])) != 1) break;
+ if (buffer[count] == 'R') break;
+ count += 1;
+ }
+ if (count < 5) return error.InvalidResponse;
+ if (buffer[0] != '\x1b' or buffer[1] != '[') return error.InvalidResponse;
+ var split = std.mem.split(u8, buffer[2..count], ";");
+ var row = try std.fmt.parseInt(u16, split.next() orelse return error.InvalidResponse, 0);
+ var col = try std.fmt.parseInt(u16, split.next() orelse return error.InvalidResponse, 0);
+ return TerminalPosition{
+ .row = row - 1,
+ .col = col - 1,
+ };
+}
+
+pub fn cursorControl(self: *Self, control: CursorControl) !void {
+ switch (control) {
+ .goto => |position| try self.print("\x1b[{};{}H", .{ position.row + 1, position.col + 1 }),
+ .up => |lines| try self.print("\x1b[{}A", .{lines}),
+ .down => |lines| try self.print("\x1b[{}B", .{lines}),
+ .left => |lines| try self.print("\x1b[{}D", .{lines}),
+ .right => |lines| try self.print("\x1b[{}C", .{lines}),
+ .save => try self.print("\x1b7", .{}),
+ .restore => try self.print("\x1b8", .{}),
+ }
+}
+
+pub fn clear(self: *Self, what: ClearTarget) !void {
+ switch (what) {
+ .from_cursor => try self.print("\x1b[0J", .{}),
+ .to_cursor => try self.print("\x1b[1J", .{}),
+ .screen => try self.print("\x1b[2J", .{}),
+ .line_from_cursor => try self.print("\x1b[0K", .{}),
+ .line_to_cursor => try self.print("\x1b[1K", .{}),
+ .line => try self.print("\x1b[2K", .{}),
+ }
+}
+
+pub const ClearTarget = enum {
+ screen,
+ to_cursor,
+ from_cursor,
+ line,
+ line_to_cursor,
+ line_from_cursor,
+};
+
+pub const CursorControl = union(enum) {
+ goto: TerminalPosition,
+ up: u16,
+ down: u16,
+ left: u16,
+ right: u16,
+ save,
+ restore,
+};
A => src/events.zig +276 -0
@@ 1,276 @@
+const std = @import("std");
+
+const ByteIterator = @import("ByteIterator.zig");
+const TerminalPosition = @import("main.zig").TerminalPosition;
+
+pub fn parseCsi(bytes: []const u8) ?Event {
+ var iterator = ByteIterator.init(bytes);
+ if (iterator.next()) |next| switch (next) {
+ '[' => if (iterator.next()) |next2| switch (next2) {
+ 'A' => return Event{ .key = .f1 },
+ 'B' => return Event{ .key = .f2 },
+ 'C' => return Event{ .key = .f3 },
+ 'D' => return Event{ .key = .f4 },
+ 'E' => return Event{ .key = .f5 },
+ else => {},
+ },
+ 'A' => return Event{ .key = .up },
+ 'B' => return Event{ .key = .down },
+ 'D' => return Event{ .key = .left },
+ 'C' => return Event{ .key = .right },
+ 'H' => return Event{ .key = .home },
+ 'F' => return Event{ .key = .end },
+ 'Z' => return Event{ .key = .back_tab },
+ // X10 mouse encoding: esc [ M Cb Cx Cy
+ 'M' => {
+ const cb = @intCast(i8, iterator.next() orelse return null) - 32;
+ const position = TerminalPosition{
+ .col = @as(u16, iterator.next() orelse return null) -| 33,
+ .row = @as(u16, iterator.next() orelse return null) -| 33,
+ };
+ const event = switch (cb) {
+ 0 => if (cb & 0x40 != 0)
+ MouseEvent{ .press = .{ .button = .wheel_up, .position = position } }
+ else
+ MouseEvent{ .press = .{ .button = .left, .position = position } },
+ 1 => if (cb & 0x40 != 0)
+ MouseEvent{ .press = .{ .button = .wheel_down, .position = position } }
+ else
+ MouseEvent{ .press = .{ .button = .middle, .position = position } },
+ 2 => MouseEvent{ .press = .{ .button = .right, .position = position } },
+ 3 => MouseEvent{ .release = position },
+ else => return null,
+ };
+ return Event{ .mouse = event };
+ },
+ // xterm mouse encoding: esc [ < Cb ; Cx ; Cy (;) (m|M)
+ '<' => {
+ var buffer: [32]u8 = undefined;
+ var index: usize = 0;
+ var char = iterator.next() orelse return null;
+ while (char != 'm' and char != 'M') {
+ buffer[index] = char;
+ index += 1;
+ char = iterator.next() orelse return null;
+ }
+
+ var nums = std.mem.split(u8, buffer[0..index], ";");
+ const cb = std.fmt.parseInt(u16, nums.next() orelse return null, 0) catch return null;
+ const position = TerminalPosition{
+ .col = (std.fmt.parseInt(u16, nums.next() orelse return null, 0) catch return null) - 1,
+ .row = (std.fmt.parseInt(u16, nums.next() orelse return null, 0) catch return null) - 1,
+ };
+
+ const event = switch (cb) {
+ 0...2, 64...65 => press_release: {
+ var button: MouseButton = switch (cb) {
+ 0 => .left,
+ 1 => .middle,
+ 2 => .right,
+ 64 => .wheel_up,
+ 65 => .wheel_down,
+ else => unreachable,
+ };
+ break :press_release switch (char) {
+ 'M' => MouseEvent{ .press = .{ .button = button, .position = position } },
+ 'm' => MouseEvent{ .release = position },
+ else => unreachable,
+ };
+ },
+ 32 => MouseEvent{ .hold = position },
+ 3 => MouseEvent{ .release = position },
+ else => return null,
+ };
+ return Event{ .mouse = event };
+ },
+ // numbered escape code
+ '0'...'9' => {
+ var buffer: [32]u8 = undefined;
+ var index: usize = 1;
+ buffer[0] = next;
+ var byte = iterator.next() orelse return null;
+ while (byte < 64 or byte > 126) {
+ buffer[index] = byte;
+ index += 1;
+ byte = iterator.next() orelse return null;
+ }
+
+ switch (byte) {
+ // rxvt mouse encoding: esc [ Cb ; Cx ; Cy ; M
+ 'M' => {
+ var nums = std.mem.split(u8, buffer[0..index], ";");
+ const cb = std.fmt.parseInt(u16, nums.next() orelse return null, 0) catch return null;
+ const position = TerminalPosition{
+ .col = std.fmt.parseInt(u16, nums.next() orelse return null, 0) catch return null,
+ .row = std.fmt.parseInt(u16, nums.next() orelse return null, 0) catch return null,
+ };
+
+ const event = switch (cb) {
+ 32 => MouseEvent{ .press = .{ .button = .left, .position = position } },
+ 33 => MouseEvent{ .press = .{ .button = .middle, .position = position } },
+ 34 => MouseEvent{ .press = .{ .button = .right, .position = position } },
+ 35 => MouseEvent{ .release = position },
+ 64 => MouseEvent{ .hold = position },
+ 96, 97 => MouseEvent{ .press = .{ .button = .wheel_up, .position = position } },
+ else => return null,
+ };
+ return Event{ .mouse = event };
+ },
+ '~' => {
+ var nums = std.mem.split(u8, buffer[0..index], ";");
+ var num = std.fmt.parseInt(
+ u16,
+ nums.next() orelse return null,
+ 0,
+ ) catch return null;
+
+ // multi-keys not handled
+ if (nums.next() != null) return null;
+
+ switch (num) {
+ 1, 7 => return Event{ .key = .home },
+ 2 => return Event{ .key = .insert },
+ 3 => return Event{ .key = .delete },
+ 4, 8 => return Event{ .key = .end },
+ 5 => return Event{ .key = .page_up },
+ 6 => return Event{ .key = .page_down },
+ 11 => return Event{ .key = .f1 },
+ 12 => return Event{ .key = .f2 },
+ 13 => return Event{ .key = .f3 },
+ 14 => return Event{ .key = .f4 },
+ 15 => return Event{ .key = .f5 },
+ 17 => return Event{ .key = .f6 },
+ 18 => return Event{ .key = .f7 },
+ 19 => return Event{ .key = .f8 },
+ 20 => return Event{ .key = .f9 },
+ 21 => return Event{ .key = .f10 },
+ 23 => return Event{ .key = .f11 },
+ 24 => return Event{ .key = .f12 },
+ else => {},
+ }
+ },
+ else => {},
+ }
+ },
+ else => {},
+ };
+ return null;
+}
+
+pub const Event = union(enum) {
+ key: Key,
+ mouse: MouseEvent,
+ // resize: TerminalSize,
+ unknown: []u8,
+};
+
+pub const MouseEvent = union(enum) {
+ press: struct { button: MouseButton, position: TerminalPosition },
+ release: TerminalPosition,
+ hold: TerminalPosition,
+
+ pub fn format(
+ self: MouseEvent,
+ comptime _: []const u8,
+ _: std.fmt.FormatOptions,
+ writer: anytype,
+ ) !void {
+ switch (self) {
+ .press => |press| try std.fmt.format(
+ writer,
+ "MouseEvent{{ .press = {s} @ ({d}, {d}) }}",
+ .{ @tagName(press.button), press.position.col, press.position.row },
+ ),
+ .release => |position| try std.fmt.format(
+ writer,
+ "MouseEvent{{ .release = ({d}, {d}) }}",
+ .{ position.col, position.row },
+ ),
+ .hold => |position| try std.fmt.format(
+ writer,
+ "MouseEvent{{ .hold = ({d}, {d}) }}",
+ .{ position.col, position.row },
+ ),
+ }
+ }
+};
+
+pub const MouseButton = enum {
+ left,
+ right,
+ middle,
+ wheel_up,
+ wheel_down,
+};
+
+pub const Key = union(enum) {
+ backspace,
+ enter,
+
+ left,
+ right,
+ up,
+ down,
+
+ home,
+ end,
+
+ page_up,
+ page_down,
+
+ tab,
+ back_tab,
+
+ delete,
+ insert,
+
+ f1,
+ f2,
+ f3,
+ f4,
+ f5,
+ f6,
+ f7,
+ f8,
+ f9,
+ f10,
+ f11,
+ f12,
+
+ char: u21,
+ alt: u21,
+ ctrl: u21,
+
+ @"null",
+ escape,
+
+ pub fn format(
+ self: Key,
+ comptime _: []const u8,
+ _: std.fmt.FormatOptions,
+ writer: anytype,
+ ) !void {
+ switch (self) {
+ .char => |char| try std.fmt.format(
+ writer,
+ "Key{{ .char = '{u}' }}",
+ .{char},
+ ),
+ .ctrl => |char| try std.fmt.format(
+ writer,
+ "Key{{ .ctrl = '{u}' }}",
+ .{char},
+ ),
+ .alt => |char| try std.fmt.format(
+ writer,
+ "Key{{ .alt = '{u}' }}",
+ .{char},
+ ),
+ else => try std.fmt.format(
+ writer,
+ "Key.{s}",
+ .{@tagName(self)},
+ ),
+ }
+ }
+};
A => src/main.zig +15 -0
@@ 1,15 @@
+const std = @import("std");
+
+pub const RawTerminal = @import("RawTerminal.zig");
+
+pub const Event = @import("events.zig").Event;
+pub const MouseEvent = @import("events.zig").MouseEvent;
+pub const MouseButton = @import("events.zig").MouseButton;
+pub const Key = @import("events.zig").Key;
+
+pub const TerminalPosition = struct { col: u16, row: u16 };
+pub const TerminalSize = struct { cols: u16, rows: u16 };
+
+test "static analysis" {
+ std.testing.refAllDecls(@This());
+}