1use std::sync::Arc;
16use std::time::Duration;
17
18use ek_core::main_engine::{MainEngine, PluginState};
19use ek_core::plugin::Plugin;
20use ek_dm::plugin::DmConfig;
21use ek_station::{PlugKind, StationConfig, default_kinds};
22
23use crate::plugin::{UI_NAME, UiPlugin};
24use crate::state::UiShared;
25
26pub fn ui_kinds(shared: &Arc<UiShared>, dm_rt: bool) -> Vec<PlugKind> {
34 let mut kinds: Vec<PlugKind> = default_kinds()
35 .into_iter()
36 .filter(|k| k.name != "dm")
37 .collect();
38 kinds.push(PlugKind {
39 name: "dm",
40 doc: "数据管理器(KlineManager;real_time 更新门开,行情板/K线页取 eBarUpdate./eBar.)",
41 build: Arc::new(move || -> Arc<dyn Plugin> {
42 Arc::new(ek_dm::plugin::DmPlugin::new(DmConfig {
43 real_time: dm_rt,
44 ..DmConfig::minute("dm")
45 }))
46 }),
47 build_named: None,
48 name_validator: None,
49 });
50 kinds.push(PlugKind {
51 name: UI_NAME,
52 doc: "常驻观测界面(eLog + eTimer 1s 快照;关窗即 stop)",
53 build: {
54 let shared = Arc::clone(shared);
55 Arc::new(move || -> Arc<dyn Plugin> { Arc::new(UiPlugin::new(Arc::clone(&shared))) })
56 },
57 build_named: None,
58 name_validator: None,
59 });
60 kinds.push(PlugKind {
65 name: "tq",
66 doc: "天勤桥(行情/K线接入;凭据走密库引用,口令永不进档案)",
67 build: Arc::new(|| -> Arc<dyn Plugin> {
68 Arc::new(ek_tq::plugin::TqPlugin::new(
69 ek_tq::plugin::TqPluginSpec::new(ek_tq::bridge::BridgeConfig::default()),
70 ))
71 }),
72 build_named: None,
73 name_validator: None,
74 });
75 kinds
76}
77
78pub fn spawn_host_watcher(
82 engine: &Arc<MainEngine>,
83 shared: &Arc<UiShared>,
84) -> std::thread::JoinHandle<()> {
85 let engine = Arc::clone(engine);
86 let shared = Arc::clone(shared);
87 std::thread::spawn(move || {
88 while !shared.is_stop() {
89 std::thread::sleep(Duration::from_millis(120));
90 }
91 shared.log_line("[host] 窗口关闭,close_all …");
92 engine.close_all(Duration::from_secs(3));
93 shared.log_line("[host] 已收场");
94 })
95}
96
97fn bundle_op(shared: &Arc<UiShared>, engine: &MainEngine, dir: &str, export: bool) -> String {
99 let host = crate::faces::CfgBridge::new(shared);
100 let dest = std::path::Path::new(dir);
101 if dir.trim().is_empty() {
102 return "目录为空(先填路径)".to_string();
103 }
104 if export {
105 let Some(root) = host.cfg.as_ref().map(|c| c.root().to_path_buf()) else {
106 return "导出失败:档案托底根不可用".to_string();
107 };
108 match crate::pluginwin::export_bundle(&root, dest) {
109 Ok((c, s)) => {
110 let msg = format!("导出完成:{c} 段 → {}(跳过非段文件 {s})", dest.display());
111 shared.log_line(format!("[ui] {msg}"));
112 msg
113 }
114 Err(e) => format!("导出失败:{e}"),
115 }
116 } else {
117 let lines = crate::pluginwin::import_bundle(&host, dest, &|ns, sec| {
118 crate::faces::apply_section(engine, ns, sec)
119 });
120 let msg = lines.join(" | ");
121 shared.log_line(format!("[ui] 档案包导入:{msg}"));
122 msg
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum Stage {
129 Setup,
131 Running,
133}
134
135pub struct StageApp {
138 shared: Arc<UiShared>,
139 engine: Arc<MainEngine>,
141 kinds: Vec<PlugKind>,
143 sel: Vec<bool>,
145 store: Option<ek_cfg::Store>,
147 stage: Stage,
149 notice: Option<String>,
151 list_dir: String,
153 fs: crate::faces::FaceStates,
155 wins_open: std::collections::HashMap<String, bool>,
157 pluginwin_prev: bool,
159 bundle_dir: String,
161 bundle_notice: Option<String>,
162 ready_marker: Option<std::path::PathBuf>,
164 watcher: Option<std::thread::JoinHandle<()>>,
166}
167
168impl StageApp {
169 #[must_use]
171 pub fn new_setup(
172 engine: Arc<MainEngine>,
173 shared: Arc<UiShared>,
174 kinds: Vec<PlugKind>,
175 store: Option<ek_cfg::Store>,
176 ready_marker: Option<std::path::PathBuf>,
177 ) -> Self {
178 let sel = kinds.iter().map(|_| true).collect::<Vec<bool>>();
179 Self {
180 shared,
181 engine,
182 kinds,
183 sel,
184 store,
185 stage: Stage::Setup,
186 notice: None,
187 list_dir: String::new(),
188 fs: crate::faces::FaceStates::new(),
189 wins_open: std::collections::HashMap::new(),
190 pluginwin_prev: false,
191 bundle_dir: String::new(),
192 bundle_notice: None,
193 ready_marker,
194 watcher: None,
195 }
196 }
197
198 #[must_use]
200 pub fn stage(&self) -> Stage {
201 self.stage
202 }
203
204 #[must_use]
206 pub fn picked(&self) -> Vec<String> {
207 self.kinds
208 .iter()
209 .zip(self.sel.iter())
210 .filter(|(_, ok)| **ok)
211 .map(|(k, _)| k.name.to_string())
212 .collect()
213 }
214
215 #[must_use]
217 pub fn assembled(&self) -> bool {
218 !self.engine.plugin_names().is_empty()
219 }
220
221 pub fn set_selected(&mut self, name: &str, on: bool) {
223 if let Some(i) = self.kinds.iter().position(|k| k.name == name) {
224 self.sel[i] = on && name != UI_NAME;
225 }
226 }
227
228 pub fn do_assemble(&mut self) -> String {
232 let kinds: Vec<PlugKind> = self
233 .kinds
234 .iter()
235 .enumerate()
236 .filter(|(i, _)| self.sel[*i])
237 .map(|(_, k)| k.clone())
238 .collect();
239 let names: Vec<String> = kinds.iter().map(|k| k.name.to_string()).collect();
240 let cfg = StationConfig {
241 plugins: names.clone(),
242 };
243 let r = match &self.store {
244 Some(s) => ek_station::boot_with_cfg(&self.engine, &kinds, &cfg, Some(s)),
245 None => ek_station::boot(&self.engine, &kinds, &cfg),
246 };
247 match r {
248 Ok(_) => {
249 self.shared
250 .log_line(format!("[ui] 装配:{} 壳(未启动)", names.join(",")));
251 format!(
252 "已装配 {} 壳(未启动;参数面可改):{}",
253 names.len(),
254 names.join(",")
255 )
256 }
257 Err(e) => {
258 let msg = format!("装配失败:{e:?}");
259 self.shared.log_line(format!("[ui] {msg}"));
260 msg
261 }
262 }
263 }
264
265 pub fn do_start(&mut self) -> String {
268 if let Err(e) = self.engine.start() {
269 self.stage = Stage::Running;
271 self.shared.set_assembly_locked(false);
272 self.ensure_watcher();
273 let msg = format!("启动失败:{e:}(运维面可修,装配不锁定)");
274 self.shared.log_line(format!("[ui] {msg}"));
275 return msg;
276 }
277 let not_ready: Vec<String> = self
278 .engine
279 .plugin_names()
280 .into_iter()
281 .filter(|n| !matches!(self.engine.get_state(n), Some(PluginState::Running)))
282 .collect();
283 self.stage = Stage::Running;
284 self.shared.set_assembly_locked(not_ready.is_empty());
285 self.ensure_watcher();
286 self.write_ready_marker();
287 let shared_restore = Arc::clone(&self.shared);
289 std::thread::spawn(move || {
290 let rows = shared_restore.restore_saved_accounts();
291 let _ = rows;
292 });
293 let msg = if not_ready.is_empty() {
294 "已启动:装配面已锁定(只可看不可动)".to_string()
295 } else {
296 format!(
297 "已启动,{} 壳未就绪:{} — 装配**不锁定**(运维面留修路)",
298 not_ready.len(),
299 not_ready.join(",")
300 )
301 };
302 self.shared.log_line(format!("[ui] {msg}"));
303 msg
304 }
305
306 pub fn export_plugin_list(&mut self, dir: &str) -> String {
312 let factory_names: Vec<String> = default_kinds()
313 .into_iter()
314 .map(|k| k.name.to_string())
315 .collect();
316 let picked = self.picked();
317 let exportable: Vec<String> = picked
318 .iter()
319 .filter(|n| factory_names.iter().any(|f| f == *n))
320 .cloned()
321 .collect();
322 let skipped: Vec<String> = picked
323 .iter()
324 .filter(|n| !factory_names.iter().any(|f| f == *n))
325 .cloned()
326 .collect();
327 if exportable.is_empty() {
328 return "无可导出(勾选里没有无界面可用的组件)".to_string();
329 }
330 if dir.trim().is_empty() {
331 return "目录为空(先填路径)".to_string();
332 }
333 let path = std::path::Path::new(dir).join("ek-plugins.txt");
334 let header = if skipped.is_empty() {
335 "# ek-station 启动清单(ek-ui 阶段机导出,一行一名)".to_string()
336 } else {
337 format!(
338 "# ek-station 启动清单(ek-ui 阶段机导出,一行一名;跳过界面自带工厂:{})",
339 skipped.join(",")
340 )
341 };
342 let body = format!("{}\n{}\n", header, exportable.join("\n"));
343 match std::fs::File::create(&path).and_then(|mut f| {
344 use std::io::Write;
345 f.write_all(body.as_bytes())
346 }) {
347 Ok(()) => {
348 self.shared
349 .log_line(format!("[ui] 启动清单已导出:{}", path.display()));
350 if skipped.is_empty() {
351 format!("清单已导出:{}({} 项)", path.display(), exportable.len())
352 } else {
353 format!(
354 "清单已导出:{}({} 项;跳过界面自带工厂 {})",
355 path.display(),
356 exportable.len(),
357 skipped.join(",")
358 )
359 }
360 }
361 Err(e) => format!("导出失败:{e}"),
362 }
363 }
364
365 fn ensure_watcher(&mut self) {
366 if self.watcher.is_none() {
367 self.watcher = Some(spawn_host_watcher(&self.engine, &self.shared));
368 }
369 }
370
371 fn write_ready_marker(&self) {
372 let Some(marker) = &self.ready_marker else {
373 return;
374 };
375 use std::io::Write;
376 if let Ok(mut f) = std::fs::File::create(marker) {
377 let _ = f.write_all(crate::launch::READY_TEXT.as_bytes());
378 }
379 }
380
381 pub fn render_setup_panel(&mut self, ui: &mut egui::Ui) {
383 ui.heading("组件配置(启动前)");
384 ui.label("勾选要装载的组件 → 装配 → 参数面可改 → 启动运行;启动后装配锁定(只可看不可动)。");
385 ui.separator();
386 egui::ScrollArea::vertical()
387 .max_height(210.0)
388 .show(ui, |ui| {
389 for (i, k) in self.kinds.iter().enumerate() {
390 ui.horizontal(|ui| {
391 if k.name == UI_NAME {
392 let mut t = true;
394 ui.add_enabled(false, egui::Checkbox::new(&mut t, k.name));
395 self.sel[i] = true;
396 } else {
397 ui.checkbox(&mut self.sel[i], k.name);
398 }
399 let _ = ui.label(egui::RichText::new(k.doc.to_string()).small().weak());
400 });
401 }
402 });
403 ui.separator();
404 let picked = self.picked();
405 ui.label(if picked.is_empty() {
406 "已选: (无)".to_string()
407 } else {
408 format!("已选: {}", picked.join(", "))
409 });
410 ui.horizontal(|ui| {
411 let assembled = self.assembled();
412 if ui
413 .add_enabled(
414 !assembled && !picked.is_empty(),
415 egui::Button::new("装配(按勾选)"),
416 )
417 .clicked()
418 {
419 let msg = self.do_assemble();
420 self.notice = Some(msg);
421 }
422 if ui
423 .add_enabled(assembled, egui::Button::new("启动运行"))
424 .clicked()
425 {
426 let msg = self.do_start();
427 self.notice = Some(msg);
428 }
429 if ui.button("参数面(改参数)").clicked() {
430 self.shared
431 .set_pluginwin_open(!self.shared.pluginwin_open());
432 }
433 if ui.button("档案包(导入参数)").clicked() {
434 self.shared.set_bundle_open(!self.shared.bundle_open());
435 }
436 });
437 ui.horizontal(|ui| {
438 ui.add(
439 egui::TextEdit::singleline(&mut self.list_dir)
440 .desired_width(300.0)
441 .hint_text("启动清单导出目录,如 C:\\Users\\<你>\\Documents"),
442 );
443 if ui.button("导出启动清单(--from 可用)").clicked() {
444 let dir = self.list_dir.clone();
445 let msg = self.export_plugin_list(&dir);
446 self.notice = Some(msg);
447 }
448 });
449 if self.assembled() && self.stage == Stage::Setup {
450 ui.label(
451 egui::RichText::new("已装配(未启动):参数面窗可改,启动时按档案回灌。")
452 .color(crate::tokens::info(ui.ctx()))
453 .small(),
454 );
455 }
456 if let Some(n) = &self.notice {
457 ui.label(egui::RichText::new(n.as_str()).monospace().small());
458 }
459 }
460}
461
462impl eframe::App for StageApp {
463 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
464 let host = crate::faces::CfgBridge::new(&self.shared);
465 let mounted = crate::faces::mounted(&self.engine);
466 let pw = self.shared.pluginwin_open();
468 if pw && !self.pluginwin_prev {
469 self.wins_open.clear();
470 }
471 self.pluginwin_prev = pw;
472 if pw {
473 for (name, title) in mounted.iter() {
474 let mut open = *self.wins_open.get(name).unwrap_or(&true);
475 egui::Window::new(title.as_str())
476 .id(egui::Id::new(format!("pluginwin-{name}")))
477 .open(&mut open)
478 .default_width(460.0)
479 .show(ctx, |ui| {
480 crate::faces::render_face(ui, &self.engine, name, &host, &mut self.fs);
481 });
482 self.wins_open.insert(name.clone(), open);
483 }
484 }
485 if self.shared.bundle_open() {
488 let mut open = true;
489 let setup = self.stage == Stage::Setup;
490 egui::Window::new("档案包")
491 .id(egui::Id::new("bundlewin"))
492 .open(&mut open)
493 .default_width(430.0)
494 .show(ctx, |ui| {
495 ui.label("目录(导出目标 / 导入来源,同一目录可两用):");
496 ui.add(
497 egui::TextEdit::singleline(&mut self.bundle_dir)
498 .desired_width(400.0)
499 .hint_text("C:\\Users\\<你>\\Documents\\ek-bundle"),
500 );
501 ui.horizontal(|ui| {
502 if ui
503 .add_enabled(
504 !setup,
505 egui::Button::new("导出(段文件整批复制)"),
506 )
507 .clicked()
508 {
509 self.bundle_notice =
510 Some(bundle_op(&self.shared, &self.engine, &self.bundle_dir, true));
511 }
512 if ui.button("导入(逐段收编+受理)").clicked() {
513 self.bundle_notice = Some(bundle_op(
514 &self.shared,
515 &self.engine,
516 &self.bundle_dir,
517 false,
518 ));
519 }
520 });
521 if let Some(n) = &self.bundle_notice {
522 ui.label(egui::RichText::new(n.as_str()).monospace().small());
523 }
524 ui.label(
525 egui::RichText::new("密码不落包(在 Windows 凭据管理器);换机后口令走 ekapp add-account(stdin)")
526 .small()
527 .weak(),
528 );
529 });
530 self.shared.set_bundle_open(open);
531 }
532 if self.stage == Stage::Setup {
535 egui::CentralPanel::default().show(ctx, |ui| {
536 self.render_setup_panel(ui);
537 });
538 } else {
539 crate::render_body(ctx, &self.shared);
540 }
541 ctx.request_repaint_after(Duration::from_secs(1));
542 }
543}
544
545#[derive(Debug, Clone)]
548pub struct RunOpts {
549 pub ready_marker: Option<std::path::PathBuf>,
551 pub dm_rt: bool,
553 pub auto_boot: bool,
556}
557
558impl Default for RunOpts {
559 fn default() -> Self {
563 Self {
564 ready_marker: None,
565 dm_rt: true,
566 auto_boot: true,
567 }
568 }
569}
570
571impl RunOpts {
572 pub fn plain() -> Self {
574 Self::default()
575 }
576}
577
578pub fn run_station_ui(cfg: &StationConfig, opts: &RunOpts) -> Result<(), String> {
581 let engine = Arc::new(MainEngine::new(None));
582 let shared = Arc::new(UiShared::new());
583 let store = ek_cfg::Store::eco();
584
585 let store_ok = store.ok();
590 if opts.auto_boot {
591 let boot = match &store_ok {
592 Some(s) => {
593 ek_station::boot_with_cfg(&engine, &ui_kinds(&shared, opts.dm_rt), cfg, Some(s))
594 }
595 None => ek_station::boot(&engine, &ui_kinds(&shared, opts.dm_rt), cfg),
596 };
597 boot.map_err(|e| format!("boot: {e:?}"))?;
598 if let Err(e) = engine.start() {
603 engine.close_all(Duration::from_secs(3));
604 return Err(format!("start: {e:?}"));
605 }
606 shared.set_assembly_locked(true);
607 }
608
609 let mut app = StageApp::new_setup(
610 Arc::clone(&engine),
611 Arc::clone(&shared),
612 ui_kinds(&shared, opts.dm_rt),
613 store_ok,
614 opts.ready_marker.clone(),
615 );
616 if opts.auto_boot {
617 app = StageApp {
618 stage: Stage::Running,
619 ..app
620 };
621 app.write_ready_marker();
622 }
623
624 let watcher = spawn_host_watcher(&engine, &shared);
625 let _ = &watcher;
627
628 let opts = eframe::NativeOptions {
629 viewport: egui::ViewportBuilder::default()
630 .with_inner_size([1280.0, 860.0])
631 .with_title("ek-station"),
632 ..Default::default()
633 };
634 let result = eframe::run_native(
636 "ek-station",
637 opts,
638 Box::new(|cc| {
639 crate::fonts::apply_cjk_fonts(&cc.egui_ctx); Ok(Box::new(app) as Box<dyn eframe::App>)
641 }),
642 );
643 shared.request_stop();
644 let _ = watcher.join();
645 result.map_err(|e| format!("ui: {e:?}"))
646}
647
648pub fn run_desktop() -> Result<(), String> {
652 let engine = Arc::new(MainEngine::new(None));
653 let shared = Arc::new(UiShared::new());
654 let store = ek_cfg::Store::eco().ok();
655 let app = StageApp::new_setup(
656 Arc::clone(&engine),
657 Arc::clone(&shared),
658 ui_kinds(&shared, true),
659 store,
660 None,
661 );
662 let watcher = spawn_host_watcher(&engine, &shared);
663 let opts = eframe::NativeOptions {
664 viewport: egui::ViewportBuilder::default()
665 .with_inner_size([1280.0, 860.0])
666 .with_title("ek-station 桌面"),
667 ..Default::default()
668 };
669 let result = eframe::run_native(
670 "ek-station 桌面",
671 opts,
672 Box::new(|cc| {
673 crate::fonts::apply_cjk_fonts(&cc.egui_ctx);
674 Ok(Box::new(app) as Box<dyn eframe::App>)
675 }),
676 );
677 shared.request_stop();
678 let _ = watcher.join();
679 result.map_err(|e| format!("ui: {e:?}"))
680}