use std::sync::Arc;
use std::time::Duration;
use ek_core::main_engine::{MainEngine, PluginState};
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.push(PlugKind {
name: "tq",
doc: "天勤桥(行情/K线接入;凭据走密库引用,口令永不进档案)",
build: Arc::new(|| -> Arc<dyn Plugin> {
let bridge_cfg = ek_tq::bridge::BridgeConfig {
program: ek_tq::pyenv::python_program(),
script: ek_tq::pyenv::real_script(),
handshake_ms: ek_tq::consts::BRIDGE_HANDSHAKE_MS,
..Default::default()
};
Arc::new(ek_tq::plugin::TqPlugin::new(
ek_tq::plugin::TqPluginSpec::new(bridge_cfg),
))
}),
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
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
Setup,
Running,
}
pub struct StageApp {
shared: Arc<UiShared>,
engine: Arc<MainEngine>,
kinds: Vec<PlugKind>,
sel: Vec<bool>,
store: Option<ek_cfg::Store>,
stage: Stage,
notice: Option<String>,
list_dir: String,
fs: crate::faces::FaceStates,
wins_open: std::collections::HashMap<String, bool>,
pluginwin_prev: bool,
bundle_dir: String,
bundle_notice: Option<String>,
ready_marker: Option<std::path::PathBuf>,
watcher: Option<std::thread::JoinHandle<()>>,
}
impl StageApp {
#[must_use]
pub fn new_setup(
engine: Arc<MainEngine>,
shared: Arc<UiShared>,
kinds: Vec<PlugKind>,
store: Option<ek_cfg::Store>,
ready_marker: Option<std::path::PathBuf>,
) -> Self {
let sel = kinds.iter().map(|_| true).collect::<Vec<bool>>();
Self {
shared,
engine,
kinds,
sel,
store,
stage: Stage::Setup,
notice: None,
list_dir: String::new(),
fs: crate::faces::FaceStates::new(),
wins_open: std::collections::HashMap::new(),
pluginwin_prev: false,
bundle_dir: String::new(),
bundle_notice: None,
ready_marker,
watcher: None,
}
}
#[must_use]
pub fn stage(&self) -> Stage {
self.stage
}
#[must_use]
pub fn picked(&self) -> Vec<String> {
self.kinds
.iter()
.zip(self.sel.iter())
.filter(|(_, ok)| **ok)
.map(|(k, _)| k.name.to_string())
.collect()
}
#[must_use]
pub fn assembled(&self) -> bool {
!self.engine.plugin_names().is_empty()
}
pub fn set_selected(&mut self, name: &str, on: bool) {
if let Some(i) = self.kinds.iter().position(|k| k.name == name) {
self.sel[i] = on && name != UI_NAME;
}
}
pub fn do_assemble(&mut self) -> String {
let kinds: Vec<PlugKind> = self
.kinds
.iter()
.enumerate()
.filter(|(i, _)| self.sel[*i])
.map(|(_, k)| k.clone())
.collect();
let names: Vec<String> = kinds.iter().map(|k| k.name.to_string()).collect();
let cfg = StationConfig {
plugins: names.clone(),
};
let r = match &self.store {
Some(s) => ek_station::boot_with_cfg(&self.engine, &kinds, &cfg, Some(s)),
None => ek_station::boot(&self.engine, &kinds, &cfg),
};
match r {
Ok(_) => {
self.shared
.log_line(format!("[ui] 装配:{} 壳(未启动)", names.join(",")));
format!(
"已装配 {} 壳(未启动;参数面可改):{}",
names.len(),
names.join(",")
)
}
Err(e) => {
let msg = format!("装配失败:{e:?}");
self.shared.log_line(format!("[ui] {msg}"));
msg
}
}
}
pub fn do_start(&mut self) -> String {
if let Err(e) = self.engine.start() {
self.stage = Stage::Running;
self.shared.set_assembly_locked(false);
self.ensure_watcher();
let msg = format!("启动失败:{e:}(运维面可修,装配不锁定)");
self.shared.log_line(format!("[ui] {msg}"));
return msg;
}
let not_ready: Vec<String> = self
.engine
.plugin_names()
.into_iter()
.filter(|n| !matches!(self.engine.get_state(n), Some(PluginState::Running)))
.collect();
self.stage = Stage::Running;
self.shared.set_assembly_locked(not_ready.is_empty());
self.ensure_watcher();
self.write_ready_marker();
let shared_restore = Arc::clone(&self.shared);
std::thread::spawn(move || {
let rows = shared_restore.restore_saved_accounts();
let _ = rows;
});
let msg = if not_ready.is_empty() {
"已启动:装配面已锁定(只可看不可动)".to_string()
} else {
format!(
"已启动,{} 壳未就绪:{} — 装配**不锁定**(运维面留修路)",
not_ready.len(),
not_ready.join(",")
)
};
self.shared.log_line(format!("[ui] {msg}"));
msg
}
pub fn export_plugin_list(&mut self, dir: &str) -> String {
let factory_names: Vec<String> = default_kinds()
.into_iter()
.map(|k| k.name.to_string())
.collect();
let picked = self.picked();
let exportable: Vec<String> = picked
.iter()
.filter(|n| factory_names.iter().any(|f| f == *n))
.cloned()
.collect();
let skipped: Vec<String> = picked
.iter()
.filter(|n| !factory_names.iter().any(|f| f == *n))
.cloned()
.collect();
if exportable.is_empty() {
return "无可导出(勾选里没有无界面可用的组件)".to_string();
}
if dir.trim().is_empty() {
return "目录为空(先填路径)".to_string();
}
let path = std::path::Path::new(dir).join("ek-plugins.txt");
let header = if skipped.is_empty() {
"# ek-station 启动清单(ek-ui 阶段机导出,一行一名)".to_string()
} else {
format!(
"# ek-station 启动清单(ek-ui 阶段机导出,一行一名;跳过界面自带工厂:{})",
skipped.join(",")
)
};
let body = format!("{}\n{}\n", header, exportable.join("\n"));
match std::fs::File::create(&path).and_then(|mut f| {
use std::io::Write;
f.write_all(body.as_bytes())
}) {
Ok(()) => {
self.shared
.log_line(format!("[ui] 启动清单已导出:{}", path.display()));
if skipped.is_empty() {
format!("清单已导出:{}({} 项)", path.display(), exportable.len())
} else {
format!(
"清单已导出:{}({} 项;跳过界面自带工厂 {})",
path.display(),
exportable.len(),
skipped.join(",")
)
}
}
Err(e) => format!("导出失败:{e}"),
}
}
fn ensure_watcher(&mut self) {
if self.watcher.is_none() {
self.watcher = Some(spawn_host_watcher(&self.engine, &self.shared));
}
}
fn write_ready_marker(&self) {
let Some(marker) = &self.ready_marker else {
return;
};
use std::io::Write;
if let Ok(mut f) = std::fs::File::create(marker) {
let _ = f.write_all(crate::launch::READY_TEXT.as_bytes());
}
}
pub fn render_setup_panel(&mut self, ui: &mut egui::Ui) {
ui.heading("组件配置(启动前)");
ui.label("勾选要装载的组件 → 装配 → 参数面可改 → 启动运行;启动后装配锁定(只可看不可动)。");
ui.separator();
egui::ScrollArea::vertical()
.max_height(210.0)
.show(ui, |ui| {
for (i, k) in self.kinds.iter().enumerate() {
ui.horizontal(|ui| {
if k.name == UI_NAME {
let mut t = true;
ui.add_enabled(false, egui::Checkbox::new(&mut t, k.name));
self.sel[i] = true;
} else {
ui.checkbox(&mut self.sel[i], k.name);
}
let _ = ui.label(egui::RichText::new(k.doc.to_string()).small().weak());
});
}
});
ui.separator();
let picked = self.picked();
ui.label(if picked.is_empty() {
"已选: (无)".to_string()
} else {
format!("已选: {}", picked.join(", "))
});
ui.horizontal(|ui| {
let assembled = self.assembled();
if ui
.add_enabled(
!assembled && !picked.is_empty(),
egui::Button::new("装配(按勾选)"),
)
.clicked()
{
let msg = self.do_assemble();
self.notice = Some(msg);
}
if ui
.add_enabled(assembled, egui::Button::new("启动运行"))
.clicked()
{
let msg = self.do_start();
self.notice = Some(msg);
}
if ui.button("参数面(改参数)").clicked() {
self.shared
.set_pluginwin_open(!self.shared.pluginwin_open());
}
if ui.button("档案包(导入参数)").clicked() {
self.shared.set_bundle_open(!self.shared.bundle_open());
}
});
ui.horizontal(|ui| {
ui.add(
egui::TextEdit::singleline(&mut self.list_dir)
.desired_width(300.0)
.hint_text("启动清单导出目录,如 C:\\Users\\<你>\\Documents"),
);
if ui.button("导出启动清单(--from 可用)").clicked() {
let dir = self.list_dir.clone();
let msg = self.export_plugin_list(&dir);
self.notice = Some(msg);
}
});
if self.assembled() && self.stage == Stage::Setup {
ui.label(
egui::RichText::new("已装配(未启动):参数面窗可改,启动时按档案回灌。")
.color(crate::tokens::info(ui.ctx()))
.small(),
);
}
if let Some(n) = &self.notice {
ui.label(egui::RichText::new(n.as_str()).monospace().small());
}
}
}
impl eframe::App for StageApp {
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;
let setup = self.stage == Stage::Setup;
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
.add_enabled(
!setup,
egui::Button::new("导出(段文件整批复制)"),
)
.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);
}
if self.stage == Stage::Setup {
egui::CentralPanel::default().show(ctx, |ui| {
self.render_setup_panel(ui);
});
} else {
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,
pub auto_boot: bool,
}
impl Default for RunOpts {
fn default() -> Self {
Self {
ready_marker: None,
dm_rt: true,
auto_boot: 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 store_ok = store.ok();
if opts.auto_boot {
let boot = match &store_ok {
Some(s) => {
ek_station::boot_with_cfg(&engine, &ui_kinds(&shared, opts.dm_rt), cfg, Some(s))
}
None => 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:?}"));
}
shared.set_assembly_locked(true);
}
let mut app = StageApp::new_setup(
Arc::clone(&engine),
Arc::clone(&shared),
ui_kinds(&shared, opts.dm_rt),
store_ok,
opts.ready_marker.clone(),
);
if opts.auto_boot {
app = StageApp {
stage: Stage::Running,
..app
};
app.write_ready_marker();
}
let watcher = spawn_host_watcher(&engine, &shared);
let _ = &watcher;
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(app) as Box<dyn eframe::App>)
}),
);
shared.request_stop();
let _ = watcher.join();
result.map_err(|e| format!("ui: {e:?}"))
}
pub fn run_desktop() -> Result<(), String> {
let engine = Arc::new(MainEngine::new(None));
let shared = Arc::new(UiShared::new());
let store = ek_cfg::Store::eco().ok();
let app = StageApp::new_setup(
Arc::clone(&engine),
Arc::clone(&shared),
ui_kinds(&shared, true),
store,
None,
);
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(app) as Box<dyn eframe::App>)
}),
);
shared.request_stop();
let _ = watcher.join();
result.map_err(|e| format!("ui: {e:?}"))
}