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 bundle_dir: String,
114 bundle_notice: Option<String>,
115}
116
117impl eframe::App for StationApp {
118 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
119 let host = crate::faces::CfgBridge::new(&self.shared);
120 let mounted = crate::faces::mounted(&self.engine);
121 if self.shared.pluginwin_open() {
123 for (name, title) in mounted.iter() {
124 let mut open = true;
125 egui::Window::new(title.as_str())
126 .id(egui::Id::new(format!("pluginwin-{name}")))
127 .open(&mut open)
128 .default_width(460.0)
129 .show(ctx, |ui| {
130 crate::faces::render_face(ui, &self.engine, name, &host, &mut self.fs);
131 });
132 let _ = open;
133 }
134 }
135 if self.shared.bundle_open() {
137 let mut open = true;
138 egui::Window::new("档案包")
139 .id(egui::Id::new("bundlewin"))
140 .open(&mut open)
141 .default_width(430.0)
142 .show(ctx, |ui| {
143 ui.label("目录(导出目标 / 导入来源,同一目录可两用):");
144 ui.add(
145 egui::TextEdit::singleline(&mut self.bundle_dir)
146 .desired_width(400.0)
147 .hint_text("C:\\Users\\<你>\\Documents\\ek-bundle"),
148 );
149 ui.horizontal(|ui| {
150 if ui.button("导出(段文件整批复制)").clicked() {
151 self.bundle_notice =
152 Some(bundle_op(&self.shared, &self.engine, &self.bundle_dir, true));
153 }
154 if ui.button("导入(逐段收编+受理)").clicked() {
155 self.bundle_notice = Some(bundle_op(
156 &self.shared,
157 &self.engine,
158 &self.bundle_dir,
159 false,
160 ));
161 }
162 });
163 if let Some(n) = &self.bundle_notice {
164 ui.label(egui::RichText::new(n.as_str()).monospace().small());
165 }
166 ui.label(
167 egui::RichText::new("密码不落包(在 Windows 凭据管理器);换机后口令走 ekapp add-account(stdin)")
168 .small()
169 .weak(),
170 );
171 });
172 let _ = open;
173 }
174 crate::render_body(ctx, &self.shared);
176 ctx.request_repaint_after(Duration::from_secs(1));
177 }
178}
179
180#[derive(Debug, Clone)]
183pub struct RunOpts {
184 pub ready_marker: Option<std::path::PathBuf>,
186 pub dm_rt: bool,
188}
189
190impl Default for RunOpts {
191 fn default() -> Self {
194 Self {
195 ready_marker: None,
196 dm_rt: true,
197 }
198 }
199}
200
201impl RunOpts {
202 pub fn plain() -> Self {
204 Self::default()
205 }
206}
207
208pub fn run_station_ui(cfg: &StationConfig, opts: &RunOpts) -> Result<(), String> {
211 let engine = Arc::new(MainEngine::new(None));
212 let shared = Arc::new(UiShared::new());
213
214 let store = ek_cfg::Store::eco();
219 let boot = match &store {
220 Ok(s) => ek_station::boot_with_cfg(&engine, &ui_kinds(&shared, opts.dm_rt), cfg, Some(s)),
221 Err(e) => {
222 let _ = e; ek_station::boot(&engine, &ui_kinds(&shared, opts.dm_rt), cfg)
224 }
225 };
226 boot.map_err(|e| format!("boot: {e:?}"))?;
227 if let Err(e) = engine.start() {
232 engine.close_all(Duration::from_secs(3));
233 return Err(format!("start: {e:?}"));
234 }
235
236 if let Some(marker) = &opts.ready_marker {
237 use std::io::Write;
238 if let Ok(mut f) = std::fs::File::create(marker) {
239 let _ = f.write_all(crate::launch::READY_TEXT.as_bytes());
240 } else {
241 eprintln!("ek-ui: ready marker write failed: {}", marker.display());
242 }
243 }
244
245 let watcher = spawn_host_watcher(&engine, &shared);
246
247 let opts = eframe::NativeOptions {
248 viewport: egui::ViewportBuilder::default()
249 .with_inner_size([1280.0, 860.0])
250 .with_title("ek-station"),
251 ..Default::default()
252 };
253 let result = eframe::run_native(
255 "ek-station",
256 opts,
257 Box::new(|cc| {
258 crate::fonts::apply_cjk_fonts(&cc.egui_ctx); Ok(Box::new(StationApp {
260 shared: Arc::clone(&shared),
261 engine: Arc::clone(&engine),
262 fs: crate::faces::FaceStates::new(),
263 bundle_dir: String::new(),
264 bundle_notice: None,
265 }) as Box<dyn eframe::App>)
266 }),
267 );
268 shared.request_stop();
269 let _ = watcher.join();
270 result.map_err(|e| format!("ui: {e:?}"))
271}