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] 已收场");
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
Setup,
Running,
}
pub struct StageApp {
shared: Arc<UiShared>,
engine: Arc<MainEngine>,
kinds: Vec<PlugKind>,
sel: Arc<std::sync::Mutex<Vec<bool>>>,
store: Option<ek_cfg::Store>,
stage: Stage,
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: Arc::new(std::sync::Mutex::new(sel)),
store,
stage: Stage::Setup,
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.lock().unwrap().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 name == UI_NAME {
return; }
if let Some(i) = self.kinds.iter().position(|k| k.name == name) {
self.sel.lock().unwrap()[i] = on;
}
}
pub fn do_assemble(&mut self) -> String {
let mut kinds: Vec<PlugKind> = self
.kinds
.iter()
.enumerate()
.filter(|(i, _)| self.sel.lock().unwrap()[*i])
.map(|(_, k)| k.clone())
.collect();
if !kinds.iter().any(|k| k.name == "cfg")
&& let Some(k) = self.kinds.iter().find(|k| k.name == "cfg")
{
kinds.push(k.clone());
}
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 !self.assembled() {
let msg = self.do_assemble();
if !self.assembled() {
return msg;
}
let _ = msg;
}
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();
for gw in &rows {
let Some(a) = shared_restore.account(gw) else {
continue;
};
if a.auto_connect
&& matches!(a.stage, crate::state::AccountStage::NotAssembled)
&& !a.connected
{
let form = a.form.clone();
crate::render::push_connect(&shared_restore, gw, &form);
}
}
});
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
}
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) {
use crate::render::{OpTone, op_button, pinned_table};
ui.heading("组件配置(启动前)");
ui.label("勾选要装载的组件 → 「启动运行」(先按勾选装配,再起跑;装配失败会报错,等您改勾选后重按)。");
ui.separator();
let sel = Arc::clone(&self.sel);
let (row_h, head_h) = crate::render::table_heights(ui);
let table_h = head_h + ui.visuals().clip_rect_margin
+ self.kinds.len() as f32 * row_h
+ 6.0;
let avail_x = ui.available_width();
ui.allocate_ui(egui::vec2(avail_x, table_h), |ui| {
pinned_table(ui, "setup_grid", &["装载", "组件", "说明"], |body, row_h| {
for (i, k) in self.kinds.iter().enumerate() {
let name = k.name.to_string();
let doc = k.doc.to_string();
let sel_cell = Arc::clone(&sel);
let label0 = name.clone();
body.row(row_h, false, |row| {
row.cell(move |ui, _ctx| {
if label0 == UI_NAME {
let mut t = true;
ui.add_enabled(false, egui::Checkbox::new(&mut t, ""));
sel_cell.lock().unwrap()[i] = true;
} else {
let mut on = sel_cell.lock().unwrap()[i];
if ui.checkbox(&mut on, "").changed() {
sel_cell.lock().unwrap()[i] = on;
}
}
});
let label = name.clone();
row.cell(move |ui, _ctx| {
ui.label(egui::RichText::new(&label).strong());
});
row.cell(move |ui, _ctx| {
ui.add(
egui::Label::new(egui::RichText::new(&doc))
.wrap_mode(egui::TextWrapMode::Extend),
);
});
});
}
});
});
ui.separator();
let picked = self.picked();
ui.label(if picked.is_empty() {
"已选: (无)".to_string()
} else {
format!("已选: {}", picked.join(", "))
});
ui.horizontal(|ui| {
if ui
.add_enabled_ui(!picked.is_empty(), |ui| {
op_button(ui, "启动运行", OpTone::Primary).clicked()
})
.inner
{
self.notice = Some(self.do_start());
}
});
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) {
crate::render::apply_ui_style(ctx, self.shared.theme());
if self.stage == Stage::Setup {
self.setup_fit_once(ctx);
egui::CentralPanel::default().show(ctx, |ui| {
self.render_setup_panel(ui);
});
} else {
let sent_key = egui::Id::new("running_geo_sent");
if !ctx.data(|d| d.get_temp::<bool>(sent_key).unwrap_or(false)) {
ctx.data_mut(|d| d.insert_temp(sent_key, true));
let maximized = ctx.viewport(|vp| vp.builder.maximized.unwrap_or(false));
if !maximized {
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(
1280.0, 860.0,
)));
}
}
crate::render_body(ctx, &self.shared);
}
ctx.request_repaint_after(Duration::from_secs(1));
}
}
impl StageApp {
fn setup_fit_once(&mut self, ctx: &egui::Context) {
use egui::Id;
#[derive(Clone, Copy)]
struct FitOneShot {
w: f32,
h: f32,
stable: u8,
frames: u16,
done: bool,
}
let key = Id::new(("fit_oneshot", "ekapp_main"));
let st = ctx.data(|d| d.get_temp::<FitOneShot>(key)).unwrap_or(FitOneShot {
w: -1.0,
h: -1.0,
stable: 0,
frames: 0,
done: false,
});
let maximized = ctx.viewport(|vp| vp.builder.maximized.unwrap_or(false));
if st.done || maximized {
return;
}
let mut sc = (*ctx.style()).clone();
sc.wrap_mode = Some(egui::TextWrapMode::Extend);
let measure_w = ctx
.data(|d| d.get_temp::<f32>(Id::new(("fit_w", "ekapp_main"))).unwrap_or(48.0));
let mut probe = egui::Ui::new(
ctx.clone(),
Id::new(("fit_probe", "ekapp_main")),
egui::UiBuilder::new()
.global_scope(true)
.invisible()
.style(sc)
.max_rect(egui::Rect::from_min_size(
egui::pos2(0.0, 0.0),
egui::vec2(measure_w.max(48.0), 1.0e5),
))
.layout(egui::Layout::top_down(egui::Align::Min)),
);
self.render_setup_panel(&mut probe);
let nat_w = probe.min_rect().width();
let nat_h = probe.min_rect().height();
ctx.data_mut(|d| d.insert_temp(Id::new(("fit_w", "ekapp_main")), nat_w));
let stable = (nat_w - st.w).abs() < 0.5 && (nat_h - st.h).abs() < 0.5;
let mut next = FitOneShot {
w: nat_w,
h: nat_h,
stable: if stable { st.stable + 1 } else { 0 },
frames: st.frames + 1,
done: false,
};
if next.stable >= 3 && next.frames >= 6 {
let want = egui::vec2(
(nat_w + 20.0).clamp(420.0, 1280.0),
(nat_h + 26.0).clamp(260.0, 900.0),
);
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(want));
next.done = true;
}
ctx.data_mut(|d| d.insert_temp(key, next));
}
}
#[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:?}"))
}