Files
keep/src/meta_plugin/program.rs
Andrew Phillips d530f1bc43 refactor: remove is_default from meta plugins and add read_time/read_rate plugins
Co-authored-by: aider (openai/andrew.openrouter.qwen.qwen3-coder) <aider@aider.chat>
2025-07-29 11:32:48 -03:00

140 lines
4.3 KiB
Rust

use crate::plugins::ProgramWriter;
use anyhow::{Context, Result, anyhow};
use log::*;
use std::env;
use std::fs;
use std::io;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::process::{Command, Stdio};
use crate::meta_plugin::MetaPlugin;
#[derive(Clone, Debug)]
pub struct MetaPluginProgram {
pub program: String,
pub args: Vec<String>,
pub supported: bool,
pub meta_name: String,
pub split_whitespace: bool,
buffer: Vec<u8>,
}
impl MetaPluginProgram {
pub fn new(program: &str, args: Vec<&str>, meta_name: String, split_whitespace: bool) -> MetaPluginProgram {
let program_path = get_program_path(program);
let supported = program_path.is_ok();
MetaPluginProgram {
program: program_path.unwrap_or(program.to_string()),
args: args.iter().map(|s| s.to_string()).collect(),
supported,
meta_name,
split_whitespace,
buffer: Vec::new(),
}
}
}
impl MetaPlugin for MetaPluginProgram {
fn is_supported(&self) -> bool {
self.supported
}
fn create(&self) -> Result<Box<dyn Write>> {
debug!("META: Writing using {:?}", *self);
let program = self.program.clone();
let args = self.args.clone();
debug!("META: Executing command: {:?} {:?}", program, args);
let mut process = Command::new(program.clone())
.args(args.clone())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.context(anyhow!(
"Problem spawning child process: {:?} {:?}",
program,
args
))?;
Ok(Box::new(ProgramWriter {
stdin: process.stdin.take().unwrap(),
}))
}
fn finalize(&mut self) -> io::Result<String> {
let program = self.program.clone();
let args = self.args.clone();
debug!("META: Executing command for finalize: {:?} {:?}", program, args);
let mut process = Command::new(program)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to spawn process: {}", e)))?;
let stdin = process.stdin.as_mut().unwrap();
stdin.write_all(&self.buffer)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to write to stdin: {}", e)))?;
let output = process.wait_with_output()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to wait for process: {}", e)))?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed_result = stdout.trim();
// For certain programs, we only want the first part before whitespace
if self.split_whitespace {
let parts: Vec<&str> = trimmed_result.split_whitespace().collect();
if !parts.is_empty() {
Ok(parts[0].to_string())
} else {
Ok(trimmed_result.to_string())
}
} else {
Ok(trimmed_result.to_string())
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(io::Error::new(
io::ErrorKind::Other,
format!("Command failed: {}", stderr.trim()),
))
}
}
fn update(&mut self, data: &[u8]) {
self.buffer.extend_from_slice(data);
}
fn meta_name(&mut self) -> String {
self.meta_name.clone()
}
}
fn get_program_path(program: &str) -> Result<String> {
debug!("META: Looking for executable: {}", program);
if let Ok(path) = env::var("PATH") {
for p in path.split(':') {
let p_str = format!("{}/{}", p, program);
let stat = fs::metadata(p_str.clone());
if let Ok(stat) = stat {
let md = stat;
let permissions = md.permissions();
if md.is_file() && permissions.mode() & 0o111 != 0 {
return Ok(p_str);
}
}
}
}
Err(anyhow!("Unable to find binary {} in PATH", program))
}