use std::collections::BTreeMap;
use serde_json::{Value, json};
use super::{CaptureInputs, DEFAULT_SEARCH_LIMIT, QueryInputs, SearchInputs};
#[must_use]
pub fn shell_quote(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('\'');
for ch in value.chars() {
if ch == '\'' {
out.push_str("'\\''");
} else {
out.push(ch);
}
}
out.push('\'');
out
}
pub(super) fn render_capture_command(template: &str, inputs: &CaptureInputs) -> String {
let context_quoted = shell_quote(inputs.context.as_deref().unwrap_or(""));
let files = render_repeated_flag("--file", &inputs.files);
let folders = render_repeated_flag("--folder", &inputs.folders);
substitute(template, |name| match name {
"context" => Some(context_quoted.clone()),
"files" => Some(files.clone()),
"folders" => Some(folders.clone()),
_ => None,
})
}
pub(super) fn render_search_command(template: &str, inputs: &SearchInputs) -> String {
let query_quoted = shell_quote(&inputs.query);
let limit_value = inputs.limit.unwrap_or(DEFAULT_SEARCH_LIMIT).to_string();
let scope_quoted = shell_quote(inputs.scope.as_deref().unwrap_or(""));
substitute(template, |name| match name {
"query" => Some(query_quoted.clone()),
"limit" => Some(limit_value.clone()),
"scope" => Some(scope_quoted.clone()),
_ => None,
})
}
pub(super) fn render_query_command(template: &str, inputs: &QueryInputs) -> String {
let query_quoted = shell_quote(&inputs.query);
substitute(template, |name| match name {
"query" => Some(query_quoted.clone()),
_ => None,
})
}
pub(super) fn capture_inputs_as_structured(inputs: &CaptureInputs) -> BTreeMap<String, Value> {
let mut out = BTreeMap::new();
out.insert(
"context".to_string(),
inputs
.context
.as_ref()
.map_or(Value::Null, |s| Value::String(s.clone())),
);
out.insert("files".to_string(), json!(inputs.files));
out.insert("folders".to_string(), json!(inputs.folders));
out
}
pub(super) fn search_inputs_as_structured(inputs: &SearchInputs) -> BTreeMap<String, Value> {
let mut out = BTreeMap::new();
out.insert("query".to_string(), Value::String(inputs.query.clone()));
out.insert(
"limit".to_string(),
Value::Number(inputs.limit.unwrap_or(DEFAULT_SEARCH_LIMIT).into()),
);
out.insert(
"scope".to_string(),
inputs
.scope
.as_ref()
.map_or(Value::Null, |s| Value::String(s.clone())),
);
out
}
pub(super) fn query_inputs_as_structured(inputs: &QueryInputs) -> BTreeMap<String, Value> {
let mut out = BTreeMap::new();
out.insert("query".to_string(), Value::String(inputs.query.clone()));
out
}
fn render_repeated_flag(flag: &str, values: &[String]) -> String {
let mut out = String::new();
for (idx, value) in values.iter().enumerate() {
if idx > 0 {
out.push(' ');
}
out.push_str(flag);
out.push(' ');
out.push_str(&shell_quote(value));
}
out
}
fn substitute<F>(template: &str, lookup: F) -> String
where
F: Fn(&str) -> Option<String>,
{
let bytes = template.as_bytes();
let mut out = String::with_capacity(template.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'{' {
let rest = &template[i + 1..];
if let Some(end) = rest.find('}') {
let name = &rest[..end];
if is_placeholder_name(name) {
if let Some(replacement) = lookup(name) {
out.push_str(&replacement);
} else {
out.push('{');
out.push_str(name);
out.push('}');
}
i += 1 + end + 1; continue;
}
}
}
let ch = template[i..]
.chars()
.next()
.expect("non-empty by loop guard");
out.push(ch);
i += ch.len_utf8();
}
out
}
fn is_placeholder_name(name: &str) -> bool {
!name.is_empty() && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
}