Add per-game overlay toggles (gamemode, mangohud, gamescope)
Games live under each Launcher as a Vec<Game>; the launcher itself never picks up overlays, only games launched through `play` do. - config: Game struct with gamemode/mangohud/gamescope fields, plus add_game / remove_game / set_game_flags methods. - launcher::play_game wraps the game command as `gamescope [args] -- gamemoderun umu-run <exe>` (each layer optional) and sets MANGOHUD=1 when enabled. - CLI: `play`, `games`, `config add-game`, `config remove-game`, `config set-game-flags`. - tray: per-game submenus with Play + checkmark toggles for GameMode / MangoHud / Gamescope; toggles persist to disk immediately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+152
@@ -24,6 +24,10 @@ pub struct Launcher {
|
||||
/// Config::proton_version).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub proton_version: Option<String>,
|
||||
/// Games installed through this launcher. Overlays (gamemode, mangohud,
|
||||
/// gamescope) only apply to games — the launcher itself always runs bare.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub games: Vec<Game>,
|
||||
}
|
||||
|
||||
impl Launcher {
|
||||
@@ -31,6 +35,40 @@ impl Launcher {
|
||||
pub fn full_exe_path(&self) -> PathBuf {
|
||||
self.prefix_dir.join("drive_c").join(&self.exe_path)
|
||||
}
|
||||
|
||||
pub fn find_game(&self, name: &str) -> Option<&Game> {
|
||||
self.games.iter().find(|g| g.name == name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Game {
|
||||
/// Short CLI name (e.g. "overwatch").
|
||||
pub name: String,
|
||||
/// Display name for tray / menus.
|
||||
pub display: String,
|
||||
/// Path to the game exe relative to the launcher's prefix `drive_c/`.
|
||||
pub exe_path: PathBuf,
|
||||
/// Optional extra args passed to the game exe after umu-run.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub args: Vec<String>,
|
||||
/// Wrap the game in `gamemoderun`.
|
||||
#[serde(default)]
|
||||
pub gamemode: bool,
|
||||
/// Set `MANGOHUD=1` for the game process.
|
||||
#[serde(default)]
|
||||
pub mangohud: bool,
|
||||
/// Wrap the game in `gamescope`. `None` = disabled; `Some(vec)` = enabled
|
||||
/// with those CLI args (empty vec = gamescope defaults).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gamescope: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
/// Absolute path to the game exe inside the launcher's prefix.
|
||||
pub fn full_exe_path(&self, launcher: &Launcher) -> PathBuf {
|
||||
launcher.prefix_dir.join("drive_c").join(&self.exe_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -93,6 +131,7 @@ pub fn presets() -> Vec<Launcher> {
|
||||
process_pattern: r"Battle\.net".into(),
|
||||
installer_url: None,
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
Launcher {
|
||||
name: "eaapp".into(),
|
||||
@@ -105,6 +144,7 @@ pub fn presets() -> Vec<Launcher> {
|
||||
process_pattern: r"EADesktop\.exe".into(),
|
||||
installer_url: None,
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
Launcher {
|
||||
name: "epic".into(),
|
||||
@@ -117,6 +157,7 @@ pub fn presets() -> Vec<Launcher> {
|
||||
process_pattern: r"EpicGamesLauncher\.exe".into(),
|
||||
installer_url: None,
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
Launcher {
|
||||
name: "ubisoft".into(),
|
||||
@@ -129,6 +170,7 @@ pub fn presets() -> Vec<Launcher> {
|
||||
process_pattern: r"UbisoftConnect\.exe|upc\.exe".into(),
|
||||
installer_url: None,
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
Launcher {
|
||||
name: "gog".into(),
|
||||
@@ -141,6 +183,7 @@ pub fn presets() -> Vec<Launcher> {
|
||||
process_pattern: r"GalaxyClient\.exe".into(),
|
||||
installer_url: None,
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
Launcher {
|
||||
name: "rockstar".into(),
|
||||
@@ -153,6 +196,7 @@ pub fn presets() -> Vec<Launcher> {
|
||||
process_pattern: r"Rockstar Games.*Launcher\.exe".into(),
|
||||
installer_url: None,
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -280,12 +324,120 @@ impl Config {
|
||||
process_pattern,
|
||||
installer_url,
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
});
|
||||
self.save()?;
|
||||
println!("\x1b[1;32m✓\x1b[0m Added launcher '{name}'.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn add_game(
|
||||
&mut self,
|
||||
launcher: &str,
|
||||
name: String,
|
||||
display: Option<String>,
|
||||
exe_path: PathBuf,
|
||||
gamemode: bool,
|
||||
mangohud: bool,
|
||||
gamescope: Option<Vec<String>>,
|
||||
) -> Result<()> {
|
||||
let l = self
|
||||
.launchers
|
||||
.iter_mut()
|
||||
.find(|l| l.name == launcher)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("no launcher named '{launcher}'")
|
||||
})?;
|
||||
if l.games.iter().any(|g| g.name == name) {
|
||||
anyhow::bail!(
|
||||
"launcher '{launcher}' already has a game named '{name}'"
|
||||
);
|
||||
}
|
||||
let display = display.unwrap_or_else(|| name.clone());
|
||||
l.games.push(Game {
|
||||
name: name.clone(),
|
||||
display,
|
||||
exe_path,
|
||||
args: vec![],
|
||||
gamemode,
|
||||
mangohud,
|
||||
gamescope,
|
||||
});
|
||||
self.save()?;
|
||||
println!(
|
||||
"\x1b[1;32m✓\x1b[0m Added game '{name}' under launcher '{launcher}'."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_game(&mut self, launcher: &str, name: &str) -> Result<()> {
|
||||
let l = self
|
||||
.launchers
|
||||
.iter_mut()
|
||||
.find(|l| l.name == launcher)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("no launcher named '{launcher}'")
|
||||
})?;
|
||||
let before = l.games.len();
|
||||
l.games.retain(|g| g.name != name);
|
||||
if l.games.len() == before {
|
||||
anyhow::bail!(
|
||||
"launcher '{launcher}' has no game named '{name}'"
|
||||
);
|
||||
}
|
||||
self.save()?;
|
||||
println!("\x1b[1;32m✓\x1b[0m Removed game '{name}' from '{launcher}'.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update per-game overlay flags. Each arg is `None` = leave as-is.
|
||||
/// `gamescope = Some(None)` disables it; `Some(Some(vec))` enables with args.
|
||||
pub fn set_game_flags(
|
||||
&mut self,
|
||||
launcher: &str,
|
||||
name: &str,
|
||||
gamemode: Option<bool>,
|
||||
mangohud: Option<bool>,
|
||||
gamescope: Option<Option<Vec<String>>>,
|
||||
) -> Result<()> {
|
||||
if gamemode.is_none() && mangohud.is_none() && gamescope.is_none() {
|
||||
anyhow::bail!(
|
||||
"nothing to set — pass --gamemode, --mangohud, --gamescope, or --no-gamescope"
|
||||
);
|
||||
}
|
||||
let l = self
|
||||
.launchers
|
||||
.iter_mut()
|
||||
.find(|l| l.name == launcher)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("no launcher named '{launcher}'")
|
||||
})?;
|
||||
let g = l
|
||||
.games
|
||||
.iter_mut()
|
||||
.find(|g| g.name == name)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"launcher '{launcher}' has no game named '{name}'"
|
||||
)
|
||||
})?;
|
||||
if let Some(v) = gamemode {
|
||||
g.gamemode = v;
|
||||
}
|
||||
if let Some(v) = mangohud {
|
||||
g.mangohud = v;
|
||||
}
|
||||
if let Some(v) = gamescope {
|
||||
g.gamescope = v;
|
||||
}
|
||||
self.save()?;
|
||||
println!(
|
||||
"\x1b[1;32m✓\x1b[0m Updated flags for '{launcher}/{name}'."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_launcher(&mut self, name: &str) -> Result<()> {
|
||||
let before = self.launchers.len();
|
||||
self.launchers.retain(|l| l.name != name);
|
||||
|
||||
+70
-1
@@ -1,5 +1,7 @@
|
||||
use crate::config::{Config, Launcher};
|
||||
use crate::config::{Config, Game, Launcher};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
@@ -42,6 +44,73 @@ pub fn launch(config: &Config, launcher: &Launcher) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Launch a game installed through `launcher`, wrapped in the per-game
|
||||
/// overlays (gamescope, gamemoderun, MANGOHUD). The launcher itself is
|
||||
/// never wrapped — only games run through this path pick up overlays.
|
||||
pub fn play_game(config: &Config, launcher: &Launcher, game: &Game) -> Result<()> {
|
||||
let exe = game.full_exe_path(launcher);
|
||||
if !exe.exists() {
|
||||
bail!(
|
||||
"game exe not found at {:?}\n\
|
||||
Check exe_path for '{}/{}' in config, or install the game via the launcher first.",
|
||||
exe,
|
||||
launcher.name,
|
||||
game.name,
|
||||
);
|
||||
}
|
||||
|
||||
let version = launcher
|
||||
.proton_version
|
||||
.as_deref()
|
||||
.unwrap_or(&config.proton_version);
|
||||
let proton_path: OsString = if version == "GE-Proton" {
|
||||
version.to_string().into()
|
||||
} else {
|
||||
config.proton_compat_dir.join(version).into_os_string()
|
||||
};
|
||||
|
||||
let (prog, args) = build_wrapped_argv(&exe, game);
|
||||
|
||||
let mut cmd = std::process::Command::new(&prog);
|
||||
cmd.env("WINEPREFIX", &launcher.prefix_dir)
|
||||
.env("GAMEID", &launcher.gameid)
|
||||
.env("PROTONPATH", &proton_path);
|
||||
if game.mangohud {
|
||||
cmd.env("MANGOHUD", "1");
|
||||
}
|
||||
cmd.args(&args);
|
||||
cmd.spawn().with_context(|| {
|
||||
format!(
|
||||
"Failed to spawn '{}'. Is umu-run / gamemoderun / gamescope on PATH?",
|
||||
prog.to_string_lossy()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Outermost → innermost: gamescope → gamemoderun → umu-run → exe [args]
|
||||
fn build_wrapped_argv(exe: &Path, game: &Game) -> (OsString, Vec<OsString>) {
|
||||
let mut argv: Vec<OsString> = Vec::new();
|
||||
if let Some(gs_args) = &game.gamescope {
|
||||
argv.push("gamescope".into());
|
||||
for a in gs_args {
|
||||
argv.push(a.into());
|
||||
}
|
||||
argv.push("--".into());
|
||||
}
|
||||
if game.gamemode {
|
||||
argv.push("gamemoderun".into());
|
||||
}
|
||||
argv.push("umu-run".into());
|
||||
argv.push(exe.as_os_str().to_os_string());
|
||||
for a in &game.args {
|
||||
argv.push(a.into());
|
||||
}
|
||||
let mut iter = argv.into_iter();
|
||||
let prog = iter.next().expect("argv contains at least umu-run");
|
||||
(prog, iter.collect())
|
||||
}
|
||||
|
||||
/// SIGTERM → wait 3 s → SIGKILL for a single launcher.
|
||||
pub fn kill(launcher: &Launcher) -> Result<()> {
|
||||
kill_pattern(&launcher.process_pattern);
|
||||
|
||||
+170
@@ -46,6 +46,21 @@ enum Commands {
|
||||
/// List configured launchers and whether they're installed / running
|
||||
Launchers,
|
||||
|
||||
/// Play a specific game through its launcher's prefix, applying the
|
||||
/// per-game overlay flags (gamemode, mangohud, gamescope).
|
||||
Play {
|
||||
/// Launcher name (e.g. battlenet)
|
||||
launcher: String,
|
||||
/// Game name (e.g. overwatch)
|
||||
game: String,
|
||||
},
|
||||
|
||||
/// List configured games per launcher
|
||||
Games {
|
||||
/// Only show games for this launcher (omit for all)
|
||||
launcher: Option<String>,
|
||||
},
|
||||
|
||||
/// Print setup instructions for a launcher (automated wizard coming soon)
|
||||
Setup {
|
||||
/// Launcher name
|
||||
@@ -132,6 +147,60 @@ enum ConfigAction {
|
||||
/// Short CLI name
|
||||
name: String,
|
||||
},
|
||||
/// Add a game under an existing launcher
|
||||
AddGame {
|
||||
/// Launcher that owns this game
|
||||
launcher: String,
|
||||
|
||||
/// Short CLI name for the game (e.g. "overwatch")
|
||||
name: String,
|
||||
|
||||
/// Game exe path relative to drive_c/
|
||||
#[arg(long, value_name = "PATH")]
|
||||
exe_path: PathBuf,
|
||||
|
||||
/// Display name (defaults to NAME)
|
||||
#[arg(long)]
|
||||
display: Option<String>,
|
||||
|
||||
/// Wrap the game in gamemoderun
|
||||
#[arg(long)]
|
||||
gamemode: bool,
|
||||
|
||||
/// Set MANGOHUD=1 for the game
|
||||
#[arg(long)]
|
||||
mangohud: bool,
|
||||
|
||||
/// Enable gamescope with these args (space-separated, e.g. "-f -W 2560")
|
||||
#[arg(long, value_name = "ARGS")]
|
||||
gamescope: Option<String>,
|
||||
},
|
||||
/// Remove a game from a launcher
|
||||
RemoveGame {
|
||||
launcher: String,
|
||||
name: String,
|
||||
},
|
||||
/// Toggle per-game overlay flags
|
||||
SetGameFlags {
|
||||
launcher: String,
|
||||
name: String,
|
||||
|
||||
/// true / false — wrap in gamemoderun
|
||||
#[arg(long, value_name = "BOOL")]
|
||||
gamemode: Option<bool>,
|
||||
|
||||
/// true / false — set MANGOHUD=1
|
||||
#[arg(long, value_name = "BOOL")]
|
||||
mangohud: Option<bool>,
|
||||
|
||||
/// Enable gamescope with these args (space-separated)
|
||||
#[arg(long, value_name = "ARGS", conflicts_with = "no_gamescope")]
|
||||
gamescope: Option<String>,
|
||||
|
||||
/// Disable gamescope wrapping
|
||||
#[arg(long)]
|
||||
no_gamescope: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -186,6 +255,40 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Play { launcher: lname, game: gname } => {
|
||||
let l = config.find(&lname).ok_or_else(|| {
|
||||
anyhow::anyhow!("unknown launcher '{lname}' — try `umutray launchers`")
|
||||
})?;
|
||||
let g = l.find_game(&gname).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"launcher '{lname}' has no game named '{gname}' — try `umutray games {lname}`"
|
||||
)
|
||||
})?;
|
||||
launcher::play_game(&config, l, g)?;
|
||||
}
|
||||
|
||||
Commands::Games { launcher: lname } => {
|
||||
let launchers: Vec<&config::Launcher> = match &lname {
|
||||
Some(n) => vec![config.find(n).ok_or_else(|| {
|
||||
anyhow::anyhow!("unknown launcher '{n}' — try `umutray launchers`")
|
||||
})?],
|
||||
None => config.launchers.iter().collect(),
|
||||
};
|
||||
for l in launchers {
|
||||
if l.games.is_empty() {
|
||||
println!(" {}: (no games)", l.display);
|
||||
continue;
|
||||
}
|
||||
println!(" {}:", l.display);
|
||||
for g in &l.games {
|
||||
let installed = g.full_exe_path(l).exists();
|
||||
let marker = if installed { "\x1b[1;32m✓\x1b[0m" } else { "·" };
|
||||
let flags = format_game_flags(g);
|
||||
println!(" {marker} {:14} {}{}", g.name, g.display, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Setup { name } => {
|
||||
let l = config.find(&name).ok_or_else(|| {
|
||||
anyhow::anyhow!("unknown launcher '{name}' — try `umutray launchers`")
|
||||
@@ -231,6 +334,55 @@ fn main() -> Result<()> {
|
||||
let mut c = config;
|
||||
c.remove_launcher(&name)?;
|
||||
}
|
||||
ConfigAction::AddGame {
|
||||
launcher,
|
||||
name,
|
||||
exe_path,
|
||||
display,
|
||||
gamemode,
|
||||
mangohud,
|
||||
gamescope,
|
||||
} => {
|
||||
let mut c = config;
|
||||
let gs = gamescope.map(|s| {
|
||||
s.split_whitespace().map(String::from).collect::<Vec<_>>()
|
||||
});
|
||||
c.add_game(
|
||||
&launcher,
|
||||
name,
|
||||
display,
|
||||
exe_path,
|
||||
gamemode,
|
||||
mangohud,
|
||||
gs,
|
||||
)?;
|
||||
}
|
||||
ConfigAction::RemoveGame { launcher, name } => {
|
||||
let mut c = config;
|
||||
c.remove_game(&launcher, &name)?;
|
||||
}
|
||||
ConfigAction::SetGameFlags {
|
||||
launcher,
|
||||
name,
|
||||
gamemode,
|
||||
mangohud,
|
||||
gamescope,
|
||||
no_gamescope,
|
||||
} => {
|
||||
let gs_update = if no_gamescope {
|
||||
Some(None)
|
||||
} else {
|
||||
gamescope.map(|s| {
|
||||
Some(
|
||||
s.split_whitespace()
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
};
|
||||
let mut c = config;
|
||||
c.set_game_flags(&launcher, &name, gamemode, mangohud, gs_update)?;
|
||||
}
|
||||
},
|
||||
|
||||
Commands::Service { action } => match action {
|
||||
@@ -242,3 +394,21 @@ fn main() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_game_flags(g: &config::Game) -> String {
|
||||
let mut tags: Vec<&str> = Vec::new();
|
||||
if g.gamemode {
|
||||
tags.push("gamemode");
|
||||
}
|
||||
if g.mangohud {
|
||||
tags.push("mangohud");
|
||||
}
|
||||
if g.gamescope.is_some() {
|
||||
tags.push("gamescope");
|
||||
}
|
||||
if tags.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" [{}]", tags.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
+118
-2
@@ -16,6 +16,51 @@ fn spawn_setup(name: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
enum GameFlag {
|
||||
GameMode,
|
||||
MangoHud,
|
||||
Gamescope,
|
||||
}
|
||||
|
||||
fn toggle_flag(this: &mut UmuTray, launcher: &str, game: &str, flag: GameFlag) {
|
||||
for l in this.config.launchers.iter_mut() {
|
||||
if l.name != launcher {
|
||||
continue;
|
||||
}
|
||||
for g in l.games.iter_mut() {
|
||||
if g.name != game {
|
||||
continue;
|
||||
}
|
||||
match flag {
|
||||
GameFlag::GameMode => g.gamemode = !g.gamemode,
|
||||
GameFlag::MangoHud => g.mangohud = !g.mangohud,
|
||||
GameFlag::Gamescope => {
|
||||
g.gamescope = if g.gamescope.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(vec![])
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = this.config.save() {
|
||||
eprintln!("umutray: failed to persist flag toggle: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn play_from_tray(config: &Config, launcher: &str, game: &str) {
|
||||
let Some(l) = config.find(launcher) else {
|
||||
return;
|
||||
};
|
||||
let Some(g) = l.find_game(game) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = crate::launcher::play_game(config, l, g) {
|
||||
eprintln!("umutray: play {game} failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UmuTray {
|
||||
pub config: Config,
|
||||
/// Per-launcher running state keyed by launcher.name
|
||||
@@ -69,12 +114,13 @@ impl ksni::Tray for UmuTray {
|
||||
}
|
||||
|
||||
if running {
|
||||
let kill_name = name.clone();
|
||||
items.push(
|
||||
StandardItem {
|
||||
label: format!("Kill {display}"),
|
||||
icon_name: "process-stop".into(),
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
if let Some(l) = this.config.find(&name) {
|
||||
if let Some(l) = this.config.find(&kill_name) {
|
||||
let _ = launcher::kill(l);
|
||||
}
|
||||
}),
|
||||
@@ -83,12 +129,13 @@ impl ksni::Tray for UmuTray {
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
let launch_name = name.clone();
|
||||
items.push(
|
||||
StandardItem {
|
||||
label: format!("Launch {display}"),
|
||||
icon_name: "media-playback-start".into(),
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
if let Some(l) = this.config.find(&name) {
|
||||
if let Some(l) = this.config.find(&launch_name) {
|
||||
if let Err(e) = launcher::launch(&this.config, l) {
|
||||
eprintln!("umutray: launch {} failed: {e}", l.name);
|
||||
}
|
||||
@@ -99,6 +146,75 @@ impl ksni::Tray for UmuTray {
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Per-game submenus with Play + overlay toggles.
|
||||
for g in &l.games {
|
||||
let gdisplay = g.display.clone();
|
||||
let gname_play = g.name.clone();
|
||||
let lname_play = name.clone();
|
||||
let gname_gm = g.name.clone();
|
||||
let lname_gm = name.clone();
|
||||
let gname_mh = g.name.clone();
|
||||
let lname_mh = name.clone();
|
||||
let gname_gs = g.name.clone();
|
||||
let lname_gs = name.clone();
|
||||
|
||||
let mut sub: Vec<ksni::MenuItem<Self>> = Vec::new();
|
||||
sub.push(
|
||||
StandardItem {
|
||||
label: format!("Play {gdisplay}"),
|
||||
icon_name: "media-playback-start".into(),
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
play_from_tray(&this.config, &lname_play, &gname_play);
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
sub.push(ksni::MenuItem::Separator);
|
||||
sub.push(
|
||||
CheckmarkItem {
|
||||
label: "GameMode".into(),
|
||||
checked: g.gamemode,
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
toggle_flag(this, &lname_gm, &gname_gm, GameFlag::GameMode);
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
sub.push(
|
||||
CheckmarkItem {
|
||||
label: "MangoHud".into(),
|
||||
checked: g.mangohud,
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
toggle_flag(this, &lname_mh, &gname_mh, GameFlag::MangoHud);
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
sub.push(
|
||||
CheckmarkItem {
|
||||
label: "Gamescope".into(),
|
||||
checked: g.gamescope.is_some(),
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
toggle_flag(this, &lname_gs, &gname_gs, GameFlag::Gamescope);
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
items.push(
|
||||
SubMenu {
|
||||
label: gdisplay,
|
||||
submenu: sub,
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
items.push(ksni::MenuItem::Separator);
|
||||
|
||||
Reference in New Issue
Block a user