use ek_core::Configurable;
use ek_core::cfg_face::{CfgHost, ConfigField};
use ek_core::plugin::Plugin;
use ek_core::section::{Field, SectionMap};
#[derive(Default)]
pub struct WinState {
pub open: bool,
rows: Vec<(String, String)>,
seed: Vec<String>,
pub notice: Option<String>,
}
impl WinState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[must_use]
pub fn mounted(plugins: &[std::sync::Arc<dyn Plugin>]) -> Vec<(String, String)> {
plugins
.iter()
.filter_map(|p| {
let c = p.as_configurable()?;
Some((p.name().to_string(), c.config_ns().to_string()))
})
.collect()
}
fn field_label(f: &ConfigField) -> String {
match &f.kind {
ek_core::ConfigKind::Int { unit: Some(u), .. }
| ek_core::ConfigKind::Float { unit: Some(u), .. } => format!("{}({})", f.label, u),
_ => f.label.clone(),
}
}
pub fn render_window(
ui: &mut egui::Ui,
plugin: &dyn Plugin,
host: &dyn CfgHost,
st: &mut WinState,
) -> bool {
let Some(cfg) = plugin.as_configurable() else {
ui.label("该插件不在册(无配置面)");
return false;
};
let mut want = false;
let schema = cfg.config_schema();
let view = cfg.config_view();
let keys: Vec<String> = if schema.is_empty() {
view.iter().map(|(k, _)| k.clone()).collect()
} else {
schema.iter().map(|f| f.key.clone()).collect()
};
if st.seed != keys {
st.rows = keys
.iter()
.map(|k| {
let v = view
.iter()
.find(|(vk, _)| vk == k)
.map(|(_, vv)| vv.clone())
.unwrap_or_default();
(k.clone(), v)
})
.collect();
st.seed = keys;
}
let mut bad: Vec<String> = Vec::new();
egui::Grid::new("pw_grid")
.num_columns(3)
.spacing([10.0, 6.0])
.striped(true)
.show(ui, |ui| {
for (i, (k, v)) in st.rows.iter_mut().enumerate() {
let f = schema.iter().find(|f| &f.key == k);
let label = match &f {
Some(f) => field_label(f),
None => k.clone(),
};
ui.label(egui::RichText::new(label).strong());
if v.starts_with("secret://") {
ui.label(egui::RichText::new(v.as_str()).weak());
} else {
let mut buf = v.clone();
ui.add(egui::TextEdit::singleline(&mut buf).desired_width(120.0));
if buf != *v {
*v = buf;
want = true;
}
}
match f.and_then(|f| f.help.as_deref()) {
Some(h) if !h.is_empty() => {
want |= ui.label(egui::RichText::new(h).small().weak()).hovered();
}
_ => {
ui.label("");
}
}
if let Some(f) = f
&& !v.starts_with("secret://")
&& let Err(e) = f.validate(v)
{
bad.push(format!("行 {i}: {e}"));
}
ui.end_row();
}
});
ui.add_space(6.0);
ui.horizontal(|ui| {
if ui.button("保存(写档案段+即时生效)").clicked() {
want = true;
st.notice = Some(save(cfg, host, &st.rows, &schema));
}
if ui.button("按档案重读").clicked() {
st.seed = Vec::new();
st.notice = None;
want = true;
}
});
for line in bad.iter().take(3) {
ui.label(
egui::RichText::new(line.as_str())
.small()
.color(crate::tokens::warn(ui.ctx())),
);
}
if let Some(n) = &st.notice {
ui.label(egui::RichText::new(n.as_str()).monospace().small());
}
ui.label(
egui::RichText::new(
"秘面只读;口令走 ekapp add-account(stdin)。标「待重启」的键按插件重启取新值",
)
.small()
.weak(),
);
want
}
fn save(
cfg: &dyn Configurable,
host: &dyn CfgHost,
rows: &[(String, String)],
schema: &[ConfigField],
) -> String {
let mut map = SectionMap::new(1 + rows.len() as u32);
let mut rejected: Vec<String> = Vec::new();
for (k, v) in rows {
if v.starts_with("secret://") {
continue;
}
if let Some(f) = schema.iter().find(|f| &f.key == k)
&& let Err(e) = f.validate(v)
{
rejected.push(format!("{k}:{e}"));
continue;
}
map = map.set(k.clone(), Field::plain(v.trim().to_string()));
}
if !rejected.is_empty() {
return format!("拒 {} 项:{}", rejected.len(), rejected.join(" | "));
}
if map.is_empty() {
return "无可存项(秘面只读)".to_string();
}
if let Err(e) = host.cfg_save(cfg.config_ns(), &map) {
return format!("存档失败:{e}");
}
host.log(&format!(
"[ui] 档案段 {} 已存:{}",
cfg.config_ns(),
map.iter().map(|(k, _)| k).collect::<Vec<_>>().join(",")
));
match cfg.apply_config(&map) {
Ok(rep) => {
let mut line = format!(
"已存 {} 键 · 即时生效 {} 键",
map.iter().count(),
rep.adopted.len()
);
if !rep.deferred.is_empty() {
line.push_str(&format!(";{} 键待插件重启生效", rep.deferred.len()));
}
line
}
Err(e) => format!("已存档,受理拒:{e}"),
}
}
pub fn export_bundle(
cfg_root: &std::path::Path,
dest_dir: &std::path::Path,
) -> Result<(usize, usize), String> {
std::fs::create_dir_all(dest_dir).map_err(|e| format!("建目标目录失败:{e}"))?;
let mut copied = 0usize;
let mut skipped = 0usize;
let rd = std::fs::read_dir(cfg_root).map_err(|e| format!("读档案根失败:{e}"))?;
for ent in rd.flatten() {
let p = ent.path();
if p.extension().and_then(|e| e.to_str()) == Some("ini") {
let dst = dest_dir.join(ent.file_name());
std::fs::copy(&p, &dst).map_err(|e| format!("复制失败:{e}"))?;
copied += 1;
} else {
skipped += 1;
}
}
Ok((copied, skipped))
}
pub fn import_bundle(
host: &dyn CfgHost,
src_dir: &std::path::Path,
apply: &dyn Fn(&str, &SectionMap) -> Option<String>,
) -> Vec<String> {
let mut lines = Vec::new();
let rd = match std::fs::read_dir(src_dir) {
Ok(r) => r,
Err(e) => {
lines.push(format!("读包目录失败:{e}"));
return lines;
}
};
let mut files: Vec<std::path::PathBuf> = rd
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("ini"))
.collect();
files.sort();
for p in files {
let Some(ns) = p.file_stem().and_then(|s| s.to_str()).map(String::from) else {
continue;
};
let text = match std::fs::read_to_string(&p) {
Ok(t) => t,
Err(e) => {
lines.push(format!("{ns}:读文件失败:{e}"));
continue;
}
};
let (parsed, warnings) = match ek_cfg::parse(&text, &ns) {
Ok(v) => v,
Err(e) => {
lines.push(format!("{ns}:解析失败:{e}"));
continue;
}
};
match host.cfg_save(&ns, &parsed) {
Ok(()) => {
let mut line = format!("{ns}:收编 {} 字段", parsed.len());
if !warnings.is_empty() {
line.push_str(&format!(" · 告警 {}", warnings.len()));
}
if let Some(rep) = apply(&ns, &parsed) {
line.push_str(&format!(" · {rep}"));
}
lines.push(line);
}
Err(e) => lines.push(format!("{ns}:写入失败:{e}")),
}
}
if lines.is_empty() {
lines.push("包内无档案段(.ini)".to_string());
}
lines.push("口令段只到引用形态;口令改动走 ekapp add-account(stdin)".to_string());
lines
}