commit d16a87f06db376f22748512a41d2d7b088545765
parent 049356ea597213f6209d7bb31b777d5be49cc97c
Author: Martin Ashby <martin@ashbysoft.com>
Date: Sun, 7 Jan 2024 21:47:28 +0000
Completed raw input and output chapter
Diffstat:
M | build.zig | | | 41 | +++++------------------------------------ |
M | build.zig.zon | | | 60 | ++++++------------------------------------------------------ |
M | src/main.zig | | | 227 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- |
3 files changed, 215 insertions(+), 113 deletions(-)
diff --git a/build.zig b/build.zig
@@ -1,32 +1,17 @@
const std = @import("std");
+const builtin = @import("builtin");
-// Although this function looks imperative, note that its job is to
-// declaratively construct a build graph that will be executed by an external
-// runner.
pub fn build(b: *std.Build) void {
- // Standard target options allows the person running `zig build` to choose
- // what target to build for. Here we do not override the defaults, which
- // means any target is allowed, and the default is native. Other options
- // for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
-
- // Standard optimization options allow the person running `zig build` to select
- // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
- // set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
const lib = b.addStaticLibrary(.{
.name = "kiloz",
- // In this case the main source file is merely a path, however, in more
- // complicated build scripts, this could be a generated file.
.root_source_file = .{ .path = "src/root.zig" },
.target = target,
.optimize = optimize,
});
- // This declares intent for the library to be installed into the standard
- // location when the user invokes the "install" step (the default step when
- // running `zig build`).
b.installArtifact(lib);
const exe = b.addExecutable(.{
@@ -36,36 +21,23 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
- // This declares intent for the executable to be installed into the
- // standard location when the user invokes the "install" step (the default
- // step when running `zig build`).
b.installArtifact(exe);
+
+ var opts = b.addOptions();
+ opts.addOption([]const u8, "version", "0.0.0"); // TODO figure out how to get this from build.zig.zon
+ exe.addOptions("options", opts);
- // This *creates* a Run step in the build graph, to be executed when another
- // step is evaluated that depends on it. The next line below will establish
- // such a dependency.
const run_cmd = b.addRunArtifact(exe);
- // By making the run step depend on the install step, it will be run from the
- // installation directory rather than directly from within the cache directory.
- // This is not necessary, however, if the application depends on other installed
- // files, this ensures they will be present and in the expected location.
run_cmd.step.dependOn(b.getInstallStep());
- // This allows the user to pass arguments to the application in the build
- // command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
- // This creates a build step. It will be visible in the `zig build --help` menu,
- // and can be selected like this: `zig build run`
- // This will evaluate the `run` step rather than the default, which is "install".
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
- // Creates a step for unit testing. This only builds the test executable
- // but does not run it.
const lib_unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/root.zig" },
.target = target,
@@ -82,9 +54,6 @@ pub fn build(b: *std.Build) void {
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
- // Similar to creating the run step earlier, this exposes a `test` step to
- // the `zig build --help` menu, providing a way for the user to request
- // running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step);
diff --git a/build.zig.zon b/build.zig.zon
@@ -1,62 +1,14 @@
.{
.name = "kiloz",
- // This is a [Semantic Version](https://semver.org/).
- // In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
-
- // This field is optional.
- // This is currently advisory only; Zig does not yet do anything
- // with this value.
- //.minimum_zig_version = "0.11.0",
-
- // This field is optional.
- // Each dependency must either provide a `url` and `hash`, or a `path`.
- // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
- // Once all dependencies are fetched, `zig build` no longer requires
- // Internet connectivity.
+ .minimum_zig_version = "0.12.0-dev.1769+bf5ab5451",
.dependencies = .{
- // See `zig fetch --save <url>` for a command-line interface for adding dependencies.
- //.example = .{
- // // When updating this field to a new URL, be sure to delete the corresponding
- // // `hash`, otherwise you are communicating that you expect to find the old hash at
- // // the new URL.
- // .url = "https://example.com/foo.tar.gz",
- //
- // // This is computed from the file contents of the directory of files that is
- // // obtained after fetching `url` and applying the inclusion rules given by
- // // `paths`.
- // //
- // // This field is the source of truth; packages do not come from an `url`; they
- // // come from a `hash`. `url` is just one of many possible mirrors for how to
- // // obtain a package matching this `hash`.
- // //
- // // Uses the [multihash](https://multiformats.io/multihash/) format.
- // .hash = "...",
- //
- // // When this is provided, the package is found in a directory relative to the
- // // build root. In this case the package's hash is irrelevant and therefore not
- // // computed. This field and `url` are mutually exclusive.
- // .path = "foo",
- //},
},
-
- // Specifies the set of files and directories that are included in this package.
- // Only files and directories listed here are included in the `hash` that
- // is computed for this package.
- // Paths are relative to the build root. Use the empty string (`""`) to refer to
- // the build root itself.
- // A directory listed here means that all files within, recursively, are included.
.paths = .{
- // This makes *all* files, recursively, included in this package. It is generally
- // better to explicitly list the files and directories instead, to insure that
- // fetching from tarballs, file system paths, and version control all result
- // in the same contents hash.
- "",
- // For example...
- //"build.zig",
- //"build.zig.zon",
- //"src",
- //"LICENSE",
- //"README.md",
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ "LICENSE",
+ "README.md",
},
}
diff --git a/src/main.zig b/src/main.zig
@@ -1,9 +1,14 @@
const std = @import("std");
-const quit = 17; // ctrl+q
+const version = @import("options").version;
+const quit: editorKey = .{.char = 17}; // ctrl+q
pub fn main() !void {
- var es: EditorState = .{};
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ defer _ = gpa.deinit();
+ const a = gpa.allocator();
+ var es = EditorState.init(a);
+ defer es.deinit();
es.termios_orig = try enterRawMode();
defer disableRawMode(&es.termios_orig);
defer clearScreen();
@@ -11,9 +16,9 @@ pub fn main() !void {
try initEditor(&es);
while (true) {
- try editorRefreshScreen(es);
+ try editorRefreshScreen(&es);
- editorProcessKeyPress() catch |e| switch (e) {
+ editorProcessKeyPress(&es) catch |e| switch (e) {
error.Quit => return,
else => return e,
};
@@ -23,9 +28,20 @@ pub fn main() !void {
//// Data
const EditorState = struct {
+ a: std.mem.Allocator,
+ screen_buf: std.ArrayListUnmanaged(u8) = .{},
termios_orig: std.os.linux.termios = undefined,
screenrows: u16 = 0,
screencols: u16 = 0,
+ cx: u16 = 10,
+ cy: u16 = 10,
+
+ fn init(a: std.mem.Allocator) EditorState {
+ return .{ .a = a };
+ }
+ fn deinit(self: *EditorState) void {
+ self.screen_buf.deinit(self.a);
+ }
};
//// Terminal handling
@@ -51,13 +67,77 @@ fn disableRawMode(t: *const std.os.linux.termios) void {
}
}
-fn editorReadKey() !u8 {
+const editorKey = union(enum) {
+ char: u8,
+ virt: enum {
+ ARROW_LEFT,
+ ARROW_RIGHT,
+ ARROW_UP,
+ ARROW_DOWN,
+ PAGE_UP,
+ PAGE_DOWN,
+ HOME,
+ END,
+ DEL,
+ },
+};
+
+fn editorReadKey() !editorKey {
var rdr = std.io.getStdIn().reader();
while (true) {
- return rdr.readByte() catch |e| switch (e) {
+ const ch = rdr.readByte() catch |e| switch (e) {
error.EndOfStream => continue,
else => return e,
};
+ if (ch == '\x1b') {
+ const ch1 = rdr.readByte() catch |e| switch (e) {
+ error.EndOfStream => return .{.char = '\x1b'},
+ else => return e,
+ };
+ const ch2 = rdr.readByte() catch |e| switch (e) {
+ error.EndOfStream => return .{.char = '\x1b'},
+ else => return e,
+ };
+ if (ch1 == '[') {
+ if (std.ascii.isDigit(ch2)) {
+ const ch3 = rdr.readByte() catch |e| switch (e) {
+ error.EndOfStream => return .{.char = '\x1b'},
+ else => return e,
+ };
+ if (ch3 == '~') {
+ switch (ch2) {
+ '1', '7' => return .{.virt = .HOME},
+ '4', '8' => return .{.virt = .END},
+ '5' => return .{.virt = .PAGE_UP},
+ '6' => return .{.virt = .PAGE_DOWN},
+ '3' => return .{.virt = .DEL},
+ else => return .{.char = '\x1b'},
+ }
+ } else {
+ return .{.char = '\x1b'};
+ }
+ } else {
+ switch (ch2) {
+ 'A' => return .{.virt = .ARROW_UP},
+ 'B' => return .{.virt = .ARROW_DOWN},
+ 'C' => return .{.virt = .ARROW_RIGHT},
+ 'D' => return .{.virt = .ARROW_LEFT},
+ 'F' => return .{.virt = .END},
+ 'H' => return .{.virt = .HOME},
+ else => return .{.char = '\x1b'},
+ }
+ }
+ } else if (ch1 == 'O') {
+ switch (ch2) {
+ 'F' => return .{.virt = .END},
+ 'H' => return .{.virt = .HOME},
+ else => return .{.char = '\x1b'},
+ }
+ } else {
+ return .{.char = '\x1b'};
+ }
+ }
+ return .{.char = ch};
}
}
@@ -99,34 +179,96 @@ fn getWindowSize() !Size {
//// Output
-fn editorRefreshScreen(es: EditorState) !void {
- // Check the vt100 manual!
- const wtr = std.io.getStdOut().writer();
- try wtr.writeAll("\x1b[2J");
- try wtr.writeAll("\x1b[H");
- try editorDrawRows(es);
- try wtr.writeAll("\x1b[H");
-}
-
fn clearScreen() void {
const wtr = std.io.getStdOut().writer();
wtr.writeAll("\x1b[2J") catch {};
wtr.writeAll("\x1b[H") catch {};
}
-fn editorDrawRows(es: EditorState) !void {
- const wtr = std.io.getStdOut().writer();
- for (0..(es.screenrows - 1)) |_| {
- try wtr.writeAll("~\r\n");
+fn editorRefreshScreen(es: *EditorState) !void {
+ // Use a buffer to avoid multiple write() calls to the actual terminal device
+ // Reduces rendering artifacts / flickering
+ es.screen_buf.clearRetainingCapacity();
+ const wtr = es.screen_buf.writer(es.a);
+ // Check the vt100 manual!
+ try wtr.writeAll("\x1b[?25l"); // Hide the cursor
+ //try wtr.writeAll("\x1b[2J"); // J clear 2 whole screen
+ try wtr.writeAll("\x1b[H"); // Reset cursor (to 1:1)
+ try editorDrawRows(es, wtr);
+ try std.fmt.format(wtr, "\x1b[{};{}H", .{ es.cy + 1, es.cx + 1 }); // Move the cursor to our stored position
+ try wtr.writeAll("\x1b[?25h"); // Show the cursor again
+ try std.io.getStdOut().writeAll(es.screen_buf.items);
+}
+
+fn editorDrawRows(es: *const EditorState, wtr: anytype) !void {
+ for (0..es.screenrows) |y| {
+ try wtr.writeAll("\x1b[K"); // Clear row before writing it
+
+ var lw = limitedWriter(wtr, es.screencols); // Never write more than we have columns
+ const wtr2 = lw.writer();
+ if (y == es.screenrows / 3) {
+ const welcome_msg = try std.fmt.allocPrint(es.a, "Kilo editor -- version {s}", .{version}); // TODO don't allocate every time...
+ defer es.a.free(welcome_msg);
+ const padding = (es.screencols - welcome_msg.len) / 2;
+ for (0..padding) |y2| {
+ try wtr2.writeByte(if (y2 == 0) '~' else ' ');
+ }
+ try wtr2.writeAll(welcome_msg);
+ } else {
+ try wtr2.writeAll("~");
+ }
+
+ if (y < es.screenrows-1)
+ try wtr.writeAll("\r\n");
}
}
//// Input
-fn editorProcessKeyPress() !void {
- const ch = try editorReadKey();
- switch (ch) {
- quit => return error.Quit,
+fn editorProcessKeyPress(es: *EditorState) !void {
+ const key = try editorReadKey();
+ switch (key) {
+ .char => |ch| {
+ switch (ch) {
+ quit.char => return error.Quit,
+ else => {},
+ }
+ },
+ .virt => |v| {
+ switch (v) {
+ .ARROW_UP, .ARROW_DOWN, .ARROW_LEFT, .ARROW_RIGHT, .PAGE_UP, .PAGE_DOWN, .HOME, .END => try editorMoveCursor(key, es),
+ else => {},
+ }
+ },
+ }
+}
+
+fn editorMoveCursor(key: editorKey, es: *EditorState) !void {
+ switch (key.virt) {
+ .ARROW_UP => {
+ if (es.cy > 0) es.cy -= 1;
+ },
+ .ARROW_DOWN => {
+ if (es.cy < (es.screenrows - 1)) es.cy += 1;
+ },
+ .ARROW_LEFT => {
+ if (es.cx > 0) es.cx -= 1;
+ },
+ .ARROW_RIGHT => {
+ if (es.cx < (es.screencols - 1)) es.cx += 1;
+ },
+ .PAGE_UP => {
+ es.cy = 0;
+ },
+ .PAGE_DOWN => {
+ es.cy = es.screenrows-1;
+ },
+ .HOME => {
+ es.cx = 0;
+ },
+ .END => {
+ es.cx = es.screencols-1;
+ },
else => {},
}
}
@@ -138,3 +280,42 @@ fn initEditor(es: *EditorState) !void {
es.screenrows = sz.rows;
es.screencols = sz.cols;
}
+
+//// Extra stuff
+
+fn limitedWriter(base_writer: anytype, max: usize) LimitedWriter(@TypeOf(base_writer)) {
+ return LimitedWriter(@TypeOf(base_writer)){
+ .base_writer = base_writer,
+ .max = max,
+ };
+}
+
+fn LimitedWriter(comptime BaseWriter: type) type {
+ return struct {
+ const Self = @This();
+ const WriteError = BaseWriter.Error || error{NoSpaceLeft};
+ const Writer = std.io.Writer(*Self, WriteError, Self.write);
+
+ base_writer: BaseWriter,
+ max: usize,
+ written: usize = 0,
+
+ fn write(self: *Self, bytes: []const u8) WriteError!usize {
+ const rem = self.max - self.written;
+ if (rem == 0) return error.NoSpaceLeft;
+
+ var to_write = bytes;
+ if (bytes.len > rem) {
+ to_write = bytes[0..rem];
+ }
+ self.written += to_write.len;
+ return try self.base_writer.write(to_write);
+ }
+
+ fn writer(self: *Self) Writer {
+ return .{
+ .context = self,
+ };
+ }
+ };
+}