├── .gitignore ├── README.md ├── build.zig └── src └── main.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | https://viewsourcecode.org/snaptoken/kilo/ 2 | 3 | If using macOS, requires at least Zig git checkout a021c7b1. 4 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const Builder = @import("std").build.Builder; 2 | 3 | pub fn build(b: *Builder) void { 4 | // Standard target options allows the person running `zig build` to choose 5 | // what target to build for. Here we do not override the defaults, which 6 | // means any target is allowed, and the default is native. Other options 7 | // for restricting supported target set are available. 8 | const target = b.standardTargetOptions(.{}); 9 | 10 | // Standard release options allow the person running `zig build` to select 11 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 12 | const mode = b.standardReleaseOptions(); 13 | 14 | const exe = b.addExecutable("text-editor-zig", "src/main.zig"); 15 | exe.setTarget(target); 16 | exe.setBuildMode(mode); 17 | exe.install(); 18 | 19 | const run_cmd = exe.run(); 20 | run_cmd.step.dependOn(b.getInstallStep()); 21 | if (b.args) |args| { 22 | run_cmd.addArgs(args); 23 | } 24 | 25 | const run_step = b.step("run", "Run the app"); 26 | run_step.dependOn(&run_cmd.step); 27 | } 28 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ascii = @import("std").ascii; 3 | const fmt = @import("std").fmt; 4 | const io = @import("std").io; 5 | const heap = @import("std").heap; 6 | const mem = @import("std").mem; 7 | usingnamespace @import("std").os; 8 | 9 | const kilo_version = "0.0.1"; 10 | 11 | pub fn main() anyerror!void { 12 | defer { 13 | const leaked = gpa.deinit(); 14 | if (leaked) panic("leaked memory", null); 15 | } 16 | var editor = try Editor.new(&gpa.allocator); 17 | defer gpa.allocator.destroy(editor); 18 | const args = try std.process.argsAlloc(&gpa.allocator); 19 | defer std.process.argsFree(&gpa.allocator, args); 20 | if (args.len == 2) try editor.open(args[1]); 21 | try editor.enableRawMode(); 22 | defer editor.disableRawMode(); 23 | while (true) { 24 | try editor.refreshScreen(); 25 | try editor.processKeyPress(); 26 | if (editor.shutting_down) break; 27 | } 28 | editor.free(); 29 | try stdout.writeAll("\x1b[2J"); 30 | try stdout.writeAll("\x1b[H"); 31 | } 32 | 33 | pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace) noreturn { 34 | stdout.writeAll("\x1b[2J") catch {}; 35 | stdout.writeAll("\x1b[H") catch {}; 36 | std.builtin.default_panic(msg, error_return_trace); 37 | } 38 | 39 | var gpa = heap.GeneralPurposeAllocator(.{}){}; 40 | const StringArrayList = std.ArrayList([]u8); 41 | 42 | const Editor = struct { 43 | orig_termios: termios, 44 | screen_rows: u16, 45 | cols: u16, 46 | cx: i16, 47 | cy: i16, 48 | row_offset: usize, 49 | col_offset: usize, 50 | rows: StringArrayList, 51 | shutting_down: bool, 52 | allocator: *mem.Allocator, 53 | 54 | const Self = @This(); 55 | 56 | fn new(allocator: *mem.Allocator) !*Self { 57 | const ws = try getWindowSize(); 58 | var editor = try allocator.create(Self); 59 | editor.* = .{ 60 | .orig_termios = undefined, 61 | .screen_rows = ws.rows, 62 | .cols = ws.cols, 63 | .cx = 0, 64 | .cy = 0, 65 | .row_offset = 0, 66 | .col_offset = 0, 67 | .rows = StringArrayList.init(allocator), 68 | .shutting_down = false, 69 | .allocator = allocator, 70 | }; 71 | return editor; 72 | } 73 | 74 | fn free(self: *Self) void { 75 | for (self.rows.items) |row| self.allocator.free(row); 76 | self.rows.deinit(); 77 | } 78 | 79 | const max_size = 1 * 1024 * 1024; 80 | 81 | fn open(self: *Self, filename: []u8) !void { 82 | const file = try std.fs.cwd().openFile(filename, .{}); 83 | defer file.close(); 84 | while (try file.reader().readUntilDelimiterOrEofAlloc(self.allocator, '\n', max_size)) |line| { 85 | try self.rows.append(line); 86 | } 87 | } 88 | 89 | fn enableRawMode(self: *Self) !void { 90 | self.orig_termios = try tcgetattr(stdin_fd); 91 | var raw = self.orig_termios; 92 | raw.iflag &= ~@as(tcflag_t, BRKINT | ICRNL | INPCK | ISTRIP | IXON); 93 | raw.oflag &= ~@as(tcflag_t, OPOST); 94 | raw.cflag |= CS8; 95 | raw.lflag &= ~@as(tcflag_t, ECHO | ICANON | IEXTEN | ISIG); 96 | raw.cc[VMIN] = 0; 97 | raw.cc[VTIME] = 1; 98 | try tcsetattr(stdin_fd, TCSA.FLUSH, raw); 99 | } 100 | 101 | fn disableRawMode(self: *Self) void { 102 | tcsetattr(stdin_fd, TCSA.FLUSH, self.orig_termios) catch panic("tcsetattr", null); 103 | } 104 | 105 | fn moveCursor(self: *Self, movement: Movement) void { 106 | switch (movement) { 107 | .arrow_left => { 108 | if (self.cx > 0) self.cx -= 1; 109 | }, 110 | .arrow_right => { 111 | if (self.cx < self.cols - 1) self.cx += 1; 112 | }, 113 | .arrow_up => { 114 | if (self.cy > 0) self.cy -= 1; 115 | }, 116 | .arrow_down => { 117 | if (self.cy < self.rows.items.len - 1) self.cy += 1; 118 | }, 119 | .page_up, .page_down => { 120 | var n = self.screen_rows; 121 | while (n > 0) : (n -= 1) self.moveCursor(if (movement == .page_up) .arrow_up else .arrow_down); 122 | }, 123 | .home_key => self.cx = 0, 124 | .end_key => self.cx = @intCast(i16, self.cols) - 1, 125 | } 126 | } 127 | 128 | fn processKeyPress(self: *Self) !void { 129 | const key = try self.readKey(); 130 | switch (key) { 131 | .char => |ch| switch (ch) { 132 | ctrlKey('q') => self.shutting_down = true, 133 | else => {}, 134 | }, 135 | .movement => |m| self.moveCursor(m), 136 | .delete => {}, 137 | } 138 | } 139 | 140 | fn readKey(self: *Self) !Key { 141 | const c = try readByte(); 142 | switch (c) { 143 | '\x1b' => { 144 | const c1 = readByte() catch return Key{ .char = '\x1b' }; 145 | if (c1 == '[') { 146 | const c2 = readByte() catch return Key{ .char = '\x1b' }; 147 | switch (c2) { 148 | 'A' => return Key{ .movement = .arrow_up }, 149 | 'B' => return Key{ .movement = .arrow_down }, 150 | 'C' => return Key{ .movement = .arrow_right }, 151 | 'D' => return Key{ .movement = .arrow_left }, 152 | 'F' => return Key{ .movement = .end_key }, 153 | 'H' => return Key{ .movement = .home_key }, 154 | '1' => { 155 | const c3 = readByte() catch return Key{ .char = '\x1b' }; 156 | if (c3 == '~') return Key{ .movement = .home_key }; 157 | }, 158 | '3' => { 159 | const c3 = readByte() catch return Key{ .char = '\x1b' }; 160 | if (c3 == '~') return Key.delete; 161 | }, 162 | '4' => { 163 | const c3 = readByte() catch return Key{ .char = '\x1b' }; 164 | if (c3 == '~') return Key{ .movement = .end_key }; 165 | }, 166 | '5' => { 167 | const c3 = readByte() catch return Key{ .char = '\x1b' }; 168 | if (c3 == '~') return Key{ .movement = .page_up }; 169 | }, 170 | '6' => { 171 | const c3 = readByte() catch return Key{ .char = '\x1b' }; 172 | if (c3 == '~') return Key{ .movement = .page_down }; 173 | }, 174 | else => {}, 175 | } 176 | } else if (c1 == 'O') { 177 | const c2 = readByte() catch return Key{ .char = '\x1b' }; 178 | switch (c2) { 179 | 'F' => return Key{ .movement = .end_key }, 180 | 'H' => return Key{ .movement = .home_key }, 181 | else => {}, 182 | } 183 | } 184 | }, 185 | ctrlKey('n') => return Key{ 186 | .movement = .arrow_down, 187 | }, 188 | ctrlKey('p') => return Key{ 189 | .movement = .arrow_up, 190 | }, 191 | ctrlKey('f') => return Key{ 192 | .movement = .arrow_right, 193 | }, 194 | ctrlKey('b') => return Key{ 195 | .movement = .arrow_left, 196 | }, 197 | else => {}, 198 | } 199 | return Key{ .char = c }; 200 | } 201 | 202 | fn drawRows(self: *Self, writer: anytype) !void { 203 | var y: usize = 0; 204 | while (y < self.screen_rows) : (y += 1) { 205 | const file_row = y + self.row_offset; 206 | if (file_row >= self.rows.items.len) { 207 | if (self.rows.items.len == 0 and y == self.screen_rows / 3) { 208 | var welcome = try fmt.allocPrint(self.allocator, "Kilo self -- version {s}", .{kilo_version}); 209 | defer self.allocator.free(welcome); 210 | if (welcome.len > self.cols) welcome = welcome[0..self.cols]; 211 | var padding = (self.cols - welcome.len) / 2; 212 | if (padding > 0) { 213 | try writer.writeAll("~"); 214 | padding -= 1; 215 | } 216 | while (padding > 0) : (padding -= 1) try writer.writeAll(" "); 217 | try writer.writeAll(welcome); 218 | } else { 219 | try writer.writeAll("~"); 220 | } 221 | } else { 222 | const row = self.rows.items[file_row]; 223 | var len = row.len; 224 | if (len > self.cols) len = self.cols; 225 | try writer.writeAll(row[0..len]); 226 | } 227 | try writer.writeAll("\x1b[K"); 228 | if (y < self.screen_rows - 1) try writer.writeAll("\r\n"); 229 | } 230 | } 231 | 232 | fn scroll(self: *Self) void { 233 | if (self.cy < self.row_offset) { 234 | self.row_offset = @intCast(usize, self.cy); 235 | } 236 | if (self.cy >= self.row_offset + self.screen_rows) { 237 | self.row_offset = @intCast(usize, self.cy - @intCast(i16, self.screen_rows) + 1); 238 | } 239 | } 240 | 241 | fn refreshScreen(self: *Self) !void { 242 | self.scroll(); 243 | var buf = std.ArrayList(u8).init(self.allocator); 244 | defer buf.deinit(); 245 | var writer = buf.writer(); 246 | try writer.writeAll("\x1b[?25l"); 247 | try writer.writeAll("\x1b[H"); 248 | try self.drawRows(writer); 249 | try writer.print("\x1b[{d};{d}H", .{ (self.cy - @intCast(i16, self.row_offset)) + 1, self.cx + 1 }); 250 | try writer.writeAll("\x1b[?25h"); 251 | try stdout.writeAll(buf.items); 252 | } 253 | }; 254 | 255 | inline fn ctrlKey(comptime ch: u8) u8 { 256 | return ch & 0x1f; 257 | } 258 | 259 | const stdin = io.getStdIn().reader(); 260 | const stdout = io.getStdOut().writer(); 261 | 262 | fn readByte() !u8 { 263 | var buf: [1]u8 = undefined; 264 | const n = try stdin.read(buf[0..]); 265 | return buf[0]; 266 | } 267 | 268 | const Movement = enum { 269 | arrow_left, 270 | arrow_right, 271 | arrow_up, 272 | arrow_down, 273 | page_up, 274 | page_down, 275 | home_key, 276 | end_key, 277 | }; 278 | 279 | const Key = union(enum) { 280 | char: u8, 281 | movement: Movement, 282 | delete: void, 283 | }; 284 | 285 | const WindowSize = struct { 286 | rows: u16, 287 | cols: u16, 288 | }; 289 | 290 | fn getWindowSize() !WindowSize { 291 | var ws: winsize = undefined; 292 | switch (errno(system.ioctl(stdin_fd, TIOCGWINSZ, &ws))) { 293 | 0 => return WindowSize{ .rows = ws.ws_row, .cols = ws.ws_col }, 294 | EBADF => return error.BadFileDescriptor, 295 | EINVAL => return error.InvalidRequest, 296 | ENOTTY => return error.NotATerminal, 297 | else => |err| return unexpectedErrno(err), 298 | } 299 | } 300 | 301 | const stdin_fd = io.getStdIn().handle; 302 | --------------------------------------------------------------------------------