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::watch::{run_watch_tui, WatchAgentMode, WatchConfig};
17
18pub async fn run_dashboard(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
19    show_dashboard(runtime, file).await
20}
21
22pub async fn run_watch(
23    runtime: &RuntimeConfig,
24    file: Option<PathBuf>,
25    monitor_only: bool,
26) -> Result<()> {
27    if runtime.output == crate::output::OutputFormat::Json {
28        anyhow::bail!("watch is interactive only — omit --json");
29    }
30
31    let rules_path = resolve_rules_file(file, runtime.is_interactive())?;
32    let daemon = daemon_status(&rules_path);
33
34    let agent_mode = if daemon.running {
35        WatchAgentMode::External
36    } else if monitor_only {
37        WatchAgentMode::MonitorOnly
38    } else {
39        WatchAgentMode::Embedded
40    };
41
42    let will_spawn_agent = matches!(agent_mode, WatchAgentMode::Embedded);
43
44    if will_spawn_agent && !runtime.dry_run {
45        let agent_id = RulesConfig::load(&rules_path)
46            .map(|r| r.agent_id)
47            .unwrap_or_else(|_| "agent".into());
48        require_trading_approval(
49            runtime,
50            "watch",
51            &format!("Run and watch options agent `{agent_id}`"),
52        )?;
53    }
54
55    let mut agent_runtime = runtime.clone();
56    agent_runtime.suppress_tick_output = true;
57
58    let market_snapshot = Arc::new(Mutex::new(MarketSnapshot::default()));
59    market_status::refresh_market_snapshot(runtime, &market_snapshot).await;
60    let snapshot_for_refresh = market_snapshot.clone();
61    let refresh_runtime = runtime.clone();
62    let _market_refresh = tokio::spawn(async move {
63        let mut interval = tokio::time::interval(Duration::from_secs(120));
64        interval.tick().await;
65        loop {
66            interval.tick().await;
67            market_status::refresh_market_snapshot(&refresh_runtime, &snapshot_for_refresh).await;
68        }
69    });
70
71    let agent_health: Option<SharedAgentHealth> = if will_spawn_agent {
72        Some(new_shared_health())
73    } else {
74        None
75    };
76
77    let agent_handle = if will_spawn_agent {
78        let path = rules_path.clone();
79        let health = agent_health.clone().expect("health when spawning");
80        Some(tokio::spawn(async move {
81            let result = run_agent_loop(&agent_runtime, &path, false, Some(health.clone())).await;
82            if let Err(e) = result {
83                let msg = format!("agent exited: {e:#}");
84                let _ = crate::agent::paths::append_agent_log(&path, &msg);
85                if let Ok(mut g) = health.lock() {
86                    g.loop_running = false;
87                    g.record_error(&msg);
88                }
89            }
90        }))
91    } else {
92        None
93    };
94
95    // Brief pause so the first tick can start before the TUI loads.
96    if will_spawn_agent {
97        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
98    }
99
100    let watch_config = WatchConfig {
101        rules_path: rules_path.clone(),
102        agent_mode,
103        market_snapshot,
104        agent_health,
105    };
106
107    let watch_result = tokio::task::spawn_blocking(move || run_watch_tui(&watch_config))
108        .await
109        .context("watch UI thread panicked")?;
110
111    if let Some(handle) = agent_handle {
112        handle.abort();
113        let _ = handle.await;
114    }
115
116    watch_result
117}
118
119pub async fn run_rules(runtime: &RuntimeConfig, command: RulesCommands) -> Result<()> {
120    match command {
121        RulesCommands::Show { file } => show_rules(runtime, file).await,
122        RulesCommands::List => {
123            if runtime.output == crate::output::OutputFormat::Json {
124                list_rules_files(runtime);
125            } else {
126                println!("\n{}", list_rules_files_display());
127                println!();
128            }
129            Ok(())
130        }
131    }
132}