use std::path::PathBuf;
use rustyline::error::ReadlineError;
use rustyline::history::FileHistory;
use rustyline::Editor;
use super::completer::ReplHelper;
use crate::cmd::{DEPLOY_SUBCOMMAND, REPL_SUBCOMMAND};
use crate::container::ContractStorageSource;
use crate::{ContractProvider, DeployedContractsContainer, OdraCli};
const HISTORY_FILE: &str = ".odra_cli_history";
const DEFAULT_PROMPT: &str = "odra> ";
pub(super) fn run(cli: &OdraCli, container: &mut DeployedContractsContainer) -> anyhow::Result<()> {
let mut editor: Editor<ReplHelper, FileHistory> = Editor::new()?;
let helper = ReplHelper::new(
cli.main_cmd.to_command(&[REPL_SUBCOMMAND]),
&["help", "exit", "quit"]
);
editor.set_helper(Some(helper));
let history_path = history_path();
let _ = editor.load_history(&history_path);
print_banner(cli);
let prompt = prompt();
loop {
print_status_line(cli, container);
match editor.readline(&prompt) {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
match line {
"exit" | "quit" => break,
"help" => {
let mut help_cmd = cli.main_cmd.to_command(&[REPL_SUBCOMMAND]);
let _ = help_cmd.print_help();
println!();
continue;
}
_ => {}
}
let _ = editor.add_history_entry(line);
let tokens = match shlex::split(line) {
Some(tokens) => tokens,
None => {
prettycli::error("Could not parse input: unbalanced quotes.");
continue;
}
};
let mut argv = vec!["odra-cli".to_string()];
argv.extend(tokens);
match cli
.main_cmd
.try_get_matches_from_excluding(argv, &[REPL_SUBCOMMAND])
{
Ok((cmd, args, _path)) => {
if let Err(err) = cli.dispatch(&cmd, &args, container) {
prettycli::error(&format!("{err:#}"));
continue;
}
if cmd == DEPLOY_SUBCOMMAND {
if let Err(err) = cli
.register_deployed_contracts(container, &cli.default_contract_path)
{
prettycli::error(&format!("{err:#}"));
}
}
}
Err(clap_err) => {
let _ = clap_err.print();
}
}
}
Err(ReadlineError::Interrupted) => continue,
Err(ReadlineError::Eof) => break,
Err(err) => {
prettycli::error(&format!("Input error: {err}"));
break;
}
}
}
let _ = editor.save_history(&history_path);
Ok(())
}
fn print_banner(cli: &OdraCli) {
let caller = cli.host_env.caller();
prettycli::info(&format!("Odra CLI interactive session — {}", chain_label()));
prettycli::info(&format!("Caller: {}", caller.to_string()));
prettycli::info("Type `help` for available commands, `exit` or Ctrl-D to quit.");
}
fn print_status_line(cli: &OdraCli, container: &DeployedContractsContainer) {
let network = match std::env::var("ODRA_CASPER_LIVENET_CHAIN_NAME") {
Ok(name) if !name.is_empty() => name,
_ => "no chain".to_string()
};
let caller = short_address(&cli.host_env.caller().to_string());
let file = contracts_file_name(container);
let count = container.all_contracts().len();
let contracts = format!(
"{count} {}",
if count == 1 { "contract" } else { "contracts" }
);
println!("\x1b[2m⬡ {network} · {caller} · {file} ({contracts})\x1b[0m");
}
fn contracts_file_name(container: &DeployedContractsContainer) -> String {
match container.source() {
ContractStorageSource::File { path } => path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned()),
ContractStorageSource::Memory => "memory".to_string()
}
}
fn short_address(addr: &str) -> String {
let chars: Vec<char> = addr.chars().collect();
if chars.len() <= 24 {
return addr.to_string();
}
let head: String = chars[..16].iter().collect();
let tail: String = chars[chars.len() - 4..].iter().collect();
format!("{head}…{tail}")
}
fn prompt() -> String {
match std::env::var("ODRA_CASPER_LIVENET_CHAIN_NAME") {
Ok(name) if !name.is_empty() => format!("{name}> "),
_ => DEFAULT_PROMPT.to_string()
}
}
fn chain_label() -> String {
match std::env::var("ODRA_CASPER_LIVENET_CHAIN_NAME") {
Ok(name) if !name.is_empty() => format!("chain `{name}`"),
_ => "unknown chain".to_string()
}
}
fn history_path() -> PathBuf {
match std::env::var("HOME") {
Ok(home) if !home.is_empty() => PathBuf::from(home).join(HISTORY_FILE),
_ => PathBuf::from(HISTORY_FILE)
}
}
#[cfg(test)]
mod tests {
use super::{contracts_file_name, short_address};
use crate::test_utils;
#[test]
fn short_address_truncates_long_addresses() {
let addr = "account-hash-2a6da1c25eb66de30ad01e65fe37d111f37813db12487998093c628a153a7961";
let short = short_address(addr);
assert_eq!(short, "account-hash-2a6…7961");
assert!(short.len() < addr.len());
}
#[test]
fn short_address_leaves_short_strings_untouched() {
assert_eq!(short_address("no chain"), "no chain");
}
#[test]
fn contracts_file_name_for_memory_container() {
let container = test_utils::mock_contracts_container();
assert_eq!(contracts_file_name(&container), "memory");
}
}