Skip to main content

schwab_cli/commands/
ui_cmd.rs

1use std::path::PathBuf;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use anyhow::{Context, Result};
6
7use crate::agent::{daemon_status, run_agent_loop};
8use crate::cli::RulesCommands;
9use crate::config::RuntimeConfig;
10use crate::rules::RulesConfig;
11use crate::safety::require_trading_approval;
12use crate::ui::agent_health::{new_shared_health, SharedAgentHealth};
13use crate::ui::discover::{list_rules_files_display, resolve_rules_file};
14use crate::ui::market_status::{self, MarketSnapshot};
15use crate::ui::menu::{list_rules_files, show_dashboard, show_rules};
16use crate::ui::spread_feed::{new_spread_snapshot, spawn_spread_mark_feed};
17use crate::ui::watch::{run_watch_tui, WatchAgentMode, WatchConfig};
18use crate::market_conditions::{spawn_market_conditions_feed, MarketConditionsSnapshot};
19
20pub async fn run_dashboard(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
21    show_dashboard(runtime, file).await
22}
23
24pub async fn run_watch(
25    runtime: &RuntimeConfig,
26    file: Option<PathBuf>,
27    monitor_only: bool,
28) -> Result<()> {
29    if runtime.output == crate::output::OutputFormat::Json {
30        anyhow::bail!("watch is interactive only — omit --json");
31    }
32
33    let rules_path = resolve_rules_file(file, runtime.is_interactive())?;
34    let daemon = daemon_status(&rules_path);
35
36    let agent_mode = if daemon.running {
37        WatchAgentMode::External
38    } else if monitor_only {
39        WatchAgentMode::MonitorOnly
40    } else {
41        WatchAgentMode::Embedded
42    };
43
44    let will_spawn_agent = matches!(agent_mode, WatchAgentMode::Embedded);
45
46    if will_spawn_agent && !runtime.dry_run && !runtime.simulate {
47        let agent_id = RulesConfig::load(&rules_path)
48            .map(|r| r.agent_id)
49            .unwrap_or_else(|_| "agent".into());
50        require_trading_approval(
51            runtime,
52            "watch",
53            &format!("Run and watch options agent `{agent_id}`"),
54        )?;
55    }
56
57    let agent_runtime = RuntimeConfig::for_agent_trading(
58        runtime.output,
59        runtime.yes,
60        runtime.dry_run,
61        runtime.simulate,
62        runtime.trust,
63        true,
64        runtime.safety.clone(),
65        runtime.sink.clone(),
66    );
67
68    let market_snapshot = Arc::new(Mutex::new(MarketSnapshot::default()));
69    market_status::refresh_market_snapshot(runtime, &market_snapshot).await;
70    let snapshot_for_refresh = market_snapshot.clone();
71    let refresh_runtime = runtime.clone();
72    let _market_refresh = tokio::spawn(async move {
73        let mut interval = tokio::time::interval(Duration::from_secs(120));
74        interval.tick().await;
75        loop {
76            interval.tick().await;
77            market_status::refresh_market_snapshot(&refresh_runtime, &snapshot_for_refresh).await;
78        }
79    });
80
81    let market_conditions = Arc::new(Mutex::new(MarketConditionsSnapshot::default()));
82    if let Ok(market_api) = runtime.build_market_api() {
83        if let Ok(mut guard) = market_conditions.lock() {
84            crate::market_conditions::refresh_market_conditions(&market_api, &mut guard).await;
85        }
86        let _conditions_feed =
87            spawn_market_conditions_feed(market_api, market_conditions.clone());
88    }
89
90    let agent_health: Option<SharedAgentHealth> = if will_spawn_agent {
91        Some(new_shared_health())
92    } else {
93        None
94    };
95
96    let agent_handle = if will_spawn_agent {
97        let path = rules_path.clone();
98        let health = agent_health.clone().expect("health when spawning");
99        Some(tokio::spawn(async move {
100            let result = run_agent_loop(&agent_runtime, &path, false, Some(health.clone())).await;
101            if let Err(e) = result {
102                let msg = format!("agent exited: {e:#}");
103                let _ = crate::agent::paths::append_agent_log(&path, &msg);
104                if let Ok(mut g) = health.lock() {
105                    g.loop_running = false;
106                    g.record_error(&msg);
107                }
108            }
109        }))
110    } else {
111        None
112    };
113
114    // Brief pause so the first tick can start before the TUI loads.
115    if will_spawn_agent {
116        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
117    }
118
119    let spread_snapshot = new_spread_snapshot();
120    if let (Ok(market), Ok(trader)) = (
121        runtime.build_market_api(),
122        runtime.build_api(),
123    ) {
124        let _spread_feed = spawn_spread_mark_feed(
125            rules_path.clone(),
126            runtime.simulate,
127            spread_snapshot.clone(),
128            market,
129            trader,
130        );
131    }
132
133    let watch_config = WatchConfig {
134        rules_path: rules_path.clone(),
135        agent_mode,
136        market_snapshot,
137        market_conditions,
138        agent_health,
139        spread_snapshot,
140    };
141
142    let watch_result = tokio::task::spawn_blocking(move || run_watch_tui(&watch_config))
143        .await
144        .context("watch UI thread panicked")?;
145
146    if let Some(handle) = agent_handle {
147        handle.abort();
148        let _ = handle.await;
149    }
150
151    watch_result
152}
153
154pub async fn run_rules(runtime: &RuntimeConfig, command: RulesCommands) -> Result<()> {
155    match command {
156        RulesCommands::Show { file } => show_rules(runtime, file).await,
157        RulesCommands::List => {
158            if runtime.output == crate::output::OutputFormat::Json {
159                list_rules_files(runtime);
160            } else {
161                println!("\n{}", list_rules_files_display());
162                println!();
163            }
164            Ok(())
165        }
166    }
167}