nornir 0.5.2

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
//! ⚒ The **Dwarfs tab** — nornir's CI/build forge, merging the former 🔨 Build and
//! ⚒ Brokkr tabs into one surface (N5).
//!
//! Recent nornir work renamed the CI crate/tool to **dwarfs** (`crates/dwarfs`,
//! the brokkr forge + the `sindri` scheduler). The two viz tabs were
//! **complementary**, not duplicates:
//!
//! - the **Build runner** ([`super::build_tab::BuildTabState`]) reflects a repo's
//!   `BuildSurface` into choosable target rows (binary × features × profile ×
//!   target), runs `nornir build --attest --seal`, and TEST-BOOTs the result as a
//!   job — a *single-target* builder;
//! - the **Forge** ([`super::brokkr_tab::BrokkrTabState`]) is the CI pipeline:
//!   the results/tree/DAG/agents/log facets over a `brokkr.toml`, plus the
//!   pipeline builder — a *multi-stage* pipeline runner.
//!
//! This tab hosts both behind a top-level pane switch (`🔨 Build` ⇄ `⚒ Forge`),
//! so the whole dwarfs story lives under one header. Each sub-pane keeps its own
//! `state_json` slice (surfaced at the top level as `build` / `brokkr` for
//! back-compat), and the tab adds its own `dwarfs` slice carrying the active pane.

use std::path::PathBuf;

use eframe::egui;

use super::build_tab::BuildTabState;
use super::brokkr_tab::BrokkrTabState;
use super::facett_theme::Theme;

/// The two panes of the merged dwarfs tab.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DwarfsPane {
    /// 🔨 the single-target build runner (`BuildTabState`).
    Build,
    /// ⚒ the CI pipeline forge (`BrokkrTabState`).
    Forge,
}

impl DwarfsPane {
    /// The exact selector label a user reads / a robot clicks.
    fn label(self) -> &'static str {
        match self {
            DwarfsPane::Build => "🔨 Build",
            DwarfsPane::Forge => "⚒ Forge",
        }
    }
    /// The `state_json["dwarfs"]["pane"]` name.
    fn id(self) -> &'static str {
        match self {
            DwarfsPane::Build => "Build",
            DwarfsPane::Forge => "Forge",
        }
    }
    const ALL: [DwarfsPane; 2] = [DwarfsPane::Build, DwarfsPane::Forge];
}

/// ⚒ The Dwarfs tab: the build runner + the CI forge under one header.
pub struct DwarfsTabState {
    pane: DwarfsPane,
    /// 🔨 the single-target build runner. `pub(crate)` so the app can inject a
    /// test runner + read its `state_json` slice at the top level.
    pub(crate) build: BuildTabState,
    /// ⚒ the CI pipeline forge (the former Brokkr tab). `pub(crate)` for the same
    /// reasons.
    pub(crate) forge: BrokkrTabState,
}

impl DwarfsTabState {
    /// The tab seeded with the build runner (over `workspace_root`) + the demo
    /// pipeline forge.
    pub fn new(workspace_root: PathBuf) -> Self {
        Self {
            pane: DwarfsPane::Build,
            build: BuildTabState::new(workspace_root),
            forge: BrokkrTabState::new(),
        }
    }

    /// Push the active palette into both sub-panes.
    pub fn set_palette(&mut self, p: Theme) {
        self.build.set_palette(p);
        self.forge.set_palette(p);
    }

    /// The observable state a headless test / operator reads: the active pane plus
    /// each sub-pane's slice (also mirrored at the top level of the app's
    /// `state_json` as `build` / `brokkr`).
    pub fn state_json(&self) -> serde_json::Value {
        serde_json::json!({
            "surface": "dwarfs",
            "pane": self.pane.id(),
            "build": self.build.state_json(),
            "brokkr": self.forge.state_json(),
        })
    }

    /// Draw the tab: a top-level pane switch, then the active sub-pane.
    pub fn draw(&mut self, ui: &mut egui::Ui) {
        ui.push_id("Dwarfs", |ui| {
            ui.horizontal(|ui| {
                ui.heading("⚒ Dwarfs — build + CI forge");
                ui.separator();
                for p in DwarfsPane::ALL {
                    ui.selectable_value(&mut self.pane, p, p.label());
                }
            });
            ui.separator();
            match self.pane {
                DwarfsPane::Build => self.build.draw(ui),
                DwarfsPane::Forge => self.forge.draw(ui),
            }
        });
    }
}