main.zig (19398B)
1 const std = @import("std"); 2 3 const version = @import("options").version; 4 const quit: editorKey = .{ .char = CTRL_KEY('q') }; 5 const tabstop = 8; 6 7 //#define CTRL_KEY(k) ((k) & 0x1f) 8 fn CTRL_KEY(k: u8) u8 { 9 return k & 0x1f; 10 } 11 12 var E: ?*EditorState = null; // ONLY use this for the log function... 13 pub const std_options = struct { 14 pub fn logFn( 15 comptime message_level: std.log.Level, 16 comptime scope: @Type(.EnumLiteral), 17 comptime format: []const u8, 18 args: anytype, 19 ) void { 20 const level_txt = comptime message_level.asText(); 21 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; 22 if (E) |es| { 23 nosuspend es.dbglog.writer().print(level_txt ++ prefix2 ++ format ++ "\n", args) catch return; 24 } else { 25 nosuspend std.io.getStdErr().writer().print(level_txt ++ prefix2 ++ format ++ "\n", args) catch return; 26 } 27 } 28 }; 29 const log = std.log.scoped(.kiloz); 30 31 pub fn main() !void { 32 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 33 defer _ = gpa.deinit(); 34 const a = gpa.allocator(); 35 var es = EditorState.init(a); 36 defer es.deinit(); 37 E = &es; 38 es.termios_orig = try enterRawMode(); 39 defer disableRawMode(&es.termios_orig); 40 try useAlternateScreenBuffer(); 41 defer exitAlternateScreenBuffer(); 42 defer clearScreen(); 43 44 try initEditor(&es); 45 const args = try std.process.argsAlloc(es.a); 46 defer std.process.argsFree(es.a, args); 47 if (args.len > 1) { 48 try editorOpen(&es, args[1]); 49 } 50 51 try editorSetStatusMessage(&es, "HELP: ctrl+q = quit", .{}); 52 53 while (true) { 54 try editorRefreshScreen(&es); 55 56 editorProcessKeyPress(&es) catch |e| switch (e) { 57 error.Quit => return, 58 else => return e, 59 }; 60 61 try editorProcessTimeouts(&es); 62 } 63 } 64 65 //// Data 66 67 const EditorState = struct { 68 const StatusMsg = struct { 69 msg: []const u8, 70 time: std.time.Instant, 71 }; 72 a: std.mem.Allocator, 73 screen_buf: std.ArrayListUnmanaged(u8) = .{}, 74 termios_orig: std.os.linux.termios = undefined, 75 screenrows: usize = 0, 76 screencols: usize = 0, 77 cx: usize = 0, 78 cx_t: usize = 0, 79 cy: usize = 0, 80 rx: usize = 0, 81 rowoff: usize = 0, 82 coloff: usize = 0, 83 erow: std.ArrayListUnmanaged(ERow) = .{}, 84 filename: ?[]const u8 = null, 85 statusmsg: ?StatusMsg = null, 86 87 dbglog: std.ArrayList(u8), 88 89 fn init(a: std.mem.Allocator) EditorState { 90 return .{ 91 .a = a, 92 .dbglog = std.ArrayList(u8).init(a), 93 }; 94 } 95 fn deinit(self: *EditorState) void { 96 self.screen_buf.deinit(self.a); 97 for (self.erow.items) |*erow| { 98 erow.deinit(self.a); 99 } 100 self.erow.deinit(self.a); 101 if (self.statusmsg) |*statusmsg| { 102 self.a.free(statusmsg.msg); 103 } 104 const wtr = std.io.getStdOut().writer(); 105 wtr.writeAll(self.dbglog.items) catch {}; 106 self.dbglog.deinit(); 107 } 108 }; 109 110 const CharAL = std.ArrayListUnmanaged(u8); 111 112 const ERow = struct { 113 chars: CharAL, 114 render: CharAL, 115 116 pub fn deinit(self: *ERow, a: std.mem.Allocator) void { 117 self.chars.deinit(a); 118 self.render.deinit(a); 119 } 120 }; 121 122 fn editorProcessTimeouts(es: *EditorState) !void { 123 if (es.statusmsg) |*smg| { 124 const now = try std.time.Instant.now(); 125 if (now.since(smg.time) > 5 * std.time.ns_per_s) { 126 const x = smg.msg; 127 es.statusmsg = null; 128 es.a.free(x); 129 } 130 } 131 } 132 133 //// Terminal handling 134 135 // Returns the original termios setting for restoring terminal function at exit. 136 fn enterRawMode() !std.os.linux.termios { 137 var termios_orig: std.os.linux.termios = undefined; 138 if (std.os.linux.tcgetattr(std.os.linux.STDIN_FILENO, &termios_orig) != 0) return error.TcGetAttr; 139 var termios_raw = termios_orig; 140 termios_raw.iflag &= ~(std.os.linux.IXON | std.os.linux.ICRNL | std.os.linux.BRKINT | std.os.linux.INPCK | std.os.linux.ISTRIP); 141 termios_raw.lflag &= ~(std.os.linux.ECHO | std.os.linux.ICANON | std.os.linux.ISIG | std.os.linux.IEXTEN); 142 termios_raw.oflag &= ~(std.os.linux.OPOST); 143 termios_raw.cflag |= ~(std.os.linux.CS8); 144 termios_raw.cc[std.os.linux.V.MIN] = 0; 145 termios_raw.cc[std.os.linux.V.TIME] = 1; 146 if (std.os.linux.tcsetattr(std.os.linux.STDIN_FILENO, .FLUSH, &termios_raw) != 0) return error.TcSetAttr; 147 return termios_orig; 148 } 149 150 fn disableRawMode(t: *const std.os.linux.termios) void { 151 if (std.os.linux.tcsetattr(std.os.linux.STDIN_FILENO, .FLUSH, t) != 0) { 152 std.fmt.format(std.io.getStdErr().writer(), "Error resetting terminal, it might be broken\r\n", .{}) catch {}; 153 } 154 } 155 156 fn useAlternateScreenBuffer() !void { 157 try std.io.getStdOut().writer().writeAll("\x1b[?1049h"); 158 } 159 160 fn exitAlternateScreenBuffer() void { 161 std.io.getStdOut().writer().writeAll("\x1b[?1049l") catch {}; 162 } 163 164 const editorKey = union(enum) { 165 char: u8, 166 virt: enum { 167 ARROW_LEFT, 168 ARROW_RIGHT, 169 ARROW_UP, 170 ARROW_DOWN, 171 PAGE_UP, 172 PAGE_DOWN, 173 HOME, 174 END, 175 DEL, 176 BACKSPACE, 177 }, 178 }; 179 180 fn editorReadKey() !editorKey { 181 var rdr = std.io.getStdIn().reader(); 182 while (true) { 183 const ch = rdr.readByte() catch |e| switch (e) { 184 error.EndOfStream => continue, 185 else => return e, 186 }; 187 if (ch == 127) { 188 return .{.virt = .BACKSPACE}; 189 } else if (ch == '\x1b') { 190 const ch1 = rdr.readByte() catch |e| switch (e) { 191 error.EndOfStream => return .{ .char = '\x1b' }, 192 else => return e, 193 }; 194 const ch2 = rdr.readByte() catch |e| switch (e) { 195 error.EndOfStream => return .{ .char = '\x1b' }, 196 else => return e, 197 }; 198 if (ch1 == '[') { 199 if (std.ascii.isDigit(ch2)) { 200 const ch3 = rdr.readByte() catch |e| switch (e) { 201 error.EndOfStream => return .{ .char = '\x1b' }, 202 else => return e, 203 }; 204 if (ch3 == '~') { 205 switch (ch2) { 206 '1', '7' => return .{ .virt = .HOME }, 207 '4', '8' => return .{ .virt = .END }, 208 '5' => return .{ .virt = .PAGE_UP }, 209 '6' => return .{ .virt = .PAGE_DOWN }, 210 '3' => return .{ .virt = .DEL }, 211 else => return .{ .char = '\x1b' }, 212 } 213 } else { 214 return .{ .char = '\x1b' }; 215 } 216 } else { 217 switch (ch2) { 218 'A' => return .{ .virt = .ARROW_UP }, 219 'B' => return .{ .virt = .ARROW_DOWN }, 220 'C' => return .{ .virt = .ARROW_RIGHT }, 221 'D' => return .{ .virt = .ARROW_LEFT }, 222 'F' => return .{ .virt = .END }, 223 'H' => return .{ .virt = .HOME }, 224 else => return .{ .char = '\x1b' }, 225 } 226 } 227 } else if (ch1 == 'O') { 228 switch (ch2) { 229 'F' => return .{ .virt = .END }, 230 'H' => return .{ .virt = .HOME }, 231 else => return .{ .char = '\x1b' }, 232 } 233 } else { 234 return .{ .char = '\x1b' }; 235 } 236 } 237 return .{ .char = ch }; 238 } 239 } 240 241 fn getCursorPosition() !Size { 242 const wtr = std.io.getStdOut().writer(); 243 var rdr = std.io.getStdIn().reader(); 244 try wtr.writeAll("\x1b[6n"); 245 try wtr.writeAll("\r\n"); 246 var buf = [_]u8{0} ** 32; 247 try rdr.skipBytes(2, .{}); 248 const szstr = try rdr.readUntilDelimiter(&buf, 'R'); 249 var spl = std.mem.splitScalar(u8, szstr, ';'); 250 const rowsstr = spl.first(); 251 const colsstr = spl.next() orelse return error.CursorPosFormatError; 252 const rows = try std.fmt.parseInt(u16, rowsstr, 10); 253 const cols = try std.fmt.parseInt(u16, colsstr, 10); 254 return .{ .rows = rows, .cols = cols }; 255 } 256 257 const Size = struct { 258 rows: u16, 259 cols: u16, 260 }; 261 262 fn getWindowSize() !Size { 263 var out = std.io.getStdOut(); 264 var wsz: std.os.linux.winsize = undefined; 265 if (std.os.linux.ioctl(out.handle, std.os.linux.T.IOCGWINSZ, @intFromPtr(&wsz)) == -1 or wsz.ws_col == 0) { 266 // Hack: move teh cursor then read it's position 267 try out.writer().writeAll("\x1b[999C\x1b[999B"); 268 return getCursorPosition(); 269 } else { 270 return .{ 271 .rows = wsz.ws_row, 272 .cols = wsz.ws_col, 273 }; 274 } 275 } 276 277 //// Row operations 278 279 fn editorAppendRow(es: *EditorState, line: CharAL) !void { 280 try es.erow.append(es.a, .{ 281 .chars = line, 282 .render = .{}, 283 }); 284 try editorUpdateRow(es, &es.erow.items[es.erow.items.len - 1]); 285 } 286 287 fn editorUpdateRow(es: *EditorState, erow: *ERow) !void { 288 erow.render.clearRetainingCapacity(); 289 for (erow.chars.items) |c| { 290 if (c == '\t') { 291 var tabs = tabstop - @mod(erow.render.items.len, tabstop); 292 if (tabs == 0) tabs = tabstop; 293 try erow.render.appendNTimes(es.a, ' ', tabs); 294 } else { 295 try erow.render.append(es.a, c); 296 } 297 } 298 } 299 300 fn editorRowCxToRx(erow: *ERow, cx: usize) usize { 301 var rx: usize = 0; 302 for (0..cx) |j| { 303 if (erow.chars.items[j] == '\t') { 304 rx += (tabstop - 1) - (@mod(rx, tabstop)); 305 } else { 306 rx += 1; 307 } 308 } 309 return rx; 310 } 311 312 fn editorRowInsertChar(es: *EditorState, erow: *ERow, at: usize, c: u8) !void { 313 try erow.chars.insert(es.a, at, c); 314 try editorUpdateRow(es, erow); 315 } 316 317 //// Editor operations 318 319 fn editorInsertChar(es: *EditorState, c: u8) !void { 320 if (es.cy == es.erow.items.len) { 321 try editorAppendRow(es, .{}); 322 } 323 try editorRowInsertChar(es, &es.erow.items[es.cy], es.cx, c); 324 es.cx += 1; 325 } 326 327 //// File i/o 328 329 fn editorOpen(es: *EditorState, filename: []const u8) !void { 330 const f = try std.fs.cwd().openFile(filename, .{}); 331 defer f.close(); 332 var br = std.io.bufferedReader(f.reader()); 333 var rdr = br.reader(); 334 335 lp: while (true) { 336 var line = std.ArrayListUnmanaged(u8){}; 337 errdefer line.deinit(es.a); 338 rdr.streamUntilDelimiter(line.writer(es.a), '\n', null) catch |e| switch (e) { 339 error.EndOfStream => break :lp, 340 else => return e, 341 }; 342 try editorAppendRow(es, line); 343 } 344 es.filename = filename; 345 } 346 347 fn editorSave(es: *EditorState) !void { 348 if (es.filename) |fname| { 349 const str = try editorRowsToString(es); 350 defer es.a.free(str); 351 const f = try std.fs.cwd().openFile(fname, .{.mode = .write_only}); 352 defer f.close(); 353 try f.setEndPos(str.len); 354 try f.writeAll(str); 355 } 356 } 357 358 fn editorRowsToString(es: *EditorState) ![]const u8 { 359 var str = CharAL{}; 360 defer str.deinit(es.a); 361 for (es.erow.items) |erow| { 362 try str.appendSlice(es.a, erow.chars.items); 363 try str.append(es.a, '\n'); 364 } 365 return try str.toOwnedSlice(es.a); 366 } 367 368 //// Output 369 370 fn clearScreen() void { 371 const wtr = std.io.getStdOut().writer(); 372 wtr.writeAll("\x1b[2J") catch {}; 373 wtr.writeAll("\x1b[H") catch {}; 374 } 375 376 fn editorRefreshScreen(es: *EditorState) !void { 377 editorScroll(es); 378 379 // Use a buffer to avoid multiple write() calls to the actual terminal device 380 // Reduces rendering artifacts / flickering 381 es.screen_buf.clearRetainingCapacity(); 382 const wtr = es.screen_buf.writer(es.a); 383 // Check the vt100 manual! 384 try wtr.writeAll("\x1b[?25l"); // Hide the cursor 385 //try wtr.writeAll("\x1b[2J"); // J clear 2 whole screen 386 try wtr.writeAll("\x1b[H"); // Reset cursor (to 1:1) 387 try editorDrawRows(es, wtr); 388 try editorDrawStatusBar(es, wtr); 389 try editorDrawMessageBar(es, wtr); 390 try std.fmt.format(wtr, "\x1b[{};{}H", .{ 391 es.cy - es.rowoff + 1, 392 es.rx - es.coloff + 1, 393 }); // Move the cursor to our stored position 394 try wtr.writeAll("\x1b[?25h"); // Show the cursor again 395 try std.io.getStdOut().writeAll(es.screen_buf.items); 396 } 397 398 fn editorDrawRows(es: *const EditorState, wtr: anytype) !void { 399 for (0..es.screenrows) |y| { 400 try wtr.writeAll("\x1b[K"); // Clear row before writing it 401 402 var lw = truncateWriter(wtr, es.screencols); // Never write more than we have columns 403 const wtr2 = lw.writer(); 404 const filerow = y + es.rowoff; 405 if (filerow >= es.erow.items.len) { 406 if (es.erow.items.len == 0 and y == es.screenrows / 3) { 407 const welcome_msg = try std.fmt.allocPrint(es.a, "Kilo editor -- version {s}", .{version}); // TODO don't allocate every time... 408 defer es.a.free(welcome_msg); 409 const padding = (es.screencols - welcome_msg.len) / 2; 410 for (0..padding) |y2| { 411 try wtr2.writeByte(if (y2 == 0) '~' else ' '); 412 } 413 try wtr2.writeAll(welcome_msg); 414 } else { 415 try wtr2.writeAll("~"); 416 } 417 } else { 418 var row = es.erow.items[filerow].render.items; 419 if (row.len < es.coloff) { 420 row = ""; 421 } else { 422 row = row[es.coloff..]; 423 } 424 try wtr2.writeAll(row); 425 } 426 try wtr.writeAll("\r\n"); 427 } 428 } 429 430 // wtr should be std.io.writer 431 fn editorDrawStatusBar(es: *const EditorState, wtr: anytype) !void { 432 try wtr.writeAll("\x1b[7m"); // invert colours 433 const buf = try es.a.alloc(u8, es.screencols); 434 defer es.a.free(buf); 435 @memset(buf, ' '); 436 const fname = es.filename orelse "<no file>"; 437 _ = std.fmt.bufPrint(buf, "{s} - {} lines", .{ fname, es.erow.items.len }) catch |e| switch (e) { 438 error.NoSpaceLeft => {}, 439 else => return e, 440 }; 441 const sz = std.fmt.count("{}/{}", .{ es.cy + 1, es.erow.items.len }); 442 if (buf.len >= sz) { 443 _ = std.fmt.bufPrint(buf[buf.len - sz ..], "{}/{}", .{ es.cy + 1, es.erow.items.len }) catch |e| switch (e) { 444 error.NoSpaceLeft => {}, 445 else => return e, 446 }; 447 } 448 try wtr.writeAll(buf); 449 try wtr.writeAll("\x1b[m"); // normal colours 450 try wtr.writeAll("\r\n"); // Make another line for input bar 451 } 452 453 fn editorDrawMessageBar(es: *EditorState, wtr: anytype) !void { 454 try wtr.writeAll("\x1b[K"); 455 if (es.statusmsg) |statusmsg| { 456 var tw = truncateWriter(wtr, es.screencols); 457 const wtr2 = tw.writer(); 458 try wtr2.writeAll(statusmsg.msg); 459 } 460 } 461 462 fn editorScroll(es: *EditorState) void { 463 es.rx = 0; 464 if (es.cy < es.erow.items.len) { 465 es.rx = editorRowCxToRx(&es.erow.items[es.cy], es.cx); 466 } 467 468 if (es.cy < es.rowoff) { 469 es.rowoff = es.cy; 470 } 471 if (es.cy >= es.rowoff + es.screenrows) { 472 es.rowoff = es.cy - es.screenrows + 1; 473 } 474 if (es.rx < es.coloff) { 475 es.coloff = es.rx; 476 } 477 if (es.rx >= es.coloff + es.screencols) { 478 es.coloff = es.rx - es.screencols + 1; 479 } 480 } 481 482 fn editorSetStatusMessage(es: *EditorState, comptime fmt: []const u8, args: anytype) !void { 483 if (es.statusmsg) |*smg| { 484 const x = smg.msg; 485 es.statusmsg = null; 486 es.a.free(x); 487 } 488 const y: EditorState.StatusMsg = .{ 489 .msg = try std.fmt.allocPrint(es.a, fmt, args), 490 .time = try std.time.Instant.now(), 491 }; 492 es.statusmsg = y; 493 } 494 495 //// Input 496 497 fn editorProcessKeyPress(es: *EditorState) !void { 498 const key = try editorReadKey(); 499 switch (key) { 500 .char => |ch| { 501 switch (ch) { 502 quit.char => return error.Quit, 503 CTRL_KEY('s') => try editorSave(es), 504 '\r' => { 505 // TODO 506 }, 507 else => try editorInsertChar(es, ch), 508 } 509 }, 510 .virt => |v| { 511 switch (v) { 512 .ARROW_UP, .ARROW_DOWN, .ARROW_LEFT, .ARROW_RIGHT, .PAGE_UP, .PAGE_DOWN, .HOME, .END => editorMoveCursor(key, es), 513 .DEL, .BACKSPACE => { 514 // TODO 515 }, 516 //else => {}, 517 } 518 }, 519 } 520 } 521 522 fn editorVMove(dir: enum { up, down }, n: usize, es: *EditorState) void { 523 es.cy = switch (dir) { 524 .up => std.math.sub(usize, es.cy, n) catch 0, 525 .down => std.math.clamp(es.cy + n, 0, es.erow.items.len - 1), 526 }; 527 } 528 529 fn editorMoveCursor(key: editorKey, es: *EditorState) void { 530 switch (key.virt) { 531 .ARROW_UP => { 532 editorVMove(.up, 1, es); 533 }, 534 .ARROW_DOWN => { 535 editorVMove(.down, 1, es); 536 }, 537 .ARROW_LEFT => { 538 if (es.cx_t == 0) { 539 if (es.cy > 0) { 540 es.cy -= 1; 541 es.cx_t = es.erow.items[es.cy].chars.items.len; 542 } 543 } else { 544 if (es.cx_t >= es.erow.items[es.cy].chars.items.len) { 545 es.cx_t = es.erow.items[es.cy].chars.items.len - 1; 546 } else { 547 es.cx_t -= 1; 548 } 549 } 550 }, 551 .ARROW_RIGHT => { 552 if (es.cx_t >= es.erow.items[es.cy].chars.items.len) { 553 if (es.cy < (es.erow.items.len - 1)) { 554 es.cy += 1; 555 es.cx_t = 0; 556 } 557 } else { 558 es.cx_t += 1; 559 } 560 }, 561 .PAGE_UP => { 562 editorVMove(.up, es.screenrows, es); 563 }, 564 .PAGE_DOWN => { 565 editorVMove(.down, es.screenrows, es); 566 }, 567 .HOME => { 568 es.cx_t = 0; 569 }, 570 .END => { 571 es.cx_t = std.math.maxInt(usize); 572 }, 573 else => {}, 574 } 575 576 es.cx = std.math.clamp(es.cx_t, 0, es.erow.items[es.cy].chars.items.len); 577 } 578 579 //// Init 580 581 fn initEditor(es: *EditorState) !void { 582 const sz = try getWindowSize(); 583 es.screenrows = sz.rows - 2; // Allow room for status bar and message bar 584 es.screencols = sz.cols; 585 } 586 587 //// Extra stuff 588 589 fn truncateWriter(base_writer: anytype, max: usize) TruncateWriter(@TypeOf(base_writer)) { 590 return TruncateWriter(@TypeOf(base_writer)){ 591 .base_writer = base_writer, 592 .max = max, 593 }; 594 } 595 596 fn TruncateWriter(comptime BaseWriter: type) type { 597 return struct { 598 const Self = @This(); 599 const WriteError = BaseWriter.Error || error{NoSpaceLeft}; 600 const Writer = std.io.Writer(*Self, WriteError, Self.write); 601 602 base_writer: BaseWriter, 603 max: usize, 604 written: usize = 0, 605 606 fn write(self: *Self, bytes: []const u8) WriteError!usize { 607 const rem = self.max - self.written; 608 if (rem == 0) return bytes.len; 609 var to_write = bytes; 610 if (bytes.len > rem) { 611 to_write = bytes[0..rem]; 612 } 613 const written = try self.base_writer.write(to_write); 614 self.written += written; 615 return written; 616 } 617 618 fn writer(self: *Self) Writer { 619 return .{ 620 .context = self, 621 }; 622 } 623 }; 624 }