kiloz

Following through https://viewsourcecode.org/snaptoken/kilo/index.html in Zig
git clone git://code.mfashby.net:/kiloz
Log | Files | Refs | README

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:
Mbuild.zig | 41+++++------------------------------------
Mbuild.zig.zon | 60++++++------------------------------------------------------
Msrc/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, + }; + } + }; +}