use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use rhai::{Dynamic, Engine, Map};
use linesmith_plugin::{CompiledPlugin, PluginRegistry};
use super::super::{
built_in_by_id, LineItem, Segment, Separator, BUILT_IN_SEGMENT_IDS, DEFAULT_SEGMENT_IDS,
};
use super::layout::{resolve_layout_separator, single_line_entries, validated_numbered_lines};
use super::plugins::{apply_override, bundle_plugins, toml_table_to_dynamic};
use crate::config;
use crate::plugins::RhaiSegment;
#[must_use]
pub fn build_default_segments() -> Vec<LineItem> {
debug_assert!(
DEFAULT_SEGMENT_IDS
.iter()
.all(|id| BUILT_IN_SEGMENT_IDS.contains(id)),
"DEFAULT_SEGMENT_IDS must be a subset of BUILT_IN_SEGMENT_IDS so the \
`Cow::Borrowed(*id)` shortcut here matches what resolve_segment_id would emit"
);
let segs: Vec<(Cow<'static, str>, Box<dyn Segment>)> = DEFAULT_SEGMENT_IDS
.iter()
.filter_map(|id| built_in_by_id(id, None, &mut |_| {}).map(|seg| (Cow::Borrowed(*id), seg)))
.collect();
interleave_separators(segs, &Separator::Space)
}
fn interleave_separators(
segs: Vec<(Cow<'static, str>, Box<dyn Segment>)>,
sep: &Separator,
) -> Vec<LineItem> {
let n = segs.len();
let mut items = Vec::with_capacity(n.saturating_mul(2).saturating_sub(1));
for (i, (id, segment)) in segs.into_iter().enumerate() {
items.push(LineItem::Segment { id, segment });
if i + 1 < n {
items.push(LineItem::Separator(sep.clone()));
}
}
items
}
pub fn build_segments(
config: Option<&config::Config>,
plugins: Option<(PluginRegistry, Arc<Engine>)>,
mut warn: impl FnMut(&str),
) -> Vec<LineItem> {
let configured_line = config.and_then(|c| c.line.as_ref());
let layout_mode = config.map(|c| c.layout).unwrap_or_default();
if matches!(layout_mode, config::LayoutMode::MultiLine) {
if let Some(first) = validated_numbered_lines(configured_line, &mut warn)
.and_then(|mut v| (!v.is_empty()).then(|| v.remove(0)))
{
warn("layout = \"multi-line\" passed to build_segments (the single-line API); rendering line 1 only. Call build_lines to render every [line.N] sub-table.");
let layout_separator = resolve_layout_separator(config, &mut warn);
let mut plugin_bundle = bundle_plugins(plugins);
let mut consumed = std::collections::HashSet::new();
return build_one_line(
&first,
config,
&mut plugin_bundle,
&mut consumed,
&layout_separator,
&mut warn,
);
}
}
if let Some(line) = configured_line {
if line.segments.is_empty() {
warn("[line].segments is empty; no segments will render");
}
}
let layout_separator = resolve_layout_separator(config, &mut warn);
let entries: Vec<config::LineEntry> = match configured_line {
Some(l) => l.segments.clone(),
None => DEFAULT_SEGMENT_IDS
.iter()
.map(|&s| config::LineEntry::Id(s.to_string()))
.collect(),
};
let mut plugin_bundle = bundle_plugins(plugins);
let mut consumed = std::collections::HashSet::new();
build_one_line(
&entries,
config,
&mut plugin_bundle,
&mut consumed,
&layout_separator,
&mut warn,
)
}
pub fn build_lines(
config: Option<&config::Config>,
plugins: Option<(PluginRegistry, Arc<Engine>)>,
mut warn: impl FnMut(&str),
) -> Vec<Vec<LineItem>> {
let mode = config.map(|c| c.layout).unwrap_or_default();
let line_cfg = config.and_then(|c| c.line.as_ref());
let line_entry_lists: Vec<Vec<config::LineEntry>> = match mode {
config::LayoutMode::SingleLine => {
let has_numbered = line_cfg.is_some_and(|l| !l.numbered.is_empty());
let has_segments = line_cfg.is_some_and(|l| !l.segments.is_empty());
if has_numbered && !has_segments {
if let Some(promoted) = validated_numbered_lines(line_cfg, &mut warn) {
warn("[line.N] sub-tables present but no top-level `layout` field; treating as multi-line. Add `layout = \"multi-line\"` to silence this warning.");
promoted
} else {
warn("[line.N] sub-tables present but none are usable, and [line].segments is empty; nothing will render");
single_line_entries(line_cfg, &mut warn)
}
} else {
if has_numbered {
warn("layout is single-line but [line.N] sub-tables are present; ignoring numbered tables and rendering [line].segments");
}
single_line_entries(line_cfg, &mut warn)
}
}
config::LayoutMode::MultiLine => match validated_numbered_lines(line_cfg, &mut warn) {
Some(lines) => lines,
None => {
warn("layout = \"multi-line\" but no usable [line.N] sub-tables; falling back to single-line using [line].segments");
single_line_entries(line_cfg, &mut warn)
}
},
};
let layout_separator = resolve_layout_separator(config, &mut warn);
let mut plugin_bundle = bundle_plugins(plugins);
let mut consumed_plugins = std::collections::HashSet::<String>::new();
line_entry_lists
.into_iter()
.map(|entries| {
build_one_line(
&entries,
config,
&mut plugin_bundle,
&mut consumed_plugins,
&layout_separator,
&mut warn,
)
})
.collect()
}
fn build_one_line(
entries: &[config::LineEntry],
config: Option<&config::Config>,
plugin_bundle: &mut Option<(HashMap<String, CompiledPlugin>, Arc<Engine>)>,
consumed_plugins: &mut std::collections::HashSet<String>,
layout_separator: &Separator,
warn: &mut impl FnMut(&str),
) -> Vec<LineItem> {
let mut seen = std::collections::HashSet::<String>::new();
let mut items: Vec<LineItem> = Vec::with_capacity(entries.len() * 2);
let mut merge_pending = false;
for entry in entries {
if matches!(entry, config::LineEntry::Item(item) if item.kind.as_deref() == Some("separator") && item.merge.is_some())
{
warn("[line].segments separator entry has `merge = ...`; ignoring (merge is for segment entries)");
}
if matches!(entry, config::LineEntry::Item(item) if item.kind.as_deref() != Some("separator") && item.character.is_some())
{
warn("[line].segments segment entry has `character = ...`; ignoring (character is for separator entries)");
}
match entry.kind() {
None => {
warn("[line].segments inline-table entry is missing `type`; skipping");
continue;
}
Some("separator") => {
if merge_pending {
continue;
}
match items.last() {
Some(LineItem::Segment { .. }) => {} Some(LineItem::Separator(_)) => {
warn(
"[line].segments has consecutive separator entries; keeping the first",
);
continue;
}
None => {
warn("[line].segments leads with a separator entry; skipping");
continue;
}
}
let sep = entry.separator_character().map_or_else(
|| layout_separator.clone(),
|c| Separator::Literal(Cow::Owned(c.to_string())),
);
items.push(LineItem::Separator(sep));
}
Some(id) => {
if !seen.insert(id.to_string()) {
warn(&format!(
"segment '{id}' listed more than once; keeping first occurrence"
));
continue;
}
let cfg_override = config.and_then(|c| c.segments.get(id));
let extras = cfg_override.map(|ov| &ov.extra);
let inner = if let Some(b) = built_in_by_id(id, extras, warn) {
Some(b)
} else if let Some((lookup, engine)) = plugin_bundle.as_mut() {
lookup.remove(id).map(|plugin| {
consumed_plugins.insert(id.to_string());
let plugin_config = cfg_override.map_or_else(
|| Dynamic::from_map(Map::new()),
|ov| toml_table_to_dynamic(&ov.extra),
);
Box::new(RhaiSegment::from_compiled(
plugin,
engine.clone(),
plugin_config,
)) as Box<dyn Segment>
})
} else {
None
};
let Some(inner) = inner else {
if consumed_plugins.contains(id) {
warn(&format!(
"plugin '{id}' was rendered on an earlier line; v0.1 supports each plugin on at most one line per render — skipping"
));
} else {
warn(&format!("unknown segment id '{id}' — skipping"));
}
continue;
};
let seg = apply_override(id, inner, cfg_override, warn);
if matches!(items.last(), Some(LineItem::Segment { .. })) && !merge_pending {
items.push(LineItem::Separator(layout_separator.clone()));
}
items.push(LineItem::Segment {
id: resolve_segment_id(id),
segment: seg,
});
merge_pending = entry.merge();
}
}
}
items
}
pub(super) fn resolve_segment_id(id: &str) -> Cow<'static, str> {
BUILT_IN_SEGMENT_IDS
.iter()
.find(|&&built_in| built_in == id)
.map_or_else(
|| Cow::Owned(id.to_string()),
|&built_in| Cow::Borrowed(built_in),
)
}