1use std::sync::Arc;
17use std::time::Duration;
18
19use ek_core::main_engine::{MainEngine, PluginState};
20use ek_core::plugin::Plugin;
21use ek_dm::plugin::DmConfig;
22use ek_station::{PlugKind, StationConfig, default_kinds};
23
24use crate::plugin::{UI_NAME, UiPlugin};
25use crate::state::UiShared;
26
27pub fn ui_kinds(shared: &Arc<UiShared>, dm_rt: bool) -> Vec<PlugKind> {
35 let mut kinds: Vec<PlugKind> = default_kinds()
36 .into_iter()
37 .filter(|k| k.name != "dm")
38 .collect();
39 kinds.push(PlugKind {
40 name: "dm",
41 doc: "数据管理器(KlineManager;real_time 更新门开,行情板/K线页取 eBarUpdate./eBar.)",
42 build: Arc::new(move || -> Arc<dyn Plugin> {
43 Arc::new(ek_dm::plugin::DmPlugin::new(DmConfig {
44 real_time: dm_rt,
45 ..DmConfig::minute("dm")
46 }))
47 }),
48 build_named: None,
49 name_validator: None,
50 });
51 kinds.push(PlugKind {
52 name: UI_NAME,
53 doc: "常驻观测界面(eLog + eTimer 1s 快照;关窗即 stop)",
54 build: {
55 let shared = Arc::clone(shared);
56 Arc::new(move || -> Arc<dyn Plugin> { Arc::new(UiPlugin::new(Arc::clone(&shared))) })
57 },
58 build_named: None,
59 name_validator: None,
60 });
61 kinds.push(PlugKind {
66 name: "tq",
67 doc: "天勤桥(行情/K线接入;凭据走密库引用,口令永不进档案)",
68 build: Arc::new(|| -> Arc<dyn Plugin> {
69 let bridge_cfg = ek_tq::bridge::BridgeConfig {
74 program: ek_tq::pyenv::python_program(),
75 script: ek_tq::pyenv::real_script(),
76 handshake_ms: ek_tq::consts::BRIDGE_HANDSHAKE_MS,
77 ..Default::default()
78 };
79 Arc::new(ek_tq::plugin::TqPlugin::new(
80 ek_tq::plugin::TqPluginSpec::new(bridge_cfg),
81 ))
82 }),
83 build_named: None,
84 name_validator: None,
85 });
86 kinds
87}
88
89pub fn spawn_host_watcher(
93 engine: &Arc<MainEngine>,
94 shared: &Arc<UiShared>,
95) -> std::thread::JoinHandle<()> {
96 let engine = Arc::clone(engine);
97 let shared = Arc::clone(shared);
98 std::thread::spawn(move || {
99 while !shared.is_stop() {
100 std::thread::sleep(Duration::from_millis(120));
101 }
102 shared.log_line("[host] 窗口关闭,close_all …");
103 engine.close_all(Duration::from_secs(3));
104 shared.log_line("[host] 已收场");
105 })
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum Stage {
111 Setup,
113 Running,
115}
116
117pub struct StageApp {
120 shared: Arc<UiShared>,
121 engine: Arc<MainEngine>,
123 kinds: Vec<PlugKind>,
125 sel: Arc<std::sync::Mutex<Vec<bool>>>,
130 store: Option<ek_cfg::Store>,
132 stage: Stage,
134 notice: Option<String>,
136 ready_marker: Option<std::path::PathBuf>,
138 watcher: Option<std::thread::JoinHandle<()>>,
140}
141
142impl StageApp {
143 #[must_use]
145 pub fn new_setup(
146 engine: Arc<MainEngine>,
147 shared: Arc<UiShared>,
148 kinds: Vec<PlugKind>,
149 store: Option<ek_cfg::Store>,
150 ready_marker: Option<std::path::PathBuf>,
151 ) -> Self {
152 shared.install_engine(&engine);
157 shared.set_setup_phase(true);
161 if let Some(s) = &store {
162 shared.restore_saved_accounts_from(s);
163 }
164 let sel = kinds.iter().map(|_| true).collect::<Vec<bool>>();
165 Self {
166 shared,
167 engine,
168 kinds,
169 sel: Arc::new(std::sync::Mutex::new(sel)),
170 store,
171 stage: Stage::Setup,
172 notice: None,
173 ready_marker,
174 watcher: None,
175 }
176 }
177
178 #[must_use]
180 pub fn stage(&self) -> Stage {
181 self.stage
182 }
183
184 #[must_use]
186 pub fn picked(&self) -> Vec<String> {
187 self.kinds
188 .iter()
189 .zip(self.sel.lock().unwrap().iter())
190 .filter(|(_, ok)| **ok)
191 .map(|(k, _)| k.name.to_string())
192 .collect()
193 }
194
195 #[must_use]
197 pub fn assembled(&self) -> bool {
198 !self.engine.plugin_names().is_empty()
199 }
200
201 pub fn set_selected(&mut self, name: &str, on: bool) {
206 if name == UI_NAME {
207 return; }
209 if let Some(i) = self.kinds.iter().position(|k| k.name == name) {
210 self.sel.lock().unwrap()[i] = on;
211 }
212 }
213
214 pub fn do_assemble(&mut self) -> String {
222 let mut kinds: Vec<PlugKind> = self
223 .kinds
224 .iter()
225 .enumerate()
226 .filter(|(i, _)| self.sel.lock().unwrap()[*i])
227 .map(|(_, k)| k.clone())
228 .collect();
229 if !kinds.iter().any(|k| k.name == "cfg")
232 && let Some(k) = self.kinds.iter().find(|k| k.name == "cfg")
233 {
234 kinds.push(k.clone());
235 }
236 let names: Vec<String> = kinds.iter().map(|k| k.name.to_string()).collect();
237 let cfg = StationConfig {
238 plugins: names.clone(),
239 };
240 let r = match &self.store {
241 Some(s) => ek_station::boot_with_cfg(&self.engine, &kinds, &cfg, Some(s)),
242 None => ek_station::boot(&self.engine, &kinds, &cfg),
243 };
244 match r {
245 Ok(_) => {
246 self.shared
247 .log_line(format!("[ui] 装配:{} 壳(未启动)", names.join(",")));
248 self.shared.remount_modules();
251 format!("已装配 {} 壳:{}", names.len(), names.join(","))
252 }
253 Err(e) => {
254 let msg = format!("装配失败:{e:?}");
255 self.shared.log_line(format!("[ui] {msg}"));
256 msg
257 }
258 }
259 }
260
261 pub fn do_start(&mut self) -> String {
268 if !self.assembled() {
269 let msg = self.do_assemble();
270 if !self.assembled() {
271 return msg;
273 }
274 let _ = msg;
275 }
276 if let Err(e) = self.engine.start() {
277 self.stage = Stage::Running;
279 self.shared.set_setup_phase(false);
280 self.shared.set_assembly_locked(false);
281 self.shared.remount_modules();
283 self.ensure_watcher();
284 let msg = format!("启动失败:{e:}(运维面可修,装配不锁定)");
285 self.shared.log_line(format!("[ui] {msg}"));
286 return msg;
287 }
288 let not_ready: Vec<String> = self
289 .engine
290 .plugin_names()
291 .into_iter()
292 .filter(|n| !matches!(self.engine.get_state(n), Some(PluginState::Running)))
293 .collect();
294 self.stage = Stage::Running;
295 self.shared.set_setup_phase(false);
296 self.shared.set_assembly_locked(not_ready.is_empty());
297 self.shared.remount_modules();
300 self.ensure_watcher();
301 self.write_ready_marker();
302 let shared_restore = Arc::clone(&self.shared);
307 std::thread::spawn(move || {
308 let _ = shared_restore.restore_saved_accounts();
311 let rows: Vec<String> = shared_restore
312 .accounts()
313 .iter()
314 .map(|a| a.gateway.clone())
315 .collect();
316 for gw in &rows {
317 let Some(a) = shared_restore.account(gw) else {
318 continue;
319 };
320 if a.auto_connect
321 && matches!(a.stage, crate::state::AccountStage::NotAssembled)
322 && !a.connected
323 {
324 let form = a.form.clone();
325 crate::render::push_connect(&shared_restore, gw, &form);
326 }
327 }
328 });
329 let msg = if not_ready.is_empty() {
330 "已启动:装配面已锁定(只可看不可动)".to_string()
331 } else {
332 format!(
333 "已启动,{} 壳未就绪:{} — 装配**不锁定**(运维面留修路)",
334 not_ready.len(),
335 not_ready.join(",")
336 )
337 };
338 self.shared.log_line(format!("[ui] {msg}"));
339 msg
340 }
341
342 fn ensure_watcher(&mut self) {
343 if self.watcher.is_none() {
344 self.watcher = Some(spawn_host_watcher(&self.engine, &self.shared));
345 }
346 }
347
348 fn write_ready_marker(&self) {
349 let Some(marker) = &self.ready_marker else {
350 return;
351 };
352 use std::io::Write;
353 if let Ok(mut f) = std::fs::File::create(marker) {
354 let _ = f.write_all(crate::launch::READY_TEXT.as_bytes());
355 }
356 }
357
358 pub fn render_setup_panel(&mut self, ui: &mut egui::Ui) {
368 use crate::render::{OpTone, op_button, pinned_table};
369 ui.heading("组件配置(启动前)");
370 ui.label("勾选要装载的组件 → 「启动运行」(先按勾选装配,再起跑;装配失败会报错,等您改勾选后重按)。");
371 ui.separator();
372 let sel = Arc::clone(&self.sel);
373 let (row_h, head_h) = crate::render::table_heights(ui);
379 let table_h =
380 head_h + ui.visuals().clip_rect_margin + self.kinds.len() as f32 * row_h + 6.0;
381 let avail_x = ui.available_width();
382 ui.allocate_ui(egui::vec2(avail_x, table_h), |ui| {
383 pinned_table(
384 ui,
385 "setup_grid",
386 &["装载", "组件", "说明"],
387 |body, row_h| {
388 for (i, k) in self.kinds.iter().enumerate() {
389 let name = k.name.to_string();
390 let doc = k.doc.to_string();
391 let sel_cell = Arc::clone(&sel);
392 let label0 = name.clone();
393 body.row(row_h, false, |row| {
394 row.cell(move |ui, _ctx| {
395 if label0 == UI_NAME {
396 let mut t = true;
398 ui.add_enabled(false, egui::Checkbox::new(&mut t, ""));
399 sel_cell.lock().unwrap()[i] = true;
400 } else {
401 let mut on = sel_cell.lock().unwrap()[i];
402 if ui.checkbox(&mut on, "").changed() {
403 sel_cell.lock().unwrap()[i] = on;
404 }
405 }
406 });
407 let label = name.clone();
408 row.cell(move |ui, _ctx| {
409 ui.label(egui::RichText::new(&label).strong());
410 });
411 row.cell(move |ui, _ctx| {
412 ui.add(
413 egui::Label::new(egui::RichText::new(&doc))
414 .wrap_mode(egui::TextWrapMode::Extend),
415 );
416 });
417 });
418 }
419 },
420 );
421 });
422 ui.separator();
423 let picked = self.picked();
424 ui.label(if picked.is_empty() {
425 "已选: (无)".to_string()
426 } else {
427 format!("已选: {}", picked.join(", "))
428 });
429 ui.horizontal(|ui| {
430 if ui
431 .add_enabled_ui(!picked.is_empty(), |ui| {
432 op_button(ui, "启动运行", OpTone::Primary).clicked()
433 })
434 .inner
435 {
436 self.notice = Some(self.do_start());
437 }
438 });
439 if let Some(n) = &self.notice {
440 ui.label(egui::RichText::new(n.as_str()).monospace().small());
441 }
442 }
443}
444
445impl eframe::App for StageApp {
446 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
447 crate::render::apply_ui_style(ctx, self.shared.theme());
454 if self.stage == Stage::Setup {
460 self.setup_fit_once(ctx);
461 egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
467 crate::render::render_menu_bar(ui, &self.shared);
468 });
469 egui::CentralPanel::default().show(ctx, |ui| {
470 self.render_setup_panel(ui);
471 });
472 crate::render::render_windows(ctx, &self.shared);
474 } else {
475 let sent_key = egui::Id::new("running_geo_sent");
483 if !ctx.data(|d| d.get_temp::<bool>(sent_key).unwrap_or(false)) {
484 ctx.data_mut(|d| d.insert_temp(sent_key, true));
485 let maximized = ctx.viewport(|vp| vp.builder.maximized.unwrap_or(false));
486 if !maximized {
487 ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
488 }
489 }
490 crate::render_body(ctx, &self.shared);
491 }
492 ctx.request_repaint_after(Duration::from_secs(1));
493 }
494}
495
496impl StageApp {
497 fn setup_fit_once(&mut self, ctx: &egui::Context) {
504 use egui::Id;
505 #[derive(Clone, Copy)]
506 struct FitOneShot {
507 w: f32,
508 h: f32,
509 stable: u8,
510 frames: u16,
511 done: bool,
512 }
513 let key = Id::new(("fit_oneshot", "ekapp_main"));
514 let st = ctx
515 .data(|d| d.get_temp::<FitOneShot>(key))
516 .unwrap_or(FitOneShot {
517 w: -1.0,
518 h: -1.0,
519 stable: 0,
520 frames: 0,
521 done: false,
522 });
523 let maximized = ctx.viewport(|vp| vp.builder.maximized.unwrap_or(false));
524 if st.done || maximized {
525 return;
526 }
527 let mut sc = (*ctx.style()).clone();
528 sc.wrap_mode = Some(egui::TextWrapMode::Extend);
529 let measure_w = ctx.data(|d| {
530 d.get_temp::<f32>(Id::new(("fit_w", "ekapp_main")))
531 .unwrap_or(48.0)
532 });
533 let mut probe = egui::Ui::new(
534 ctx.clone(),
535 Id::new(("fit_probe", "ekapp_main")),
536 egui::UiBuilder::new()
537 .global_scope(true)
538 .invisible()
539 .style(sc)
540 .max_rect(egui::Rect::from_min_size(
541 egui::pos2(0.0, 0.0),
542 egui::vec2(measure_w.max(48.0), 1.0e5),
543 ))
544 .layout(egui::Layout::top_down(egui::Align::Min)),
545 );
546 crate::render::render_menu_bar(&mut probe, &self.shared);
549 self.render_setup_panel(&mut probe);
550 let nat_w = probe.min_rect().width();
551 let nat_h = probe.min_rect().height();
552 ctx.data_mut(|d| d.insert_temp(Id::new(("fit_w", "ekapp_main")), nat_w));
553 let stable = (nat_w - st.w).abs() < 0.5 && (nat_h - st.h).abs() < 0.5;
554 let mut next = FitOneShot {
555 w: nat_w,
556 h: nat_h,
557 stable: if stable { st.stable + 1 } else { 0 },
558 frames: st.frames + 1,
559 done: false,
560 };
561 if next.stable >= 3 && next.frames >= 6 {
562 let want = egui::vec2(
563 (nat_w + 20.0).clamp(420.0, 1280.0),
564 (nat_h + 26.0).clamp(260.0, 900.0),
565 );
566 ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(want));
567 next.done = true;
568 }
569 ctx.data_mut(|d| d.insert_temp(key, next));
570 }
571}
572
573#[derive(Debug, Clone)]
576pub struct RunOpts {
577 pub ready_marker: Option<std::path::PathBuf>,
579 pub dm_rt: bool,
581 pub auto_boot: bool,
584}
585
586impl Default for RunOpts {
587 fn default() -> Self {
591 Self {
592 ready_marker: None,
593 dm_rt: true,
594 auto_boot: true,
595 }
596 }
597}
598
599impl RunOpts {
600 pub fn plain() -> Self {
602 Self::default()
603 }
604}
605
606pub fn run_station_ui(cfg: &StationConfig, opts: &RunOpts) -> Result<(), String> {
609 let engine = Arc::new(MainEngine::new(None));
610 let shared = Arc::new(UiShared::new());
611 let store = ek_cfg::Store::eco();
612
613 let store_ok = store.ok();
618 if opts.auto_boot {
619 let boot = match &store_ok {
620 Some(s) => {
621 ek_station::boot_with_cfg(&engine, &ui_kinds(&shared, opts.dm_rt), cfg, Some(s))
622 }
623 None => ek_station::boot(&engine, &ui_kinds(&shared, opts.dm_rt), cfg),
624 };
625 boot.map_err(|e| format!("boot: {e:?}"))?;
626 if let Err(e) = engine.start() {
631 engine.close_all(Duration::from_secs(3));
632 return Err(format!("start: {e:?}"));
633 }
634 shared.set_assembly_locked(true);
635 }
636
637 shared.remount_modules();
641
642 let mut app = StageApp::new_setup(
643 Arc::clone(&engine),
644 Arc::clone(&shared),
645 ui_kinds(&shared, opts.dm_rt),
646 store_ok,
647 opts.ready_marker.clone(),
648 );
649 if opts.auto_boot {
650 app = StageApp {
651 stage: Stage::Running,
652 ..app
653 };
654 app.write_ready_marker();
655 }
656
657 let watcher = spawn_host_watcher(&engine, &shared);
658 let _ = &watcher;
660
661 let opts = eframe::NativeOptions {
662 viewport: egui::ViewportBuilder::default()
663 .with_inner_size([1280.0, 860.0])
664 .with_title("ek-station"),
665 ..Default::default()
666 };
667 let result = eframe::run_native(
669 "ek-station",
670 opts,
671 Box::new(|cc| {
672 crate::fonts::apply_cjk_fonts(&cc.egui_ctx); Ok(Box::new(app) as Box<dyn eframe::App>)
674 }),
675 );
676 shared.request_stop();
677 let _ = watcher.join();
678 result.map_err(|e| format!("ui: {e:?}"))
679}
680
681pub fn run_desktop() -> Result<(), String> {
685 let engine = Arc::new(MainEngine::new(None));
686 let shared = Arc::new(UiShared::new());
687 let store = ek_cfg::Store::eco().ok();
688 let app = StageApp::new_setup(
689 Arc::clone(&engine),
690 Arc::clone(&shared),
691 ui_kinds(&shared, true),
692 store,
693 None,
694 );
695 let watcher = spawn_host_watcher(&engine, &shared);
696 let opts = eframe::NativeOptions {
697 viewport: egui::ViewportBuilder::default()
698 .with_inner_size([1280.0, 860.0])
699 .with_title("ek-station 桌面"),
700 ..Default::default()
701 };
702 let result = eframe::run_native(
703 "ek-station 桌面",
704 opts,
705 Box::new(|cc| {
706 crate::fonts::apply_cjk_fonts(&cc.egui_ctx);
707 Ok(Box::new(app) as Box<dyn eframe::App>)
708 }),
709 );
710 shared.request_stop();
711 let _ = watcher.join();
712 result.map_err(|e| format!("ui: {e:?}"))
713}