Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

dwarves-gui/src/settings_tree.rs

//! **The settings tree, as ⚒ draws it** — a VIEW over edda's
//! [`nornir_build_thing::tree::SettingsTree`] (stage 1 of the model, `8d6110f`): one
//! recursive node per variant, **feature ▸ setting ▸ val**, paths as ids, the kernel
//! donor as the first edge. edda owns the SHAPE; this module adds what a screen needs
//! and edda's data cannot carry: the value as it stands, and the prompt flag each node
//! equals.
//!
//! ```text
//! feature   on/off       a checkbox      --opt dhcp=off
//! setting   typed value  a field         --param dhcp.static_ip=10.0.2.15/24
//! val       one-of       a strip         --opt kernel.preset=nuked
//! ```
//!
//! **Law C is on screen**: every node carries its PATH (`dhcp.static_ip`) and the flag
//! it equals on the prompt, so the click and the flag are visibly the same statement.
//! A node with NO prompt spelling says so in as many words — an open cargo feature has
//! none today, and a checkbox that reads like a preference while reaching no build is
//! exactly the drift this file exists to make visible.
//!
//! # The `kärna` node, not a kernel leaf
//!
//! The kernel is not a special leaf; it is the first feature whose subtree is deep and
//! delegated. edda spells it two ways and this view shows ONE: an installer's tree has
//! a `kernel` feature root carrying the donor EDGE (`tunnr/tunnr-kernel : kernel`) with
//! `kernel.preset` / `kernel.profile` / `kernel.<symbol>` under it; a kernel variant's
//! tree IS the kernel (`preset`, `profile`, `<symbol>` at its root, typ `kernel`). The
//! view wraps the second shape under a synthetic `kärna` node at path `kernel` — the
//! spelling the prompt already uses (`--opt kernel.preset=nuked`,
//! `split_kernel_opts`) — so a robot, an operator and the CLI all address the kernel
//! at the same path whichever variant carries it. Its DETAIL is the kconfig canvas the
//! retired kernel leaf used to hold.
//!
//! Presence is the node's: it is on screen exactly when the resolved variant has the
//! `kärna` feature (its own `[[build.kernel]]`, or the edge). Nothing resolved → no
//! tree. A library → a tree with no `kärna`. gunnar → the node naming its donor.
//! tunnr → the node, the configurator under it.

use std::collections::BTreeMap;

use nornir_build_thing::tree::{Edge, NodeKind, SettingsNode, SettingsTree, KERNEL_PATH, PATH_SEP};
use nornir_build_thing::variant::ArtifactKind;
use serde_json::{json, Value};

use crate::build_scene::DeclaredParam;

/// The `kärna` node's label — the owner's word for it.
pub const KERNEL_LABEL: &str = "kärna";

/// One node, as drawn.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeView {
    /// The id, in the view's spelling: `dhcp.static_ip`, `kernel.preset`.
    pub path: String,
    /// The FLAT settings key this node reads and writes — the alias the scene's
    /// `chosen` map is keyed by (`static_ip` for `dhcp.static_ip`, `preset` for
    /// `kernel.preset`). edda's `SettingsTree::resolve` accepts exactly this alias.
    pub key: String,
    /// What the control says.
    pub label: String,
    /// `feature` | `setting` | `val` — edda's word.
    pub kind: &'static str,
    /// **The prompt spelling this node equals** (law C), or `None` when there is none
    /// — then [`NodeView::no_flag_why`] says why, on screen.
    pub flag: Option<String>,
    /// Why there is no flag, when there is none.
    pub no_flag_why: Option<String>,
    /// The current value (a feature: `true`/`false`; a val: the chosen word).
    pub value: String,
    /// A val's vocabulary.
    pub choices: Vec<String>,
    /// A setting's declared shape (label, kind, required, fixture) for the field.
    pub declared: Option<DeclaredParam>,
    /// A feature's cargo feature or kconfig symbol.
    pub feature: Option<String>,
    /// A kconfig symbol feature — its click goes through the kernel model's door.
    pub symbol: bool,
    /// A feature the variant declares ON and does not open: shown, not switchable.
    pub fixed: bool,
    /// A governed setting: applies only while its parent is in THIS state.
    pub when: Option<bool>,
    /// **A consumption edge** `repo/variant : typ`.
    pub edge: Option<String>,
    /// What refuses when this feature goes off. Empty ⇒ free.
    pub locked_by: Vec<String>,
    /// The children.
    pub children: Vec<NodeView>,
}

