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