diff --git a/Cargo.lock b/Cargo.lock
index 51d64ae..a183f2a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -287,6 +287,12 @@ dependencies = [
"stable_deref_trait",
]
+[[package]]
+name = "ascii"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
+
[[package]]
name = "ash"
version = "0.38.0+1.3.281"
@@ -789,6 +795,12 @@ version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
+[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
[[package]]
name = "bytemuck"
version = "1.24.0"
@@ -1471,14 +1483,17 @@ name = "dash-frontend"
version = "0.1.0"
dependencies = [
"anyhow",
+ "base64",
"chrono",
"gio 0.21.5",
"glam",
"gtk",
+ "keyvalues-parser",
"log",
"rust-embed",
"serde",
"serde_json",
+ "steam_shortcuts_util",
"wayvr-ipc",
"wgui",
"wlx-common",
@@ -3026,6 +3041,14 @@ dependencies = [
"ucd-trie",
]
+[[package]]
+name = "keyvalues-parser"
+version = "0.2.2"
+source = "git+https://github.com/CosmicHorrorDev/vdf-rs.git?rev=fc6dcbea9eb13cacb98dea40063f6f56cde6e145#fc6dcbea9eb13cacb98dea40063f6f56cde6e145"
+dependencies = [
+ "pest",
+]
+
[[package]]
name = "kurbo"
version = "0.11.3"
@@ -3446,6 +3469,17 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "nom_locate"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3"
+dependencies = [
+ "bytecount",
+ "memchr",
+ "nom 7.1.3",
+]
+
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
@@ -5269,6 +5303,18 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+[[package]]
+name = "steam_shortcuts_util"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0543ebdb23a93b196aceebc53f70cc5a573bb74248a974b3f5fa3883e6a89b6"
+dependencies = [
+ "ascii",
+ "crc32fast",
+ "nom 7.1.3",
+ "nom_locate",
+]
+
[[package]]
name = "strict-num"
version = "0.1.1"
diff --git a/dash-frontend/Cargo.toml b/dash-frontend/Cargo.toml
index 9bed73f..c1bf475 100644
--- a/dash-frontend/Cargo.toml
+++ b/dash-frontend/Cargo.toml
@@ -16,3 +16,6 @@ serde.workspace = true
serde_json.workspace = true
wlx-common = { path = "../wlx-common" }
wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
+base64 = "0.22.1"
+keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
+steam_shortcuts_util = "1.1.8"
diff --git a/dash-frontend/assets/gui/tab/games.xml b/dash-frontend/assets/gui/tab/games.xml
index 4a1fde2..2ee00a0 100644
--- a/dash-frontend/assets/gui/tab/games.xml
+++ b/dash-frontend/assets/gui/tab/games.xml
@@ -3,5 +3,6 @@
+
\ No newline at end of file
diff --git a/dash-frontend/assets/gui/view/game_list.xml b/dash-frontend/assets/gui/view/game_list.xml
new file mode 100644
index 0000000..eee5646
--- /dev/null
+++ b/dash-frontend/assets/gui/view/game_list.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/de.json b/dash-frontend/assets/lang/de.json
index dad7ee6..3ca23fa 100644
--- a/dash-frontend/assets/lang/de.json
+++ b/dash-frontend/assets/lang/de.json
@@ -64,5 +64,10 @@
"NO_WINDOWS_FOUND": "Keine Fenster gefunden",
"WINDOW_OPTIONS": "Fensteroptionen",
"APPLICATION_STARTED": "Anwendung gestartet",
- "LIST_OF_WINDOWS": "Fensterliste"
+ "LIST_OF_WINDOWS": "Fensterliste",
+ "CLOSE_WINDOW": "Fenster schließen",
+ "GAME_LIST": {
+ "NO_GAMES_FOUND": "Keine Spiele gefunden"
+ },
+ "TERMINATE_PROCESS": "Prozess beenden"
}
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json
index f5e84f1..858bdf5 100644
--- a/dash-frontend/assets/lang/en.json
+++ b/dash-frontend/assets/lang/en.json
@@ -41,6 +41,9 @@
},
"CLOSE_WINDOW": "Close window",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launcha application:",
+ "GAME_LIST": {
+ "NO_GAMES_FOUND": "No games found"
+ },
"GAMES": "Games",
"GENERAL_SETTINGS": "General settings",
"HEIGHT": "Height",
diff --git a/dash-frontend/assets/lang/es.json b/dash-frontend/assets/lang/es.json
index 194f80f..497fc6b 100644
--- a/dash-frontend/assets/lang/es.json
+++ b/dash-frontend/assets/lang/es.json
@@ -64,5 +64,10 @@
"NO_WINDOWS_FOUND": "No se encontraron ventanas",
"WINDOW_OPTIONS": "Opciones de ventana",
"APPLICATION_STARTED": "Aplicación iniciada",
- "LIST_OF_WINDOWS": "Lista de ventanas"
+ "LIST_OF_WINDOWS": "Lista de ventanas",
+ "CLOSE_WINDOW": "Cerrar ventana",
+ "GAME_LIST": {
+ "NO_GAMES_FOUND": "No se encontraron juegos"
+ },
+ "TERMINATE_PROCESS": "Finalizar proceso"
}
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/ja.json b/dash-frontend/assets/lang/ja.json
index 808dcd7..9be1c93 100644
--- a/dash-frontend/assets/lang/ja.json
+++ b/dash-frontend/assets/lang/ja.json
@@ -64,5 +64,10 @@
"NO_WINDOWS_FOUND": "ウィンドウが見つかりませんでした",
"WINDOW_OPTIONS": "ウィンドウオプション",
"APPLICATION_STARTED": "アプリケーションが起動しました",
- "LIST_OF_WINDOWS": "ウィンドウ一覧"
+ "LIST_OF_WINDOWS": "ウィンドウ一覧",
+ "CLOSE_WINDOW": "ウィンドウを閉じる",
+ "GAME_LIST": {
+ "NO_GAMES_FOUND": "ゲームが見つかりませんでした"
+ },
+ "TERMINATE_PROCESS": "プロセスを終了する"
}
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/pl.json b/dash-frontend/assets/lang/pl.json
index aafb4fa..304d832 100644
--- a/dash-frontend/assets/lang/pl.json
+++ b/dash-frontend/assets/lang/pl.json
@@ -64,5 +64,10 @@
"NO_WINDOWS_FOUND": "Nie znaleziono okien",
"WINDOW_OPTIONS": "Opcje okna",
"APPLICATION_STARTED": "Aplikacja uruchomiona",
- "LIST_OF_WINDOWS": "Lista okien"
+ "LIST_OF_WINDOWS": "Lista okien",
+ "CLOSE_WINDOW": "Zamknij okno",
+ "GAME_LIST": {
+ "NO_GAMES_FOUND": "Nie znaleziono gier"
+ },
+ "TERMINATE_PROCESS": "Zakończ proces"
}
\ No newline at end of file
diff --git a/dash-frontend/src/tab/games.rs b/dash-frontend/src/tab/games.rs
index 1a304c7..4bab982 100644
--- a/dash-frontend/src/tab/games.rs
+++ b/dash-frontend/src/tab/games.rs
@@ -1,19 +1,30 @@
use wgui::{
assets::AssetPath,
- parser::{ParseDocumentParams, ParserState},
+ parser::{Fetchable, ParseDocumentParams, ParserState},
};
-use crate::tab::{Tab, TabParams, TabType};
+use crate::{
+ tab::{Tab, TabParams, TabType},
+ views::game_list,
+};
pub struct TabGames {
#[allow(dead_code)]
pub state: ParserState,
+
+ view_game_list: game_list::View,
}
impl Tab for TabGames {
fn get_type(&self) -> TabType {
TabType::Games
}
+
+ fn update(&mut self, params: super::TabUpdateParams) -> anyhow::Result<()> {
+ self.view_game_list.update(params.layout, params.interface)?;
+
+ Ok(())
+ }
}
impl TabGames {
@@ -28,6 +39,15 @@ impl TabGames {
params.parent_id,
)?;
- Ok(Self { state })
+ let game_list_parent = state.get_widget_id("game_list_parent")?;
+
+ let view_game_list = game_list::View::new(game_list::Params {
+ frontend_tasks: params.frontend_tasks.clone(),
+ globals: params.globals,
+ layout: params.layout,
+ parent_id: game_list_parent,
+ })?;
+
+ Ok(Self { state, view_game_list })
}
}
diff --git a/dash-frontend/src/util/mod.rs b/dash-frontend/src/util/mod.rs
index 49c84fc..fb9588a 100644
--- a/dash-frontend/src/util/mod.rs
+++ b/dash-frontend/src/util/mod.rs
@@ -1,5 +1,6 @@
pub mod desktop_finder;
pub mod pactl_wrapper;
pub mod popup_manager;
+pub mod steam_utils;
pub mod toast_manager;
pub mod various;
diff --git a/dash-frontend/src/util/steam_utils.rs b/dash-frontend/src/util/steam_utils.rs
new file mode 100644
index 0000000..782ffae
--- /dev/null
+++ b/dash-frontend/src/util/steam_utils.rs
@@ -0,0 +1,408 @@
+use base64::{Engine as _, engine::general_purpose};
+use keyvalues_parser::{Obj, Vdf};
+use serde::{Deserialize, Serialize};
+use std::fs;
+use std::io::Read;
+use std::path::Path;
+use std::path::PathBuf;
+use steam_shortcuts_util::parse_shortcuts;
+
+pub struct SteamUtils {
+ steam_root: PathBuf,
+}
+
+fn get_steam_root() -> anyhow::Result {
+ let home = PathBuf::from(std::env::var("HOME")?);
+
+ let steam_paths: [&str; 3] = [
+ ".steam/steam",
+ ".steam/debian-installation",
+ ".var/app/com.valvesoftware.Steam/data/Steam",
+ ];
+ let Some(steam_path) = steam_paths
+ .iter()
+ .map(|path| home.join(path))
+ .filter(|p| p.exists())
+ .next()
+ else {
+ anyhow::bail!("Couldn't find Steam installation in search paths");
+ };
+
+ Ok(steam_path)
+}
+
+pub type AppID = String;
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct AppManifest {
+ pub app_id: AppID,
+ pub run_game_id: AppID,
+ pub name: String,
+ pub cover_b64: Option,
+ pub raw_state_flags: u64, // documentation: https://github.com/lutris/lutris/blob/master/docs/steam.rst
+ pub last_played: Option, // unix timestamp
+}
+
+pub enum GameSortMethod {
+ NameAsc,
+ NameDesc,
+ PlayDateDesc,
+}
+
+fn get_obj_first<'a>(obj: &'a Obj<'_>, key: &str) -> Option<&'a Obj<'a>> {
+ obj.get(key)?.first()?.get_obj()
+}
+
+fn get_str_first<'a>(obj: &'a Obj<'_>, key: &str) -> Option<&'a str> {
+ obj.get(key)?.first()?.get_str()
+}
+
+fn vdf_parse_libraryfolders<'a>(vdf_root: &'a Vdf<'a>) -> Option> {
+ let obj_libraryfolders = vdf_root.value.get_obj()?;
+
+ let mut res = Vec::::new();
+
+ let mut num = 0;
+ loop {
+ let Some(library_folder) = get_obj_first(obj_libraryfolders, format!("{}", num).as_str()) else {
+ // no more libraries to find
+ break;
+ };
+
+ let Some(apps) = get_obj_first(library_folder, "apps") else {
+ // no apps?
+ num += 1;
+ continue;
+ };
+
+ let Some(path) = get_str_first(library_folder, "path") else {
+ // no path?
+ num += 1;
+ continue;
+ };
+
+ //log::trace!("path: {}", path);
+
+ res.extend(
+ apps
+ .iter()
+ .filter_map(|item| item.0.parse::().ok())
+ .map(|app_id| AppEntry {
+ app_id: app_id.to_string(),
+ root_path: String::from(path),
+ }),
+ );
+
+ num += 1;
+ }
+
+ Some(res)
+}
+
+fn vdf_parse_appstate<'a>(app_id: AppID, vdf_root: &'a Vdf<'a>) -> Option {
+ let app_state_obj = vdf_root.value.get_obj()?;
+
+ let name = app_state_obj.get("name")?.first()?.get_str()?;
+
+ let raw_state_flags = app_state_obj
+ .get("StateFlags")?
+ .first()?
+ .get_str()?
+ .parse::()
+ .ok()?;
+
+ let last_played = match app_state_obj.get("LastPlayed") {
+ Some(s) => Some(s.first()?.get_str()?.parse::().ok()?),
+ None => None,
+ };
+
+ Some(AppManifest {
+ app_id: app_id.clone(),
+ run_game_id: app_id,
+ cover_b64: None,
+ name: String::from(name),
+ raw_state_flags,
+ last_played,
+ })
+}
+
+struct AppEntry {
+ pub root_path: String,
+ pub app_id: AppID,
+}
+
+pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
+ log::info!("Stopping Steam game with AppID {}", app_id);
+
+ for game in list_running_games()? {
+ if game.app_id != app_id {
+ continue;
+ }
+
+ log::info!("Killing process with PID {} and its children", game.pid);
+ let _ = std::process::Command::new("pkill")
+ .arg(if force_kill { "-9" } else { "-11" })
+ .arg("-P")
+ .arg(format!("{}", game.pid))
+ .spawn()?;
+ }
+ Ok(())
+}
+
+pub fn launch(app_id: AppID) -> anyhow::Result<()> {
+ log::info!("Launching Steam game with AppID {}", app_id);
+ call_steam(&format!("steam://rungameid/{}", app_id))?;
+ Ok(())
+}
+
+#[derive(Serialize)]
+pub struct RunningGame {
+ pub app_id: AppID,
+ pub pid: i32,
+}
+
+#[derive(Serialize)]
+struct Shortcut {
+ name: String,
+ exe: String,
+ run_game_id: u64,
+ app_id: u64,
+ cover_b64: Option,
+}
+
+pub fn list_running_games() -> anyhow::Result> {
+ let mut res = Vec::::new();
+
+ let entries = std::fs::read_dir("/proc")?;
+ for entry in entries.into_iter().flatten() {
+ let path_cmdline = entry.path().join("cmdline");
+ let Ok(cmdline) = std::fs::read(path_cmdline) else {
+ continue;
+ };
+
+ let proc_file_name = entry.file_name();
+ let Some(pid) = proc_file_name.to_str() else {
+ continue;
+ };
+
+ let Ok(pid) = pid.parse::() else {
+ continue;
+ };
+
+ let args: Vec<&str> = cmdline
+ .split(|byte| *byte == 0x00)
+ .filter_map(|arg| match std::str::from_utf8(arg) {
+ Ok(arg) => Some(arg),
+ Err(_) => None,
+ })
+ .collect();
+
+ let mut has_steam_launch = false;
+ for arg in &args {
+ if *arg == "SteamLaunch" {
+ has_steam_launch = true;
+ break;
+ }
+ }
+
+ if !has_steam_launch {
+ continue;
+ }
+
+ // Running game process found. Parse AppID
+ for arg in &args {
+ let pat = "AppId=";
+ let Some(pos) = arg.find(pat) else {
+ continue;
+ };
+
+ if pos != 0 {
+ continue;
+ }
+
+ let Some((_, second)) = arg.split_at_checked(pat.len()) else {
+ continue;
+ };
+
+ let Ok(app_id_num) = second.parse::() else {
+ continue;
+ };
+
+ // AppID found. Add it to the list
+ res.push(RunningGame {
+ app_id: app_id_num.to_string(),
+ pid,
+ });
+
+ break;
+ }
+ }
+
+ Ok(res)
+}
+
+fn call_steam(arg: &str) -> anyhow::Result<()> {
+ match std::process::Command::new("xdg-open").arg(arg).spawn() {
+ Ok(_) => Ok(()),
+ Err(_) => {
+ std::process::Command::new("steam").arg(arg).spawn()?;
+ Ok(())
+ }
+ }
+}
+
+fn shortcut_to_fake_manifest(shortcut: &Shortcut) -> AppManifest {
+ AppManifest {
+ app_id: shortcut.app_id.to_string(),
+ run_game_id: shortcut.run_game_id.to_string(),
+ name: shortcut.name.clone(),
+ cover_b64: shortcut.cover_b64.clone(),
+ raw_state_flags: 0, // Not applicable for shortcuts, 0 by default
+ last_played: None, // Steam does not use this for shortcuts
+ }
+}
+
+fn compute_rungameid(app_id: u32) -> u64 {
+ (app_id as u64) << 32 | 0x02000000
+}
+
+impl SteamUtils {
+ fn convert_cover_to_base64(app_id: &u32, original_path: &Path) -> std::io::Result