Initial commit
This commit is contained in:
commit
08103a08fc
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.DS_Store
|
338
Cargo.lock
generated
Normal file
338
Cargo.lock
generated
Normal file
|
@ -0,0 +1,338 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "argh"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab257697eb9496bf75526f0217b5ed64636a9cfafa78b8365c71bd283fcef93e"
|
||||
dependencies = [
|
||||
"argh_derive",
|
||||
"argh_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh_derive"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b382dbd3288e053331f03399e1db106c9fb0d8562ad62cb04859ae926f324fa6"
|
||||
dependencies = [
|
||||
"argh_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh_shared"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64cb94155d965e3d37ffbbe7cc5b82c3dd79dd33bd48e536f73d2cfb8d85506f"
|
||||
|
||||
[[package]]
|
||||
name = "array-init"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
|
||||
|
||||
[[package]]
|
||||
name = "binrw"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab81d22cbd2d745852348b2138f3db2103afa8ce043117a374581926a523e267"
|
||||
dependencies = [
|
||||
"array-init",
|
||||
"binrw_derive",
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "binrw_derive"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6b019a3efebe7f453612083202887b6f1ace59e20d010672e336eea4ed5be97"
|
||||
dependencies = [
|
||||
"either",
|
||||
"owo-colors",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "overcast"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argh",
|
||||
"binrw",
|
||||
"crc32fast",
|
||||
"dirs",
|
||||
"itertools",
|
||||
"steamy-vdf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "steamy-vdf"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533127ad49314bfe71c3d3fd36b3ebac3d24f40618092e70e1cfe8362c7fac79"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "overcast"
|
||||
description = "CLI tool for managing non-Steam games in Steam"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
binrw = "0.11.2"
|
||||
crc32fast = "1.3.2"
|
||||
argh = "0.1.10"
|
||||
dirs = "5.0.1"
|
||||
steamy-vdf = "0.2.0"
|
||||
itertools = "0.11.0"
|
41
README.md
Normal file
41
README.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
# overcast
|
||||
|
||||
**overcast** is a tool for managing non-Steam games, known internally as "shortcuts", via the command-line. This tool is intended to be used with the Steam Deck, and support on other platforms should not be expected.
|
||||
|
||||
## How to Use
|
||||
|
||||
The end goal is for overcast to be managed via a GUI. In the meantime, the info provided by `overcast --help` should get you started:
|
||||
|
||||
```
|
||||
Usage: overcast <command> [<args>]
|
||||
|
||||
Manage non-Steam games
|
||||
|
||||
Options:
|
||||
--help display usage information
|
||||
|
||||
Commands:
|
||||
add add a new shortcut
|
||||
list list all shortcuts
|
||||
```
|
||||
|
||||
### Adding a new shortcut
|
||||
|
||||
When adding a new game, you are required to provide its name as will be shown in your Steam library, and the command to launch it.
|
||||
|
||||
```
|
||||
overcast add -n "My Cool Game" \
|
||||
/home/flurry/Games/my_cool_game/launch.sh --fullscreen
|
||||
```
|
||||
|
||||
You may also want to set the starting directory, since most Linux launcher scripts expect to be in the same directory.
|
||||
|
||||
```
|
||||
overcast add -n "My Cool Game" \
|
||||
--start-dir /home/flurry/Games/my_cool_game \
|
||||
/home/flurry/Games/my_cool_game/launch.sh --fullscreen
|
||||
```
|
||||
|
||||
### Listing shortcuts
|
||||
|
||||
`overcast list` provides a quick list of the App IDs and names for all shortcuts you have. If you want more info, use the `--verbose` flag.
|
7
TODO
Normal file
7
TODO
Normal file
|
@ -0,0 +1,7 @@
|
|||
* Send commands to running Steam instance:
|
||||
* Reload shortcuts.vdf on change (steam://resetcollections?)
|
||||
* Set Steam Play compat tool (+app_change_compat_tool "Proton X.Y")
|
||||
* Bonus: Get list of installed compat tools so we aren't
|
||||
blindly guessing
|
||||
* Add images other than icon
|
||||
* holy grail: documentation
|
152
src/game.rs
Normal file
152
src/game.rs
Normal file
|
@ -0,0 +1,152 @@
|
|||
use crate::vdf::Dictionary;
|
||||
|
||||
pub struct NonSteamGame {
|
||||
/// UNIX timestamp of the last time the game was played
|
||||
pub last_play_time: u32,
|
||||
/// Location of the shortcut (e.g., .desktop file on Linux) the game entry was generated from
|
||||
pub shortcut_path: String,
|
||||
/// Executable path for the game
|
||||
pub executable: String,
|
||||
/// Whether to allow Steam overlay over the game
|
||||
pub allow_overlay: bool,
|
||||
/// Whether the game is hidden from the Steam library(?)
|
||||
pub is_hidden: bool,
|
||||
/// Whether the game supports OpenVR
|
||||
pub open_vr: bool,
|
||||
/// Unknown
|
||||
pub devkit_game_id: String,
|
||||
/// App name as it appears in Steam
|
||||
pub app_name: String,
|
||||
/// Starting directory for the game. Default is "./", for the current working directory.
|
||||
pub start_dir: String,
|
||||
/// What options to launch the game with. Used with `executable`.
|
||||
pub launch_options: String,
|
||||
/// The app ID of the game. Default is to use a randomly-generated integer, to avoid conflict with other installed games.
|
||||
pub app_id: u32,
|
||||
/// Unknown. Possibly means this was created by the SteamOS Devkit Client?
|
||||
pub devkit: bool,
|
||||
/// If the game is a Flatpak image, the Flatpak app ID of the game
|
||||
pub flatpak_app_id: String,
|
||||
/// If the game has a launcher, allows a Steam controller to use the desktop config to interact with it
|
||||
pub allow_desktop_config: bool,
|
||||
/// List of tags used for the game
|
||||
pub tags: Vec<String>,
|
||||
/// Path to the icon for the game. Both .ico and .png seem to be supported
|
||||
pub icon: String,
|
||||
/// Unknown. More Devkit stuff
|
||||
pub devkit_override_app_id: bool,
|
||||
}
|
||||
|
||||
impl NonSteamGame {
|
||||
pub fn new<S: AsRef<str> + Default>(
|
||||
name: S,
|
||||
executable: S,
|
||||
launch_options: Option<S>,
|
||||
icon: Option<S>,
|
||||
) -> Self {
|
||||
let app_id = {
|
||||
let input = format!("{}{}", executable.as_ref(), name.as_ref());
|
||||
crc32fast::hash(input.as_bytes()) | 0x80000000
|
||||
};
|
||||
NonSteamGame {
|
||||
app_name: name.as_ref().to_owned(),
|
||||
executable: executable.as_ref().to_owned(),
|
||||
app_id,
|
||||
launch_options: launch_options.unwrap_or_default().as_ref().to_owned(),
|
||||
icon: icon.unwrap_or_default().as_ref().to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! append_kv {
|
||||
($d:expr, $k:literal, $v:expr) => {
|
||||
$d.insert(String::from($k), crate::vdf::VdfValue::from($v))
|
||||
};
|
||||
}
|
||||
|
||||
impl Default for NonSteamGame {
|
||||
fn default() -> Self {
|
||||
NonSteamGame {
|
||||
last_play_time: 0,
|
||||
shortcut_path: String::new(),
|
||||
executable: String::new(),
|
||||
allow_overlay: true,
|
||||
is_hidden: false,
|
||||
open_vr: false,
|
||||
devkit_game_id: String::new(),
|
||||
app_name: String::new(),
|
||||
start_dir: String::from("./"),
|
||||
launch_options: String::new(),
|
||||
app_id: 0,
|
||||
devkit: false,
|
||||
flatpak_app_id: String::new(),
|
||||
allow_desktop_config: true,
|
||||
tags: Vec::new(),
|
||||
icon: String::new(),
|
||||
devkit_override_app_id: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NonSteamGame> for Dictionary {
|
||||
fn from(value: NonSteamGame) -> Self {
|
||||
let mut dict = Dictionary::new();
|
||||
|
||||
append_kv!(dict, "LastPlayTime", value.last_play_time);
|
||||
// TODO: .desktop path is stored here; why does steam use this?
|
||||
append_kv!(dict, "ShortcutPath", &value.shortcut_path);
|
||||
// quoted string
|
||||
append_kv!(dict, "Exe", &value.executable);
|
||||
append_kv!(dict, "AllowOverlay", value.allow_overlay);
|
||||
append_kv!(dict, "IsHidden", value.is_hidden);
|
||||
append_kv!(dict, "OpenVR", value.open_vr);
|
||||
append_kv!(dict, "DevkitGameID", &value.devkit_game_id);
|
||||
append_kv!(dict, "AppName", &value.app_name);
|
||||
append_kv!(dict, "StartDir", &value.start_dir);
|
||||
append_kv!(dict, "LaunchOptions", &value.launch_options);
|
||||
append_kv!(dict, "appid", value.app_id);
|
||||
append_kv!(dict, "Devkit", value.devkit);
|
||||
append_kv!(dict, "FlatpakAppID", &value.flatpak_app_id);
|
||||
append_kv!(dict, "AllowDesktopConfig", value.allow_desktop_config);
|
||||
append_kv!(dict, "tags", crate::vdf::VdfValue::from(value.tags));
|
||||
append_kv!(dict, "icon", &value.icon);
|
||||
append_kv!(dict, "DevkitOverrideAppID", value.devkit_override_app_id);
|
||||
|
||||
dict
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! map_to_struct {
|
||||
($dict:expr, $st:tt { $($ent:tt => $key:literal),+ } ) => {
|
||||
$st {
|
||||
$($ent: $dict[$key].clone().try_into()?),+
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl TryFrom<Dictionary> for NonSteamGame {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
// TODO: do we care if this panics? maybe we should care
|
||||
fn try_from(value: Dictionary) -> Result<Self, Self::Error> {
|
||||
Ok(map_to_struct!(value, NonSteamGame {
|
||||
last_play_time => "LastPlayTime",
|
||||
shortcut_path => "ShortcutPath",
|
||||
executable => "Exe",
|
||||
allow_overlay => "AllowOverlay",
|
||||
is_hidden => "IsHidden",
|
||||
open_vr => "OpenVR",
|
||||
devkit_game_id => "DevkitGameID",
|
||||
app_name => "AppName",
|
||||
start_dir => "StartDir",
|
||||
launch_options => "LaunchOptions",
|
||||
app_id => "appid",
|
||||
devkit => "Devkit",
|
||||
flatpak_app_id => "FlatpakAppID",
|
||||
allow_desktop_config => "AllowDesktopConfig",
|
||||
tags => "tags",
|
||||
icon => "icon",
|
||||
devkit_override_app_id => "DevkitOverrideAppID"
|
||||
}))
|
||||
}
|
||||
}
|
171
src/main.rs
Normal file
171
src/main.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use argh::FromArgs;
|
||||
|
||||
use game::NonSteamGame;
|
||||
use itertools::Itertools;
|
||||
use vdf::{Dictionary, VdfValue};
|
||||
|
||||
mod game;
|
||||
mod user;
|
||||
mod vdf;
|
||||
|
||||
#[derive(Debug, FromArgs)]
|
||||
/// Manage non-Steam games
|
||||
struct Args {
|
||||
#[argh(subcommand)]
|
||||
cmd: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromArgs)]
|
||||
#[argh(subcommand)]
|
||||
enum Commands {
|
||||
Add(AddArgs),
|
||||
/// list all shortcuts
|
||||
List(ListArgs),
|
||||
}
|
||||
|
||||
/// add a new shortcut
|
||||
#[derive(Debug, FromArgs)]
|
||||
#[argh(subcommand, name = "add")]
|
||||
struct AddArgs {
|
||||
/// starting directory
|
||||
#[argh(option, short = 'd')]
|
||||
start_dir: Option<String>,
|
||||
/// path to the game icon
|
||||
#[argh(option, short = 'i')]
|
||||
icon: Option<PathBuf>,
|
||||
/// name of the app as it'll show in Steam
|
||||
#[argh(option, short = 'n')]
|
||||
app_name: String,
|
||||
/// what command and args to run to execute the app
|
||||
#[argh(positional, greedy)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
/// list all shortcuts
|
||||
#[derive(Debug, FromArgs)]
|
||||
#[argh(subcommand, name = "list")]
|
||||
struct ListArgs {
|
||||
/// print too much info about apps
|
||||
#[argh(switch, short = 'v')]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Args = argh::from_env();
|
||||
|
||||
let shortcuts_path = {
|
||||
let mut dir = user::get_userdir()?;
|
||||
dir.push("config/shortcuts.vdf");
|
||||
dir
|
||||
};
|
||||
|
||||
match args.cmd {
|
||||
Commands::Add(args) => add_shortcut(shortcuts_path, args),
|
||||
Commands::List(args) => list_shortcuts(shortcuts_path, args),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_shortcuts(
|
||||
shortcuts_path: PathBuf,
|
||||
args: ListArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if shortcuts_path.exists() {
|
||||
for shortcut in get_shortcuts(shortcuts_path)? {
|
||||
if args.verbose {
|
||||
println!("App {} (ID {}):", shortcut.app_name, shortcut.app_id);
|
||||
println!(" Start from: {}", shortcut.start_dir);
|
||||
println!(" Target: {}", shortcut.executable);
|
||||
if !shortcut.launch_options.is_empty() {
|
||||
println!(" Launch Args: {}", shortcut.launch_options);
|
||||
}
|
||||
if !shortcut.tags.is_empty() {
|
||||
println!(" Tags: {}", shortcut.tags.join(", "));
|
||||
}
|
||||
if !shortcut.icon.is_empty() {
|
||||
println!(" Icon Path: {}", shortcut.icon);
|
||||
}
|
||||
} else {
|
||||
println!("App ID {}: {}", shortcut.app_id, shortcut.app_name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("shortcuts.vdf doesn't exist!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_shortcut(shortcuts_path: PathBuf, args: AddArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut shortcuts = if shortcuts_path.exists() {
|
||||
get_shortcuts(shortcuts_path.clone())?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
for sc in &shortcuts {
|
||||
println!("App {}: {}, {}", sc.app_id, sc.app_name, sc.executable);
|
||||
}
|
||||
|
||||
let executable = prepare_arg(&args.command[0]);
|
||||
let exe_opts: Option<String> = if args.command.len() > 1 {
|
||||
Some(args.command[1..].to_vec().iter().map(prepare_arg).join(" "))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut new_game = NonSteamGame::new(args.app_name, executable, exe_opts, None);
|
||||
|
||||
if let Some(start_dir) = &args.start_dir {
|
||||
new_game.start_dir = format!("\"{}\"", start_dir);
|
||||
}
|
||||
|
||||
if let Some(icon) = &args.icon {
|
||||
new_game.icon = icon.display().to_string();
|
||||
}
|
||||
|
||||
shortcuts.push(new_game);
|
||||
|
||||
let vdf_out: vdf::Dictionary = {
|
||||
let mut map = Dictionary::new();
|
||||
let arr: vdf::Dictionary = shortcuts
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, game)| (idx.to_string(), VdfValue::Dict(game.into())))
|
||||
.collect();
|
||||
map.insert("shortcuts".to_owned(), VdfValue::Dict(arr));
|
||||
map
|
||||
};
|
||||
|
||||
vdf::write_file(vdf_out, shortcuts_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_shortcuts(path: PathBuf) -> Result<Vec<NonSteamGame>, String> {
|
||||
let vdf_map =
|
||||
vdf::read_file(&path).map_err(|x| format!("Error opening {}: {}", &path.display(), x))?;
|
||||
let vec: Result<Vec<NonSteamGame>, String> = vdf_map["shortcuts"]
|
||||
.clone()
|
||||
.into_vec()
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
if let vdf::VdfValue::Dict(dict) = x {
|
||||
Ok(NonSteamGame::try_from(dict).unwrap())
|
||||
} else {
|
||||
Err("Couldn't parse shortcuts.vdf".to_owned())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
vec
|
||||
}
|
||||
|
||||
fn prepare_arg<S: AsRef<str>>(input: S) -> String {
|
||||
let input = input.as_ref();
|
||||
if input.contains(char::is_whitespace) {
|
||||
format!("\"{}\"", input)
|
||||
} else {
|
||||
input.to_owned()
|
||||
}
|
||||
}
|
68
src/user.rs
Normal file
68
src/user.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use std::{fmt, ops::Deref, path::PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HeuristicError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for HeuristicError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", &self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for HeuristicError {}
|
||||
|
||||
impl HeuristicError {
|
||||
fn new<S: AsRef<str>>(mesg: S) -> Self {
|
||||
Self {
|
||||
message: mesg.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_steamdir() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let mut dir = dirs::data_local_dir().unwrap_or_else(|| {
|
||||
let mut dir = dirs::home_dir().unwrap();
|
||||
dir.push(".local/share");
|
||||
dir
|
||||
});
|
||||
dir.push("Steam");
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Attempt to get the userdata dir via loginusers.vdf
|
||||
pub fn get_userdir() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let steam_dir = get_steamdir()?;
|
||||
|
||||
let users_path = {
|
||||
let mut dir = steam_dir.clone();
|
||||
dir.push("config/loginusers.vdf");
|
||||
dir
|
||||
};
|
||||
if let steamy_vdf::Entry::Table(table) = steamy_vdf::load(users_path)? {
|
||||
if let steamy_vdf::Entry::Table(users) = &table["users"] {
|
||||
let mut last_id: Option<u64> = None;
|
||||
for (uid, data) in users.iter() {
|
||||
let data = data.as_table().unwrap();
|
||||
if let Some(steamy_vdf::Entry::Value(most_recent)) = data.get("MostRecent") {
|
||||
if most_recent.deref() == "1" {
|
||||
last_id = Some(uid.parse()?);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(last_id) = last_id {
|
||||
let mut userdir = steam_dir;
|
||||
userdir.push(format!("userdata/{}", last_id & 0xffffffff));
|
||||
return Ok(userdir);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(HeuristicError::new(
|
||||
"steamy_vdf::load gave us something other than Table ???",
|
||||
))?;
|
||||
}
|
||||
|
||||
Err(HeuristicError::new("Couldn't guess your user ID"))?
|
||||
}
|
196
src/vdf.rs
Normal file
196
src/vdf.rs
Normal file
|
@ -0,0 +1,196 @@
|
|||
use binrw::{until, BinRead, BinWrite, NullString};
|
||||
use std::{borrow::Cow, collections::HashMap, fmt, fs::File, path::Path};
|
||||
|
||||
#[derive(BinRead, BinWrite, Clone, PartialEq)]
|
||||
#[brw(little)]
|
||||
enum VdfBlock {
|
||||
#[brw(magic(0u8))]
|
||||
Dict(NullString, VdfDict),
|
||||
#[brw(magic(1u8))]
|
||||
Str(NullString, NullString),
|
||||
#[brw(magic(2u8))]
|
||||
Int(NullString, u32),
|
||||
#[brw(magic(8u8))]
|
||||
EndDict,
|
||||
}
|
||||
|
||||
#[derive(BinRead, BinWrite, Clone, PartialEq)]
|
||||
#[brw(little)]
|
||||
struct VdfDict(
|
||||
#[br(parse_with(until(|block: &VdfBlock| block == &VdfBlock::EndDict)))] Vec<VdfBlock>,
|
||||
);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum VdfValue {
|
||||
Dict(Dictionary),
|
||||
Str(String),
|
||||
Int(u32),
|
||||
}
|
||||
|
||||
impl VdfValue {
|
||||
pub fn into_vec(self) -> Vec<VdfValue> {
|
||||
if let VdfValue::Dict(dict) = self {
|
||||
// lazily assuming this is an array of strings
|
||||
dict.into_values().collect()
|
||||
} else {
|
||||
panic!("Not a Dict! (maybe this should be less brutal of error mgmt!)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Dictionary = HashMap<String, VdfValue>;
|
||||
|
||||
macro_rules! as_str {
|
||||
($var:expr) => {{
|
||||
use std::borrow::Borrow;
|
||||
String::from_utf8_lossy($var.borrow()).to_string()
|
||||
}};
|
||||
}
|
||||
|
||||
// === VdfValue Conversions ===
|
||||
|
||||
// Conversion error type for TryFrom
|
||||
#[derive(Debug)]
|
||||
pub enum VdfError {
|
||||
WrongType,
|
||||
}
|
||||
|
||||
impl fmt::Display for VdfError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
VdfError::WrongType => write!(f, "Attempt to convert value to wrong type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VdfError {}
|
||||
|
||||
// macros
|
||||
|
||||
macro_rules! impl_convert_to_vdfvalue {
|
||||
($t:ty, $e:tt, from_only) => {
|
||||
impl From<$t> for VdfValue {
|
||||
fn from(value: $t) -> Self {
|
||||
VdfValue::$e(value.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($t:ty, $e:tt) => {
|
||||
impl_convert_to_vdfvalue!($t, $e, from_only);
|
||||
|
||||
impl TryFrom<VdfValue> for $t {
|
||||
type Error = VdfError;
|
||||
fn try_from(value: VdfValue) -> Result<Self, Self::Error> {
|
||||
if let VdfValue::$e(x) = value {
|
||||
Ok(x)
|
||||
} else {
|
||||
Err(VdfError::WrongType)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_convert_to_vdfvalue!(u32, Int);
|
||||
impl_convert_to_vdfvalue!(String, Str);
|
||||
impl_convert_to_vdfvalue!(&String, Str, from_only);
|
||||
impl_convert_to_vdfvalue!(&str, Str, from_only);
|
||||
impl_convert_to_vdfvalue!(bool, Int, from_only);
|
||||
impl_convert_to_vdfvalue!(Dictionary, Dict);
|
||||
impl_convert_to_vdfvalue!(Cow<'_, str>, Str, from_only);
|
||||
|
||||
// bool requires a special TryFrom<VdfValue>
|
||||
impl TryFrom<VdfValue> for bool {
|
||||
type Error = VdfError;
|
||||
fn try_from(value: VdfValue) -> Result<Self, Self::Error> {
|
||||
if let VdfValue::Int(x) = value {
|
||||
Ok(!matches!(x, 0))
|
||||
} else {
|
||||
Err(VdfError::WrongType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Arrays also require special TryFrom
|
||||
impl<T> TryFrom<VdfValue> for Vec<T>
|
||||
where
|
||||
T: TryFrom<VdfValue>,
|
||||
{
|
||||
type Error = VdfError;
|
||||
fn try_from(value: VdfValue) -> Result<Self, Self::Error> {
|
||||
if let VdfValue::Dict(dict) = value {
|
||||
// lazily assuming this is an array of strings
|
||||
let results: Result<Vec<T>, T::Error> = dict.into_values().map(T::try_from).collect();
|
||||
results.map_err(|_| VdfError::WrongType)
|
||||
} else {
|
||||
Err(VdfError::WrongType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for VdfValue
|
||||
where
|
||||
T: Into<VdfValue>,
|
||||
{
|
||||
fn from(value: Vec<T>) -> Self {
|
||||
let dict: HashMap<String, VdfValue> = value
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, val)| (idx.to_string(), val.into()))
|
||||
.collect();
|
||||
VdfValue::Dict(dict)
|
||||
}
|
||||
}
|
||||
|
||||
/// === VdfDict (raw) to Dictionary Conversions ===
|
||||
|
||||
impl From<VdfDict> for Dictionary {
|
||||
fn from(value: VdfDict) -> Self {
|
||||
let map: Dictionary = value
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|block| match block.clone() {
|
||||
VdfBlock::Dict(k, v) => Some((as_str!(k), VdfValue::Dict(v.into()))),
|
||||
VdfBlock::Str(k, v) => Some((as_str!(k), VdfValue::Str(as_str!(v)))),
|
||||
VdfBlock::Int(k, v) => Some((as_str!(k), VdfValue::Int(v))),
|
||||
VdfBlock::EndDict => None,
|
||||
})
|
||||
.collect();
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Dictionary> for VdfDict {
|
||||
fn from(value: Dictionary) -> Self {
|
||||
let mut dict: Vec<VdfBlock> = value
|
||||
.into_iter()
|
||||
.map(|ent| {
|
||||
let key = NullString::from(ent.0.as_str());
|
||||
match ent.1 {
|
||||
VdfValue::Dict(v) => VdfBlock::Dict(key, v.into()),
|
||||
VdfValue::Int(v) => VdfBlock::Int(key, v),
|
||||
VdfValue::Str(v) => VdfBlock::Str(key, NullString::from(v.as_str())),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
dict.push(VdfBlock::EndDict);
|
||||
VdfDict(dict)
|
||||
}
|
||||
}
|
||||
|
||||
// === Exported Functions ===
|
||||
|
||||
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<Dictionary, Box<dyn std::error::Error>> {
|
||||
let mut fd = File::open(path)?;
|
||||
Ok(VdfDict::read(&mut fd)?.into())
|
||||
}
|
||||
|
||||
pub fn write_file<P: AsRef<Path>>(
|
||||
dict: Dictionary,
|
||||
path: P,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut fd = File::create(path)?;
|
||||
VdfDict::from(dict).write(&mut fd)?;
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in a new issue