yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
use clap::Parser;
use std::sync::Arc;
use yog::app::Roots;
use yog::cli_outbound::{Binary, Cli};
use yog::config_edit;
use yog::projects::runner::BlCli;
use yog::shell::ShellState;
use yog::ui_state::SystemClock;
use yog::watch::{Bridge, EguiRepaint};
use yog::world::hatch;
use yog::world::toolgate::CliProbe;
use yog::xdg::Env;
use yog::{AppModel, Args, shell};

/// Wall-clock unix seconds for the §5.2 startup sweep. main.rs is the
/// coverage-excluded eframe entry, so this lone real-time read lives here,
/// keeping the sweep decision ([`config_edit::branch::edit::stale_staging`])
/// pure and testable.
fn now_unix() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |d| d.as_secs() as i64)
}

fn main() -> eframe::Result<()> {
    // §9.3 shim mode: the `$EDITOR` lernie execs re-enters here BEFORE clap or
    // eframe. argv is `<yog> --editor-apply <checkout>`; `YOG_EDIT_SRC` (env)
    // carries the staging dir. Exit 0/non-zero — non-zero aborts lernie's
    // commit cleanly (see `config_edit::apply` for the invocation shape).
    let argv: Vec<String> = std::env::args().collect();
    if argv.get(1).map(String::as_str) == Some(config_edit::apply::EDITOR_APPLY_FLAG) {
        std::process::exit(config_edit::apply::run_shim(
            std::env::var("YOG_EDIT_SRC").ok(),
            argv.get(2).cloned(),
        ));
    }

    // Read the ambient env once (§16.2): the world's fixed override set derives
    // from it and stands on every child spawn (§16.6 W2); both escape hatches
    // below join a human to that same world.
    let ambient = Env::from_env();
    let overrides = yog::world::overrides(&ambient);
    // §8.4 world escape hatches (`yog env` / `yog exec`): multi-call subcommands
    // beside `--editor-apply`, dispatched before clap and eframe — they need no
    // display, and clap must never see `env`/`exec` as unknown positionals.
    match argv.get(1).map(String::as_str) {
        // `eval "$(yog env)"` drops the caller's shell into the world.
        Some(hatch::ENV_SUBCMD) => {
            print!("{}", hatch::env_script(&overrides));
            return Ok(());
        }
        // `yog exec [--cwd DIR] <cmd…>` runs one command inside the world, its
        // exit faithfully yog's (a plan parse error is 2; a spawn failure 127).
        Some(hatch::EXEC_SUBCMD) => match hatch::parse_exec(argv.get(2..).unwrap_or_default()) {
            Ok(plan) => {
                let cmd_args: Vec<&str> = plan.args.iter().map(String::as_str).collect();
                match Cli::exec_in_world(&plan.cmd, &overrides, plan.cwd.as_deref(), &cmd_args) {
                    Ok(info) => std::process::exit(info.shell_code()),
                    Err(e) => {
                        eprintln!("yog {}: {e}", hatch::EXEC_SUBCMD);
                        std::process::exit(127);
                    }
                }
            }
            Err(e) => {
                eprintln!("yog {}: {e}", hatch::EXEC_SUBCMD);
                std::process::exit(2);
            }
        },
        _ => {}
    }
    let args = Args::parse();
    // Compose the nested world (§16.2): every read below derives through `world`
    // (so yog watches the nested clones/state/lernie-home) and every child spawns
    // with `overrides` standing (§16.6 W2), so reads and spawns agree.
    let world = yog::world::compose(&ambient);
    // §5.2 startup sweep: drop scripted-editor staging dirs older than 24 h.
    config_edit::branch::edit::sweep_staging(&world.yog_stage_root(), now_unix());
    let roots = Roots {
        yog_data: world.yog_data_root(),
        lernie_data: world.lernie_data_root(),
        yog_state: world.yog_state_root(),
        balls_clones: world.balls_clones_dir(),
        home: world.home_dir(),
    };
    // The claim identity's $USER fallback, read once from the env snapshot.
    let user = world.user();
    eframe::run_native(
        "yog",
        eframe::NativeOptions {
            // Size the first-launch window in logical points (winit applies the
            // display scale): the S0 surface needs the roster plus a real center,
            // so a default-tiny window never slivers the composer on HiDPI.
            viewport: egui::ViewportBuilder::default()
                .with_inner_size([1150.0, 760.0])
                .with_min_inner_size([700.0, 500.0]),
            ..Default::default()
        },
        Box::new(move |cc| {
            // The congeries visuals (§11): installed once, before first paint.
            yog::theme::apply(&cc.egui_ctx);
            // The model enumerates every workspace, snapshots each through the
            // held probe stack, arms the watch set, derives the startup focus,
            // and takes the first ball/ops fetch through the injected `bl`
            // runner; the bridge (sharing its watch/dirty handles) feeds disk
            // events in and wakes the egui loop (§7.2).
            let balls = Box::new(BlCli::new(Cli::resolve_in_world(Binary::Bl, &overrides)));
            // The phase-1 toolchain probe (§16.4 W5): the three resolved tool
            // CLIs, capability-probed (each driven verb by `--help`) at startup
            // before first render, in the world.
            let probe = Box::new(CliProbe::new(
                Cli::resolve_in_world(Binary::Bl, &overrides),
                Cli::resolve_in_world(Binary::Lernie, &overrides),
                Cli::resolve_in_world(Binary::Bz, &overrides),
            ));
            let model = AppModel::new(
                roots,
                args.workspace,
                Arc::new(SystemClock),
                balls,
                probe,
                user,
            );
            let bridge = Bridge::spawn(
                model.watchset_handle(),
                model.dirty_handle(),
                EguiRepaint(cc.egui_ctx.clone()),
            );
            // The shell's RAM surfaces, incl. the config editors folded from the
            // world env (§9) — their `bz`/`bl conf` runners nest too. A load
            // error here is fatal at bring-up only.
            let state = ShellState::new(&world)?;
            Ok(Box::new(App {
                model,
                _bridge: bridge,
                state,
                lernie: Cli::resolve_in_world(Binary::Lernie, &overrides),
                bl: Cli::resolve_in_world(Binary::Bl, &overrides),
                bz: Cli::resolve_in_world(Binary::Bz, &overrides),
            }))
        }),
    )
}

struct App {
    model: AppModel,
    // Held so the bridge thread lives as long as the window; dropped on exit.
    _bridge: Bridge,
    // Every RAM surface the shell owns: the action/start drafts, the inspector
    // ephemera, and the config editors (§3.5 — discarded on exit).
    state: ShellState,
    // The mutating-verb binaries: message/stop/scan/prompt on `lernie`,
    // close/unclaim/create/update on `bl` (§8.2), and `bz --login` — bz's one
    // interactive verb — streamed from the toolchain pane (§8.3). Ball *reads* go
    // through the model's own `BlCli`; these drive the short *actions*.
    lernie: Cli,
    bl: Cli,
    bz: Cli,
}

impl eframe::App for App {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        // Re-derive on any bridge-delivered dirt or due sweep (§7.2).
        self.model.tick();
        // Poll floor (I4): wake at least every cheap-sweep interval even absent
        // interaction or an fs event, so correctness never rides on a watch.
        ctx.request_repaint_after(yog::app::CHEAP_SWEEP);
        shell::render(
            ctx,
            &mut self.model,
            &mut self.state,
            &self.lernie,
            &self.bl,
            &self.bz,
        );
    }

    fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
        // Flush any pending ui.json change (pins/collapsed/seen) on close.
        let _ = self.model.flush_ui();
    }
}