yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The congeries palette — yog's single colour authority (DESIGN §11).
//!
//! Yog-Sothoth manifests as *a congeries of iridescent globes* — and yog's
//! visual identity is exactly that: luminous sphere-hues against the void.
//! Every colour the UI paints is named here, once; renderers import the hue,
//! never restate an RGB triple. The naming keys each hue to the platform's
//! lore so the vocabulary is stable across the tools yog drives: **lernie**
//! (the hydra) owns the liveness green, **brazen** (the brazen head) owns the
//! oracle bronze, and yog itself — the key and the gate — owns the violet.
//!
//! [`visuals`] derives the whole egui [`egui::Visuals`] (void backgrounds,
//! gate-violet selection, moonlit text) and [`apply`] installs it at eframe
//! bring-up; [`pulse`] is the shared in-flight animation colour; [`wordmark`]
//! is the congeries mark both wordmark seats render.

/// Hydra green — lernie's liveness hue: the `Live` state badge, ✓ ok tool
/// results, `Complete` framing, healthy toolchain rows.
pub const HYDRA: egui::Color32 = egui::Color32::from_rgb(110, 222, 148);

/// Spectral blue — light mid-transit through the gate: the `InFlight` badge,
/// the live streaming tail, the pulsing tool chip, hyperlinks.
pub const SPECTRE: egui::Color32 = egui::Color32::from_rgb(118, 188, 242);

/// Brazen bronze — the oracle head's metal: pending-mail ✉, warnings,
/// budget-exhausted marks, attention counts.
pub const BRAZEN: egui::Color32 = egui::Color32::from_rgb(232, 176, 96);

/// Tarnished brazen — the `Quiescent` (finished-for-now) badge: still warm,
/// no longer bright.
pub const BRAZEN_DIM: egui::Color32 = egui::Color32::from_rgb(184, 152, 104);

/// Ichor red — the wound hue: ✗ error results, `Failed` framing,
/// declined-transfer marks, fatal ops rows.
pub const ICHOR: egui::Color32 = egui::Color32::from_rgb(242, 108, 120);

/// Ash grey — `Stopped` / `Killed`: nothing behind the gate.
pub const ASH: egui::Color32 = egui::Color32::from_rgb(150, 150, 166);

/// Sigil magenta — the "?" uncertainty suffix (probe-degraded, DESIGN
/// §10/§11): never blends into a definite state's hue.
pub const SIGIL: egui::Color32 = egui::Color32::from_rgb(230, 118, 214);

/// Gate violet — yog's own hue: selection, focus, the wordmark. "It is the
/// key and the gate."
pub const GATE: egui::Color32 = egui::Color32::from_rgb(160, 112, 240);

/// The tagline both wordmark seats may set beneath the mark.
pub const TAGLINE: &str = "the key and the gate";

// The void — background strata, darkest at the deepest inset. Private: only
// `visuals()` composes backgrounds; renderers never paint their own.
const VOID_WINDOW: egui::Color32 = egui::Color32::from_rgb(15, 12, 22);
const VOID_PANEL: egui::Color32 = egui::Color32::from_rgb(19, 15, 27);
const VOID_DEEP: egui::Color32 = egui::Color32::from_rgb(10, 8, 15);
const VOID_FAINT: egui::Color32 = egui::Color32::from_rgb(30, 24, 43);
const VOID_EDGE: egui::Color32 = egui::Color32::from_rgb(54, 43, 78);

// Moonlit text ramp — lavender-cast greys, brightening with interactivity.
const MOONLIT: egui::Color32 = egui::Color32::from_rgb(198, 190, 216);
const MOONLIT_BRIGHT: egui::Color32 = egui::Color32::from_rgb(216, 208, 234);
const MOONLIT_FULL: egui::Color32 = egui::Color32::from_rgb(240, 234, 250);

/// Repaint cadence for pulsing in-flight indicators — ~30 fps, smooth enough
/// for the eye and cheap enough for an idle UI. One cadence, so the whole UI
/// pulses in step.
pub const PULSE_REPAINT_DELAY: std::time::Duration = std::time::Duration::from_millis(33);

/// Pulse frequency in radians per second — ~0.6 Hz, slow enough to read as
/// "alive" rather than "blinking error".
const PULSE_RATE_RAD_PER_SEC: f64 = 4.0;

/// The in-flight pulse colour at animation-clock `time` (seconds): SPECTRE
/// breathing between dim and full. Shared by every pulsing indicator
/// (git_tree tool rows, transcript tool chips) so they beat as one.
pub fn pulse(time: f64) -> egui::Color32 {
    let alpha = (0.5 + 0.5 * (time * PULSE_RATE_RAD_PER_SEC).sin()).clamp(0.0, 1.0);
    SPECTRE.gamma_multiply(0.35 + 0.65 * alpha as f32)
}

