╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
407 const tz_p2 = "The tooltip on each entry repeats the same UTC value, so there is no way to see the local time without converting by hand."; 408 const tz_p3 = "A quick survey of other clients shows every one of them rendering local time by default."; 409 const tz_p4 = "Convert to local time and include the offset in tooltips."; 410 411 // the sync issue's description, in paragraphs, so each side of its 412 // seeded conflict can rework a different one 413 const sync_p1 = "The sync engine only has unit tests with mocked transports."; 414 const sync_p2 = "Failures seen in production involve reconnects and partial writes, which the mocks can't reproduce."; 415 const sync_p3 = "Add end-to-end tests that run two instances against a real local server."; 416 417 // seed issues so every repo's issue tracker has content 418 const issue_data = [_]struct { 419 title: []const u8, 420 description: []const u8, 421 tags: []const u8, 422 status: evt.Issue.Status = .open, 423 }{ 424 .{ 425 .title = "Crash on startup when config file is missing", 426 .description = "Running the program without a config file present dereferences a null pointer and segfaults. Fall back to the built-in defaults and log a warning instead.", 427 .tags = "bug priority-high crash", 428 }, 429 .{ 430 .title = "Support dark mode in the settings panel", 431 .description = "The settings panel is always rendered with the light palette even when the rest of the app is in dark mode. Read the active theme and pick colors accordingly.", 432 .tags = "enhancement ui theme", 433 }, 434 .{ 435 .title = "Memory leak when reconnecting after network failure", 436 .description = "Each reconnect allocates a new connection state without freeing the previous one. After a flaky network session, memory usage grows by several megabytes per hour.", 437 .tags = "bug memory networking", 438 .status = .closed, 439 }, 440 .{ 441 .title = "Document the plugin API", 442 .description = "The plugin interface has no documentation beyond the header comments. Add a guide covering the lifecycle hooks, the event callbacks, and a minimal working plugin.", 443 .tags = "documentation plugins", 444 }, 445 .{ 446 .title = "Slow file indexing on large directories", 447 .description = "Indexing a directory with more than 100k files takes several minutes because every entry is stat'd twice. Cache the first stat result and batch the reads.", 448 .tags = "performance indexing", 449 }, 450 .{ 451 .title = "Add keyboard shortcut for quick search", 452 .description = "Opening the search box currently requires clicking the toolbar icon. Bind a shortcut and show it in the tooltip so keyboard users can search without the mouse.", 453 .tags = "enhancement ux keyboard", 454 }, 455 .{ 456 .title = "Unicode filenames are garbled in the export dialog", 457 .description = "Filenames containing non-ASCII characters display as replacement characters in the export dialog. The dialog decodes the path as Latin-1 instead of UTF-8.", 458 .tags = "bug unicode i18n", 459 }, 460 .{ 461 .title = "Flaky test: integration suite times out on CI", 462 .description = "The integration suite intermittently exceeds the CI time limit because the server fixture waits for a fixed 30 seconds. Poll for readiness instead of sleeping.", 463 .tags = "bug testing ci", 464 }, 465 .{ 466 .title = "Upgrade bundled zlib to the latest release", 467 .description = "The vendored zlib is two major releases behind and misses several upstream fixes. Update the bundled copy and re-run the compression benchmarks.", 468 .tags = "dependencies maintenance", 469 }, 470 .{ 471 .title = "Progress bar overshoots 100% during resumed downloads", 472 .description = "Resuming a partial download counts the already-downloaded bytes twice, so the progress bar reads up to 150%. Subtract the resume offset from the total.", 473 .tags = "bug ui downloads", 474 .status = .closed, 475 }, 476 .{ 477 .title = "Config parser rejects trailing commas", 478 .description = "A trailing comma after the last entry in a config block is reported as a syntax error. Most editors add one automatically, so accept it.", 479 .tags = "bug config parser", 480 }, 481 .{ 482 .title = "Add a --json flag to the status command", 483 .description = "Scripts currently scrape the human-readable status output, which breaks whenever the format changes. Emit a stable machine-readable JSON form behind a flag.", 484 .tags = "enhancement cli", 485 }, 486 .{ 487 .title = "Race condition between autosave and manual save", 488 .description = "Saving manually while an autosave is in flight can interleave the two writes and corrupt the file. Serialize saves through a single queue.", 489 .tags = "bug priority-high data-loss", 490 }, 491 .{ 492 .title = "Reduce binary size of release builds", 493 .description = "The release binary has grown past 40 MB, mostly from debug info and an unused bundled font. Strip symbols and drop the font from the default build.", 494 .tags = "performance build", 495 }, 496 .{ 497 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 498 .description = "Moving the cursor across a toolbar hides and re-shows the tooltip for every button. Keep the tooltip open with a short grace period between neighbors.", 499 .tags = "bug ui polish", 500 }, 501 .{ 502 .title = "Support environment variable expansion in config paths", 503 .description = "Paths in the config file are taken literally, so shared configs can't refer to the home directory portably. Expand environment variables when loading.", 504 .tags = "enhancement config", 505 }, 506 .{ 507 .title = "Log rotation deletes the newest file instead of the oldest", 508 .description = "When the log directory hits its size cap, the rotation logic sorts by name rather than mtime and removes the most recent log. Sort by modification time.", 509 .tags = "bug logging", 510 .status = .closed, 511 }, 512 .{ 513 .title = "Add man pages for all subcommands", 514 .description = "Only the top-level command has a man page. Generate one per subcommand from the existing help text as part of the release build.", 515 .tags = "documentation cli", 516 }, 517 .{ 518 .title = "High CPU usage while idle in the background", 519 .description = "The main loop polls for file changes every 10 ms even when no window is visible. Switch to native file watching and idle at zero CPU.", 520 .tags = "performance priority-high", 521 }, 522 .{ 523 .title = "Paste from clipboard drops the final newline", 524 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content exactly.", 525 .tags = "bug editor clipboard", 526 }, 527 .{ 528 .title = "Improve error message for expired credentials", 529 .description = "An expired token currently surfaces as a bare 401 with no guidance. Detect the expiry case and tell the user how to re-authenticate.", 530 .tags = "enhancement ux auth", 531 }, 532 .{ 533 .title = "Crash when window is resized during startup animation", 534 .description = "Resizing the window while the splash animation is running dereferences a freed layout node. Cancel the animation before rebuilding the layout.", 535 .tags = "bug crash ui", 536 }, 537 .{ 538 .title = "Add integration tests for the sync engine", 539 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ sync_p3, 540 .tags = "testing sync", 541 }, 542 .{ 543 .title = "Timestamps display in UTC instead of local time", 544 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p3 ++ "\n\n" ++ tz_p4, 545 .tags = "bug i18n time", 546 }, 547 .{ 548 .title = "Deprecate the legacy plugin format", 549 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 550 .tags = "maintenance plugins", 551 }, 552 }; 553 554 var issue_events: [issue_data.len]evt.EventWithId = undefined; 555 for (issue_data, 0..) |issue, i| { 556 // the fifth-newest issue's description runs past what the detail 557 // pane shows, so it shows the truncated description and its 558 // link. 559 const description = if (i == issue_data.len - 5) desc: { 560 var desc_writer = std.Io.Writer.Allocating.init(allocator); 561 defer desc_writer.deinit(); 562 try desc_writer.writer.print("{s}", .{issue.description}); 563 var line: usize = 0; 564 while (desc_writer.written().len <= ui.Repo.Issues.max_description_size) : (line += 1) { 565 try desc_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 566 } 567 break :desc try arena.allocator().dupe(u8, desc_writer.written()); 568 } else issue.description; 569 issue_events[i] = .{ 570 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 571 // stepped timestamps so the issues list in a stable order 572 .timestamp = @intCast(i + 1), 573 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 574 .event = .{ 575 .issue = .{ 576 .title = issue.title, 577 .description = description, 578 .tags = issue.tags, 579 .status = issue.status, 580 }, 581 }, 582 }; 583 } 584 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 585 // the tip is the branch point for the conflicting edits below 586 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 587 588 // two divergent edits per conflicted issue: ours on the events 589 // branch, theirs on a temp branch rooted at the seed tip, then a 590 // merge. the 4th-newest issue conflicts on title and tags; the 591 // 3rd-newest on its description with every hunk auto-resolving 592 // (each side reworks a different paragraph); the 2nd-newest on 593 // its description with a deletion conflict (ours removes a 594 // paragraph theirs rewords) and an insertion conflict (each side 595 // appends a different closing paragraph). 596 { 597 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 598 const title_issue = issue_data[issue_data.len - 4]; 599 const sync_issue = issue_data[issue_data.len - 3]; 600 const desc_issue = issue_data[issue_data.len - 2]; 601 602 const ours = [_]evt.EventWithId{ .{ 603 .id = issue_events[issue_data.len - 4].id, 604 .timestamp = 100, 605 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 606 .event = .{ .issue = .{ 607 .title = "Crash when resizing the window during the splash animation", 608 .description = title_issue.description, 609 .tags = "bug crash ui priority-high", 610 } }, 611 }, .{ 612 .id = issue_events[issue_data.len - 3].id, 613 .timestamp = 101, 614 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 615 .event = .{ .issue = .{ 616 .title = sync_issue.title, 617 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 618 sync_p2 ++ "\n\n" ++ sync_p3, 619 .tags = sync_issue.tags, 620 } }, 621 }, .{ 622 .id = issue_events[issue_data.len - 2].id, 623 .timestamp = 102, 624 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 625 .event = .{ .issue = .{ 626 .title = desc_issue.title, 627 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 628 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 629 .tags = desc_issue.tags, 630 } }, 631 } }; 632 const theirs = [_]evt.EventWithId{ .{ 633 .id = issue_events[issue_data.len - 4].id, 634 .timestamp = 103, 635 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 636 .event = .{ .issue = .{ 637 .title = "Segfault on early window resize", 638 .description = title_issue.description, 639 .tags = "bug crash rendering", 640 } }, 641 }, .{ 642 .id = issue_events[issue_data.len - 3].id, 643 .timestamp = 104, 644 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 645 .event = .{ .issue = .{ 646 .title = sync_issue.title, 647 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 648 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 649 .tags = sync_issue.tags, 650 } }, 651 }, .{ 652 .id = issue_events[issue_data.len - 2].id, 653 .timestamp = 105, 654 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 655 .event = .{ .issue = .{ 656 .title = desc_issue.title, 657 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 658 "Most other clients already render local time by default, which makes our UTC output stand out as a bug." ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 659 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 660 .tags = desc_issue.tags, 661 } }, 662 } }; 663 664 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 665 666 // consume can't root a new branch, so theirs commits by hand 667 var json: std.Io.Writer.Allocating = .init(allocator); 668 defer json.deinit(); 669 for (theirs, 0..) |event, i| { 670 json.clearRetainingCapacity(); 671 try std.json.Stringify.value(event, .{}, &json.writer); 672 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 673 defer allocator.free(author); 674 _ = try template_repo.commitAtRef(io, allocator, .{ 675 .author = author, 676 .message = json.written(), 677 .timestamp = event.timestamp, 678 // root the branch at the shared seed tip 679 .parent_oids = if (i == 0) &.{seed_tip} else null, 680 }, null, other_ref); 681 } 682 683 { 684 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 685 defer to_events.deinit(); 686 } 687 { 688 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 689 defer merge.deinit(); 690 if (merge.result != .success) return error.MergeFailed; 691 } 692 { 693 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 694 defer to_master.deinit(); 695 } 696 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 697 } 698 699 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 700 701 // seed a small comment tree on the newest issue 702 var comment_ids: [5][evt.event_id_size]u8 = undefined; 703 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 704 705 const comment_bodies = [_][]const u8{ 706 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 707 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 708 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 709 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 710 "I'll add release-note text once the warning format is settled.", 711 }; 712 713 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 714 for (&comment_events, 0..) |*event, i| { 715 event.* = .{ 716 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 717 .timestamp = @intCast(200 + i), 718 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 719 .event = .{ .comment = .{ 720 .thread_id = issue_events[issue_events.len - 1].id, 721 .parent_id = switch (i) { 722 0, 1 => issue_events[issue_events.len - 1].id, 723 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 724 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 725 else => unreachable, 726 }, 727 .body = comment_bodies[i], 728 } }, 729 }; 730 } 731 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 732 } 733 734 // copy the template to each repo's on-disk location, named by its 735 // hex-encoded event id. the template repo is deinitialized above, so its 736 // db file is fully written before we copy it. 737 { 738 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 739 defer template_dir.close(io); 740 741 for (repo_event_ids) |id_bytes| { 742 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 743 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 744 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 745 defer dest_dir.close(io); 746 try copyDir(io, template_dir, dest_dir); 747 } 748 } 749 750 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 751 }; 752 session.is_terminal = true; 753 754 // start the server 755 756 // let the native TUI's page builders open the on-disk repos for the file tree 757 session.io = io; 758 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 759 760 // leave a one-shot session for the first browser to hit the web ui, so it 761 // starts logged in as admin 762 { 763 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 764 defer data_dir.close(io); 765 const store = try hx.web.SessionStore.init(io, data_dir); 766 defer store.deinit(); 767 const token = try store.create(&admin_user_id); 768 try store.offerAutoLogin(&token); 769 } 770 771 if (cli) { 772 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 773 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 774 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 775 776 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 777 defer allocator.free(key_path); 778 779 const Runnable = struct { 780 io: std.Io, 781 key_path: []const u8, 782 783 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 784 _ = web_port; 785 _ = http_port; 786 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 787 std.debug.print( 788 \\ 789 \\connect to the TUI with: 790 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 791 \\ 792 \\create a git repo and push over SSH: 793 \\ mkdir -p temp-try/client/test 794 \\ cd temp-try/client/test 795 \\ git init 796 \\ echo "hello" > hello.txt 797 \\ git add hello.txt 798 \\ git commit -m "let there be light" 799 \\ GIT_SSH_COMMAND='ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i {s}' git push localhost:admin/test HEAD:master 800 \\ 801 \\to quit, press enter. 802 \\ 803 , .{ ssh_port, ssh_port, self.key_path }); 804 // portable stdin read via std.Io — equivalent to the 805 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 806 // on windows. 807 var buf: [1]u8 = undefined; 808 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 809 _ = stdin_reader.interface.takeByte() catch {}; 810 } 811 }; 812 813 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 814 } else { 815 var null_writer = std.Io.Writer.Discarding.init(&.{}); 816 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 817 818 const Runnable = struct { 819 io: std.Io, 820 allocator: std.mem.Allocator, 821 session: *ui.Session, 822 repo: *Repo, 823 824 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 825 // launch the TUI; the footer points at whatever port was bound 826 self.session.web_port = web_port; 827 self.session.data.clone_http_port = http_port; 828 self.session.data.clone_ssh_port = ssh_port; 829 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 830 } 831 }; 832 833 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 834 } 835 } 836 837 // words cycled through to pad generated lines into prose-like content 838 const scatter_words = [_][]const u8{ 839 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 840 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 841 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 842 "scatter", "gather", "encode", "decode", "render", "commit", 843 }; 844 845 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 846 // index `fi` shift which lines change and the words used, so each (c, fi) 847 // yields a distinct multi-hunk diff against the previous revision. untouched 848 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 849 // small hunks rather than a full rewrite. 850 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 851 const file = try repo_dir.createFile(io, path, .{}); 852 defer file.close(io); 853 var writer = std.Io.Writer.Allocating.init(allocator); 854 defer writer.deinit(); 855 for (0..40) |line| { 856 const start = writer.written().len; 857 if ((line + fi + c) % 8 == 0) { 858 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 859 try writer.writer.print("rev {d} {s}", .{ c, tag }); 860 } 861 const target = 40 + (line * 17 + fi * 23) % 70; 862 var w = line + fi; 863 while (true) : (w += 1) { 864 const word = scatter_words[w % scatter_words.len]; 865 const len = writer.written().len - start; 866 if (len >= target or len + 1 + word.len > 120) break; 867 if (len == 0) 868 try writer.writer.print("{s}", .{word}) 869 else 870 try writer.writer.print(" {s}", .{word}); 871 } 872 try writer.writer.writeByte('\n'); 873 } 874 try file.writeStreamingAll(io, writer.written()); 875 } 876 877 // tag the current HEAD as the next sequential version: v1, v2, and so on 878 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 879 var buf: [16]u8 = undefined; 880 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 881 _ = try repo.addTag(io, allocator, .{ .name = name }); 882 n.* += 1; 883 } 884 885 // recursively copy the contents of src_dir into dest_dir 886 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 887 var iter = src_dir.iterate(); 888 while (try iter.next(io)) |entry| { 889 switch (entry.kind) { 890 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 891 .directory => { 892 try dest_dir.createDirPath(io, entry.name); 893 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 894 defer dest_entry_dir.close(io); 895 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 896 defer src_entry_dir.close(io); 897 try copyDir(io, src_entry_dir, dest_entry_dir); 898 }, 899 else => {}, 900 } 901 } 902 } 903