╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ web.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
472 try appendEscapedHtml(allocator, &out, action_url); 473 try out.appendSlice(allocator, "\" method=\"post\">"); 474 475 // form.focus.children was populated by addChild, which flattens the 476 // whole subtree into one map — so descendants (inputs + the submit 477 // button) all appear here. positions on form.focus are relative to 478 // the form, so we look up absolute rects via root_focus for layout. 479 var inner_iter = child.focus.children.iterator(); 480 while (inner_iter.next()) |inner_entry| { 481 const inner_id = inner_entry.key_ptr.*; 482 const root_child = root_focus.children.get(inner_id) orelse continue; 483 const r = root_child.rect; 484 switch (root_child.focus.kind) { 485 .text_input, .text_input_password => { 486 const ti: *wgt.TextInput(ui.Widget) = @fieldParentPtr("focus", root_child.focus); 487 const inner_left = r.x + 1; 488 const inner_top = r.y + 1; 489 const inner_width = if (r.size.width > 2) r.size.width - 2 else 0; 490 491 try out.appendSlice(allocator, "<input type=\""); 492 try out.appendSlice(allocator, if (root_child.focus.kind == .text_input_password) "password" else "text"); 493 try out.appendSlice(allocator, "\" data-focus-id=\""); 494 var id_buf: [32]u8 = undefined; 495 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{inner_id})); 496 if (ti.options.name.len > 0) { 497 try out.appendSlice(allocator, "\" name=\""); 498 try appendEscapedHtml(allocator, &out, ti.options.name); 499 } 500 // intentionally no `value` attribute: the browser tracks user input 501 // natively, and including the wasm-side value here would make the 502 // overlay HTML differ on every keystroke — that would trip the diff 503 // in _setOverlay and rebuild the <input>, eating the user's caret. 504 try out.appendSlice(allocator, "\" style=\"left:"); 505 var pos_buf: [64]u8 = undefined; 506 try out.appendSlice(allocator, try std.fmt.bufPrint(&pos_buf, "{d}ch;top:{d}em;width:{d}ch;height:1em", .{ inner_left, inner_top, inner_width })); 507 try out.appendSlice(allocator, "\">"); 508 }, 509 .custom => |custom| { 510 if (std.mem.eql(u8, "submit", custom)) { 511 // inside the dashed border the TUI cells draw around the button. 512 const inner_left = r.x + 1; 513 const inner_top = r.y + 1; 514 const inner_width = if (r.size.width > 2) r.size.width - 2 else 0; 515 const inner_height = if (r.size.height > 2) r.size.height - 2 else 0; 516 try out.appendSlice(allocator, "<button type=\"submit\" data-focus-id=\""); 517 var id_buf: [32]u8 = undefined; 518 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{inner_id})); 519 try out.appendSlice(allocator, "\" style=\"left:"); 520 var pos_buf: [128]u8 = undefined; 521 try out.appendSlice(allocator, try std.fmt.bufPrint(&pos_buf, "{d}ch;top:{d}em;width:{d}ch;height:{d}em", .{ inner_left, inner_top, inner_width, inner_height })); 522 try out.appendSlice(allocator, "\"></button>"); 523 } 524 }, 525 else => {}, 526 } 527 } 528 529 try out.appendSlice(allocator, "</form>"); 530 } 531 532 return try out.toOwnedSlice(allocator); 533 } 534 535 fn cellFocusId(focus: *Focus, x: usize, y: usize) ?usize { 536 var iter = focus.children.iterator(); 537 while (iter.next()) |entry| { 538 const child = entry.value_ptr.*; 539 if (!child.focus.focusable) continue; 540 const r = child.rect; 541 if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) { 542 return entry.key_ptr.*; 543 } 544 } 545 return null; 546 } 547 548 fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void { 549 for (input) |ch| { 550 switch (ch) { 551 '&' => try out.appendSlice(allocator, "&amp;"), 552 '<' => try out.appendSlice(allocator, "&lt;"), 553 '>' => try out.appendSlice(allocator, "&gt;"), 554 '"' => try out.appendSlice(allocator, "&quot;"), 555 '\'' => try out.appendSlice(allocator, "&#39;"), 556 else => try out.append(allocator, ch), 557 } 558 } 559 } 560 561 // --- helpers -------------------------------------------------------------- 562 563 fn openHaxyMoment( 564 comptime repo_opts: rp.RepoOpts(.xit), 565 repo: *rp.Repo(.xit, repo_opts), 566 ) !rp.Repo(.xit, repo_opts).DB.HashMap(.read_only) { 567 const DB = rp.Repo(.xit, repo_opts).DB; 568 const history = try DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 569 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 570 const moment = try DB.HashMap(.read_only).init(moment_cursor); 571 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 572 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 573 _ = try last_object_id_cursor.readBytes(&last_object_id); 574 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 575 const haxy = try DB.ArrayList(.read_only).init(haxy_cursor); 576 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 577 const haxy_moments = try DB.HashMap(.read_only).init(haxy_moments_cursor); 578 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 579 return try DB.HashMap(.read_only).init(haxy_moment_cursor); 580 } 581 582 fn getCookieValue(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { 583 var iter = request.iterateHeaders(); 584 while (iter.next()) |header| { 585 if (!std.ascii.eqlIgnoreCase(header.name, "cookie")) continue; 586 var pairs = std.mem.splitScalar(u8, header.value, ';'); 587 while (pairs.next()) |pair| { 588 const trimmed = std.mem.trim(u8, pair, " \t"); 589 const eq = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue; 590 if (std.mem.eql(u8, trimmed[0..eq], name)) { 591 return trimmed[eq + 1 ..]; 592 } 593 } 594 } 595 return null; 596 } 597 598 fn parseFormField(allocator: std.mem.Allocator, body: []const u8, key: []const u8) !?[]u8 { 599 var iter = std.mem.splitScalar(u8, body, '&'); 600 while (iter.next()) |pair| { 601 const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue; 602 if (std.mem.eql(u8, pair[0..eq], key)) { 603 return try decodeFormValue(allocator, pair[eq + 1 ..]); 604 } 605 } 606 return null; 607 } 608 609 fn decodeFormValue(allocator: std.mem.Allocator, encoded: []const u8) ![]u8 { 610 var out: std.ArrayList(u8) = .empty; 611 errdefer out.deinit(allocator); 612 var i: usize = 0; 613 while (i < encoded.len) : (i += 1) { 614 switch (encoded[i]) { 615 '+' => try out.append(allocator, ' '), 616 '%' => { 617 if (i + 2 >= encoded.len) return error.InvalidPercentEncoding; 618 const hi = std.fmt.charToDigit(encoded[i + 1], 16) catch return error.InvalidPercentEncoding; 619 const lo = std.fmt.charToDigit(encoded[i + 2], 16) catch return error.InvalidPercentEncoding; 620 try out.append(allocator, hi * 16 + lo); 621 i += 2; 622 }, 623 else => try out.append(allocator, encoded[i]), 624 } 625 } 626 return try out.toOwnedSlice(allocator); 627 } 628