╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────╮ │1 const std = @import("std"); │ │2 const xit = @import("xit"); │ │3 const rp = xit.repo; │ │4 -const hash = xit.hash; │ │4 const xitui = xit.xitui; │ │5 const term = xitui.terminal;│ │6 const wgt = xitui.widget; │ ╰───────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │54 data: Data = .{}, // serializable data sent down to web client │ │55 arena: *std.heap.ArenaAllocator, │ │56 haxy_moment: ?DB.HashMap(.read_only) = null, // db cursor (null on the wasm side)│ │57 + pending: std.ArrayList(Action) = .empty, // actions queued by widgets this frame │ │58 │ │59 pub const Data = struct { │ │60 user_id: ?[]const u8 = null, │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 71 repo: *rp.Repo(.xit, repo_opts), │ │ 72 data: Data, │ │ 73 ) !Session { │ │ 74 - const RepoDB = rp.Repo(.xit, repo_opts).DB; │ │ 74 + var session = Session{ │ │ 75 + .data = data, │ │ 76 + .arena = arena, │ │ 77 + .haxy_moment = try evt.currentMoment(repo_opts, repo), │ │ 78 + }; │ │ 79 + try session.loadUserPrefs(); │ │ 80 + return session; │ │ 81 + } │ │ 82 │ │ 76 - const history = try RepoDB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); │ │ 83 + // load the logged-in user's persisted preferences from the db │ │ 84 + pub fn loadUserPrefs(self: *Session) !void { │ │ 85 + const user_id = self.data.user_id orelse return; │ │ 86 + const moment = self.haxy_moment orelse return; │ │ 87 + if (try evt.User.readById(DB, canonical_repo_opts.hash, moment, self.arena, user_id)) |user| { │ │ 88 + self.data.enable_ansi = user.enable_ansi; │ │ 89 + } │ │ 90 + } │ │ 91 │ │ 78 - const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; │ │ 79 - const moment = try RepoDB.HashMap(.read_only).init(moment_cursor); │ │ 92 + // queue an action for the host to drain this frame. │ │ 93 + pub fn push(self: *Session, action: Action) !void { │ │ 94 + try self.pending.append(self.arena.allocator(), action); │ │ 95 + } │ │ 96 │ │ 81 - const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound;│ │ 82 - var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │ 83 - _ = try last_object_id_cursor.readBytes(&last_object_id); │ │ 97 + // apply a single action's in-memory effect only (no persistence). │ │ 98 + pub fn apply(self: *Session, action: Action) void { │ │ 99 + switch (action) { │ │100 + .toggle_ansi => self.data.enable_ansi = !self.data.enable_ansi, │ │101 + } │ │102 + } │ │103 │ │ 85 - const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; │ │ 86 - const haxy = try RepoDB.ArrayList(.read_only).init(haxy_cursor); │ │104 + // drain queued actions, applying only their in-memory effect. used on the │ │105 + // wasm path, which has no repo to persist to (and only anonymous toggles │ │106 + // reach it anyway). │ │107 + pub fn applyPending(self: *Session) void { │ │108 + for (self.pending.items) |action| self.apply(action); │ │109 + self.pending.clearRetainingCapacity(); │ │110 + } │ │111 +}; │ │112 │ │ 88 - const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; │ │ 89 - const haxy_moments = try RepoDB.HashMap(.read_only).init(haxy_moments_cursor); │ │113 +// a user-initiated state change. widgets enqueue these on the Session during │ │114 +// input instead of mutating state or touching the DB themselves; the host │ │115 +// drains them each frame (see dispatchPending / applyPending). this keeps DB │ │116 +// side effects out of the widget tree and gives every render path one place to │ │117 +// turn a UI action into a state change + event. │ │118 +pub const Action = union(enum) { │ │119 + toggle_ansi, │ │120 │ │ 91 - const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound;│ │ 92 - const haxy_moment = try RepoDB.HashMap(.read_only).init(haxy_moment_cursor); │ │121 + // apply this action and, when it maps to persistent state and the session │ │122 + // is logged in, emit the corresponding event. the single place that knows │ │123 + // how a UI action becomes a DB write. │ │124 + pub fn dispatch( │ │125 + self: Action, │ │126 + comptime repo_opts: rp.RepoOpts(.xit), │ │127 + io: std.Io, │ │128 + allocator: std.mem.Allocator, │ │129 + repo: *rp.Repo(.xit, repo_opts), │ │130 + session: *Session, │ │131 + ) !void { │ │132 + session.apply(self); │ │133 + switch (self) { │ │134 + .toggle_ansi => if (session.data.user_id) |user_id| { │ │135 + try evt.User.toggleAnsi(repo_opts, io, allocator, repo, user_id); │ │136 + }, │ │137 + } │ │138 + } │ │139 │ │ 94 - return .{ │ │ 95 - .data = data, │ │ 96 - .arena = arena, │ │ 97 - .haxy_moment = haxy_moment, │ │ 98 - }; │ │140 + // drain the session's queued actions through dispatch. always clears the │ │141 + // queue, even if a dispatch fails, so a single bad write isn't retried │ │142 + // every frame. │ │143 + pub fn dispatchPending( │ │144 + comptime repo_opts: rp.RepoOpts(.xit), │ │145 + io: std.Io, │ │146 + allocator: std.mem.Allocator, │ │147 + repo: *rp.Repo(.xit, repo_opts), │ │148 + session: *Session, │ │149 + ) !void { │ │150 + defer session.pending.clearRetainingCapacity(); │ │151 + for (session.pending.items) |action| { │ │152 + try action.dispatch(repo_opts, io, allocator, repo, session); │ │153 + } │ │154 } │ │155 }; │ │156 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │191 blocking = false; │ │192 } │ │193 │ │194 + // local run has no repo to persist to, so just apply in-memory │ │195 + session.applyPending(); │ │196 + │ │197 try root.build(allocator, .{ │ │198 .min_size = .{ .width = null, .height = null }, │ │199 .max_size = .{ .width = last_size.width, .height = last_size.height },│ ╰───────────────────────────────────────────────────────────────────────────────────────╯