1use std::sync::Arc;
10use std::time::Duration;
11
12use ek_core::main_engine::MainEngine;
13use ek_core::plugin::Plugin;
14use ek_dm::plugin::DmConfig;
15use ek_station::{PlugKind, StationConfig, default_kinds};
16
17use crate::plugin::{UI_NAME, UiPlugin};
18use crate::state::UiShared;
19
20pub fn ui_kinds(shared: &Arc<UiShared>, dm_rt: bool) -> Vec<PlugKind> {
28 let mut kinds: Vec<PlugKind> = default_kinds()
29 .into_iter()
30 .filter(|k| k.name != "dm")
31 .collect();
32 kinds.push(PlugKind {
33 name: "dm",
34 doc: "数据管理器(KlineManager;real_time 更新门开,行情板/K线页取 eBarUpdate./eBar.)",
35 build: Arc::new(move || -> Arc<dyn Plugin> {
36 Arc::new(ek_dm::plugin::DmPlugin::new(DmConfig {
37 real_time: dm_rt,
38 ..DmConfig::minute("dm")
39 }))
40 }),
41 build_named: None,
42 name_validator: None,
43 });
44 kinds.push(PlugKind {
45 name: UI_NAME,
46 doc: "常驻观测界面(eLog + eTimer 1s 快照;关窗即 stop)",
47 build: {
48 let shared = Arc::clone(shared);
49 Arc::new(move || -> Arc<dyn Plugin> { Arc::new(UiPlugin::new(Arc::clone(&shared))) })
50 },
51 build_named: None,
52 name_validator: None,
53 });
54 kinds
55}
56
57pub fn spawn_host_watcher(
61 engine: &Arc<MainEngine>,
62 shared: &Arc<UiShared>,
63) -> std::thread::JoinHandle<()> {
64 let engine = Arc::clone(engine);
65 let shared = Arc::clone(shared);
66 std::thread::spawn(move || {
67 while !shared.is_stop() {
68 std::thread::sleep(Duration::from_millis(120));
69 }
70 shared.log_line("[host] 窗口关闭,close_all …");
71 engine.close_all(Duration::from_secs(3));
72 shared.log_line("[host] 已收场");
73 })
74}
75
76fn bundle_op(shared: &Arc<UiShared>, engine: &MainEngine, dir: &str, export: bool) -> String {
78 let host = crate::faces::CfgBridge::new(shared);
79 let dest = std::path::Path::new(dir);
80 if dir.trim().is_empty() {
81 return "目录为空(先填路径)".to_string();
82 }
83 if export {
84 let Some(root) = host.cfg.as_ref().map(|c| c.root().to_path_buf()) else {
85 return "导出失败:档案托底根不可用".to_string();
86 };
87 match crate::pluginwin::export_bundle(&root, dest) {
88 Ok((c, s)) => {
89 let msg = format!("导出完成:{c} 段 → {}(跳过非段文件 {s})", dest.display());
90 shared.log_line(format!("[ui] {msg}"));
91 msg
92 }
93 Err(e) => format!("导出失败:{e}"),
94 }
95 } else {
96 let lines = crate::pluginwin::import_bundle(&host, dest, &|ns, sec| {
97 crate::faces::apply_section(engine, ns, sec)
98 });
99 let msg = lines.join(" | ");
100 shared.log_line(format!("[ui] 档案包导入:{msg}"));
101 msg
102 }
103}
104
105struct StationApp {
107 shared: Arc<UiShared>,
108 engine: Arc<MainEngine>,
110 fs: crate::faces::FaceStates,
112 wins_open: std::collections::HashMap<String, bool>,
114 pluginwin_prev: bool,
116 bundle_dir: String,
118 bundle_notice: Option<String>,
119}
120
121impl eframe::App for StationApp {
122 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
123 let host = crate::faces::CfgBridge::new(&self.shared);
124 let mounted = crate::faces::mounted(&self.engine);
125 let pw = self.shared.pluginwin_open();
128 if pw && !self.pluginwin_prev {
129 self.wins_open.clear();
130 }
131 self.pluginwin_prev = pw;
132 if pw {
133 for (name, title) in mounted.iter() {
134 let mut open = *self.wins_open.get(name).unwrap_or(&true);
135 egui::Window::new(title.as_str())
136 .id(egui::Id::new(format!("pluginwin-{name}")))
137 .open(&mut open)
138 .default_width(460.0)
139 .show(ctx, |ui| {
140 crate::faces::render_face(ui, &self.engine, name, &host, &mut self.fs);
141 });
142 self.wins_open.insert(name.clone(), open);
143 }
144 }
145 if self.shared.bundle_open() {
148 let mut open = true;
149 egui::Window::new("档案包")
150 .id(egui::Id::new("bundlewin"))
151 .open(&mut open)
152 .default_width(430.0)
153 .show(ctx, |ui| {
154 ui.label("目录(导出目标 / 导入来源,同一目录可两用):");
155 ui.add(
156 egui::TextEdit::singleline(&mut self.bundle_dir)
157 .desired_width(400.0)
158 .hint_text("C:\\Users\\<你>\\Documents\\ek-bundle"),
159 );
160 ui.horizontal(|ui| {
161 if ui.button("导出(段文件整批复制)").clicked() {
162 self.bundle_notice =
163 Some(bundle_op(&self.shared, &self.engine, &self.bundle_dir, true));
164 }
165 if ui.button("导入(逐段收编+受理)").clicked() {
166 self.bundle_notice = Some(bundle_op(
167 &self.shared,
168 &self.engine,
169 &self.bundle_dir,
170 false,
171 ));
172 }
173 });
174 if let Some(n) = &self.bundle_notice {
175 ui.label(egui::RichText::new(n.as_str()).monospace().small());
176 }
177 ui.label(
178 egui::RichText::new("密码不落包(在 Windows 凭据管理器);换机后口令走 ekapp add-account(stdin)")
179 .small()
180 .weak(),
181 );
182 });
183 self.shared.set_bundle_open(open);
184 }
185 crate::render_body(ctx, &self.shared);
187 ctx.request_repaint_after(Duration::from_secs(1));
188 }
189}
190
191#[derive(Debug, Clone)]
194pub struct RunOpts {
195 pub ready_marker: Option<std::path::PathBuf>,
197 pub dm_rt: bool,
199}
200
201impl Default for RunOpts {
202 fn default() -> Self {
205 Self {
206 ready_marker: None,
207 dm_rt: true,
208 }
209 }
210}
211
212impl RunOpts {
213 pub fn plain() -> Self {
215 Self::default()
216 }
217}
218
219pub fn run_station_ui(cfg: &StationConfig, opts: &RunOpts) -> Result<(), String> {
222 let engine = Arc::new(MainEngine::new(None));
223 let shared = Arc::new(UiShared::new());
224
225 let store = ek_cfg::Store::eco();
230 let boot = match &store {
231 Ok(s) => ek_station::boot_with_cfg(&engine, &ui_kinds(&shared, opts.dm_rt), cfg, Some(s)),
232 Err(e) => {
233 let _ = e; ek_station::boot(&engine, &ui_kinds(&shared, opts.dm_rt), cfg)
235 }
236 };
237 boot.map_err(|e| format!("boot: {e:?}"))?;
238 if let Err(e) = engine.start() {
243 engine.close_all(Duration::from_secs(3));
244 return Err(format!("start: {e:?}"));
245 }
246
247 if let Some(marker) = &opts.ready_marker {
248 use std::io::Write;
249 if let Ok(mut f) = std::fs::File::create(marker) {
250 let _ = f.write_all(crate::launch::READY_TEXT.as_bytes());
251 } else {
252 eprintln!("ek-ui: ready marker write failed: {}", marker.display());
253 }
254 }
255
256 let watcher = spawn_host_watcher(&engine, &shared);
257
258 let opts = eframe::NativeOptions {
259 viewport: egui::ViewportBuilder::default()
260 .with_inner_size([1280.0, 860.0])
261 .with_title("ek-station"),
262 ..Default::default()
263 };
264 let result = eframe::run_native(
266 "ek-station",
267 opts,
268 Box::new(|cc| {
269 crate::fonts::apply_cjk_fonts(&cc.egui_ctx); Ok(Box::new(StationApp {
271 shared: Arc::clone(&shared),
272 engine: Arc::clone(&engine),
273 fs: crate::faces::FaceStates::new(),
274 wins_open: std::collections::HashMap::new(),
275 pluginwin_prev: false,
276 bundle_dir: String::new(),
277 bundle_notice: None,
278 }) as Box<dyn eframe::App>)
279 }),
280 );
281 shared.request_stop();
282 let _ = watcher.join();
283 result.map_err(|e| format!("ui: {e:?}"))
284}