╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ web.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
510 const r = root_child.rect; 511 switch (root_child.focus.kind) { 512 .text_input, .text_input_password => { 513 const ti: *wgt.TextInput(ui.Widget) = @fieldParentPtr("focus", root_child.focus); 514 const inner_left = r.x + 1; 515 const inner_top = r.y + 1; 516 const inner_width = if (r.size.width > 2) r.size.width - 2 else 0; 517 518 try out.appendSlice(allocator, "<input type=\""); 519 try out.appendSlice(allocator, if (root_child.focus.kind == .text_input_password) "password" else "text"); 520 try out.appendSlice(allocator, "\" data-focus-id=\""); 521 var id_buf: [32]u8 = undefined; 522 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{inner_id})); 523 if (ti.options.name.len > 0) { 524 try out.appendSlice(allocator, "\" name=\""); 525 try appendEscapedHtml(allocator, &out, ti.options.name); 526 } 527 // intentionally no `value` attribute: the browser tracks user input 528 // natively, and including the wasm-side value here would make the 529 // overlay HTML differ on every keystroke — that would trip the diff 530 // in _setOverlay and rebuild the <input>, eating the user's caret. 531 try out.appendSlice(allocator, "\" style=\"left:"); 532 var pos_buf: [64]u8 = undefined; 533 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 })); 534 try out.appendSlice(allocator, "\">"); 535 }, 536 .custom => |custom| { 537 if (std.mem.eql(u8, "submit", custom)) { 538 // inside the dashed border the TUI cells draw around the button. 539 const inner_left = r.x + 1; 540 const inner_top = r.y + 1; 541 const inner_width = if (r.size.width > 2) r.size.width - 2 else 0; 542 const inner_height = if (r.size.height > 2) r.size.height - 2 else 0; 543 try out.appendSlice(allocator, "<button type=\"submit\" data-focus-id=\""); 544 var id_buf: [32]u8 = undefined; 545 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{inner_id})); 546 try out.appendSlice(allocator, "\" style=\"left:"); 547 var pos_buf: [128]u8 = undefined; 548 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 })); 549 try out.appendSlice(allocator, "\"></button>"); 550 } 551 }, 552 else => {}, 553 } 554 } 555 556 try out.appendSlice(allocator, "</form>"); 557 } 558 559 return try out.toOwnedSlice(allocator); 560 } 561 562 fn colorEql(a: ?Grid.Color, b: ?Grid.Color) bool { 563 if (a) |av| return if (b) |bv| av.eql(bv) else false; 564 return b == null; 565 } 566 567 // builds an HTML ` style="color:#rrggbb;background-color:#rrggbb"` attribute 568 // from a cell's fg/bg into `buf`, omitting whichever color is unset. returns an 569 // empty slice when neither is set. 570 fn styleAttr(buf: []u8, fg: ?Grid.Color, bg: ?Grid.Color) []const u8 { 571 if (fg == null and bg == null) return buf[0..0]; 572 const prefix = " style=\""; 573 @memcpy(buf[0..prefix.len], prefix); 574 var i: usize = prefix.len; 575 if (fg) |c| { 576 const s = std.fmt.bufPrint(buf[i..], "color:#{x:0>2}{x:0>2}{x:0>2};", .{ c.r, c.g, c.b }) catch return buf[0..0]; 577 i += s.len; 578 } 579 if (bg) |c| { 580 const s = std.fmt.bufPrint(buf[i..], "background-color:#{x:0>2}{x:0>2}{x:0>2};", .{ c.r, c.g, c.b }) catch return buf[0..0]; 581 i += s.len; 582 } 583 buf[i] = '"'; 584 i += 1; 585 return buf[0..i]; 586 } 587 588 fn cellFocusId(focus: *Focus, x: usize, y: usize) ?usize { 589 var iter = focus.children.iterator(); 590 while (iter.next()) |entry| { 591 const child = entry.value_ptr.*; 592 if (!child.focus.focusable) continue; 593 const r = child.rect; 594 if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) { 595 return entry.key_ptr.*; 596 } 597 } 598 return null; 599 } 600 601 fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void { 602 for (input) |ch| { 603 switch (ch) { 604 '&' => try out.appendSlice(allocator, "&amp;"), 605 '<' => try out.appendSlice(allocator, "&lt;"), 606 '>' => try out.appendSlice(allocator, "&gt;"), 607 '"' => try out.appendSlice(allocator, "&quot;"), 608 '\'' => try out.appendSlice(allocator, "&#39;"), 609 else => try out.append(allocator, ch), 610 } 611 } 612 } 613 614 // --- helpers -------------------------------------------------------------- 615 616 fn openHaxyMoment( 617 comptime repo_opts: rp.RepoOpts(.xit), 618 repo: *rp.Repo(.xit, repo_opts), 619 ) !rp.Repo(.xit, repo_opts).DB.HashMap(.read_only) { 620 const DB = rp.Repo(.xit, repo_opts).DB; 621 const history = try DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 622 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 623 const moment = try DB.HashMap(.read_only).init(moment_cursor); 624 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 625 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 626 _ = try last_object_id_cursor.readBytes(&last_object_id); 627 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 628 const haxy = try DB.ArrayList(.read_only).init(haxy_cursor); 629 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 630 const haxy_moments = try DB.HashMap(.read_only).init(haxy_moments_cursor); 631 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 632 return try DB.HashMap(.read_only).init(haxy_moment_cursor); 633 } 634 635 fn getCookieValue(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { 636 var iter = request.iterateHeaders(); 637 while (iter.next()) |header| { 638 if (!std.ascii.eqlIgnoreCase(header.name, "cookie")) continue; 639 var pairs = std.mem.splitScalar(u8, header.value, ';'); 640 while (pairs.next()) |pair| { 641 const trimmed = std.mem.trim(u8, pair, " \t"); 642 const eq = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue; 643 if (std.mem.eql(u8, trimmed[0..eq], name)) { 644 return trimmed[eq + 1 ..]; 645 } 646 } 647 } 648 return null; 649 } 650 651 fn parseFormField(allocator: std.mem.Allocator, body: []const u8, key: []const u8) !?[]u8 { 652 var iter = std.mem.splitScalar(u8, body, '&'); 653 while (iter.next()) |pair| { 654 const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue; 655 if (std.mem.eql(u8, pair[0..eq], key)) { 656 return try decodeFormValue(allocator, pair[eq + 1 ..]); 657 } 658 } 659 return null; 660 } 661 662 fn decodeFormValue(allocator: std.mem.Allocator, encoded: []const u8) ![]u8 { 663 var out: std.ArrayList(u8) = .empty; 664 errdefer out.deinit(allocator); 665 var i: usize = 0; 666 while (i < encoded.len) : (i += 1) { 667 switch (encoded[i]) { 668 '+' => try out.append(allocator, ' '), 669 '%' => { 670 if (i + 2 >= encoded.len) return error.InvalidPercentEncoding; 671 const hi = std.fmt.charToDigit(encoded[i + 1], 16) catch return error.InvalidPercentEncoding; 672 const lo = std.fmt.charToDigit(encoded[i + 2], 16) catch return error.InvalidPercentEncoding; 673 try out.append(allocator, hi * 16 + lo); 674 i += 2; 675 }, 676 else => try out.append(allocator, encoded[i]), 677 } 678 } 679 return try out.toOwnedSlice(allocator); 680 } 681