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"));
}
}