diff --git a/.typos.toml b/.typos.toml index 96261e3..6a7224c 100644 --- a/.typos.toml +++ b/.typos.toml @@ -9,3 +9,4 @@ serde = "serde" reqwest = "reqwest" clippy = "clippy" streamable = "streamable" +indicatif = "indicatif" diff --git a/Cargo.lock b/Cargo.lock index f45ad04..a38fcc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -332,11 +345,12 @@ dependencies = [ [[package]] name = "deepwiki-cli" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", "assert_cmd", "clap", + "indicatif", "predicates", "rmcp", "serde", @@ -373,6 +387,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "errno" version = "0.3.14" @@ -765,6 +785,19 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -900,6 +933,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.21.3" @@ -965,6 +1004,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1808,6 +1853,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" @@ -2068,6 +2119,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" diff --git a/Cargo.toml b/Cargo.toml index c062bf8..fd2f7e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deepwiki-cli" -version = "0.1.1" +version = "0.2.0" edition = "2021" description = "CLI for DeepWiki — query GitHub repo wikis without MCP overhead" license = "MIT" @@ -15,6 +15,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0" clap = { version = "4", features = ["derive"] } +indicatif = "0.17" rmcp = { version = "0.17", features = ["transport-streamable-http-client-reqwest", "client", "reqwest"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/main.rs b/src/main.rs index 6f53570..b30e007 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ mod cli; mod client; mod output; +mod spinner; use anyhow::Result; use clap::Parser; use cli::{Cli, Command}; use client::DeepWikiClient; +use spinner::Spinner; #[tokio::main] async fn main() { @@ -27,13 +29,16 @@ async fn run() -> Result<()> { return Ok(()); } + let spinner = Spinner::start("Connecting to DeepWiki..."); let client = DeepWikiClient::connect().await?; + spinner.set_message(&command_spinner_message(&cli.command)); let text = match &cli.command { Command::Ask { repo, question } => client.ask_question(repo, question).await?, Command::Structure { repo } => client.read_wiki_structure(repo).await?, Command::Read { repo } => client.read_wiki_contents(repo).await?, }; + spinner.finish(); println!("{}", output::format_for_claude(&text, repo, query_type)); client.cancel().await?; @@ -41,6 +46,14 @@ async fn run() -> Result<()> { Ok(()) } +fn command_spinner_message(command: &Command) -> String { + match command { + Command::Ask { repo, .. } => format!("Asking DeepWiki about {}...", repo), + Command::Structure { repo } => format!("Fetching wiki structure for {}...", repo), + Command::Read { repo } => format!("Reading wiki contents for {}...", repo), + } +} + fn repo_and_query_type(command: &Command) -> (&str, &str) { match command { Command::Ask { repo, .. } => (repo, "ask"), diff --git a/src/spinner.rs b/src/spinner.rs new file mode 100644 index 0000000..21e3f61 --- /dev/null +++ b/src/spinner.rs @@ -0,0 +1,58 @@ +use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; +use std::time::Duration; + +const TICK_INTERVAL: Duration = Duration::from_millis(80); + +pub struct Spinner { + bar: ProgressBar, +} + +impl Spinner { + pub fn start(message: &str) -> Self { + let bar = ProgressBar::new_spinner(); + bar.set_draw_target(ProgressDrawTarget::stderr()); + bar.set_style( + ProgressStyle::default_spinner() + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]) + .template("{spinner} {msg}") + .expect("template should be valid"), + ); + bar.set_message(message.to_string()); + bar.enable_steady_tick(TICK_INTERVAL); + Self { bar } + } + + pub fn set_message(&self, message: &str) { + self.bar.set_message(message.to_string()); + } + + pub fn finish(self) { + self.bar.finish_and_clear(); + } +} + +impl Drop for Spinner { + fn drop(&mut self) { + if !self.bar.is_finished() { + self.bar.finish_and_clear(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn spinner_starts_and_finishes_without_panic() { + let spinner = Spinner::start("test message"); + spinner.set_message("updated message"); + spinner.finish(); + } + + #[test] + fn spinner_drop_cleans_up() { + let spinner = Spinner::start("test message"); + drop(spinner); + } +}