/// The whole-app [`egui::Visuals`]: egui's dark theme re-grounded in the
/// void strata, with gate-violet selection and the moonlit text ramp. Pure —
/// testable without a window; [`apply`] installs it.
pub fn visuals() -> egui::Visuals {
    let mut v = egui::Visuals::dark();
    v.panel_fill = VOID_PANEL;
    v.window_fill = VOID_WINDOW;
    v.extreme_bg_color = VOID_DEEP;
    v.code_bg_color = VOID_DEEP;
    v.faint_bg_color = VOID_FAINT;
    v.window_stroke = egui::Stroke::new(1.0, VOID_EDGE);
    v.selection.bg_fill = GATE.gamma_multiply(0.35);
    v.selection.stroke = egui::Stroke::new(1.0, GATE);
    v.hyperlink_color = SPECTRE;
    v.warn_fg_color = BRAZEN;
    v.error_fg_color = ICHOR;
    v.widgets.noninteractive.bg_fill = egui::Color32::from_rgb(24, 19, 34);
    v.widgets.noninteractive.weak_bg_fill = VOID_FAINT;
    v.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, VOID_EDGE);
    v.widgets.noninteractive.fg_stroke.color = MOONLIT;
    v.widgets.inactive.bg_fill = egui::Color32::from_rgb(36, 29, 52);
    v.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(28, 22, 40);
    v.widgets.inactive.fg_stroke.color = MOONLIT_BRIGHT;
    v.widgets.hovered.bg_fill = egui::Color32::from_rgb(48, 38, 70);
    v.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, GATE.gamma_multiply(0.6));
    v.widgets.hovered.fg_stroke.color = MOONLIT_FULL;
    v.widgets.active.bg_fill = egui::Color32::from_rgb(60, 47, 88);
    v.widgets.active.bg_stroke = egui::Stroke::new(1.0, GATE);
    v.widgets.active.fg_stroke.color = MOONLIT_FULL;
    v.widgets.open.bg_fill = egui::Color32::from_rgb(36, 29, 52);
    v.widgets.open.fg_stroke.color = MOONLIT_BRIGHT;
    v
}

/// Install the congeries visuals on an egui context — called once from the
/// eframe creation closure (main.rs, coverage-excluded), so theming rides a
/// covered pure function plus this one-line seam.
pub fn apply(ctx: &egui::Context) {
    ctx.set_visuals(visuals());
}

/// Glyph + colour for each §3.5 agent state — the state-badge mapping every
/// badge seat shares (conversation rows, descent-tree members). Pure over the
/// enum, so tests assert the mapping without a windowing context; the palette
/// module is its one home (§11: renderers never restate an RGB triple).
pub fn state_badge(state: crate::git_tree::AgentState) -> (&'static str, egui::Color32) {
    use crate::git_tree::AgentState;
    match state {
        // A driver holds the lock, not in a model call: solid + hydra green.
        AgentState::Live => ("●", HYDRA),
        // A model call is streaming right now: half-filled + spectral blue.
        AgentState::InFlight => ("◐", SPECTRE),
        // Finished-for-now, awaiting a message: hollow + tarnished brazen.
        AgentState::Quiescent => ("○", BRAZEN_DIM),
        // No live executor, no clean terminal: square + ash.
        AgentState::Stopped => ("■", ASH),
    }
}

/// The hue of a driven integration, keyed by its CLI name: `lernie` the
/// hydra (green), `bz` the brazen head (bronze), `bl` the balls — the
/// congeries itself, so it wears the gate's violet. Anything else reads in
/// the moonlit text default. The one authoritative home for the tool → hue
/// branding (toolchain rows, config-editor headings).
pub fn integration_hue(tool: &str) -> egui::Color32 {
    match tool {
        "lernie" => HYDRA,
        "bz" => BRAZEN,
        "bl" => GATE,
        _ => MOONLIT,
    }
}

/// The hue of a conversation's start-flow ball badge, keyed by its §3.5 join
/// state (DESIGN §3.5, §11): the derived status coloured at a glance. `Bound` is
/// the healthy working ball (hydra green); `Delivered` has gone quiet (ash);
/// `Blocked`/`ClaimedElsewhere` want a look (brazen); an `OrphanedProject` ball
/// is a wound (ichor). The startable/unassigned states never head a conversation
/// ball, so they read in the neutral moonlit default. One home for the mapping;
/// a stamp the join could not resolve (`None` state) the caller paints moonlit.
pub fn ball_hue(state: crate::projects::join::JoinState) -> egui::Color32 {
    use crate::projects::join::JoinState;
    match state {
        JoinState::Bound => HYDRA,
        JoinState::Delivered => ASH,
        JoinState::Blocked | JoinState::ClaimedElsewhere => BRAZEN,
        JoinState::OrphanedProject => ICHOR,
        JoinState::ReadyStartable | JoinState::UnassignedWorkspace => MOONLIT,
    }
}

/// The wordmark: a small congeries of iridescent spheres, then "yog" in gate
/// violet. Seats: the attention strip and the empty-workspace placeholder
/// (§11 altitude 0/1) — the same mark everywhere, so it reads as identity,
/// not decoration.
pub fn wordmark(ui: &mut egui::Ui) {
    ui.horizontal(|ui| {
        ui.spacing_mut().item_spacing.x = 1.5;
        for (hue, sphere) in [(HYDRA, "•"), (GATE, "●"), (BRAZEN, "•")] {
            ui.colored_label(hue, sphere);
        }
        ui.add_space(5.0);
        ui.heading(egui::RichText::new("yog").color(GATE).strong());
    });
}

#[cfg(test)]
mod tests;