//! I tried to create a proper parser, but those abstractions turned out to be not-so-zero cost! //! Here's a simple version instead #![allow(clippy::similar_names)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::needless_pass_by_value)] use fork::daemon; use std::collections::{HashMap, HashSet}; use std::io::{self, prelude::*}; use std::time::{Duration, SystemTime}; fn read_file(p: impl AsRef) -> io::Result> { let p = p.as_ref().to_owned(); let mut vec = Vec::new(); let mut file = std::fs::File::open(p)?; file.read_to_end(&mut vec)?; Ok(vec) } fn read_file_s(p: impl AsRef) -> io::Result { let p = p.as_ref().to_owned(); let mut s = String::new(); let mut file = std::fs::File::open(p)?; std::io::Read::read_to_string(&mut file, &mut s)?; Ok(s) } fn write_file(p: impl AsRef, data: Vec) -> io::Result<()> { let p = p.as_ref().to_owned(); let mut file = std::fs::File::create(p)?; std::io::Write::write_all(&mut file, &data) } #[derive(Clone, Debug, PartialEq)] pub struct AppInfoEntry { pub app_id: u32, pub info_state: u32, pub last_updated: u32, pub pics_token: u64, pub text_vdf_sha1: [u8; 20], pub change_number: u32, pub info: HashMap, Value>, } #[derive(Clone, Debug, PartialEq)] pub struct AppInfo { pub magic: u32, pub universe: u32, pub entries: Vec, } #[derive(Clone, Debug, PartialEq)] pub enum Value { Map(HashMap, Value>), String(Vec), } #[allow(clippy::missing_const_for_fn)] impl Value { fn into_map(self) -> Option, Self>> { if let Self::Map(map) = self { Some(map) } else { None } } fn into_string(self) -> Option> { if let Self::String(s) = self { Some(s) } else { None } } } fn read_map(reader: &mut impl io::Read) -> io::Result, Value>> { let mut ret = HashMap::new(); let mut buf = [0u8]; let mut buf2 = [0u8; 2]; let mut buf4 = [0u8; 4]; let mut buf8 = [0u8; 8]; loop { reader.read_exact(&mut buf)?; let kind = buf[0]; if kind == 8 || kind == 11 { break Ok(ret); } let mut key = vec![]; loop { reader.read_exact(&mut buf)?; if buf[0] == 0 { break; } key.push(buf[0]); } #[allow(clippy::match_same_arms)] match kind { 0 => { ret.insert(key, Value::Map(read_map(reader)?)); } 1 => { let mut s = vec![]; loop { reader.read_exact(&mut buf)?; if buf[0] == 0 { break; } s.push(buf[0]); } ret.insert(key, Value::String(s)); } 2 => { reader.read_exact(&mut buf4)?; // ret.insert(key, Value::I32(i32::from_le_bytes(buf4)))?; } 3 => { reader.read_exact(&mut buf4)?; // ret.insert(key, Value::F32(f32::from_le_bytes(buf4)))?; } 4 => { reader.read_exact(&mut buf4)?; // ret.insert(key, Value::Pointer(i32::from_le_bytes(buf4)))?; } 5 => { let mut s = vec![0u16; 2]; loop { reader.read_exact(&mut buf2)?; if buf2 == [0u8, 0u8] { break; } s.extend_from_slice(&[u16::from_le_bytes(buf2)]); } // utf-8 is used instead of utf-16 here // ret.insert(key, Value::WideString(s))?; } 7 => { reader.read_exact(&mut buf8)?; // ret.insert(key, Value::U64(u64::from_le_bytes(buf8)))?; } 10 => { reader.read_exact(&mut buf8)?; // ret.insert(key, Value::I64(i64::from_le_bytes(buf8)))?; } n => panic!("invalid vdf data type: {n}"), } } } fn read_app_info(reader: &mut impl io::Read) -> io::Result { let mut buf4 = [0u8; 4]; // let mut buf8 = [0u8; 8]; // let mut buf20 = [0u8; 20]; let mut buf64 = [0u8; 64]; reader.read_exact(&mut buf4)?; assert_eq!(buf4, [0x28, 0x44, 0x56, 0x07]); reader.read_exact(&mut buf4)?; assert_eq!(u32::from_le_bytes(buf4), 1); let mut ret = AppInfo { magic: 0x0756_4428, universe: 1, entries: vec![], }; loop { reader.read_exact(&mut buf4)?; let app_id = u32::from_le_bytes(buf4); if app_id == 0 { break Ok(ret); } let mut entry = AppInfoEntry { app_id, info_state: 0, last_updated: 0, pics_token: 0, text_vdf_sha1: [0u8; 20], change_number: 0, info: HashMap::new(), }; reader.read_exact(&mut buf64[..4 * 3 + 8 + 20 + 4 + 20])?; // reader.read_exact(&mut buf4)?; // size // reader.read_exact(&mut buf4)?; // entry.info_state = u32::from_le_bytes(buf4); // reader.read_exact(&mut buf4)?; // entry.last_updated = u32::from_le_bytes(buf4); // reader.read_exact(&mut buf8)?; // entry.pics_token = u64::from_le_bytes(buf8); // reader.read_exact(&mut buf20)?; // entry.text_vdf_sha1 = buf20; // reader.read_exact(&mut buf4)?; // entry.change_number = u32::from_le_bytes(buf4); // reader.read_exact(&mut buf20)?; // bin sha1 entry.info = read_map(reader)?; ret.entries.push(entry); } } fn home() -> String { std::env::var("HOME").unwrap() } fn xdg_home() -> String { std::env::var("XDG_DATA_HOME").unwrap_or_else(|_| home() + "/.local/share") } fn xdg_cache() -> String { std::env::var("XDG_CACHE_HOME").unwrap_or_else(|_| home() + "/.cache") } fn cache_dir() -> String { let dir = xdg_cache() + "/rofi-steam-game-list"; let _ = std::fs::create_dir_all(&dir); dir } fn history_dir() -> String { let dir = xdg_home() + "/rofi-steam-game-list"; let _ = std::fs::create_dir_all(&dir); dir } fn history(k: &str) -> HashMap { let dir = history_dir(); let mut ret = HashMap::new(); let Ok(data) = read_file(dir + "/history_" + k) else { return ret; }; if data.len() < 8 { return ret; } let count = u32::from_le_bytes(data[4..8].try_into().unwrap()); let data = &mut &data[8..]; let mut buf4 = [0u8; 4]; for _ in 0..count { if std::io::Read::read_exact(data, &mut buf4).is_err() { return ret; } let k = u32::from_le_bytes(buf4); if std::io::Read::read_exact(data, &mut buf4).is_err() { return ret; } let v = u32::from_le_bytes(buf4); ret.insert(k, v); } ret } fn write_history(m: &HashMap, k: &str) { let dir = history_dir(); let mut data = vec![]; data.extend_from_slice(&[0; 4]); data.extend_from_slice(&(m.len() as u32).to_le_bytes()); for (k, v) in m.iter() { data.extend_from_slice(&k.to_le_bytes()); data.extend_from_slice(&v.to_le_bytes()); } let _ = write_file(dir + "/history_" + k, data); } fn read_time(s: String) -> io::Result { std::fs::metadata(s + "/Steam/appcache/appinfo.vdf")?.modified() } fn read_appinfo(target_type: &str, s: String) -> io::Result<(SystemTime, Vec<(u32, String)>)> { let time = read_time(s.clone())?; let vec = read_file(s + "/Steam/appcache/appinfo.vdf")?; let data = read_app_info(&mut &vec[..])?; let mut ret = Vec::new(); let target_types = target_type.split(',').collect::>(); for mut info in data.entries { if let Some(mut x) = info .info .remove(&b"appinfo"[..]) .and_then(Value::into_map) .and_then(|mut x| x.remove(&b"common"[..])) .and_then(Value::into_map) { if let Some(mut t) = x .remove(&b"type"[..]) .and_then(Value::into_string) .and_then(|x| String::from_utf8(x).ok()) { if let Some(n) = x .remove(&b"name"[..]) .and_then(Value::into_string) .and_then(|x| String::from_utf8(x).ok()) { t.make_ascii_lowercase(); if target_types.contains(t.as_str()) { ret.push((info.app_id, n)); } } } } } Ok((time, ret)) } fn list_appids(s: &str) -> HashSet { let Ok(data) = read_file_s(s.to_owned() + "/Steam/steamapps/libraryfolders.vdf") else { return HashSet::new(); }; let ret = keyvalues_parser::Vdf::parse(&data) .unwrap() .value .get_obj() .unwrap() .values() .flat_map(|x| { x.iter().flat_map(|x| { x.get_obj().unwrap().get("apps").into_iter().flat_map(|x| { x.iter().flat_map(|x| { x.get_obj() .unwrap() .keys() .filter_map(|x| x.parse::().ok()) }) }) }) }) .collect::>(); ret } fn read_cache(k: &str) -> io::Result<(SystemTime, Vec<(u32, String)>)> { let path = cache_dir() + "/type_" + k; let mut file = std::fs::File::open(path)?; let mut data = Vec::new(); file.read_to_end(&mut data)?; if data.len() >= 16 { let (_, data) = data.split_at(4); let (first, data) = data.split_at(8); let time = SystemTime::UNIX_EPOCH + Duration::from_millis(u64::from_le_bytes(first.try_into().unwrap())); let (first, data) = data.split_at(4); let count = u32::from_le_bytes(first.try_into().unwrap()); let data = &mut &data[..]; let mut buf4 = [0u8; 4]; let mut buf1 = [0u8; 1]; let mut ret = Vec::with_capacity(count as usize); for _ in 0..count { std::io::Read::read_exact(data, &mut buf4)?; std::io::Read::read_exact(data, &mut buf1)?; let len = if buf1[0] == 255 { std::io::Read::read_exact(data, &mut buf1)?; 255 + buf1[0] as usize } else { buf1[0] as usize }; let mut buf = vec![0; len]; std::io::Read::read_exact(data, &mut buf)?; if let Ok(s) = String::from_utf8(buf) { ret.push((u32::from_le_bytes(buf4), s)); } } Ok((time, ret)) } else { Err(std::io::Error::new( std::io::ErrorKind::Other, "invalid app id cache format", )) } } fn write_cache(k: &str, time: SystemTime, ids: &[(u32, String)]) { let mut data = Vec::new(); data.extend_from_slice(&[0; 4]); data.extend_from_slice( &(time .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_millis() as u64) .to_le_bytes(), ); data.extend_from_slice(&(ids.len() as u32).to_le_bytes()); for (id, s) in ids { if s.len() > u8::MAX as usize + u8::MAX as usize { continue; } data.extend_from_slice(&id.to_le_bytes()); if s.len() > u8::MAX as usize { data.extend_from_slice(&255u8.to_le_bytes()); data.extend_from_slice(&((s.len() - u8::MAX as usize) as u8).to_le_bytes()); } else { data.extend_from_slice(&(s.len() as u8).to_le_bytes()); } data.extend_from_slice(s.as_bytes()); } let path = cache_dir() + "/type_" + k; if let Ok(mut file) = std::fs::File::create(path) { let _ = file.write_all(&data); } } fn main() { let target_type = std::env::var("STEAM_GAME_LIST_TYPE").map_or_else( |_| "game,application".to_owned(), |mut x| { x.make_ascii_lowercase(); x }, ); if let Ok(appid) = std::env::var("ROFI_INFO") { let _ = daemon(true, false); let mut cmd = std::process::Command::new("xdg-open") .arg(&format!("steam://rungameid/{appid}")) .spawn() .unwrap(); if let Ok(x) = appid.parse::() { let mut history = history(&target_type); history.entry(x).and_modify(|curr| *curr += 1).or_insert(1); write_history(&history, &target_type); } let _ = cmd.wait(); return; } let xdg_home = xdg_home(); let target_type2 = target_type.clone(); let target_type3 = target_type.clone(); let history_thread = std::thread::spawn(move || history(&target_type2)); let cache_thread = std::thread::spawn(move || { read_cache(&target_type3).ok() }); let installed_games = list_appids(&xdg_home); let history = history_thread.join().unwrap(); let mut time1 = None; if let Some((time, app_info)) = cache_thread.join().ok().flatten() { time1 = Some(time); let mut app_info_2 = app_info .iter() .filter_map(|x| { if installed_games.contains(&x.0) { Some(x.clone()) } else { None } }) .collect::>(); app_info_2.sort_by_key(|x| u32::MAX - history.get(&x.0).unwrap_or(&0)); let mut stdout = std::io::stdout().lock(); for (app_id, n) in &app_info_2 { let icon = format!("{xdg_home}/Steam/appcache/librarycache/{app_id}_icon.jpg"); if std::fs::metadata(&icon).is_ok() { writeln!(stdout, "{n}\0info\x1f{app_id}\x1ficon\x1f{icon}").unwrap(); } else { writeln!(stdout, "{n}\0info\x1f{app_id}").unwrap(); } } } let _ = daemon(true, false); if matches!(time1, Some(time) if read_time(xdg_home.clone()).unwrap() <= time) { return; } if let Ok((time, app_info)) = read_appinfo(&target_type, xdg_home) { write_cache(&target_type, time, &app_info); } }