╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ web.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
428 }.lt); 429 430 // emit the overlay inputs FIRST so they precede any submit-button span 431 // in DOM order; with the submit button carrying tabindex="0", that puts 432 // it at the end of the natural tab order — username -> password -> button. 433 for (inputs.items) |entry| { 434 const inner_left = entry.x + 1; 435 const inner_top = entry.y + 1; 436 const inner_width = if (entry.width > 2) entry.width - 2 else 0; 437 438 try out.appendSlice(allocator, "<input type=\""); 439 try out.appendSlice(allocator, if (entry.is_password) "password" else "text"); 440 try out.appendSlice(allocator, "\" data-focus-id=\""); 441 var id_buf: [32]u8 = undefined; 442 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{entry.focus_id})); 443 if (entry.ti.options.name.len > 0) { 444 try out.appendSlice(allocator, "\" name=\""); 445 try appendEscapedHtml(allocator, &out, entry.ti.options.name); 446 } 447 // mark the wasm-focused element so JS can mirror it onto DOM focus 448 // — otherwise moving focus via arrow keys leaves the TUI border 449 // showing focus but the underlying <input> unfocused. 450 if (entry.focus_id == root_focus.grandchild_id) { 451 try out.appendSlice(allocator, "\" data-focused=\"true"); 452 } 453 try out.appendSlice(allocator, "\" value=\""); 454 var value_buf: std.ArrayList(u8) = .empty; 455 defer value_buf.deinit(allocator); 456 for (entry.ti.content.items) |cp| { 457 try value_buf.appendSlice(allocator, cp); 458 } 459 try appendEscapedHtml(allocator, &out, value_buf.items); 460 try out.appendSlice(allocator, "\" style=\"left:"); 461 var pos_buf: [64]u8 = undefined; 462 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 })); 463 try out.appendSlice(allocator, "\">"); 464 } 465 466 for (0..grid.size.height) |y| { 467 // wrap runs of cells belonging to a focusable widget in a span tagged 468 // with that widget's focus id. CSS paints them with a pointer cursor; 469 // JS reads the id on click and dispatches focus directly. 470 var current_id: ?usize = null; 471 for (0..grid.size.width) |x| { 472 const cell_id = cellFocusId(root_focus, x, y); 473 if (cell_id != current_id) { 474 if (current_id != null) try out.appendSlice(allocator, "</span>"); 475 if (cell_id) |id| { 476 const kind = if (root_focus.children.get(id)) |entry| entry.focus.kind else .container; 477 const focused_attr: []const u8 = if (id == root_focus.grandchild_id) " data-focused=\"true\"" else ""; 478 var buf: [256]u8 = undefined; 479 // tabindex="0" on submit buttons puts them in the browser's 480 // natural Tab order alongside the inputs above. 481 const tag = if (kind == .submit_button) 482 try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\" data-action=\"submit\" data-url=\"{s}\" tabindex=\"0\"{s}>", .{ id, submit_url, focused_attr }) 483 else 484 try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\"{s}>", .{ id, focused_attr }); 485 try out.appendSlice(allocator, tag); 486 } 487 current_id = cell_id; 488 } 489 const rune = grid.cells.items[try grid.cells.at(.{ y, x })].rune orelse " "; 490 try appendEscapedHtml(allocator, &out, rune); 491 } 492 if (current_id != null) try out.appendSlice(allocator, "</span>"); 493 try out.append(allocator, '\n'); 494 } 495 496 return try out.toOwnedSlice(allocator); 497 } 498 499 fn cellFocusId(focus: *Focus, x: usize, y: usize) ?usize { 500 var iter = focus.children.iterator(); 501 while (iter.next()) |entry| { 502 const child = entry.value_ptr.*; 503 if (!child.focus.focusable) continue; 504 const r = child.rect; 505 if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) { 506 return entry.key_ptr.*; 507 } 508 } 509 return null; 510 } 511 512 fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void { 513 for (input) |ch| { 514 switch (ch) { 515 '&' => try out.appendSlice(allocator, "&amp;"), 516 '<' => try out.appendSlice(allocator, "&lt;"), 517 '>' => try out.appendSlice(allocator, "&gt;"), 518 '"' => try out.appendSlice(allocator, "&quot;"), 519 '\'' => try out.appendSlice(allocator, "&#39;"), 520 else => try out.append(allocator, ch), 521 } 522 } 523 } 524 525 // --- helpers -------------------------------------------------------------- 526 527 fn openHaxyMoment( 528 comptime repo_opts: rp.RepoOpts(.xit), 529 repo: *rp.Repo(.xit, repo_opts), 530 ) !rp.Repo(.xit, repo_opts).DB.HashMap(.read_only) { 531 const DB = rp.Repo(.xit, repo_opts).DB; 532 const history = try DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 533 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 534 const moment = try DB.HashMap(.read_only).init(moment_cursor); 535 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 536 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 537 _ = try last_object_id_cursor.readBytes(&last_object_id); 538 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 539 const haxy = try DB.ArrayList(.read_only).init(haxy_cursor); 540 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 541 const haxy_moments = try DB.HashMap(.read_only).init(haxy_moments_cursor); 542 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 543 return try DB.HashMap(.read_only).init(haxy_moment_cursor); 544 } 545 546 fn getCookieValue(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { 547 var iter = request.iterateHeaders(); 548 while (iter.next()) |header| { 549 if (!std.ascii.eqlIgnoreCase(header.name, "cookie")) continue; 550 var pairs = std.mem.splitScalar(u8, header.value, ';'); 551 while (pairs.next()) |pair| { 552 const trimmed = std.mem.trim(u8, pair, " \t"); 553 const eq = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue; 554 if (std.mem.eql(u8, trimmed[0..eq], name)) { 555 return trimmed[eq + 1 ..]; 556 } 557 } 558 } 559 return null; 560 } 561 562 fn decodeHexAlloc(allocator: std.mem.Allocator, hex: []const u8) ![]u8 { 563 if (hex.len % 2 != 0) return error.InvalidHex; 564 const bytes = try allocator.alloc(u8, hex.len / 2); 565 errdefer allocator.free(bytes); 566 _ = std.fmt.hexToBytes(bytes, hex) catch return error.InvalidHex; 567 return bytes; 568 } 569 570 fn parseFormField(allocator: std.mem.Allocator, body: []const u8, key: []const u8) !?[]u8 { 571 var iter = std.mem.splitScalar(u8, body, '&'); 572 while (iter.next()) |pair| { 573 const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue; 574 if (std.mem.eql(u8, pair[0..eq], key)) { 575 return try decodeFormValue(allocator, pair[eq + 1 ..]); 576 } 577 } 578 return null; 579 } 580 581 fn decodeFormValue(allocator: std.mem.Allocator, encoded: []const u8) ![]u8 { 582 var out: std.ArrayList(u8) = .empty; 583 errdefer out.deinit(allocator); 584 var i: usize = 0; 585 while (i < encoded.len) : (i += 1) { 586 switch (encoded[i]) { 587 '+' => try out.append(allocator, ' '), 588 '%' => { 589 if (i + 2 >= encoded.len) return error.InvalidPercentEncoding; 590 const hi = std.fmt.charToDigit(encoded[i + 1], 16) catch return error.InvalidPercentEncoding; 591 const lo = std.fmt.charToDigit(encoded[i + 2], 16) catch return error.InvalidPercentEncoding; 592 try out.append(allocator, hi * 16 + lo); 593 i += 2; 594 }, 595 else => try out.append(allocator, encoded[i]), 596 } 597 } 598 return try out.toOwnedSlice(allocator); 599 } 600