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,
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    extra.push("--trust".into());
138    extra.push("--yes".into());
139    let pid = spawn_background(&rules_path, &extra)?;
140    runtime.emit(ResponseEnvelope::ok(
141        "agent background",
142        serde_json::json!({
143            "pid": pid,
144            "rules": rules_path,
145            "log_file": log_path(&rules_path),
146        }),
147    ));
148    Ok(())
149}
150
151async fn stop_agent(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
152    let rules_path = resolve_rules_file(file, true)?;
153    stop_daemon(&rules_path)?;
154    runtime.emit(ResponseEnvelope::ok(
155        "agent stop",
156        serde_json::json!({ "stopped": true, "rules": rules_path }),
157    ));
158    Ok(())
159}
160
161fn print_menu_help() {
162    println!(
163        r#"
164  schwab dashboard [rules.yaml]     Rich status dashboard
165  schwab watch [rules.yaml]         Live TUI + agent (q stops both)
166  schwab watch --monitor-only       Attach to running daemon only
167  schwab rules show [rules.yaml]    Full rules breakdown
168  schwab rules list               Discover rules/*.yaml
169  schwab agent run --background   Start daemon
170  schwab agent stop               Stop daemon
171
172  Set SCHWAB_RULES=path/to/rules.yaml to skip file prompts.
173  Agent mode: add --json for machine-readable output.
174"#
175    );
176}
177
178pub fn list_rules_files(runtime: &RuntimeConfig) {
179    use crate::ui::discover::discover_rules_files;
180    let files = discover_rules_files();
181    runtime.emit(ResponseEnvelope::ok(
182        "rules list",
183        serde_json::json!({
184            "files": files,
185            "env": "SCHWAB_RULES",
186        }),
187    ));
188}