1
use owo_colors::OwoColorize;
2
use protontweaks_api::{app::App, system::SystemTweaks};
3
use std::{collections::HashMap, process::Command};
4

            
5
use clap::{Args, CommandFactory};
6
use regex::Regex;
7

            
8
use crate::{
9
    apps,
10
    config::Config,
11
    utils::{command, gamemode, mangohud},
12
    Cli, API,
13
};
14

            
15
#[derive(Debug, Args)]
16
pub struct CommandArgs {
17
    /// The steam launch command '%command%'
18
    pub command_args: Option<Vec<String>>,
19
}
20

            
21
3
pub async fn command(config: Config, mut args: CommandArgs) -> Result<(), String> {
22
3
    if args.command_args.is_none() {
23
3
        Cli::command()
24
3
            .print_help()
25
3
            .expect("Failed to output help information.");
26
3
        return Ok(());
27
    }
28

            
29
    // Not a fan of this, but it fixes an issue on wayland where it utilizes '--' after the steam-launch-wrapper
30
    // Without it games never start
31
    args.command_args = Some(
32
        std::env::args()
33
            .into_iter()
34
            .filter(|x| x != "cargo" && x != "protontweaks" && x != "run")
35
            .collect::<Vec<String>>(),
36
    );
37

            
38
    let (command, args, app, system_tweaks) = parse_command(config, args).await?;
39

            
40
    if let Some(app) = &app {
41
        let (app_tweaks_applied, app_total_tweaks) = apps::apply_safe(app).await;
42

            
43
        if app_tweaks_applied == 0 {
44
            println!(
45
                "{} {}",
46
                "No tweaks were necessary!".green().bold(),
47
                format!("({app_total_tweaks} tweaks attempted)").italic()
48
            );
49
        } else {
50
            println!(
51
                "Applied {} successfully!",
52
                format!("{app_tweaks_applied} tweaks").bold()
53
            );
54
        }
55
    }
56

            
57
    let env = &system_tweaks.map_or(HashMap::new(), |tweaks| tweaks.env);
58

            
59
    let mut command = Command::new(command);
60

            
61
    command.args(args).envs(env);
62

            
63
    info!("Starting app... {:?}", command);
64

            
65
    command
66
        .spawn()
67
        .expect("Failed to run command")
68
        .wait()
69
        .expect("Failed to wait for command");
70

            
71
    Ok(())
72
3
}
73

            
74
9
async fn parse_command(
75
9
    config: Config,
76
9
    args: CommandArgs,
77
9
) -> Result<(String, Vec<String>, Option<App>, Option<SystemTweaks>), String> {
78
9
    let command_args = args.command_args.unwrap();
79
9
    let is_proton = command_args
80
9
        .iter()
81
39
        .any(|arg| arg.to_lowercase().contains("proton"));
82

            
83
45
    let command_args: Vec<&str> = command_args.iter().map(|x| x.as_str()).collect();
84
9
    let command = command::join(command_args)?;
85

            
86
9
    let app = if is_proton {
87
3
        let re = Regex::new(r"AppId=(?<app_id>\d+)").unwrap();
88

            
89
3
        if let Some(caps) = re.captures(&command) {
90
3
            let app_id = &caps["app_id"];
91

            
92
3
            println!("App ID: {0}", &caps["app_id"]);
93

            
94
3
            API.try_app(app_id).await.ok()
95
        } else {
96
            warn!("Unable to detect App ID, acting purely as a passthrough...");
97
            None
98
        }
99
    } else {
100
6
        info!("Native app detected, skipping proton steps...");
101
6
        None
102
    };
103

            
104
9
    let mut command = command::split(&command)?;
105

            
106
9
    let tweaks = if let Some(app) = &app {
107
3
        Some(app.flatten().await)
108
    } else {
109
6
        None
110
    };
111

            
112
9
    if let Some(tweaks) = &tweaks {
113
3
        if config.mangohud
114
            && tweaks.settings.mangohud.unwrap_or(false)
115
            && mangohud::is_installed().await
116
        {
117
            command.splice(0..0, vec!["mangohud".to_string()]);
118
3
        }
119

            
120
3
        if config.gamemode
121
            && tweaks.settings.gamemode.unwrap_or(true)
122
            && gamemode::is_installed().await
123
        {
124
            command.splice(0..0, vec!["gamemoderun".to_string()]);
125
3
        }
126

            
127
3
        if tweaks.args.len() > 0 {
128
            command.extend(tweaks.args.clone());
129
3
        }
130
6
    }
131

            
132
9
    return Ok((command[0].clone(), command[1..].to_vec(), app, tweaks));
133
9
}
134

            
135
#[cfg(test)]
136
pub mod tests {
137
    use crate::config::Config;
138

            
139
    use super::{command, parse_command, CommandArgs};
140

            
141
    #[tokio::test]
142
3
    pub async fn parse_command_should_support_simple_commands() {
143
3
        let (command, args, app, system_tweaks) = parse_command(
144
3
            Config::off(),
145
3
            CommandArgs {
146
3
                command_args: Some(vec!["echo".to_string(), "hello".to_string()]),
147
3
            },
148
3
        )
149
3
        .await
150
3
        .expect("Failed to parse command.");
151

            
152
3
        assert_eq!(command, "echo");
153
3
        assert_eq!(args, vec!["hello"]);
154
3
        assert!(app.is_none(), "Expected app to not be defined!");
155
3
        assert!(
156
3
            system_tweaks.is_none(),
157
3
            "Expected systemTweaks to not be defined!"
158
3
        );
159
3
    }
160

            
161
    #[tokio::test]
162
3
    pub async fn command_should_support_no_args() {
163
3
        command(Config::off(), CommandArgs { command_args: None })
164
3
            .await
165
3
            .expect("Failed to parse command.");
166
3
    }
167

            
168
    #[tokio::test]
169
3
    pub async fn parse_command_should_support_unified_commands() {
170
3
        let (command, args, app, system_tweaks) = parse_command(
171
3
            Config::off(),
172
3
            CommandArgs {
173
3
                command_args: Some(vec!["echo hello".to_string()]),
174
3
            },
175
3
        )
176
3
        .await
177
3
        .expect("Failed to execute command.");
178

            
179
3
        assert_eq!(command, "echo");
180
3
        assert_eq!(args, vec!["hello"]);
181
3
        assert!(app.is_none(), "Expected app to not be defined!");
182
3
        assert!(
183
3
            system_tweaks.is_none(),
184
3
            "Expected systemTweaks to not be defined!"
185
3
        );
186
3
    }
187

            
188
    #[tokio::test]
189
3
    pub async fn parse_command_should_support_steam_launch_commands() {
190
3
        let command_args = vec![
191
3
            "~/.local/share/Steam/ubuntu12_32/reaper",
192
3
            "SteamLaunch",
193
3
            "AppId=644930",
194
3
            "--",
195
3
            "/home/ceci/.local/share/Steam/ubuntu12_32/steam-launch-wrapper",
196
3
            "--",
197
3
            "'/home/ceci/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper'/_v2-entry-point",
198
3
            "--verb=waitforexitandrun",
199
3
            "--",
200
3
            "'/home/ceci/.local/share/Steam/steamapps/common/Proton 9.0 (Beta)'/proton",
201
3
            "waitforexitandrun",
202
3
            "'/home/ceci/.local/share/Steam/steamapps/common/They Are Billions/TheyAreBillions.exe'"
203
36
        ].iter_mut().map(|x| x.to_string()).collect::<Vec<String>>();
204

            
205
3
        let (command, args, app, system_tweaks) = parse_command(
206
3
            Config::off(),
207
3
            CommandArgs {
208
3
                command_args: Some(command_args),
209
3
            },
210
3
        )
211
3
        .await
212
3
        .expect("Failed to execute command.");
213

            
214
3
        assert_eq!(command, "~/.local/share/Steam/ubuntu12_32/reaper");
215
3
        assert_eq!(args.len(), 11);
216

            
217
3
        let app = app.unwrap();
218

            
219
3
        assert_eq!(app.tweaks.env.len(), 0);
220
3
        assert_eq!(app.tweaks.tricks.len(), 1);
221

            
222
3
        let system_tweaks = system_tweaks.unwrap();
223

            
224
3
        assert_eq!(system_tweaks.env.len(), app.tweaks.env.len());
225
3
        assert_eq!(system_tweaks.tricks.len(), app.tweaks.tricks.len());
226
3
    }
227
}