use gdk_pixbuf::Pixbuf; use log::{debug, error, info}; use search_provider::{ ResultID, ResultMeta, ResultMetaBuilder, SearchProvider, SearchProviderImpl, }; use serde::Deserialize; use serde_json::Error as JsonError; use std::{ collections::HashMap, fmt::Display, fs::{self, DirEntry}, io::{BufReader, Error as IoError}, path::Path, process::Command, result::Result as SResult, }; use thiserror::Error; use zbus::Result; const GAMEDATA_PATH: &str = "/home/jonas/.var/app/hu.kramo.Cartridges/data/cartridges/games/"; const STEAM_ICON_PATH: &str = "/home/jonas/.local/share/Steam/appcache/librarycache/"; #[derive(Error, Debug)] enum MainError { #[error(transparent)] Json(#[from] JsonError), #[error(transparent)] Io(#[from] IoError), } #[derive(Debug, Deserialize)] #[serde(untagged)] enum Execute { One(String), Two(String, String), } #[derive(Debug, Deserialize)] struct CartGame { executable: Execute, source: String, hidden: bool, name: String, game_id: String, developer: Option, removed: Option, blacklisted: Option, } impl CartGame { fn ignore(&self) -> bool { match (self.removed, self.blacklisted) { (Some(true), _) => true, (_, Some(true)) => true, _ => self.hidden, } } } #[derive(Debug)] enum Source { Steam(u32), Lutris, Other, } impl Default for Source { fn default() -> Self { Source::Other } } #[derive(Debug)] struct Game { cmd: String, args: Vec, name: String, source: Source, developer: Option, } impl Game { fn run(&self) { let output = match Command::new(&self.cmd).args(&self.args).output() { Ok(output) => { info!("Running {}", self); output } Err(err) => { error!("{}", err); return; } }; debug!("status: {}", output.status); } } impl Display for Game { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name) } } impl From for Game { fn from(value: CartGame) -> Self { let (cmd, args) = match value.executable { Execute::One(s) => { let mut ss = s.split_whitespace().map(|s| s.to_string()); let cmd = ss.next().unwrap(); let args = ss.collect(); (cmd, args) } Execute::Two(s1, s2) => { let cmd = s1; let args = s2.split_whitespace().map(|s| s.to_string()).collect(); (cmd, args) } }; let source = if value.source == "steam" { Source::Steam(value.game_id.split('_').last().unwrap().parse().unwrap()) } else if value.source.starts_with("lutris") { Source::Lutris } else { Source::default() }; Self { cmd, args, source, name: value.name, developer: value.developer, } } } async fn read_game_from_entry(entry: SResult) -> SResult { let fp = entry?.path(); let f = fs::File::open(fp)?; let rdr = BufReader::new(f); let game = serde_json::from_reader(rdr)?; Ok(game) } async fn find_imported_games() -> Result> { let pth: &Path = Path::new(GAMEDATA_PATH); let mut games: Vec = vec![]; let entries = match fs::read_dir(pth) { Ok(entries) => entries, Err(e) => { error!("Failed to read directory {pth:?}: {e}"); return Err(e.into()); } }; let entries = entries.map(read_game_from_entry); for entry in entries { let game = match entry.await { Ok(game) => game, Err(e) => { error!("Could not read game info: {e}"); continue; } }; games.push(game) } let games: HashMap = games .into_iter() .filter_map(|cgame| { if cgame.ignore() { return None; }; let game: Game = cgame.into(); Some((game.name.clone(), game)) }) .collect(); Ok(games) } fn get_icon(game: Game) -> Option { match game.source { Source::Steam(s_id) => { let icon_path = Path::new(STEAM_ICON_PATH).join(format!("{s_id}_icon.jpg")); Pixbuf::from_file(icon_path).ok() } Source::Lutris | Source::Other => None, } } #[derive(Debug)] struct Application { games: HashMap, } impl SearchProviderImpl for Application { fn activate_result(&self, identifier: ResultID, _terms: &[String], _timestamp: u32) { let result = match self.games.get(&identifier) { Some(g) => g, None => return, }; result.run(); } fn initial_result_set(&self, terms: &[String]) -> Vec { self.games .keys() .filter_map(|key| { if terms.iter().any(|t| key.starts_with(t)) { Some(key.to_string()) } else { None } }) .collect() } fn result_metas(&self, identifiers: &[ResultID]) -> Vec { identifiers .iter() .map(|id| { let game = self.games.get(id).unwrap(); let mut builder = ResultMetaBuilder::new(id.to_string(), &game.name); builder = match &game.developer { Some(dev) => builder.description(&format!("by {dev}")), None => builder, }; builder = match get_icon(game) { Some(ico) => builder.icon_data(&ico.into()), }; builder.build() }) .collect() } } #[tokio::main] async fn main() -> Result<()> { env_logger::init(); let games = find_imported_games().await.unwrap(); let app = Application { games }; let _provider = SearchProvider::new( app, "hu.kramo.Cartridges.SearchProvider", "/hu/kramo/Cartridges/SearchProvider", ) .await?; Ok(()) }