1use crate::provider::prelude::*;
8
9#[derive(Debug, Default, PartialEq)]
10pub struct Flatpak {}
11
12impl Flatpak {
13 pub fn new() -> Self {
14 Default::default()
15 }
16}
17
18impl fmt::Display for Flatpak {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 write!(f, "Flatpak")
21 }
22}
23
24#[async_trait]
25impl IsProvider for Flatpak {
26 async fn search_internal(
27 &self,
28 command: &str,
29 target_env: Arc<Environment>,
30 ) -> ProviderResult<Vec<Candidate>> {
31 let mut state = FlatpakState::default();
32 state
33 .discover_remotes(&target_env)
34 .await
35 .map_err(|e| match e {
36 RemoteError::Execution(ExecutionError::NotFound(val)) => {
37 ProviderError::Requirements(val)
38 }
39 RemoteError::Execution(e) => ProviderError::Execution(e),
40 err => ProviderError::ApplicationError(anyhow::Error::new(err)),
41 })?;
42
43 let candidates = state
44 .get_remote_flatpaks(&target_env, command)
45 .await
46 .map_err(Error::RemoteFlatpaks)?;
47
48 state
49 .check_installed_flatpaks(&target_env, candidates)
50 .await
51 .map_err(Error::LocalFlatpaks)
52 .map_err(|e| e.into())
53 }
54}
55
56#[derive(Debug, Default)]
57struct FlatpakState {
58 remotes: Vec<Remote>,
59}
60
61impl FlatpakState {
62 pub async fn discover_remotes(&mut self, env: &Arc<Environment>) -> Result<(), RemoteError> {
67 let stdout = env
68 .output_of(cmd!("flatpak", "remotes", "--columns=name:f,options:f"))
69 .await?;
70
71 if stdout.trim().is_empty() {
73 return Err(RemoteError::NoRemote);
74 }
75
76 for line in stdout.lines() {
77 match line.split_once('\t') {
78 Some((name, opts)) => {
79 let remote_type = if opts.contains("user") {
80 RemoteType::User
81 } else if opts.contains("system") {
82 RemoteType::System
83 } else {
84 return Err(RemoteError::UnknownType(opts.to_string()));
85 };
86
87 let remote = Remote {
88 name: name.to_string(),
89 r#type: remote_type,
90 };
91
92 self.remotes.push(remote);
93 }
94 None => return Err(RemoteError::Parse(stdout)),
95 }
96 }
97
98 Ok(())
99 }
100
101 pub async fn get_remote_flatpaks(
106 &mut self,
107 env: &Arc<Environment>,
108 search_for: &str,
109 ) -> Result<Vec<Candidate>, RemoteFlatpakError> {
110 debug_assert!(
112 !self.remotes.is_empty(),
113 "cannot query remote flatpaks without a remote"
114 );
115 let mut candidates: Vec<Candidate> = vec![];
116
117 for remote in &self.remotes {
118 let output = env
119 .output_of(cmd!(
120 "flatpak",
121 "remote-ls",
122 "--app",
123 "--cached",
124 "--columns=application:f,version:f,origin:f,description:f",
125 &remote.r#type.to_cli(),
126 &remote.name
127 ))
128 .await
129 .map_err(|e| RemoteFlatpakError::Execution {
130 remote: remote.clone(),
131 source: e,
132 })?;
133
134 'next_line: for line in output.lines() {
135 let mut candidate = Candidate::default();
136 for (index, split) in line.split('\t').enumerate() {
137 match index {
138 0 => {
139 if split.to_lowercase().contains(&search_for.to_lowercase()) {
140 candidate.package = split.to_string();
141 } else {
142 continue 'next_line;
143 }
144 }
145 1 => candidate.version = split.to_string(),
146 2 => candidate.origin = remote.to_origin(),
147 3 => candidate.description = split.to_string(),
148 _ => warn!("superfluous fragment '{}' in line '{}'", split, line),
149 }
150 }
151
152 if !candidate.package.is_empty() {
153 let mut install_action = cmd!(
154 "flatpak",
155 "install",
156 "--app",
157 &remote.r#type.to_cli(),
158 &remote.name,
159 &candidate.package
160 );
161 install_action.needs_privileges(matches!(remote.r#type, RemoteType::System));
162 candidate.actions.install = Some(install_action);
163
164 candidate.actions.execute = cmd!(
165 "flatpak".to_string(),
166 "run".to_string(),
167 remote.r#type.to_cli(),
168 candidate.package.clone()
169 );
170
171 candidates.push(candidate);
172 }
173 }
174 }
175
176 Ok(candidates)
177 }
178
179 pub async fn check_installed_flatpaks(
184 &self,
185 env: &Arc<Environment>,
186 mut candidates: Vec<Candidate>,
187 ) -> Result<Vec<Candidate>, LocalFlatpaksError> {
188 let output = env
189 .output_of(cmd!(
190 "flatpak",
191 "list",
192 "--app",
193 "--columns=application:f,version:f,origin:f,installation:f,description:f"
194 ))
195 .await?;
196
197 for candidate in candidates.iter_mut() {
198 if output.lines().any(|line| {
199 let (origin, installation) = candidate
200 .origin
201 .trim_end_matches(')')
202 .split_once(" (")
203 .with_context(|| {
204 format!(
205 "failed to unparse package origin '{}' into origin and installation",
206 candidate.origin
207 )
208 })
209 .to_log()
210 .unwrap_or(("", ""));
211 line.contains(&candidate.package)
212 && line.contains(&candidate.version)
213 && line.contains(&candidate.description)
214 && line.contains(origin)
215 && line.contains(installation)
216 }) {
217 candidate.actions.install = None;
219 }
220 }
221
222 Ok(candidates)
223 }
224}
225
226#[derive(Debug, Default, PartialEq, Clone)]
228pub struct Remote {
229 name: String,
231 r#type: RemoteType,
233}
234
235impl Remote {
236 pub fn to_origin(&self) -> String {
238 self.to_string()
239 }
240}
241
242impl fmt::Display for Remote {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 write!(f, "{} ({})", self.name, self.r#type)
245 }
246}
247
248#[derive(Debug, Default, PartialEq, Clone)]
250pub enum RemoteType {
251 #[default]
252 User,
253 System,
254 Other(String),
256}
257
258impl RemoteType {
259 pub fn to_cli(&self) -> String {
261 match self {
262 Self::User => "--user".to_string(),
263 Self::System => "--system".to_string(),
264 Self::Other(val) => format!("--installation={}", val),
265 }
266 }
267}
268
269impl fmt::Display for RemoteType {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 match self {
272 Self::User => write!(f, "user"),
273 Self::System => write!(f, "system"),
274 Self::Other(val) => write!(f, "{}", val),
275 }
276 }
277}
278
279#[derive(Debug, ThisError)]
280pub enum Error {
281 #[error("failed to obtain remotes to search in")]
282 Remote(#[from] RemoteError),
283
284 #[error(transparent)]
285 LocalFlatpaks(#[from] LocalFlatpaksError),
286
287 #[error(transparent)]
288 RemoteFlatpaks(#[from] RemoteFlatpakError),
289}
290
291impl From<Error> for ProviderError {
292 fn from(value: Error) -> Self {
293 match value {
294 Error::Remote(RemoteError::Execution(ExecutionError::NotFound(val))) => {
295 ProviderError::Requirements(val)
296 }
297 _ => ProviderError::ApplicationError(anyhow::Error::new(value)),
298 }
299 }
300}
301
302#[derive(Debug, ThisError)]
304pub enum RemoteError {
305 #[error("no configured remote found")]
306 NoRemote,
307
308 #[error("failed to parse remote info from output: {0}")]
309 Parse(String),
310
311 #[error("failed to determine remote type from options: '{0}'")]
312 UnknownType(String),
313
314 #[error("failed to query configured flatpak remotes")]
315 Execution(#[from] ExecutionError),
316}
317
318#[derive(Debug, ThisError)]
319pub enum RemoteFlatpakError {
320 #[error("failed to query available applications from remote '{remote}'")]
321 Execution {
322 remote: Remote,
323 source: ExecutionError,
324 },
325}
326
327#[derive(Debug, ThisError)]
328pub enum LocalFlatpaksError {
329 #[error("failed to query locally installed flatpaks")]
330 Execution(#[from] ExecutionError),
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::test::prelude::*;
337
338 test::default_tests!(Flatpak::new());
339
340 #[test]
341 fn no_remotes() {
342 let query = quick_test!(Flatpak::new(), Ok("\n".to_string()));
343
344 assert::is_err!(query);
345 assert::err::application!(query);
346 }
347
348 #[test]
349 fn single_remote_nonexistent_app() {
350 let query = quick_test!(
351 Flatpak::new(),
352 Ok("foobar\tuser\n".to_string()),
354 Ok("app.cool.my\t0.12.56-beta\tfoobar\tSome descriptive text\n".to_string()),
356 Ok("\n".to_string())
358 );
359
360 assert::is_err!(query);
361 assert::err::not_found!(query);
362 }
363
364 #[test]
365 fn single_remote_matching_app() {
366 let query = quick_test!(
367 Flatpak::new(),
368 Ok("foobar\tuser\n".to_string()),
370 Ok("app.cool.my-test-app\t0.12.56-beta\tfoobar\tSome descriptive text\n".to_string()),
372 Ok("\n".to_string())
374 );
375
376 let result = query.results.unwrap();
377 assert_eq!(result.len(), 1);
378 assert_eq!(result[0].package, "app.cool.my-test-app".to_string());
379 assert_eq!(result[0].origin, "foobar (user)".to_string());
380 }
381}