ito_core/memory/
rendering.rs1use std::collections::BTreeMap;
16
17use serde_json::{Value, json};
18
19use super::{CaptureInputs, DEFAULT_SEARCH_LIMIT, QueryInputs, SearchInputs};
20
21#[must_use]
27pub fn shell_quote(value: &str) -> String {
28 let mut out = String::with_capacity(value.len() + 2);
29 out.push('\'');
30 for ch in value.chars() {
31 if ch == '\'' {
32 out.push_str("'\\''");
33 } else {
34 out.push(ch);
35 }
36 }
37 out.push('\'');
38 out
39}
40
41pub(super) fn render_capture_command(template: &str, inputs: &CaptureInputs) -> String {
44 let context_quoted = shell_quote(inputs.context.as_deref().unwrap_or(""));
45 let files = render_repeated_flag("--file", &inputs.files);
46 let folders = render_repeated_flag("--folder", &inputs.folders);
47 substitute(template, |name| match name {
48 "context" => Some(context_quoted.clone()),
49 "files" => Some(files.clone()),
50 "folders" => Some(folders.clone()),
51 _ => None,
52 })
53}
54
55pub(super) fn render_search_command(template: &str, inputs: &SearchInputs) -> String {
58 let query_quoted = shell_quote(&inputs.query);
59 let limit_value = inputs.limit.unwrap_or(DEFAULT_SEARCH_LIMIT).to_string();
60 let scope_quoted = shell_quote(inputs.scope.as_deref().unwrap_or(""));
64 substitute(template, |name| match name {
65 "query" => Some(query_quoted.clone()),
66 "limit" => Some(limit_value.clone()),
67 "scope" => Some(scope_quoted.clone()),
68 _ => None,
69 })
70}
71
72pub(super) fn render_query_command(template: &str, inputs: &QueryInputs) -> String {
74 let query_quoted = shell_quote(&inputs.query);
75 substitute(template, |name| match name {
76 "query" => Some(query_quoted.clone()),
77 _ => None,
78 })
79}
80
81pub(super) fn capture_inputs_as_structured(inputs: &CaptureInputs) -> BTreeMap<String, Value> {
83 let mut out = BTreeMap::new();
84 out.insert(
85 "context".to_string(),
86 inputs
87 .context
88 .as_ref()
89 .map_or(Value::Null, |s| Value::String(s.clone())),
90 );
91 out.insert("files".to_string(), json!(inputs.files));
92 out.insert("folders".to_string(), json!(inputs.folders));
93 out
94}
95
96pub(super) fn search_inputs_as_structured(inputs: &SearchInputs) -> BTreeMap<String, Value> {
98 let mut out = BTreeMap::new();
99 out.insert("query".to_string(), Value::String(inputs.query.clone()));
100 out.insert(
101 "limit".to_string(),
102 Value::Number(inputs.limit.unwrap_or(DEFAULT_SEARCH_LIMIT).into()),
103 );
104 out.insert(
105 "scope".to_string(),
106 inputs
107 .scope
108 .as_ref()
109 .map_or(Value::Null, |s| Value::String(s.clone())),
110 );
111 out
112}
113
114pub(super) fn query_inputs_as_structured(inputs: &QueryInputs) -> BTreeMap<String, Value> {
116 let mut out = BTreeMap::new();
117 out.insert("query".to_string(), Value::String(inputs.query.clone()));
118 out
119}
120
121fn render_repeated_flag(flag: &str, values: &[String]) -> String {
124 let mut out = String::new();
125 for (idx, value) in values.iter().enumerate() {
126 if idx > 0 {
127 out.push(' ');
128 }
129 out.push_str(flag);
130 out.push(' ');
131 out.push_str(&shell_quote(value));
132 }
133 out
134}
135
136fn substitute<F>(template: &str, lookup: F) -> String
143where
144 F: Fn(&str) -> Option<String>,
145{
146 let bytes = template.as_bytes();
147 let mut out = String::with_capacity(template.len());
148 let mut i = 0;
149 while i < bytes.len() {
150 if bytes[i] == b'{' {
151 let rest = &template[i + 1..];
154 if let Some(end) = rest.find('}') {
155 let name = &rest[..end];
156 if is_placeholder_name(name) {
157 if let Some(replacement) = lookup(name) {
158 out.push_str(&replacement);
159 } else {
160 out.push('{');
162 out.push_str(name);
163 out.push('}');
164 }
165 i += 1 + end + 1; continue;
167 }
168 }
169 }
170 let ch = template[i..]
173 .chars()
174 .next()
175 .expect("non-empty by loop guard");
176 out.push(ch);
177 i += ch.len_utf8();
178 }
179 out
180}
181
182fn is_placeholder_name(name: &str) -> bool {
187 !name.is_empty() && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
188}