impl NodeView {
    /// Whether a feature is on.
    #[must_use]
    pub fn is_on(&self) -> bool {
        self.value == "true"
    }

    /// This node and its children, as data.
    #[must_use]
    pub fn state_json(&self) -> Value {
        json!({
            "path": self.path,
            "key": self.key,
            "label": self.label,
            "kind": self.kind,
            "flag": self.flag,
            "no_flag_why": self.no_flag_why,
            "value": self.value,
            "fixed": self.fixed,
            "edge": self.edge,
            "when": self.when,
            "locked_by": self.locked_by,
            "children": self.children.iter().map(NodeView::state_json).collect::<Vec<_>>(),
        })
    }

    fn find(&self, path: &str) -> Option<&NodeView> {
        if self.path == path {
            return Some(self);
        }
        self.children.iter().find_map(|c| c.find(path))
    }
}

/// The tree of one variant, as drawn.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TreeView {
    /// The variant.
    pub variant: String,
    /// Its typ.
    pub typ: ArtifactKind,
    /// The roots, in edda's order; `kärna` where edda put it (last for an installer).
    pub roots: Vec<NodeView>,
}

/// **What the view needs beyond edda's shape**: the values as they stand and the
/// kernel model's cascade.
pub struct Standing<'a> {
    /// The operator's edits, by flat key.
    pub chosen: &'a BTreeMap<String, String>,
    /// A kconfig symbol feature's state as the tree's judge last decided it (cascade
    /// included), by the node's PATH in the view's spelling (`kernel.virtio-net`).
    /// Absent ⇒ the node's declared default.
    pub symbols: &'a BTreeMap<String, bool>,
}

impl TreeView {
    /// Build the view over edda's tree.
    #[must_use]
    pub fn of(tree: &SettingsTree, standing: &Standing<'_>) -> Self {
        let mut roots: Vec<NodeView> = Vec::new();
        if tree.typ == ArtifactKind::Kernel {
            // The variant IS the kernel: one `kärna` node, the roots under it at
            // `kernel.<name>` — the prompt's own spelling.
            //
            // The SCOPE is edda's now (lane AK, 2026-08-30): `SettingsTree::of_kernel`
            // spells a kernel row's nodes `kernel.preset` / `kernel.virtio-net`
            // itself, because that is the spelling every door accepts and the one
            // `--list-options` prints. This view used to add the prefix here — the
            // pane compensating for a tree that disagreed with the CLI — and adding
            // it now would spell `kernel.kernel.preset`. One writer, edda's.
            let mut k = kernel_head(&tree.variant, None);
            k.children = tree
                .roots
                .iter()
                .map(|n| view(n, None, tree, standing))
                .collect();
            roots.push(k);
        } else {
            for n in &tree.roots {
                roots.push(view(n, None, tree, standing));
            }
        }
        TreeView {
            variant: tree.variant.clone(),
            typ: tree.typ,
            roots,
        }
    }

    /// A node by path.
    #[must_use]
    pub fn find(&self, path: &str) -> Option<&NodeView> {
        self.roots.iter().find_map(|r| r.find(path))
    }

    /// The `kärna` node, when the variant has one.
    #[must_use]
    pub fn kernel(&self) -> Option<&NodeView> {
        self.roots.iter().find(|r| r.path == KERNEL_PATH)
    }

    /// **The prompt, as the tree stands** — every flag the nodes equal, in tree order:
    /// what a click in ⚒ equals on the prompt, whole.
    #[must_use]
    pub fn flags(&self) -> Vec<String> {
        fn walk(n: &NodeView, out: &mut Vec<String>) {
            if let Some(f) = &n.flag {
                out.push(f.clone());
            }
            for c in &n.children {
                walk(c, out);
            }
        }
        let mut out = Vec::new();
        for r in &self.roots {
            walk(r, &mut out);
        }
        out
    }

    /// The view, as data.
    #[must_use]
    pub fn state_json(&self) -> Value {
        json!({
            "variant": self.variant,
            "typ": self.typ.as_str(),
            "roots": self.roots.iter().map(NodeView::state_json).collect::<Vec<_>>(),
        })
    }
}

