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 let sel = kinds.iter().map(|_| true).collect::<Vec<bool>>();
153 Self {
154 shared,
155 engine,
156 kinds,
157 sel: Arc::new(std::sync::Mutex::new(sel)),
158 store,
159 stage: Stage::Setup,
160 notice: None,
161 ready_marker,
162 watcher: None,
163 }
164 }
165
166 #[must_use]
168 pub fn stage(&self) -> Stage {
169 self.stage
170 }
171
172 #[must_use]
174 pub fn picked(&self) -> Vec<String> {
175 self.kinds
176 .iter()
177 .zip(self.sel.lock().unwrap().iter())
178 .filter(|(_, ok)| **ok)
179 .map(|(k, _)| k.name.to_string())
180 .collect()
181 }
182
183 #[must_use]
185 pub fn assembled(&self) -> bool {
186 !self.engine.plugin_names().is_empty()
187 }
188
189 pub fn set_selected(&mut self, name: &str, on: bool) {
194 if name == UI_NAME {
195 return; }
197 if let Some(i) = self.kinds.iter().position(|k| k.name == name) {
198 self.sel.lock().unwrap()[i] = on;
199 }
200 }
201
202 pub fn do_assemble(&mut self) -> String {
210 let mut kinds: Vec<PlugKind> = self
211 .kinds
212 .iter()
213 .enumerate()
214 .filter(|(i, _)| self.sel.lock().unwrap()[*i])
215 .map(|(_, k)| k.clone())
216 .collect();
217 if !kinds.iter().any(|k| k.name == "cfg")
220 && let Some(k) = self.kinds.iter().find(|k| k.name == "cfg")
221 {
222 kinds.push(k.clone());
223 }
224 let names: Vec<String> = kinds.iter().map(|k| k.name.to_string()).collect();
225 let cfg = StationConfig {
226 plugins: names.clone(),
227 };
228 let r = match &self.store {
229 Some(s) => ek_station::boot_with_cfg(&self.engine, &kinds, &cfg, Some(s)),
230 None => ek_station::boot(&self.engine, &kinds, &cfg),
231 };
232 match r {
233 Ok(_) => {
234 self.shared
235 .log_line(format!("[ui] 装配:{} 壳(未启动)", names.join(",")));
236 format!("已装配 {} 壳:{}", names.len(), names.join(","))
237 }
238 Err(e) => {
239 let msg = format!("装配失败:{e:?}");
240 self.shared.log_line(format!("[ui] {msg}"));
241 msg
242 }
243 }
244 }
245
246 pub fn do_start(&mut self) -> String {
253 if !self.assembled() {
254 let msg = self.do_assemble();
255 if !self.assembled() {
256 return msg;
258 }
259 let _ = msg;
260 }
261 if let Err(e) = self.engine.start() {
262 self.stage = Stage::Running;
264 self.shared.set_assembly_locked(false);
265 self.ensure_watcher();
266 let msg = format!("启动失败:{e:}(运维面可修,装配不锁定)");
267 self.shared.log_line(format!("[ui] {msg}"));
268 return msg;
269 }
270 let not_ready: Vec<String> = self
271 .engine
272 .plugin_names()
273 .into_iter()
274 .filter(|n| !matches!(self.engine.get_state(n), Some(PluginState::Running)))
275 .collect();
276 self.stage = Stage::Running;
277 self.shared.set_assembly_locked(not_ready.is_empty());
278 self.ensure_watcher();
279 self.write_ready_marker();
280 let shared_restore = Arc::clone(&self.shared);
285 std::thread::spawn(move || {
286 let rows = shared_restore.restore_saved_accounts();
287 for gw in &rows {
288 let Some(a) = shared_restore.account(gw) else {
289 continue;
290 };
291 if a.auto_connect
292 && matches!(a.stage, crate::state::AccountStage::NotAssembled)
293 && !a.connected
294 {
295 let form = a.form.clone();
296 crate::render::push_connect(&shared_restore, gw, &form);
297 }
298 }
299 });
300 let msg = if not_ready.is_empty() {
301 "已启动:装配面已锁定(只可看不可动)".to_string()
302 } else {
303 format!(
304 "已启动,{} 壳未就绪:{} — 装配**不锁定**(运维面留修路)",
305 not_ready.len(),
306 not_ready.join(",")
307 )
308 };
309 self.shared.log_line(format!("[ui] {msg}"));
310 msg
311 }
312
313 fn ensure_watcher(&mut self) {
314 if self.watcher.is_none() {
315 self.watcher = Some(spawn_host_watcher(&self.engine, &self.shared));
316 }
317 }
318
319 fn write_ready_marker(&self) {
320 let Some(marker) = &self.ready_marker else {
321 return;
322 };
323 use std::io::Write;
324 if let Ok(mut f) = std::fs::File::create(marker) {
325 let _ = f.write_all(crate::launch::READY_TEXT.as_bytes());
326 }
327 }
328
329 pub fn render_setup_panel(&mut self, ui: &mut egui::Ui) {
339 use crate::render::{OpTone, op_button, pinned_table};
340 ui.heading("组件配置(启动前)");
341 ui.label("勾选要装载的组件 → 「启动运行」(先按勾选装配,再起跑;装配失败会报错,等您改勾选后重按)。");
342 ui.separator();
343 let sel = Arc::clone(&self.sel);
344 let (row_h, head_h) = crate::render::table_heights(ui);
350 let table_h = head_h + ui.visuals().clip_rect_margin
351 + self.kinds.len() as f32 * row_h
352 + 6.0;
353 let avail_x = ui.available_width();
354 ui.allocate_ui(egui::vec2(avail_x, table_h), |ui| {
355 pinned_table(ui, "setup_grid", &["装载", "组件", "说明"], |body, row_h| {
356 for (i, k) in self.kinds.iter().enumerate() {
357 let name = k.name.to_string();
358 let doc = k.doc.to_string();
359 let sel_cell = Arc::clone(&sel);
360 let label0 = name.clone();
361 body.row(row_h, false, |row| {
362 row.cell(move |ui, _ctx| {
363 if label0 == UI_NAME {
364 let mut t = true;
366 ui.add_enabled(false, egui::Checkbox::new(&mut t, ""));
367 sel_cell.lock().unwrap()[i] = true;
368 } else {
369 let mut on = sel_cell.lock().unwrap()[i];
370 if ui.checkbox(&mut on, "").changed() {
371 sel_cell.lock().unwrap()[i] = on;
372 }
373 }
374 });
375 let label = name.clone();
376 row.cell(move |ui, _ctx| {
377 ui.label(egui::RichText::new(&label).strong());
378 });
379 row.cell(move |ui, _ctx| {
380 ui.add(
381 egui::Label::new(egui::RichText::new(&doc))
382 .wrap_mode(egui::TextWrapMode::Extend),
383 );
384 });
385 });
386 }
387 });
388 });
389 ui.separator();
390 let picked = self.picked();
391 ui.label(if picked.is_empty() {
392 "已选: (无)".to_string()
393 } else {
394 format!("已选: {}", picked.join(", "))
395 });
396 ui.horizontal(|ui| {
397 if ui
398 .add_enabled_ui(!picked.is_empty(), |ui| {
399 op_button(ui, "启动运行", OpTone::Primary).clicked()
400 })
401 .inner
402 {
403 self.notice = Some(self.do_start());
404 }
405 });
406 if let Some(n) = &self.notice {
407 ui.label(egui::RichText::new(n.as_str()).monospace().small());
408 }
409 }
410}
411
412impl eframe::App for StageApp {
413 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
414 crate::render::apply_ui_style(ctx, self.shared.theme());
421 if self.stage == Stage::Setup {
427 self.setup_fit_once(ctx);
428 egui::CentralPanel::default().show(ctx, |ui| {
429 self.render_setup_panel(ui);
430 });
431 } else {
432 let sent_key = egui::Id::new("running_geo_sent");
437 if !ctx.data(|d| d.get_temp::<bool>(sent_key).unwrap_or(false)) {
438 ctx.data_mut(|d| d.insert_temp(sent_key, true));
439 let maximized = ctx.viewport(|vp| vp.builder.maximized.unwrap_or(false));
440 if !maximized {
441 ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(
442 1280.0, 860.0,
443 )));
444 }
445 }
446 crate::render_body(ctx, &self.shared);
447 }
448 ctx.request_repaint_after(Duration::from_secs(1));
449 }
450}
451
452impl StageApp {
453 fn setup_fit_once(&mut self, ctx: &egui::Context) {
460 use egui::Id;
461 #[derive(Clone, Copy)]
462 struct FitOneShot {
463 w: f32,
464 h: f32,
465 stable: u8,
466 frames: u16,
467 done: bool,
468 }
469 let key = Id::new(("fit_oneshot", "ekapp_main"));
470 let st = ctx.data(|d| d.get_temp::<FitOneShot>(key)).unwrap_or(FitOneShot {
471 w: -1.0,
472 h: -1.0,
473 stable: 0,
474 frames: 0,
475 done: false,
476 });
477 let maximized = ctx.viewport(|vp| vp.builder.maximized.unwrap_or(false));
478 if st.done || maximized {
479 return;
480 }
481 let mut sc = (*ctx.style()).clone();
482 sc.wrap_mode = Some(egui::TextWrapMode::Extend);
483 let measure_w = ctx
484 .data(|d| d.get_temp::<f32>(Id::new(("fit_w", "ekapp_main"))).unwrap_or(48.0));
485 let mut probe = egui::Ui::new(
486 ctx.clone(),
487 Id::new(("fit_probe", "ekapp_main")),
488 egui::UiBuilder::new()
489 .global_scope(true)
490 .invisible()
491 .style(sc)
492 .max_rect(egui::Rect::from_min_size(
493 egui::pos2(0.0, 0.0),
494 egui::vec2(measure_w.max(48.0), 1.0e5),
495 ))
496 .layout(egui::Layout::top_down(egui::Align::Min)),
497 );
498 self.render_setup_panel(&mut probe);
499 let nat_w = probe.min_rect().width();
500 let nat_h = probe.min_rect().height();
501 ctx.data_mut(|d| d.insert_temp(Id::new(("fit_w", "ekapp_main")), nat_w));
502 let stable = (nat_w - st.w).abs() < 0.5 && (nat_h - st.h).abs() < 0.5;
503 let mut next = FitOneShot {
504 w: nat_w,
505 h: nat_h,
506 stable: if stable { st.stable + 1 } else { 0 },
507 frames: st.frames + 1,
508 done: false,
509 };
510 if next.stable >= 3 && next.frames >= 6 {
511 let want = egui::vec2(
512 (nat_w + 20.0).clamp(420.0, 1280.0),
513 (nat_h + 26.0).clamp(260.0, 900.0),
514 );
515 ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(want));
516 next.done = true;
517 }
518 ctx.data_mut(|d| d.insert_temp(key, next));
519 }
520}
521
522#[derive(Debug, Clone)]
525pub struct RunOpts {
526 pub ready_marker: Option<std::path::PathBuf>,
528 pub dm_rt: bool,
530 pub auto_boot: bool,
533}
534
535impl Default for RunOpts {
536 fn default() -> Self {
540 Self {
541 ready_marker: None,
542 dm_rt: true,
543 auto_boot: true,
544 }
545 }
546}
547
548impl RunOpts {
549 pub fn plain() -> Self {
551 Self::default()
552 }
553}
554
555pub fn run_station_ui(cfg: &StationConfig, opts: &RunOpts) -> Result<(), String> {
558 let engine = Arc::new(MainEngine::new(None));
559 let shared = Arc::new(UiShared::new());
560 let store = ek_cfg::Store::eco();
561
562 let store_ok = store.ok();
567 if opts.auto_boot {
568 let boot = match &store_ok {
569 Some(s) => {
570 ek_station::boot_with_cfg(&engine, &ui_kinds(&shared, opts.dm_rt), cfg, Some(s))
571 }
572 None => ek_station::boot(&engine, &ui_kinds(&shared, opts.dm_rt), cfg),
573 };
574 boot.map_err(|e| format!("boot: {e:?}"))?;
575 if let Err(e) = engine.start() {
580 engine.close_all(Duration::from_secs(3));
581 return Err(format!("start: {e:?}"));
582 }
583 shared.set_assembly_locked(true);
584 }
585
586 let mut app = StageApp::new_setup(
587 Arc::clone(&engine),
588 Arc::clone(&shared),
589 ui_kinds(&shared, opts.dm_rt),
590 store_ok,
591 opts.ready_marker.clone(),
592 );
593 if opts.auto_boot {
594 app = StageApp {
595 stage: Stage::Running,
596 ..app
597 };
598 app.write_ready_marker();
599 }
600
601 let watcher = spawn_host_watcher(&engine, &shared);
602 let _ = &watcher;
604
605 let opts = eframe::NativeOptions {
606 viewport: egui::ViewportBuilder::default()
607 .with_inner_size([1280.0, 860.0])
608 .with_title("ek-station"),
609 ..Default::default()
610 };
611 let result = eframe::run_native(
613 "ek-station",
614 opts,
615 Box::new(|cc| {
616 crate::fonts::apply_cjk_fonts(&cc.egui_ctx); Ok(Box::new(app) as Box<dyn eframe::App>)
618 }),
619 );
620 shared.request_stop();
621 let _ = watcher.join();
622 result.map_err(|e| format!("ui: {e:?}"))
623}
624
625pub fn run_desktop() -> Result<(), String> {
629 let engine = Arc::new(MainEngine::new(None));
630 let shared = Arc::new(UiShared::new());
631 let store = ek_cfg::Store::eco().ok();
632 let app = StageApp::new_setup(
633 Arc::clone(&engine),
634 Arc::clone(&shared),
635 ui_kinds(&shared, true),
636 store,
637 None,
638 );
639 let watcher = spawn_host_watcher(&engine, &shared);
640 let opts = eframe::NativeOptions {
641 viewport: egui::ViewportBuilder::default()
642 .with_inner_size([1280.0, 860.0])
643 .with_title("ek-station 桌面"),
644 ..Default::default()
645 };
646 let result = eframe::run_native(
647 "ek-station 桌面",
648 opts,
649 Box::new(|cc| {
650 crate::fonts::apply_cjk_fonts(&cc.egui_ctx);
651 Ok(Box::new(app) as Box<dyn eframe::App>)
652 }),
653 );
654 shared.request_stop();
655 let _ = watcher.join();
656 result.map_err(|e| format!("ui: {e:?}"))
657}