1use ek_core::Configurable;
16use ek_core::cfg_face::{CfgHost, ConfigField};
17use ek_core::plugin::Plugin;
18use ek_core::section::{Field, SectionMap};
19
20#[derive(Default)]
22pub struct WinState {
23 pub open: bool,
25 rows: Vec<(String, String)>,
26 seed: Vec<String>,
27 pub notice: Option<String>,
29}
30
31impl WinState {
32 #[must_use]
34 pub fn new() -> Self {
35 Self::default()
36 }
37}
38
39#[must_use]
41pub fn mounted(plugins: &[std::sync::Arc<dyn Plugin>]) -> Vec<(String, String)> {
42 plugins
43 .iter()
44 .filter_map(|p| {
45 let c = p.as_configurable()?;
46 Some((p.name().to_string(), c.config_ns().to_string()))
47 })
48 .collect()
49}
50
51fn field_label(f: &ConfigField) -> String {
52 match &f.kind {
53 ek_core::ConfigKind::Int { unit: Some(u), .. }
54 | ek_core::ConfigKind::Float { unit: Some(u), .. } => format!("{}({})", f.label, u),
55 _ => f.label.clone(),
56 }
57}
58
59pub fn render_window(
61 ui: &mut egui::Ui,
62 plugin: &dyn Plugin,
63 host: &dyn CfgHost,
64 st: &mut WinState,
65) -> bool {
66 let Some(cfg) = plugin.as_configurable() else {
67 ui.label("该插件不在册(无配置面)");
68 return false;
69 };
70 let mut want = false;
71 let schema = cfg.config_schema();
72 let view = cfg.config_view();
73 let keys: Vec<String> = if schema.is_empty() {
74 view.iter().map(|(k, _)| k.clone()).collect()
75 } else {
76 schema.iter().map(|f| f.key.clone()).collect()
77 };
78 if st.seed != keys {
79 st.rows = keys
80 .iter()
81 .map(|k| {
82 let v = view
83 .iter()
84 .find(|(vk, _)| vk == k)
85 .map(|(_, vv)| vv.clone())
86 .unwrap_or_default();
87 (k.clone(), v)
88 })
89 .collect();
90 st.seed = keys;
91 }
92
93 let mut bad: Vec<String> = Vec::new();
94 egui::Grid::new("pw_grid")
95 .num_columns(3)
96 .spacing([10.0, 6.0])
97 .striped(true)
98 .show(ui, |ui| {
99 for (i, (k, v)) in st.rows.iter_mut().enumerate() {
100 let f = schema.iter().find(|f| &f.key == k);
101 let label = match &f {
102 Some(f) => field_label(f),
103 None => k.clone(),
104 };
105 ui.label(egui::RichText::new(label).strong());
106 if v.starts_with("secret://") {
107 ui.label(egui::RichText::new(v.as_str()).weak());
108 } else {
109 let mut buf = v.clone();
110 ui.add(egui::TextEdit::singleline(&mut buf).desired_width(120.0));
111 if buf != *v {
112 *v = buf;
113 want = true;
114 }
115 }
116 match f.and_then(|f| f.help.as_deref()) {
117 Some(h) if !h.is_empty() => {
118 want |= ui.label(egui::RichText::new(h).small().weak()).hovered();
119 }
120 _ => {
121 ui.label("");
122 }
123 }
124 if let Some(f) = f
125 && !v.starts_with("secret://")
126 && let Err(e) = f.validate(v)
127 {
128 bad.push(format!("行 {i}: {e}"));
129 }
130 ui.end_row();
131 }
132 });
133
134 ui.add_space(6.0);
135 ui.horizontal(|ui| {
136 if ui.button("保存(写档案段+即时生效)").clicked() {
137 want = true;
138 st.notice = Some(save(cfg, host, &st.rows, &schema));
139 }
140 if ui.button("按档案重读").clicked() {
141 st.seed = Vec::new();
142 st.notice = None;
143 want = true;
144 }
145 });
146 for line in bad.iter().take(3) {
147 ui.label(
148 egui::RichText::new(line.as_str())
149 .small()
150 .color(crate::tokens::warn(ui.ctx())),
151 );
152 }
153 if let Some(n) = &st.notice {
154 ui.label(egui::RichText::new(n.as_str()).monospace().small());
155 }
156 ui.label(
157 egui::RichText::new(
158 "秘面只读;口令走 ekapp add-account(stdin)。标「待重启」的键按插件重启取新值",
159 )
160 .small()
161 .weak(),
162 );
163 want
164}
165
166fn save(
168 cfg: &dyn Configurable,
169 host: &dyn CfgHost,
170 rows: &[(String, String)],
171 schema: &[ConfigField],
172) -> String {
173 let mut map = SectionMap::new(1 + rows.len() as u32);
174 let mut rejected: Vec<String> = Vec::new();
175 for (k, v) in rows {
176 if v.starts_with("secret://") {
177 continue;
178 }
179 if let Some(f) = schema.iter().find(|f| &f.key == k)
180 && let Err(e) = f.validate(v)
181 {
182 rejected.push(format!("{k}:{e}"));
183 continue;
184 }
185 map = map.set(k.clone(), Field::plain(v.trim().to_string()));
186 }
187 if !rejected.is_empty() {
188 return format!("拒 {} 项:{}", rejected.len(), rejected.join(" | "));
189 }
190 if map.is_empty() {
191 return "无可存项(秘面只读)".to_string();
192 }
193 if let Err(e) = host.cfg_save(cfg.config_ns(), &map) {
194 return format!("存档失败:{e}");
195 }
196 host.log(&format!(
197 "[ui] 档案段 {} 已存:{}",
198 cfg.config_ns(),
199 map.iter().map(|(k, _)| k).collect::<Vec<_>>().join(",")
200 ));
201 match cfg.apply_config(&map) {
202 Ok(rep) => {
203 let mut line = format!(
204 "已存 {} 键 · 即时生效 {} 键",
205 map.iter().count(),
206 rep.adopted.len()
207 );
208 if !rep.deferred.is_empty() {
209 line.push_str(&format!(";{} 键待插件重启生效", rep.deferred.len()));
210 }
211 line
212 }
213 Err(e) => format!("已存档,受理拒:{e}"),
214 }
215}
216
217pub fn export_bundle(
220 cfg_root: &std::path::Path,
221 dest_dir: &std::path::Path,
222) -> Result<(usize, usize), String> {
223 std::fs::create_dir_all(dest_dir).map_err(|e| format!("建目标目录失败:{e}"))?;
224 let mut copied = 0usize;
225 let mut skipped = 0usize;
226 let rd = std::fs::read_dir(cfg_root).map_err(|e| format!("读档案根失败:{e}"))?;
227 for ent in rd.flatten() {
228 let p = ent.path();
229 if p.extension().and_then(|e| e.to_str()) == Some("ini") {
230 let dst = dest_dir.join(ent.file_name());
231 std::fs::copy(&p, &dst).map_err(|e| format!("复制失败:{e}"))?;
232 copied += 1;
233 } else {
234 skipped += 1;
235 }
236 }
237 Ok((copied, skipped))
238}
239
240pub fn import_bundle(
244 host: &dyn CfgHost,
245 src_dir: &std::path::Path,
246 apply: &dyn Fn(&str, &SectionMap) -> Option<String>,
247) -> Vec<String> {
248 let mut lines = Vec::new();
249 let rd = match std::fs::read_dir(src_dir) {
250 Ok(r) => r,
251 Err(e) => {
252 lines.push(format!("读包目录失败:{e}"));
253 return lines;
254 }
255 };
256 let mut files: Vec<std::path::PathBuf> = rd
257 .flatten()
258 .map(|e| e.path())
259 .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("ini"))
260 .collect();
261 files.sort();
262 for p in files {
263 let Some(ns) = p.file_stem().and_then(|s| s.to_str()).map(String::from) else {
264 continue;
265 };
266 let text = match std::fs::read_to_string(&p) {
268 Ok(t) => t,
269 Err(e) => {
270 lines.push(format!("{ns}:读文件失败:{e}"));
271 continue;
272 }
273 };
274 let (parsed, warnings) = match ek_cfg::parse(&text, &ns) {
275 Ok(v) => v,
276 Err(e) => {
277 lines.push(format!("{ns}:解析失败:{e}"));
278 continue;
279 }
280 };
281 match host.cfg_save(&ns, &parsed) {
282 Ok(()) => {
283 let mut line = format!("{ns}:收编 {} 字段", parsed.len());
284 if !warnings.is_empty() {
285 line.push_str(&format!(" · 告警 {}", warnings.len()));
286 }
287 if let Some(rep) = apply(&ns, &parsed) {
288 line.push_str(&format!(" · {rep}"));
289 }
290 lines.push(line);
291 }
292 Err(e) => lines.push(format!("{ns}:写入失败:{e}")),
293 }
294 }
295 if lines.is_empty() {
296 lines.push("包内无档案段(.ini)".to_string());
297 }
298 lines.push("口令段只到引用形态;口令改动走 ekapp add-account(stdin)".to_string());
299 lines
300}