1use std::collections::{BTreeMap, VecDeque};
7use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9
10use ek_data::bar::BarData;
11use ek_dm::wire::BarsFrame;
12use ek_station::wire::PlugRow;
13
14pub const RING_CAP: usize = 512;
16
17#[derive(Debug)]
19pub struct LogRing {
20 lines: VecDeque<String>,
21 cap: usize,
22 spilled: u64,
23}
24
25impl Default for LogRing {
26 fn default() -> Self {
27 Self::new(RING_CAP)
28 }
29}
30
31impl LogRing {
32 pub fn new(cap: usize) -> Self {
34 Self {
35 lines: VecDeque::with_capacity(cap),
36 cap,
37 spilled: 0,
38 }
39 }
40
41 pub fn push(&mut self, line: impl Into<String>) {
43 if self.cap == 0 {
44 self.spilled += 1;
45 return;
46 }
47 if self.lines.len() == self.cap {
48 self.lines.pop_front();
49 self.spilled += 1;
50 }
51 self.lines.push_back(line.into());
52 }
53
54 pub fn len(&self) -> usize {
56 self.lines.len()
57 }
58
59 pub fn is_empty(&self) -> bool {
61 self.lines.is_empty()
62 }
63
64 pub fn spilled(&self) -> u64 {
66 self.spilled
67 }
68
69 pub fn snapshot(&self) -> Vec<String> {
71 self.lines.iter().cloned().collect()
72 }
73
74 pub fn clear(&mut self) {
76 self.lines.clear();
77 }
78}
79
80#[derive(Debug, Clone, Default)]
82pub struct BusSnap {
83 pub rows: Vec<PlugRow>,
85 pub services: Vec<(String, Vec<String>)>,
87}
88
89#[derive(Debug, Clone, PartialEq)]
91pub enum UiCmd {
92 Start { factory: String, name: String },
95 Remove { name: String },
97 Reload {
99 interval: i64,
100 symbol: String,
101 bars: Vec<BarData>,
102 },
103 SendOrder {
109 req: ek_data::order_request::OrderRequest,
110 gateway: String,
111 mode: ek_oms::LockMode,
112 },
113 CancelOrder { orderid: String, gateway: String },
117 CtpConfigure { gateway: String, form: CtpForm },
121 Connect { gateway: String, form: CtpForm },
126 CtpSubscribe {
129 gateway: String,
130 symbols: Vec<String>,
131 },
132 Disconnect { gateway: String },
136}
137
138pub const KLINE_CELL_CAP: usize = 4096;
140
141pub const MAX_ACCOUNTS: usize = 8;
144
145pub const QUERY_REJECT_STREAK_LIMIT: u32 = 3;
150
151pub const ACCOUNT_STALE_SECS: i64 = 30;
156
157pub const STATUS_POLL_TICKS: u32 = 2;
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum ConfirmStep {
166 Armed,
167 Fired,
168 Switched,
169}
170
171#[must_use]
174pub fn is_quarantined(state: &str) -> bool {
175 state.contains("Quarantin")
176}
177
178#[derive(Debug, Default)]
182pub struct MarketStore {
183 cells: Mutex<BTreeMap<(String, i64), Vec<BarData>>>,
184 frames: AtomicU64,
186 updates: AtomicU64,
188 reloads: AtomicU64,
190}
191
192#[derive(Debug, Clone, PartialEq)]
194pub struct CellView {
195 pub symbol: String,
196 pub interval: i64,
197 pub bars: Vec<BarData>,
198}
199
200impl MarketStore {
201 pub fn on_frame(&self, frame: &BarsFrame) {
203 let mut cells = self.cells.lock().unwrap();
204 for b in &frame.bars {
205 let key = (b.symbol.clone(), frame.interval);
206 let cell = cells.entry(key).or_default();
207 match cell.last() {
208 Some(last) if b.datetime < last.datetime => continue,
209 Some(last) if b.datetime == last.datetime => {
210 let slot = cell.last_mut().expect("len>1");
211 *slot = b.clone();
212 }
213 _ => {
214 cell.push(b.clone());
215 if cell.len() > KLINE_CELL_CAP {
216 cell.remove(0);
217 }
218 }
219 }
220 }
221 self.frames.fetch_add(1, Ordering::AcqRel);
222 }
223
224 pub fn on_update(&self, b: &BarData) {
226 let mut cells = self.cells.lock().unwrap();
227 let iv = b
228 .interval
229 .unwrap_or(ek_data::constant::Interval::MINUTE)
230 .seconds();
231 let key = (b.symbol.clone(), iv);
232 let cell = cells.entry(key).or_default();
233 match cell.last() {
234 Some(last) if b.datetime == last.datetime => {
235 let slot = cell.last_mut().expect("len>1");
236 *slot = b.clone();
237 }
238 Some(last) if b.datetime < last.datetime => {}
239 _ => {
240 cell.push(b.clone());
241 if cell.len() > KLINE_CELL_CAP {
242 cell.remove(0);
243 }
244 }
245 }
246 self.updates.fetch_add(1, Ordering::AcqRel);
247 }
248
249 pub fn note_reload(&self) {
251 self.reloads.fetch_add(1, Ordering::AcqRel);
252 }
253
254 pub fn cells(&self) -> Vec<CellView> {
256 self.cells
257 .lock()
258 .unwrap()
259 .iter()
260 .map(|((sym, iv), bars)| CellView {
261 symbol: sym.clone(),
262 interval: *iv,
263 bars: bars.clone(),
264 })
265 .collect()
266 }
267
268 pub fn counts(&self) -> (u64, u64, u64) {
270 (
271 self.frames.load(Ordering::Acquire),
272 self.updates.load(Ordering::Acquire),
273 self.reloads.load(Ordering::Acquire),
274 )
275 }
276}
277
278#[derive(Debug, Default, Clone, PartialEq)]
281pub struct ReloadForm {
282 pub interval: String,
283 pub symbol: String,
284 pub csv: String,
285}
286
287pub fn parse_gateway(raw: &str, fallback: Option<&str>) -> Result<String, String> {
294 let g = raw.trim();
295 if !g.is_empty() {
296 if g.chars().any(|c| c.is_whitespace()) {
297 return Err(format!("网关解析: 含空白:{raw:?}"));
298 }
299 return Ok(g.to_string());
300 }
301 match fallback.map(|f| f.trim()).filter(|f| !f.is_empty()) {
302 Some(f) => Ok(f.to_string()),
303 None => Err("网关解析: 未选择账户(先在账户面板添加账户)".to_string()),
304 }
305}
306
307#[derive(Debug, Clone, PartialEq)]
310pub struct CtpForm {
311 pub broker_id: String,
312 pub td_addr: String,
313 pub td_user: String,
314 pub td_password: String,
315 pub td_auth_code: String,
316 pub td_app_id: String,
317 pub md_addr: String,
318 pub md_user: String,
319 pub md_password: String,
320 pub symbols: String,
322 pub accept_futures: bool,
325 pub accept_options: bool,
326}
327
328impl Default for CtpForm {
331 fn default() -> Self {
332 Self {
333 broker_id: String::new(),
334 td_addr: String::new(),
335 td_user: String::new(),
336 td_password: String::new(),
337 td_auth_code: String::new(),
338 td_app_id: String::new(),
339 md_addr: String::new(),
340 md_user: String::new(),
341 md_password: String::new(),
342 symbols: String::new(),
343 accept_futures: true,
344 accept_options: false,
345 }
346 }
347}
348
349impl CtpForm {
350 #[must_use]
354 pub fn product_classes(&self) -> Vec<String> {
355 let mut codes = Vec::new();
356 if self.accept_futures {
357 codes.push("1".to_string());
358 }
359 if self.accept_options {
360 codes.push("2".to_string());
361 }
362 codes
363 }
364
365 pub fn to_configure_req(&self) -> ek_ctp::plugin::ConfigureReq {
367 ek_ctp::plugin::ConfigureReq {
368 broker_id: self.broker_id.trim().to_string(),
369 td_addr: self.td_addr.trim().to_string(),
370 td_user: self.td_user.trim().to_string(),
371 td_password: self.td_password.clone(),
372 td_auth_code: self.td_auth_code.trim().to_string(),
373 td_app_id: self.td_app_id.trim().to_string(),
374 md_addr: self.md_addr.trim().to_string(),
375 md_user: self.md_user.trim().to_string(),
376 md_password: self.md_password.clone(),
377 product_classes: self.product_classes(),
378 }
379 }
380
381 pub fn parse_symbols(&self) -> Result<Vec<String>, String> {
383 let list: Vec<String> = self
384 .symbols
385 .split([',', ',', ' '])
386 .map(|s| s.trim().to_string())
387 .filter(|s| !s.is_empty())
388 .collect();
389 if list.is_empty() {
390 return Err("订阅解析: 标的位空".to_string());
391 }
392 Ok(list)
393 }
394
395 pub fn validate(&self) -> Result<(), String> {
404 fn front(name: &str, v: &str) -> Result<(), String> {
405 let v = v.trim();
406 let Some(rest) = v.strip_prefix("tcp://") else {
407 return Err(format!("{name} 形制应为 tcp://host:port(当前 {:?})", v));
408 };
409 let Some((host, port)) = rest.rsplit_once(':') else {
410 return Err(format!("{name} 缺端口:{name} {:?}", v));
411 };
412 if host.trim().is_empty() {
413 return Err(format!("{name} 主机位空:{}", v));
414 }
415 let port: u32 = port
416 .parse()
417 .map_err(|_| format!("{name} 端口不是数字:{}", v))?;
418 if port == 0 || port > 65535 {
419 return Err(format!("{name} 端口越界(1..=65535):{}", v));
420 }
421 Ok(())
422 }
423 if self.broker_id.trim().is_empty() {
424 return Err("broker 位空".to_string());
425 }
426 if self.td_user.trim().is_empty() {
427 return Err("交易账号位空".to_string());
428 }
429 front("td 前置", &self.td_addr)?;
430 if !self.md_addr.trim().is_empty() {
431 front("md 前置", &self.md_addr)?;
432 }
433 if self.td_app_id.trim().is_empty() {
434 return Err("AppID 位空(CTP 登录必需)".to_string());
435 }
436 if self.td_auth_code.trim().is_empty() {
437 return Err("AuthCode 位空(CTP 登录必需)".to_string());
438 }
439 Ok(())
440 }
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
450pub enum AccountStage {
451 #[default]
453 NotAssembled,
454 Assembling,
456 Assembled,
458 AssembleFailed,
460}
461
462impl AccountStage {
463 #[must_use]
465 pub fn label(self) -> &'static str {
466 match self {
467 AccountStage::NotAssembled => "未装配",
468 AccountStage::Assembling => "装配中…",
469 AccountStage::Assembled => "已装配·登录态未知",
472 AccountStage::AssembleFailed => "装配失败",
473 }
474 }
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
482pub enum LoginState {
483 #[default]
488 Unknown,
489 Online,
491 LoginFailed,
493 FrontDown,
495 QueryStarved,
497 Stopped,
499}
500
501impl LoginState {
502 #[must_use]
504 pub fn label(self) -> &'static str {
505 match self {
506 LoginState::Unknown => "登录态未知",
507 LoginState::Online => "在线",
508 LoginState::LoginFailed => "登录失败",
509 LoginState::FrontDown => "掉线",
510 LoginState::QueryStarved => "查询断供",
511 LoginState::Stopped => "已断开",
512 }
513 }
514
515 #[must_use]
517 pub fn is_alarm(self) -> bool {
518 matches!(self, LoginState::LoginFailed | LoginState::FrontDown)
519 }
520}
521
522fn count_note(label: &str, n: u32) -> Option<String> {
524 (n > 0).then(|| format!("{label} {n} 次"))
525}
526
527#[derive(Debug, Clone, PartialEq, Default)]
533pub struct AccountRow {
534 pub gateway: String,
536 pub alias: String,
538 pub broker_id: String,
539 pub account: String,
540 pub md_source: bool,
542 pub form: CtpForm,
544 pub stage: AccountStage,
546 pub fail_note: Option<String>,
548 pub effective_classes: Vec<String>,
553 pub connected: bool,
556 pub login: LoginState,
560 pub status_note: Option<String>,
563}
564
565impl AccountRow {
566 pub fn derive(form: &CtpForm) -> Result<Self, String> {
571 let broker = form.broker_id.trim();
572 let account = form.td_user.trim();
573 let gateway = ek_ctp::derived_gateway_name(broker, account).ok_or_else(|| {
574 "账户: broker/账号 不能为空且只能含字母数字(且派生名 ≤32)".to_string()
575 })?;
576 Ok(Self {
577 gateway,
578 alias: String::new(),
579 broker_id: broker.to_string(),
580 account: account.to_string(),
581 md_source: false,
582 form: form.clone(),
583 stage: AccountStage::NotAssembled,
584 fail_note: None,
585 effective_classes: Vec::new(),
586 connected: false,
587 login: LoginState::Unknown,
588 status_note: None,
589 })
590 }
591
592 #[must_use]
598 pub fn status_label(&self) -> String {
599 if self.stage == AccountStage::Assembled {
600 match self.login {
601 LoginState::Unknown => "已装配·登录态未知".to_string(),
602 other => format!("已装配·{}", other.label()),
603 }
604 } else {
605 self.stage.label().to_string()
606 }
607 }
608
609 pub fn apply_status(
622 &mut self,
623 st: &ek_ctp::plugin::GatewayStatus,
624 now_ms: i64,
625 reject_limit: u32,
626 stale_secs: i64,
627 ) {
628 let td_side = !self.form.td_addr.trim().is_empty();
629 let (login, note) = if td_side {
630 if st.td_login_failed && !st.td_logged_in {
631 (
632 LoginState::LoginFailed,
633 st.td_last_error
634 .clone()
635 .or_else(|| Some("登录失败(网关未回带原因)".to_string())),
636 )
637 } else if st.td_logged_in {
638 if st.td_query_reject_streak >= reject_limit {
639 (
640 LoginState::QueryStarved,
641 Some(format!("查询连续被拒 {} 次", st.td_query_reject_streak)),
642 )
643 } else if let Some(last) = st.td_last_account_ms {
644 let idle_ms = now_ms.saturating_sub(last);
645 if idle_ms > stale_secs.saturating_mul(1_000) {
646 (
647 LoginState::QueryStarved,
648 Some(format!("资金回报已 {}s 未更新", idle_ms / 1_000)),
649 )
650 } else {
651 (LoginState::Online, count_note("掉线", st.td_disconnects))
652 }
653 } else {
654 let since_login_ms = st.td_last_login_ok_ms.map(|ok| now_ms.saturating_sub(ok));
661 match since_login_ms {
662 Some(ms) if ms > stale_secs.saturating_mul(1_000) => (
663 LoginState::QueryStarved,
664 Some(format!("资金回报从未到手(登录已 {}s)", ms / 1_000)),
665 ),
666 _ => (LoginState::Online, Some("资金回报尚未到手".to_string())),
667 }
668 }
669 } else if self.login == LoginState::Stopped {
670 let extra = if st.td_disconnects > 0 {
671 format!("(此前掉线 {} 次)", st.td_disconnects)
672 } else {
673 String::new()
674 };
675 (
676 LoginState::Stopped,
677 Some(format!("会话已停{extra};『连接』可恢复")),
678 )
679 } else if st.td_disconnects > 0 {
680 (
681 LoginState::FrontDown,
682 Some(format!("掉线 {} 次", st.td_disconnects)),
683 )
684 } else {
685 (LoginState::Unknown, st.td_last_error.clone())
686 }
687 } else if st.md_logged_in {
688 (
689 LoginState::Online,
690 count_note("掉线", st.md_disconnects)
691 .or_else(|| Some("纯行情账户(无 TD 会话)".to_string())),
692 )
693 } else if self.login == LoginState::Stopped {
694 (
695 LoginState::Stopped,
696 Some("会话已停;『连接』可恢复".to_string()),
697 )
698 } else if st.md_disconnects > 0 {
699 (
700 LoginState::FrontDown,
701 Some(format!("掉线 {} 次", st.md_disconnects)),
702 )
703 } else {
704 (LoginState::Unknown, st.md_last_error.clone())
705 };
706 let note = if td_side && st.td_instrument_rejects > 0 {
711 let add = format!("合约查询被拒 {} 次", st.td_instrument_rejects);
712 match note {
713 Some(base) if base.contains("合约查询被拒") => Some(base),
715 Some(base) => Some(format!("{base};{add}")),
716 None => Some(add),
717 }
718 } else {
719 note
720 };
721 self.login = login;
722 self.status_note = note;
723 }
724
725 #[must_use]
727 pub fn display_name(&self) -> String {
728 let a = self.alias.trim();
729 if a.is_empty() {
730 self.account.clone()
731 } else {
732 a.to_string()
733 }
734 }
735}
736
737#[derive(Debug, Clone, PartialEq, Default)]
739pub struct TradeSnap {
740 pub orders: Vec<ek_data::order::OrderData>,
741 pub trades: Vec<ek_data::trade::TradeData>,
742 pub positions: Vec<ek_data::position::PositionData>,
743 pub accounts: Vec<ek_data::account::AccountData>,
745}
746
747pub fn parse_reload_csv(form: &ReloadForm) -> Result<(i64, String, Vec<BarData>), String> {
750 let interval: i64 = form
751 .interval
752 .trim()
753 .parse()
754 .map_err(|_| format!("回灌 解析: interval 非整数:{:?}", form.interval))?;
755 let symbol = form.symbol.trim().to_string();
756 if symbol.is_empty() {
757 return Err("回灌解析: symbol 空".to_string());
758 }
759 if form.csv.trim().is_empty() {
760 return Err("回灌解析: csv 空".to_string());
761 }
762 let mut bars = Vec::new();
763 for (i, raw) in form.csv.lines().enumerate() {
764 let line = raw.trim();
765 if line.is_empty() || line.starts_with('#') {
766 continue;
767 }
768 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
769 if parts.len() != 6 {
770 return Err(format!("回灌解析: 第 {} 列数 ≠ 6", i + 1));
771 }
772 let num = |s: &str| -> Result<f64, String> {
773 s.parse::<f64>()
774 .map_err(|_| format!("回灌解析: 第 {} 列数值坏:{:?}", i + 1, s))
775 };
776 let dt_s: i64 = parts[0]
777 .parse()
778 .map_err(|_| format!("回灌解析: 第 {} 列 dt 坏:{:?}", i + 1, parts[0]))?;
779 bars.push(BarData {
780 symbol: symbol.clone(),
781 datetime: dt_s * 1_000_000_000,
782 interval: Some(
783 ek_dm::wire::interval_of_seconds(interval)
784 .unwrap_or(ek_data::constant::Interval::MINUTE),
785 ),
786 open_price: num(parts[1])?,
787 high_price: num(parts[2])?,
788 low_price: num(parts[3])?,
789 close_price: num(parts[4])?,
790 volume: num(parts[5])?,
791 ..BarData::default()
792 });
793 }
794 if bars.is_empty() {
795 return Err("回灌解析: 无有效行".to_string());
796 }
797 Ok((interval, symbol, bars))
798}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
805pub enum TopLeftTab {
806 #[default]
807 Orders,
808 Active,
809}
810
811impl TopLeftTab {
812 pub fn label(self) -> &'static str {
813 match self {
814 Self::Orders => "委托",
815 Self::Active => "未成交",
816 }
817 }
818}
819
820#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
822pub enum BottomRightTab {
823 #[default]
824 Account,
825 Logs,
826}
827
828impl BottomRightTab {
829 pub fn label(self) -> &'static str {
830 match self {
831 Self::Account => "资金",
832 Self::Logs => "日志",
833 }
834 }
835}
836
837#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
841pub enum Theme {
842 #[default]
844 FinanceDark,
845 EguiDark,
847 EguiLight,
849 QtClassic,
851 QDarkStyle,
857}
858
859impl Theme {
860 pub fn label(self) -> &'static str {
861 match self {
862 Self::FinanceDark => "金融深色",
863 Self::EguiDark => "egui深色",
864 Self::EguiLight => "egui浅色",
865 Self::QtClassic => "Qt灰白",
866 Self::QDarkStyle => "Qt深色",
867 }
868 }
869
870 pub fn next(self) -> Self {
872 match self {
873 Self::FinanceDark => Self::EguiDark,
874 Self::EguiDark => Self::EguiLight,
875 Self::EguiLight => Self::QtClassic,
876 Self::QtClassic => Self::QDarkStyle,
877 Self::QDarkStyle => Self::FinanceDark,
878 }
879 }
880}
881
882#[derive(Debug, Default)]
885pub struct TickStore {
886 ticks: BTreeMap<String, ek_data::tick::TickData>,
887}
888
889impl TickStore {
890 pub fn put(&mut self, t: &ek_data::tick::TickData) {
893 self.ticks
894 .insert(format!("{}.{}", t.exchange.as_str(), t.symbol), t.clone());
895 }
896
897 pub fn rows(&self) -> Vec<ek_data::tick::TickData> {
899 let mut v: Vec<ek_data::tick::TickData> = self.ticks.values().cloned().collect();
900 v.sort_by(|a, b| {
901 (a.exchange.as_str().to_string(), a.symbol.clone())
902 .cmp(&(b.exchange.as_str().to_string(), b.symbol.clone()))
903 });
904 v
905 }
906
907 pub fn groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
909 let mut out: Vec<(String, Vec<ek_data::tick::TickData>)> = Vec::new();
910 for t in self.rows() {
911 let ex = t.exchange.as_str().to_string();
912 match out.last_mut() {
913 Some((e, v)) if *e == ex => v.push(t),
914 _ => out.push((ex, vec![t])),
915 }
916 }
917 out
918 }
919
920 pub fn len(&self) -> usize {
921 self.ticks.len()
922 }
923
924 pub fn is_empty(&self) -> bool {
925 self.ticks.is_empty()
926 }
927}
928
929#[derive(Debug, Clone, PartialEq)]
931pub struct OrderForm {
932 pub exchange: String,
934 pub symbol: String,
936 pub direction: ek_data::constant::Direction,
937 pub offset: ek_data::constant::Offset,
938 pub order_type: ek_data::constant::OrderType,
939 pub price: String,
941 pub volume: String,
942 pub gateway: String,
944 pub follow_price: bool,
946 pub auto_split: bool,
949}
950
951impl Default for OrderForm {
952 fn default() -> Self {
953 Self {
954 exchange: "SHFE".to_string(),
955 symbol: String::new(),
956 direction: ek_data::constant::Direction::LONG,
957 offset: ek_data::constant::Offset::OPEN,
958 order_type: ek_data::constant::OrderType::LIMIT,
959 price: String::new(),
960 volume: "1".to_string(),
961 gateway: String::new(),
962 follow_price: true,
963 auto_split: true,
964 }
965 }
966}
967
968impl OrderForm {
969 pub fn to_request(&self) -> Result<ek_data::order_request::OrderRequest, String> {
971 use std::str::FromStr;
972 let symbol = self.symbol.trim();
973 if symbol.is_empty() {
974 return Err("下单: 代码为空".to_string());
975 }
976 if symbol.contains('.') {
977 return Err("下单: 代码不带交易所前缀(交易所走下拉)".to_string());
978 }
979 let exchange = ek_data::constant::Exchange::from_str(self.exchange.trim())
980 .map_err(|e| format!("下单: {e}"))?;
981 let price: f64 = self
982 .price
983 .trim()
984 .parse()
985 .map_err(|_| format!("下单: 价格无效 {:?}", self.price))?;
986 let volume: f64 = self
987 .volume
988 .trim()
989 .parse()
990 .map_err(|_| format!("下单: 数量无效 {:?}", self.volume))?;
991 if self.order_type != ek_data::constant::OrderType::MARKET && price <= 0.0 {
992 return Err("下单: 限价单价格须为正".to_string());
993 }
994 if volume <= 0.0 {
995 return Err("下单: 数量须为正".to_string());
996 }
997 Ok(ek_data::order_request::OrderRequest {
998 symbol: symbol.to_string(),
999 exchange,
1000 direction: self.direction,
1001 order_type: self.order_type,
1002 volume,
1003 price,
1004 offset: self.offset,
1005 ..ek_data::order_request::OrderRequest::default()
1006 })
1007 }
1008}
1009
1010#[derive(Debug, Default)]
1016pub struct ContractStore {
1017 contracts: BTreeMap<String, ek_data::contract::ContractData>,
1018}
1019
1020impl ContractStore {
1021 pub fn put(&mut self, c: &ek_data::contract::ContractData) {
1022 self.contracts
1023 .insert(format!("{}.{}", c.exchange.as_str(), c.symbol), c.clone());
1024 }
1025
1026 pub fn query(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
1028 let p = pattern.trim();
1029 self.contracts
1030 .values()
1031 .filter(|c| {
1032 p.is_empty()
1033 || c.symbol.contains(p)
1034 || c.exchange.as_str().eq_ignore_ascii_case(p)
1035 || c.name.contains(p)
1036 })
1037 .cloned()
1038 .collect()
1039 }
1040
1041 pub fn len(&self) -> usize {
1042 self.contracts.len()
1043 }
1044
1045 pub fn is_empty(&self) -> bool {
1046 self.contracts.is_empty()
1047 }
1048}
1049
1050#[derive(Debug, Default)]
1052pub struct UiShared {
1053 logs: Mutex<LogRing>,
1054 snap: Mutex<Arc<BusSnap>>,
1055 market: MarketStore,
1056 ctp_form: Mutex<CtpForm>,
1057 trade_snap: Mutex<Arc<TradeSnap>>,
1058 tick_store: Mutex<TickStore>,
1059 order_form: Mutex<OrderForm>,
1060 selected_exchange: Mutex<String>,
1061 topleft_tab: Mutex<TopLeftTab>,
1062 bottomright_tab: Mutex<BottomRightTab>,
1063 ops_window: AtomicBool,
1064 accounts_window: AtomicBool,
1067 contracts_window: AtomicBool,
1068 theme: AtomicU8,
1070 contract_store: Mutex<ContractStore>,
1071 contract_query: Mutex<String>,
1072 contract_selected: Mutex<Vec<String>>,
1073 kline_windows: Mutex<Vec<String>>,
1074 rev: AtomicU64,
1075 stop: AtomicBool,
1077 cmds: Mutex<VecDeque<UiCmd>>,
1079 pending_confirm: Mutex<Option<String>>,
1081 accounts: Mutex<Vec<AccountRow>>,
1083 account_edit_window: AtomicBool,
1085 editing_account: Mutex<Option<String>>,
1087 edit_alias: Mutex<String>,
1089 edit_error: Mutex<Option<String>>,
1091 md_source: Mutex<String>,
1093 subscriptions: Mutex<Vec<String>>,
1095 row_sel: Mutex<BTreeMap<String, String>>,
1098 status_tick: AtomicU32,
1100 status_thresholds: Mutex<Option<(u32, i64)>>,
1103}
1104
1105impl UiShared {
1106 pub fn new() -> Self {
1108 Self::default()
1109 }
1110
1111 pub fn update_tick(&self, t: &ek_data::tick::TickData) {
1115 self.tick_store.lock().unwrap().put(t);
1116 self.bump();
1117 }
1118
1119 pub fn tick_rows(&self) -> Vec<ek_data::tick::TickData> {
1121 self.tick_store.lock().unwrap().rows()
1122 }
1123
1124 pub fn tick_groups(&self) -> Vec<(String, Vec<ek_data::tick::TickData>)> {
1126 self.tick_store.lock().unwrap().groups()
1127 }
1128
1129 pub fn tick_count(&self) -> usize {
1131 self.tick_store.lock().unwrap().len()
1132 }
1133
1134 pub fn order_form(&self) -> OrderForm {
1136 self.order_form.lock().unwrap().clone()
1137 }
1138
1139 pub fn set_order_form(&self, f: OrderForm) {
1141 *self.order_form.lock().unwrap() = f;
1142 self.bump();
1143 }
1144
1145 pub fn selected_exchange(&self) -> String {
1147 self.selected_exchange.lock().unwrap().clone()
1148 }
1149
1150 pub fn set_selected_exchange(&self, s: impl Into<String>) {
1152 *self.selected_exchange.lock().unwrap() = s.into();
1153 self.bump();
1154 }
1155
1156 pub fn topleft_tab(&self) -> TopLeftTab {
1158 *self.topleft_tab.lock().unwrap()
1159 }
1160
1161 pub fn set_topleft_tab(&self, t: TopLeftTab) {
1163 *self.topleft_tab.lock().unwrap() = t;
1164 self.bump();
1165 }
1166
1167 pub fn bottomright_tab(&self) -> BottomRightTab {
1169 *self.bottomright_tab.lock().unwrap()
1170 }
1171
1172 pub fn set_bottomright_tab(&self, t: BottomRightTab) {
1174 *self.bottomright_tab.lock().unwrap() = t;
1175 self.bump();
1176 }
1177
1178 pub fn theme(&self) -> Theme {
1185 match self.theme.load(Ordering::Acquire) {
1186 1 => Theme::EguiDark,
1187 2 => Theme::EguiLight,
1188 3 => Theme::QtClassic,
1189 4 => Theme::QDarkStyle,
1190 _ => Theme::FinanceDark,
1191 }
1192 }
1193
1194 pub fn set_theme(&self, t: Theme) {
1196 self.theme.store(t as u8, Ordering::Release);
1197 self.bump();
1198 }
1199
1200 pub fn cycle_theme(&self) -> Theme {
1202 let t = self.theme().next();
1203 self.set_theme(t);
1204 t
1205 }
1206
1207 pub fn ops_window_open(&self) -> bool {
1209 self.ops_window.load(Ordering::Acquire)
1210 }
1211
1212 pub fn set_ops_window_open(&self, v: bool) {
1214 self.ops_window.store(v, Ordering::Release);
1215 self.bump();
1216 }
1217
1218 pub fn accounts_window_open(&self) -> bool {
1222 self.accounts_window.load(Ordering::Acquire)
1223 }
1224
1225 pub fn set_accounts_window_open(&self, v: bool) {
1227 self.accounts_window.store(v, Ordering::Release);
1228 self.bump();
1229 }
1230
1231 pub fn account_edit_open(&self) -> bool {
1233 self.account_edit_window.load(Ordering::Acquire)
1234 }
1235
1236 pub fn editing_account(&self) -> Option<String> {
1238 self.editing_account.lock().unwrap().clone()
1239 }
1240
1241 pub fn begin_account_edit(&self, gateway: Option<&str>) -> bool {
1245 match gateway {
1246 Some(gw) => {
1247 let Some(row) = self.account(gw) else {
1248 return false;
1249 };
1250 *self.editing_account.lock().unwrap() = Some(gw.to_string());
1251 *self.edit_alias.lock().unwrap() = row.alias.clone();
1252 self.set_ctp_form(row.form.clone());
1253 }
1254 None => {
1255 *self.editing_account.lock().unwrap() = None;
1256 *self.edit_alias.lock().unwrap() = String::new();
1257 let draft = self.draft_from_existing();
1258 self.set_ctp_form(draft);
1259 }
1260 }
1261 *self.edit_error.lock().unwrap() = None;
1262 self.account_edit_window.store(true, Ordering::Release);
1263 self.bump();
1264 true
1265 }
1266
1267 pub fn end_account_edit(&self) {
1269 *self.editing_account.lock().unwrap() = None;
1270 *self.edit_alias.lock().unwrap() = String::new();
1271 *self.edit_error.lock().unwrap() = None;
1272 self.account_edit_window.store(false, Ordering::Release);
1273 let draft = self.draft_from_existing();
1274 self.set_ctp_form(draft);
1275 self.bump();
1276 }
1277
1278 pub fn end_account_edit_if_editing(&self, gateway: &str) {
1284 if self.editing_account().as_deref() == Some(gateway) {
1285 self.end_account_edit();
1286 }
1287 }
1288
1289 pub fn edit_alias(&self) -> String {
1291 self.edit_alias.lock().unwrap().clone()
1292 }
1293
1294 pub fn set_edit_alias(&self, v: impl Into<String>) {
1296 *self.edit_alias.lock().unwrap() = v.into();
1297 self.bump();
1298 }
1299
1300 pub fn edit_error(&self) -> Option<String> {
1302 self.edit_error.lock().unwrap().clone()
1303 }
1304
1305 pub fn set_edit_error(&self, e: Option<String>) {
1307 *self.edit_error.lock().unwrap() = e;
1308 self.bump();
1309 }
1310
1311 pub fn account_edit_dirty(&self) -> bool {
1313 let editing = self.editing_account();
1314 let draft = self.ctp_form();
1315 match editing {
1316 Some(gw) => self.account(&gw).map(|a| a.form != draft).unwrap_or(false),
1317 None => draft != CtpForm::default() && !draft.td_user.trim().is_empty(),
1318 }
1319 }
1320
1321 pub fn accounts(&self) -> Vec<AccountRow> {
1323 self.accounts.lock().unwrap().clone()
1324 }
1325
1326 pub fn account(&self, gateway: &str) -> Option<AccountRow> {
1328 self.accounts
1329 .lock()
1330 .unwrap()
1331 .iter()
1332 .find(|a| a.gateway == gateway)
1333 .cloned()
1334 }
1335
1336 pub fn add_account(&self, row: AccountRow) -> Result<(), String> {
1342 let mut acc = self.accounts.lock().unwrap();
1343 if acc.iter().any(|a| a.gateway == row.gateway) {
1344 return Err(format!("账户 {} 已存在(同 broker + 账号)", row.gateway));
1345 }
1346 let alias = row.alias.trim();
1347 if !alias.is_empty()
1348 && acc
1349 .iter()
1350 .any(|a| a.alias.trim().eq_ignore_ascii_case(alias))
1351 {
1352 return Err(format!("别名 {alias:?} 已被其他账户占用(别名须唯一)"));
1353 }
1354 if acc.len() >= MAX_ACCOUNTS {
1355 return Err(format!(
1356 "账户已达软上限 {MAX_ACCOUNTS} 个(先移除一个再添加)"
1357 ));
1358 }
1359 acc.push(row);
1360 drop(acc);
1361 self.bump();
1362 Ok(())
1363 }
1364
1365 #[must_use]
1367 pub fn can_add_account(&self) -> bool {
1368 self.accounts.lock().unwrap().len() < MAX_ACCOUNTS
1369 }
1370
1371 pub fn set_account_alias(&self, gateway: &str, alias: &str) -> bool {
1377 let alias = alias.trim().to_string();
1378 let mut acc = self.accounts.lock().unwrap();
1379 if !alias.is_empty()
1380 && acc
1381 .iter()
1382 .any(|a| a.gateway != gateway && a.alias.eq_ignore_ascii_case(&alias))
1383 {
1384 return false;
1385 }
1386 match acc.iter_mut().find(|a| a.gateway == gateway) {
1387 Some(a) => {
1388 a.alias = alias;
1389 drop(acc);
1390 self.bump();
1391 true
1392 }
1393 None => false,
1394 }
1395 }
1396
1397 pub fn note_connect_sent(&self, gateway: &str) {
1402 let mut acc = self.accounts.lock().unwrap();
1403 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1404 a.stage = AccountStage::Assembling;
1405 a.fail_note = None;
1406 a.login = LoginState::Unknown;
1407 a.status_note = None;
1408 drop(acc);
1409 self.bump();
1410 }
1411 }
1412
1413 pub fn note_connect_result(&self, gateway: &str, result: Result<(), String>) {
1417 let mut acc = self.accounts.lock().unwrap();
1418 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1419 match result {
1420 Ok(()) => {
1421 a.stage = AccountStage::Assembled;
1422 a.fail_note = None;
1423 }
1424 Err(e) => {
1425 a.stage = AccountStage::AssembleFailed;
1426 a.fail_note = Some(e);
1427 }
1428 }
1429 drop(acc);
1430 self.bump();
1431 }
1432 }
1433
1434 pub fn note_effective_product_classes(&self, gateway: &str, classes: &[String]) {
1438 let mut acc = self.accounts.lock().unwrap();
1439 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1440 a.effective_classes = classes.to_vec();
1441 drop(acc);
1442 self.bump();
1443 }
1444 }
1445
1446 #[must_use]
1450 pub fn status_thresholds(&self) -> (u32, i64) {
1451 self.status_thresholds
1452 .lock()
1453 .unwrap()
1454 .unwrap_or((QUERY_REJECT_STREAK_LIMIT, ACCOUNT_STALE_SECS))
1455 }
1456
1457 pub fn set_status_thresholds(&self, reject_limit: u32, stale_secs: i64) {
1459 *self.status_thresholds.lock().unwrap() = Some((reject_limit, stale_secs));
1460 self.bump();
1461 }
1462
1463 pub fn status_poll_due(&self) -> bool {
1465 let n = self.status_tick.fetch_add(1, Ordering::Relaxed) + 1;
1466 n % STATUS_POLL_TICKS == 0
1467 }
1468
1469 pub fn note_gateway_status(&self, gateway: &str, st: &ek_ctp::plugin::GatewayStatus) {
1471 let (limit, stale) = self.status_thresholds();
1472 let now = ek_ctp::map::now_epoch_ms();
1473 let mut acc = self.accounts.lock().unwrap();
1474 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1475 a.apply_status(st, now, limit, stale);
1476 drop(acc);
1477 self.bump();
1478 }
1479 }
1480
1481 pub fn note_status_unreachable(&self, gateway: &str, why: &str) {
1485 let mut acc = self.accounts.lock().unwrap();
1486 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1487 a.login = LoginState::Unknown;
1488 a.status_note = Some(format!("状态口问不到:{why}"));
1489 drop(acc);
1490 self.bump();
1491 }
1492 }
1493
1494 pub fn mark_account_stopped(&self, gateway: &str) {
1496 let mut acc = self.accounts.lock().unwrap();
1497 if let Some(a) = acc.iter_mut().find(|a| a.gateway == gateway) {
1498 a.login = LoginState::Stopped;
1499 a.status_note = Some("会话已停;『连接』可恢复".to_string());
1500 drop(acc);
1501 self.bump();
1502 }
1503 }
1504
1505 pub fn prepare_connect_form(&self, gateway: &str, form: CtpForm) -> CtpForm {
1508 let source = self.md_source();
1509 let mut f = form;
1510 if source.as_str() != gateway {
1511 f.md_addr.clear();
1512 f.md_user.clear();
1513 f.md_password.clear();
1514 }
1515 self.update_account_form(gateway, f.clone());
1516 f
1517 }
1518
1519 #[must_use]
1523 pub fn draft_from_existing(&self) -> CtpForm {
1524 let acc = self.accounts.lock().unwrap();
1525 let mut draft = CtpForm::default();
1526 if let Some(a) = acc.last() {
1527 draft.broker_id = a.form.broker_id.clone();
1528 draft.td_auth_code = a.form.td_auth_code.clone();
1529 draft.td_app_id = a.form.td_app_id.clone();
1530 draft.accept_futures = a.form.accept_futures;
1531 draft.accept_options = a.form.accept_options;
1532 }
1533 draft
1534 }
1535
1536 pub fn update_account_form(&self, gateway: &str, form: CtpForm) -> bool {
1538 let mut acc = self.accounts.lock().unwrap();
1539 match acc.iter_mut().find(|a| a.gateway == gateway) {
1540 Some(a) => {
1541 a.form = form;
1542 drop(acc);
1543 self.bump();
1544 true
1545 }
1546 None => false,
1547 }
1548 }
1549
1550 pub fn remove_account(&self, gateway: &str) -> bool {
1552 let mut acc = self.accounts.lock().unwrap();
1553 let before = acc.len();
1554 acc.retain(|a| a.gateway != gateway);
1555 let removed = acc.len() != before;
1556 if removed {
1557 let mut md = self.md_source.lock().unwrap();
1559 if md.as_str() == gateway {
1560 md.clear();
1561 }
1562 drop(acc);
1563 self.bump();
1564 }
1565 removed
1566 }
1567
1568 pub fn sync_accounts_connected(&self, rows: &[PlugRow]) -> Vec<String> {
1577 let mut newly = Vec::new();
1578 {
1579 let mut acc = self.accounts.lock().unwrap();
1580 for a in acc.iter_mut() {
1581 let up = rows.iter().any(|r| r.name == a.gateway);
1582 if up && !a.connected {
1583 newly.push(a.gateway.clone());
1584 }
1585 a.connected = up;
1586 if up {
1587 a.stage = AccountStage::Assembled;
1588 a.fail_note = None;
1589 } else if a.stage == AccountStage::Assembled {
1590 a.stage = AccountStage::NotAssembled;
1591 a.login = LoginState::Unknown;
1592 a.status_note = None;
1593 }
1594 }
1595 }
1596 if !newly.is_empty() {
1597 self.bump();
1598 }
1599 newly
1600 }
1601
1602 pub fn md_source(&self) -> String {
1604 self.md_source.lock().unwrap().clone()
1605 }
1606
1607 pub fn accounts_locked(&self) -> bool {
1609 self.accounts.lock().unwrap().iter().any(|a| a.connected)
1610 }
1611
1612 pub fn set_md_source(&self, gateway: &str) -> Result<(), String> {
1614 {
1615 let acc = self.accounts.lock().unwrap();
1616 if acc.iter().any(|a| a.connected) {
1617 return Err("元账号已锁定:有账户在线,切换行情源需先断开该账户".to_string());
1618 }
1619 if !acc.iter().any(|a| a.gateway == gateway) {
1620 return Err(format!("账户 {gateway} 不在清册"));
1621 }
1622 }
1623 let mut acc = self.accounts.lock().unwrap();
1624 for a in acc.iter_mut() {
1625 a.md_source = a.gateway == gateway;
1626 }
1627 drop(acc);
1628 let mut md = self.md_source.lock().unwrap();
1629 *md = gateway.to_string();
1630 self.bump();
1631 Ok(())
1632 }
1633
1634 pub fn subscriptions(&self) -> Vec<String> {
1636 self.subscriptions.lock().unwrap().clone()
1637 }
1638
1639 pub fn note_subscriptions(&self, symbols: &[String]) {
1641 {
1642 let mut subs = self.subscriptions.lock().unwrap();
1643 for s in symbols {
1644 let s = s.trim();
1645 if !s.is_empty() && !subs.iter().any(|x| x == s) {
1646 subs.push(s.to_string());
1647 }
1648 }
1649 }
1650 self.bump();
1651 }
1652
1653 pub fn default_gateway(&self) -> Option<String> {
1660 let chosen = {
1661 let f = self.order_form.lock().unwrap();
1662 f.gateway.trim().to_string()
1663 };
1664 if !chosen.is_empty() && self.account(&chosen).is_some_and(|a| a.connected) {
1665 return Some(chosen);
1666 }
1667 let acc = self.accounts.lock().unwrap();
1668 acc.iter().find(|a| a.connected).map(|a| a.gateway.clone())
1669 }
1670
1671 pub fn kline_symbols(&self) -> Vec<String> {
1673 self.kline_windows.lock().unwrap().clone()
1674 }
1675
1676 pub fn open_kline(&self, symbol: impl Into<String>) {
1678 let s = symbol.into();
1679 let mut v = self.kline_windows.lock().unwrap();
1680 if !v.contains(&s) {
1681 v.push(s.clone());
1682 }
1683 drop(v);
1684 self.log_line(format!("[kline] 打开 K线窗 {s}"));
1685 }
1686
1687 pub fn close_kline(&self, symbol: &str) {
1689 self.kline_windows.lock().unwrap().retain(|s| s != symbol);
1690 self.bump();
1691 }
1692
1693 pub fn contracts_window_open(&self) -> bool {
1697 self.contracts_window.load(Ordering::Acquire)
1698 }
1699
1700 pub fn set_contracts_window_open(&self, v: bool) {
1702 self.contracts_window.store(v, Ordering::Release);
1703 self.bump();
1704 }
1705
1706 pub fn update_contract(&self, c: &ek_data::contract::ContractData) {
1708 self.contract_store.lock().unwrap().put(c);
1709 self.bump();
1710 }
1711
1712 pub fn contracts(&self, pattern: &str) -> Vec<ek_data::contract::ContractData> {
1714 self.contract_store.lock().unwrap().query(pattern)
1715 }
1716
1717 pub fn contract_count(&self) -> usize {
1719 self.contract_store.lock().unwrap().len()
1720 }
1721
1722 pub fn contract_query(&self) -> String {
1724 self.contract_query.lock().unwrap().clone()
1725 }
1726
1727 pub fn set_contract_query(&self, q: impl Into<String>) {
1729 *self.contract_query.lock().unwrap() = q.into();
1730 self.bump();
1731 }
1732
1733 pub fn toggle_contract_selection(&self, en_symbol: &str) {
1735 let mut v = self.contract_selected.lock().unwrap();
1736 match v.iter().position(|s| s == en_symbol) {
1737 Some(i) => {
1738 v.remove(i);
1739 }
1740 None => v.push(en_symbol.to_string()),
1741 }
1742 self.bump();
1743 }
1744
1745 pub fn selected_contracts(&self) -> Vec<String> {
1747 self.contract_selected.lock().unwrap().clone()
1748 }
1749
1750 pub fn log_line(&self, line: impl Into<String>) {
1752 self.logs.lock().unwrap().push(line);
1753 self.bump();
1754 }
1755
1756 pub fn clear_logs(&self) {
1758 self.logs.lock().unwrap().clear();
1759 self.bump();
1760 }
1761
1762 pub fn logs_snapshot(&self) -> (Vec<String>, u64) {
1764 let r = self.logs.lock().unwrap();
1765 (r.snapshot(), r.spilled())
1766 }
1767
1768 pub fn logs_len(&self) -> usize {
1770 self.logs.lock().unwrap().len()
1771 }
1772
1773 pub fn set_snapshot(&self, snap: BusSnap) {
1775 *self.snap.lock().unwrap() = Arc::new(snap);
1776 self.bump();
1777 }
1778
1779 pub fn snapshot(&self) -> Arc<BusSnap> {
1781 Arc::clone(&self.snap.lock().unwrap())
1782 }
1783
1784 pub fn rev(&self) -> u64 {
1786 self.rev.load(Ordering::Acquire)
1787 }
1788
1789 pub fn bump(&self) {
1791 self.rev.fetch_add(1, Ordering::AcqRel);
1792 }
1793
1794 pub fn request_stop(&self) {
1796 self.stop.store(true, Ordering::Release);
1797 }
1798
1799 pub fn is_stop(&self) -> bool {
1801 self.stop.load(Ordering::Acquire)
1802 }
1803
1804 pub fn push_cmd(&self, cmd: UiCmd) {
1808 self.cmds.lock().unwrap().push_back(cmd);
1809 self.bump();
1810 }
1811
1812 pub fn drain_cmds(&self) -> Vec<UiCmd> {
1814 self.cmds.lock().unwrap().drain(..).collect()
1815 }
1816
1817 pub fn pending_confirm(&self) -> Option<String> {
1819 self.pending_confirm.lock().unwrap().clone()
1820 }
1821
1822 pub fn confirm_step(&self, name: &str) -> ConfirmStep {
1824 let mut g = self.pending_confirm.lock().unwrap();
1825 match g.take() {
1826 Some(p) if p == name => ConfirmStep::Fired,
1827 Some(_) => {
1828 *g = Some(name.to_string());
1829 ConfirmStep::Switched
1830 }
1831 None => {
1832 *g = Some(name.to_string());
1833 ConfirmStep::Armed
1834 }
1835 }
1836 }
1837
1838 pub fn confirm_cancel(&self) {
1840 *self.pending_confirm.lock().unwrap() = None;
1841 self.bump();
1842 }
1843
1844 pub fn market(&self) -> &MarketStore {
1848 &self.market
1849 }
1850
1851 pub fn set_trade_snap(&self, ts: TradeSnap) {
1855 *self.trade_snap.lock().unwrap() = Arc::new(ts);
1856 self.bump();
1857 }
1858
1859 pub fn trade_snap(&self) -> Arc<TradeSnap> {
1861 self.trade_snap.lock().unwrap().clone()
1862 }
1863
1864 pub fn ctp_form(&self) -> CtpForm {
1868 self.ctp_form.lock().unwrap().clone()
1869 }
1870
1871 pub fn set_ctp_form(&self, f: CtpForm) {
1873 *self.ctp_form.lock().unwrap() = f;
1874 self.bump();
1875 }
1876
1877 pub fn row_selected(&self, table: &str, key: &str) -> bool {
1881 self.row_sel
1882 .lock()
1883 .unwrap()
1884 .get(table)
1885 .is_some_and(|k| k == key)
1886 }
1887
1888 pub fn select_row(&self, table: &str, key: &str) {
1890 self.row_sel
1891 .lock()
1892 .unwrap()
1893 .insert(table.to_string(), key.to_string());
1894 }
1895}