yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The per-project no-marks knob's shell pane (§16.3, §16.6 W4) — a thin egui
//! section of config mode. Coverage-excluded glue (`src/shell/*`,
//! tarpaulin.toml): the VM ([`crate::world::marks`]) carries all logic and is
//! headless-tested; this only wires buttons to it. Split from
//! [`super::config_edit`] so both stay under the 300-line cap.

use crate::AppModel;
use crate::world::marks::{self, BlConf, MarksState, Mode};
use crate::xdg::Env;
use std::path::{Path, PathBuf};

/// The knob's RAM state (§3.5): the injected `bl conf` runner, the focused
/// project's last-read state (and which project it was read for), the custom
/// branch input, and the status line.
pub struct MarksPane {
    runner: BlConf,
    project: Option<PathBuf>,
    state: Option<MarksState>,
    branch: String,
    status: String,
}

impl MarksPane {
    /// Resolve the world `bl` runner (§16.6 W2); everything else starts empty
    /// (read on demand).
    pub fn resolve(world: &Env) -> Self {
        Self {
            runner: BlConf::resolve(world),
            project: None,
            state: None,
            branch: String::new(),
            status: String::new(),
        }
    }
}

/// Render the knob for the focused project. Reads on demand (never per-frame),
/// applies one of the three [`Mode`]s, and renders the trade plainly.
pub fn render(ui: &mut egui::Ui, model: &mut AppModel, pane: &mut MarksPane) {
    ui.heading(
        egui::RichText::new("project marks (bl store branch)")
            .color(crate::theme::integration_hue("bl")),
    );
    let Some(project) = model.focused_join().map(|r| r.project.clone()) else {
        ui.weak("focus a workspace bound to a project to set its marks");
        return;
    };
    let state_root = model.state_root().to_path_buf();
    ui.monospace(project.display().to_string());
    if pane.project.as_deref() == Some(project.as_path())
        && let Some(state) = &pane.state
    {
        ui.label(format!("mode: {}", describe_mode(&state.mode)));
        ui.weak(format!("provenance: {}", state.provenance));
    }
    ui.weak(
        "stealth = claims invisible to ambient `bl list`; custom branch = a parallel task universe",
    );
    ui.horizontal(|ui| {
        if ui.button("Read current").clicked() {
            pane.status = read_marks(pane, &project);
        }
        if ui.button("Shared").clicked() {
            pane.status = apply_marks(pane, &state_root, &project, &Mode::Shared);
        }
        if ui.button("Stealth").clicked() {
            pane.status = apply_marks(pane, &state_root, &project, &Mode::Stealth);
        }
    });
    ui.horizontal(|ui| {
        ui.label("custom branch:");
        ui.text_edit_singleline(&mut pane.branch);
        if ui.button("Set custom branch").clicked() {
            pane.status = match Mode::custom_branch(&pane.branch) {
                Some(mode) => apply_marks(pane, &state_root, &project, &mode),
                None => "branch name required (and not the default)".to_string(),
            };
        }
    });
    if !pane.status.is_empty() {
        ui.weak(&pane.status);
    }
}

/// One-line rendering of a marks [`Mode`], trade included (§16.3).
fn describe_mode(mode: &Mode) -> String {
    match mode {
        Mode::Shared => "shared (origin / balls/tasks) — coordinates with ambient bl".to_string(),
        Mode::Stealth => "stealth (local-only) — invisible to ambient bl list".to_string(),
        Mode::CustomBranch(name) => format!("custom branch: {name} — a parallel task universe"),
    }
}

/// Read the focused project's knob into `pane.state` (keyed by project so a
/// focus change never shows a stale mode).
fn read_marks(pane: &mut MarksPane, project: &Path) -> String {
    match marks::read(&pane.runner, project) {
        Ok(state) => {
            pane.project = Some(project.to_path_buf());
            let msg = summary(state.as_ref());
            pane.state = state;
            msg
        }
        Err(e) => e.to_string(),
    }
}

/// Apply `mode` (logging to `ops.jsonl`), then adopt the re-read state.
fn apply_marks(pane: &mut MarksPane, state_root: &Path, project: &Path, mode: &Mode) -> String {
    match marks::apply(&pane.runner, state_root, &super::now_ts(), project, mode) {
        Ok(state) => {
            pane.project = Some(project.to_path_buf());
            let msg = summary(state.as_ref());
            pane.state = state;
            format!("applied — now {msg}")
        }
        Err(e) => e.to_string(),
    }
}

/// The knob-state summary ("not a balls project" when unprimed).
fn summary(state: Option<&MarksState>) -> String {
    match state {
        Some(s) => describe_mode(&s.mode),
        None => "not a balls project (run bl prime)".to_string(),
    }
}