kiloz

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

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 }