Skip to main content

schwab_cli/ui/
menu.rs

1use std::io::{self, Write};
2use std::path::PathBuf;
3
4use anyhow::Result;
5use inquire::Select;
6
7use crate::agent::{log_path, spawn_background, stop_daemon};
8use crate::config::RuntimeConfig;
9use crate::output::ResponseEnvelope;
10use crate::rules::RulesConfig;
11use crate::ui::context::{tail_lines, DashboardContext};
12use crate::ui::dashboard::render_dashboard;
13use crate::ui::discover::{list_rules_files_display, resolve_rules_file};
14use crate::ui::rules_view::render_rules_detail;
15
16pub async fn run_interactive_menu(runtime: &RuntimeConfig) -> Result<()> {
17    loop {
18        let choice = Select::new("schwab — what next?", menu_choices())
19            .with_help_message("Human mode interactive menu")
20            .prompt();
21
22        let choice = match choice {
23            Ok(c) => c,
24            Err(_) => break,
25        };
26
27        match choice.as_str() {
28            "Dashboard" => show_dashboard(runtime, None).await?,
29            "Watch agent (live)" => {
30                crate::commands::ui_cmd::run_watch(runtime, None, false).await?;
31            }
32            "Show rules" => show_rules(runtime, None).await?,
33            "Tail agent log" => tail_agent_log(None)?,
34            "Validate rules" => validate_rules(runtime, None).await?,
35            "Run agent (background)" => run_agent_background(runtime, None).await?,
36            "Stop agent" => stop_agent(runtime, None).await?,
37            "List rules files" => {
38                println!("\n{}", list_rules_files_display());
39                println!();
40            }
41            "Help" => print_menu_help(),
42            "Quit" => break,
43            _ => {}
44        }
45    }
46    Ok(())
47}
48
49fn menu_choices() -> Vec<String> {
50    vec![
51        "Dashboard".into(),
52        "Watch agent (live)".into(),
53        "Show rules".into(),
54        "Tail agent log".into(),
55        "Validate rules".into(),
56        "Run agent (background)".into(),
57        "Stop agent".into(),
58        "List rules files".into(),
59        "Help".into(),
60        "Quit".into(),
61    ]
62}
63
64pub async fn show_dashboard(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
65    let rules_path = resolve_rules_file(file, runtime.is_interactive())?;
66    let ctx = DashboardContext::load(&rules_path)?;
67
68    if runtime.output == crate::output::OutputFormat::Json {
69        runtime.emit(ResponseEnvelope::ok("dashboard", ctx.to_json()));
70        return Ok(());
71    }
72
73    print!("{}", render_dashboard(&ctx));
74    io::stdout().flush().ok();
75    Ok(())
76}
77
78pub async fn show_rules(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
79    let rules_path = resolve_rules_file(file, runtime.is_interactive())?;
80    let ctx = DashboardContext::load(&rules_path)?;
81
82    if runtime.output == crate::output::OutputFormat::Json {
83        runtime.emit(ResponseEnvelope::ok("rules show", ctx.to_json()));
84        return Ok(());
85    }
86
87    print!("{}", render_rules_detail(&ctx));
88    io::stdout().flush().ok();
89    Ok(())
90}
91
92pub fn tail_agent_log(file: Option<PathBuf>) -> Result<()> {
93    let rules_path = resolve_rules_file(file, true)?;
94    let log = log_path(&rules_path);
95    let lines = tail_lines(&log, 40);
96    if lines.is_empty() {
97        println!("\n  (no log at {})\n", log.display());
98        return Ok(());
99    }
100    println!("\n  Agent log — {}\n", log.display());
101    for line in lines {
102        println!("  {line}");
103    }
104    println!();
105    Ok(())
106}
107
108async fn validate_rules(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
109    let rules_path = resolve_rules_file(file, true)?;
110    let rules = RulesConfig::load(&rules_path)?;
111    runtime.emit(ResponseEnvelope::ok(
112        "agent validate",
113        serde_json::json!({
114            "valid": true,
115            "agent_id": rules.agent_id,
116            "rules_path": rules_path,
117            "accounts": rules.accounts.len(),
118            "watchlist": rules.watchlist_items(),
119            "llm_enabled": rules.llm.enabled,
120            "telegram_enabled": rules.notify.telegram.enabled,
121        }),
122    ));
123    Ok(())
124}
125
126async fn run_agent_background(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
127    let rules_path = resolve_rules_file(file, true)?;
128    if !runtime.trust || !runtime.yes {
129        anyhow::bail!(
130            "background agent requires --trust --yes (live trading guardrails still apply)"
131        );
132    }
133    let mut extra = Vec::new();
134    if runtime.dry_run {
135        extra.push("--dry-run".into());
136    }
137    if runtime.simulate {
138        extra.push("--simulate".into());
139    }
140    extra.push("--trust".into());
141    extra.push("--yes".into());
142    let pid = spawn_background(&rules_path, &extra)?;
143    runtime.emit(ResponseEnvelope::ok(
144        "agent background",
145        serde_json::json!({
146            "pid": pid,
147            "rules": rules_path,
148            "log_file": log_path(&rules_path),
149        }),
150    ));
151    Ok(())
152}
153
154async fn stop_agent(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
155    let rules_path = resolve_rules_file(file, true)?;
156    stop_daemon(&rules_path)?;
157    runtime.emit(ResponseEnvelope::ok(
158        "agent stop",
159        serde_json::json!({ "stopped": true, "rules": rules_path }),
160    ));
161    Ok(())
162}
163
164fn print_menu_help() {
165    println!(
166        r#"
167  schwab dashboard [rules.yaml]     Rich status dashboard
168  schwab watch [rules.yaml]         Live TUI + agent (q stops both)
169  schwab watch --monitor-only       Attach to running daemon only
170  schwab rules show [rules.yaml]    Full rules breakdown
171  schwab rules list               Discover rules/*.yaml
172  schwab agent run --background   Start daemon
173  schwab agent stop               Stop daemon
174
175  Set SCHWAB_RULES=path/to/rules.yaml to skip file prompts.
176  Agent mode: add --json for machine-readable output.
177"#
178    );
179}
180
181pub fn list_rules_files(runtime: &RuntimeConfig) {
182    use crate::ui::discover::discover_rules_files;
183    let files = discover_rules_files();
184    runtime.emit(ResponseEnvelope::ok(
185        "rules list",
186        serde_json::json!({
187            "files": files,
188            "env": "SCHWAB_RULES",
189        }),
190    ));
191}