/// Whether a tree IS a kernel (typ `kernel`): its roots are the kernel's own, wrapped
/// under `kärna` by the view and spelled `kernel.*` on the prompt.
#[must_use]
pub fn is_kernel_tree(tree: &SettingsTree) -> bool {
    tree.typ == ArtifactKind::Kernel
}

/// The synthetic head over a kernel variant's roots, or the installer's edge node.
fn kernel_head(variant: &str, edge: Option<String>) -> NodeView {
    NodeView {
        path: KERNEL_PATH.to_string(),
        key: KERNEL_PATH.to_string(),
        label: KERNEL_LABEL.to_string(),
        kind: "feature",
        // A consumption edge IS a flag (law C, `flags_of`): `--build kernel` — the verb
        // `BYGG kärnan` in the node's menu runs exactly that line. A kernel variant's
        // own `kärna` has no flag: the variant is the kernel.
        flag: edge.as_ref().map(|_| format!("--build {KERNEL_PATH}")),
        no_flag_why: match &edge {
            Some(_) => None,
            None => Some(format!("`{variant}` ÄR kärnan — dess val är barnen nedan")),
        },
        value: "true".to_string(),
        choices: Vec::new(),
        declared: None,
        feature: None,
        symbol: false,
        fixed: true,
        when: None,
        edge,
        locked_by: Vec::new(),
        children: Vec::new(),
    }
}

/// The view's spelling of a path: a kernel variant's roots live under `kernel.`.
fn spelled(prefix: Option<&str>, path: &str) -> String {
    match prefix {
        Some(p) => format!("{p}{PATH_SEP}{path}"),
        None => path.to_string(),
    }
}

fn view(
    n: &SettingsNode,
    prefix: Option<&str>,
    tree: &SettingsTree,
    standing: &Standing<'_>,
) -> NodeView {
    let path = spelled(prefix, &n.path);
    let key = n.name().to_string();
    let is_kernel_root = prefix.is_none() && n.path == KERNEL_PATH && n.edge.is_some();
    if is_kernel_root {
        let edge = n.edge.as_ref().map(Edge::spelled);
        let mut k = kernel_head(&tree.variant, edge);
        k.children = n
            .children
            .iter()
            .map(|c| view(c, None, tree, standing))
            .collect();
        return k;
    }
    let children: Vec<NodeView> = n
        .children
        .iter()
        .map(|c| view(c, prefix, tree, standing))
        .collect();
    let label = n.display_label().to_string();
    match &n.kind {
        NodeKind::Feature(f) => {
            let symbol = f.feature.starts_with("CONFIG_");
            let on = if symbol {
                standing.symbols.get(&path).copied().unwrap_or(f.default)
            } else {
                match standing.chosen.get(&key).map(String::as_str) {
                    Some("true") => true,
                    Some("false") => false,
                    _ => f.default,
                }
            };
            // A cargo variant's feature (exe/lib/…): declared ON is "in the variant"
            // and not switchable; declared OPEN has no prompt spelling today. An
            // installer's / iso's feature is an opt-out with `--opt`.
            // An opt-out with a prompt spelling is an INSTALLER's or an ISO's feature
            // (`[[build.*.options]]`); every other typ's feature is a cargo feature the
            // prompt has no flag for today.
            let cargo_typ = !matches!(tree.typ, ArtifactKind::Installer | ArtifactKind::Iso);
            // A feature bound to another build thing's artifact (wave 2's typed edge on
            // ANY variant kind) equals `--build <path>`, and the node bears BYGG.
            let edge = n.edge.as_ref().map(Edge::spelled);
            let (flag, no_flag_why, fixed) = if edge.is_some() {
                (Some(format!("--build {path}")), None, true)
            } else if symbol {
                (Some(format!("--opt {path}={}", onoff(on))), None, false)
            } else if cargo_typ && f.default {
                (
                    None,
                    Some("i varianten — deklarerad på, inget val".to_string()),
                    true,
                )
            } else if cargo_typ {
                // An OPEN cargo feature is a real flag now: ticked, it is the
                // `--features <f>` the build gets (CLI, ⚒ and the forge's BuildThing
                // RPC alike); unticked it is not on the command line at all.
                (on.then(|| format!("--features {}", f.feature)), None, false)
            } else {
                (Some(format!("--opt {path}={}", onoff(on))), None, false)
            };
            NodeView {
                path,
                key,
                label: if symbol { f.feature.clone() } else { label },
                kind: n.kind.word(),
                flag,
                no_flag_why,
                value: on.to_string(),
                choices: Vec::new(),
                declared: None,
                feature: Some(f.feature.clone()),
                symbol,
                fixed,
                when: n.when,
                edge,
                locked_by: f.locked_by.clone(),
                children,
            }
        }
        NodeKind::Setting(s) => {
            let value = standing.chosen.get(&key).cloned().unwrap_or_default();
            let shown = if value.trim().is_empty() {
                "…".to_string()
            } else {
                value.clone()
            };
            NodeView {
                path: path.clone(),
                key: key.clone(),
                label: label.clone(),
                kind: n.kind.word(),
                flag: Some(format!("--param {path}={shown}")),
                no_flag_why: None,
                value,
                choices: Vec::new(),
                declared: Some(DeclaredParam {
                    label,
                    kind: s.param_kind,
                    required: s.required,
                    fixture: s.fixture.map(|f| f.label().to_string()),
                    multiline: s.param_kind == nornir_build_thing::params::ParamKind::Text
                        && !s.staged_file.trim().is_empty(),
                }),
                feature: None,
                symbol: false,
                fixed: false,
                when: n.when,
                edge: None,
                locked_by: Vec::new(),
                children,
            }
        }
        NodeKind::Val(v) => {
            let value = standing
                .chosen
                .get(&key)
                .cloned()
                .unwrap_or_else(|| v.default.clone());
            NodeView {
                path: path.clone(),
                key,
                label,
                kind: n.kind.word(),
                flag: Some(format!("--opt {path}={value}")),
                no_flag_why: None,
                value,
                choices: v.choices.clone(),
                declared: None,
                feature: None,
                symbol: false,
                fixed: false,
                when: n.when,
                edge: None,
                locked_by: Vec::new(),
                children,
            }
        }
    }
}

