diff options
Diffstat (limited to 'src/dump.zig')
| -rw-r--r-- | src/dump.zig | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/src/dump.zig b/src/dump.zig new file mode 100644 index 00000000..9c1dceec --- /dev/null +++ b/src/dump.zig @@ -0,0 +1,218 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub const default_path = "/tmp/pardes-dump.zon"; +pub const magic = "pardes-dump"; +pub const version: u32 = 1; +pub const max_panes: usize = 16; +pub const max_cols: usize = 6; + +pub const Size = struct { + cols: u16, + rows: u16, +}; + +pub const Cursor = struct { + col: u16 = 0, + row: u16 = 0, +}; + +pub const PaneKind = enum { + terminal, + file, + image, +}; + +pub const Terminal = struct { + cwd: []const u8 = "", + stream: []const u8 = "", + stream_b64: []const u8 = "", + cursor: Cursor = .{}, +}; + +pub const File = struct { + path: []const u8 = "", + content: []const u8 = "", + content_b64: []const u8 = "", +}; + +pub const Image = struct { + path: []const u8 = "", + bytes_b64: []const u8 = "", +}; + +pub const Pane = struct { + kind: PaneKind, + tag: []const u8, + body: []const u8, + scroll: usize = 0, + cols: u16 = 0, + rows: u16 = 0, + vweight: f32 = 1, + terminal: ?Terminal = null, + file: ?File = null, + image: ?Image = null, +}; + +pub const Column = struct { + weight: f32 = 1, + panes: []const usize = &.{}, +}; + +pub const State = struct { + magic: []const u8 = magic, + version: u32 = version, + screen: Size, + active: usize = 0, + topbar: []const u8 = "", + theme: []const u8 = "dark", + columns: []const Column = &.{}, + panes: []const Pane = &.{}, +}; + +pub fn validate(state: State) !void { + if (!std.mem.eql(u8, state.magic, magic)) return error.BadDumpMagic; + if (state.version != version) return error.BadDumpVersion; + if (state.panes.len == 0 or state.panes.len > max_panes) return error.BadDumpPanes; + if (state.columns.len == 0 or state.columns.len > max_cols) return error.BadDumpColumns; + if (state.active >= state.panes.len) return error.BadDumpActive; + for (state.columns) |col| { + if (col.panes.len == 0 or col.panes.len > max_panes) return error.BadDumpColumns; + for (col.panes) |pane| { + if (pane >= state.panes.len) return error.BadDumpPaneRef; + } + } + for (state.panes) |pane| { + switch (pane.kind) { + .terminal => if (pane.terminal == null) return error.BadDumpPaneKind, + .file => if (pane.file == null) return error.BadDumpPaneKind, + .image => if (pane.image == null) return error.BadDumpPaneKind, + } + } +} + +pub fn writeFile(io: std.Io, gpa: std.mem.Allocator, path: []const u8, state: State) !void { + try validate(state); + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + try std.zon.stringify.serialize(state, .{ .whitespace = true }, &out.writer); + + var file = try std.Io.Dir.createFileAbsolute(io, path, .{}); + defer file.close(io); + try file.writeStreamingAll(io, out.written()); +} + +pub fn readFile(io: std.Io, gpa: std.mem.Allocator, path: []const u8) !State { + const bytes = try std.Io.Dir.cwd().readFileAlloc(io, path, gpa, .limited(64 * 1024 * 1024)); + defer gpa.free(bytes); + return readZon(gpa, bytes, path); +} + +pub fn readZon(gpa: std.mem.Allocator, bytes: []const u8, label: []const u8) !State { + const source = try gpa.dupeZ(u8, bytes); + defer gpa.free(source); + return readZonZ(gpa, source, label); +} + +pub fn readZonZ(gpa: std.mem.Allocator, source: [:0]const u8, label: []const u8) !State { + if (builtin.os.tag == .emscripten) { + const state = std.zon.parse.fromSliceAlloc(State, gpa, source, null, .{}) catch |err| { + std.log.err("parse dump {s}: {s}", .{ label, @errorName(err) }); + return err; + }; + errdefer std.zon.parse.free(gpa, state); + try validate(state); + return state; + } + + var diag: std.zon.parse.Diagnostics = .{}; + defer diag.deinit(gpa); + const state = std.zon.parse.fromSliceAlloc(State, gpa, source, &diag, .{}) catch |err| { + std.log.err("parse dump {s}: {f}", .{ label, diag }); + return err; + }; + errdefer std.zon.parse.free(gpa, state); + try validate(state); + return state; +} + +pub fn free(gpa: std.mem.Allocator, state: State) void { + std.zon.parse.free(gpa, state); +} + +pub fn encodeBytes(alloc: std.mem.Allocator, bytes: []const u8) ![]const u8 { + const enc = std.base64.standard.Encoder; + const out = try alloc.alloc(u8, enc.calcSize(bytes.len)); + return enc.encode(out, bytes); +} + +pub fn decodeBytes(alloc: std.mem.Allocator, b64: []const u8) ![]u8 { + const dec = std.base64.standard.Decoder; + const n = try dec.calcSizeForSlice(b64); + const out = try alloc.alloc(u8, n); + errdefer alloc.free(out); + try dec.decode(out, b64); + return out; +} + +test "dump zon roundtrip" { + const gpa = std.testing.allocator; + const tty_b64 = try encodeBytes(gpa, "old\nhello\nworld"); + defer gpa.free(tty_b64); + const file_b64 = try encodeBytes(gpa, "alpha\nbeta\n"); + defer gpa.free(file_b64); + const panes = [_]Pane{ + .{ + .kind = .terminal, + .tag = "nm /tmp Del", + .body = "hello\nworld", + .scroll = 1, + .cols = 40, + .rows = 10, + .terminal = .{ .cwd = "/tmp", .stream = "old\nhello\nworld", .stream_b64 = tty_b64, .cursor = .{ .col = 3, .row = 2 } }, + }, + .{ + .kind = .file, + .tag = "nm /tmp/a.txt Save Del", + .body = " 1 alpha", + .cols = 20, + .rows = 5, + .file = .{ .path = "/tmp/a.txt", .content = "alpha\nbeta\n", .content_b64 = file_b64 }, + }, + }; + const col_panes = [_]usize{ 0, 1 }; + const columns = [_]Column{.{ .weight = 1, .panes = &col_panes }}; + const state: State = .{ + .screen = .{ .cols = 80, .rows = 24 }, + .topbar = "Kill Newcol Dump", + .theme = "dark", + .columns = &columns, + .panes = &panes, + }; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + try std.zon.stringify.serialize(state, .{ .whitespace = true }, &out.writer); + const source = try out.toOwnedSliceSentinel(0); + defer gpa.free(source); + + var diag: std.zon.parse.Diagnostics = .{}; + defer diag.deinit(gpa); + const parsed = try std.zon.parse.fromSliceAlloc(State, gpa, source, &diag, .{}); + defer std.zon.parse.free(gpa, parsed); + try validate(parsed); + try std.testing.expectEqual(@as(usize, 2), parsed.panes.len); + try std.testing.expectEqualStrings("dark", parsed.theme); + try std.testing.expectEqualStrings("old\nhello\nworld", parsed.panes[0].terminal.?.stream); + try std.testing.expectEqualStrings("alpha\nbeta\n", parsed.panes[1].file.?.content); + { + const bytes = try decodeBytes(gpa, parsed.panes[0].terminal.?.stream_b64); + defer gpa.free(bytes); + try std.testing.expectEqualStrings("old\nhello\nworld", bytes); + } + { + const bytes = try decodeBytes(gpa, parsed.panes[1].file.?.content_b64); + defer gpa.free(bytes); + try std.testing.expectEqualStrings("alpha\nbeta\n", bytes); + } +} |
