╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#adc733bec608fbb3f8ded4f300ff77cb6f113ade",│ │ 7 - .hash = "xit-0.0.0-Ub3nDFiLUAHYgIZkm-espBoQ3mEe3_ltpaNQWr9TILvO", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#b6b75683bbef5565ad5f5a051b125897cc81057f",│ │ 7 + .hash = "xit-0.0.0-Ub3nDFiLUAFW6PUrE1SlcSzbjg6OTJQorb7hfncQBa9h", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/embed/index.html│ ╰────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │58 their parent (.page) is the positioning ancestor and the│ │59 grid sits at its top-left. */ │ │60 .page input, │ │61 + .page textarea, │ │62 .page button { │ │63 position: absolute; │ │64 font: inherit; │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────╮ │68 box-sizing: border-box; │ │69 } │ │70 │ │70 - .page input { │ │71 + .page input, │ │72 + .page textarea { │ │73 color: inherit; │ │74 background: transparent;│ │75 border: none; │ ╰────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │76 } │ │77 │ │78 + /* the TUI border cells own the frame, so the textarea can't be│ │79 + drag-resized past them */ │ │80 + .page textarea { │ │81 + resize: none; │ │82 + overflow-y: auto; │ │83 + } │ │84 + │ │85 /* the TUI cells under the button render the border + label; │ │86 the overlay button stays transparent so they show through │ │87 and just owns the click + form-submit behavior. */ │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │189 // let the browser handle modifier combos. the TUI only uses unmodified keys,│ │190 // so intercepting these would just break normal browser navigation. │ │191 if (event.altKey || event.ctrlKey || event.metaKey) return; │ │192 - // when a form element (text input or submit button) owns focus, │ │193 - // the browser handles typing, Enter-to-submit, and Tab natively. │ │194 - // we only forward keys into the TUI when focus is elsewhere. │ │192 + // when a form element (text input or submit button) owns focus, the │ │193 + // browser handles typing and Tab natively (and Enter on the button │ │194 + // submits). we only forward keys into the TUI when focus is elsewhere. │ │195 // exception: arrow up/down on an input unfocuses it and forwards │ │196 // the event so the TUI can move focus between widgets. │ │197 if (document.activeElement) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │198 const tag = document.activeElement.tagName; │ │199 const isArrow = event.key === "ArrowUp" || event.key === "ArrowDown" || │ │200 event.key === "ArrowLeft" || event.key === "ArrowRight"; │ │201 + if (tag === "INPUT" && event.key === "Enter") { │ │202 + // enter in a text input never submits the form; only the │ │203 + // submit button does │ │204 + event.preventDefault(); │ │205 + return; │ │206 + } │ │207 if (tag === "INPUT" && (event.key === "ArrowUp" || event.key === "ArrowDown")) {│ │208 // up/down moves between widgets; left/right stay in the input │ │209 document.activeElement.blur(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │210 } else if (tag === "BUTTON" && isArrow) { │ │211 // arrows navigate away from a focused submit button │ │212 document.activeElement.blur(); │ │207 - } else if (tag === "INPUT" || tag === "BUTTON") { │ │208 - // Enter (submit), typing, Tab — handled natively by the browser │ │213 + } else if (tag === "INPUT" || tag === "BUTTON" || tag === "TEXTAREA") {│ │214 + // Enter (submit / newline), typing, Tab, and a textarea's │ │215 + // arrows — handled natively by the browser │ │216 return; │ │217 } │ │218 } │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │227 // input/focus events from form elements still bubble up to document. │ │228 document.addEventListener("input", (event) => { │ │229 const t = event.target; │ │223 - if (!t || t.tagName !== "INPUT" || !t.dataset.focusId) return; │ │230 + if (!t || (t.tagName !== "INPUT" && t.tagName !== "TEXTAREA") || !t.dataset.focusId) return;│ │231 sendTextInputValue(Number(t.dataset.focusId), t.value); │ │232 }); │ │233 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │519 return try DB.HashMap(.read_only).init(haxy_moment_cursor); │ │520 } │ │521 │ │522 +// commit each event as a JSON commit message on `ref`, then consume them into │ │523 +// the db the repo's views read: the repo's own db, or the standalone local │ │524 +// event db when `is_local`. │ │525 pub fn commitAndConsume( │ │523 - comptime repo_opts: rp.RepoOpts(.xit), │ │526 + comptime repo_kind: rp.RepoKind, │ │527 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │528 io: std.Io, │ │529 allocator: std.mem.Allocator, │ │526 - repo: *rp.Repo(.xit, repo_opts), │ │530 + repo: *rp.Repo(repo_kind, repo_opts), │ │531 ref: rf.Ref, │ │532 events: []const EventWithId, │ │533 + is_local: bool, │ │534 ) !void { │ │535 + // a branch that only exists on a remote continues from the remote tip, │ │536 + // so a cloned repo's local branch stays connected to the server history │ │537 + const first_parent_oids: ?[1][hash.hexLen(repo_opts.hash)]u8 = blk: { │ │538 + if (try repo.readRef(io, ref) != null) break :blk null; │ │539 + var remotes = try repo.listRemotes(io, allocator); │ │540 + defer remotes.deinit(); │ │541 + for (remotes.sections.keys()) |remote_name| { │ │542 + const remote_ref: rf.Ref = .{ .kind = .{ .remote = remote_name }, .name = ref.name }; │ │543 + if (try repo.readRef(io, remote_ref)) |oid| break :blk .{oid}; │ │544 + } │ │545 + break :blk null; │ │546 + }; │ │547 + var parent_oids: ?[]const [hash.hexLen(repo_opts.hash)]u8 = if (first_parent_oids) |*oids| oids else null;│ │548 + │ │549 var json: std.Io.Writer.Allocating = .init(allocator); │ │550 defer json.deinit(); │ │551 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │552 for (events) |event| { │ │553 json.clearRetainingCapacity(); │ │554 try std.json.Stringify.value(event, .{}, &json.writer); │ │536 - _ = try repo.commitAtRef(io, allocator, .{ .message = json.written(), .timestamp = event.timestamp }, null, ref); │ │555 + _ = try repo.commitAtRef(io, allocator, .{ .author = "haxy <user@haxy>", .message = json.written(), .timestamp = event.timestamp, .parent_oids = parent_oids }, null, ref);│ │556 + // later events parent on the ref's new tip │ │557 + parent_oids = null; │ │558 } │ │559 │ │539 - try consume(repo_opts, io, allocator, repo, ref); │ │560 + if (is_local) { │ │561 + try syncLocalEvents(repo_kind, repo_opts, io, allocator, repo); │ │562 + } else switch (repo_kind) { │ │563 + .xit => try consume(repo_opts, io, allocator, repo, ref), │ │564 + // server repos are always xit │ │565 + .git => unreachable, │ │566 + } │ │567 } │ │568 │ │569 // build the key for SortedSets sorted by timestamp. the big-endian timestamp │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