╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
354 var repo_dir = try cwd.openDir(io, template_path, .{}); 355 defer repo_dir.close(io); 356 try writeScatterFile(io, allocator, repo_dir, b.file, b.fi, b.rev); 357 } 358 try template_repo.add(io, allocator, &.{b.file}); 359 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); 360 try addNextTag(&template_repo, io, allocator, &tag_num); 361 362 { 363 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 364 defer to_master.deinit(); 365 } 366 } 367 368 // seed issues so every repo's issue tracker has content 369 const issue_data = [_]struct { 370 title: []const u8, 371 description: []const u8, 372 tags: []const u8, 373 status: evt.Issue.Status = .open, 374 }{ 375 .{ 376 .title = "Crash on startup when config file is missing", 377 .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.", 378 .tags = "bug,priority-high,crash", 379 }, 380 .{ 381 .title = "Support dark mode in the settings panel", 382 .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.", 383 .tags = "enhancement,ui,theme", 384 }, 385 .{ 386 .title = "Memory leak when reconnecting after network failure", 387 .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.", 388 .tags = "bug,memory,networking", 389 .status = .closed, 390 }, 391 .{ 392 .title = "Document the plugin API", 393 .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.", 394 .tags = "documentation,plugins", 395 }, 396 .{ 397 .title = "Slow file indexing on large directories", 398 .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.", 399 .tags = "performance,indexing", 400 }, 401 .{ 402 .title = "Add keyboard shortcut for quick search", 403 .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.", 404 .tags = "enhancement,ux,keyboard", 405 }, 406 .{ 407 .title = "Unicode filenames are garbled in the export dialog", 408 .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.", 409 .tags = "bug,unicode,i18n", 410 }, 411 .{ 412 .title = "Flaky test: integration suite times out on CI", 413 .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.", 414 .tags = "bug,testing,ci", 415 }, 416 .{ 417 .title = "Upgrade bundled zlib to the latest release", 418 .description = "The vendored zlib is two major releases behind and misses several upstream fixes. Update the bundled copy and re-run the compression benchmarks.", 419 .tags = "dependencies,maintenance", 420 }, 421 .{ 422 .title = "Progress bar overshoots 100% during resumed downloads", 423 .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.", 424 .tags = "bug,ui,downloads", 425 .status = .closed, 426 }, 427 .{ 428 .title = "Config parser rejects trailing commas", 429 .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.", 430 .tags = "bug,config,parser", 431 }, 432 .{ 433 .title = "Add a --json flag to the status command", 434 .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.", 435 .tags = "enhancement,cli", 436 }, 437 .{ 438 .title = "Race condition between autosave and manual save", 439 .description = "Saving manually while an autosave is in flight can interleave the two writes and corrupt the file. Serialize saves through a single queue.", 440 .tags = "bug,priority-high,data-loss", 441 }, 442 .{ 443 .title = "Reduce binary size of release builds", 444 .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.", 445 .tags = "performance,build", 446 }, 447 .{ 448 .title = "Tooltips flicker when the cursor moves between adjacent buttons", 449 .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.", 450 .tags = "bug,ui,polish", 451 }, 452 .{ 453 .title = "Support environment variable expansion in config paths", 454 .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.", 455 .tags = "enhancement,config", 456 }, 457 .{ 458 .title = "Log rotation deletes the newest file instead of the oldest", 459 .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.", 460 .tags = "bug,logging", 461 .status = .closed, 462 }, 463 .{ 464 .title = "Add man pages for all subcommands", 465 .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.", 466 .tags = "documentation,cli", 467 }, 468 .{ 469 .title = "High CPU usage while idle in the background", 470 .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.", 471 .tags = "performance,priority-high", 472 }, 473 .{ 474 .title = "Paste from clipboard drops the final newline", 475 .description = "Pasting text that ends with a newline silently trims it, which breaks pasted shell snippets. Preserve the clipboard content exactly.", 476 .tags = "bug,editor,clipboard", 477 }, 478 .{ 479 .title = "Improve error message for expired credentials", 480 .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.", 481 .tags = "enhancement,ux,auth", 482 }, 483 .{ 484 .title = "Crash when window is resized during startup animation", 485 .description = "Resizing the window while the splash animation is running dereferences a freed layout node. Cancel the animation before rebuilding the layout.", 486 .tags = "bug,crash,ui", 487 }, 488 .{ 489 .title = "Add integration tests for the sync engine", 490 .description = "The sync engine only has unit tests with mocked transports. Add end-to-end tests that run two instances against a real local server.", 491 .tags = "testing,sync", 492 }, 493 .{ 494 .title = "Timestamps display in UTC instead of local time", 495 .description = "All timestamps in the activity feed render in UTC regardless of the system timezone. Convert to local time and include the offset in tooltips.", 496 .tags = "bug,i18n,time", 497 }, 498 .{ 499 .title = "Deprecate the legacy plugin format", 500 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 501 .tags = "maintenance,plugins", 502 }, 503 }; 504 505 var issue_events: [issue_data.len]evt.EventWithId = undefined; 506 for (issue_data, 0..) |issue, i| { 507 issue_events[i] = .{ 508 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 509 // stepped timestamps so the issues list in a stable order 510 .timestamp = @intCast(i + 1), 511 .event = .{ 512 .issue = .{ 513 .title = issue.title, 514 .description = issue.description, 515 .tags = issue.tags, 516 .status = issue.status, 517 }, 518 }, 519 }; 520 } 521 try evt.commitAndConsume(.{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 522 } 523 524 // copy the template to each repo's on-disk location, named by its 525 // hex-encoded event id. the template repo is deinitialized above, so its 526 // db file is fully written before we copy it. 527 { 528 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 529 defer template_dir.close(io); 530 531 for (repo_event_ids) |id_bytes| { 532 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 533 const repo_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "server", "repos", &repo_id }); 534 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 535 defer dest_dir.close(io); 536 try copyDir(io, template_dir, dest_dir); 537 } 538 } 539 540 break :blk try ui.Session.init(&session_arena, &repo, .{}); 541 }; 542 session.is_terminal = true; 543 // try uses the default serve options below, so the footer's url points at 544 // the default web UI port. 545 session.web_port = try (srv.Options{}).wuiPort(); 546 547 // start the server 548 549 var cli = false; 550 551 var arg_it = try init.minimal.args.iterateAllocator(allocator); 552 defer arg_it.deinit(); 553 _ = arg_it.skip(); 554 while (arg_it.next()) |arg| { 555 if (std.mem.eql(u8, "--cli", arg)) { 556 cli = true; 557 } 558 } 559 560 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 561 defer allocator.free(server_path); 562 563 // let the native TUI's page builders open the on-disk repos for the file tree 564 session.io = io; 565 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 566 567 if (cli) { 568 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 569 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 570 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 571 572 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 573 defer allocator.free(key_path); 574 575 const Runnable = struct { 576 io: std.Io, 577 key_path: []const u8, 578 579 pub fn run(self: @This()) !void { 580 std.debug.print( 581 \\ 582 \\connect to the TUI with: 583 \\ ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 584 \\ 585 \\create a git repo and push over SSH: 586 \\ mkdir -p temp-try/client/test 587 \\ cd temp-try/client/test 588 \\ git init 589 \\ echo "hello" > hello.txt 590 \\ git add hello.txt 591 \\ git commit -m "let there be light" 592 \\ GIT_SSH_COMMAND='ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i {s}' git push localhost:admin/test HEAD:master 593 \\ 594 \\to quit, press enter. 595 \\ 596 , .{self.key_path}); 597 // portable stdin read via std.Io — equivalent to the 598 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 599 // on windows. 600 var buf: [1]u8 = undefined; 601 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 602 _ = stdin_reader.interface.takeByte() catch {}; 603 } 604 }; 605 606 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 607 .data_dir = server_path, 608 }, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 609 } else { 610 var null_writer = std.Io.Writer.Discarding.init(&.{}); 611 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 612 613 const Runnable = struct { 614 io: std.Io, 615 allocator: std.mem.Allocator, 616 session: *ui.Session, 617 repo: *Repo, 618 619 pub fn run(self: @This()) !void { 620 // launch the TUI 621 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 622 } 623 }; 624 625 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 626 .data_dir = server_path, 627 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 628 } 629 } 630 631 // words cycled through to pad generated lines into prose-like content 632 const scatter_words = [_][]const u8{ 633 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 634 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 635 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 636 "scatter", "gather", "encode", "decode", "render", "commit", 637 }; 638 639 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 640 // index `fi` shift which lines change and the words used, so each (c, fi) 641 // yields a distinct multi-hunk diff against the previous revision. untouched 642 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 643 // small hunks rather than a full rewrite. 644 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 645 const file = try repo_dir.createFile(io, path, .{}); 646 defer file.close(io); 647 var writer = std.Io.Writer.Allocating.init(allocator); 648 defer writer.deinit(); 649 for (0..40) |line| { 650 const start = writer.written().len; 651 if ((line + fi + c) % 8 == 0) { 652 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 653 try writer.writer.print("rev {d} {s}", .{ c, tag }); 654 } 655 const target = 40 + (line * 17 + fi * 23) % 70; 656 var w = line + fi; 657 while (true) : (w += 1) { 658 const word = scatter_words[w % scatter_words.len]; 659 const len = writer.written().len - start; 660 if (len >= target or len + 1 + word.len > 120) break; 661 if (len == 0) 662 try writer.writer.print("{s}", .{word}) 663 else 664 try writer.writer.print(" {s}", .{word}); 665 } 666 try writer.writer.writeByte('\n'); 667 } 668 try file.writeStreamingAll(io, writer.written()); 669 } 670 671 // tag the current HEAD as the next sequential version: v1, v2, and so on 672 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 673 var buf: [16]u8 = undefined; 674 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 675 _ = try repo.addTag(io, allocator, .{ .name = name }); 676 n.* += 1; 677 } 678 679 // recursively copy the contents of src_dir into dest_dir 680 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 681 var iter = src_dir.iterate(); 682 while (try iter.next(io)) |entry| { 683 switch (entry.kind) { 684 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 685 .directory => { 686 try dest_dir.createDirPath(io, entry.name); 687 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 688 defer dest_entry_dir.close(io); 689 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 690 defer src_entry_dir.close(io); 691 try copyDir(io, src_entry_dir, dest_entry_dir); 692 }, 693 else => {}, 694 } 695 } 696 } 697