sandbox_quant/command/
recorder.rs1use 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}