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