╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ web.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
420 var submit: ?SubmitButton = null; 421 422 var iter = root_focus.children.iterator(); 423 while (iter.next()) |entry| { 424 const child = entry.value_ptr.*; 425 switch (child.focus.kind) { 426 .text_input, .text_input_password => { 427 try inputs.append(allocator, .{ 428 .focus_id = entry.key_ptr.*, 429 .x = child.rect.x, 430 .y = child.rect.y, 431 .width = child.rect.size.width, 432 .is_password = child.focus.kind == .text_input_password, 433 .ti = @fieldParentPtr("focus", child.focus), 434 }); 435 }, 436 .submit_button => { 437 submit = .{ 438 .focus_id = entry.key_ptr.*, 439 .x = child.rect.x, 440 .y = child.rect.y, 441 .width = child.rect.size.width, 442 .height = child.rect.size.height, 443 }; 444 }, 445 else => {}, 446 } 447 } 448 449 if (submit == null and inputs.items.len == 0) return try out.toOwnedSlice(allocator); 450 451 // top-to-bottom, left-to-right order so the natural tab cycle matches 452 // visual layout (username -> password -> submit button). 453 std.mem.sort(InputEntry, inputs.items, {}, struct { 454 fn lt(_: void, a: InputEntry, b: InputEntry) bool { 455 if (a.y != b.y) return a.y < b.y; 456 return a.x < b.x; 457 } 458 }.lt); 459 460 const action_url: []const u8 = if (session.data.user_id != null) "/logout" else "/login"; 461 462 try out.appendSlice(allocator, "<form action=\""); 463 try out.appendSlice(allocator, action_url); 464 try out.appendSlice(allocator, "\" method=\"post\">"); 465 466 for (inputs.items) |entry| { 467 const inner_left = entry.x + 1; 468 const inner_top = entry.y + 1; 469 const inner_width = if (entry.width > 2) entry.width - 2 else 0; 470 471 try out.appendSlice(allocator, "<input type=\""); 472 try out.appendSlice(allocator, if (entry.is_password) "password" else "text"); 473 try out.appendSlice(allocator, "\" data-focus-id=\""); 474 var id_buf: [32]u8 = undefined; 475 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{entry.focus_id})); 476 if (entry.ti.options.name.len > 0) { 477 try out.appendSlice(allocator, "\" name=\""); 478 try appendEscapedHtml(allocator, &out, entry.ti.options.name); 479 } 480 // intentionally no `value` attribute: the browser tracks user input 481 // natively, and including the wasm-side value here would make the 482 // overlay HTML differ on every keystroke — that would trip the diff 483 // in _setOverlay and rebuild the <input>, eating the user's caret. 484 try out.appendSlice(allocator, "\" style=\"left:"); 485 var pos_buf: [64]u8 = undefined; 486 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 })); 487 try out.appendSlice(allocator, "\">"); 488 } 489 490 if (submit) |btn| { 491 // inside the dashed border the TUI cells draw around the button. 492 const inner_left = btn.x + 1; 493 const inner_top = btn.y + 1; 494 const inner_width = if (btn.width > 2) btn.width - 2 else 0; 495 const inner_height = if (btn.height > 2) btn.height - 2 else 0; 496 try out.appendSlice(allocator, "<button type=\"submit\" data-focus-id=\""); 497 var id_buf: [32]u8 = undefined; 498 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{btn.focus_id})); 499 try out.appendSlice(allocator, "\" style=\"left:"); 500 var pos_buf: [128]u8 = undefined; 501 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 })); 502 try out.appendSlice(allocator, "\"></button>"); 503 } 504 505 try out.appendSlice(allocator, "</form>"); 506 507 return try out.toOwnedSlice(allocator); 508 } 509 510 fn cellFocusId(focus: *Focus, x: usize, y: usize) ?usize { 511 var iter = focus.children.iterator(); 512 while (iter.next()) |entry| { 513 const child = entry.value_ptr.*; 514 if (!child.focus.focusable) continue; 515 const r = child.rect; 516 if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) { 517 return entry.key_ptr.*; 518 } 519 } 520 return null; 521 } 522 523 fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void { 524 for (input) |ch| { 525 switch (ch) { 526 '&' => try out.appendSlice(allocator, "&amp;"), 527 '<' => try out.appendSlice(allocator, "&lt;"), 528 '>' => try out.appendSlice(allocator, "&gt;"), 529 '"' => try out.appendSlice(allocator, "&quot;"), 530 '\'' => try out.appendSlice(allocator, "&#39;"), 531 else => try out.append(allocator, ch), 532 } 533 } 534 } 535 536 // --- helpers -------------------------------------------------------------- 537 538 fn openHaxyMoment( 539 comptime repo_opts: rp.RepoOpts(.xit), 540 repo: *rp.Repo(.xit, repo_opts), 541 ) !rp.Repo(.xit, repo_opts).DB.HashMap(.read_only) { 542 const DB = rp.Repo(.xit, repo_opts).DB; 543 const history = try DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 544 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 545 const moment = try DB.HashMap(.read_only).init(moment_cursor); 546 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 547 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 548 _ = try last_object_id_cursor.readBytes(&last_object_id); 549 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 550 const haxy = try DB.ArrayList(.read_only).init(haxy_cursor); 551 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 552 const haxy_moments = try DB.HashMap(.read_only).init(haxy_moments_cursor); 553 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 554 return try DB.HashMap(.read_only).init(haxy_moment_cursor); 555 } 556 557 fn getCookieValue(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { 558 var iter = request.iterateHeaders(); 559 while (iter.next()) |header| { 560 if (!std.ascii.eqlIgnoreCase(header.name, "cookie")) continue; 561 var pairs = std.mem.splitScalar(u8, header.value, ';'); 562 while (pairs.next()) |pair| { 563 const trimmed = std.mem.trim(u8, pair, " \t"); 564 const eq = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue; 565 if (std.mem.eql(u8, trimmed[0..eq], name)) { 566 return trimmed[eq + 1 ..]; 567 } 568 } 569 } 570 return null; 571 } 572 573 fn decodeHexAlloc(allocator: std.mem.Allocator, hex: []const u8) ![]u8 { 574 if (hex.len % 2 != 0) return error.InvalidHex; 575 const bytes = try allocator.alloc(u8, hex.len / 2); 576 errdefer allocator.free(bytes); 577 _ = std.fmt.hexToBytes(bytes, hex) catch return error.InvalidHex; 578 return bytes; 579 } 580 581 fn parseFormField(allocator: std.mem.Allocator, body: []const u8, key: []const u8) !?[]u8 { 582 var iter = std.mem.splitScalar(u8, body, '&'); 583 while (iter.next()) |pair| { 584 const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue; 585 if (std.mem.eql(u8, pair[0..eq], key)) { 586 return try decodeFormValue(allocator, pair[eq + 1 ..]); 587 } 588 } 589 return null; 590 } 591 592 fn decodeFormValue(allocator: std.mem.Allocator, encoded: []const u8) ![]u8 { 593 var out: std.ArrayList(u8) = .empty; 594 errdefer out.deinit(allocator); 595 var i: usize = 0; 596 while (i < encoded.len) : (i += 1) { 597 switch (encoded[i]) { 598 '+' => try out.append(allocator, ' '), 599 '%' => { 600 if (i + 2 >= encoded.len) return error.InvalidPercentEncoding; 601 const hi = std.fmt.charToDigit(encoded[i + 1], 16) catch return error.InvalidPercentEncoding; 602 const lo = std.fmt.charToDigit(encoded[i + 2], 16) catch return error.InvalidPercentEncoding; 603 try out.append(allocator, hi * 16 + lo); 604 i += 2; 605 }, 606 else => try out.append(allocator, encoded[i]), 607 } 608 } 609 return try out.toOwnedSlice(allocator); 610 } 611