diff --git a/src/app.rs b/src/app.rs index 887fd51..3728241 100644 --- a/src/app.rs +++ b/src/app.rs @@ -476,7 +476,7 @@ impl App { // Step 7: Clipboard / dry-run / commit if self.cli.clipboard { - Self::copy_to_clipboard(&message)?; + Self::copy_to_clipboard(message.clone()).await?; eprintln!("{} Copied to clipboard!", style("✓").green().bold()); println!("{}", message); return Ok(()); @@ -553,7 +553,7 @@ impl App { clap_complete::generate(*shell, &mut cmd, "commitbee", &mut std::io::stdout()); Ok(()) } - Commands::Hook { action } => self.handle_hook(action), + Commands::Hook { action } => self.handle_hook(action).await, #[cfg(feature = "secure-storage")] Commands::SetKey { provider } => self.set_api_key(provider), #[cfg(feature = "secure-storage")] @@ -1009,28 +1009,25 @@ impl App { // ─── Hook Commands ─── - fn handle_hook(&self, action: &HookAction) -> Result<()> { + async fn handle_hook(&self, action: &HookAction) -> Result<()> { match action { - HookAction::Install => self.hook_install(), - HookAction::Uninstall => self.hook_uninstall(), - HookAction::Status => self.hook_status(), + HookAction::Install => self.hook_install().await, + HookAction::Uninstall => self.hook_uninstall().await, + HookAction::Status => self.hook_status().await, } } - fn hook_dir(&self) -> Result { + async fn hook_dir(&self) -> Result { // Verify we're in a git repo first let _git = GitService::discover()?; - // `hook_dir` runs under the Tokio runtime (`main` is `#[tokio::main]` - // and hook commands reach here via `app.run().await`), so this - // `std::process::Command` can block a worker thread. F-002 will - // migrate the hook / clipboard paths to `tokio::process::Command`; - // until then, allow the lint locally so the new clippy.toml rule - // does not block unrelated PRs. - #[allow(clippy::disallowed_methods)] - let output = std::process::Command::new("git") + // Use `tokio::process::Command` so the spawn does not block a Tokio + // worker thread — `hook_dir` is reached via `app.run().await`, so it + // is always invoked under the runtime. + let output: std::process::Output = tokio::process::Command::new("git") .args(["rev-parse", "--git-dir"]) - .output()?; + .output() + .await?; if !output.status.success() { return Err(Error::Git("Cannot find .git directory".into())); @@ -1040,12 +1037,12 @@ impl App { Ok(PathBuf::from(git_dir).join("hooks")) } - fn hook_path(&self) -> Result { - Ok(self.hook_dir()?.join("prepare-commit-msg")) + async fn hook_path(&self) -> Result { + Ok(self.hook_dir().await?.join("prepare-commit-msg")) } - fn hook_install(&self) -> Result<()> { - let hooks_dir = self.hook_dir()?; + async fn hook_install(&self) -> Result<()> { + let hooks_dir = self.hook_dir().await?; let hook_path = hooks_dir.join("prepare-commit-msg"); let backup_path = hooks_dir.join("prepare-commit-msg.commitbee-backup"); @@ -1127,8 +1124,8 @@ fi Ok(()) } - fn hook_uninstall(&self) -> Result<()> { - let hooks_dir = self.hook_dir()?; + async fn hook_uninstall(&self) -> Result<()> { + let hooks_dir = self.hook_dir().await?; let hook_path = hooks_dir.join("prepare-commit-msg"); let backup_path = hooks_dir.join("prepare-commit-msg.commitbee-backup"); @@ -1175,8 +1172,8 @@ fi Ok(()) } - fn hook_status(&self) -> Result<()> { - let hook_path = self.hook_path()?; + async fn hook_status(&self) -> Result<()> { + let hook_path = self.hook_path().await?; if !hook_path.exists() { eprintln!( @@ -1555,18 +1552,26 @@ fi // ─── Clipboard Helpers ─── /// Copy text to the system clipboard using the arboard crate. - fn copy_to_clipboard(text: &str) -> Result<()> { - let mut clipboard = arboard::Clipboard::new().map_err(|e| { - Error::Config(format!( - "Failed to initialize clipboard: {e}. If on Linux, ensure x11 or wayland dependencies are installed." - )) - })?; + /// + /// `arboard` is a synchronous library and may briefly block on the X11 / + /// Wayland / Cocoa clipboard daemon, so we delegate to + /// `tokio::task::spawn_blocking` to avoid stalling a runtime worker. + async fn copy_to_clipboard(text: String) -> Result<()> { + tokio::task::spawn_blocking(move || -> Result<()> { + let mut clipboard = arboard::Clipboard::new().map_err(|e| { + Error::Config(format!( + "Failed to initialize clipboard: {e}. If on Linux, ensure x11 or wayland dependencies are installed." + )) + })?; - clipboard - .set_text(text) - .map_err(|e| Error::Config(format!("Failed to copy to clipboard: {e}")))?; + clipboard + .set_text(text) + .map_err(|e| Error::Config(format!("Failed to copy to clipboard: {e}")))?; - Ok(()) + Ok(()) + }) + .await + .map_err(|e| Error::Config(format!("clipboard task failed to join: {e}")))? } }