diff --git a/Cargo.lock b/Cargo.lock index 26a3032..31066b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4093,6 +4093,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -4433,6 +4443,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index a01bf05..bcb330d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" polymarket-client-sdk = { version = "0.4", features = ["gamma", "data", "bridge", "clob", "ctf"] } alloy = { version = "1.6.3", default-features = false, features = ["providers", "sol-types", "contract", "reqwest", "reqwest-rustls-tls", "signer-local", "signers"] } clap = { version = "4", features = ["derive"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] } serde_json = "1" serde = { version = "1", features = ["derive"] } tabled = "0.17" diff --git a/src/main.rs b/src/main.rs index 61af087..9e7c32b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,10 @@ pub(crate) struct Cli { #[arg(short, long, global = true, default_value = "table")] pub(crate) output: OutputFormat, + /// Auto-refresh every N seconds (table output only) + #[arg(short, long, global = true)] + pub(crate) watch: Option, + /// Private key (overrides env var and config file) #[arg(long, global = true)] private_key: Option, @@ -71,6 +75,18 @@ async fn main() -> ExitCode { let cli = Cli::parse(); let output = cli.output; + if let Some(interval) = cli.watch { + if matches!(output, OutputFormat::Json) { + eprintln!("Error: --watch is not supported with JSON output"); + return ExitCode::FAILURE; + } + if interval == 0 { + eprintln!("Error: --watch interval must be at least 1 second"); + return ExitCode::FAILURE; + } + return watch_loop(interval).await; + } + if let Err(e) = run(cli).await { match output { OutputFormat::Json => { @@ -86,6 +102,28 @@ async fn main() -> ExitCode { ExitCode::SUCCESS } +async fn watch_loop(interval_secs: u64) -> ExitCode { + let interval = std::time::Duration::from_secs(interval_secs); + loop { + print!("\x1b[2J\x1b[H"); + let now = chrono::Local::now().format("%H:%M:%S"); + println!("Every {interval_secs}s \u{2014} {now} (Ctrl+C to stop)\n"); + + let cli = Cli::parse(); + if let Err(e) = run(cli).await { + eprintln!("Error: {e}"); + } + + tokio::select! { + _ = tokio::time::sleep(interval) => {} + _ = tokio::signal::ctrl_c() => { + println!(); + return ExitCode::SUCCESS; + } + } + } +} + #[allow(clippy::too_many_lines)] pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { match cli.command { diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 41d3d11..bcdd98f 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -486,3 +486,30 @@ fn wallet_address_succeeds_or_fails_gracefully() { // Either succeeds or fails with an error message — not a panic assert!(output.status.success() || !output.stderr.is_empty()); } + +#[test] +fn watch_flag_appears_in_help() { + polymarket() + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains("--watch")); +} + +#[test] +fn watch_with_json_output_rejected() { + polymarket() + .args(["--watch", "5", "-o", "json", "wallet", "show"]) + .assert() + .failure() + .stderr(predicate::str::contains("--watch is not supported with JSON")); +} + +#[test] +fn watch_zero_interval_rejected() { + polymarket() + .args(["--watch", "0", "wallet", "show"]) + .assert() + .failure() + .stderr(predicate::str::contains("at least 1 second")); +}