From 97f329c8254fc2cd90fccd5a490eb1d3e948b473 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 4 Apr 2026 05:57:58 +0200 Subject: [PATCH] feat: ui --- .agent/controller-loop/task.toon | 9 + .agent/controllers/AGENTS.md | 18 + .../goal.md | 3 + .../plan.toon | 3 + .../standards.md | 5 + .../state.toon | 21 + .agent/controllers/controller-loop/goal.md | 4 + .agent/controllers/controller-loop/plan.toon | 3 + .../controllers/controller-loop/standards.md | 8 + .agent/controllers/controller-loop/state.toon | 17 + .../controllers/keystone-seam-audit/goal.md | 20 + .../controllers/keystone-seam-audit/plan.toon | 94 + .../keystone-seam-audit/standards.md | 14 + .../keystone-seam-audit/state.toon | 25 + .direnv/bin/nix-direnv-reload | 19 + .../01x5k4nlxcpyd85nnr0b9gm89rm8ff4x-source | 1 + .../j8wb3r6xmck1kwx5yfhgl0dlg8y2qa1b-source | 1 + .../xaknai40sz4yyy5658prwrwmfycj4xvm-source | 1 + .../yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source | 1 + ...e-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa | 1 + ...5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc | 2156 +++++++++++++ .envrc | 1 + .gitignore | 2 + Cargo.lock | 2851 +++++++++++++++++ Cargo.toml | 16 + README.md | 57 + flake.lock | 61 + flake.nix | 48 + nix/packages/codex-controller-loop.nix | 12 + src/app/input.rs | 20 + src/app/mod.rs | 215 ++ src/app/picker.rs | 91 + src/app/runtime.rs | 241 ++ src/app/session.rs | 156 + src/app/tests.rs | 117 + src/app/workspace_input.rs | 309 ++ src/cli.rs | 79 + src/controller/engine.rs | 206 ++ src/controller/executor.rs | 61 + src/controller/goal_checker.rs | 13 + src/controller/mod.rs | 6 + src/controller/planner.rs | 50 + src/controller/state.rs | 9 + src/controller/verifier.rs | 44 + src/error.rs | 7 + src/main.rs | 17 + src/model.rs | 774 +++++ src/planning/forwarder.rs | 98 + src/planning/mod.rs | 2 + src/planning/session.rs | 79 + src/process.rs | 604 ++++ src/repo.rs | 62 + src/storage/mod.rs | 1 + src/storage/toon.rs | 388 +++ src/ui/mod.rs | 905 ++++++ 55 files changed, 10026 insertions(+) create mode 100644 .agent/controller-loop/task.toon create mode 100644 .agent/controllers/AGENTS.md create mode 100644 .agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/goal.md create mode 100644 .agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/plan.toon create mode 100644 .agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/standards.md create mode 100644 .agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/state.toon create mode 100644 .agent/controllers/controller-loop/goal.md create mode 100644 .agent/controllers/controller-loop/plan.toon create mode 100644 .agent/controllers/controller-loop/standards.md create mode 100644 .agent/controllers/controller-loop/state.toon create mode 100644 .agent/controllers/keystone-seam-audit/goal.md create mode 100644 .agent/controllers/keystone-seam-audit/plan.toon create mode 100644 .agent/controllers/keystone-seam-audit/standards.md create mode 100644 .agent/controllers/keystone-seam-audit/state.toon create mode 100755 .direnv/bin/nix-direnv-reload create mode 120000 .direnv/flake-inputs/01x5k4nlxcpyd85nnr0b9gm89rm8ff4x-source create mode 120000 .direnv/flake-inputs/j8wb3r6xmck1kwx5yfhgl0dlg8y2qa1b-source create mode 120000 .direnv/flake-inputs/xaknai40sz4yyy5658prwrwmfycj4xvm-source create mode 120000 .direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source create mode 120000 .direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa create mode 100644 .direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/packages/codex-controller-loop.nix create mode 100644 src/app/input.rs create mode 100644 src/app/mod.rs create mode 100644 src/app/picker.rs create mode 100644 src/app/runtime.rs create mode 100644 src/app/session.rs create mode 100644 src/app/tests.rs create mode 100644 src/app/workspace_input.rs create mode 100644 src/cli.rs create mode 100644 src/controller/engine.rs create mode 100644 src/controller/executor.rs create mode 100644 src/controller/goal_checker.rs create mode 100644 src/controller/mod.rs create mode 100644 src/controller/planner.rs create mode 100644 src/controller/state.rs create mode 100644 src/controller/verifier.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/model.rs create mode 100644 src/planning/forwarder.rs create mode 100644 src/planning/mod.rs create mode 100644 src/planning/session.rs create mode 100644 src/process.rs create mode 100644 src/repo.rs create mode 100644 src/storage/mod.rs create mode 100644 src/storage/toon.rs create mode 100644 src/ui/mod.rs diff --git a/.agent/controller-loop/task.toon b/.agent/controller-loop/task.toon new file mode 100644 index 0000000..a4f726b --- /dev/null +++ b/.agent/controller-loop/task.toon @@ -0,0 +1,9 @@ +engine: "data-driven-v1" +goal_file: ".agent/controllers/keystone-seam-audit/goal.md" +plan_file: ".agent/controllers/keystone-seam-audit/plan.toon" +state_file: ".agent/controllers/keystone-seam-audit/state.toon" +standards_file: ".agent/controllers/keystone-seam-audit/standards.md" +branch: "codex/keystone-seam-audit" +continue_until: "fixed-point" +max_runs: 12 +max_wall_clock: 4h \ No newline at end of file diff --git a/.agent/controllers/AGENTS.md b/.agent/controllers/AGENTS.md new file mode 100644 index 0000000..683191b --- /dev/null +++ b/.agent/controllers/AGENTS.md @@ -0,0 +1,18 @@ +# AGENTS.md + +## Scope + +These instructions apply under `.agent/controllers/`. + +## What Belongs Here + +- checked-in controller state files +- checked-in controller plans +- controller goals and standards documents + +## Rules + +- Keep controller state in TOON and checked in. +- Keep plans in TOON and goals/standards in Markdown. +- Do not add TypeScript, JavaScript, or prompt-template assets here. +- The Rust engine owns runtime logic; `.agent/controllers/` holds data, not controller code. diff --git a/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/goal.md b/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/goal.md new file mode 100644 index 0000000..1315b20 --- /dev/null +++ b/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/goal.md @@ -0,0 +1,3 @@ +# Goal + +Describe the goal for this controller. diff --git a/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/plan.toon b/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/plan.toon new file mode 100644 index 0000000..c0524ef --- /dev/null +++ b/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/plan.toon @@ -0,0 +1,3 @@ +version: 1 +goal_summary: No plan yet +steps[0]: \ No newline at end of file diff --git a/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/standards.md b/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/standards.md new file mode 100644 index 0000000..e7b6088 --- /dev/null +++ b/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/standards.md @@ -0,0 +1,5 @@ +# Standards + +- Keep code maintainable. +- Avoid one-off hacks. +- Leave tests green. diff --git a/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/state.toon b/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/state.toon new file mode 100644 index 0000000..fdb40a3 --- /dev/null +++ b/.agent/controllers/code-refactoring-act-as-as-a-senior-software-arc/state.toon @@ -0,0 +1,21 @@ +version: 1 +phase: planning +goal_status: unknown +goal_revision: 0 +current_step_id: null +iteration: 0 +replan_required: false +completed_steps[0]: +blocked_steps[0]: +last_verification: null +last_cleanup_summary: null +last_full_test_summary: null +history[0]: +notes[0]: +planning_session: + pending_question: null + transcript[0]: +started_at: "1775272586" +last_usage_refresh_at: "1775272706" +last_usage_input_tokens: null +last_usage_output_tokens: null \ No newline at end of file diff --git a/.agent/controllers/controller-loop/goal.md b/.agent/controllers/controller-loop/goal.md new file mode 100644 index 0000000..3fe078a --- /dev/null +++ b/.agent/controllers/controller-loop/goal.md @@ -0,0 +1,4 @@ +# Goal + +Rewrite `codex-controller-loop` as a Rust TUI-first autonomous controller with TOON-backed machine state and a hard planning/execution phase boundary. + diff --git a/.agent/controllers/controller-loop/plan.toon b/.agent/controllers/controller-loop/plan.toon new file mode 100644 index 0000000..1ad859f --- /dev/null +++ b/.agent/controllers/controller-loop/plan.toon @@ -0,0 +1,3 @@ +version: 1 +goal_summary: Rust TUI-first autonomous controller +steps[0]: diff --git a/.agent/controllers/controller-loop/standards.md b/.agent/controllers/controller-loop/standards.md new file mode 100644 index 0000000..9ecb78f --- /dev/null +++ b/.agent/controllers/controller-loop/standards.md @@ -0,0 +1,8 @@ +# Standards + +- Keep the Rust code modular and readable. +- Treat planning as the only user-input phase. +- Treat execution as autonomous except for pause, resume, stop, and goal update. +- Keep controller-owned machine state in TOON files. +- Leave the codebase in a maintainable state after each completed step. + diff --git a/.agent/controllers/controller-loop/state.toon b/.agent/controllers/controller-loop/state.toon new file mode 100644 index 0000000..37ce501 --- /dev/null +++ b/.agent/controllers/controller-loop/state.toon @@ -0,0 +1,17 @@ +version: 1 +phase: planning +goal_status: unknown +goal_revision: 0 +current_step_id: null +iteration: 0 +replan_required: false +completed_steps[0]: +blocked_steps[0]: +last_verification: null +last_cleanup_summary: null +last_full_test_summary: null +history[0]: +notes[0]: +planning_session: + pending_question: null + transcript[0]: diff --git a/.agent/controllers/keystone-seam-audit/goal.md b/.agent/controllers/keystone-seam-audit/goal.md new file mode 100644 index 0000000..8bc5325 --- /dev/null +++ b/.agent/controllers/keystone-seam-audit/goal.md @@ -0,0 +1,20 @@ +# Goal + +Identify oversized, hand-maintained source files in the repository, prioritize the highest-value refactor targets, and split them into smaller, cohesive modules without changing external behavior. + +A file should be considered a refactor candidate when it is materially large or overloaded, using these default signals: +- More than 300 lines of hand-written code. +- Multiple unrelated responsibilities in one file. +- Difficult-to-test logic mixed with I/O, UI, routing, state wiring, or formatting. + +Execution requirements: +- Ignore generated, vendored, build, cache, and lock files unless the repository clearly treats them as hand-maintained source. +- Refactor incrementally, one target at a time, starting with the largest safe candidate. +- Preserve public APIs and user-visible behavior unless a compatibility adjustment is required to complete the split safely. +- Leave the repository in a clean, test-passing state. + +Expected outputs: +- Smaller files with clearer ownership boundaries. +- Any necessary import/export or module wiring updates. +- Tests updated or added when needed to preserve behavior. +- A concise summary of files split, new module boundaries, and verification results. \ No newline at end of file diff --git a/.agent/controllers/keystone-seam-audit/plan.toon b/.agent/controllers/keystone-seam-audit/plan.toon new file mode 100644 index 0000000..1b11d6d --- /dev/null +++ b/.agent/controllers/keystone-seam-audit/plan.toon @@ -0,0 +1,94 @@ +version: 1 +goal_summary: "Audit the repository for oversized hand-maintained source files, prioritize safe high-value refactor targets, split them into smaller cohesive modules, and finish with passing validation and a clean diff." +steps[6]: + - id: s1 + title: Establish Safe Refactor Scope + purpose: "Load controller inputs and repository constraints, confirm the working tree state, and define the exact file-selection rules so execution can proceed without ambiguity." + inputs[4]: ".agent/controllers/keystone-seam-audit/goal.md",".agent/controllers/keystone-seam-audit/standards.md",repository working tree,"repo-level instructions such as AGENTS.md if present" + outputs[3]: confirmed execution constraints,candidate exclusion rules,list of locally modified files to avoid or handle carefully + dependencies[0]: + verification[2]: + - label: Check working tree state + commands[1]: "git status --short" + - label: Locate repo instructions + commands[1]: "rg --files -g 'AGENTS.md' -g '.agent/**'" + cleanup_requirements[1]{label,description}: + No accidental overlap with user edits,Do not modify files with unrelated local changes unless the change is required and the existing edits are understood and preserved. + status: active + attempts: 1 + - id: s2 + title: "Inventory Large Hand-Maintained Files" + purpose: "Produce a ranked inventory of oversized source files while excluding generated and third-party material." + inputs[2]: repository file list,selection thresholds from goal and standards + outputs[3]: ranked candidate list with line counts,"excluded-path list",initial top refactor targets + dependencies[1]: s1 + verification[2]: + - label: Enumerate tracked files + commands[1]: "rg --files" + - label: Rank large files by line count + commands[1]: "python - <<'PY'\nimport os, subprocess\nexclude = {'node_modules','dist','build','coverage','.git','.next','.svelte-kit','target','vendor'}\nfiles = subprocess.check_output(['rg','--files']).decode().splitlines()\nrows = []\nfor f in files:\n parts = set(f.split('/'))\n if parts & exclude:\n continue\n if os.path.splitext(f)[1] in {'.png','.jpg','.jpeg','.gif','.svg','.lock','.snap','.min.js','.map'}:\n continue\n try:\n with open(f,'r',encoding='utf-8') as fh:\n n = sum(1 for _ in fh)\n except Exception:\n continue\n if n > 300:\n rows.append((n,f))\nfor n,f in sorted(rows, reverse=True)[:50]:\n print(f'{n}\\t{f}')\nPY" + cleanup_requirements[1]{label,description}: + Discard false positives,"Remove generated files, migration dumps, fixtures, and machine-authored artifacts from the candidate list before choosing targets." + status: todo + attempts: 0 + - id: s3 + title: Choose Refactor Order And Boundaries + purpose: "Inspect the largest candidates, decide which files are safe and high-value to split first, and define intended module boundaries before editing." + inputs[3]: ranked candidate list,current file contents,existing module structure + outputs[3]: ordered target list,boundary notes for each target,"explicit non-goals for each refactor" + dependencies[1]: s2 + verification[2]: + - label: Inspect top candidates + commands[2]: "sed -n '1,220p' ","sed -n '221,440p' " + - label: Map exports and dependents + commands[1]: "rg -n \"from ['\\\"]|require\\(\" " + cleanup_requirements[1]{label,description}: + "Avoid cosmetic-only churn","Do not split files purely by line count; only proceed when coherent seams such as utilities, domain logic, adapters, routes, or components are identifiable." + status: todo + attempts: 0 + - id: s4 + title: Refactor First Target Incrementally + purpose: "Split the highest-priority candidate into smaller cohesive files while preserving behavior and keeping the change reviewable." + inputs[3]: first target file,boundary notes,repo conventions + outputs[3]: new smaller modules,updated imports/exports,"target-specific validation result" + dependencies[1]: s3 + verification[2]: + - label: Run targeted tests or checks + commands[1]: "" + - label: Confirm file size reduction + commands[1]: "wc -l " + cleanup_requirements[1]{label,description}: + Remove temporary seams,"Delete transitional helpers, dead exports, and unused imports created during the split before moving on." + status: todo + attempts: 0 + - id: s5 + title: "Repeat For Remaining High-Value Targets" + purpose: "Continue the same refactor pattern for additional oversized files until the main high-value targets are addressed or diminishing returns are reached." + inputs[2]: remaining ordered targets,lessons from first refactor + outputs[3]: additional split modules,updated dependency wiring,"per-target validation notes" + dependencies[1]: s4 + verification[2]: + - label: "Run per-target validation after each split" + commands[1]: "" + - label: Track remaining oversized files + commands[1]: "python - <<'PY'\nimport os, subprocess\nexclude = {'node_modules','dist','build','coverage','.git','.next','.svelte-kit','target','vendor'}\nfiles = subprocess.check_output(['rg','--files']).decode().splitlines()\nfor f in files:\n parts = set(f.split('/'))\n if parts & exclude:\n continue\n try:\n with open(f,'r',encoding='utf-8') as fh:\n n = sum(1 for _ in fh)\n except Exception:\n continue\n if n > 300:\n print(f'{n}\\t{f}')\nPY" + cleanup_requirements[1]{label,description}: + Stop at sensible boundary,"Do not keep splitting once modules are cohesive and maintainable; leave well-structured files intact even if they remain moderately large." + status: todo + attempts: 0 + - id: s6 + title: Run Full Validation And Final Cleanup + purpose: "Verify repository health, remove leftover refactor debris, and produce a concise execution summary for the controller result." + inputs[2]: all refactor changes,repository validation commands + outputs[3]: final passing validation results,cleaned diff,"summary of targets, seams, and tests" + dependencies[1]: s5 + verification[2]: + - label: Run broadest available validation + commands[3]: "","","" + - label: Check for leftover issues + commands[2]: "git diff --check","git status --short" + cleanup_requirements[2]{label,description}: + Leave clean refactor artifacts,"Remove unused files, stale exports, dead code, and temporary comments; ensure only intentional source changes remain." + Record outcome,"Summarize which files were split, the new module boundaries, any tests added or updated, and any remaining large files intentionally left unchanged." + status: todo + attempts: 0 \ No newline at end of file diff --git a/.agent/controllers/keystone-seam-audit/standards.md b/.agent/controllers/keystone-seam-audit/standards.md new file mode 100644 index 0000000..6c84948 --- /dev/null +++ b/.agent/controllers/keystone-seam-audit/standards.md @@ -0,0 +1,14 @@ +# Standards + +- Prefer cohesive modules with a single clear responsibility over line-count-only splitting. +- Preserve behavior first; do not mix feature work with the refactor. +- Avoid one-off abstractions. New modules must reflect real ownership boundaries that the codebase can keep using. +- Keep public interfaces stable unless a minimal coordinated update is required. +- Exclude generated, vendored, compiled, snapshot, fixture, and lock files from refactor targets unless explicitly treated as source by the repo. +- Before editing, check for existing local changes in candidate files and avoid overwriting user work. +- Refactor in small, reviewable increments so each target can be validated independently. +- Run the smallest relevant test or lint command after each target, then run the broadest available final validation before finishing. +- If no automated tests exist for a risky seam, add or strengthen focused tests before or during the refactor when practical. +- Keep names, module placement, and imports maintainable and consistent with existing repo conventions. +- Remove dead helpers, obsolete re-exports, and temporary scaffolding introduced during the split. +- Leave tests green. \ No newline at end of file diff --git a/.agent/controllers/keystone-seam-audit/state.toon b/.agent/controllers/keystone-seam-audit/state.toon new file mode 100644 index 0000000..7498c9f --- /dev/null +++ b/.agent/controllers/keystone-seam-audit/state.toon @@ -0,0 +1,25 @@ +version: 1 +phase: executing +goal_status: "in-progress" +goal_revision: 1 +current_step_id: null +iteration: 0 +replan_required: false +completed_steps[0]: +blocked_steps[0]: +last_verification: null +last_cleanup_summary: null +last_full_test_summary: null +history[0]: +notes[0]: +planning_session: + pending_question: null + transcript[4]{role,content}: + user,test + assistant,"What should the `keystone-seam-audit` controller actually do? Please specify the target system/repo area, what a \"seam audit\" should inspect, and the expected output artifact(s)." + user,find large files and refactor to smaller files + assistant,Planning completed +started_at: "1775273562" +last_usage_refresh_at: "1775274722" +last_usage_input_tokens: null +last_usage_output_tokens: null \ No newline at end of file diff --git a/.direnv/bin/nix-direnv-reload b/.direnv/bin/nix-direnv-reload new file mode 100755 index 0000000..9a2cff7 --- /dev/null +++ b/.direnv/bin/nix-direnv-reload @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e +if [[ ! -d "/Users/eric/Projects/nodeiwest/codex-controller-loop" ]]; then + echo "Cannot find source directory; Did you move it?" + echo "(Looking for "/Users/eric/Projects/nodeiwest/codex-controller-loop")" + echo 'Cannot force reload with this script - use "direnv reload" manually and then try again' + exit 1 +fi + +# rebuild the cache forcefully +_nix_direnv_force_reload=1 direnv exec "/Users/eric/Projects/nodeiwest/codex-controller-loop" true + +# Update the mtime for .envrc. +# This will cause direnv to reload again - but without re-building. +touch "/Users/eric/Projects/nodeiwest/codex-controller-loop/.envrc" + +# Also update the timestamp of whatever profile_rc we have. +# This makes sure that we know we are up to date. +touch -r "/Users/eric/Projects/nodeiwest/codex-controller-loop/.envrc" "/Users/eric/Projects/nodeiwest/codex-controller-loop/.direnv"/*.rc diff --git a/.direnv/flake-inputs/01x5k4nlxcpyd85nnr0b9gm89rm8ff4x-source b/.direnv/flake-inputs/01x5k4nlxcpyd85nnr0b9gm89rm8ff4x-source new file mode 120000 index 0000000..f7fce9f --- /dev/null +++ b/.direnv/flake-inputs/01x5k4nlxcpyd85nnr0b9gm89rm8ff4x-source @@ -0,0 +1 @@ +/nix/store/01x5k4nlxcpyd85nnr0b9gm89rm8ff4x-source \ No newline at end of file diff --git a/.direnv/flake-inputs/j8wb3r6xmck1kwx5yfhgl0dlg8y2qa1b-source b/.direnv/flake-inputs/j8wb3r6xmck1kwx5yfhgl0dlg8y2qa1b-source new file mode 120000 index 0000000..9346760 --- /dev/null +++ b/.direnv/flake-inputs/j8wb3r6xmck1kwx5yfhgl0dlg8y2qa1b-source @@ -0,0 +1 @@ +/nix/store/j8wb3r6xmck1kwx5yfhgl0dlg8y2qa1b-source \ No newline at end of file diff --git a/.direnv/flake-inputs/xaknai40sz4yyy5658prwrwmfycj4xvm-source b/.direnv/flake-inputs/xaknai40sz4yyy5658prwrwmfycj4xvm-source new file mode 120000 index 0000000..72e7534 --- /dev/null +++ b/.direnv/flake-inputs/xaknai40sz4yyy5658prwrwmfycj4xvm-source @@ -0,0 +1 @@ +/nix/store/xaknai40sz4yyy5658prwrwmfycj4xvm-source \ No newline at end of file diff --git a/.direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source b/.direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source new file mode 120000 index 0000000..f17959f --- /dev/null +++ b/.direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source @@ -0,0 +1 @@ +/nix/store/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa new file mode 120000 index 0000000..4ba9f1e --- /dev/null +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa @@ -0,0 +1 @@ +/nix/store/y0camphrdlb2higdygca6b3sqia1bgf6-nix-shell-env \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc new file mode 100644 index 0000000..839510b --- /dev/null +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc @@ -0,0 +1,2156 @@ +unset shellHook +PATH=${PATH:-} +nix_saved_PATH="$PATH" +XDG_DATA_DIRS=${XDG_DATA_DIRS:-} +nix_saved_XDG_DATA_DIRS="$XDG_DATA_DIRS" +AR='ar' +export AR +AS='as' +export AS +BASH='/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash' +CC='clang' +export CC +CONFIG_SHELL='/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash' +export CONFIG_SHELL +CXX='clang++' +export CXX +DEVELOPER_DIR='/nix/store/q2dccg26bm7bn6ia1q30qkl5jck7wwgb-apple-sdk-14.4' +export DEVELOPER_DIR +HOSTTYPE='aarch64' +HOST_PATH='/nix/store/a85h00app701vf0ggln0r97yayszvwkk-libiconv-109.100.2/bin:/nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin:/nix/store/f9ik2jdvk6shdnzr4l8mibqdiqjd9chb-findutils-4.10.0/bin:/nix/store/hrkhhbx6v8fzwnizwvqh9h0yff5vcp75-diffutils-3.12/bin:/nix/store/m188brzrrd4f0jdiy495vz8pz75j5kpn-gnused-4.9/bin:/nix/store/mwj8nml055g8w0c2yq1apajcwrqgsg9q-gnugrep-3.12/bin:/nix/store/bvrbfzyimpjxwn679252bhbbccnb43nr-gawk-5.3.2/bin:/nix/store/k5q5hl5zhvvh9j2q9nr6wk8wx4f5rddv-gnutar-1.35/bin:/nix/store/fx6qvxgcvfpjhy5wr9hsvpkpjqw5zgf0-gzip-1.14/bin:/nix/store/4rqfrsyqp53wavnk86k4flm35vhagwjk-bzip2-1.0.8-bin/bin:/nix/store/y40n7jzzy9qydb120kxgbzi55mprbkfm-gnumake-4.4.1/bin:/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin:/nix/store/jqcdmxsh8zk7bn1qcxwq3dwjjx4p7i0f-patch-2.8/bin:/nix/store/fc6w1zkl1klj979isgaag8a66jmpq1qs-xz-5.8.2-bin/bin:/nix/store/5n5ynvylwkjrblm2lpj3ns2qc821fvmi-file-5.45/bin' +export HOST_PATH +IFS=' +' +IN_NIX_SHELL='impure' +export IN_NIX_SHELL +LD='ld' +export LD +LD_DYLD_PATH='/usr/lib/dyld' +export LD_DYLD_PATH +LINENO='80' +MACHTYPE='aarch64-apple-darwin25.3.0' +MACOSX_DEPLOYMENT_TARGET='14.0' +export MACOSX_DEPLOYMENT_TARGET +NIX_APPLE_SDK_VERSION='140400' +export NIX_APPLE_SDK_VERSION +NIX_BINTOOLS='/nix/store/hwn7mviydmcdr2gw3zx30rcc0xi7k40c-cctools-binutils-darwin-wrapper-1010.6' +export NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_arm64_apple_darwin='1' +export NIX_BINTOOLS_WRAPPER_TARGET_HOST_arm64_apple_darwin +NIX_BUILD_CORES='8' +export NIX_BUILD_CORES +NIX_CC='/nix/store/s7qlr26bmc6n4r607scz8iiwcg6yg4ic-clang-wrapper-21.1.8' +export NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_arm64_apple_darwin='1' +export NIX_CC_WRAPPER_TARGET_HOST_arm64_apple_darwin +NIX_CFLAGS_COMPILE=' -frandom-seed=y0camphrdl -isystem /nix/store/gas29mwgqh0i3d4083ygl9b65sll1yil-libcxx-20.1.0+apple-sdk-26.0/include -fmacro-prefix-map=/nix/store/gas29mwgqh0i3d4083ygl9b65sll1yil-libcxx-20.1.0+apple-sdk-26.0=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libcxx-20.1.0+apple-sdk-26.0 -isystem /nix/store/pnazzar06qzgcbsln5shjnmrq0krryww-compiler-rt-libc-21.1.8-dev/include -fmacro-prefix-map=/nix/store/pnazzar06qzgcbsln5shjnmrq0krryww-compiler-rt-libc-21.1.8-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-compiler-rt-libc-21.1.8-dev -isystem /nix/store/4l8729yjln4zhnry763pklqb75dwmrd2-libiconv-109.100.2-dev/include -fmacro-prefix-map=/nix/store/4l8729yjln4zhnry763pklqb75dwmrd2-libiconv-109.100.2-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libiconv-109.100.2-dev -isystem /nix/store/d6dh7sdypw64lf74iv0gwgphvs1dq3fa-libresolv-91-dev/include -fmacro-prefix-map=/nix/store/d6dh7sdypw64lf74iv0gwgphvs1dq3fa-libresolv-91-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libresolv-91-dev -isystem /nix/store/ykwz6395vf4gn185zfvkx30avzmap11y-libsbuf-14.1.0-dev/include -fmacro-prefix-map=/nix/store/ykwz6395vf4gn185zfvkx30avzmap11y-libsbuf-14.1.0-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libsbuf-14.1.0-dev -isystem /nix/store/gas29mwgqh0i3d4083ygl9b65sll1yil-libcxx-20.1.0+apple-sdk-26.0/include -fmacro-prefix-map=/nix/store/gas29mwgqh0i3d4083ygl9b65sll1yil-libcxx-20.1.0+apple-sdk-26.0=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libcxx-20.1.0+apple-sdk-26.0 -isystem /nix/store/pnazzar06qzgcbsln5shjnmrq0krryww-compiler-rt-libc-21.1.8-dev/include -fmacro-prefix-map=/nix/store/pnazzar06qzgcbsln5shjnmrq0krryww-compiler-rt-libc-21.1.8-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-compiler-rt-libc-21.1.8-dev -isystem /nix/store/4l8729yjln4zhnry763pklqb75dwmrd2-libiconv-109.100.2-dev/include -fmacro-prefix-map=/nix/store/4l8729yjln4zhnry763pklqb75dwmrd2-libiconv-109.100.2-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libiconv-109.100.2-dev -isystem /nix/store/d6dh7sdypw64lf74iv0gwgphvs1dq3fa-libresolv-91-dev/include -fmacro-prefix-map=/nix/store/d6dh7sdypw64lf74iv0gwgphvs1dq3fa-libresolv-91-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libresolv-91-dev -isystem /nix/store/ykwz6395vf4gn185zfvkx30avzmap11y-libsbuf-14.1.0-dev/include -fmacro-prefix-map=/nix/store/ykwz6395vf4gn185zfvkx30avzmap11y-libsbuf-14.1.0-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libsbuf-14.1.0-dev' +export NIX_CFLAGS_COMPILE +NIX_DONT_SET_RPATH='1' +export NIX_DONT_SET_RPATH +NIX_DONT_SET_RPATH_FOR_BUILD='1' +export NIX_DONT_SET_RPATH_FOR_BUILD +NIX_ENFORCE_NO_NATIVE='1' +export NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE='bindnow format fortify fortify3 libcxxhardeningfast pic relro stackclashprotection stackprotector strictflexarrays1 strictoverflow zerocallusedregs' +export NIX_HARDENING_ENABLE +NIX_IGNORE_LD_THROUGH_GCC='1' +export NIX_IGNORE_LD_THROUGH_GCC +NIX_LDFLAGS=' -L/nix/store/gas29mwgqh0i3d4083ygl9b65sll1yil-libcxx-20.1.0+apple-sdk-26.0/lib -L/nix/store/hnb6cg3cfrsnhfppiaa6zzpk1i57wzm3-compiler-rt-libc-21.1.8/lib -L/nix/store/a85h00app701vf0ggln0r97yayszvwkk-libiconv-109.100.2/lib -L/nix/store/ymxg8vmmk5l8wvs5lld3vl9k3rhvdh59-libresolv-91/lib -L/nix/store/3kl23yrdmyy7c8m4fvzg2kilks9ca83s-libsbuf-14.1.0/lib -L/nix/store/22mhkcy5mpkgsa7k1d04qkxybnkwhqc4-libutil-72/lib -L/nix/store/gas29mwgqh0i3d4083ygl9b65sll1yil-libcxx-20.1.0+apple-sdk-26.0/lib -L/nix/store/hnb6cg3cfrsnhfppiaa6zzpk1i57wzm3-compiler-rt-libc-21.1.8/lib -L/nix/store/a85h00app701vf0ggln0r97yayszvwkk-libiconv-109.100.2/lib -L/nix/store/ymxg8vmmk5l8wvs5lld3vl9k3rhvdh59-libresolv-91/lib -L/nix/store/3kl23yrdmyy7c8m4fvzg2kilks9ca83s-libsbuf-14.1.0/lib -L/nix/store/22mhkcy5mpkgsa7k1d04qkxybnkwhqc4-libutil-72/lib' +export NIX_LDFLAGS +NIX_NO_SELF_RPATH='1' +export NIX_NO_SELF_RPATH +NIX_STORE='/nix/store' +export NIX_STORE +NM='nm' +export NM +OBJCOPY='objcopy' +export OBJCOPY +OBJDUMP='objdump' +export OBJDUMP +OLDPWD='' +export OLDPWD +OPTERR='1' +OSTYPE='darwin25.3.0' +PATH='/nix/store/6qbj40r0s289k5slmy8yna5x2hfz01wg-git-2.53.0/bin:/nix/store/0p9bi4b2dzlggz2irpnbvcf5rb6lcm9m-rustc-wrapper-1.94.0/bin:/nix/store/fp6mq617pb2rwrkc2fspjbwgf85jdb6n-cargo-1.94.0/bin:/nix/store/zhiy290rs2xwi9146nyz9w72q0sa9iq1-clippy-1.94.0/bin:/nix/store/fwgjpxfnwrbymfab8yd6hp9nws2b8jpn-rustfmt-1.94.0/bin:/nix/store/lnvhspzjms1qx8abfwr735h3cg3h265r-rust-analyzer-2026-03-23/bin:/nix/store/s7qlr26bmc6n4r607scz8iiwcg6yg4ic-clang-wrapper-21.1.8/bin:/nix/store/6adskryjj6g2p508xjxp2x4iwyy15gsr-clang-21.1.8/bin:/nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin:/nix/store/hwn7mviydmcdr2gw3zx30rcc0xi7k40c-cctools-binutils-darwin-wrapper-1010.6/bin:/nix/store/yppcv1v8rdm3086ykynmf7w5yy61v30b-cctools-binutils-darwin-1010.6/bin:/nix/store/930py6p7h4xw7fwys7y3hx7rkyqnpvqq-xcbuild-0.1.1-unstable-2019-11-20-xcrun/bin:/nix/store/a85h00app701vf0ggln0r97yayszvwkk-libiconv-109.100.2/bin:/nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin:/nix/store/f9ik2jdvk6shdnzr4l8mibqdiqjd9chb-findutils-4.10.0/bin:/nix/store/hrkhhbx6v8fzwnizwvqh9h0yff5vcp75-diffutils-3.12/bin:/nix/store/m188brzrrd4f0jdiy495vz8pz75j5kpn-gnused-4.9/bin:/nix/store/mwj8nml055g8w0c2yq1apajcwrqgsg9q-gnugrep-3.12/bin:/nix/store/bvrbfzyimpjxwn679252bhbbccnb43nr-gawk-5.3.2/bin:/nix/store/k5q5hl5zhvvh9j2q9nr6wk8wx4f5rddv-gnutar-1.35/bin:/nix/store/fx6qvxgcvfpjhy5wr9hsvpkpjqw5zgf0-gzip-1.14/bin:/nix/store/4rqfrsyqp53wavnk86k4flm35vhagwjk-bzip2-1.0.8-bin/bin:/nix/store/y40n7jzzy9qydb120kxgbzi55mprbkfm-gnumake-4.4.1/bin:/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin:/nix/store/jqcdmxsh8zk7bn1qcxwq3dwjjx4p7i0f-patch-2.8/bin:/nix/store/fc6w1zkl1klj979isgaag8a66jmpq1qs-xz-5.8.2-bin/bin:/nix/store/5n5ynvylwkjrblm2lpj3ns2qc821fvmi-file-5.45/bin' +export PATH +PATH_LOCALE='/nix/store/r0di2qx1f6g3g2b4894ql8vyqd5h6q9h-locale-118/share/locale' +export PATH_LOCALE +PS4='+ ' +RANLIB='ranlib' +export RANLIB +SDKROOT='/nix/store/q2dccg26bm7bn6ia1q30qkl5jck7wwgb-apple-sdk-14.4/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk' +export SDKROOT +SHELL='/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash' +export SHELL +SIZE='size' +export SIZE +SOURCE_DATE_EPOCH='315532800' +export SOURCE_DATE_EPOCH +STRINGS='strings' +export STRINGS +STRIP='strip' +export STRIP +XDG_DATA_DIRS='/nix/store/6qbj40r0s289k5slmy8yna5x2hfz01wg-git-2.53.0/share:/nix/store/fp6mq617pb2rwrkc2fspjbwgf85jdb6n-cargo-1.94.0/share' +export XDG_DATA_DIRS +ZERO_AR_DATE='1' +export ZERO_AR_DATE +__darwinAllowLocalNetworking='' +export __darwinAllowLocalNetworking +__impureHostDeps='/bin/sh /usr/lib/libSystem.B.dylib /usr/lib/system/libunc.dylib /dev/zero /dev/random /dev/urandom /bin/sh' +export __impureHostDeps +__propagatedImpureHostDeps='' +export __propagatedImpureHostDeps +__propagatedSandboxProfile='' +export __propagatedSandboxProfile +__sandboxProfile='' +export __sandboxProfile +__structuredAttrs='' +export __structuredAttrs +_substituteStream_has_warned_replace_deprecation='false' +buildInputs='' +export buildInputs +buildPhase='{ echo "------------------------------------------------------------"; + echo " WARNING: the existence of this path is not guaranteed."; + echo " It is an internal implementation detail for pkgs.mkShell."; + echo "------------------------------------------------------------"; + echo; + # Record all build inputs as runtime dependencies + export; +} >> "$out" +' +export buildPhase +builder='/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash' +export builder +cmakeFlags='' +export cmakeFlags +configureFlags='' +export configureFlags +defaultBuildInputs='/nix/store/q2dccg26bm7bn6ia1q30qkl5jck7wwgb-apple-sdk-14.4' +defaultNativeBuildInputs='/nix/store/avjzyij6c5vbva2pqvfp7y9ch4aii05g-update-autotools-gnu-config-scripts-hook /nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh /nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh /nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh /nix/store/p3l1a5y7nllfyrjn2krlwgcc3z0cd3fq-make-symlinks-relative.sh /nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh /nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh /nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh /nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh /nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh /nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh /nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh /nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh /nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh /nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh /nix/store/s7qlr26bmc6n4r607scz8iiwcg6yg4ic-clang-wrapper-21.1.8' +depsBuildBuild='' +export depsBuildBuild +depsBuildBuildPropagated='' +export depsBuildBuildPropagated +depsBuildTarget='' +export depsBuildTarget +depsBuildTargetPropagated='' +export depsBuildTargetPropagated +depsHostHost='' +export depsHostHost +depsHostHostPropagated='' +export depsHostHostPropagated +depsTargetTarget='' +export depsTargetTarget +depsTargetTargetPropagated='' +export depsTargetTargetPropagated +doCheck='' +export doCheck +doInstallCheck='' +export doInstallCheck +dontAddDisableDepTrack='1' +export dontAddDisableDepTrack +declare -a envBuildBuildHooks=() +declare -a envBuildHostHooks=() +declare -a envBuildTargetHooks=() +declare -a envHostHostHooks=('ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' ) +declare -a envHostTargetHooks=('ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' ) +declare -a envTargetTargetHooks=() +declare -a fixupOutputHooks=('if [[ -z "${noAuditTmpdir-}" && -e "$prefix" ]]; then auditTmpdir "$prefix"; fi' 'if [ -z "${dontGzipMan-}" ]; then compressManPages "$prefix"; fi' '_moveLib64' '_moveSbin' '_moveSystemdUserUnits' 'patchShebangsAuto' '_pruneLibtoolFiles' '_doStrip' ) +initialPath='/nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10 /nix/store/f9ik2jdvk6shdnzr4l8mibqdiqjd9chb-findutils-4.10.0 /nix/store/hrkhhbx6v8fzwnizwvqh9h0yff5vcp75-diffutils-3.12 /nix/store/m188brzrrd4f0jdiy495vz8pz75j5kpn-gnused-4.9 /nix/store/mwj8nml055g8w0c2yq1apajcwrqgsg9q-gnugrep-3.12 /nix/store/bvrbfzyimpjxwn679252bhbbccnb43nr-gawk-5.3.2 /nix/store/k5q5hl5zhvvh9j2q9nr6wk8wx4f5rddv-gnutar-1.35 /nix/store/fx6qvxgcvfpjhy5wr9hsvpkpjqw5zgf0-gzip-1.14 /nix/store/4rqfrsyqp53wavnk86k4flm35vhagwjk-bzip2-1.0.8-bin /nix/store/y40n7jzzy9qydb120kxgbzi55mprbkfm-gnumake-4.4.1 /nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9 /nix/store/jqcdmxsh8zk7bn1qcxwq3dwjjx4p7i0f-patch-2.8 /nix/store/fc6w1zkl1klj979isgaag8a66jmpq1qs-xz-5.8.2-bin /nix/store/5n5ynvylwkjrblm2lpj3ns2qc821fvmi-file-5.45' +mesonFlags='' +export mesonFlags +name='nix-shell-env' +export name +nativeBuildInputs='/nix/store/6qbj40r0s289k5slmy8yna5x2hfz01wg-git-2.53.0 /nix/store/0p9bi4b2dzlggz2irpnbvcf5rb6lcm9m-rustc-wrapper-1.94.0 /nix/store/fp6mq617pb2rwrkc2fspjbwgf85jdb6n-cargo-1.94.0 /nix/store/zhiy290rs2xwi9146nyz9w72q0sa9iq1-clippy-1.94.0 /nix/store/fwgjpxfnwrbymfab8yd6hp9nws2b8jpn-rustfmt-1.94.0 /nix/store/lnvhspzjms1qx8abfwr735h3cg3h265r-rust-analyzer-2026-03-23' +export nativeBuildInputs +out='/Users/eric/Projects/nodeiwest/codex-controller-loop/outputs/out' +export out +outputBin='out' +outputDev='out' +outputDevdoc='REMOVE' +outputDevman='out' +outputDoc='out' +outputInclude='out' +outputInfo='out' +outputLib='out' +outputMan='out' +outputs='out' +export outputs +patches='' +export patches +phases='buildPhase' +export phases +pkg='/nix/store/q2dccg26bm7bn6ia1q30qkl5jck7wwgb-apple-sdk-14.4' +declare -a pkgsBuildBuild=() +declare -a pkgsBuildHost=('/nix/store/6qbj40r0s289k5slmy8yna5x2hfz01wg-git-2.53.0' '/nix/store/0p9bi4b2dzlggz2irpnbvcf5rb6lcm9m-rustc-wrapper-1.94.0' '/nix/store/fp6mq617pb2rwrkc2fspjbwgf85jdb6n-cargo-1.94.0' '/nix/store/zhiy290rs2xwi9146nyz9w72q0sa9iq1-clippy-1.94.0' '/nix/store/fwgjpxfnwrbymfab8yd6hp9nws2b8jpn-rustfmt-1.94.0' '/nix/store/lnvhspzjms1qx8abfwr735h3cg3h265r-rust-analyzer-2026-03-23' '/nix/store/avjzyij6c5vbva2pqvfp7y9ch4aii05g-update-autotools-gnu-config-scripts-hook' '/nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh' '/nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh' '/nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh' '/nix/store/p3l1a5y7nllfyrjn2krlwgcc3z0cd3fq-make-symlinks-relative.sh' '/nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh' '/nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh' '/nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh' '/nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh' '/nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh' '/nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh' '/nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh' '/nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh' '/nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh' '/nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh' '/nix/store/s7qlr26bmc6n4r607scz8iiwcg6yg4ic-clang-wrapper-21.1.8' '/nix/store/hwn7mviydmcdr2gw3zx30rcc0xi7k40c-cctools-binutils-darwin-wrapper-1010.6' '/nix/store/930py6p7h4xw7fwys7y3hx7rkyqnpvqq-xcbuild-0.1.1-unstable-2019-11-20-xcrun' ) +declare -a pkgsBuildTarget=() +declare -a pkgsHostHost=('/nix/store/gas29mwgqh0i3d4083ygl9b65sll1yil-libcxx-20.1.0+apple-sdk-26.0' '/nix/store/pnazzar06qzgcbsln5shjnmrq0krryww-compiler-rt-libc-21.1.8-dev' '/nix/store/hnb6cg3cfrsnhfppiaa6zzpk1i57wzm3-compiler-rt-libc-21.1.8' ) +declare -a pkgsHostTarget=('/nix/store/q2dccg26bm7bn6ia1q30qkl5jck7wwgb-apple-sdk-14.4' '/nix/store/4l8729yjln4zhnry763pklqb75dwmrd2-libiconv-109.100.2-dev' '/nix/store/a85h00app701vf0ggln0r97yayszvwkk-libiconv-109.100.2' '/nix/store/d6dh7sdypw64lf74iv0gwgphvs1dq3fa-libresolv-91-dev' '/nix/store/ymxg8vmmk5l8wvs5lld3vl9k3rhvdh59-libresolv-91' '/nix/store/ykwz6395vf4gn185zfvkx30avzmap11y-libsbuf-14.1.0-dev' '/nix/store/3kl23yrdmyy7c8m4fvzg2kilks9ca83s-libsbuf-14.1.0' '/nix/store/22mhkcy5mpkgsa7k1d04qkxybnkwhqc4-libutil-72' ) +declare -a pkgsTargetTarget=() +declare -a postFixupHooks=('noBrokenSymlinksInAllOutputs' '_makeSymlinksRelative' '_multioutPropagateDev' ) +declare -a postUnpackHooks=('_updateSourceDateEpochFromSourceRoot' ) +declare -a preConfigureHooks=('_multioutConfig' ) +preConfigurePhases=' updateAutotoolsGnuConfigScriptsPhase' +declare -a preFixupHooks=('_moveToShare' '_multioutDocs' '_multioutDevs' ) +preferLocalBuild='1' +export preferLocalBuild +prefix='/Users/eric/Projects/nodeiwest/codex-controller-loop/outputs/out' +declare -a propagatedBuildDepFiles=('propagated-build-build-deps' 'propagated-native-build-inputs' 'propagated-build-target-deps' ) +propagatedBuildInputs='' +export propagatedBuildInputs +declare -a propagatedHostDepFiles=('propagated-host-host-deps' 'propagated-build-inputs' ) +propagatedNativeBuildInputs='' +export propagatedNativeBuildInputs +declare -a propagatedTargetDepFiles=('propagated-target-target-deps' ) +shell='/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash' +export shell +shellHook='' +export shellHook +stdenv='/nix/store/4c9ajc7qmxd8kaanj1c9v0fbi60bn805-stdenv-darwin' +export stdenv +strictDeps='' +export strictDeps +stripDebugFlags='-S' +system='aarch64-darwin' +export system +declare -a unpackCmdHooks=('_defaultUnpack' ) +_activatePkgs () +{ + + local hostOffset targetOffset; + local pkg; + for hostOffset in "${allPlatOffsets[@]}"; + do + local pkgsVar="${pkgAccumVarVars[hostOffset + 1]}"; + for targetOffset in "${allPlatOffsets[@]}"; + do + (( hostOffset <= targetOffset )) || continue; + local pkgsRef="${pkgsVar}[$targetOffset - $hostOffset]"; + local pkgsSlice="${!pkgsRef}[@]"; + for pkg in ${!pkgsSlice+"${!pkgsSlice}"}; + do + activatePackage "$pkg" "$hostOffset" "$targetOffset"; + done; + done; + done +} +_addRpathPrefix () +{ + + if [ "${NIX_NO_SELF_RPATH:-0}" != 1 ]; then + export NIX_LDFLAGS="-rpath $1/lib ${NIX_LDFLAGS-}"; + fi +} +_addToEnv () +{ + + local depHostOffset depTargetOffset; + local pkg; + for depHostOffset in "${allPlatOffsets[@]}"; + do + local hookVar="${pkgHookVarVars[depHostOffset + 1]}"; + local pkgsVar="${pkgAccumVarVars[depHostOffset + 1]}"; + for depTargetOffset in "${allPlatOffsets[@]}"; + do + (( depHostOffset <= depTargetOffset )) || continue; + local hookRef="${hookVar}[$depTargetOffset - $depHostOffset]"; + if [[ -z "${strictDeps-}" ]]; then + local visitedPkgs=""; + for pkg in "${pkgsBuildBuild[@]}" "${pkgsBuildHost[@]}" "${pkgsBuildTarget[@]}" "${pkgsHostHost[@]}" "${pkgsHostTarget[@]}" "${pkgsTargetTarget[@]}"; + do + if [[ "$visitedPkgs" = *"$pkg"* ]]; then + continue; + fi; + runHook "${!hookRef}" "$pkg"; + visitedPkgs+=" $pkg"; + done; + else + local pkgsRef="${pkgsVar}[$depTargetOffset - $depHostOffset]"; + local pkgsSlice="${!pkgsRef}[@]"; + for pkg in ${!pkgsSlice+"${!pkgsSlice}"}; + do + runHook "${!hookRef}" "$pkg"; + done; + fi; + done; + done +} +_allFlags () +{ + + export system pname name version; + while IFS='' read -r varName; do + nixTalkativeLog "@${varName}@ -> ${!varName}"; + args+=("--subst-var" "$varName"); + done < <(awk 'BEGIN { for (v in ENVIRON) if (v ~ /^[a-z][a-zA-Z0-9_]*$/) print v }') +} +_assignFirst () +{ + + local varName="$1"; + local _var; + local REMOVE=REMOVE; + shift; + for _var in "$@"; + do + if [ -n "${!_var-}" ]; then + eval "${varName}"="${_var}"; + return; + fi; + done; + echo; + echo "error: _assignFirst: could not find a non-empty variable whose name to assign to ${varName}."; + echo " The following variables were all unset or empty:"; + echo " $*"; + if [ -z "${out:-}" ]; then + echo ' If you do not want an "out" output in your derivation, make sure to define'; + echo ' the other specific required outputs. This can be achieved by picking one'; + echo " of the above as an output."; + echo ' You do not have to remove "out" if you want to have a different default'; + echo ' output, because the first output is taken as a default.'; + echo; + fi; + return 1 +} +_callImplicitHook () +{ + + local def="$1"; + local hookName="$2"; + if declare -F "$hookName" > /dev/null; then + nixTalkativeLog "calling implicit '$hookName' function hook"; + "$hookName"; + else + if type -p "$hookName" > /dev/null; then + nixTalkativeLog "sourcing implicit '$hookName' script hook"; + source "$hookName"; + else + if [ -n "${!hookName:-}" ]; then + nixTalkativeLog "evaling implicit '$hookName' string hook"; + eval "${!hookName}"; + else + return "$def"; + fi; + fi; + fi +} +_defaultUnpack () +{ + + local fn="$1"; + local destination; + if [ -d "$fn" ]; then + destination="$(stripHash "$fn")"; + if [ -e "$destination" ]; then + echo "Cannot copy $fn to $destination: destination already exists!"; + echo "Did you specify two \"srcs\" with the same \"name\"?"; + return 1; + fi; + cp -r --preserve=timestamps --reflink=auto -- "$fn" "$destination"; + else + case "$fn" in + *.tar.xz | *.tar.lzma | *.txz) + ( XZ_OPT="--threads=$NIX_BUILD_CORES" xz -d < "$fn"; + true ) | tar xf - --mode=+w --warning=no-timestamp + ;; + *.tar | *.tar.* | *.tgz | *.tbz2 | *.tbz) + tar xf "$fn" --mode=+w --warning=no-timestamp + ;; + *) + return 1 + ;; + esac; + fi +} +_doStrip () +{ + + local -ra flags=(dontStripHost dontStripTarget); + local -ra debugDirs=(stripDebugList stripDebugListTarget); + local -ra allDirs=(stripAllList stripAllListTarget); + local -ra stripCmds=(STRIP STRIP_FOR_TARGET); + local -ra ranlibCmds=(RANLIB RANLIB_FOR_TARGET); + stripDebugList=${stripDebugList[*]:-lib lib32 lib64 libexec bin sbin Applications Library/Frameworks}; + stripDebugListTarget=${stripDebugListTarget[*]:-}; + stripAllList=${stripAllList[*]:-}; + stripAllListTarget=${stripAllListTarget[*]:-}; + local i; + for i in ${!stripCmds[@]}; + do + local -n flag="${flags[$i]}"; + local -n debugDirList="${debugDirs[$i]}"; + local -n allDirList="${allDirs[$i]}"; + local -n stripCmd="${stripCmds[$i]}"; + local -n ranlibCmd="${ranlibCmds[$i]}"; + if [[ -n "${dontStrip-}" || -n "${flag-}" ]] || ! type -f "${stripCmd-}" 2> /dev/null 1>&2; then + continue; + fi; + stripDirs "$stripCmd" "$ranlibCmd" "$debugDirList" "${stripDebugFlags[*]:--S -p}"; + stripDirs "$stripCmd" "$ranlibCmd" "$allDirList" "${stripAllFlags[*]:--s -p}"; + done +} +_eval () +{ + + if declare -F "$1" > /dev/null 2>&1; then + "$@"; + else + eval "$1"; + fi +} +_logHook () +{ + + if [[ -z ${NIX_LOG_FD-} ]]; then + return; + fi; + local hookKind="$1"; + local hookExpr="$2"; + shift 2; + if declare -F "$hookExpr" > /dev/null 2>&1; then + nixTalkativeLog "calling '$hookKind' function hook '$hookExpr'" "$@"; + else + if type -p "$hookExpr" > /dev/null; then + nixTalkativeLog "sourcing '$hookKind' script hook '$hookExpr'"; + else + if [[ "$hookExpr" != "_callImplicitHook"* ]]; then + local exprToOutput; + if [[ ${NIX_DEBUG:-0} -ge 5 ]]; then + exprToOutput="$hookExpr"; + else + local hookExprLine; + while IFS= read -r hookExprLine; do + hookExprLine="${hookExprLine#"${hookExprLine%%[![:space:]]*}"}"; + if [[ -n "$hookExprLine" ]]; then + exprToOutput+="$hookExprLine\\n "; + fi; + done <<< "$hookExpr"; + exprToOutput="${exprToOutput%%\\n }"; + fi; + nixTalkativeLog "evaling '$hookKind' string hook '$exprToOutput'"; + fi; + fi; + fi +} +_makeSymlinksRelative () +{ + + local prefixes; + prefixes=(); + for output in $(getAllOutputNames); + do + [ ! -e "${!output}" ] && continue; + prefixes+=("${!output}"); + done; + find "${prefixes[@]}" -type l -printf '%H\0%p\0' | xargs -0 -n2 -r -P "$NIX_BUILD_CORES" sh -c ' + output="$1" + link="$2" + + linkTarget=$(readlink "$link") + + # only touch links that point inside the same output tree + [[ $linkTarget == "$output"/* ]] || exit 0 + + if [ ! -e "$linkTarget" ]; then + echo "the symlink $link is broken, it points to $linkTarget (which is missing)" + fi + + echo "making symlink relative: $link" + ln -snrf "$linkTarget" "$link" + ' _ +} +_moveLib64 () +{ + + if [ "${dontMoveLib64-}" = 1 ]; then + return; + fi; + if [ ! -e "$prefix/lib64" -o -L "$prefix/lib64" ]; then + return; + fi; + echo "moving $prefix/lib64/* to $prefix/lib"; + mkdir -p $prefix/lib; + shopt -s dotglob; + for i in $prefix/lib64/*; + do + mv --no-clobber "$i" $prefix/lib; + done; + shopt -u dotglob; + rmdir $prefix/lib64; + ln -s lib $prefix/lib64 +} +_moveSbin () +{ + + if [ "${dontMoveSbin-}" = 1 ]; then + return; + fi; + if [ ! -e "$prefix/sbin" -o -L "$prefix/sbin" ]; then + return; + fi; + echo "moving $prefix/sbin/* to $prefix/bin"; + mkdir -p $prefix/bin; + shopt -s dotglob; + for i in $prefix/sbin/*; + do + mv "$i" $prefix/bin; + done; + shopt -u dotglob; + rmdir $prefix/sbin; + ln -s bin $prefix/sbin +} +_moveSystemdUserUnits () +{ + + if [ "${dontMoveSystemdUserUnits:-0}" = 1 ]; then + return; + fi; + if [ ! -e "${prefix:?}/lib/systemd/user" ]; then + return; + fi; + local source="$prefix/lib/systemd/user"; + local target="$prefix/share/systemd/user"; + echo "moving $source/* to $target"; + mkdir -p "$target"; + ( shopt -s dotglob; + for i in "$source"/*; + do + mv "$i" "$target"; + done ); + rmdir "$source"; + ln -s "$target" "$source" +} +_moveToShare () +{ + + if [ -n "$__structuredAttrs" ]; then + if [ -z "${forceShare-}" ]; then + forceShare=(man doc info); + fi; + else + forceShare=(${forceShare:-man doc info}); + fi; + if [[ -z "$out" ]]; then + return; + fi; + for d in "${forceShare[@]}"; + do + if [ -d "$out/$d" ]; then + if [ -d "$out/share/$d" ]; then + echo "both $d/ and share/$d/ exist!"; + else + echo "moving $out/$d to $out/share/$d"; + mkdir -p $out/share; + mv $out/$d $out/share/; + fi; + fi; + done +} +_multioutConfig () +{ + + if [ "$(getAllOutputNames)" = "out" ] || [ -z "${setOutputFlags-1}" ]; then + return; + fi; + if [ -z "${shareDocName:-}" ]; then + local confScript="${configureScript:-}"; + if [ -z "$confScript" ] && [ -x ./configure ]; then + confScript=./configure; + fi; + if [ -f "$confScript" ]; then + local shareDocName="$(sed -n "s/^PACKAGE_TARNAME='\(.*\)'$/\1/p" < "$confScript")"; + fi; + if [ -z "$shareDocName" ] || echo "$shareDocName" | grep -q '[^a-zA-Z0-9_-]'; then + shareDocName="$(echo "$name" | sed 's/-[^a-zA-Z].*//')"; + fi; + fi; + prependToVar configureFlags --bindir="${!outputBin}"/bin --sbindir="${!outputBin}"/sbin --includedir="${!outputInclude}"/include --mandir="${!outputMan}"/share/man --infodir="${!outputInfo}"/share/info --docdir="${!outputDoc}"/share/doc/"${shareDocName}" --libdir="${!outputLib}"/lib --libexecdir="${!outputLib}"/libexec --localedir="${!outputLib}"/share/locale; + prependToVar installFlags pkgconfigdir="${!outputDev}"/lib/pkgconfig m4datadir="${!outputDev}"/share/aclocal aclocaldir="${!outputDev}"/share/aclocal +} +_multioutDevs () +{ + + if [ "$(getAllOutputNames)" = "out" ] || [ -z "${moveToDev-1}" ]; then + return; + fi; + moveToOutput include "${!outputInclude}"; + moveToOutput lib/pkgconfig "${!outputDev}"; + moveToOutput share/pkgconfig "${!outputDev}"; + moveToOutput lib/cmake "${!outputDev}"; + moveToOutput share/aclocal "${!outputDev}"; + for f in "${!outputDev}"/{lib,share}/pkgconfig/*.pc; + do + echo "Patching '$f' includedir to output ${!outputInclude}"; + sed -i "/^includedir=/s,=\${prefix},=${!outputInclude}," "$f"; + done +} +_multioutDocs () +{ + + local REMOVE=REMOVE; + moveToOutput share/info "${!outputInfo}"; + moveToOutput share/doc "${!outputDoc}"; + moveToOutput share/gtk-doc "${!outputDevdoc}"; + moveToOutput share/devhelp/books "${!outputDevdoc}"; + moveToOutput share/man "${!outputMan}"; + moveToOutput share/man/man3 "${!outputDevman}" +} +_multioutPropagateDev () +{ + + if [ "$(getAllOutputNames)" = "out" ]; then + return; + fi; + local outputFirst; + for outputFirst in $(getAllOutputNames); + do + break; + done; + local propagaterOutput="$outputDev"; + if [ -z "$propagaterOutput" ]; then + propagaterOutput="$outputFirst"; + fi; + if [ -z "${propagatedBuildOutputs+1}" ]; then + local po_dirty="$outputBin $outputInclude $outputLib"; + set +o pipefail; + propagatedBuildOutputs=`echo "$po_dirty" | tr -s ' ' '\n' | grep -v -F "$propagaterOutput" | sort -u | tr '\n' ' ' `; + set -o pipefail; + fi; + if [ -z "$propagatedBuildOutputs" ]; then + return; + fi; + mkdir -p "${!propagaterOutput}"/nix-support; + for output in $propagatedBuildOutputs; + do + echo -n " ${!output}" >> "${!propagaterOutput}"/nix-support/propagated-build-inputs; + done +} +_nixLogWithLevel () +{ + + [[ -z ${NIX_LOG_FD-} || ${NIX_DEBUG:-0} -lt ${1:?} ]] && return 0; + local logLevel; + case "${1:?}" in + 0) + logLevel=ERROR + ;; + 1) + logLevel=WARN + ;; + 2) + logLevel=NOTICE + ;; + 3) + logLevel=INFO + ;; + 4) + logLevel=TALKATIVE + ;; + 5) + logLevel=CHATTY + ;; + 6) + logLevel=DEBUG + ;; + 7) + logLevel=VOMIT + ;; + *) + echo "_nixLogWithLevel: called with invalid log level: ${1:?}" >&"$NIX_LOG_FD"; + return 1 + ;; + esac; + local callerName="${FUNCNAME[2]}"; + if [[ $callerName == "_callImplicitHook" ]]; then + callerName="${hookName:?}"; + fi; + printf "%s: %s: %s\n" "$logLevel" "$callerName" "${2:?}" >&"$NIX_LOG_FD" +} +_overrideFirst () +{ + + if [ -z "${!1-}" ]; then + _assignFirst "$@"; + fi +} +_pruneLibtoolFiles () +{ + + if [ "${dontPruneLibtoolFiles-}" ] || [ ! -e "$prefix" ]; then + return; + fi; + find "$prefix" -type f -name '*.la' -exec grep -q '^# Generated by .*libtool' {} \; -exec grep -q "^old_library=''" {} \; -exec sed -i {} -e "/^dependency_libs='[^']/ c dependency_libs='' #pruned" \; +} +_updateSourceDateEpochFromSourceRoot () +{ + + if [ -n "$sourceRoot" ]; then + updateSourceDateEpoch "$sourceRoot"; + fi +} +activatePackage () +{ + + local pkg="$1"; + local -r hostOffset="$2"; + local -r targetOffset="$3"; + (( hostOffset <= targetOffset )) || exit 1; + if [ -f "$pkg" ]; then + nixTalkativeLog "sourcing setup hook '$pkg'"; + source "$pkg"; + fi; + if [[ -z "${strictDeps-}" || "$hostOffset" -le -1 ]]; then + addToSearchPath _PATH "$pkg/bin"; + fi; + if (( hostOffset <= -1 )); then + addToSearchPath _XDG_DATA_DIRS "$pkg/share"; + fi; + if [[ "$hostOffset" -eq 0 && -d "$pkg/bin" ]]; then + addToSearchPath _HOST_PATH "$pkg/bin"; + fi; + if [[ -f "$pkg/nix-support/setup-hook" ]]; then + nixTalkativeLog "sourcing setup hook '$pkg/nix-support/setup-hook'"; + source "$pkg/nix-support/setup-hook"; + fi +} +addEnvHooks () +{ + + local depHostOffset="$1"; + shift; + local pkgHookVarsSlice="${pkgHookVarVars[$depHostOffset + 1]}[@]"; + local pkgHookVar; + for pkgHookVar in "${!pkgHookVarsSlice}"; + do + eval "${pkgHookVar}s"'+=("$@")'; + done +} +addToSearchPath () +{ + + addToSearchPathWithCustomDelimiter ":" "$@" +} +addToSearchPathWithCustomDelimiter () +{ + + local delimiter="$1"; + local varName="$2"; + local dir="$3"; + if [[ -d "$dir" && "${!varName:+${delimiter}${!varName}${delimiter}}" != *"${delimiter}${dir}${delimiter}"* ]]; then + export "${varName}=${!varName:+${!varName}${delimiter}}${dir}"; + fi +} +appendToVar () +{ + + local -n nameref="$1"; + local useArray type; + if [ -n "$__structuredAttrs" ]; then + useArray=true; + else + useArray=false; + fi; + if type=$(declare -p "$1" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "appendToVar(): ERROR: trying to use appendToVar on an associative array, use variable+=([\"X\"]=\"Y\") instead." 1>&2; + return 1 + ;; + -a*) + useArray=true + ;; + *) + useArray=false + ;; + esac; + fi; + shift; + if $useArray; then + nameref=(${nameref+"${nameref[@]}"} "$@"); + else + nameref="${nameref-} $*"; + fi +} +auditTmpdir () +{ + + local dir="$1"; + [ -e "$dir" ] || return 0; + echo "checking for references to $TMPDIR/ in $dir..."; + local tmpdir elf_fifo script_fifo; + tmpdir="$(mktemp -d)"; + elf_fifo="$tmpdir/elf"; + script_fifo="$tmpdir/script"; + mkfifo "$elf_fifo" "$script_fifo"; + ( find "$dir" -type f -not -path '*/.build-id/*' -print0 | while IFS= read -r -d '' file; do + if isELF "$file"; then + printf '%s\0' "$file" 1>&3; + else + if isScript "$file"; then + filename=${file##*/}; + dir=${file%/*}; + if [ -e "$dir/.$filename-wrapped" ]; then + printf '%s\0' "$file" 1>&4; + fi; + fi; + fi; + done; + exec 3>&- 4>&- ) 3> "$elf_fifo" 4> "$script_fifo" & ( xargs -0 -r -P "$NIX_BUILD_CORES" -n 1 sh -c ' + if { printf :; patchelf --print-rpath "$1"; } | grep -q -F ":$TMPDIR/"; then + echo "RPATH of binary $1 contains a forbidden reference to $TMPDIR/" + exit 1 + fi + ' _ < "$elf_fifo" ) & local pid_elf=$!; + local pid_script; + ( xargs -0 -r -P "$NIX_BUILD_CORES" -n 1 sh -c ' + if grep -q -F "$TMPDIR/" "$1"; then + echo "wrapper script $1 contains a forbidden reference to $TMPDIR/" + exit 1 + fi + ' _ < "$script_fifo" ) & local pid_script=$!; + wait "$pid_elf" || { + echo "Some binaries contain forbidden references to $TMPDIR/. Check the error above!"; + exit 1 + }; + wait "$pid_script" || { + echo "Some scripts contain forbidden references to $TMPDIR/. Check the error above!"; + exit 1 + }; + rm -r "$tmpdir" +} +bintoolsWrapper_addLDVars () +{ + + local role_post; + getHostRoleEnvHook; + if [[ -d "$1/lib64" && ! -L "$1/lib64" ]]; then + export NIX_LDFLAGS${role_post}+=" -L$1/lib64"; + fi; + if [[ -d "$1/lib" ]]; then + local -a glob=($1/lib/lib*); + if [ "${#glob[*]}" -gt 0 ]; then + export NIX_LDFLAGS${role_post}+=" -L$1/lib"; + fi; + fi +} +buildPhase () +{ + + runHook preBuild; + if [[ -z "${makeFlags-}" && -z "${makefile:-}" && ! ( -e Makefile || -e makefile || -e GNUmakefile ) ]]; then + echo "no Makefile or custom buildPhase, doing nothing"; + else + foundMakefile=1; + local flagsArray=(${enableParallelBuilding:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray buildFlags buildFlagsArray; + echoCmd 'build flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + runHook postBuild +} +ccWrapper_addCVars () +{ + + local role_post; + getHostRoleEnvHook; + local found=; + if [ -d "$1/include" ]; then + export NIX_CFLAGS_COMPILE${role_post}+=" -isystem $1/include"; + found=1; + fi; + if [ -d "$1/Library/Frameworks" ]; then + export NIX_CFLAGS_COMPILE${role_post}+=" -iframework $1/Library/Frameworks"; + found=1; + fi; + if [[ -n "1" && -n ${NIX_STORE:-} && -n $found ]]; then + local scrubbed="$NIX_STORE/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${1#"$NIX_STORE"/*-}"; + export NIX_CFLAGS_COMPILE${role_post}+=" -fmacro-prefix-map=$1=$scrubbed"; + fi +} +checkPhase () +{ + + runHook preCheck; + if [[ -z "${foundMakefile:-}" ]]; then + echo "no Makefile or custom checkPhase, doing nothing"; + runHook postCheck; + return; + fi; + if [[ -z "${checkTarget:-}" ]]; then + if make -n ${makefile:+-f $makefile} check > /dev/null 2>&1; then + checkTarget="check"; + else + if make -n ${makefile:+-f $makefile} test > /dev/null 2>&1; then + checkTarget="test"; + fi; + fi; + fi; + if [[ -z "${checkTarget:-}" ]]; then + echo "no check/test target in ${makefile:-Makefile}, doing nothing"; + else + local flagsArray=(${enableParallelChecking:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray checkFlags=VERBOSE=y checkFlagsArray checkTarget; + echoCmd 'check flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + runHook postCheck +} +compressManPages () +{ + + local dir="$1"; + if [ -L "$dir"/share ] || [ -L "$dir"/share/man ] || [ ! -d "$dir/share/man" ]; then + return; + fi; + echo "gzipping man pages under $dir/share/man/"; + find "$dir"/share/man/ -type f -a '!' -regex '.*\.\(bz2\|gz\|xz\)$' -print0 | xargs -0 -n1 -P "$NIX_BUILD_CORES" gzip -n -f; + find "$dir"/share/man/ -type l -a '!' -regex '.*\.\(bz2\|gz\|xz\)$' -print0 | sort -z | while IFS= read -r -d '' f; do + local target; + target="$(readlink -f "$f")"; + if [ -f "$target".gz ]; then + ln -sf "$target".gz "$f".gz && rm "$f"; + fi; + done +} +concatStringsSep () +{ + + local sep="$1"; + local name="$2"; + local type oldifs; + if type=$(declare -p "$name" 2> /dev/null); then + local -n nameref="$name"; + case "${type#* }" in + -A*) + echo "concatStringsSep(): ERROR: trying to use concatStringsSep on an associative array." 1>&2; + return 1 + ;; + -a*) + local IFS="$(printf '\036')" + ;; + *) + local IFS=" " + ;; + esac; + local ifs_separated="${nameref[*]}"; + echo -n "${ifs_separated//"$IFS"/"$sep"}"; + fi +} +concatTo () +{ + + local -; + set -o noglob; + local -n targetref="$1"; + shift; + local arg default name type; + for arg in "$@"; + do + IFS="=" read -r name default <<< "$arg"; + local -n nameref="$name"; + if [[ -z "${nameref[*]}" && -n "$default" ]]; then + targetref+=("$default"); + else + if type=$(declare -p "$name" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "concatTo(): ERROR: trying to use concatTo on an associative array." 1>&2; + return 1 + ;; + -a*) + targetref+=("${nameref[@]}") + ;; + *) + if [[ "$name" = *"Array" ]]; then + nixErrorLog "concatTo(): $name is not declared as array, treating as a singleton. This will become an error in future"; + targetref+=(${nameref+"${nameref[@]}"}); + else + targetref+=(${nameref-}); + fi + ;; + esac; + fi; + fi; + done +} +configurePhase () +{ + + runHook preConfigure; + : "${configureScript=}"; + if [[ -z "$configureScript" && -x ./configure ]]; then + configureScript=./configure; + fi; + if [ -z "${dontFixLibtool:-}" ]; then + export lt_cv_deplibs_check_method="${lt_cv_deplibs_check_method-pass_all}"; + local i; + find . -iname "ltmain.sh" -print0 | while IFS='' read -r -d '' i; do + echo "fixing libtool script $i"; + fixLibtool "$i"; + done; + CONFIGURE_MTIME_REFERENCE=$(mktemp configure.mtime.reference.XXXXXX); + find . -executable -type f -name configure -exec grep -l 'GNU Libtool is free software; you can redistribute it and/or modify' {} \; -exec touch -r {} "$CONFIGURE_MTIME_REFERENCE" \; -exec sed -i s_/usr/bin/file_file_g {} \; -exec touch -r "$CONFIGURE_MTIME_REFERENCE" {} \;; + rm -f "$CONFIGURE_MTIME_REFERENCE"; + fi; + if [[ -z "${dontAddPrefix:-}" && -n "$prefix" ]]; then + local -r prefixKeyOrDefault="${prefixKey:---prefix=}"; + if [ "${prefixKeyOrDefault: -1}" = " " ]; then + prependToVar configureFlags "$prefix"; + prependToVar configureFlags "${prefixKeyOrDefault::-1}"; + else + prependToVar configureFlags "$prefixKeyOrDefault$prefix"; + fi; + fi; + if [[ -f "$configureScript" ]]; then + if [ -z "${dontAddDisableDepTrack:-}" ]; then + if grep -q dependency-tracking "$configureScript"; then + prependToVar configureFlags --disable-dependency-tracking; + fi; + fi; + if [ -z "${dontDisableStatic:-}" ]; then + if grep -q enable-static "$configureScript"; then + prependToVar configureFlags --disable-static; + fi; + fi; + if [ -z "${dontPatchShebangsInConfigure:-}" ]; then + patchShebangs --build "$configureScript"; + fi; + fi; + if [ -n "$configureScript" ]; then + local -a flagsArray; + concatTo flagsArray configureFlags configureFlagsArray; + echoCmd 'configure flags' "${flagsArray[@]}"; + $configureScript "${flagsArray[@]}"; + unset flagsArray; + else + echo "no configure script, doing nothing"; + fi; + runHook postConfigure +} +consumeEntire () +{ + + if IFS='' read -r -d '' "$1"; then + echo "consumeEntire(): ERROR: Input null bytes, won't process" 1>&2; + return 1; + fi +} +definePhases () +{ + + if [ -z "${phases[*]:-}" ]; then + phases="${prePhases[*]:-} unpackPhase patchPhase ${preConfigurePhases[*]:-} configurePhase ${preBuildPhases[*]:-} buildPhase checkPhase ${preInstallPhases[*]:-} installPhase ${preFixupPhases[*]:-} fixupPhase installCheckPhase ${preDistPhases[*]:-} distPhase ${postPhases[*]:-}"; + fi +} +distPhase () +{ + + runHook preDist; + local flagsArray=(); + concatTo flagsArray distFlags distFlagsArray distTarget=dist; + echo 'dist flags: %q' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + if [ "${dontCopyDist:-0}" != 1 ]; then + mkdir -p "$out/tarballs"; + cp -pvd ${tarballs[*]:-*.tar.gz} "$out/tarballs"; + fi; + runHook postDist +} +dumpVars () +{ + + if [[ "${noDumpEnvVars:-0}" != 1 && -d "$NIX_BUILD_TOP" ]]; then + local old_umask; + old_umask=$(umask); + umask 0077; + export 2> /dev/null > "$NIX_BUILD_TOP/env-vars"; + umask "$old_umask"; + fi +} +echoCmd () +{ + + printf "%s:" "$1"; + shift; + printf ' %q' "$@"; + echo +} +exitHandler () +{ + + exitCode="$?"; + set +e; + if [ -n "${showBuildStats:-}" ]; then + read -r -d '' -a buildTimes < <(times); + echo "build times:"; + echo "user time for the shell ${buildTimes[0]}"; + echo "system time for the shell ${buildTimes[1]}"; + echo "user time for all child processes ${buildTimes[2]}"; + echo "system time for all child processes ${buildTimes[3]}"; + fi; + if (( "$exitCode" != 0 )); then + runHook failureHook; + if [ -n "${succeedOnFailure:-}" ]; then + echo "build failed with exit code $exitCode (ignored)"; + mkdir -p "$out/nix-support"; + printf "%s" "$exitCode" > "$out/nix-support/failed"; + exit 0; + fi; + else + runHook exitHook; + fi; + return "$exitCode" +} +findInputs () +{ + + local -r pkg="$1"; + local -r hostOffset="$2"; + local -r targetOffset="$3"; + (( hostOffset <= targetOffset )) || exit 1; + local varVar="${pkgAccumVarVars[hostOffset + 1]}"; + local varRef="$varVar[$((targetOffset - hostOffset))]"; + local var="${!varRef}"; + unset -v varVar varRef; + local varSlice="$var[*]"; + case " ${!varSlice-} " in + *" $pkg "*) + return 0 + ;; + esac; + unset -v varSlice; + eval "$var"'+=("$pkg")'; + if ! [ -e "$pkg" ]; then + echo "build input $pkg does not exist" 1>&2; + exit 1; + fi; + function mapOffset () + { + local -r inputOffset="$1"; + local -n outputOffset="$2"; + if (( inputOffset <= 0 )); then + outputOffset=$((inputOffset + hostOffset)); + else + outputOffset=$((inputOffset - 1 + targetOffset)); + fi + }; + local relHostOffset; + for relHostOffset in "${allPlatOffsets[@]}"; + do + local files="${propagatedDepFilesVars[relHostOffset + 1]}"; + local hostOffsetNext; + mapOffset "$relHostOffset" hostOffsetNext; + (( -1 <= hostOffsetNext && hostOffsetNext <= 1 )) || continue; + local relTargetOffset; + for relTargetOffset in "${allPlatOffsets[@]}"; + do + (( "$relHostOffset" <= "$relTargetOffset" )) || continue; + local fileRef="${files}[$relTargetOffset - $relHostOffset]"; + local file="${!fileRef}"; + unset -v fileRef; + local targetOffsetNext; + mapOffset "$relTargetOffset" targetOffsetNext; + (( -1 <= hostOffsetNext && hostOffsetNext <= 1 )) || continue; + [[ -f "$pkg/nix-support/$file" ]] || continue; + local pkgNext; + read -r -d '' pkgNext < "$pkg/nix-support/$file" || true; + for pkgNext in $pkgNext; + do + findInputs "$pkgNext" "$hostOffsetNext" "$targetOffsetNext"; + done; + done; + done +} +fixLibtool () +{ + + local search_path; + for flag in $NIX_LDFLAGS; + do + case $flag in + -L*) + search_path+=" ${flag#-L}" + ;; + esac; + done; + sed -i "$1" -e "s^eval \(sys_lib_search_path=\).*^\1'${search_path:-}'^" -e 's^eval sys_lib_.+search_path=.*^^' +} +fixupPhase () +{ + + local output; + for output in $(getAllOutputNames); + do + if [ -e "${!output}" ]; then + chmod -R u+w,u-s,g-s "${!output}"; + fi; + done; + runHook preFixup; + local output; + for output in $(getAllOutputNames); + do + prefix="${!output}" runHook fixupOutput; + done; + recordPropagatedDependencies; + if [ -n "${setupHook:-}" ]; then + mkdir -p "${!outputDev}/nix-support"; + substituteAll "$setupHook" "${!outputDev}/nix-support/setup-hook"; + fi; + if [ -n "${setupHooks:-}" ]; then + mkdir -p "${!outputDev}/nix-support"; + local hook; + for hook in ${setupHooks[@]}; + do + local content; + consumeEntire content < "$hook"; + substituteAllStream content "file '$hook'" >> "${!outputDev}/nix-support/setup-hook"; + unset -v content; + done; + unset -v hook; + fi; + if [ -n "${propagatedUserEnvPkgs[*]:-}" ]; then + mkdir -p "${!outputBin}/nix-support"; + printWords "${propagatedUserEnvPkgs[@]}" > "${!outputBin}/nix-support/propagated-user-env-packages"; + fi; + runHook postFixup +} +genericBuild () +{ + + export GZIP_NO_TIMESTAMPS=1; + if [ -f "${buildCommandPath:-}" ]; then + source "$buildCommandPath"; + return; + fi; + if [ -n "${buildCommand:-}" ]; then + eval "$buildCommand"; + return; + fi; + definePhases; + for curPhase in ${phases[*]}; + do + runPhase "$curPhase"; + done +} +getAllOutputNames () +{ + + if [ -n "$__structuredAttrs" ]; then + echo "${!outputs[*]}"; + else + echo "$outputs"; + fi +} +getHostRole () +{ + + getRole "$hostOffset" +} +getHostRoleEnvHook () +{ + + getRole "$depHostOffset" +} +getRole () +{ + + case $1 in + -1) + role_post='_FOR_BUILD' + ;; + 0) + role_post='' + ;; + 1) + role_post='_FOR_TARGET' + ;; + *) + echo "apple-sdk-14.4: used as improper sort of dependency" 1>&2; + return 1 + ;; + esac +} +getTargetRole () +{ + + getRole "$targetOffset" +} +getTargetRoleEnvHook () +{ + + getRole "$depTargetOffset" +} +getTargetRoleWrapper () +{ + + case $targetOffset in + -1) + export NIX_@wrapperName@_TARGET_BUILD_@suffixSalt@=1 + ;; + 0) + export NIX_@wrapperName@_TARGET_HOST_@suffixSalt@=1 + ;; + 1) + export NIX_@wrapperName@_TARGET_TARGET_@suffixSalt@=1 + ;; + *) + echo "apple-sdk-14.4: used as improper sort of dependency" 1>&2; + return 1 + ;; + esac +} +installCheckPhase () +{ + + runHook preInstallCheck; + if [[ -z "${foundMakefile:-}" ]]; then + echo "no Makefile or custom installCheckPhase, doing nothing"; + else + if [[ -z "${installCheckTarget:-}" ]] && ! make -n ${makefile:+-f $makefile} "${installCheckTarget:-installcheck}" > /dev/null 2>&1; then + echo "no installcheck target in ${makefile:-Makefile}, doing nothing"; + else + local flagsArray=(${enableParallelChecking:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray installCheckFlags installCheckFlagsArray installCheckTarget=installcheck; + echoCmd 'installcheck flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + fi; + runHook postInstallCheck +} +installPhase () +{ + + runHook preInstall; + if [[ -z "${makeFlags-}" && -z "${makefile:-}" && ! ( -e Makefile || -e makefile || -e GNUmakefile ) ]]; then + echo "no Makefile or custom installPhase, doing nothing"; + runHook postInstall; + return; + else + foundMakefile=1; + fi; + if [ -n "$prefix" ]; then + mkdir -p "$prefix"; + fi; + local flagsArray=(${enableParallelInstalling:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray installFlags installFlagsArray installTargets=install; + echoCmd 'install flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + runHook postInstall +} +isELF () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 4 -u "$fd" magic; + exec {fd}>&-; + if [ "$magic" = 'ELF' ]; then + return 0; + else + return 1; + fi +} +isMachO () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 4 -u "$fd" magic; + exec {fd}>&-; + if [[ "$magic" = $(echo -ne "\xfe\xed\xfa\xcf") || "$magic" = $(echo -ne "\xcf\xfa\xed\xfe") ]]; then + return 0; + else + if [[ "$magic" = $(echo -ne "\xfe\xed\xfa\xce") || "$magic" = $(echo -ne "\xce\xfa\xed\xfe") ]]; then + return 0; + else + if [[ "$magic" = $(echo -ne "\xca\xfe\xba\xbe") || "$magic" = $(echo -ne "\xbe\xba\xfe\xca") ]]; then + return 0; + else + return 1; + fi; + fi; + fi +} +isScript () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 2 -u "$fd" magic; + exec {fd}>&-; + if [[ "$magic" =~ \#! ]]; then + return 0; + else + return 1; + fi +} +mapOffset () +{ + + local -r inputOffset="$1"; + local -n outputOffset="$2"; + if (( inputOffset <= 0 )); then + outputOffset=$((inputOffset + hostOffset)); + else + outputOffset=$((inputOffset - 1 + targetOffset)); + fi +} +moveToOutput () +{ + + local patt="$1"; + local dstOut="$2"; + local output; + for output in $(getAllOutputNames); + do + if [ "${!output}" = "$dstOut" ]; then + continue; + fi; + local srcPath; + for srcPath in "${!output}"/$patt; + do + if [ ! -e "$srcPath" ] && [ ! -L "$srcPath" ]; then + continue; + fi; + if [ "$dstOut" = REMOVE ]; then + echo "Removing $srcPath"; + rm -r "$srcPath"; + else + local dstPath="$dstOut${srcPath#${!output}}"; + echo "Moving $srcPath to $dstPath"; + if [ -d "$dstPath" ] && [ -d "$srcPath" ]; then + rmdir "$srcPath" --ignore-fail-on-non-empty; + if [ -d "$srcPath" ]; then + mv -t "$dstPath" "$srcPath"/*; + rmdir "$srcPath"; + fi; + else + mkdir -p "$(readlink -m "$dstPath/..")"; + mv "$srcPath" "$dstPath"; + fi; + fi; + local srcParent="$(readlink -m "$srcPath/..")"; + if [ -n "$(find "$srcParent" -maxdepth 0 -type d -empty 2> /dev/null)" ]; then + echo "Removing empty $srcParent/ and (possibly) its parents"; + rmdir -p --ignore-fail-on-non-empty "$srcParent" 2> /dev/null || true; + fi; + done; + done +} +nixChattyLog () +{ + + _nixLogWithLevel 5 "$*" +} +nixDebugLog () +{ + + _nixLogWithLevel 6 "$*" +} +nixErrorLog () +{ + + _nixLogWithLevel 0 "$*" +} +nixInfoLog () +{ + + _nixLogWithLevel 3 "$*" +} +nixLog () +{ + + [[ -z ${NIX_LOG_FD-} ]] && return 0; + local callerName="${FUNCNAME[1]}"; + if [[ $callerName == "_callImplicitHook" ]]; then + callerName="${hookName:?}"; + fi; + printf "%s: %s\n" "$callerName" "$*" >&"$NIX_LOG_FD" +} +nixNoticeLog () +{ + + _nixLogWithLevel 2 "$*" +} +nixTalkativeLog () +{ + + _nixLogWithLevel 4 "$*" +} +nixVomitLog () +{ + + _nixLogWithLevel 7 "$*" +} +nixWarnLog () +{ + + _nixLogWithLevel 1 "$*" +} +noBrokenSymlinks () +{ + + local -r output="${1:?}"; + local path; + local pathParent; + local symlinkTarget; + local -i numDanglingSymlinks=0; + local -i numReflexiveSymlinks=0; + local -i numUnreadableSymlinks=0; + if [[ ! -e $output ]]; then + nixWarnLog "skipping non-existent output $output"; + return 0; + fi; + nixInfoLog "running on $output"; + while IFS= read -r -d '' path; do + pathParent="$(dirname "$path")"; + if ! symlinkTarget="$(readlink "$path")"; then + nixErrorLog "the symlink $path is unreadable"; + numUnreadableSymlinks+=1; + continue; + fi; + if [[ $symlinkTarget == /* ]]; then + nixInfoLog "symlink $path points to absolute target $symlinkTarget"; + else + nixInfoLog "symlink $path points to relative target $symlinkTarget"; + symlinkTarget="$(realpath --no-symlinks --canonicalize-missing "$pathParent/$symlinkTarget")"; + fi; + if [[ $symlinkTarget = "$TMPDIR"/* ]]; then + nixErrorLog "the symlink $path points to $TMPDIR directory: $symlinkTarget"; + numDanglingSymlinks+=1; + continue; + fi; + if [[ $symlinkTarget != "$NIX_STORE"/* ]]; then + nixInfoLog "symlink $path points outside the Nix store; ignoring"; + continue; + fi; + if [[ $path == "$symlinkTarget" ]]; then + nixErrorLog "the symlink $path is reflexive"; + numReflexiveSymlinks+=1; + else + if [[ ! -e $symlinkTarget ]]; then + nixErrorLog "the symlink $path points to a missing target: $symlinkTarget"; + numDanglingSymlinks+=1; + else + nixDebugLog "the symlink $path is irreflexive and points to a target which exists"; + fi; + fi; + done < <(find "$output" -type l -print0); + if ((numDanglingSymlinks > 0 || numReflexiveSymlinks > 0 || numUnreadableSymlinks > 0)); then + nixErrorLog "found $numDanglingSymlinks dangling symlinks, $numReflexiveSymlinks reflexive symlinks and $numUnreadableSymlinks unreadable symlinks"; + exit 1; + fi; + return 0 +} +noBrokenSymlinksInAllOutputs () +{ + + if [[ -z ${dontCheckForBrokenSymlinks-} ]]; then + for output in $(getAllOutputNames); + do + noBrokenSymlinks "${!output}"; + done; + fi +} +patchPhase () +{ + + runHook prePatch; + local -a patchesArray; + concatTo patchesArray patches; + local -a flagsArray; + concatTo flagsArray patchFlags=-p1; + for i in "${patchesArray[@]}"; + do + echo "applying patch $i"; + local uncompress=cat; + case "$i" in + *.gz) + uncompress="gzip -d" + ;; + *.bz2) + uncompress="bzip2 -d" + ;; + *.xz) + uncompress="xz -d" + ;; + *.lzma) + uncompress="lzma -d" + ;; + esac; + $uncompress < "$i" 2>&1 | patch "${flagsArray[@]}"; + done; + runHook postPatch +} +patchShebangs () +{ + + local pathName; + local update=false; + while [[ $# -gt 0 ]]; do + case "$1" in + --host) + pathName=HOST_PATH; + shift + ;; + --build) + pathName=PATH; + shift + ;; + --update) + update=true; + shift + ;; + --) + shift; + break + ;; + -* | --*) + echo "Unknown option $1 supplied to patchShebangs" 1>&2; + return 1 + ;; + *) + break + ;; + esac; + done; + echo "patching script interpreter paths in $@"; + local f; + local oldPath; + local newPath; + local arg0; + local args; + local oldInterpreterLine; + local newInterpreterLine; + if [[ $# -eq 0 ]]; then + echo "No arguments supplied to patchShebangs" 1>&2; + return 0; + fi; + local f; + while IFS= read -r -d '' f; do + isScript "$f" || continue; + read -r oldInterpreterLine < "$f" || [ "$oldInterpreterLine" ]; + read -r oldPath arg0 args <<< "${oldInterpreterLine:2}"; + if [[ -z "${pathName:-}" ]]; then + if [[ -n $strictDeps && $f == "$NIX_STORE"* ]]; then + pathName=HOST_PATH; + else + pathName=PATH; + fi; + fi; + if [[ "$oldPath" == *"/bin/env" ]]; then + if [[ $arg0 == "-S" ]]; then + arg0=${args%% *}; + [[ "$args" == *" "* ]] && args=${args#* } || args=; + newPath="$(PATH="${!pathName}" type -P "env" || true)"; + args="-S $(PATH="${!pathName}" type -P "$arg0" || true) $args"; + else + if [[ $arg0 == "-"* || $arg0 == *"="* ]]; then + echo "$f: unsupported interpreter directive \"$oldInterpreterLine\" (set dontPatchShebangs=1 and handle shebang patching yourself)" 1>&2; + exit 1; + else + newPath="$(PATH="${!pathName}" type -P "$arg0" || true)"; + fi; + fi; + else + if [[ -z $oldPath ]]; then + oldPath="/bin/sh"; + fi; + newPath="$(PATH="${!pathName}" type -P "$(basename "$oldPath")" || true)"; + args="$arg0 $args"; + fi; + newInterpreterLine="$newPath $args"; + newInterpreterLine=${newInterpreterLine%${newInterpreterLine##*[![:space:]]}}; + if [[ -n "$oldPath" && ( "$update" == true || "${oldPath:0:${#NIX_STORE}}" != "$NIX_STORE" ) ]]; then + if [[ -n "$newPath" && "$newPath" != "$oldPath" ]]; then + echo "$f: interpreter directive changed from \"$oldInterpreterLine\" to \"$newInterpreterLine\""; + escapedInterpreterLine=${newInterpreterLine//\\/\\\\}; + timestamp=$(stat --printf "%y" "$f"); + tmpFile=$(mktemp -t patchShebangs.XXXXXXXXXX); + sed -e "1 s|.*|#\!$escapedInterpreterLine|" "$f" > "$tmpFile"; + local restoreReadOnly; + if [[ ! -w "$f" ]]; then + chmod +w "$f"; + restoreReadOnly=true; + fi; + cat "$tmpFile" > "$f"; + rm "$tmpFile"; + if [[ -n "${restoreReadOnly:-}" ]]; then + chmod -w "$f"; + fi; + touch --date "$timestamp" "$f"; + fi; + fi; + done < <(find "$@" -type f -perm -0100 -print0) +} +patchShebangsAuto () +{ + + if [[ -z "${dontPatchShebangs-}" && -e "$prefix" ]]; then + if [[ "$output" != out && "$output" = "$outputDev" ]]; then + patchShebangs --build "$prefix"; + else + patchShebangs --host "$prefix"; + fi; + fi +} +prependToVar () +{ + + local -n nameref="$1"; + local useArray type; + if [ -n "$__structuredAttrs" ]; then + useArray=true; + else + useArray=false; + fi; + if type=$(declare -p "$1" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "prependToVar(): ERROR: trying to use prependToVar on an associative array." 1>&2; + return 1 + ;; + -a*) + useArray=true + ;; + *) + useArray=false + ;; + esac; + fi; + shift; + if $useArray; then + nameref=("$@" ${nameref+"${nameref[@]}"}); + else + nameref="$* ${nameref-}"; + fi +} +printLines () +{ + + (( "$#" > 0 )) || return 0; + printf '%s\n' "$@" +} +printPhases () +{ + + definePhases; + local phase; + for phase in ${phases[*]}; + do + printf '%s\n' "$phase"; + done +} +printWords () +{ + + (( "$#" > 0 )) || return 0; + printf '%s ' "$@" +} +recordPropagatedDependencies () +{ + + declare -ra flatVars=(depsBuildBuildPropagated propagatedNativeBuildInputs depsBuildTargetPropagated depsHostHostPropagated propagatedBuildInputs depsTargetTargetPropagated); + declare -ra flatFiles=("${propagatedBuildDepFiles[@]}" "${propagatedHostDepFiles[@]}" "${propagatedTargetDepFiles[@]}"); + local propagatedInputsIndex; + for propagatedInputsIndex in "${!flatVars[@]}"; + do + local propagatedInputsSlice="${flatVars[$propagatedInputsIndex]}[@]"; + local propagatedInputsFile="${flatFiles[$propagatedInputsIndex]}"; + [[ -n "${!propagatedInputsSlice}" ]] || continue; + mkdir -p "${!outputDev}/nix-support"; + printWords ${!propagatedInputsSlice} > "${!outputDev}/nix-support/$propagatedInputsFile"; + done +} +runHook () +{ + + local hookName="$1"; + shift; + local hooksSlice="${hookName%Hook}Hooks[@]"; + local hook; + for hook in "_callImplicitHook 0 $hookName" ${!hooksSlice+"${!hooksSlice}"}; + do + _logHook "$hookName" "$hook" "$@"; + _eval "$hook" "$@"; + done; + return 0 +} +runOneHook () +{ + + local hookName="$1"; + shift; + local hooksSlice="${hookName%Hook}Hooks[@]"; + local hook ret=1; + for hook in "_callImplicitHook 1 $hookName" ${!hooksSlice+"${!hooksSlice}"}; + do + _logHook "$hookName" "$hook" "$@"; + if _eval "$hook" "$@"; then + ret=0; + break; + fi; + done; + return "$ret" +} +runPhase () +{ + + local curPhase="$*"; + if [[ "$curPhase" = unpackPhase && -n "${dontUnpack:-}" ]]; then + return; + fi; + if [[ "$curPhase" = patchPhase && -n "${dontPatch:-}" ]]; then + return; + fi; + if [[ "$curPhase" = configurePhase && -n "${dontConfigure:-}" ]]; then + return; + fi; + if [[ "$curPhase" = buildPhase && -n "${dontBuild:-}" ]]; then + return; + fi; + if [[ "$curPhase" = checkPhase && -z "${doCheck:-}" ]]; then + return; + fi; + if [[ "$curPhase" = installPhase && -n "${dontInstall:-}" ]]; then + return; + fi; + if [[ "$curPhase" = fixupPhase && -n "${dontFixup:-}" ]]; then + return; + fi; + if [[ "$curPhase" = installCheckPhase && -z "${doInstallCheck:-}" ]]; then + return; + fi; + if [[ "$curPhase" = distPhase && -z "${doDist:-}" ]]; then + return; + fi; + showPhaseHeader "$curPhase"; + dumpVars; + local startTime endTime; + startTime=$(date +"%s"); + eval "${!curPhase:-$curPhase}"; + endTime=$(date +"%s"); + showPhaseFooter "$curPhase" "$startTime" "$endTime"; + if [ "$curPhase" = unpackPhase ]; then + [ -n "${sourceRoot:-}" ] && chmod +x -- "${sourceRoot}"; + cd -- "${sourceRoot:-.}"; + fi +} +showPhaseFooter () +{ + + local phase="$1"; + local startTime="$2"; + local endTime="$3"; + local delta=$(( endTime - startTime )); + (( delta < 30 )) && return; + local H=$((delta/3600)); + local M=$((delta%3600/60)); + local S=$((delta%60)); + echo -n "$phase completed in "; + (( H > 0 )) && echo -n "$H hours "; + (( M > 0 )) && echo -n "$M minutes "; + echo "$S seconds" +} +showPhaseHeader () +{ + + local phase="$1"; + echo "Running phase: $phase"; + if [[ -z ${NIX_LOG_FD-} ]]; then + return; + fi; + printf "@nix { \"action\": \"setPhase\", \"phase\": \"%s\" }\n" "$phase" >&"$NIX_LOG_FD" +} +stripDirs () +{ + + local cmd="$1"; + local ranlibCmd="$2"; + local paths="$3"; + local stripFlags="$4"; + local excludeFlags=(); + local pathsNew=; + [ -z "$cmd" ] && echo "stripDirs: Strip command is empty" 1>&2 && exit 1; + [ -z "$ranlibCmd" ] && echo "stripDirs: Ranlib command is empty" 1>&2 && exit 1; + local pattern; + if [ -n "${stripExclude:-}" ]; then + for pattern in "${stripExclude[@]}"; + do + excludeFlags+=(-a '!' '(' -name "$pattern" -o -wholename "$prefix/$pattern" ')'); + done; + fi; + local p; + for p in ${paths}; + do + if [ -e "$prefix/$p" ]; then + pathsNew="${pathsNew} $prefix/$p"; + fi; + done; + paths=${pathsNew}; + if [ -n "${paths}" ]; then + echo "stripping (with command $cmd and flags $stripFlags) in $paths"; + local striperr; + striperr="$(mktemp --tmpdir="$TMPDIR" 'striperr.XXXXXX')"; + find $paths -type f "${excludeFlags[@]}" -a '!' -path "$prefix/lib/debug/*" -printf '%D-%i,%p\0' | sort -t, -k1,1 -u -z | cut -d, -f2- -z | xargs -r -0 -n1 -P "$NIX_BUILD_CORES" -- $cmd $stripFlags 2> "$striperr" || exit_code=$?; + [[ "$exit_code" = 123 || -z "$exit_code" ]] || ( cat "$striperr" 1>&2 && exit 1 ); + rm "$striperr"; + find $paths -name '*.a' -type f -exec $ranlibCmd '{}' \; 2> /dev/null; + fi +} +stripHash () +{ + + local strippedName casematchOpt=0; + strippedName="$(basename -- "$1")"; + shopt -q nocasematch && casematchOpt=1; + shopt -u nocasematch; + if [[ "$strippedName" =~ ^[a-z0-9]{32}- ]]; then + echo "${strippedName:33}"; + else + echo "$strippedName"; + fi; + if (( casematchOpt )); then + shopt -s nocasematch; + fi +} +substitute () +{ + + local input="$1"; + local output="$2"; + shift 2; + if [ ! -f "$input" ]; then + echo "substitute(): ERROR: file '$input' does not exist" 1>&2; + return 1; + fi; + local content; + consumeEntire content < "$input"; + if [ -e "$output" ]; then + chmod +w "$output"; + fi; + substituteStream content "file '$input'" "$@" > "$output" +} +substituteAll () +{ + + local input="$1"; + local output="$2"; + local -a args=(); + _allFlags; + substitute "$input" "$output" "${args[@]}" +} +substituteAllInPlace () +{ + + local fileName="$1"; + shift; + substituteAll "$fileName" "$fileName" "$@" +} +substituteAllStream () +{ + + local -a args=(); + _allFlags; + substituteStream "$1" "$2" "${args[@]}" +} +substituteInPlace () +{ + + local -a fileNames=(); + for arg in "$@"; + do + if [[ "$arg" = "--"* ]]; then + break; + fi; + fileNames+=("$arg"); + shift; + done; + if ! [[ "${#fileNames[@]}" -gt 0 ]]; then + echo "substituteInPlace called without any files to operate on (files must come before options!)" 1>&2; + return 1; + fi; + for file in "${fileNames[@]}"; + do + substitute "$file" "$file" "$@"; + done +} +substituteStream () +{ + + local var=$1; + local description=$2; + shift 2; + while (( "$#" )); do + local replace_mode="$1"; + case "$1" in + --replace) + if ! "$_substituteStream_has_warned_replace_deprecation"; then + echo "substituteStream() in derivation $name: WARNING: '--replace' is deprecated, use --replace-{fail,warn,quiet}. ($description)" 1>&2; + _substituteStream_has_warned_replace_deprecation=true; + fi; + replace_mode='--replace-warn' + ;& + --replace-quiet | --replace-warn | --replace-fail) + pattern="$2"; + replacement="$3"; + shift 3; + if ! [[ "${!var}" == *"$pattern"* ]]; then + if [ "$replace_mode" == --replace-warn ]; then + printf "substituteStream() in derivation $name: WARNING: pattern %q doesn't match anything in %s\n" "$pattern" "$description" 1>&2; + else + if [ "$replace_mode" == --replace-fail ]; then + printf "substituteStream() in derivation $name: ERROR: pattern %q doesn't match anything in %s\n" "$pattern" "$description" 1>&2; + return 1; + fi; + fi; + fi; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}' + ;; + --subst-var) + local varName="$2"; + shift 2; + if ! [[ "$varName" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + echo "substituteStream() in derivation $name: ERROR: substitution variables must be valid Bash names, \"$varName\" isn't." 1>&2; + return 1; + fi; + if [ -z ${!varName+x} ]; then + echo "substituteStream() in derivation $name: ERROR: variable \$$varName is unset" 1>&2; + return 1; + fi; + pattern="@$varName@"; + replacement="${!varName}"; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}' + ;; + --subst-var-by) + pattern="@$2@"; + replacement="$3"; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}'; + shift 3 + ;; + *) + echo "substituteStream() in derivation $name: ERROR: Invalid command line argument: $1" 1>&2; + return 1 + ;; + esac; + done; + printf "%s" "${!var}" +} +unpackFile () +{ + + curSrc="$1"; + echo "unpacking source archive $curSrc"; + if ! runOneHook unpackCmd "$curSrc"; then + echo "do not know how to unpack source archive $curSrc"; + exit 1; + fi +} +unpackPhase () +{ + + runHook preUnpack; + if [ -z "${srcs:-}" ]; then + if [ -z "${src:-}" ]; then + echo 'variable $src or $srcs should point to the source'; + exit 1; + fi; + srcs="$src"; + fi; + local -a srcsArray; + concatTo srcsArray srcs; + local dirsBefore=""; + for i in *; + do + if [ -d "$i" ]; then + dirsBefore="$dirsBefore $i "; + fi; + done; + for i in "${srcsArray[@]}"; + do + unpackFile "$i"; + done; + : "${sourceRoot=}"; + if [ -n "${setSourceRoot:-}" ]; then + runOneHook setSourceRoot; + else + if [ -z "$sourceRoot" ]; then + for i in *; + do + if [ -d "$i" ]; then + case $dirsBefore in + *\ $i\ *) + + ;; + *) + if [ -n "$sourceRoot" ]; then + echo "unpacker produced multiple directories"; + exit 1; + fi; + sourceRoot="$i" + ;; + esac; + fi; + done; + fi; + fi; + if [ -z "$sourceRoot" ]; then + echo "unpacker appears to have produced no directories"; + exit 1; + fi; + echo "source root is $sourceRoot"; + if [ "${dontMakeSourcesWritable:-0}" != 1 ]; then + chmod -R u+w -- "$sourceRoot"; + fi; + runHook postUnpack +} +updateAutotoolsGnuConfigScriptsPhase () +{ + + if [ -n "${dontUpdateAutotoolsGnuConfigScripts-}" ]; then + return; + fi; + for script in config.sub config.guess; + do + for f in $(find . -type f -name "$script"); + do + echo "Updating Autotools / GNU config script to a newer upstream version: $f"; + cp -f "/nix/store/wr33c2hmpj37sly95dzmy8b5jzpdm5gq-gnu-config-2024-01-01/$script" "$f"; + done; + done +} +updateSourceDateEpoch () +{ + + local path="$1"; + [[ $path == -* ]] && path="./$path"; + local -a res=($(find "$path" -type f -not -newer "$NIX_BUILD_TOP/.." -printf '%T@ "%p"\0' | sort -n --zero-terminated | tail -n1 --zero-terminated | head -c -1)); + local time="${res[0]//\.[0-9]*/}"; + local newestFile="${res[1]}"; + if [ "${time:-0}" -gt "$SOURCE_DATE_EPOCH" ]; then + echo "setting SOURCE_DATE_EPOCH to timestamp $time of file $newestFile"; + export SOURCE_DATE_EPOCH="$time"; + local now="$(date +%s)"; + if [ "$time" -gt $((now - 60)) ]; then + echo "warning: file $newestFile may be generated; SOURCE_DATE_EPOCH may be non-deterministic"; + fi; + fi +} +PATH="$PATH${nix_saved_PATH:+:$nix_saved_PATH}" +XDG_DATA_DIRS="$XDG_DATA_DIRS${nix_saved_XDG_DATA_DIRS:+:$nix_saved_XDG_DATA_DIRS}" +export NIX_BUILD_TOP="$(mktemp -d -t nix-shell.XXXXXX)" +export TMP="$NIX_BUILD_TOP" +export TMPDIR="$NIX_BUILD_TOP" +export TEMP="$NIX_BUILD_TOP" +export TEMPDIR="$NIX_BUILD_TOP" +eval "${shellHook:-}" diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b5a49d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..73b666d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2851 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codex-controller-loop" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "clap", + "crossterm 0.29.0", + "ratatui 0.30.0", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "toon-format", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "comfy-table" +version = "7.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +dependencies = [ + "crossterm 0.29.0", + "unicode-segmentation", + "unicode-width 0.2.0", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.11.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.11.0", + "cassowary", + "compact_str 0.8.1", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", + "lru 0.12.5", + "paste", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate 1.1.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.11.0", + "compact_str 0.9.0", + "hashbrown 0.16.1", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru 0.16.3", + "strum 0.27.2", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate 2.0.1", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "strum 0.27.2", + "time", + "unicode-segmentation", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.18", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.11.0", + "fancy-regex 0.11.0", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tiktoken-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a19830747d9034cd9da43a60eaa8e552dfda7712424aebf187b7a60126bae0d" +dependencies = [ + "anyhow", + "base64", + "bstr", + "fancy-regex 0.13.0", + "lazy_static", + "regex", + "rustc-hash", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "toon-format" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5933bebbba70ee979314a8ecb021f53075a63984f94f89a10b4bdcf0af6c62b6" +dependencies = [ + "anyhow", + "arboard", + "chrono", + "clap", + "comfy-table", + "crossterm 0.28.1", + "indexmap", + "ratatui 0.29.0", + "serde", + "serde_json", + "syntect", + "thiserror 2.0.18", + "tiktoken-rs", + "tui-textarea", + "unicode-width 0.2.0", +] + +[[package]] +name = "tui-textarea" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" +dependencies = [ + "crossterm 0.28.1", + "ratatui 0.29.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools 0.14.0", + "unicode-segmentation", + "unicode-width 0.2.0", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "atomic", + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f7c7d26 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "codex-controller-loop" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.102" +base64 = "0.22.1" +clap = { version = "4.6.0", features = ["derive"] } +crossterm = "0.29.0" +ratatui = "0.30.0" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +tempfile = "3.27.0" +thiserror = "2.0.18" +toon-format = "0.4.5" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e7b2a8 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# `codex-controller-loop` + +Rust TUI-first autonomous controller with TOON-backed machine state. + +## Product Shape + +`codex-controller-loop` now targets a single full-screen terminal experience: + +- center session stream for planning and execution events +- persistent plan board on the right +- bottom composer for planning replies or execution commands +- Codex-backed planning and Codex-backed per-step execution + +The controller has a hard two-phase model: + +1. Planning + - user can provide the goal or answer Codex follow-up questions +2. Executing + - controller runs autonomously + - user can only pause, resume, stop, inspect status, or update the goal + +## Storage + +Controller-local artifacts live under `.agent/controllers//`: + +```text +goal.md +plan.toon +state.toon +standards.md +``` + +The repo-local task config lives at: + +```text +.agent/controller-loop/task.toon +``` + +## Commands + +```bash +codex-controller-loop +codex-controller-loop init --task-id controller-loop +codex-controller-loop status +codex-controller-loop run +``` + +`codex-controller-loop` defaults to the TUI. + +## Build + +Use Nix or Cargo: + +```bash +nix develop -c cargo test +nix develop -c cargo run +``` diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..03cb250 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1775036866, + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..162fe45 --- /dev/null +++ b/flake.nix @@ -0,0 +1,48 @@ +{ + description = "Standalone Codex controller loop framework"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + codex-controller-loop = import ./nix/packages/codex-controller-loop.nix { inherit pkgs; }; + in + { + packages = { + inherit codex-controller-loop; + default = codex-controller-loop; + }; + + apps = { + codex-controller-loop = { + type = "app"; + program = "${codex-controller-loop}/bin/codex-controller-loop"; + }; + default = self.apps.${system}.codex-controller-loop; + }; + + devShells.default = pkgs.mkShell { + packages = [ + pkgs.git + pkgs.rustc + pkgs.cargo + pkgs.clippy + pkgs.rustfmt + pkgs.rust-analyzer + ]; + }; + } + ); +} diff --git a/nix/packages/codex-controller-loop.nix b/nix/packages/codex-controller-loop.nix new file mode 100644 index 0000000..3b6f936 --- /dev/null +++ b/nix/packages/codex-controller-loop.nix @@ -0,0 +1,12 @@ +{ pkgs }: +pkgs.rustPlatform.buildRustPackage { + pname = "codex-controller-loop"; + version = "0.1.0"; + src = ../..; + cargoLock.lockFile = ../../Cargo.lock; + + meta = { + description = "Standalone Codex controller loop framework"; + mainProgram = "codex-controller-loop"; + }; +} diff --git a/src/app/input.rs b/src/app/input.rs new file mode 100644 index 0000000..d2f5311 --- /dev/null +++ b/src/app/input.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +use crate::model::Screen; + +use super::App; + +impl App { + pub(super) fn handle_key(&mut self, key: KeyEvent) -> Result { + if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { + return Ok(true); + } + + match self.screen { + Screen::ControllerPicker => self.handle_picker_key(key), + Screen::CreateController => self.handle_create_key(key), + Screen::Workspace => self.handle_workspace_key(key), + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..8748331 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,215 @@ +mod input; +mod picker; +mod runtime; +mod session; +mod workspace_input; + +#[cfg(test)] +mod tests; + +use std::path::PathBuf; +use std::sync::mpsc::{Receiver, Sender}; +use std::time::{Duration, Instant}; + +use anyhow::Result; +use crossterm::{ + event::{self, Event, KeyEvent}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{backend::CrosstermBackend, Terminal}; + +use crate::cli::DEFAULT_TASK_CONFIG_PATH; +use crate::model::{ + group_session_entries, ControllerPhase, ControllerState, Plan, Screen, SessionEntry, + SessionSelection, StatusSnapshot, TaskConfig, UsageSnapshot, +}; +use crate::ui; + +pub(crate) const USAGE_REFRESH_INTERVAL: Duration = Duration::from_secs(120); + +#[derive(Debug, Clone)] +pub enum AppEvent { + Session(SessionEntry), + Snapshot { + goal_md: String, + standards_md: String, + plan: Plan, + state: ControllerState, + }, + CodexUsage { + input_tokens: u64, + output_tokens: u64, + }, +} + +#[derive(Debug)] +pub enum ControlCommand { + Submit(String), + Pause, + Resume, + Stop, + Quit, +} + +pub(crate) struct WorkspaceRuntime { + pub(crate) task_config: TaskConfig, + pub(crate) goal_md: String, + pub(crate) standards_md: String, + pub(crate) plan: Plan, + pub(crate) state: ControllerState, + pub(crate) input: String, + pub(crate) session_entries: Vec, + pub(crate) event_rx: Receiver, + pub(crate) control_tx: Sender, + pub(crate) session_input_tokens: Option, + pub(crate) session_output_tokens: Option, + pub(crate) usage_snapshot: UsageSnapshot, + pub(crate) last_usage_refresh: Instant, + pub(crate) session_scroll: usize, + pub(crate) session_follow_output: bool, + pub(crate) session_viewport_lines: usize, + pub(crate) session_selection: Option, + pub(crate) session_drag_active: bool, +} + +pub struct App { + pub screen: Screen, + pub picker_items: Vec, + pub picker_selected: usize, + pub create_input: String, + pub create_error: Option, + pub default_task_path: PathBuf, + pub(crate) workspace: Option, +} + +impl App { + pub fn bootstrap(task_path: Option) -> Result { + let default_task_path = PathBuf::from(DEFAULT_TASK_CONFIG_PATH); + let mut app = Self { + screen: Screen::ControllerPicker, + picker_items: Vec::new(), + picker_selected: 0, + create_input: String::new(), + create_error: None, + default_task_path: default_task_path.clone(), + workspace: None, + }; + + if let Some(task_path) = task_path { + app.open_workspace_from_task_file(task_path)?; + } else { + app.refresh_picker()?; + } + + Ok(app) + } + + pub fn run(&mut self) -> Result<()> { + enable_raw_mode()?; + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen, crossterm::event::EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let result = self.run_loop(&mut terminal); + + self.shutdown_runtime(); + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + crossterm::event::DisableMouseCapture, + LeaveAlternateScreen + )?; + terminal.show_cursor()?; + + result + } + + pub fn workspace_groups(&self) -> Vec { + self.workspace + .as_ref() + .map(|workspace| group_session_entries(&workspace.session_entries)) + .unwrap_or_default() + } + + pub fn workspace_status_snapshot(&self) -> Option { + let workspace = self.workspace.as_ref()?; + Some(StatusSnapshot { + controller_id: workspace.task_config.controller_id(), + branch: workspace.task_config.branch.clone(), + started_at: workspace.state.started_at.clone(), + phase: workspace.state.phase.clone(), + iteration: workspace.state.iteration, + session_input_tokens: workspace.session_input_tokens, + session_output_tokens: workspace.session_output_tokens, + usage: workspace.usage_snapshot.clone(), + }) + } + + pub(crate) fn workspace(&self) -> Option<&WorkspaceRuntime> { + self.workspace.as_ref() + } + + pub(crate) fn workspace_input(&self) -> Option<&str> { + self.workspace + .as_ref() + .map(|workspace| workspace.input.as_str()) + } + + pub(crate) fn workspace_session_selection(&self) -> Option<&SessionSelection> { + self.workspace + .as_ref() + .and_then(|workspace| workspace.session_selection.as_ref()) + } + + pub(crate) fn workspace_session_scroll(&self) -> usize { + let Some(workspace) = self.workspace.as_ref() else { + return 0; + }; + + let max_scroll = self + .workspace_session_line_count() + .saturating_sub(workspace.session_viewport_lines); + if workspace.session_follow_output { + max_scroll + } else { + workspace.session_scroll.min(max_scroll) + } + } + + pub(crate) fn workspace_session_line_count(&self) -> usize { + self.workspace + .as_ref() + .map(|workspace| Self::session_line_count_for_entries(&workspace.session_entries)) + .unwrap_or_default() + } + + fn run_loop( + &mut self, + terminal: &mut Terminal>, + ) -> Result<()> { + loop { + self.drain_workspace_events()?; + self.maybe_refresh_usage()?; + self.update_workspace_viewport(terminal.size()?.height as usize); + + terminal.draw(|frame| ui::render(frame, self))?; + + if event::poll(Duration::from_millis(100))? { + match event::read()? { + Event::Key(key) => { + if self.handle_key(key)? { + break; + } + } + Event::Mouse(mouse) => self.handle_mouse(mouse, terminal.size()?.into())?, + Event::Resize(_, height) => self.update_workspace_viewport(height as usize), + _ => {} + } + } + } + + Ok(()) + } +} diff --git a/src/app/picker.rs b/src/app/picker.rs new file mode 100644 index 0000000..8a9f198 --- /dev/null +++ b/src/app/picker.rs @@ -0,0 +1,91 @@ +use anyhow::Result; +use crossterm::event::{KeyCode, KeyEvent}; + +use crate::model::Screen; + +use super::App; + +impl App { + pub(super) fn handle_picker_key(&mut self, key: KeyEvent) -> Result { + let total_rows = self.picker_items.len() + 1; + match key.code { + KeyCode::Esc => Ok(true), + KeyCode::Down | KeyCode::Char('j') => { + if self.picker_selected + 1 < total_rows { + self.picker_selected += 1; + } + Ok(false) + } + KeyCode::Up | KeyCode::Char('k') => { + if self.picker_selected > 0 { + self.picker_selected -= 1; + } + Ok(false) + } + KeyCode::Char('n') => { + self.screen = Screen::CreateController; + self.create_error = None; + Ok(false) + } + KeyCode::Enter => { + if self.picker_selected == self.picker_items.len() { + self.screen = Screen::CreateController; + self.create_error = None; + return Ok(false); + } + + if let Some(controller_id) = self + .picker_items + .get(self.picker_selected) + .map(|controller| controller.id.clone()) + { + let config = crate::model::TaskConfig::default_for(&controller_id); + self.open_workspace(config, Some(self.default_task_path.clone()))?; + } + Ok(false) + } + _ => Ok(false), + } + } + + pub(super) fn handle_create_key(&mut self, key: KeyEvent) -> Result { + match key.code { + KeyCode::Esc => { + self.screen = Screen::ControllerPicker; + self.create_error = None; + Ok(false) + } + KeyCode::Backspace => { + self.create_input.pop(); + self.create_error = None; + Ok(false) + } + KeyCode::Enter => { + let goal = self.create_input.trim().to_string(); + if goal.is_empty() { + self.create_error = + Some("Enter the work this controller should own.".to_string()); + return Ok(false); + } + + match self.create_workspace_from_goal(goal.clone()) { + Ok(()) => { + self.submit_workspace_input(goal)?; + self.create_input.clear(); + self.create_error = None; + } + Err(error) => { + self.create_error = Some(error.to_string()); + } + } + Ok(false) + } + KeyCode::Char(ch) => { + self.create_input.push(ch); + self.create_error = None; + Ok(false) + } + _ => Ok(false), + } + } +} diff --git a/src/app/runtime.rs b/src/app/runtime.rs new file mode 100644 index 0000000..0c9d039 --- /dev/null +++ b/src/app/runtime.rs @@ -0,0 +1,241 @@ +use std::path::PathBuf; +use std::sync::mpsc; +use std::thread; +use std::time::Instant; + +use anyhow::{anyhow, Result}; + +use crate::controller::engine; +use crate::model::{SessionEntry, SessionSource, SessionStream, TaskConfig, UsageSnapshot}; +use crate::repo; +use crate::storage::toon; + +use super::{App, AppEvent, ControlCommand, WorkspaceRuntime, USAGE_REFRESH_INTERVAL}; + +impl App { + pub(super) fn open_workspace_from_task_file(&mut self, task_path: PathBuf) -> Result<()> { + let config = toon::read_task_config(&task_path)?; + self.open_workspace(config, Some(task_path)) + } + + pub(super) fn create_workspace_from_goal(&mut self, goal: String) -> Result<()> { + let suggested_id = crate::process::generate_controller_id(&repo::repo_root(), &goal) + .map_err(|error| anyhow!("Failed to generate controller id with GPT-5.4 mini: {error:#}"))?; + let controller_id = toon::make_unique_controller_id(&suggested_id); + let config = toon::create_controller(&self.default_task_path, &controller_id)?; + self.open_workspace(config, Some(self.default_task_path.clone())) + } + + pub(super) fn open_workspace( + &mut self, + config: TaskConfig, + persist_task_path: Option, + ) -> Result<()> { + self.shutdown_runtime(); + + if let Some(task_path) = persist_task_path.as_ref() { + toon::write_task_config(task_path, &config)?; + } + toon::ensure_controller_files(&config)?; + + let mut state = toon::read_state(&config.state_file)?; + if state.started_at.is_none() { + state.started_at = Some(repo::now_timestamp()); + toon::write_state(&config.state_file, &state)?; + } + + let goal_md = toon::read_markdown(&config.goal_file)?; + let standards_md = toon::read_markdown(&config.standards_file)?; + let plan = toon::read_plan(&config.plan_file)?; + + let usage_snapshot = if state.last_usage_input_tokens.is_some() + || state.last_usage_output_tokens.is_some() + { + UsageSnapshot { + input_tokens: state.last_usage_input_tokens, + output_tokens: state.last_usage_output_tokens, + refreshed_at: state.last_usage_refresh_at.clone(), + available: true, + note: None, + } + } else { + UsageSnapshot::unavailable("usage unavailable") + }; + + let (event_tx, event_rx) = mpsc::channel(); + let (control_tx, control_rx) = mpsc::channel(); + let repo_root = repo::repo_root(); + let worker_config = config.clone(); + thread::spawn(move || { + if let Err(error) = + engine::runtime_loop(repo_root, worker_config, control_rx, event_tx.clone()) + { + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Warning, + stream: SessionStream::Stderr, + title: "Runtime".to_string(), + tag: None, + body: format!("controller runtime error: {error:#}"), + run_id: repo::next_run_id(), + })); + } + }); + + let session_entries = vec![SessionEntry { + source: SessionSource::Controller, + stream: SessionStream::Status, + title: "Session".to_string(), + tag: Some(config.controller_id()), + body: format!( + "Loaded {} and opened controller workspace.", + config.plan_file.display() + ), + run_id: repo::next_run_id(), + }]; + + self.workspace = Some(WorkspaceRuntime { + task_config: config, + goal_md, + standards_md, + plan, + state, + input: String::new(), + session_entries, + event_rx, + control_tx, + session_input_tokens: None, + session_output_tokens: None, + usage_snapshot, + last_usage_refresh: Instant::now(), + session_scroll: 0, + session_follow_output: true, + session_viewport_lines: 0, + session_selection: None, + session_drag_active: false, + }); + self.screen = crate::model::Screen::Workspace; + self.refresh_picker()?; + Ok(()) + } + + pub(super) fn refresh_picker(&mut self) -> Result<()> { + self.picker_items = toon::list_controller_summaries()?; + if self.picker_selected > self.picker_items.len() { + self.picker_selected = self.picker_items.len(); + } + Ok(()) + } + + pub(super) fn drain_workspace_events(&mut self) -> Result<()> { + let Some(workspace) = self.workspace.as_mut() else { + return Ok(()); + }; + + let mut events = Vec::new(); + while let Ok(event) = workspace.event_rx.try_recv() { + events.push(event); + } + for event in events { + self.apply_event(event)?; + } + Ok(()) + } + + fn apply_event(&mut self, event: AppEvent) -> Result<()> { + let Some(workspace) = self.workspace.as_mut() else { + return Ok(()); + }; + + match event { + AppEvent::Session(entry) => { + if workspace.session_follow_output { + workspace.session_scroll = + Self::session_line_count_for_entries(&workspace.session_entries) + .saturating_sub(workspace.session_viewport_lines); + } + workspace.session_selection = None; + workspace.session_drag_active = false; + workspace.session_entries.push(entry); + } + AppEvent::Snapshot { + goal_md, + standards_md, + plan, + state, + } => { + workspace.goal_md = goal_md; + workspace.standards_md = standards_md; + workspace.plan = plan; + workspace.state = state.clone(); + if state.last_usage_input_tokens.is_some() + || state.last_usage_output_tokens.is_some() + { + workspace.usage_snapshot = UsageSnapshot { + input_tokens: state.last_usage_input_tokens, + output_tokens: state.last_usage_output_tokens, + refreshed_at: state.last_usage_refresh_at, + available: true, + note: None, + }; + } + } + AppEvent::CodexUsage { + input_tokens, + output_tokens, + } => { + let input_total = workspace.session_input_tokens.get_or_insert(0); + *input_total += input_tokens; + let output_total = workspace.session_output_tokens.get_or_insert(0); + *output_total += output_tokens; + } + } + Ok(()) + } + + pub(super) fn maybe_refresh_usage(&mut self) -> Result<()> { + let Some(workspace) = self.workspace.as_mut() else { + return Ok(()); + }; + if workspace.last_usage_refresh.elapsed() < USAGE_REFRESH_INTERVAL { + return Ok(()); + } + + let snapshot = crate::process::refresh_usage_snapshot(&workspace.state); + workspace.last_usage_refresh = Instant::now(); + workspace.usage_snapshot = snapshot.clone(); + workspace.state.last_usage_refresh_at = snapshot.refreshed_at.clone(); + workspace.state.last_usage_input_tokens = snapshot.input_tokens; + workspace.state.last_usage_output_tokens = snapshot.output_tokens; + toon::write_state(&workspace.task_config.state_file, &workspace.state)?; + Ok(()) + } + + pub(super) fn push_local_entry( + &mut self, + source: SessionSource, + stream: SessionStream, + title: &str, + tag: Option, + body: &str, + ) { + if let Some(workspace) = self.workspace.as_mut() { + workspace.session_follow_output = true; + workspace.session_selection = None; + workspace.session_drag_active = false; + workspace.session_entries.push(SessionEntry { + source, + stream, + title: title.to_string(), + tag, + body: body.to_string(), + run_id: repo::next_run_id(), + }); + } + } + + pub(super) fn shutdown_runtime(&mut self) { + if let Some(workspace) = self.workspace.take() { + let _ = workspace.control_tx.send(ControlCommand::Quit); + } + } +} diff --git a/src/app/session.rs b/src/app/session.rs new file mode 100644 index 0000000..28efdc3 --- /dev/null +++ b/src/app/session.rs @@ -0,0 +1,156 @@ +use base64::Engine; +use std::io::Write; + +use anyhow::Result; +use crossterm::event::MouseEvent; + +use crate::model::{group_session_entries, SessionCursor, SessionEntry}; +use crate::ui; + +use super::App; + +impl App { + pub(super) fn session_line_count_for_entries(entries: &[SessionEntry]) -> usize { + group_session_entries(entries) + .iter() + .map(|group| group.lines.len() + 3) + .sum() + } + + pub(super) fn scroll_session_by(&mut self, delta: isize) { + let Some(workspace) = self.workspace.as_mut() else { + return; + }; + + let max_scroll = Self::session_line_count_for_entries(&workspace.session_entries) + .saturating_sub(workspace.session_viewport_lines); + let current = if workspace.session_follow_output { + max_scroll + } else { + workspace.session_scroll + }; + let next = if delta.is_negative() { + current.saturating_sub(delta.unsigned_abs()) + } else { + current.saturating_add(delta as usize).min(max_scroll) + }; + + workspace.session_scroll = next; + workspace.session_follow_output = false; + } + + pub(super) fn jump_session_to_start(&mut self) { + if let Some(workspace) = self.workspace.as_mut() { + workspace.session_scroll = 0; + workspace.session_follow_output = false; + } + } + + pub(super) fn follow_session_output(&mut self) { + if let Some(workspace) = self.workspace.as_mut() { + workspace.session_follow_output = true; + } + } + + pub(super) fn update_workspace_viewport(&mut self, terminal_height: usize) { + if let Some(workspace) = self.workspace.as_mut() { + workspace.session_viewport_lines = terminal_height.saturating_sub(8); + } + } + + pub(super) fn mouse_over_session_text( + &self, + mouse: MouseEvent, + terminal_area: ratatui::layout::Rect, + ) -> bool { + let rows = ui::build_session_render_rows(&self.workspace_groups()); + ui::workspace_layout(terminal_area) + .session_text_rect(rows.len() > self.visible_session_lines(terminal_area)) + .contains(ratatui::layout::Position::new(mouse.column, mouse.row)) + } + + pub(super) fn session_cursor_from_mouse( + &self, + mouse: MouseEvent, + terminal_area: ratatui::layout::Rect, + clamp: bool, + ) -> Option { + let rows = ui::build_session_render_rows(&self.workspace_groups()); + if rows.is_empty() { + return None; + } + + let layout = ui::workspace_layout(terminal_area); + let text_rect = + layout.session_text_rect(rows.len() > self.visible_session_lines(terminal_area)); + if text_rect.width == 0 || text_rect.height == 0 { + return None; + } + + let point = ratatui::layout::Position::new(mouse.column, mouse.row); + if !clamp && !text_rect.contains(point) { + return None; + } + + let clamped_row = mouse.row.clamp( + text_rect.y, + text_rect + .y + .saturating_add(text_rect.height.saturating_sub(1)), + ); + let clamped_col = mouse.column.clamp( + text_rect.x, + text_rect + .x + .saturating_add(text_rect.width.saturating_sub(1)), + ); + let line_index = + self.workspace_session_scroll() + clamped_row.saturating_sub(text_rect.y) as usize; + let row = rows.get(line_index.min(rows.len().saturating_sub(1)))?; + + if !clamp { + let (start, end) = row.selectable_range?; + let rel_col = clamped_col.saturating_sub(text_rect.x) as usize; + if rel_col < start || rel_col >= end { + return None; + } + return Some(SessionCursor { + line: line_index.min(rows.len().saturating_sub(1)), + column: rel_col, + }); + } + + let column = match row.selectable_range { + Some((start, end)) if end > start => { + let rel_col = clamped_col.saturating_sub(text_rect.x) as usize; + rel_col.clamp(start, end.saturating_sub(1)) + } + _ => 0, + }; + + Some(SessionCursor { + line: line_index.min(rows.len().saturating_sub(1)), + column, + }) + } + + pub(super) fn selected_session_text(&self) -> Option { + let selection = self.workspace_session_selection()?; + let rows = ui::build_session_render_rows(&self.workspace_groups()); + ui::selected_session_text(&rows, selection) + } + + fn visible_session_lines(&self, terminal_area: ratatui::layout::Rect) -> usize { + ui::workspace_layout(terminal_area) + .session_text_rect(self.workspace_session_line_count() > 0) + .height as usize + } +} + +pub(super) fn copy_to_clipboard_osc52(text: &str) -> Result<()> { + let encoded = base64::engine::general_purpose::STANDARD.encode(text); + let mut stdout = std::io::stdout(); + write!(stdout, "\u{1b}]52;c;{encoded}\u{7}")?; + stdout.flush()?; + Ok(()) +} diff --git a/src/app/tests.rs b/src/app/tests.rs new file mode 100644 index 0000000..2102fe9 --- /dev/null +++ b/src/app/tests.rs @@ -0,0 +1,117 @@ +use std::path::PathBuf; +use std::sync::mpsc; +use std::time::Instant; + +use crossterm::event::{KeyCode, KeyEvent}; + +use super::{App, WorkspaceRuntime}; +use crate::cli::DEFAULT_TASK_CONFIG_PATH; +use crate::model::{ + ControllerPhase, ControllerState, Plan, Screen, SessionEntry, SessionSource, SessionStream, + TaskConfig, UsageSnapshot, +}; + +fn sample_app() -> App { + let (event_tx, event_rx) = mpsc::channel(); + let (control_tx, _control_rx) = mpsc::channel(); + drop(event_tx); + App { + screen: Screen::ControllerPicker, + picker_items: vec![crate::model::ControllerSummary { + id: "alpha".to_string(), + goal_summary: "Ship the picker".to_string(), + phase: ControllerPhase::Planning, + current_step_id: None, + completed_steps: 0, + total_steps: 2, + last_updated: Some("10".to_string()), + branch: "codex/alpha".to_string(), + }], + picker_selected: 0, + create_input: String::new(), + create_error: None, + default_task_path: PathBuf::from(DEFAULT_TASK_CONFIG_PATH), + workspace: Some(WorkspaceRuntime { + task_config: TaskConfig::default_for("alpha"), + goal_md: String::new(), + standards_md: String::new(), + plan: Plan::default(), + state: ControllerState::default(), + input: String::new(), + session_entries: Vec::new(), + event_rx, + control_tx, + session_input_tokens: None, + session_output_tokens: None, + usage_snapshot: UsageSnapshot::unavailable("usage unavailable"), + last_usage_refresh: Instant::now(), + session_scroll: 0, + session_follow_output: true, + session_viewport_lines: 0, + session_selection: None, + session_drag_active: false, + }), + } +} + +#[test] +fn picker_navigation_can_select_create_row() { + let mut app = sample_app(); + app.workspace = None; + app.handle_picker_key(KeyEvent::from(KeyCode::Down)) + .expect("move selection"); + assert_eq!(app.picker_selected, 1); + app.handle_picker_key(KeyEvent::from(KeyCode::Enter)) + .expect("enter"); + assert!(matches!(app.screen, Screen::CreateController)); +} + +#[test] +fn planning_mode_blocks_slash_commands() { + let mut app = sample_app(); + app.screen = Screen::Workspace; + app.dispatch_workspace_input("/pause".to_string()) + .expect("dispatch"); + let last = app + .workspace + .as_ref() + .and_then(|workspace| workspace.session_entries.last()) + .expect("warning entry"); + assert_eq!(last.source, SessionSource::Warning); + assert!(last.body.contains("Slash commands")); +} + +#[test] +fn workspace_scroll_can_move_away_from_follow_mode() { + let mut app = sample_app(); + app.screen = Screen::Workspace; + if let Some(workspace) = app.workspace.as_mut() { + workspace.session_entries = vec![ + SessionEntry { + source: SessionSource::Planner, + stream: SessionStream::Stdout, + title: "Thought".to_string(), + tag: None, + body: "line one\nline two\nline three\nline four".to_string(), + run_id: 1, + }, + SessionEntry { + source: SessionSource::Verifier, + stream: SessionStream::Stdout, + title: "Tests".to_string(), + tag: None, + body: "line five\nline six".to_string(), + run_id: 2, + }, + ]; + workspace.session_viewport_lines = 3; + } + + app.handle_workspace_key(KeyEvent::from(KeyCode::Up)) + .expect("scroll up"); + assert_eq!(app.workspace_session_scroll(), 8); + + app.handle_workspace_key(KeyEvent::from(KeyCode::Home)) + .expect("home"); + assert_eq!(app.workspace_session_scroll(), 0); +} diff --git a/src/app/workspace_input.rs b/src/app/workspace_input.rs new file mode 100644 index 0000000..05b0c31 --- /dev/null +++ b/src/app/workspace_input.rs @@ -0,0 +1,309 @@ +use anyhow::{anyhow, Result}; +use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}; + +use crate::model::{ + ControllerPhase, SessionEntry, SessionSelection, SessionSource, SessionStream, +}; +use crate::repo; +use crate::storage::toon; + +use super::{App, ControlCommand}; + +impl App { + pub(super) fn handle_mouse( + &mut self, + mouse: MouseEvent, + terminal_area: ratatui::layout::Rect, + ) -> Result<()> { + if !matches!(self.screen, crate::model::Screen::Workspace) { + return Ok(()); + } + + match mouse.kind { + MouseEventKind::ScrollUp => { + if self.mouse_over_session_text(mouse, terminal_area) { + self.scroll_session_by(-3); + } + } + MouseEventKind::ScrollDown => { + if self.mouse_over_session_text(mouse, terminal_area) { + self.scroll_session_by(3); + } + } + MouseEventKind::Down(MouseButton::Left) => { + if let Some(cursor) = self.session_cursor_from_mouse(mouse, terminal_area, false) { + if let Some(workspace) = self.workspace.as_mut() { + workspace.session_selection = Some(SessionSelection { + anchor: cursor, + focus: cursor, + }); + workspace.session_drag_active = true; + } + } else if let Some(workspace) = self.workspace.as_mut() { + workspace.session_drag_active = false; + workspace.session_selection = None; + } + } + MouseEventKind::Drag(MouseButton::Left) => { + if let Some(cursor) = self.session_cursor_from_mouse(mouse, terminal_area, true) { + if let Some(workspace) = self.workspace.as_mut() { + if workspace.session_drag_active { + if let Some(selection) = workspace.session_selection.as_mut() { + selection.focus = cursor; + } + } + } + } + } + MouseEventKind::Up(MouseButton::Left) => { + let selection_text = if let Some(cursor) = + self.session_cursor_from_mouse(mouse, terminal_area, true) + { + if let Some(workspace) = self.workspace.as_mut() { + if workspace.session_drag_active { + if let Some(selection) = workspace.session_selection.as_mut() { + selection.focus = cursor; + } + } + } + self.selected_session_text() + } else { + None + }; + + if let Some(workspace) = self.workspace.as_mut() { + workspace.session_drag_active = false; + } + + if let Some(text) = selection_text.filter(|text| !text.is_empty()) { + super::session::copy_to_clipboard_osc52(&text)?; + } + } + _ => {} + } + + Ok(()) + } + + pub(super) fn handle_workspace_key(&mut self, key: KeyEvent) -> Result { + match key.code { + KeyCode::Esc => Ok(true), + KeyCode::Up => { + self.scroll_session_by(-1); + Ok(false) + } + KeyCode::Down => { + self.scroll_session_by(1); + Ok(false) + } + KeyCode::PageUp => { + self.scroll_session_by(-10); + Ok(false) + } + KeyCode::PageDown => { + self.scroll_session_by(10); + Ok(false) + } + KeyCode::Home => { + self.jump_session_to_start(); + Ok(false) + } + KeyCode::End => { + self.follow_session_output(); + Ok(false) + } + KeyCode::Backspace => { + if let Some(workspace) = self.workspace.as_mut() { + workspace.input.pop(); + } + Ok(false) + } + KeyCode::Enter => { + let input = if let Some(workspace) = self.workspace.as_mut() { + let input = workspace.input.trim().to_string(); + workspace.input.clear(); + input + } else { + String::new() + }; + if input.is_empty() { + return Ok(false); + } + self.dispatch_workspace_input(input)?; + Ok(false) + } + KeyCode::Char(ch) => { + if let Some(workspace) = self.workspace.as_mut() { + workspace.input.push(ch); + } + Ok(false) + } + _ => Ok(false), + } + } + + pub(super) fn dispatch_workspace_input(&mut self, input: String) -> Result<()> { + let phase = self + .workspace + .as_ref() + .map(|workspace| workspace.state.phase.clone()) + .ok_or_else(|| anyhow!("workspace is not active"))?; + + if input.starts_with('/') { + if matches!(phase, ControllerPhase::Planning) { + self.push_local_entry( + SessionSource::Warning, + SessionStream::Status, + "Planning", + None, + "Slash commands are only available outside planning.", + ); + return Ok(()); + } + + return self.handle_workspace_command(&input); + } + + self.submit_workspace_input(input) + } + + fn handle_workspace_command(&mut self, input: &str) -> Result<()> { + if self.workspace.is_none() { + return Ok(()); + } + + let local_entry = match input { + "/pause" => { + if let Some(workspace) = self.workspace.as_ref() { + let _ = workspace.control_tx.send(ControlCommand::Pause); + } + Some(( + SessionSource::Controller, + SessionStream::Status, + "Controller", + None, + "Pause requested.".to_string(), + )) + } + "/resume" => { + if let Some(workspace) = self.workspace.as_ref() { + let _ = workspace.control_tx.send(ControlCommand::Resume); + } + Some(( + SessionSource::Controller, + SessionStream::Status, + "Controller", + None, + "Resume requested.".to_string(), + )) + } + "/stop" => { + if let Some(workspace) = self.workspace.as_ref() { + let _ = workspace.control_tx.send(ControlCommand::Stop); + } + Some(( + SessionSource::Warning, + SessionStream::Status, + "Controller", + None, + "Stop requested.".to_string(), + )) + } + "/status" => { + let summary = self + .workspace + .as_ref() + .map(|workspace| { + format!( + "phase={} current_step={} completed={}/{}", + workspace.state.phase.label(), + workspace + .state + .current_step_id + .clone() + .unwrap_or_else(|| "none".to_string()), + workspace.state.completed_steps.len(), + workspace.plan.steps.len() + ) + }) + .unwrap_or_else(|| "No workspace loaded.".to_string()); + Some(( + SessionSource::Controller, + SessionStream::Status, + "Status", + None, + summary, + )) + } + "/diff" => Some(( + SessionSource::Controller, + SessionStream::Status, + "Diff", + None, + "Use git diff in a separate terminal; inline diff view is not implemented." + .to_string(), + )), + "/tests" => { + let summary = self + .workspace + .as_ref() + .and_then(|workspace| workspace.state.last_full_test_summary.as_ref()) + .map(|tests| tests.summary.clone()) + .unwrap_or_else(|| "No test summary recorded yet.".to_string()); + Some(( + SessionSource::Verifier, + SessionStream::Status, + "Tests", + None, + summary, + )) + } + "/goal update" => { + if let Some(workspace) = self.workspace.as_mut() { + workspace.state.phase = ControllerPhase::Planning; + toon::write_state(&workspace.task_config.state_file, &workspace.state)?; + } + Some(( + SessionSource::Controller, + SessionStream::Status, + "Planning", + None, + "Goal update requested. Enter the new goal or answer the next planning question." + .to_string(), + )) + } + _ => Some(( + SessionSource::Warning, + SessionStream::Status, + "Command", + None, + format!("Unknown command: {input}"), + )), + }; + + if let Some((source, stream, title, tag, body)) = local_entry { + self.push_local_entry(source, stream, title, tag, &body); + } + Ok(()) + } + + pub(super) fn submit_workspace_input(&mut self, input: String) -> Result<()> { + let Some(workspace) = self.workspace.as_mut() else { + return Err(anyhow!("workspace is not active")); + }; + + workspace.session_follow_output = true; + workspace.session_selection = None; + workspace.session_drag_active = false; + workspace.session_entries.push(SessionEntry { + source: SessionSource::User, + stream: SessionStream::Status, + title: "You".to_string(), + tag: None, + body: input.clone(), + run_id: repo::next_run_id(), + }); + let _ = workspace.control_tx.send(ControlCommand::Submit(input)); + Ok(()) + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..e828165 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,79 @@ +use std::path::PathBuf; + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +use crate::app::App; +use crate::model::TaskConfig; +use crate::storage::toon; + +pub const DEFAULT_TASK_CONFIG_PATH: &str = ".agent/controller-loop/task.toon"; + +#[derive(Debug, Parser)] +#[command(name = "codex-controller-loop")] +#[command(about = "Rust TUI-first autonomous controller with TOON persistence")] +pub struct Cli { + #[arg(long, global = true)] + pub task: Option, + #[command(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + Init { + #[arg(long, default_value = "controller-loop")] + task_id: String, + }, + Run, + Status, + Tui, +} + +impl Cli { + pub fn parse_args() -> Self { + Self::parse() + } + + pub fn run(self) -> Result<()> { + match self.command.unwrap_or(Command::Tui) { + Command::Init { task_id } => { + let task_path = self + .task + .unwrap_or_else(|| PathBuf::from(DEFAULT_TASK_CONFIG_PATH)); + let config = TaskConfig::default_for(&task_id); + toon::write_task_config(&task_path, &config)?; + toon::ensure_controller_files(&config)?; + println!("Initialized controller task at {}", task_path.display()); + Ok(()) + } + Command::Run | Command::Tui => { + let mut app = App::bootstrap(self.task)?; + app.run() + } + Command::Status => { + let task_path = self + .task + .unwrap_or_else(|| PathBuf::from(DEFAULT_TASK_CONFIG_PATH)); + let config = toon::read_task_config(&task_path)?; + let plan = toon::read_plan(&config.plan_file)?; + let state = toon::read_state(&config.state_file)?; + println!("phase: {:?}", state.phase); + println!("goal summary: {}", plan.goal_summary); + println!( + "steps: total={} done={} blocked={}", + plan.steps.len(), + plan.steps + .iter() + .filter(|step| step.status.is_done()) + .count(), + plan.steps + .iter() + .filter(|step| step.status.is_blocked()) + .count() + ); + Ok(()) + } + } + } +} diff --git a/src/controller/engine.rs b/src/controller/engine.rs new file mode 100644 index 0000000..50c31e2 --- /dev/null +++ b/src/controller/engine.rs @@ -0,0 +1,206 @@ +use std::path::PathBuf; +use std::sync::mpsc::{Receiver, Sender, TryRecvError}; + +use anyhow::Result; + +use crate::app::{AppEvent, ControlCommand}; +use crate::controller::{executor, goal_checker, planner, verifier}; +use crate::model::{ + ControllerPhase, GoalStatus, SessionEntry, SessionSource, SessionStream, TaskConfig, +}; +use crate::repo; +use crate::storage::toon; + +pub fn runtime_loop( + repo_root: PathBuf, + config: TaskConfig, + control_rx: Receiver, + event_tx: Sender, +) -> Result<()> { + toon::ensure_controller_files(&config)?; + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Controller, + stream: SessionStream::Status, + title: "Session".to_string(), + tag: Some(config.controller_id()), + body: format!("Controller task loaded from {}", config.plan_file.display()), + run_id: repo::next_run_id(), + })); + + loop { + let mut plan = toon::read_plan(&config.plan_file)?; + let mut state = toon::read_state(&config.state_file)?; + let goal_md = toon::read_markdown(&config.goal_file)?; + let standards_md = toon::read_markdown(&config.standards_file)?; + let _ = event_tx.send(AppEvent::Snapshot { + goal_md, + standards_md, + plan: plan.clone(), + state: state.clone(), + }); + + match control_rx.try_recv() { + Ok(ControlCommand::Quit) => break, + Ok(ControlCommand::Pause) => { + crate::controller::state::pause(&mut state); + toon::write_state(&config.state_file, &state)?; + continue; + } + Ok(ControlCommand::Resume) => { + crate::controller::state::resume(&mut state); + toon::write_state(&config.state_file, &state)?; + continue; + } + Ok(ControlCommand::Stop) => { + state.phase = ControllerPhase::Blocked; + state.goal_status = GoalStatus::Blocked; + state.notes.push("Stopped by user".to_string()); + toon::write_state(&config.state_file, &state)?; + continue; + } + Ok(ControlCommand::Submit(text)) => { + if matches!(state.phase, ControllerPhase::Planning) { + let response = + crate::planning::session::advance(&repo_root, &config, &text, &event_tx)?; + if let Some(question) = response.question { + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Planner, + stream: SessionStream::Status, + title: "Question".to_string(), + tag: Some(config.controller_id()), + body: question, + run_id: repo::next_run_id(), + })); + } + } else { + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Warning, + stream: SessionStream::Status, + title: "Execution".to_string(), + tag: Some(config.controller_id()), + body: "Execution is autonomous. Use /pause, /resume, /stop, /status, /diff, /tests, or /goal update." + .to_string(), + run_id: repo::next_run_id(), + })); + } + continue; + } + Err(TryRecvError::Disconnected) => break, + Err(TryRecvError::Empty) => {} + } + + match state.phase { + ControllerPhase::Planning + | ControllerPhase::Paused + | ControllerPhase::Blocked + | ControllerPhase::Done => { + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } + ControllerPhase::Executing => {} + } + + if goal_checker::is_done(&plan, &state)? { + state.phase = ControllerPhase::Done; + state.goal_status = GoalStatus::Done; + toon::write_state(&config.state_file, &state)?; + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Controller, + stream: SessionStream::Status, + title: "Goal".to_string(), + tag: Some(config.controller_id()), + body: "Goal complete".to_string(), + run_id: repo::next_run_id(), + })); + continue; + } + + if state.replan_required || plan.has_no_actionable_steps() { + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Planner, + stream: SessionStream::Status, + title: "Planner".to_string(), + tag: Some(config.controller_id()), + body: "Refining plan".to_string(), + run_id: repo::next_run_id(), + })); + plan = + planner::refine_without_user_input(&repo_root, &config, &plan, &state, &event_tx)?; + state.replan_required = false; + toon::write_plan(&config.plan_file, &plan)?; + toon::write_state(&config.state_file, &state)?; + continue; + } + + let Some(step) = planner::next_step(&plan, &state) else { + state.phase = ControllerPhase::Blocked; + state.goal_status = GoalStatus::Blocked; + state.notes.push( + "No actionable step remained and autonomous replan produced nothing.".to_string(), + ); + toon::write_state(&config.state_file, &state)?; + continue; + }; + + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Executor, + stream: SessionStream::Status, + title: "Step".to_string(), + tag: Some(step.id.clone()), + body: format!("Executing {}", step.title), + run_id: repo::next_run_id(), + })); + plan.mark_active(&step.id); + state.current_step_id = Some(step.id.clone()); + state.iteration += 1; + toon::write_plan(&config.plan_file, &plan)?; + toon::write_state(&config.state_file, &state)?; + + let exec = executor::implement(&repo_root, &config, &plan, &step, &event_tx)?; + if goal_checker::needs_goal_clarification(&exec) { + state.phase = ControllerPhase::Planning; + state.notes.push(format!( + "Execution requested goal clarification while processing {}", + step.id + )); + toon::write_state(&config.state_file, &state)?; + continue; + } + + let verification = verifier::verify_step(&repo_root, &exec, &event_tx)?; + if !verification.passed { + plan.mark_blocked(&step.id); + state.last_verification = Some(verification); + state.blocked_steps.push(step.id.clone()); + state.replan_required = true; + toon::write_plan(&config.plan_file, &plan)?; + toon::write_state(&config.state_file, &state)?; + continue; + } + + let cleanup = verifier::verify_cleanup(&config, &step, &exec)?; + if !cleanup.passed { + plan.mark_todo(&step.id); + state.last_cleanup_summary = Some(cleanup); + toon::write_plan(&config.plan_file, &plan)?; + toon::write_state(&config.state_file, &state)?; + continue; + } + + let tests = verifier::run_tests(&repo_root, &exec, &event_tx)?; + if !tests.passed { + plan.mark_todo(&step.id); + state.last_full_test_summary = Some(tests); + toon::write_plan(&config.plan_file, &plan)?; + toon::write_state(&config.state_file, &state)?; + continue; + } + + plan.mark_done(&step.id); + state.complete_step(&step, verification, cleanup, tests); + toon::write_plan(&config.plan_file, &plan)?; + toon::write_state(&config.state_file, &state)?; + } + + Ok(()) +} diff --git a/src/controller/executor.rs b/src/controller/executor.rs new file mode 100644 index 0000000..6a68d2f --- /dev/null +++ b/src/controller/executor.rs @@ -0,0 +1,61 @@ +use std::sync::mpsc::Sender; + +use anyhow::Result; +use serde_json::json; + +use crate::app::AppEvent; +use crate::model::{ExecutionResponse, Plan, PlanStep, SessionSource, TaskConfig}; +use crate::process; +use crate::storage::toon; + +pub fn implement( + repo_root: &std::path::Path, + config: &TaskConfig, + plan: &Plan, + step: &PlanStep, + event_tx: &Sender, +) -> Result { + let goal_md = toon::read_markdown(&config.goal_file)?; + let standards_md = toon::read_markdown(&config.standards_file)?; + let prompt = format!( + concat!( + "You are the autonomous execution worker for a Rust TUI-first controller.\n", + "You are in execution mode. Do not ask the user questions.\n", + "Implement the step, verify it, clean up the implementation, and leave the codebase in maintainable condition.\n", + "You may edit files in the repository. You own the implementation decisions.\n", + "If the goal itself is ambiguous, set needs_goal_clarification=true.\n", + "Return verification commands and test commands that the controller should run after your work.\n\n", + "Goal:\n{goal}\n\n", + "Standards:\n{standards}\n\n", + "Current plan:\n{plan}\n\n", + "Active step:\n{step}\n" + ), + goal = goal_md, + standards = standards_md, + plan = serde_json::to_string_pretty(plan)?, + step = serde_json::to_string_pretty(step)?, + ); + + let schema = json!({ + "type": "object", + "additionalProperties": false, + "required": ["status", "summary", "verification_commands", "test_commands", "notes", "needs_goal_clarification"], + "properties": { + "status": { "type": "string", "enum": ["done", "blocked", "needs-replan"] }, + "summary": { "type": "string" }, + "verification_commands": { "type": "array", "items": { "type": "string" } }, + "test_commands": { "type": "array", "items": { "type": "string" } }, + "notes": { "type": "array", "items": { "type": "string" } }, + "needs_goal_clarification": { "type": "boolean" } + } + }); + let raw = process::run_codex_with_schema( + repo_root, + &prompt, + &schema, + event_tx, + SessionSource::Executor, + Some(step.id.clone()), + )?; + Ok(serde_json::from_str(&raw)?) +} diff --git a/src/controller/goal_checker.rs b/src/controller/goal_checker.rs new file mode 100644 index 0000000..d443229 --- /dev/null +++ b/src/controller/goal_checker.rs @@ -0,0 +1,13 @@ +use anyhow::Result; + +use crate::model::{ControllerState, GoalStatus, Plan}; + +pub fn is_done(plan: &Plan, state: &ControllerState) -> Result { + Ok(!plan.steps.is_empty() + && plan.steps.iter().all(|step| step.status.is_done()) + && !matches!(state.goal_status, GoalStatus::Blocked)) +} + +pub fn needs_goal_clarification(response: &crate::model::ExecutionResponse) -> bool { + response.needs_goal_clarification +} diff --git a/src/controller/mod.rs b/src/controller/mod.rs new file mode 100644 index 0000000..d11e2e9 --- /dev/null +++ b/src/controller/mod.rs @@ -0,0 +1,6 @@ +pub mod engine; +pub mod executor; +pub mod goal_checker; +pub mod planner; +pub mod state; +pub mod verifier; diff --git a/src/controller/planner.rs b/src/controller/planner.rs new file mode 100644 index 0000000..d078026 --- /dev/null +++ b/src/controller/planner.rs @@ -0,0 +1,50 @@ +use std::sync::mpsc::Sender; + +use anyhow::Result; + +use crate::app::AppEvent; +use crate::model::{self, ControllerState, Plan, SessionSource, TaskConfig}; +use crate::process; +use crate::storage::toon; + +pub fn refine_without_user_input( + repo_root: &std::path::Path, + config: &TaskConfig, + plan: &Plan, + state: &ControllerState, + event_tx: &Sender, +) -> Result { + let goal_md = toon::read_markdown(&config.goal_file)?; + let standards_md = toon::read_markdown(&config.standards_file)?; + let prompt = format!( + concat!( + "You are refining the execution plan for an autonomous coding controller.\n", + "Do not ask the user any questions.\n", + "Use the goal, standards, current plan, and state to produce the next complete plan.\n", + "Keep existing stable step ids when still valid.\n", + "Return only the plan object.\n\n", + "Goal:\n{goal}\n\n", + "Standards:\n{standards}\n\n", + "Current plan:\n{plan}\n\n", + "Current state:\n{state}\n" + ), + goal = goal_md, + standards = standards_md, + plan = serde_json::to_string_pretty(plan)?, + state = serde_json::to_string_pretty(state)?, + ); + let schema = model::plan_schema(); + let raw = process::run_codex_with_schema( + repo_root, + &prompt, + &schema, + event_tx, + SessionSource::Planner, + Some(config.controller_id()), + )?; + Ok(serde_json::from_str(&raw)?) +} + +pub fn next_step(plan: &Plan, state: &ControllerState) -> Option { + plan.next_actionable_step(&state.completed_steps) +} diff --git a/src/controller/state.rs b/src/controller/state.rs new file mode 100644 index 0000000..64a300a --- /dev/null +++ b/src/controller/state.rs @@ -0,0 +1,9 @@ +use crate::model::{ControllerPhase, ControllerState}; + +pub fn pause(state: &mut ControllerState) { + state.phase = ControllerPhase::Paused; +} + +pub fn resume(state: &mut ControllerState) { + state.phase = ControllerPhase::Executing; +} diff --git a/src/controller/verifier.rs b/src/controller/verifier.rs new file mode 100644 index 0000000..3f0778a --- /dev/null +++ b/src/controller/verifier.rs @@ -0,0 +1,44 @@ +use std::sync::mpsc::Sender; + +use anyhow::Result; + +use crate::app::AppEvent; +use crate::model::{ + CleanupSummary, ExecutionResponse, PlanStep, TaskConfig, TestSummary, VerificationSummary, +}; +use crate::process; + +pub fn verify_step( + repo_root: &std::path::Path, + response: &ExecutionResponse, + event_tx: &Sender, +) -> Result { + process::run_shell_commands( + repo_root, + &response.verification_commands, + event_tx, + "Verification", + None, + ) +} + +pub fn verify_cleanup( + _config: &TaskConfig, + step: &PlanStep, + response: &ExecutionResponse, +) -> Result { + Ok(CleanupSummary { + passed: !response.summary.trim().is_empty(), + summary: format!("Cleanup accepted for {}", step.id), + commands: Vec::new(), + output: response.notes.clone(), + }) +} + +pub fn run_tests( + repo_root: &std::path::Path, + response: &ExecutionResponse, + event_tx: &Sender, +) -> Result { + process::run_shell_commands(repo_root, &response.test_commands, event_tx, "Tests", None) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..463eae1 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ControllerError { + #[error("command failed: {0}")] + CommandFailed(String), +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b00847c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,17 @@ +mod app; +mod cli; +mod controller; +mod error; +mod model; +mod planning; +mod process; +mod repo; +mod storage; +mod ui; + +use anyhow::Result; + +fn main() -> Result<()> { + let cli = cli::Cli::parse_args(); + cli.run() +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..34daf42 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,774 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Screen { + ControllerPicker, + CreateController, + Workspace, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum ContinueUntil { + FixedPoint, + GreenRoot, + ManualStop, +} + +impl Default for ContinueUntil { + fn default() -> Self { + Self::FixedPoint + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ControllerPhase { + Planning, + Executing, + Paused, + Blocked, + Done, +} + +impl ControllerPhase { + pub fn label(&self) -> &'static str { + match self { + Self::Planning => "Planning", + Self::Executing => "Executing", + Self::Paused => "Paused", + Self::Blocked => "Blocked", + Self::Done => "Done", + } + } +} + +impl Default for ControllerPhase { + fn default() -> Self { + Self::Planning + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum GoalStatus { + Unknown, + InProgress, + Done, + Blocked, +} + +impl Default for GoalStatus { + fn default() -> Self { + Self::Unknown + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum StepStatus { + Todo, + Active, + Done, + Blocked, +} + +impl StepStatus { + pub fn is_done(&self) -> bool { + matches!(self, Self::Done) + } + + pub fn is_blocked(&self) -> bool { + matches!(self, Self::Blocked) + } + + pub fn marker(&self) -> &'static str { + match self { + Self::Todo => "[ ]", + Self::Active => "[>]", + Self::Done => "[x]", + Self::Blocked => "[-]", + } + } +} + +impl Default for StepStatus { + fn default() -> Self { + Self::Todo + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskConfig { + pub engine: String, + pub goal_file: PathBuf, + pub plan_file: PathBuf, + pub state_file: PathBuf, + pub standards_file: PathBuf, + pub branch: String, + pub continue_until: ContinueUntil, + pub max_runs: u32, + pub max_wall_clock: String, +} + +impl TaskConfig { + pub fn default_for(task_id: &str) -> Self { + let root = PathBuf::from(format!(".agent/controllers/{task_id}")); + Self { + engine: "data-driven-v1".to_string(), + goal_file: root.join("goal.md"), + plan_file: root.join("plan.toon"), + state_file: root.join("state.toon"), + standards_file: root.join("standards.md"), + branch: format!("codex/{task_id}"), + continue_until: ContinueUntil::FixedPoint, + max_runs: 12, + max_wall_clock: "4h".to_string(), + } + } + + pub fn controller_id(&self) -> String { + self.goal_file + .parent() + .and_then(|path| path.file_name()) + .and_then(|name| name.to_str()) + .unwrap_or("controller-loop") + .to_string() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct VerificationCheck { + pub label: String, + pub commands: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CleanupRule { + pub label: String, + pub description: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct PlanStep { + pub id: String, + pub title: String, + pub purpose: String, + pub inputs: Vec, + pub outputs: Vec, + pub dependencies: Vec, + pub verification: Vec, + pub cleanup_requirements: Vec, + pub status: StepStatus, + pub attempts: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Plan { + pub version: u32, + pub goal_summary: String, + pub steps: Vec, +} + +impl Default for Plan { + fn default() -> Self { + Self { + version: 1, + goal_summary: "No plan yet".to_string(), + steps: Vec::new(), + } + } +} + +impl Plan { + pub fn has_no_actionable_steps(&self) -> bool { + self.steps + .iter() + .all(|step| !matches!(step.status, StepStatus::Todo)) + } + + pub fn next_actionable_step(&self, completed_steps: &[String]) -> Option { + self.steps + .iter() + .find(|step| { + matches!(step.status, StepStatus::Todo) + && step + .dependencies + .iter() + .all(|dependency| completed_steps.iter().any(|done| done == dependency)) + }) + .cloned() + } + + pub fn mark_active(&mut self, step_id: &str) { + self.set_status(step_id, StepStatus::Active); + } + + pub fn mark_todo(&mut self, step_id: &str) { + self.set_status(step_id, StepStatus::Todo); + } + + pub fn mark_done(&mut self, step_id: &str) { + self.set_status(step_id, StepStatus::Done); + } + + pub fn mark_blocked(&mut self, step_id: &str) { + self.set_status(step_id, StepStatus::Blocked); + } + + pub fn current_step_title(&self, step_id: Option<&str>) -> Option { + let step_id = step_id?; + self.steps + .iter() + .find(|step| step.id == step_id) + .map(|step| step.title.clone()) + } + + fn set_status(&mut self, step_id: &str, status: StepStatus) { + for step in &mut self.steps { + if step.id == step_id { + step.status = status.clone(); + if matches!(status, StepStatus::Active) { + step.attempts += 1; + } + } else if matches!(status, StepStatus::Active) + && matches!(step.status, StepStatus::Active) + { + step.status = StepStatus::Todo; + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CommandSummary { + pub passed: bool, + pub summary: String, + pub commands: Vec, + pub output: Vec, +} + +pub type VerificationSummary = CommandSummary; +pub type CleanupSummary = CommandSummary; +pub type TestSummary = CommandSummary; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct HistoryEvent { + pub timestamp: String, + pub kind: String, + pub detail: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct PlanningTurn { + pub role: String, + pub content: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct PlanningSessionMeta { + pub pending_question: Option, + pub transcript: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ControllerState { + pub version: u32, + pub phase: ControllerPhase, + pub goal_status: GoalStatus, + pub goal_revision: u32, + pub current_step_id: Option, + pub iteration: u32, + pub replan_required: bool, + pub completed_steps: Vec, + pub blocked_steps: Vec, + pub last_verification: Option, + pub last_cleanup_summary: Option, + pub last_full_test_summary: Option, + pub history: Vec, + pub notes: Vec, + pub planning_session: PlanningSessionMeta, + pub started_at: Option, + pub last_usage_refresh_at: Option, + pub last_usage_input_tokens: Option, + pub last_usage_output_tokens: Option, +} + +impl Default for ControllerState { + fn default() -> Self { + Self { + version: 1, + phase: ControllerPhase::Planning, + goal_status: GoalStatus::Unknown, + goal_revision: 0, + current_step_id: None, + iteration: 0, + replan_required: false, + completed_steps: Vec::new(), + blocked_steps: Vec::new(), + last_verification: None, + last_cleanup_summary: None, + last_full_test_summary: None, + history: Vec::new(), + notes: Vec::new(), + planning_session: PlanningSessionMeta::default(), + started_at: None, + last_usage_refresh_at: None, + last_usage_input_tokens: None, + last_usage_output_tokens: None, + } + } +} + +impl ControllerState { + pub fn complete_step( + &mut self, + step: &PlanStep, + verification: VerificationSummary, + cleanup: CleanupSummary, + tests: TestSummary, + ) { + self.current_step_id = None; + self.goal_status = GoalStatus::InProgress; + self.completed_steps.push(step.id.clone()); + self.replan_required = true; + self.last_verification = Some(verification); + self.last_cleanup_summary = Some(cleanup); + self.last_full_test_summary = Some(tests); + self.history.push(HistoryEvent { + timestamp: crate::repo::now_timestamp(), + kind: "step-complete".to_string(), + detail: format!("Completed {}", step.id), + }); + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlannerResponse { + pub kind: String, + pub question: Option, + pub goal_md: Option, + pub standards_md: Option, + pub plan: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ExecutionResponse { + pub status: String, + pub summary: String, + pub verification_commands: Vec, + pub test_commands: Vec, + pub notes: Vec, + pub needs_goal_clarification: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControllerSummary { + pub id: String, + pub goal_summary: String, + pub phase: ControllerPhase, + pub current_step_id: Option, + pub completed_steps: usize, + pub total_steps: usize, + pub last_updated: Option, + pub branch: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SessionSource { + User, + Controller, + Planner, + Executor, + Verifier, + Warning, +} + +impl SessionSource { + pub fn label(&self) -> &'static str { + match self { + Self::User => "User", + Self::Controller => "Controller", + Self::Planner => "Planner", + Self::Executor => "Executor", + Self::Verifier => "Verifier", + Self::Warning => "Warning", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SessionStream { + Status, + Stdout, + Stderr, +} + +impl SessionStream { + pub fn label(&self) -> &'static str { + match self { + Self::Status => "status", + Self::Stdout => "stdout", + Self::Stderr => "stderr", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SessionEntry { + pub source: SessionSource, + pub stream: SessionStream, + pub title: String, + pub tag: Option, + pub body: String, + pub run_id: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SessionGroup { + pub source: SessionSource, + pub stream: SessionStream, + pub title: String, + pub tag: Option, + pub lines: Vec, + pub run_id: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SessionCursor { + pub line: usize, + pub column: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SessionSelection { + pub anchor: SessionCursor, + pub focus: SessionCursor, +} + +impl SessionSelection { + pub fn ordered(&self) -> (SessionCursor, SessionCursor) { + if (self.anchor.line, self.anchor.column) <= (self.focus.line, self.focus.column) { + (self.anchor, self.focus) + } else { + (self.focus, self.anchor) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UsageSnapshot { + pub input_tokens: Option, + pub output_tokens: Option, + pub refreshed_at: Option, + pub available: bool, + pub note: Option, +} + +impl Default for UsageSnapshot { + fn default() -> Self { + Self::unavailable("usage unavailable") + } +} + +impl UsageSnapshot { + pub fn unavailable(note: impl Into) -> Self { + Self { + input_tokens: None, + output_tokens: None, + refreshed_at: Some(crate::repo::now_timestamp()), + available: false, + note: Some(note.into()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StatusSnapshot { + pub controller_id: String, + pub branch: String, + pub started_at: Option, + pub phase: ControllerPhase, + pub iteration: u32, + pub session_input_tokens: Option, + pub session_output_tokens: Option, + pub usage: UsageSnapshot, +} + +pub fn group_session_entries(entries: &[SessionEntry]) -> Vec { + let mut groups: Vec = Vec::new(); + + for entry in entries { + let body_lines = if entry.body.is_empty() { + vec![String::new()] + } else { + entry + .body + .lines() + .map(ToString::to_string) + .collect::>() + }; + + if let Some(last) = groups.last_mut() { + if last.source == entry.source + && last.stream == entry.stream + && last.title == entry.title + && last.tag == entry.tag + && last.run_id == entry.run_id + { + last.lines.extend(body_lines); + continue; + } + } + + groups.push(SessionGroup { + source: entry.source, + stream: entry.stream, + title: entry.title.clone(), + tag: entry.tag.clone(), + lines: body_lines, + run_id: entry.run_id, + }); + } + + groups +} + +pub fn verification_check_schema() -> serde_json::Value { + json!({ + "type": "object", + "additionalProperties": false, + "required": ["label", "commands"], + "properties": { + "label": { "type": "string" }, + "commands": { + "type": "array", + "items": { "type": "string" } + } + } + }) +} + +pub fn cleanup_rule_schema() -> serde_json::Value { + json!({ + "type": "object", + "additionalProperties": false, + "required": ["label", "description"], + "properties": { + "label": { "type": "string" }, + "description": { "type": "string" } + } + }) +} + +pub fn plan_step_schema() -> serde_json::Value { + json!({ + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "title", + "purpose", + "inputs", + "outputs", + "dependencies", + "verification", + "cleanup_requirements", + "status", + "attempts" + ], + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "purpose": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } }, + "dependencies": { "type": "array", "items": { "type": "string" } }, + "verification": { + "type": "array", + "items": verification_check_schema() + }, + "cleanup_requirements": { + "type": "array", + "items": cleanup_rule_schema() + }, + "status": { + "type": "string", + "enum": ["todo", "active", "done", "blocked"] + }, + "attempts": { "type": "integer" } + } + }) +} + +pub fn plan_schema() -> serde_json::Value { + json!({ + "type": "object", + "additionalProperties": false, + "required": ["version", "goal_summary", "steps"], + "properties": { + "version": { "type": "integer" }, + "goal_summary": { "type": "string" }, + "steps": { + "type": "array", + "items": plan_step_schema() + } + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn next_actionable_step_respects_dependencies() { + let mut plan = Plan { + version: 1, + goal_summary: "goal".to_string(), + steps: vec![ + PlanStep { + id: "a".to_string(), + title: "A".to_string(), + status: StepStatus::Todo, + ..PlanStep::default() + }, + PlanStep { + id: "b".to_string(), + title: "B".to_string(), + dependencies: vec!["a".to_string()], + status: StepStatus::Todo, + ..PlanStep::default() + }, + ], + }; + + assert_eq!( + plan.next_actionable_step(&[]) + .expect("expected first step") + .id, + "a" + ); + plan.mark_done("a"); + assert_eq!( + plan.next_actionable_step(&["a".to_string()]) + .expect("expected second step") + .id, + "b" + ); + } + + #[test] + fn mark_active_increments_attempts() { + let mut plan = Plan { + version: 1, + goal_summary: "goal".to_string(), + steps: vec![PlanStep { + id: "a".to_string(), + title: "A".to_string(), + status: StepStatus::Todo, + ..PlanStep::default() + }], + }; + + plan.mark_active("a"); + assert_eq!(plan.steps[0].attempts, 1); + assert!(matches!(plan.steps[0].status, StepStatus::Active)); + } + + #[test] + fn complete_step_records_history() { + let mut state = ControllerState::default(); + let step = PlanStep { + id: "a".to_string(), + title: "A".to_string(), + ..PlanStep::default() + }; + + state.complete_step( + &step, + VerificationSummary { + passed: true, + summary: "ok".to_string(), + commands: vec![], + output: vec![], + }, + CleanupSummary { + passed: true, + summary: "ok".to_string(), + commands: vec![], + output: vec![], + }, + TestSummary { + passed: true, + summary: "ok".to_string(), + commands: vec![], + output: vec![], + }, + ); + + assert_eq!(state.completed_steps, vec!["a".to_string()]); + assert!(state.replan_required); + assert_eq!(state.history.len(), 1); + } + + #[test] + fn groups_consecutive_session_entries() { + let groups = group_session_entries(&[ + SessionEntry { + source: SessionSource::Planner, + stream: SessionStream::Stdout, + title: "Thought".to_string(), + tag: Some("plan".to_string()), + body: "first line".to_string(), + run_id: 1, + }, + SessionEntry { + source: SessionSource::Planner, + stream: SessionStream::Stdout, + title: "Thought".to_string(), + tag: Some("plan".to_string()), + body: "second line".to_string(), + run_id: 1, + }, + SessionEntry { + source: SessionSource::Planner, + stream: SessionStream::Stderr, + title: "Thought".to_string(), + tag: Some("plan".to_string()), + body: "warning".to_string(), + run_id: 1, + }, + ]); + + assert_eq!(groups.len(), 2); + assert_eq!( + groups[0].lines, + vec!["first line".to_string(), "second line".to_string()] + ); + assert_eq!(groups[1].stream, SessionStream::Stderr); + } + + #[test] + fn plan_schema_locks_down_nested_objects() { + let schema = plan_schema(); + assert_eq!(schema["additionalProperties"], false); + assert_eq!( + schema["properties"]["steps"]["items"]["additionalProperties"], + false + ); + assert_eq!( + schema["properties"]["steps"]["items"]["properties"]["verification"]["items"] + ["additionalProperties"], + false + ); + assert_eq!( + schema["properties"]["steps"]["items"]["properties"]["cleanup_requirements"]["items"] + ["additionalProperties"], + false + ); + } +} diff --git a/src/planning/forwarder.rs b/src/planning/forwarder.rs new file mode 100644 index 0000000..e141fc4 --- /dev/null +++ b/src/planning/forwarder.rs @@ -0,0 +1,98 @@ +use serde_json::json; + +use crate::model::{self, ControllerState, PlannerResponse, TaskConfig}; + +pub fn planning_schema() -> serde_json::Value { + json!({ + "type": "object", + "additionalProperties": false, + "required": ["kind", "question", "goal_md", "standards_md", "plan"], + "properties": { + "kind": { "type": "string", "enum": ["question", "final"] }, + "question": { "type": ["string", "null"] }, + "goal_md": { "type": ["string", "null"] }, + "standards_md": { "type": ["string", "null"] }, + "plan": { + "anyOf": [ + model::plan_schema(), + { "type": "null" } + ] + } + } + }) +} + +pub fn build_planning_prompt( + config: &TaskConfig, + goal_md: &str, + standards_md: &str, + state: &ControllerState, + latest_user_input: &str, +) -> String { + let transcript = state + .planning_session + .transcript + .iter() + .map(|turn| format!("{}: {}", turn.role, turn.content)) + .collect::>() + .join("\n"); + + format!( + concat!( + "You are embedded Codex planning mode for a Rust autonomous controller.\n", + "You are only handling the planning phase.\n\n", + "Rules:\n", + "- Ask at most one follow-up question if the goal is still ambiguous.\n", + "- If you have enough information, return kind=final.\n", + "- Always include all response keys.\n", + "- Use null for any field that does not apply in the current response.\n", + "- The final plan must be decision-complete for autonomous execution.\n", + "- The plan should be maintainable and production-quality.\n", + "- The controller directory contains only Markdown and TOON files.\n\n", + "Task config paths:\n", + "- goal: {goal}\n", + "- plan: {plan}\n", + "- state: {state}\n", + "- standards: {standards}\n\n", + "Current goal markdown:\n{goal_md}\n\n", + "Current standards markdown:\n{standards_md}\n\n", + "Transcript so far:\n{transcript}\n\n", + "Latest user input:\n{latest}\n\n", + "When returning kind=final, include:\n", + "- goal_md: rewritten goal markdown\n", + "- standards_md: rewritten standards markdown\n", + "- plan: structured machine-readable plan object with ordered steps, verification, cleanup requirements, and statuses.\n" + ), + goal = config.goal_file.display(), + plan = config.plan_file.display(), + state = config.state_file.display(), + standards = config.standards_file.display(), + goal_md = goal_md, + standards_md = standards_md, + transcript = transcript, + latest = latest_user_input, + ) +} + +pub fn parse_planning_response(raw: &str) -> anyhow::Result { + Ok(serde_json::from_str(raw)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn planning_schema_requires_all_declared_keys() { + let schema = planning_schema(); + assert_eq!( + schema["required"], + json!(["kind", "question", "goal_md", "standards_md", "plan"]) + ); + assert_eq!( + schema["properties"]["question"]["type"], + json!(["string", "null"]) + ); + assert!(schema["properties"]["plan"]["anyOf"].is_array()); + } +} diff --git a/src/planning/mod.rs b/src/planning/mod.rs new file mode 100644 index 0000000..3ef23a5 --- /dev/null +++ b/src/planning/mod.rs @@ -0,0 +1,2 @@ +pub mod forwarder; +pub mod session; diff --git a/src/planning/session.rs b/src/planning/session.rs new file mode 100644 index 0000000..7906021 --- /dev/null +++ b/src/planning/session.rs @@ -0,0 +1,79 @@ +use std::sync::mpsc::Sender; + +use anyhow::Result; + +use crate::app::AppEvent; +use crate::model::{ControllerPhase, PlannerResponse, PlanningTurn, SessionSource, TaskConfig}; +use crate::process; +use crate::storage::toon; + +pub fn advance( + repo_root: &std::path::Path, + config: &TaskConfig, + latest_user_input: &str, + event_tx: &Sender, +) -> Result { + let mut state = toon::read_state(&config.state_file)?; + let goal_md = toon::read_markdown(&config.goal_file)?; + let standards_md = toon::read_markdown(&config.standards_file)?; + + state.phase = ControllerPhase::Planning; + state.planning_session.transcript.push(PlanningTurn { + role: "user".to_string(), + content: latest_user_input.to_string(), + }); + toon::write_state(&config.state_file, &state)?; + + let prompt = crate::planning::forwarder::build_planning_prompt( + config, + &goal_md, + &standards_md, + &state, + latest_user_input, + ); + let raw = process::run_codex_with_schema( + repo_root, + &prompt, + &crate::planning::forwarder::planning_schema(), + event_tx, + SessionSource::Planner, + Some(config.controller_id()), + )?; + let response = crate::planning::forwarder::parse_planning_response(&raw)?; + + match response.kind.as_str() { + "question" => { + if let Some(question) = &response.question { + state.planning_session.pending_question = Some(question.clone()); + state.planning_session.transcript.push(PlanningTurn { + role: "assistant".to_string(), + content: question.clone(), + }); + toon::write_state(&config.state_file, &state)?; + } + } + "final" => { + let next_goal = response.goal_md.clone().unwrap_or(goal_md); + let next_standards = response.standards_md.clone().unwrap_or(standards_md); + let plan = response.plan.clone().unwrap_or_default(); + + toon::write_markdown(&config.goal_file, &next_goal)?; + toon::write_markdown(&config.standards_file, &next_standards)?; + toon::write_plan(&config.plan_file, &plan)?; + + state.phase = ControllerPhase::Executing; + state.goal_revision += 1; + state.goal_status = crate::model::GoalStatus::InProgress; + state.replan_required = false; + state.planning_session.pending_question = None; + state.planning_session.transcript.push(PlanningTurn { + role: "assistant".to_string(), + content: "Planning completed".to_string(), + }); + toon::write_state(&config.state_file, &state)?; + } + _ => {} + } + + Ok(response) +} diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..61bf387 --- /dev/null +++ b/src/process.rs @@ -0,0 +1,604 @@ +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::process::{Command, Stdio}; +use std::sync::mpsc::Sender; +use std::thread; + +use anyhow::{Context, Result}; +use serde_json::{json, Value}; +use tempfile::NamedTempFile; + +use crate::app::AppEvent; +use crate::error::ControllerError; +use crate::model::{ + CommandSummary, ControllerState, SessionEntry, SessionSource, SessionStream, UsageSnapshot, +}; +use crate::repo; + +pub fn run_codex_with_schema( + repo_root: &Path, + prompt: &str, + schema: &Value, + event_tx: &Sender, + source: SessionSource, + tag: Option, +) -> Result { + let mut schema_file = NamedTempFile::new()?; + let output_file = NamedTempFile::new()?; + + std::io::Write::write_all( + &mut schema_file, + serde_json::to_string_pretty(schema)?.as_bytes(), + )?; + + let run_id = repo::next_run_id(); + let mut child = Command::new("codex") + .arg("exec") + .arg("--json") + .arg("-C") + .arg(repo_root) + .arg("--full-auto") + .arg("--color") + .arg("never") + .arg("--output-schema") + .arg(schema_file.path()) + .arg("-o") + .arg(output_file.path()) + .arg(prompt) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("failed to spawn codex exec")?; + + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + let stdout_tx = event_tx.clone(); + let stderr_tx = event_tx.clone(); + let stdout_tag = tag.clone(); + let stderr_tag = tag.clone(); + + let stdout_handle = stdout.map(|pipe| { + thread::spawn(move || -> (u64, u64) { + let reader = BufReader::new(pipe); + let mut input_tokens = 0; + let mut output_tokens = 0; + for line in reader.lines().map_while(std::result::Result::ok) { + let parsed = parse_codex_line(&line); + if !parsed.display.is_empty() { + let _ = stdout_tx.send(AppEvent::Session(SessionEntry { + source, + stream: SessionStream::Stdout, + title: parsed.title, + tag: stdout_tag.clone(), + body: parsed.display, + run_id, + })); + } + input_tokens += parsed.input_tokens; + output_tokens += parsed.output_tokens; + } + (input_tokens, output_tokens) + }) + }); + + let stderr_handle = stderr.map(|pipe| { + thread::spawn(move || { + let reader = BufReader::new(pipe); + for line in reader.lines().map_while(std::result::Result::ok) { + if should_ignore_codex_stderr(&line) { + continue; + } + let _ = stderr_tx.send(AppEvent::Session(SessionEntry { + source, + stream: SessionStream::Stderr, + title: "Output".to_string(), + tag: stderr_tag.clone(), + body: line, + run_id, + })); + } + }) + }); + + let status = child.wait()?; + let (input_tokens, output_tokens) = stdout_handle + .map(|handle| handle.join().unwrap_or((0, 0))) + .unwrap_or((0, 0)); + if let Some(handle) = stderr_handle { + let _ = handle.join(); + } + + if input_tokens > 0 || output_tokens > 0 { + let _ = event_tx.send(AppEvent::CodexUsage { + input_tokens, + output_tokens, + }); + } + + if !status.success() { + return Err( + ControllerError::CommandFailed(format!("codex exec exited with {status}")).into(), + ); + } + + std::fs::read_to_string(output_file.path()).context("failed to read codex output schema file") +} + +pub fn generate_controller_id(repo_root: &Path, goal: &str) -> Result { + let schema = json!({ + "type": "object", + "required": ["id"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 3, + "maxLength": 48 + } + } + }); + let prompt = format!( + "Generate a concise controller id for this coding task.\n\ + Return only JSON that matches the schema.\n\ + Constraints:\n\ + - Synthesize a fresh durable name; do not slugify or copy the prompt.\n\ + - Use lowercase kebab-case.\n\ + - Prefer 2 to 4 short words.\n\ + - Avoid generic ids like controller-loop, task, work, or fix.\n\ + - Avoid timestamps unless absolutely necessary.\n\ + Goal:\n{goal}" + ); + + let mut schema_file = NamedTempFile::new()?; + let output_file = NamedTempFile::new()?; + std::io::Write::write_all( + &mut schema_file, + serde_json::to_string_pretty(&schema)?.as_bytes(), + )?; + + let output = Command::new("codex") + .arg("exec") + .arg("--json") + .arg("-C") + .arg(repo_root) + .arg("--sandbox") + .arg("read-only") + .arg("--model") + .arg("gpt-5.4-mini") + .arg("--color") + .arg("never") + .arg("--output-schema") + .arg(schema_file.path()) + .arg("-o") + .arg(output_file.path()) + .arg(prompt) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .output() + .context("failed to spawn codex exec for controller id generation")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(ControllerError::CommandFailed(if stderr.is_empty() { + format!("codex exec exited with {}", output.status) + } else { + format!("codex exec exited with {}: {stderr}", output.status) + }) + .into()); + } + + let response = std::fs::read_to_string(output_file.path()) + .context("failed to read controller id output file")?; + let value: Value = + serde_json::from_str(&response).context("failed to parse controller id JSON")?; + value + .get("id") + .and_then(Value::as_str) + .map(str::trim) + .map(ToString::to_string) + .filter(|id| !id.is_empty()) + .context("controller id response did not include a usable id") +} + +pub fn run_shell_commands( + repo_root: &Path, + commands: &[String], + event_tx: &Sender, + title: &str, + tag: Option, +) -> Result { + let run_id = repo::next_run_id(); + let mut output = Vec::new(); + let mut passed = true; + + for command in commands { + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Verifier, + stream: SessionStream::Status, + title: title.to_string(), + tag: tag.clone(), + body: command.clone(), + run_id, + })); + let result = Command::new("zsh") + .arg("-lc") + .arg(command) + .current_dir(repo_root) + .output() + .with_context(|| format!("failed to execute shell command: {command}"))?; + + let stdout = String::from_utf8_lossy(&result.stdout).trim().to_string(); + if !stdout.is_empty() { + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Verifier, + stream: SessionStream::Stdout, + title: title.to_string(), + tag: tag.clone(), + body: stdout.clone(), + run_id, + })); + output.push(stdout); + } + + let stderr = String::from_utf8_lossy(&result.stderr).trim().to_string(); + if !stderr.is_empty() { + let _ = event_tx.send(AppEvent::Session(SessionEntry { + source: SessionSource::Verifier, + stream: SessionStream::Stderr, + title: title.to_string(), + tag: tag.clone(), + body: stderr.clone(), + run_id, + })); + output.push(stderr); + } + + if !result.status.success() { + passed = false; + break; + } + } + + Ok(CommandSummary { + passed, + summary: if commands.is_empty() { + "No commands requested".to_string() + } else if passed { + "All commands passed".to_string() + } else { + "One or more commands failed".to_string() + }, + commands: commands.to_vec(), + output, + }) +} + +pub fn refresh_usage_snapshot(state: &ControllerState) -> UsageSnapshot { + if state.last_usage_input_tokens.is_some() || state.last_usage_output_tokens.is_some() { + UsageSnapshot { + input_tokens: state.last_usage_input_tokens, + output_tokens: state.last_usage_output_tokens, + refreshed_at: Some(repo::now_timestamp()), + available: true, + note: Some("cached snapshot".to_string()), + } + } else { + UsageSnapshot::unavailable("codex usage unavailable") + } +} + +struct ParsedCodexLine { + title: String, + display: String, + input_tokens: u64, + output_tokens: u64, +} + +fn parse_codex_line(line: &str) -> ParsedCodexLine { + let Ok(value) = serde_json::from_str::(line) else { + return ParsedCodexLine { + title: "Output".to_string(), + display: line.to_string(), + input_tokens: 0, + output_tokens: 0, + }; + }; + + let (title, display) = parse_codex_event(&value).unwrap_or_else(|| { + ( + value + .get("type") + .and_then(Value::as_str) + .map(humanize_type) + .unwrap_or_else(|| "Output".to_string()), + first_text(&value).unwrap_or_else(|| line.to_string()), + ) + }); + + ParsedCodexLine { + title, + display, + input_tokens: collect_usage_tokens(&value, true), + output_tokens: collect_usage_tokens(&value, false), + } +} + +fn parse_codex_event(value: &Value) -> Option<(String, String)> { + let event_type = value + .get("type") + .and_then(Value::as_str) + .unwrap_or_default(); + match event_type { + "thread.started" | "turn.started" | "turn.completed" => { + Some(("Status".to_string(), String::new())) + } + "item.started" | "item.completed" => value + .get("item") + .and_then(|item| parse_codex_item(event_type, item)), + _ => { + let display = first_text(value)?; + Some((humanize_type(event_type), display)) + } + } +} + +fn parse_codex_item(event_type: &str, item: &Value) -> Option<(String, String)> { + let item_type = item.get("type").and_then(Value::as_str).unwrap_or_default(); + let title = classify_codex_item(item_type, item); + let display = match title.as_str() { + "Thinking" => item + .get("text") + .and_then(Value::as_str) + .map(ToString::to_string) + .or_else(|| first_text(item)), + "Command" => render_command_event(event_type, item), + "Patch" | "MCP" | "Plugin" => render_tool_event(event_type, item), + _ => first_text(item), + }?; + let display = display.trim().to_string(); + (!display.is_empty()).then_some((title, display)) +} + +fn classify_codex_item(item_type: &str, item: &Value) -> String { + let item_type = item_type.to_ascii_lowercase(); + let item_name = codex_item_name(item) + .unwrap_or_default() + .to_ascii_lowercase(); + + if item_type == "agent_message" + || item_type.contains("reasoning") + || item_type.contains("thought") + { + return "Thinking".to_string(); + } + + if item_name.contains("apply_patch") || item_type.contains("patch") { + return "Patch".to_string(); + } + + if item_type.contains("command") || item.get("command").is_some() { + return "Command".to_string(); + } + + if item_name.starts_with("mcp__") + || item_type.contains("mcp") + || item.get("server").is_some() + || item.get("server_name").is_some() + { + return "MCP".to_string(); + } + + if item_type.contains("tool") { + return "Plugin".to_string(); + } + + humanize_type(if item_type.is_empty() { + "output" + } else { + item_type.as_str() + }) +} + +fn render_command_event(event_type: &str, item: &Value) -> Option { + if event_type == "item.started" { + return string_field(item, &["command"]).map(|command| format!("$ {command}")); + } + + let output = string_field(item, &["aggregated_output", "output", "result"]) + .unwrap_or_default() + .trim() + .to_string(); + let exit_code = item.get("exit_code").and_then(Value::as_i64); + + match (output.is_empty(), exit_code) { + (false, Some(code)) if code != 0 => Some(format!("{output}\nexit {code}")), + (false, _) => Some(output), + (true, Some(code)) => Some(format!("exit {code}")), + _ => string_field(item, &["command"]).map(|command| format!("$ {command}")), + } +} + +fn render_tool_event(event_type: &str, item: &Value) -> Option { + let name = codex_item_name(item).unwrap_or_else(|| "tool".to_string()); + if event_type == "item.started" { + return Some(name); + } + + let output = string_field( + item, + &["output", "result", "aggregated_output", "summary", "text"], + ) + .unwrap_or_default() + .trim() + .to_string(); + if !output.is_empty() { + return Some(output); + } + + let status = string_field(item, &["status"]).unwrap_or_default(); + if !status.is_empty() && status != "completed" { + return Some(status); + } + + None +} + +fn codex_item_name(item: &Value) -> Option { + string_field( + item, + &["tool_name", "tool", "name", "server_name", "server"], + ) +} + +fn string_field(value: &Value, keys: &[&str]) -> Option { + let Value::Object(map) = value else { + return None; + }; + + keys.iter() + .find_map(|key| map.get(*key).and_then(Value::as_str)) + .map(ToString::to_string) +} + +fn humanize_type(raw: &str) -> String { + raw.split(['-', '_', '.']) + .filter(|part| !part.is_empty()) + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + Some(first) => { + let mut word = String::new(); + word.push(first.to_ascii_uppercase()); + word.push_str(chars.as_str()); + word + } + None => String::new(), + } + }) + .collect::>() + .join(" ") +} + +fn first_text(value: &Value) -> Option { + match value { + Value::String(text) => Some(text.clone()), + Value::Array(items) => items.iter().find_map(first_text), + Value::Object(map) => { + for key in ["msg", "message", "text", "content", "summary"] { + if let Some(text) = map.get(key).and_then(Value::as_str) { + return Some(text.to_string()); + } + } + map.values().find_map(first_text) + } + _ => None, + } +} + +fn collect_usage_tokens(value: &Value, input: bool) -> u64 { + match value { + Value::Array(items) => items + .iter() + .map(|item| collect_usage_tokens(item, input)) + .sum(), + Value::Object(map) => { + let mut total = 0; + for (key, child) in map { + let matched_key = if input { + ["input_tokens", "input_token_count", "prompt_tokens"] + } else { + ["output_tokens", "output_token_count", "completion_tokens"] + }; + if matched_key.contains(&key.as_str()) { + total += child.as_u64().unwrap_or_default(); + } else { + total += collect_usage_tokens(child, input); + } + } + total + } + _ => 0, + } +} + +fn should_ignore_codex_stderr(line: &str) -> bool { + line.contains("failed to stat skills entry") + || line.trim() == "Reading additional input from stdin..." +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn refresh_usage_snapshot_uses_cached_values_when_available() { + let state = ControllerState { + last_usage_input_tokens: Some(11), + last_usage_output_tokens: Some(29), + ..ControllerState::default() + }; + + let snapshot = refresh_usage_snapshot(&state); + assert!(snapshot.available); + assert_eq!(snapshot.input_tokens, Some(11)); + assert_eq!(snapshot.output_tokens, Some(29)); + assert!(snapshot.refreshed_at.is_some()); + } + + #[test] + fn refresh_usage_snapshot_falls_back_when_usage_missing() { + let snapshot = refresh_usage_snapshot(&ControllerState::default()); + assert!(!snapshot.available); + assert_eq!(snapshot.input_tokens, None); + assert_eq!(snapshot.output_tokens, None); + } + + #[test] + fn filters_known_codex_stderr_noise() { + assert!(should_ignore_codex_stderr( + "2026-04-04T03:43:38Z ERROR codex_core_skills::loader: failed to stat skills entry /tmp/foo" + )); + assert!(should_ignore_codex_stderr( + "Reading additional input from stdin..." + )); + assert!(!should_ignore_codex_stderr("actual planner failure")); + } + + #[test] + fn parses_agent_messages_as_thinking() { + let parsed = parse_codex_line( + r#"{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Planning next step."}}"#, + ); + + assert_eq!(parsed.title, "Thinking"); + assert_eq!(parsed.display, "Planning next step."); + } + + #[test] + fn parses_command_events_into_command_groups() { + let started = parse_codex_line( + r#"{"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"/bin/zsh -lc pwd","status":"in_progress"}}"#, + ); + let completed = parse_codex_line( + r#"{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"/bin/zsh -lc pwd","aggregated_output":"/tmp/demo\n","exit_code":0,"status":"completed"}}"#, + ); + + assert_eq!(started.title, "Command"); + assert_eq!(started.display, "$ /bin/zsh -lc pwd"); + assert_eq!(completed.title, "Command"); + assert_eq!(completed.display, "/tmp/demo"); + } + + #[test] + fn hides_turn_lifecycle_output_but_keeps_usage() { + let parsed = parse_codex_line( + r#"{"type":"turn.completed","usage":{"input_tokens":12,"output_tokens":34}}"#, + ); + + assert!(parsed.display.is_empty()); + assert_eq!(parsed.input_tokens, 12); + assert_eq!(parsed.output_tokens, 34); + } +} diff --git a/src/repo.rs b/src/repo.rs new file mode 100644 index 0000000..4f21ec5 --- /dev/null +++ b/src/repo.rs @@ -0,0 +1,62 @@ +use std::env; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +static RUN_ID: AtomicU64 = AtomicU64::new(1); + +pub fn repo_root() -> PathBuf { + env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) +} + +pub fn now_timestamp() -> String { + let seconds = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .unwrap_or_default(); + seconds.to_string() +} + +pub fn now_timestamp_u64() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .unwrap_or_default() +} + +pub fn parse_timestamp(timestamp: &str) -> Option { + timestamp.parse::().ok() +} + +pub fn format_age(timestamp: Option<&str>) -> String { + let Some(timestamp) = timestamp.and_then(parse_timestamp) else { + return "--".to_string(); + }; + let now = now_timestamp_u64(); + if now <= timestamp { + return "now".to_string(); + } + + let seconds = now - timestamp; + if seconds < 60 { + format!("{seconds}s ago") + } else if seconds < 3_600 { + format!("{}m ago", seconds / 60) + } else if seconds < 86_400 { + format!("{}h ago", seconds / 3_600) + } else { + format!("{}d ago", seconds / 86_400) + } +} + +pub fn absolute(path: &Path) -> PathBuf { + if path.is_absolute() { + path.to_path_buf() + } else { + repo_root().join(path) + } +} + +pub fn next_run_id() -> u64 { + RUN_ID.fetch_add(1, Ordering::Relaxed) +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..31fa14c --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1 @@ +pub mod toon; diff --git a/src/storage/toon.rs b/src/storage/toon.rs new file mode 100644 index 0000000..52fcc57 --- /dev/null +++ b/src/storage/toon.rs @@ -0,0 +1,388 @@ +use std::fs; +use std::path::Path; + +use anyhow::{Context, Result}; +use serde::{de::DeserializeOwned, Serialize}; +use tempfile::NamedTempFile; +use toon_format::{decode_default, encode_default}; + +use crate::model::{ControllerState, ControllerSummary, Plan, TaskConfig}; +use crate::repo; + +const DEFAULT_GOAL: &str = "# Goal\n\nDescribe the goal for this controller.\n"; +const DEFAULT_STANDARDS: &str = + "# Standards\n\n- Keep code maintainable.\n- Avoid one-off hacks.\n- Leave tests green.\n"; +const CONTROLLERS_ROOT: &str = ".agent/controllers"; +const MAX_CONTROLLER_ID_LEN: usize = 48; + +pub fn ensure_controller_files(config: &TaskConfig) -> Result<()> { + for path in [ + &config.goal_file, + &config.plan_file, + &config.state_file, + &config.standards_file, + ] { + if let Some(parent) = repo::absolute(path).parent() { + fs::create_dir_all(parent)?; + } + } + + if !repo::absolute(&config.goal_file).exists() { + fs::write(repo::absolute(&config.goal_file), DEFAULT_GOAL)?; + } + if !repo::absolute(&config.standards_file).exists() { + fs::write(repo::absolute(&config.standards_file), DEFAULT_STANDARDS)?; + } + if !repo::absolute(&config.plan_file).exists() { + write_plan(&config.plan_file, &Plan::default())?; + } + if !repo::absolute(&config.state_file).exists() { + write_state(&config.state_file, &ControllerState::default())?; + } + + Ok(()) +} + +pub fn read_task_config(path: &Path) -> Result { + read_from_toon(path) +} + +pub fn write_task_config(path: &Path, config: &TaskConfig) -> Result<()> { + write_to_toon(path, config) +} + +pub fn read_plan(path: &Path) -> Result { + read_from_toon(path) +} + +pub fn write_plan(path: &Path, plan: &Plan) -> Result<()> { + write_to_toon(path, plan) +} + +pub fn read_state(path: &Path) -> Result { + read_from_toon(path) +} + +pub fn write_state(path: &Path, state: &ControllerState) -> Result<()> { + write_to_toon(path, state) +} + +pub fn read_markdown(path: &Path) -> Result { + fs::read_to_string(repo::absolute(path)) + .with_context(|| format!("failed to read {}", path.display())) +} + +pub fn write_markdown(path: &Path, content: &str) -> Result<()> { + let absolute = repo::absolute(path); + if let Some(parent) = absolute.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&absolute, content).with_context(|| format!("failed to write {}", path.display())) +} + +pub fn list_controller_summaries() -> Result> { + list_controller_summaries_in(&repo::absolute(Path::new(CONTROLLERS_ROOT))) +} + +pub fn controller_exists(controller_id: &str) -> bool { + repo::absolute(&Path::new(CONTROLLERS_ROOT).join(controller_id)).is_dir() +} + +pub fn normalize_controller_id_candidate(input: &str) -> String { + let mut slug = String::new(); + let mut pending_dash = false; + + for ch in input.chars() { + if ch.is_ascii_alphanumeric() { + if pending_dash && !slug.is_empty() { + slug.push('-'); + } + slug.push(ch.to_ascii_lowercase()); + pending_dash = false; + } else if !slug.is_empty() { + pending_dash = true; + } + + if slug.len() >= MAX_CONTROLLER_ID_LEN { + break; + } + } + + slug.trim_matches('-').to_string() +} + +pub fn make_unique_controller_id(candidate: &str) -> String { + let mut controller_id = normalize_controller_id_candidate(candidate); + if controller_id.is_empty() { + controller_id = fallback_controller_id(); + } + + if !controller_exists(&controller_id) { + return controller_id; + } + + for suffix in 2..10_000 { + let candidate = controller_id_with_suffix(&controller_id, suffix); + if !controller_exists(&candidate) { + return candidate; + } + } + + controller_id_with_suffix(&fallback_controller_id(), repo::next_run_id()) +} + +fn fallback_controller_id() -> String { + format!("controller-{}", repo::now_timestamp_u64()) +} + +fn controller_id_with_suffix(base: &str, suffix: impl std::fmt::Display) -> String { + let suffix = format!("-{suffix}"); + let base_len = MAX_CONTROLLER_ID_LEN.saturating_sub(suffix.len()); + let mut trimmed = base.chars().take(base_len).collect::(); + trimmed = trimmed.trim_matches('-').to_string(); + + if trimmed.is_empty() { + fallback_controller_id() + } else { + format!("{trimmed}{suffix}") + } +} + +pub fn create_controller(task_path: &Path, controller_id: &str) -> Result { + let config = TaskConfig::default_for(controller_id); + write_task_config(task_path, &config)?; + ensure_controller_files(&config)?; + Ok(config) +} + +fn list_controller_summaries_in(root: &Path) -> Result> { + if !root.exists() { + return Ok(Vec::new()); + } + + let mut controllers = Vec::new(); + for entry in fs::read_dir(root)? { + let entry = entry?; + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let Some(controller_id) = path.file_name().and_then(|value| value.to_str()) else { + continue; + }; + let config = TaskConfig::default_for(controller_id); + let goal_md = read_markdown_from_root(root, controller_id, "goal.md") + .unwrap_or_else(|_| DEFAULT_GOAL.to_string()); + let plan = + read_toon_from_root::(root, controller_id, "plan.toon").unwrap_or_default(); + let state = read_toon_from_root::(root, controller_id, "state.toon") + .unwrap_or_default(); + controllers.push(ControllerSummary { + id: controller_id.to_string(), + goal_summary: goal_summary(&goal_md, &plan), + phase: state.phase.clone(), + current_step_id: state.current_step_id.clone(), + completed_steps: state.completed_steps.len(), + total_steps: plan.steps.len(), + last_updated: controller_last_updated(&state), + branch: config.branch.clone(), + }); + } + + controllers.sort_by(|left, right| { + right + .last_updated + .cmp(&left.last_updated) + .then_with(|| left.id.cmp(&right.id)) + }); + Ok(controllers) +} + +fn read_markdown_from_root(root: &Path, controller_id: &str, filename: &str) -> Result { + let path = root.join(controller_id).join(filename); + fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display())) +} + +fn read_toon_from_root( + root: &Path, + controller_id: &str, + filename: &str, +) -> Result { + let path = root.join(controller_id).join(filename); + let content = + fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?; + decode_default(&content).with_context(|| format!("failed to decode {}", path.display())) +} + +fn goal_summary(goal_md: &str, plan: &Plan) -> String { + goal_md + .lines() + .map(str::trim) + .find(|line| !line.is_empty() && !line.starts_with('#')) + .map(ToString::to_string) + .filter(|line| !line.is_empty()) + .unwrap_or_else(|| plan.goal_summary.clone()) +} + +fn controller_last_updated(state: &ControllerState) -> Option { + state + .last_usage_refresh_at + .clone() + .or_else(|| state.history.last().map(|event| event.timestamp.clone())) + .or_else(|| state.started_at.clone()) +} + +fn read_from_toon(path: &Path) -> Result { + let content = fs::read_to_string(repo::absolute(path)) + .with_context(|| format!("failed to read {}", path.display()))?; + decode_default(&content).with_context(|| format!("failed to decode {}", path.display())) +} + +fn write_to_toon(path: &Path, value: &T) -> Result<()> { + let content = + encode_default(value).with_context(|| format!("failed to encode {}", path.display()))?; + let absolute = repo::absolute(path); + if let Some(parent) = absolute.parent() { + fs::create_dir_all(parent)?; + } + let mut temp = NamedTempFile::new_in( + absolute + .parent() + .context("expected file to have a parent directory")?, + )?; + use std::io::Write; + temp.write_all(content.as_bytes())?; + temp.persist(absolute)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use crate::model::{ControllerPhase, ControllerState, Plan, TaskConfig}; + + use super::*; + + #[test] + fn task_config_roundtrip_uses_toon() { + let temp = tempdir().expect("tempdir"); + let path_buf = temp.path().join(".agent/controller-loop/task.toon"); + let path = path_buf.as_path(); + let config = TaskConfig::default_for("roundtrip"); + write_task_config(path, &config).expect("write task config"); + + let decoded = read_task_config(path).expect("read task config"); + assert_eq!(decoded.engine, "data-driven-v1"); + assert_eq!(decoded.branch, "codex/roundtrip"); + } + + #[test] + fn default_plan_and_state_roundtrip() { + let temp = tempdir().expect("tempdir"); + let plan_path_buf = temp.path().join(".agent/controllers/example/plan.toon"); + let state_path_buf = temp.path().join(".agent/controllers/example/state.toon"); + let plan_path = plan_path_buf.as_path(); + let state_path = state_path_buf.as_path(); + + write_plan(plan_path, &Plan::default()).expect("write plan"); + write_state(state_path, &ControllerState::default()).expect("write state"); + + let plan = read_plan(plan_path).expect("read plan"); + let state = read_state(state_path).expect("read state"); + + assert_eq!(plan.goal_summary, "No plan yet"); + assert!(matches!( + state.phase, + crate::model::ControllerPhase::Planning + )); + } + + #[test] + fn normalize_controller_id_candidate_slugifies_model_output() { + assert_eq!( + normalize_controller_id_candidate( + "Build the controller picker for security hardening!" + ), + "build-the-controller-picker-for-security-hardeni" + ); + assert_eq!(normalize_controller_id_candidate("???"), ""); + } + + #[test] + fn make_unique_controller_id_falls_back_and_suffixes_cleanly() { + assert_eq!( + controller_id_with_suffix("deep-cleanup", 2), + "deep-cleanup-2" + ); + let fallback = make_unique_controller_id("???"); + assert!(fallback.starts_with("controller-")); + } + + #[test] + fn controller_discovery_handles_partial_files() { + let temp = tempdir().expect("tempdir"); + let root = temp.path().join(".agent/controllers"); + fs::create_dir_all(root.join("alpha")).expect("create alpha"); + fs::create_dir_all(root.join("beta")).expect("create beta"); + fs::write( + root.join("alpha/goal.md"), + "# Goal\n\nShip the picker experience.\n", + ) + .expect("write goal"); + fs::write( + root.join("alpha/plan.toon"), + encode_default(&Plan { + version: 1, + goal_summary: "Alpha goal".to_string(), + steps: vec![], + }) + .expect("encode plan"), + ) + .expect("write plan"); + fs::write( + root.join("alpha/state.toon"), + encode_default(&ControllerState { + phase: ControllerPhase::Executing, + started_at: Some("10".to_string()), + ..ControllerState::default() + }) + .expect("encode state"), + ) + .expect("write state"); + fs::write( + root.join("beta/goal.md"), + "# Goal\n\nMissing state and plan.\n", + ) + .expect("write partial goal"); + + let controllers = list_controller_summaries_in(&root).expect("list controllers"); + assert_eq!(controllers.len(), 2); + assert_eq!(controllers[0].id, "alpha"); + assert_eq!(controllers[0].goal_summary, "Ship the picker experience."); + assert_eq!(controllers[0].phase, ControllerPhase::Executing); + assert_eq!(controllers[1].id, "beta"); + assert_eq!(controllers[1].total_steps, 0); + } + + #[test] + fn create_controller_writes_task_config_and_files() { + let temp = tempdir().expect("tempdir"); + let task_path = temp.path().join(".agent/controller-loop/task.toon"); + let previous_cwd = std::env::current_dir().expect("cwd"); + std::env::set_current_dir(temp.path()).expect("set cwd"); + + let config = create_controller(&task_path, "picker").expect("create controller"); + assert_eq!(config.controller_id(), "picker"); + assert!(temp + .path() + .join(".agent/controllers/picker/goal.md") + .exists()); + assert!(task_path.exists()); + + std::env::set_current_dir(previous_cwd).expect("restore cwd"); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..a1b1b17 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,905 @@ +use ratatui::{ + layout::{Constraint, Direction, Layout, Margin, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{ + Block, BorderType, Borders, Padding, Paragraph, Scrollbar, ScrollbarOrientation, + ScrollbarState, Wrap, + }, + Frame, +}; + +use crate::app::App; +use crate::model::{ + ControllerPhase, Screen, SessionGroup, SessionSelection, SessionSource, SessionStream, +}; +use crate::repo; + +const BORDER: Color = Color::DarkGray; +const BORDER_ACTIVE: Color = Color::Blue; +const TEXT: Color = Color::Reset; +const TEXT_DIM: Color = Color::DarkGray; +const CYAN: Color = Color::Cyan; +const GREEN: Color = Color::Green; +const GOLD: Color = Color::Yellow; +const RED: Color = Color::Red; +const MAGENTA: Color = Color::Magenta; +const BLUE: Color = Color::Blue; + +#[derive(Debug, Clone)] +pub(crate) struct WorkspaceLayout { + pub session: Rect, + pub sidebar: Rect, + pub status: Rect, + pub composer: Rect, +} + +#[derive(Debug, Clone)] +pub(crate) struct SessionRenderRow { + pub text: String, + pub border_style: Style, + pub content_style: Style, + pub selectable_range: Option<(usize, usize)>, +} + +impl WorkspaceLayout { + pub fn session_text_rect(&self, with_scrollbar: bool) -> Rect { + let extra_right = if with_scrollbar { 1 } else { 0 }; + Rect { + x: self.session.x.saturating_add(2), + y: self.session.y.saturating_add(1), + width: self + .session + .width + .saturating_sub(4) + .saturating_sub(extra_right), + height: self.session.height.saturating_sub(2), + } + } +} + +pub(crate) fn workspace_layout(area: Rect) -> WorkspaceLayout { + let outer = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(1), + Constraint::Length(3), + Constraint::Length(3), + ]) + .split(area); + + let top = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(74), Constraint::Percentage(26)]) + .split(outer[0]); + + WorkspaceLayout { + session: top[0], + sidebar: top[1], + status: outer[1], + composer: outer[2], + } +} + +pub fn render(frame: &mut Frame, app: &App) { + match app.screen { + Screen::ControllerPicker => render_picker(frame, app), + Screen::CreateController => render_create_controller(frame, app), + Screen::Workspace => render_workspace(frame, app), + } +} + +fn render_picker(frame: &mut Frame, app: &App) { + let outer = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(3)]) + .split(frame.area()); + + let mut lines = vec![ + Line::from(Span::styled( + "Select a controller loop or create a new one.", + Style::default().fg(CYAN).add_modifier(Modifier::BOLD), + )), + Line::from(""), + ]; + + for (index, controller) in app.picker_items.iter().enumerate() { + let selected = app.picker_selected == index; + let prefix = if selected { "> " } else { " " }; + let style = if selected { + Style::default() + .fg(TEXT) + .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(TEXT) + }; + lines.push(Line::from(vec![ + Span::styled( + if selected { "▌ " } else { " " }, + Style::default().fg(if selected { BORDER_ACTIVE } else { BORDER }), + ), + Span::styled( + format!( + "{prefix}{} [{}] {}/{} done current={}", + controller.id, + controller.phase.label(), + controller.completed_steps, + controller.total_steps, + controller + .current_step_id + .clone() + .unwrap_or_else(|| "none".to_string()) + ), + style, + ), + ])); + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled( + "goal ", + Style::default().fg(TEXT_DIM).add_modifier(Modifier::ITALIC), + ), + Span::styled( + controller.goal_summary.clone(), + Style::default().fg(if selected { TEXT } else { TEXT_DIM }), + ), + Span::raw(" "), + Span::styled("updated ", Style::default().fg(TEXT_DIM)), + Span::styled( + repo::format_age(controller.last_updated.as_deref()), + Style::default().fg(TEXT_DIM), + ), + ])); + lines.push(Line::from("")); + } + + let create_selected = app.picker_selected == app.picker_items.len(); + let create_style = if create_selected { + Style::default() + .fg(TEXT) + .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(GREEN) + }; + lines.push(Line::from(vec![ + Span::styled( + if create_selected { "▌ " } else { " " }, + Style::default().fg(if create_selected { GREEN } else { BORDER }), + ), + Span::styled( + format!( + "{}Create new controller", + if create_selected { "> " } else { " " } + ), + create_style, + ), + ])); + + let picker = Paragraph::new(lines) + .block(shell_block(" Controller Picker ", true)) + .style(Style::default().fg(TEXT)) + .wrap(Wrap { trim: false }); + let footer = Paragraph::new("Up/Down or j/k to move. Enter opens. n creates. Esc quits.") + .block(shell_block(" Controls ", false)) + .style(Style::default().fg(TEXT_DIM)); + + frame.render_widget(picker, outer[0]); + frame.render_widget(footer, outer[1]); +} + +fn render_create_controller(frame: &mut Frame, app: &App) { + let outer = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(6)]) + .split(frame.area()); + + let mut lines = vec![ + Line::from(Span::styled( + "Describe the work this controller should own. The first submission goes straight into the Codex planning helper.", + Style::default().fg(CYAN).add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from(vec![ + Span::styled("Controller id ", Style::default().fg(TEXT_DIM)), + Span::styled( + "generated by GPT-5.4 mini on submit", + Style::default().fg(GREEN), + ), + ]), + Line::from(""), + Line::from(Span::styled( + "Example: Build the intuitive controller-first TUI picker and workspace.", + Style::default().fg(TEXT_DIM), + )), + ]; + + if let Some(error) = &app.create_error { + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + error.clone(), + Style::default().fg(RED).add_modifier(Modifier::BOLD), + ))); + } + + let help = Paragraph::new(lines) + .block(shell_block(" Create Controller ", true)) + .style(Style::default().fg(TEXT)) + .wrap(Wrap { trim: false }); + let composer_scroll = composer_scroll_offset(app.create_input.as_str(), outer[1], true); + let composer = Paragraph::new(app.create_input.as_str()) + .block(shell_block(" Goal-First Planning Input ", true)) + .style(Style::default().fg(TEXT)) + .wrap(Wrap { trim: false }) + .scroll((composer_scroll as u16, 0)); + + frame.render_widget(help, outer[0]); + frame.render_widget(composer, outer[1]); +} + +fn render_workspace(frame: &mut Frame, app: &App) { + let layout = workspace_layout(frame.area()); + let session_rows = build_session_render_rows(&app.workspace_groups()); + let session_has_scrollbar = + session_rows.len() > layout.session_text_rect(false).height as usize; + let session_lines = render_session_lines(&session_rows, app.workspace_session_selection()); + let session = Paragraph::new(session_lines.clone()) + .block(shell_block(" Session ", true)) + .style(Style::default().fg(TEXT)) + .scroll((app.workspace_session_scroll() as u16, 0)) + .wrap(Wrap { trim: false }); + + let sidebar = Paragraph::new(plan_board_lines(app)) + .block(shell_block(" Plan Board ", false)) + .style(Style::default().fg(TEXT)) + .wrap(Wrap { trim: false }); + + let status = Paragraph::new(status_line(app)) + .block(shell_block(" Status ", true)) + .style(Style::default().fg(TEXT_DIM)); + + let composer_title = match app + .workspace() + .map(|workspace| workspace.state.phase.clone()) + .unwrap_or(ControllerPhase::Planning) + { + ControllerPhase::Planning => "Composer (planning input)", + _ => "Composer (execution commands or notes)", + }; + let composer_scroll = composer_scroll_offset( + app.workspace_input().unwrap_or_default(), + layout.composer, + true, + ); + let composer = Paragraph::new(app.workspace_input().unwrap_or_default()) + .block(shell_block(&format!(" {composer_title} "), true)) + .style(Style::default().fg(TEXT)) + .wrap(Wrap { trim: false }) + .scroll((composer_scroll as u16, 0)); + + frame.render_widget(session, layout.session); + frame.render_widget(sidebar, layout.sidebar); + frame.render_widget(status, layout.status); + frame.render_widget(composer, layout.composer); + + if session_has_scrollbar { + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .track_symbol(Some("│")) + .thumb_symbol("█") + .track_style(Style::default().fg(BORDER)) + .thumb_style(Style::default().fg(BORDER_ACTIVE)); + let mut scrollbar_state = + ScrollbarState::new(session_lines.len()).position(app.workspace_session_scroll()); + frame.render_stateful_widget( + scrollbar, + layout.session.inner(Margin { + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + } +} + +pub(crate) fn build_session_render_rows(groups: &[SessionGroup]) -> Vec { + let mut rows = Vec::new(); + for group in groups { + let accent_style = session_group_style(group); + let body_style = if matches!(group.stream, SessionStream::Stderr) { + Style::default().fg(RED) + } else { + Style::default().fg(TEXT) + }; + + let tag = group.tag.clone().unwrap_or_default(); + let header = format!( + "┌ {} [{}]{}{}", + group.title, + group.source.label(), + if matches!(group.stream, SessionStream::Stdout) { + String::new() + } else { + format!(" {}", group.stream.label()) + }, + if tag.is_empty() { + String::new() + } else { + format!(" {tag}") + } + ); + rows.push(SessionRenderRow { + selectable_range: Some((2, header.chars().count())), + text: header, + border_style: accent_style, + content_style: accent_style.add_modifier(Modifier::BOLD), + }); + + for line in &group.lines { + let text = format!("│ {line}"); + rows.push(SessionRenderRow { + selectable_range: Some((2, text.chars().count())), + text, + border_style: accent_style, + content_style: body_style, + }); + } + rows.push(SessionRenderRow { + text: "╰─".to_string(), + border_style: accent_style, + content_style: accent_style, + selectable_range: None, + }); + rows.push(SessionRenderRow { + text: String::new(), + border_style: Style::default(), + content_style: Style::default(), + selectable_range: Some((0, 0)), + }); + } + + rows +} + +fn render_session_lines( + rows: &[SessionRenderRow], + selection: Option<&SessionSelection>, +) -> Vec> { + rows.iter() + .enumerate() + .map(|(index, row)| render_session_row(row, index, selection)) + .collect() +} + +fn render_session_row( + row: &SessionRenderRow, + row_index: usize, + selection: Option<&SessionSelection>, +) -> Line<'static> { + let total_chars = row.text.chars().count(); + let content_start = row + .selectable_range + .map(|(start, _)| start) + .unwrap_or(total_chars); + let mut spans = Vec::new(); + if content_start > 0 { + spans.push(Span::styled( + slice_chars(&row.text, 0, content_start), + row.border_style, + )); + } + + let Some((selected_start, selected_end)) = selected_range_for_row(row, row_index, selection) + else { + if content_start < total_chars { + spans.push(Span::styled( + slice_chars(&row.text, content_start, total_chars), + row.content_style, + )); + } + return Line::from(spans); + }; + + if selected_start > content_start { + spans.push(Span::styled( + slice_chars(&row.text, content_start, selected_start), + row.content_style, + )); + } + spans.push(Span::styled( + slice_chars(&row.text, selected_start, selected_end), + selected_style(row.content_style), + )); + if selected_end < total_chars { + spans.push(Span::styled( + slice_chars(&row.text, selected_end, total_chars), + row.content_style, + )); + } + Line::from(spans) +} + +fn selected_range_for_row( + row: &SessionRenderRow, + row_index: usize, + selection: Option<&SessionSelection>, +) -> Option<(usize, usize)> { + let selection = selection?; + let selectable = row.selectable_range?; + let (start, end) = selection.ordered(); + if row_index < start.line || row_index > end.line { + return None; + } + + let raw_start = if row_index == start.line { + start.column + } else { + 0 + }; + let raw_end = if row_index == end.line { + end.column.saturating_add(1) + } else { + row.text.chars().count() + }; + + let clipped_start = raw_start.max(selectable.0); + let clipped_end = raw_end.min(selectable.1); + (clipped_end > clipped_start).then_some((clipped_start, clipped_end)) +} + +pub(crate) fn selected_session_text( + rows: &[SessionRenderRow], + selection: &SessionSelection, +) -> Option { + let (start, end) = selection.ordered(); + let end_line = end.line.min(rows.len().saturating_sub(1)); + let start_line = start.line.min(end_line); + let mut output = Vec::new(); + + for row_index in start_line..=end_line { + let row = rows.get(row_index)?; + let text = if let Some((selected_start, selected_end)) = + selected_range_for_row(row, row_index, Some(selection)) + { + slice_chars(&row.text, selected_start, selected_end) + } else { + String::new() + }; + output.push(text); + } + + Some(output.join("\n")) +} + +fn slice_chars(text: &str, start: usize, end: usize) -> String { + text.chars() + .skip(start) + .take(end.saturating_sub(start)) + .collect() +} + +fn plan_board_lines(app: &App) -> Vec> { + let Some(workspace) = app.workspace() else { + return vec![Line::from("No workspace loaded.")]; + }; + + let current_objective = workspace + .plan + .current_step_title(workspace.state.current_step_id.as_deref()) + .or_else(|| { + workspace + .plan + .steps + .iter() + .find(|step| matches!(step.status, crate::model::StepStatus::Todo)) + .map(|step| step.title.clone()) + }) + .unwrap_or_else(|| "none".to_string()); + + let mut lines = Vec::new(); + push_sidebar_field( + &mut lines, + "Phase", + workspace.state.phase.label(), + Style::default().fg(CYAN), + ); + push_sidebar_field( + &mut lines, + "Goal", + workspace.plan.goal_summary.clone(), + Style::default().fg(TEXT), + ); + push_sidebar_field( + &mut lines, + "Focus", + current_objective, + Style::default().fg(TEXT), + ); + push_sidebar_field( + &mut lines, + "Active Step", + workspace + .state + .current_step_id + .clone() + .unwrap_or_else(|| "none".to_string()), + Style::default().fg(TEXT), + ); + + lines.push(Line::from(Span::styled( + "Plan", + Style::default().fg(GOLD).add_modifier(Modifier::BOLD), + ))); + lines.push(Line::from("")); + + for step in &workspace.plan.steps { + lines.push(Line::from(vec![ + Span::styled( + step.status.marker(), + Style::default().fg(step_color(step.status.clone())), + ), + Span::raw(" "), + Span::styled(format!("{}:", step.id), Style::default().fg(TEXT_DIM)), + ])); + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled( + step.title.clone(), + Style::default().fg(TEXT).add_modifier( + if matches!(step.status, crate::model::StepStatus::Active) { + Modifier::BOLD + } else { + Modifier::empty() + }, + ), + ), + ])); + lines.push(Line::from("")); + } + + if !workspace.state.blocked_steps.is_empty() { + lines.push(Line::from(Span::styled( + "Blocked", + Style::default().fg(RED).add_modifier(Modifier::BOLD), + ))); + lines.push(Line::from("")); + for blocked in &workspace.state.blocked_steps { + lines.push(Line::from(vec![ + Span::styled("• ", Style::default().fg(RED)), + Span::styled(blocked.clone(), Style::default().fg(TEXT)), + ])); + } + lines.push(Line::from("")); + } + + if let Some(summary) = &workspace.state.last_verification { + lines.push(Line::from(Span::styled( + "Verification", + Style::default().fg(GREEN).add_modifier(Modifier::BOLD), + ))); + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + summary.summary.clone(), + Style::default().fg(TEXT), + ))); + lines.push(Line::from("")); + } + + if let Some(summary) = &workspace.state.last_full_test_summary { + lines.push(Line::from(Span::styled( + "Tests", + Style::default().fg(GREEN).add_modifier(Modifier::BOLD), + ))); + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + summary.summary.clone(), + Style::default().fg(TEXT), + ))); + lines.push(Line::from("")); + } + + lines +} + +fn status_line(app: &App) -> Line<'static> { + let Some(status) = app.workspace_status_snapshot() else { + return Line::from("No workspace loaded."); + }; + + let usage_age = repo::format_age(status.usage.refreshed_at.as_deref()); + let usage_label = if status.usage.available { + usage_age + } else { + format!( + "{} ({})", + usage_age, + status + .usage + .note + .unwrap_or_else(|| "unavailable".to_string()) + ) + }; + + Line::from(vec![ + status_kv("controller", status.controller_id, CYAN), + Span::raw(" "), + status_kv("branch", status.branch, TEXT), + Span::raw(" "), + status_kv( + "started", + repo::format_age(status.started_at.as_deref()), + TEXT, + ), + Span::raw(" "), + status_kv("phase", status.phase.label(), CYAN), + Span::raw(" "), + status_kv("iter", status.iteration.to_string(), TEXT), + Span::raw(" "), + status_kv("tok_in", token_label(status.session_input_tokens), GOLD), + Span::raw(" "), + status_kv("tok_out", token_label(status.session_output_tokens), GOLD), + Span::raw(" "), + status_kv("codex", usage_label, TEXT_DIM), + ]) +} + +fn token_label(value: Option) -> String { + value + .map(|value| value.to_string()) + .unwrap_or_else(|| "--".to_string()) +} + +fn source_style(source: SessionSource) -> Style { + Style::default().fg(source_color(source)) +} + +fn source_color(source: SessionSource) -> Color { + match source { + SessionSource::User => MAGENTA, + SessionSource::Controller => BLUE, + SessionSource::Planner => CYAN, + SessionSource::Executor => GREEN, + SessionSource::Verifier => GOLD, + SessionSource::Warning => RED, + } +} + +fn shell_block(title: &str, active: bool) -> Block<'static> { + Block::default() + .title(Span::styled( + title.to_string(), + Style::default() + .fg(if active { TEXT } else { TEXT_DIM }) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(Style::default().fg(if active { BORDER_ACTIVE } else { BORDER })) + .style(Style::default()) + .padding(Padding::horizontal(1)) +} + +fn status_kv(label: &str, value: impl Into, value_color: Color) -> Span<'static> { + Span::styled( + format!("{label}={}", value.into()), + Style::default().fg(value_color), + ) +} + +fn step_color(status: crate::model::StepStatus) -> Color { + match status { + crate::model::StepStatus::Todo => TEXT_DIM, + crate::model::StepStatus::Active => CYAN, + crate::model::StepStatus::Done => GREEN, + crate::model::StepStatus::Blocked => RED, + } +} + +fn session_group_style(group: &SessionGroup) -> Style { + let color = if matches!(group.stream, SessionStream::Stderr) { + RED + } else { + match group.title.as_str() { + "Thinking" => CYAN, + "Command" => BLUE, + "Patch" => GREEN, + "MCP" => MAGENTA, + "Plugin" => GOLD, + _ => source_color(group.source), + } + }; + Style::default().fg(color) +} + +fn selected_style(style: Style) -> Style { + style.add_modifier(Modifier::REVERSED) +} + +fn push_sidebar_field( + lines: &mut Vec>, + label: &str, + value: impl Into, + value_style: Style, +) { + lines.push(Line::from(Span::styled( + label.to_string(), + Style::default().fg(TEXT_DIM).add_modifier(Modifier::BOLD), + ))); + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled(value.into(), value_style), + ])); + lines.push(Line::from("")); +} + +fn composer_scroll_offset(text: &str, area: ratatui::layout::Rect, padded_block: bool) -> usize { + let horizontal_chrome = if padded_block { 4 } else { 2 }; + let visible_rows = area.height.saturating_sub(2) as usize; + let width = area.width.saturating_sub(horizontal_chrome) as usize; + wrapped_line_count(text, width).saturating_sub(visible_rows) +} + +fn wrapped_line_count(text: &str, width: usize) -> usize { + if width == 0 { + return 0; + } + + let mut total = 0; + for line in text.split('\n') { + let chars = line.chars().count(); + total += chars.max(1).div_ceil(width); + } + total.max(1) +} + +#[cfg(test)] +mod tests { + use std::sync::mpsc; + use std::time::Instant; + + use ratatui::{backend::TestBackend, Terminal}; + + use crate::app::WorkspaceRuntime; + use crate::model::{ + ControllerPhase, ControllerState, Plan, Screen, SessionEntry, SessionSource, SessionStream, + TaskConfig, UsageSnapshot, + }; + + use super::*; + + fn sample_app(screen: Screen) -> App { + let (_control_tx, control_rx) = mpsc::channel(); + let (event_tx, event_rx) = mpsc::channel(); + drop(control_rx); + drop(event_tx); + App { + screen, + picker_items: vec![crate::model::ControllerSummary { + id: "alpha".to_string(), + goal_summary: "Ship the picker".to_string(), + phase: ControllerPhase::Planning, + current_step_id: Some("s1".to_string()), + completed_steps: 1, + total_steps: 3, + last_updated: Some("10".to_string()), + branch: "codex/alpha".to_string(), + }], + picker_selected: 0, + create_input: "Build the picker flow".to_string(), + create_error: None, + default_task_path: std::path::PathBuf::from(".agent/controller-loop/task.toon"), + workspace: Some(WorkspaceRuntime { + task_config: TaskConfig::default_for("alpha"), + goal_md: "# Goal\n\nShip it.\n".to_string(), + standards_md: "# Standards\n\n- Keep tests green.\n".to_string(), + plan: Plan { + version: 1, + goal_summary: "Ship the picker".to_string(), + steps: vec![], + }, + state: ControllerState { + phase: ControllerPhase::Planning, + started_at: Some("10".to_string()), + ..ControllerState::default() + }, + input: "hello".to_string(), + session_entries: vec![SessionEntry { + source: SessionSource::Planner, + stream: SessionStream::Stdout, + title: "Thought".to_string(), + tag: Some("alpha".to_string()), + body: "Plan the controller picker.".to_string(), + run_id: 1, + }], + event_rx, + control_tx: _control_tx, + session_input_tokens: Some(12), + session_output_tokens: Some(34), + usage_snapshot: UsageSnapshot::unavailable("usage unavailable"), + last_usage_refresh: Instant::now(), + session_scroll: 0, + session_follow_output: true, + session_viewport_lines: 0, + session_selection: None, + session_drag_active: false, + }), + } + } + + fn render_to_text(app: &App) -> String { + let backend = TestBackend::new(100, 40); + let mut terminal = Terminal::new(backend).expect("terminal"); + terminal.draw(|frame| render(frame, app)).expect("draw"); + let backend = terminal.backend(); + backend + .buffer() + .content + .iter() + .map(|cell| cell.symbol()) + .collect::>() + .join("") + } + + #[test] + fn renders_picker_screen() { + let app = sample_app(Screen::ControllerPicker); + let rendered = render_to_text(&app); + assert!(rendered.contains("Controller Picker")); + assert!(rendered.contains("Create new controller")); + } + + #[test] + fn renders_create_screen() { + let app = sample_app(Screen::CreateController); + let rendered = render_to_text(&app); + assert!(rendered.contains("Create Controller")); + assert!(rendered.contains("generated by GPT-5.4 mini")); + } + + #[test] + fn renders_workspace_screen() { + let mut app = sample_app(Screen::Workspace); + if let Some(workspace) = app.workspace.as_mut() { + workspace.plan.steps.push(crate::model::PlanStep { + id: "s1".to_string(), + title: "Design picker".to_string(), + status: crate::model::StepStatus::Todo, + ..crate::model::PlanStep::default() + }); + } + let rendered = render_to_text(&app); + assert!(rendered.contains("Session")); + assert!(rendered.contains("Plan Board")); + assert!(rendered.contains("controller=alpha")); + } + + #[test] + fn wrapped_line_count_handles_long_input() { + assert_eq!(wrapped_line_count("", 10), 1); + assert_eq!(wrapped_line_count("abcdefghij", 10), 1); + assert_eq!(wrapped_line_count("abcdefghijk", 10), 2); + assert_eq!(wrapped_line_count("abc\ndefghijk", 4), 3); + } + + #[test] + fn selected_session_text_skips_decorative_prefixes() { + let rows = build_session_render_rows(&[SessionGroup { + source: SessionSource::Planner, + stream: SessionStream::Stdout, + title: "Error".to_string(), + tag: Some("alpha".to_string()), + lines: vec!["plain text".to_string()], + run_id: 1, + }]); + let selection = crate::model::SessionSelection { + anchor: crate::model::SessionCursor { line: 0, column: 2 }, + focus: crate::model::SessionCursor { line: 1, column: 6 }, + }; + + let text = selected_session_text(&rows, &selection).expect("selection text"); + assert!(text.starts_with("Error [Planner]")); + assert!(text.contains("plain")); + assert!(!text.contains("┌ ")); + assert!(!text.contains("│ ")); + } +}