fn onoff(on: bool) -> &'static str {
    if on {
        "on"
    } else {
        "off"
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use nornir_build_thing::variant::BuildVariantSet;

    const INSTALLER: &str = r#"
[build]
base_suite = "ORIGINAL"

[[build.variant]]
name = "orten-server"
package = "orten"
bin = "orten"
role = "server"
default = true
suites = ["ORIGINAL"]
kind = "exe"
open = ["gpu"]

[[build.installer]]
name = "orten-installer"
variant = "orten-server"
kernel_donor = { repo = "tunnr", kernel = "tunnr-kernel" }

[[build.installer.params]]
name = "site"
kind = "text"
required = true

[[build.installer.params]]
name = "static_ip"
kind = "text"

[[build.installer.options]]
name = "dhcp"
label = "DHCP"
feature = "dhcp"
params_when_off = ["static_ip"]
"#;

    fn set() -> BuildVariantSet {
        BuildVariantSet::from_toml_str(INSTALLER).expect("the fixture parses")
    }

    /// **Law C on the nodes, over edda's shape**: an option is a feature with `--opt`,
    /// its governed setting under it with `--param dhcp.static_ip=`, a root setting with
    /// `--param site=`, the donor edge as the `kärna` node with `kernel.preset` a val
    /// spelled `--opt kernel.preset=`.
    #[test]
    fn the_view_carries_edda_paths_and_the_flags_they_equal() {
        let set = set();
        let tree = SettingsTree::of_set(&set, "orten-installer").expect("the installer's tree");
        let mut chosen = BTreeMap::new();
        chosen.insert("dhcp".to_string(), "false".to_string());
        chosen.insert("static_ip".to_string(), "10.0.2.15/24".to_string());
        let symbols = BTreeMap::new();
        let v = TreeView::of(
            &tree,
            &Standing {
                chosen: &chosen,
                symbols: &symbols,
            },
        );
        assert_eq!(v.typ, ArtifactKind::Installer);
        let dhcp = v.find("dhcp").expect("the option is a feature root");
        assert_eq!(dhcp.kind, "feature");
        assert_eq!(dhcp.flag.as_deref(), Some("--opt dhcp=off"));
        let ip = v
            .find("dhcp.static_ip")
            .expect("the governed setting is its child");
        assert_eq!(ip.key, "static_ip", "the flat alias survives");
        assert_eq!(ip.when, Some(false));
        assert_eq!(
            ip.flag.as_deref(),
            Some("--param dhcp.static_ip=10.0.2.15/24")
        );
        assert_eq!(
            v.find("site").unwrap().flag.as_deref(),
            Some("--param site=…")
        );
        let k = v.kernel().expect("the donor edge is the kärna node");
        assert_eq!(k.label, KERNEL_LABEL);
        assert_eq!(k.edge.as_deref(), Some("tunnr/tunnr-kernel : kernel"));
        let preset = v.find("kernel.preset").expect("kernel.preset under kärna");
        assert_eq!(preset.kind, "val");
        assert_eq!(preset.key, "preset");
        assert!(
            preset
                .flag
                .as_deref()
                .is_some_and(|f| f.starts_with("--opt kernel.preset=")),
            "{:?}",
            preset.flag
        );
        assert!(k.children.iter().any(|c| c.symbol
            && c.flag
                .as_deref()
                .is_some_and(|f| f.starts_with("--opt kernel."))));
        assert!(v.flags().iter().any(|f| f == "--opt dhcp=off"));
    }

    /// An open cargo feature IS a flag: ticked, `--features <f>` (the spelling the
    /// CLI, ⚒ and the forge's RPC carry); unticked, nothing — and no notice saying it
    /// reaches nothing. A declared-on one is fixed.
    #[test]
    fn a_cargo_variants_features_say_what_they_are() {
        let set = set();
        let tree = SettingsTree::of_set(&set, "orten-server").expect("the exe's tree");
        let v = TreeView::of(
            &tree,
            &Standing {
                chosen: &BTreeMap::new(),
                symbols: &BTreeMap::new(),
            },
        );
        let gpu = v.find("gpu").expect("the open feature");
        assert!(gpu.flag.is_none() && !gpu.fixed, "unticked: not on the command line");
        assert_eq!(gpu.no_flag_why, None, "no notice: the feature has a flag");
        let ticked: BTreeMap<String, String> = [("gpu".to_string(), "true".to_string())].into();
        let v2 = TreeView::of(&tree, &Standing { chosen: &ticked, symbols: &BTreeMap::new() });
        assert_eq!(v2.find("gpu").unwrap().flag.as_deref(), Some("--features gpu"), "ticked: the flag the build gets");
        assert!(v2.flags().iter().any(|f| f == "--features gpu"));
        assert!(v.kernel().is_none(), "an exe variant has no kärna");
    }

    /// A kernel variant IS the kernel: its roots are wrapped under `kärna` at `kernel`,
    /// spelled the way the prompt spells them, and the symbol states come from the
    /// model's cascade rather than the declared default.
    #[test]
    fn a_kernel_variant_is_wrapped_under_the_kernel_node() {
        let pin = "949d166a1263bd0cf0c5aab44eeb57869615099e7c48bf88d0e4adb77379b999";
        let set = BuildVariantSet::from_toml_str(&format!(
            "[build]\nbase_suite = \"ORIGINAL\"\n\n[[build.kernel]]\nname = \"orten-kernel\"\nversion = \"6.12.1\"\npreset = \"nuked\"\ntarball_sha256 = \"{pin}\"\n"
        ))
        .expect("parses");
        let tree = SettingsTree::of_set(&set, "orten-kernel").expect("the kernel's tree");
        assert_eq!(tree.typ, ArtifactKind::Kernel);
        let mut symbols = BTreeMap::new();
        let first = tree.roots.iter().find_map(|n| match &n.kind {
            NodeKind::Feature(f) if f.feature.starts_with("CONFIG_") => {
                Some((n.path.clone(), f.feature.clone()))
            }
            _ => None,
        });
        let (first_path, first) = first.expect("the pick has symbols");
        // The path is ALREADY `kernel.<name>` — edda scopes a kernel row's tree
        // itself since lane AK, and this view no longer re-prefixes it.
        symbols.insert(first_path.clone(), false);
        let v = TreeView::of(
            &tree,
            &Standing {
                chosen: &BTreeMap::new(),
                symbols: &symbols,
            },
        );
        let k = v.kernel().expect("the synthetic kärna head");
        assert!(k.edge.is_none());
        assert_eq!(
            v.find("kernel.preset").unwrap().flag.as_deref(),
            Some("--opt kernel.preset=nuked")
        );
        let sym = k
            .children
            .iter()
            .find(|c| c.feature.as_deref() == Some(first.as_str()))
            .unwrap();
        assert_eq!(
            sym.value, "false",
            "the cascade state wins over the declared default"
        );
        assert!(sym.flag.as_deref().unwrap().ends_with("=off"));
    }
}