Skip to main content

sandbox_quant/command/
recorder.rs

1use crate::app::bootstrap::BinanceMode;
2use crate::app::cli::normalize_instrument_symbol;
3use crate::terminal::completion::ShellCompletion;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum RecorderCommand {
7    Start { symbols: Vec<String> },
8    Status,
9    Stop,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum RecorderShellInput {
14    Empty,
15    Help,
16    Exit,
17    Mode(BinanceMode),
18    Command(RecorderCommand),
19}
20
21pub fn recorder_help_text() -> &'static str {
22    "/start [symbols...]\n/status\n/stop\n/mode <real|demo>\n/help\n/exit"
23}
24
25pub fn parse_recorder_shell_input(line: &str) -> Result<RecorderShellInput, String> {
26    let trimmed = line.trim();
27    if trimmed.is_empty() {
28        return Ok(RecorderShellInput::Empty);
29    }
30
31    let without_prefix = trimmed.strip_prefix('/').unwrap_or(trimmed);
32    match without_prefix {
33        "help" => return Ok(RecorderShellInput::Help),
34        "exit" | "quit" => return Ok(RecorderShellInput::Exit),
35        _ => {}
36    }
37
38    let args: Vec<String> = without_prefix
39        .split_whitespace()
40        .map(str::to_string)
41        .collect();
42    if args.first().map(String::as_str) == Some("mode") {
43        let raw_mode = args.get(1).ok_or("usage: /mode <real|demo>")?;
44        let mode = match raw_mode.as_str() {
45            "real" => BinanceMode::Real,
46            "demo" => BinanceMode::Demo,
47            _ => return Err(format!("unsupported mode: {raw_mode}")),
48        };
49        return Ok(RecorderShellInput::Mode(mode));
50    }
51
52    parse_recorder_command(&args).map(RecorderShellInput::Command)
53}
54
55pub fn parse_recorder_command(args: &[String]) -> Result<RecorderCommand, String> {
56    match args.first().map(String::as_str) {
57        Some("start") => Ok(RecorderCommand::Start {
58            symbols: args[1..]
59                .iter()
60                .map(|raw| normalize_instrument_symbol(raw))
61                .collect(),
62        }),
63        Some("status") => {
64            if args.len() > 1 {
65                Err("usage: /status".to_string())
66            } else {
67                Ok(RecorderCommand::Status)
68            }
69        }
70        Some("stop") => {
71            if args.len() > 1 {
72                Err("usage: /stop".to_string())
73            } else {
74                Ok(RecorderCommand::Stop)
75            }
76        }
77        Some(other) => Err(format!("unsupported command: {other}")),
78        None => Err("missing recorder command".to_string()),
79    }
80}
81
82pub fn complete_recorder_input(line: &str) -> Vec<ShellCompletion> {
83    let trimmed = line.trim_start();
84    let without_prefix = trimmed.strip_prefix('/').unwrap_or(trimmed);
85    let trailing_space = without_prefix.ends_with(' ');
86    let parts: Vec<&str> = without_prefix.split_whitespace().collect();
87
88    if parts.is_empty() {
89        return vec![
90            completion("/start", "start recorder with optional symbols"),
91            completion("/status", "show recorder status"),
92            completion("/stop", "stop recorder"),
93            completion("/mode", "switch mode"),
94            completion("/help", "show help"),
95            completion("/exit", "exit"),
96        ];
97    }
98
99    if parts.len() == 1 && !trailing_space {
100        return ["/start", "/status", "/stop", "/mode", "/help", "/exit"]
101            .into_iter()
102            .filter(|item| item.trim_start_matches('/').starts_with(parts[0]))
103            .map(|item| completion(item, ""))
104            .collect();
105    }
106
107    match parts.first().copied() {
108        Some("mode") => ["real", "demo"]
109            .into_iter()
110            .filter(|item| item.starts_with(parts.last().copied().unwrap_or_default()))
111            .map(|item| completion(&format!("/mode {item}"), "switch recorder mode"))
112            .collect(),
113        _ => Vec::new(),
114    }
115}
116
117fn completion(value: &str, description: &str) -> ShellCompletion {
118    ShellCompletion {
119        value: value.to_string(),
120        description: description.to_string(),
121    }
122}