use std::sync::Arc;
use std::time::Duration;
use ek_core::main_engine::MainEngine;
use ek_core::plugin::Plugin;
use ek_dm::plugin::DmConfig;
use ek_station::{PlugKind, StationConfig, default_kinds};
use crate::plugin::{UI_NAME, UiPlugin};
use crate::state::UiShared;
pub fn ui_kinds(shared: &Arc<UiShared>, dm_rt: bool) -> Vec<PlugKind> {
let mut kinds: Vec<PlugKind> = default_kinds()
.into_iter()
.filter(|k| k.name != "dm")
.collect();
kinds.push(PlugKind {
name: "dm",
doc: "数据管理器(KlineManager;real_time 更新门开,行情板/K线页取 eBarUpdate./eBar.)",
build: Arc::new(move || -> Arc<dyn Plugin> {
Arc::new(ek_dm::plugin::DmPlugin::new(DmConfig {
real_time: dm_rt,
..DmConfig::minute("dm")
}))
}),
build_named: None,
name_validator: None,
});
kinds.push(PlugKind {
name: UI_NAME,
doc: "常驻观测界面(eLog + eTimer 1s 快照;关窗即 stop)",
build: {
let shared = Arc::clone(shared);
Arc::new(move || -> Arc<dyn Plugin> { Arc::new(UiPlugin::new(Arc::clone(&shared))) })
},
build_named: None,
name_validator: None,
});
kinds
}
pub fn spawn_host_watcher(
engine: &Arc<MainEngine>,
shared: &Arc<UiShared>,
) -> std::thread::JoinHandle<()> {
let engine = Arc::clone(engine);
let shared = Arc::clone(shared);
std::thread::spawn(move || {
while !shared.is_stop() {
std::thread::sleep(Duration::from_millis(120));
}
shared.log_line("[host] 窗口关闭,close_all …");
engine.close_all(Duration::from_secs(3));
shared.log_line("[host] 已收场");
})
}
fn bundle_op(shared: &Arc<UiShared>, engine: &MainEngine, dir: &str, export: bool) -> String {
let host = crate::faces::CfgBridge::new(shared);
let dest = std::path::Path::new(dir);
if dir.trim().is_empty() {
return "目录为空(先填路径)".to_string();
}
if export {
let Some(root) = host.cfg.as_ref().map(|c| c.root().to_path_buf()) else {
return "导出失败:档案托底根不可用".to_string();
};
match crate::pluginwin::export_bundle(&root, dest) {
Ok((c, s)) => {
let msg = format!("导出完成:{c} 段 → {}(跳过非段文件 {s})", dest.display());
shared.log_line(format!("[ui] {msg}"));
msg
}
Err(e) => format!("导出失败:{e}"),
}
} else {
let lines = crate::pluginwin::import_bundle(&host, dest, &|ns, sec| {
crate::faces::apply_section(engine, ns, sec)
});
let msg = lines.join(" | ");
shared.log_line(format!("[ui] 档案包导入:{msg}"));
msg
}
}
struct StationApp {
shared: Arc<UiShared>,
engine: Arc<MainEngine>,
fs: crate::faces::FaceStates,
wins_open: std::collections::HashMap<String, bool>,
pluginwin_prev: bool,
bundle_dir: String,
bundle_notice: Option<String>,
}
impl eframe::App for StationApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let host = crate::faces::CfgBridge::new(&self.shared);
let mounted = crate::faces::mounted(&self.engine);
let pw = self.shared.pluginwin_open();
if pw && !self.pluginwin_prev {
self.wins_open.clear();
}
self.pluginwin_prev = pw;
if pw {
for (name, title) in mounted.iter() {
let mut open = *self.wins_open.get(name).unwrap_or(&true);
egui::Window::new(title.as_str())
.id(egui::Id::new(format!("pluginwin-{name}")))
.open(&mut open)
.default_width(460.0)
.show(ctx, |ui| {
crate::faces::render_face(ui, &self.engine, name, &host, &mut self.fs);
});
self.wins_open.insert(name.clone(), open);
}
}
if self.shared.bundle_open() {
let mut open = true;
egui::Window::new("档案包")
.id(egui::Id::new("bundlewin"))
.open(&mut open)
.default_width(430.0)
.show(ctx, |ui| {
ui.label("目录(导出目标 / 导入来源,同一目录可两用):");
ui.add(
egui::TextEdit::singleline(&mut self.bundle_dir)
.desired_width(400.0)
.hint_text("C:\\Users\\<你>\\Documents\\ek-bundle"),
);
ui.horizontal(|ui| {
if ui.button("导出(段文件整批复制)").clicked() {
self.bundle_notice =
Some(bundle_op(&self.shared, &self.engine, &self.bundle_dir, true));
}
if ui.button("导入(逐段收编+受理)").clicked() {
self.bundle_notice = Some(bundle_op(
&self.shared,
&self.engine,
&self.bundle_dir,
false,
));
}
});
if let Some(n) = &self.bundle_notice {
ui.label(egui::RichText::new(n.as_str()).monospace().small());
}
ui.label(
egui::RichText::new("密码不落包(在 Windows 凭据管理器);换机后口令走 ekapp add-account(stdin)")
.small()
.weak(),
);
});
self.shared.set_bundle_open(open);
}
crate::render_body(ctx, &self.shared);
ctx.request_repaint_after(Duration::from_secs(1));
}
}
#[derive(Debug, Clone)]
pub struct RunOpts {
pub ready_marker: Option<std::path::PathBuf>,
pub dm_rt: bool,
}
impl Default for RunOpts {
fn default() -> Self {
Self {
ready_marker: None,
dm_rt: true,
}
}
}
impl RunOpts {
pub fn plain() -> Self {
Self::default()
}
}
pub fn run_station_ui(cfg: &StationConfig, opts: &RunOpts) -> Result<(), String> {
let engine = Arc::new(MainEngine::new(None));
let shared = Arc::new(UiShared::new());
let store = ek_cfg::Store::eco();
let boot = match &store {
Ok(s) => ek_station::boot_with_cfg(&engine, &ui_kinds(&shared, opts.dm_rt), cfg, Some(s)),
Err(e) => {
let _ = e; ek_station::boot(&engine, &ui_kinds(&shared, opts.dm_rt), cfg)
}
};
boot.map_err(|e| format!("boot: {e:?}"))?;
if let Err(e) = engine.start() {
engine.close_all(Duration::from_secs(3));
return Err(format!("start: {e:?}"));
}
if let Some(marker) = &opts.ready_marker {
use std::io::Write;
if let Ok(mut f) = std::fs::File::create(marker) {
let _ = f.write_all(crate::launch::READY_TEXT.as_bytes());
} else {
eprintln!("ek-ui: ready marker write failed: {}", marker.display());
}
}
let watcher = spawn_host_watcher(&engine, &shared);
let opts = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1280.0, 860.0])
.with_title("ek-station"),
..Default::default()
};
let result = eframe::run_native(
"ek-station",
opts,
Box::new(|cc| {
crate::fonts::apply_cjk_fonts(&cc.egui_ctx); Ok(Box::new(StationApp {
shared: Arc::clone(&shared),
engine: Arc::clone(&engine),
fs: crate::faces::FaceStates::new(),
wins_open: std::collections::HashMap::new(),
pluginwin_prev: false,
bundle_dir: String::new(),
bundle_notice: None,
}) as Box<dyn eframe::App>)
}),
);
shared.request_stop();
let _ = watcher.join();
result.map_err(|e| format!("ui: {e:?}"))
}