From 9da95a27b3869a157cc9aa14402f6e277db541be Mon Sep 17 00:00:00 2001 From: Albert Armea Date: Sat, 7 Feb 2026 16:22:55 -0500 Subject: [PATCH] Add "steam"-specific type This implementation allows each platform to choose how to launch Steam (on Linux, we use the snap as the examples suggested before), and keeps Steam alive after an activity exits so that save sync, game updates, etc. can continue to run. Change written by Codex 5.2 on medium: Consider this GitHub issue https://github.com/aarmea/shepherd-launcher/issues/4. On Linux, an activity that uses the "steam" type should launch Steam via the snap as shown in the example configuration in this repository. Go ahead and implement the feature. I'm expecting one of the tricky bits to be killing the activity while keeping Steam alive, as we can no longer just kill the Steam snap cgroup. --- config.example.toml | 10 +- crates/shepherd-api/src/types.rs | 13 ++ crates/shepherd-config/README.md | 3 + .../src/bin/validate-config.rs | 3 + crates/shepherd-config/src/policy.rs | 1 + crates/shepherd-config/src/schema.rs | 11 ++ crates/shepherd-config/src/validation.rs | 8 + crates/shepherd-host-api/README.md | 1 + crates/shepherd-host-api/src/capabilities.rs | 1 + crates/shepherd-host-linux/README.md | 15 ++ crates/shepherd-host-linux/src/adapter.rs | 184 +++++++++++++++--- crates/shepherd-host-linux/src/process.rs | 53 +++++ crates/shepherd-launcher-ui/src/tile.rs | 1 + 13 files changed, 271 insertions(+), 33 deletions(-) diff --git a/config.example.toml b/config.example.toml index b86eee8..90f54ca 100644 --- a/config.example.toml +++ b/config.example.toml @@ -180,9 +180,8 @@ label = "Celeste" icon = "~/Games/Icons/Celeste.png" [entries.kind] -type = "snap" -snap_name = "steam" -args = ["steam://rungameid/504230"] # Steam App ID (passed to 'snap run steam') +type = "steam" +app_id = 504230 # Steam App ID [entries.availability] [[entries.availability.windows]] @@ -206,9 +205,8 @@ label = "A Short Hike" icon = "~/Games/Icons/A_Short_Hike.png" [entries.kind] -type = "snap" -snap_name = "steam" -args = ["steam://rungameid/1055540"] # Steam App ID (passed to 'snap run steam') +type = "steam" +app_id = 1055540 # Steam App ID [entries.availability] [[entries.availability.windows]] diff --git a/crates/shepherd-api/src/types.rs b/crates/shepherd-api/src/types.rs index 20bb6df..fe43fc4 100644 --- a/crates/shepherd-api/src/types.rs +++ b/crates/shepherd-api/src/types.rs @@ -13,6 +13,7 @@ use std::time::Duration; pub enum EntryKindTag { Process, Snap, + Steam, Flatpak, Vm, Media, @@ -47,6 +48,17 @@ pub enum EntryKind { #[serde(default)] env: HashMap, }, + /// Steam game launched via the Steam snap (Linux) + Steam { + /// Steam App ID (e.g., 504230 for Celeste) + app_id: u32, + /// Additional command-line arguments passed to Steam + #[serde(default)] + args: Vec, + /// Additional environment variables + #[serde(default)] + env: HashMap, + }, /// Flatpak application - uses systemd scope-based process management Flatpak { /// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher") @@ -79,6 +91,7 @@ impl EntryKind { match self { EntryKind::Process { .. } => EntryKindTag::Process, EntryKind::Snap { .. } => EntryKindTag::Snap, + EntryKind::Steam { .. } => EntryKindTag::Steam, EntryKind::Flatpak { .. } => EntryKindTag::Flatpak, EntryKind::Vm { .. } => EntryKindTag::Vm, EntryKind::Media { .. } => EntryKindTag::Media, diff --git a/crates/shepherd-config/README.md b/crates/shepherd-config/README.md index 6c3c809..30450e2 100644 --- a/crates/shepherd-config/README.md +++ b/crates/shepherd-config/README.md @@ -110,6 +110,9 @@ kind = { type = "process", command = "/usr/bin/game", args = ["--fullscreen"] } # Snap application kind = { type = "snap", snap_name = "mc-installer" } +# Steam game (via Steam snap) +kind = { type = "steam", app_id = 504230 } + # Virtual machine (future) kind = { type = "vm", driver = "qemu", args = { disk = "game.qcow2" } } diff --git a/crates/shepherd-config/src/bin/validate-config.rs b/crates/shepherd-config/src/bin/validate-config.rs index 7d12688..7fd579f 100644 --- a/crates/shepherd-config/src/bin/validate-config.rs +++ b/crates/shepherd-config/src/bin/validate-config.rs @@ -54,6 +54,9 @@ fn main() -> ExitCode { EntryKind::Snap { snap_name, .. } => { format!("snap ({})", snap_name) } + EntryKind::Steam { app_id, .. } => { + format!("steam ({})", app_id) + } EntryKind::Flatpak { app_id, .. } => { format!("flatpak ({})", app_id) } diff --git a/crates/shepherd-config/src/policy.rs b/crates/shepherd-config/src/policy.rs index ce6d113..62dfd27 100644 --- a/crates/shepherd-config/src/policy.rs +++ b/crates/shepherd-config/src/policy.rs @@ -256,6 +256,7 @@ fn convert_entry_kind(raw: RawEntryKind) -> EntryKind { match raw { RawEntryKind::Process { command, args, env, cwd } => EntryKind::Process { command, args, env, cwd }, RawEntryKind::Snap { snap_name, command, args, env } => EntryKind::Snap { snap_name, command, args, env }, + RawEntryKind::Steam { app_id, args, env } => EntryKind::Steam { app_id, args, env }, RawEntryKind::Flatpak { app_id, args, env } => EntryKind::Flatpak { app_id, args, env }, RawEntryKind::Vm { driver, args } => EntryKind::Vm { driver, args }, RawEntryKind::Media { library_id, args } => EntryKind::Media { library_id, args }, diff --git a/crates/shepherd-config/src/schema.rs b/crates/shepherd-config/src/schema.rs index a68b4d1..6ee6aac 100644 --- a/crates/shepherd-config/src/schema.rs +++ b/crates/shepherd-config/src/schema.rs @@ -116,6 +116,17 @@ pub enum RawEntryKind { #[serde(default)] env: HashMap, }, + /// Steam game launched via the Steam snap (Linux) + Steam { + /// Steam App ID (e.g., 504230 for Celeste) + app_id: u32, + /// Additional command-line arguments passed to Steam + #[serde(default)] + args: Vec, + /// Additional environment variables + #[serde(default)] + env: HashMap, + }, /// Flatpak application - uses systemd scope-based process management Flatpak { /// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher") diff --git a/crates/shepherd-config/src/validation.rs b/crates/shepherd-config/src/validation.rs index 27dc3de..3c27645 100644 --- a/crates/shepherd-config/src/validation.rs +++ b/crates/shepherd-config/src/validation.rs @@ -71,6 +71,14 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec }); } } + RawEntryKind::Steam { app_id, .. } => { + if *app_id == 0 { + errors.push(ValidationError::EntryError { + entry_id: entry.id.clone(), + message: "app_id must be > 0".into(), + }); + } + } RawEntryKind::Flatpak { app_id, .. } => { if app_id.is_empty() { errors.push(ValidationError::EntryError { diff --git a/crates/shepherd-host-api/README.md b/crates/shepherd-host-api/README.md index 600c3ad..c608922 100644 --- a/crates/shepherd-host-api/README.md +++ b/crates/shepherd-host-api/README.md @@ -30,6 +30,7 @@ let caps = host.capabilities(); // Check supported entry kinds if caps.supports_kind(EntryKindTag::Process) { /* ... */ } if caps.supports_kind(EntryKindTag::Snap) { /* ... */ } +if caps.supports_kind(EntryKindTag::Steam) { /* ... */ } // Check enforcement capabilities if caps.can_kill_forcefully { /* Can use SIGKILL/TerminateProcess */ } diff --git a/crates/shepherd-host-api/src/capabilities.rs b/crates/shepherd-host-api/src/capabilities.rs index 8bfed89..d13c493 100644 --- a/crates/shepherd-host-api/src/capabilities.rs +++ b/crates/shepherd-host-api/src/capabilities.rs @@ -59,6 +59,7 @@ impl HostCapabilities { let mut spawn_kinds = HashSet::new(); spawn_kinds.insert(EntryKindTag::Process); spawn_kinds.insert(EntryKindTag::Snap); + spawn_kinds.insert(EntryKindTag::Steam); spawn_kinds.insert(EntryKindTag::Flatpak); spawn_kinds.insert(EntryKindTag::Vm); spawn_kinds.insert(EntryKindTag::Media); diff --git a/crates/shepherd-host-linux/README.md b/crates/shepherd-host-linux/README.md index 29af682..405df61 100644 --- a/crates/shepherd-host-linux/README.md +++ b/crates/shepherd-host-linux/README.md @@ -90,6 +90,21 @@ let entry_kind = EntryKind::Snap { let handle = host.spawn(session_id, &entry_kind, options).await?; ``` +### Spawning Steam Games + +Steam games are launched via the Steam snap: + +```rust +let entry_kind = EntryKind::Steam { + app_id: 504230, + args: vec![], + env: Default::default(), +}; + +// Spawns via: snap run steam steam://rungameid/504230 +let handle = host.spawn(session_id, &entry_kind, options).await?; +``` + ### Stopping Sessions ```rust diff --git a/crates/shepherd-host-linux/src/adapter.rs b/crates/shepherd-host-linux/src/adapter.rs index 43b8e06..62b9ad2 100644 --- a/crates/shepherd-host-linux/src/adapter.rs +++ b/crates/shepherd-host-linux/src/adapter.rs @@ -3,17 +3,20 @@ use async_trait::async_trait; use shepherd_api::EntryKind; use shepherd_host_api::{ - HostAdapter, HostCapabilities, HostError, HostEvent, HostHandlePayload, + ExitStatus, HostAdapter, HostCapabilities, HostError, HostEvent, HostHandlePayload, HostResult, HostSessionHandle, SpawnOptions, StopMode, }; use shepherd_util::SessionId; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::sync::mpsc; use tracing::{info, warn}; -use crate::process::{init, kill_by_command, kill_flatpak_cgroup, kill_snap_cgroup, ManagedProcess}; +use crate::process::{ + find_steam_game_pids, init, kill_by_command, kill_flatpak_cgroup, kill_snap_cgroup, + kill_steam_game_processes, ManagedProcess, +}; /// Expand `~` at the beginning of a path to the user's home directory fn expand_tilde(path: &str) -> String { @@ -40,6 +43,15 @@ struct SessionInfo { command_name: String, snap_name: Option, flatpak_app_id: Option, + steam_app_id: Option, +} + +#[derive(Clone, Debug)] +struct SteamSession { + pid: u32, + pgid: u32, + app_id: u32, + seen_game: bool, } /// Linux host adapter @@ -48,6 +60,7 @@ pub struct LinuxHost { processes: Arc>>, /// Track session info for killing session_info: Arc>>, + steam_sessions: Arc>>, event_tx: mpsc::UnboundedSender, event_rx: Arc>>>, } @@ -63,6 +76,7 @@ impl LinuxHost { capabilities: HostCapabilities::linux_full(), processes: Arc::new(Mutex::new(HashMap::new())), session_info: Arc::new(Mutex::new(HashMap::new())), + steam_sessions: Arc::new(Mutex::new(HashMap::new())), event_tx: tx, event_rx: Arc::new(Mutex::new(Some(rx))), } @@ -71,6 +85,7 @@ impl LinuxHost { /// Start the background process monitor pub fn start_monitor(&self) -> tokio::task::JoinHandle<()> { let processes = self.processes.clone(); + let steam_sessions = self.steam_sessions.clone(); let event_tx = self.event_tx.clone(); tokio::spawn(async move { @@ -78,13 +93,22 @@ impl LinuxHost { tokio::time::sleep(Duration::from_millis(100)).await; let mut exited = Vec::new(); + let steam_pids: HashSet = { + steam_sessions + .lock() + .unwrap() + .keys() + .cloned() + .collect() + }; { let mut procs = processes.lock().unwrap(); for (pid, proc) in procs.iter_mut() { match proc.try_wait() { Ok(Some(status)) => { - exited.push((*pid, proc.pgid, status)); + let is_steam = steam_pids.contains(pid); + exited.push((*pid, proc.pgid, status, is_steam)); } Ok(None) => {} Err(e) => { @@ -93,12 +117,16 @@ impl LinuxHost { } } - for (pid, _, _) in &exited { + for (pid, _, _, _) in &exited { procs.remove(pid); } } - for (pid, pgid, status) in exited { + for (pid, pgid, status, is_steam) in exited { + if is_steam { + info!(pid = pid, pgid = pgid, status = ?status, "Steam launch process exited"); + continue; + } info!(pid = pid, pgid = pgid, status = ?status, "Process exited - sending HostEvent::Exited"); // We don't have the session_id here, so we use a placeholder @@ -110,6 +138,50 @@ impl LinuxHost { let _ = event_tx.send(HostEvent::Exited { handle, status }); } + + // Track Steam sessions by Steam App ID instead of process exit + let steam_snapshot: Vec = { + steam_sessions + .lock() + .unwrap() + .values() + .cloned() + .collect() + }; + + let mut ended = Vec::new(); + + for session in &steam_snapshot { + let has_game = !find_steam_game_pids(session.app_id).is_empty(); + if has_game { + if let Some(mut map) = steam_sessions.lock().ok() { + if let Some(entry) = map.get_mut(&session.pid) { + entry.seen_game = true; + } + } + } else if session.seen_game { + ended.push((session.pid, session.pgid)); + } + } + + if !ended.is_empty() { + let mut map = steam_sessions.lock().unwrap(); + let mut procs = processes.lock().unwrap(); + + for (pid, pgid) in ended { + map.remove(&pid); + procs.remove(&pid); + + let handle = HostSessionHandle::new( + SessionId::new(), + HostHandlePayload::Linux { pid, pgid }, + ); + let _ = event_tx.send(HostEvent::Exited { + handle, + status: ExitStatus::success(), + }); + } + } } }) } @@ -133,15 +205,15 @@ impl HostAdapter for LinuxHost { entry_kind: &EntryKind, options: SpawnOptions, ) -> HostResult { - // Extract argv, env, cwd, snap_name, and flatpak_app_id based on entry kind - let (argv, env, cwd, snap_name, flatpak_app_id) = match entry_kind { + // Extract argv, env, cwd, snap_name, flatpak_app_id, and steam_app_id based on entry kind + let (argv, env, cwd, snap_name, flatpak_app_id, steam_app_id) = match entry_kind { EntryKind::Process { command, args, env, cwd } => { let mut argv = vec![expand_tilde(command)]; argv.extend(expand_args(args)); let expanded_cwd = cwd.as_ref().map(|c| { std::path::PathBuf::from(expand_tilde(&c.to_string_lossy())) }); - (argv, env.clone(), expanded_cwd, None, None) + (argv, env.clone(), expanded_cwd, None, None, None) } EntryKind::Snap { snap_name, command, args, env } => { // For snap apps, we need to use 'snap run ' to launch them. @@ -154,13 +226,24 @@ impl HostAdapter for LinuxHost { argv.push(cmd.clone()); } argv.extend(expand_args(args)); - (argv, env.clone(), None, Some(snap_name.clone()), None) + (argv, env.clone(), None, Some(snap_name.clone()), None, None) + } + EntryKind::Steam { app_id, args, env } => { + // Steam games are launched via the Steam snap: snap run steam steam://rungameid/ + let mut argv = vec![ + "snap".to_string(), + "run".to_string(), + "steam".to_string(), + format!("steam://rungameid/{}", app_id), + ]; + argv.extend(expand_args(args)); + (argv, env.clone(), None, None, None, Some(*app_id)) } EntryKind::Flatpak { app_id, args, env } => { // For Flatpak apps, we use 'flatpak run ' to launch them. let mut argv = vec!["flatpak".to_string(), "run".to_string(), app_id.clone()]; argv.extend(expand_args(args)); - (argv, env.clone(), None, None, Some(app_id.clone())) + (argv, env.clone(), None, None, Some(app_id.clone()), None) } EntryKind::Vm { driver, args } => { // Construct command line from VM driver @@ -173,13 +256,13 @@ impl HostAdapter for LinuxHost { argv.push(value.to_string()); } } - (argv, HashMap::new(), None, None, None) + (argv, HashMap::new(), None, None, None, None) } EntryKind::Media { library_id, args: _ } => { // For media, we'd typically launch a media player // This is a placeholder - real implementation would integrate with a player let argv = vec!["xdg-open".to_string(), expand_tilde(library_id)]; - (argv, HashMap::new(), None, None, None) + (argv, HashMap::new(), None, None, None, None) } EntryKind::Custom { type_name: _, payload: _ } => { return Err(HostError::UnsupportedKind); @@ -190,6 +273,8 @@ impl HostAdapter for LinuxHost { // For snap/flatpak apps, use the app name (not "snap"/"flatpak") to avoid killing unrelated processes let command_name = if let Some(ref snap) = snap_name { snap.clone() + } else if steam_app_id.is_some() { + "steam".to_string() } else if let Some(ref app_id) = flatpak_app_id { app_id.clone() } else { @@ -215,6 +300,7 @@ impl HostAdapter for LinuxHost { command_name: command_name.clone(), snap_name: snap_name.clone(), flatpak_app_id: flatpak_app_id.clone(), + steam_app_id, }; self.session_info.lock().unwrap().insert(session_id.clone(), session_info_entry); info!(session_id = %session_id, command = %command_name, snap = ?snap_name, flatpak = ?flatpak_app_id, "Tracking session info"); @@ -226,6 +312,18 @@ impl HostAdapter for LinuxHost { self.processes.lock().unwrap().insert(pid, proc); + if let Some(app_id) = steam_app_id { + self.steam_sessions.lock().unwrap().insert( + pid, + SteamSession { + pid, + pgid, + app_id, + seen_game: false, + }, + ); + } + info!(pid = pid, pgid = pgid, "Spawned process"); Ok(handle) @@ -256,6 +354,14 @@ impl HostAdapter for LinuxHost { if let Some(ref snap) = info.snap_name { kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGTERM); info!(snap = %snap, "Sent SIGTERM via snap cgroup"); + } else if let Some(app_id) = info.steam_app_id { + let _ = kill_steam_game_processes(app_id, nix::sys::signal::Signal::SIGTERM); + if let Some(mut map) = self.steam_sessions.lock().ok() { + if let Some(entry) = map.get_mut(&pid) { + entry.seen_game = true; + } + } + info!(steam_app_id = app_id, "Sent SIGTERM to Steam game processes"); } else if let Some(ref app_id) = info.flatpak_app_id { kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGTERM); info!(flatpak = %app_id, "Sent SIGTERM via flatpak cgroup"); @@ -265,9 +371,10 @@ impl HostAdapter for LinuxHost { info!(command = %info.command_name, "Sent SIGTERM via command name"); } } - - // Also send SIGTERM via process handle - { + + // Also send SIGTERM via process handle (skip for Steam sessions) + let is_steam = session_info.as_ref().and_then(|info| info.steam_app_id).is_some(); + if !is_steam { let procs = self.processes.lock().unwrap(); if let Some(p) = procs.get(&pid) { let _ = p.terminate(); @@ -283,6 +390,9 @@ impl HostAdapter for LinuxHost { if let Some(ref snap) = info.snap_name { kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL); info!(snap = %snap, "Sent SIGKILL via snap cgroup (timeout)"); + } else if let Some(app_id) = info.steam_app_id { + let _ = kill_steam_game_processes(app_id, nix::sys::signal::Signal::SIGKILL); + info!(steam_app_id = app_id, "Sent SIGKILL to Steam game processes (timeout)"); } else if let Some(ref app_id) = info.flatpak_app_id { kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL); info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup (timeout)"); @@ -291,17 +401,26 @@ impl HostAdapter for LinuxHost { info!(command = %info.command_name, "Sent SIGKILL via command name (timeout)"); } } - - // Also force kill via process handle - let procs = self.processes.lock().unwrap(); - if let Some(p) = procs.get(&pid) { - let _ = p.kill(); + + // Also force kill via process handle (skip for Steam sessions) + if !is_steam { + let procs = self.processes.lock().unwrap(); + if let Some(p) = procs.get(&pid) { + let _ = p.kill(); + } } break; } // Check if process is still running - let still_running = self.processes.lock().unwrap().contains_key(&pid); + let still_running = if is_steam { + let app_id = session_info.as_ref().and_then(|info| info.steam_app_id); + app_id + .map(|id| !find_steam_game_pids(id).is_empty()) + .unwrap_or(false) + } else { + self.processes.lock().unwrap().contains_key(&pid) + }; if !still_running { break; @@ -316,6 +435,14 @@ impl HostAdapter for LinuxHost { if let Some(ref snap) = info.snap_name { kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL); info!(snap = %snap, "Sent SIGKILL via snap cgroup"); + } else if let Some(app_id) = info.steam_app_id { + let _ = kill_steam_game_processes(app_id, nix::sys::signal::Signal::SIGKILL); + if let Some(mut map) = self.steam_sessions.lock().ok() { + if let Some(entry) = map.get_mut(&pid) { + entry.seen_game = true; + } + } + info!(steam_app_id = app_id, "Sent SIGKILL to Steam game processes"); } else if let Some(ref app_id) = info.flatpak_app_id { kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL); info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup"); @@ -324,11 +451,14 @@ impl HostAdapter for LinuxHost { info!(command = %info.command_name, "Sent SIGKILL via command name"); } } - - // Also force kill via process handle - let procs = self.processes.lock().unwrap(); - if let Some(p) = procs.get(&pid) { - let _ = p.kill(); + + // Also force kill via process handle (skip for Steam sessions) + let is_steam = session_info.as_ref().and_then(|info| info.steam_app_id).is_some(); + if !is_steam { + let procs = self.processes.lock().unwrap(); + if let Some(p) = procs.get(&pid) { + let _ = p.kill(); + } } } } diff --git a/crates/shepherd-host-linux/src/process.rs b/crates/shepherd-host-linux/src/process.rs index e4f54bd..b9cdd19 100644 --- a/crates/shepherd-host-linux/src/process.rs +++ b/crates/shepherd-host-linux/src/process.rs @@ -155,6 +155,59 @@ pub fn kill_flatpak_cgroup(app_id: &str, _signal: Signal) -> bool { stopped_any } +/// Find Steam game process IDs by Steam App ID (from environment variables) +pub fn find_steam_game_pids(app_id: u32) -> Vec { + let mut pids = Vec::new(); + let target = app_id.to_string(); + let keys = ["SteamAppId", "SteamAppID", "STEAM_APP_ID"]; + + if let Ok(entries) = std::fs::read_dir("/proc") { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if let Ok(pid) = name_str.parse::() { + let env_path = format!("/proc/{}/environ", pid); + let Ok(env_bytes) = std::fs::read(&env_path) else { + continue; + }; + + for var in env_bytes.split(|b| *b == 0) { + if var.is_empty() { + continue; + } + let Ok(var_str) = std::str::from_utf8(var) else { + continue; + }; + for key in &keys { + let prefix = format!("{}=", key); + if let Some(val) = var_str.strip_prefix(&prefix) { + if val == target { + pids.push(pid); + } + } + } + } + } + } + } + + pids +} + +/// Kill Steam game processes by Steam App ID +pub fn kill_steam_game_processes(app_id: u32, signal: Signal) -> bool { + let pids = find_steam_game_pids(app_id); + if pids.is_empty() { + return false; + } + + for pid in pids { + let _ = signal::kill(Pid::from_raw(pid), signal); + } + + true +} + /// Kill processes by command name using pkill pub fn kill_by_command(command_name: &str, signal: Signal) -> bool { let signal_name = match signal { diff --git a/crates/shepherd-launcher-ui/src/tile.rs b/crates/shepherd-launcher-ui/src/tile.rs index 6ab3fc0..c2b6ad7 100644 --- a/crates/shepherd-launcher-ui/src/tile.rs +++ b/crates/shepherd-launcher-ui/src/tile.rs @@ -79,6 +79,7 @@ impl LauncherTile { let fallback_icon = match entry.kind_tag { shepherd_api::EntryKindTag::Process => "application-x-executable", shepherd_api::EntryKindTag::Snap => "application-x-executable", + shepherd_api::EntryKindTag::Steam => "application-x-executable", shepherd_api::EntryKindTag::Flatpak => "application-x-executable", shepherd_api::EntryKindTag::Vm => "computer", shepherd_api::EntryKindTag::Media => "video-x-generic",