use crate::provider::prelude::*;
#[derive(Debug, Default, PartialEq)]
pub struct Flatpak {}
impl Flatpak {
pub fn new() -> Self {
Default::default()
}
}
impl fmt::Display for Flatpak {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Flatpak")
}
}
#[async_trait]
impl IsProvider for Flatpak {
async fn search_internal(
&self,
command: &str,
target_env: Arc<Environment>,
) -> ProviderResult<Vec<Candidate>> {
let mut state = FlatpakState::default();
state
.discover_remotes(&target_env)
.await
.map_err(|e| match e {
RemoteError::Execution(ExecutionError::NotFound(val)) => {
ProviderError::Requirements(val)
}
RemoteError::Execution(e) => ProviderError::Execution(e),
err => ProviderError::ApplicationError(anyhow::Error::new(err)),
})?;
let candidates = state
.get_remote_flatpaks(&target_env, command)
.await
.map_err(Error::RemoteFlatpaks)?;
state
.check_installed_flatpaks(&target_env, candidates)
.await
.map_err(Error::LocalFlatpaks)
.map_err(|e| e.into())
}
}
#[derive(Debug, Default)]
struct FlatpakState {
remotes: Vec<Remote>,
}
impl FlatpakState {
pub async fn discover_remotes(&mut self, env: &Arc<Environment>) -> Result<(), RemoteError> {
let stdout = env
.output_of(cmd!("flatpak", "remotes", "--columns=name:f,options:f"))
.await?;
if stdout.trim().is_empty() {
return Err(RemoteError::NoRemote);
}
for line in stdout.lines() {
match line.split_once('\t') {
Some((name, opts)) => {
let remote_type = if opts.contains("user") {
RemoteType::User
} else if opts.contains("system") {
RemoteType::System
} else {
return Err(RemoteError::UnknownType(opts.to_string()));
};
let remote = Remote {
name: name.to_string(),
r#type: remote_type,
};
self.remotes.push(remote);
}
None => return Err(RemoteError::Parse(stdout)),
}
}
Ok(())
}
pub async fn get_remote_flatpaks(
&mut self,
env: &Arc<Environment>,
search_for: &str,
) -> Result<Vec<Candidate>, RemoteFlatpakError> {
debug_assert!(
!self.remotes.is_empty(),
"cannot query remote flatpaks without a remote"
);
let mut candidates: Vec<Candidate> = vec![];
for remote in &self.remotes {
let output = env
.output_of(cmd!(
"flatpak",
"remote-ls",
"--app",
"--cached",
"--columns=application:f,version:f,origin:f,description:f",
&remote.r#type.to_cli(),
&remote.name
))
.await
.map_err(|e| RemoteFlatpakError::Execution {
remote: remote.clone(),
source: e,
})?;
'next_line: for line in output.lines() {
let mut candidate = Candidate::default();
for (index, split) in line.split('\t').enumerate() {
match index {
0 => {
if split.to_lowercase().contains(&search_for.to_lowercase()) {
candidate.package = split.to_string();
} else {
continue 'next_line;
}
}
1 => candidate.version = split.to_string(),
2 => candidate.origin = remote.to_origin(),
3 => candidate.description = split.to_string(),
_ => log::warn!("superfluous fragment '{}' in line '{}'", split, line),
}
}
if !candidate.package.is_empty() {
let mut install_action = cmd!(
"flatpak",
"install",
"--app",
&remote.r#type.to_cli(),
&remote.name,
&candidate.package
);
install_action.needs_privileges(matches!(remote.r#type, RemoteType::System));
candidate.actions.install = Some(install_action);
candidate.actions.execute = cmd!(
"flatpak".to_string(),
"run".to_string(),
remote.r#type.to_cli(),
candidate.package.clone()
);
candidates.push(candidate);
}
}
}
Ok(candidates)
}
pub async fn check_installed_flatpaks(
&self,
env: &Arc<Environment>,
mut candidates: Vec<Candidate>,
) -> Result<Vec<Candidate>, LocalFlatpaksError> {
let output = env
.output_of(cmd!(
"flatpak",
"list",
"--app",
"--columns=application:f,version:f,origin:f,installation:f,description:f"
))
.await?;
for candidate in candidates.iter_mut() {
if output.lines().any(|line| {
let (origin, installation) = candidate
.origin
.trim_end_matches(')')
.split_once(" (")
.with_context(|| {
format!(
"failed to unparse package origin '{}' into origin and installation",
candidate.origin
)
})
.to_log()
.unwrap_or(("", ""));
line.contains(&candidate.package)
&& line.contains(&candidate.version)
&& line.contains(&candidate.description)
&& line.contains(origin)
&& line.contains(installation)
}) {
candidate.actions.install = None;
}
}
Ok(candidates)
}
}
#[derive(Debug, Default, PartialEq, Clone)]
pub struct Remote {
name: String,
r#type: RemoteType,
}
impl Remote {
pub fn to_origin(&self) -> String {
self.to_string()
}
}
impl fmt::Display for Remote {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.name, self.r#type)
}
}
#[derive(Debug, Default, PartialEq, Clone)]
pub enum RemoteType {
#[default]
User,
System,
Other(String),
}
impl RemoteType {
pub fn to_cli(&self) -> String {
match self {
Self::User => "--user".to_string(),
Self::System => "--system".to_string(),
Self::Other(val) => format!("--installation={}", val),
}
}
}
impl fmt::Display for RemoteType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::User => write!(f, "user"),
Self::System => write!(f, "system"),
Self::Other(val) => write!(f, "{}", val),
}
}
}
#[derive(Debug, ThisError)]
pub enum Error {
#[error("failed to obtain remotes to search in")]
Remote(#[from] RemoteError),
#[error(transparent)]
LocalFlatpaks(#[from] LocalFlatpaksError),
#[error(transparent)]
RemoteFlatpaks(#[from] RemoteFlatpakError),
}
impl From<Error> for ProviderError {
fn from(value: Error) -> Self {
match value {
Error::Remote(RemoteError::Execution(ExecutionError::NotFound(val))) => {
ProviderError::Requirements(val)
}
_ => ProviderError::ApplicationError(anyhow::Error::new(value)),
}
}
}
#[derive(Debug, ThisError)]
pub enum RemoteError {
#[error("no configured remote found")]
NoRemote,
#[error("failed to parse remote info from output: {0}")]
Parse(String),
#[error("failed to determine remote type from options: '{0}'")]
UnknownType(String),
#[error("failed to query configured flatpak remotes")]
Execution(#[from] ExecutionError),
}
#[derive(Debug, ThisError)]
pub enum RemoteFlatpakError {
#[error("failed to query available applications from remote '{remote}'")]
Execution {
remote: Remote,
source: ExecutionError,
},
}
#[derive(Debug, ThisError)]
pub enum LocalFlatpaksError {
#[error("failed to query locally installed flatpaks")]
Execution(#[from] ExecutionError),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::prelude::*;
test::default_tests!(Flatpak::new());
#[test]
fn no_remotes() {
let query = quick_test!(Flatpak::new(), Ok("\n".to_string()));
assert::is_err!(query);
assert::err::application!(query);
}
#[test]
fn single_remote_nonexistent_app() {
let query = quick_test!(
Flatpak::new(),
Ok("foobar\tuser\n".to_string()),
Ok("app.cool.my\t0.12.56-beta\tfoobar\tSome descriptive text\n".to_string()),
Ok("\n".to_string())
);
assert::is_err!(query);
assert::err::not_found!(query);
}
#[test]
fn single_remote_matching_app() {
let query = quick_test!(
Flatpak::new(),
Ok("foobar\tuser\n".to_string()),
Ok("app.cool.my-test-app\t0.12.56-beta\tfoobar\tSome descriptive text\n".to_string()),
Ok("\n".to_string())
);
let result = query.results.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].package, "app.cool.my-test-app".to_string());
assert_eq!(result[0].origin, "foobar (user)".to_string());
}
}