gravel_provider_custom/
lib.rs1use gravel_ffi::prelude::*;
85use nonempty::NonEmpty;
86use serde::Deserialize;
87use std::{ffi::OsStr, io, process::Command};
88
89struct CustomProvider {
90 hits: StaticHitCache,
91}
92
93#[gravel_provider("custom")]
94impl Provider for CustomProvider {
95 fn new(config: &PluginConfigAdapter<'_>) -> Self {
96 let hits = config.get::<Config>("").hits;
97 log::trace!("initializing custom provider with {} hits", hits.len());
98
99 Self {
100 hits: StaticHitCache::new(hits.into_iter().map(into_hit)),
101 }
102 }
103
104 fn query(&self, _query: &str) -> ProviderResult {
105 ProviderResult::from_cached(self.hits.get())
106 }
107}
108
109fn into_hit(config: HitConfig) -> SimpleHit {
110 let subtitle = config.subtitle.unwrap_or_default();
111 let wait = config.wait.unwrap_or(true);
112
113 let mut hit = SimpleHit::new(config.title, subtitle, move |h, context| {
114 run_action(&config.action, wait, h);
115 run_post_action(context, config.post_action);
116 })
117 .with_score(config.override_score);
118
119 if let Some(secondary_action) = config.secondary_action {
120 hit = hit.with_secondary(move |hit, context| {
121 run_action(&secondary_action, wait, hit);
122 run_post_action(context, config.post_action);
123 });
124 }
125
126 hit
127}
128
129fn run_action(action: &Action, wait: bool, hit: &SimpleHit) {
130 log::debug!("running custom hit '{}'", hit.title());
131
132 let command = match action {
133 #[cfg(unix)]
134 Action::SystemShell(arg) => command("sh", ["-c", arg]),
135
136 #[cfg(windows)]
137 Action::SystemShell(arg) => command("cmd", ["/c", arg]),
138
139 Action::Shell { shell, command: arg } => command(shell, ["-c", arg]),
140 Action::Command(args) => command(args.first(), args.iter().skip(1)),
141 };
142
143 if let Err(e) = run_command(command, wait) {
144 log::error!("unable to run custom hit '{}': {e}", hit.title());
145 }
146}
147
148fn run_command(mut command: Command, wait: bool) -> io::Result<()> {
149 command.spawn().and_then(|mut p| {
150 if wait {
151 p.wait()?;
152 }
153
154 Ok(())
155 })
156}
157
158fn command<I, P>(executable: impl AsRef<OsStr>, args: I) -> Command
159where
160 I: IntoIterator<Item = P>,
161 P: AsRef<OsStr>,
162{
163 let mut command = Command::new(executable);
164 command.args(args);
165
166 command
167}
168
169fn run_post_action(context: RefDynHitActionContext<'_>, action: PostAction) {
170 match action {
171 PostAction::Nothing => (),
172 PostAction::Hide => context.hide_frontend(),
173 PostAction::Refresh => context.refresh_frontend(),
174 }
175}
176
177#[derive(Deserialize, Debug)]
178struct Config {
179 pub hits: Vec<HitConfig>,
180}
181
182#[derive(Deserialize, Debug)]
183struct HitConfig {
184 pub title: String,
185 pub subtitle: Option<String>,
186 pub override_score: Option<u32>,
187 pub wait: Option<bool>,
188 pub action: Action,
189 pub secondary_action: Option<Action>,
190
191 #[serde(default)]
192 pub post_action: PostAction,
193}
194
195#[derive(Deserialize, Debug)]
196#[serde(untagged)]
197#[serde(expecting = "data did not match any known action format")]
198enum Action {
199 SystemShell(String),
200 Shell { shell: String, command: String },
201 Command(NonEmpty<String>),
202}
203
204#[derive(Deserialize, Debug, Clone, Copy)]
205#[serde(rename_all = "snake_case")]
206#[derive(Default)]
207enum PostAction {
208 Nothing,
209 #[default]
210 Hide,
211 Refresh,
212}