gestura_core_hooks/
executor.rs1use async_trait::async_trait;
4use std::time::Instant;
5use tokio::process::Command;
6
7use gestura_core_foundation::error::{AppError, Result};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct HookOutput {
12 pub exit_code: Option<i32>,
14 pub stdout: String,
16 pub stderr: String,
18 pub duration_ms: u64,
20}
21
22#[async_trait]
27pub trait HookExecutor: Send + Sync {
28 async fn execute(
30 &self,
31 program: &str,
32 args: &[String],
33 cwd: Option<&std::path::Path>,
34 max_output_bytes: usize,
35 ) -> Result<HookOutput>;
36}
37
38pub struct ProcessHookExecutor;
40
41#[async_trait]
42impl HookExecutor for ProcessHookExecutor {
43 async fn execute(
44 &self,
45 program: &str,
46 args: &[String],
47 cwd: Option<&std::path::Path>,
48 max_output_bytes: usize,
49 ) -> Result<HookOutput> {
50 let start = Instant::now();
51
52 let mut cmd = Command::new(program);
53 cmd.args(args);
54 if let Some(dir) = cwd {
55 cmd.current_dir(dir);
56 }
57
58 let output = cmd.output().await.map_err(AppError::from)?;
59
60 let mut stdout = String::from_utf8_lossy(&output.stdout).to_string();
61 let mut stderr = String::from_utf8_lossy(&output.stderr).to_string();
62
63 if stdout.len() > max_output_bytes {
64 stdout.truncate(max_output_bytes);
65 }
66 if stderr.len() > max_output_bytes {
67 stderr.truncate(max_output_bytes);
68 }
69
70 Ok(HookOutput {
71 exit_code: output.status.code(),
72 stdout,
73 stderr,
74 duration_ms: start.elapsed().as_millis() as u64,
75 })
76 }
77}