diff --git a/CHANGELOG.md b/CHANGELOG.md index 844b236..9b449bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ # Changelog -Keep track of changes with every release. +Keep track of changes with every release of https://github.com/thingsiplay/enjoy . + +## [0.3.0] - September 2, 2022 + +- new: option `-n` and `--list-cores` to list all custom core names in section + "[cores]" from user configuration, if a game is given too then only matching + cores to the game will be printed +- new: option `-W` and `--which-command`, similar to `--which` but will print + complete commandline used to run RetroArch +- changed: option `-o` and `--open-config` will now print path to the config too +- changed: better error message if a game file is not found, pointing to the + file it was looking for +- bug: annoying error message when game run just fine, now checks for + "exit status: 0" instead "exit code: 0" +- internal: replaced or updated some backend libraries +- internal: updated code base to Rust Edition to 2021 ## [0.2.0] - June 8, 2021 diff --git a/Cargo.lock b/Cargo.lock index 39e6dab..f2d5b40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,13 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi", -] +version = 3 [[package]] name = "atty" @@ -20,11 +13,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cfg-if" @@ -34,116 +33,158 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.33.3" +version = "3.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" dependencies = [ - "ansi_term", "atty", "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", "strsim", + "termcolor", "textwrap", - "unicode-width", - "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", ] [[package]] name = "configparser" -version = "2.0.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aad39d76dbe45b809ef6d783b8c597732225b8f3d6c4d8ceb4a4f834a844ffe" +checksum = "65cbde0d4b888436a51954624cafd198aee478d00cf5db866c3519a8986ecb18" [[package]] -name = "dirs-next" -version = "2.0.0" +name = "dirs" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ - "cfg-if", - "dirs-sys-next", + "dirs-sys", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "dirs-sys" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", "winapi", ] -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - [[package]] name = "enjoy" -version = "0.1.1" +version = "0.3.0" dependencies = [ "atty", + "clap", "configparser", "open", "shellexpand", "shlex", - "structopt", "wildmatch", ] [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", "wasi", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "heck" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" -dependencies = [ - "unicode-segmentation", -] +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "indexmap" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] [[package]] name = "libc" -version = "0.2.94" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "once_cell" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] name = "open" -version = "1.7.0" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1711eb4b31ce4ad35b0f316d8dfba4fe5c7ad601c448446d84aae7a896627b20" +checksum = "f23a407004a1033f53e93f9b45580d14de23928faad187384f891507c9b0c045" dependencies = [ - "which", - "winapi", + "pathdiff", + "windows-sys", ] +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -170,157 +211,132 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", "redox_syscall", + "thiserror", ] [[package]] name = "shellexpand" -version = "2.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" dependencies = [ - "dirs-next", + "dirs", ] [[package]] name = "shlex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "structopt" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.72" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "termcolor" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ - "unicode-width", + "winapi-util", ] [[package]] -name = "unicode-segmentation" -version = "1.7.1" +name = "textwrap" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] -name = "unicode-width" -version = "0.1.8" +name = "thiserror" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57" +dependencies = [ + "thiserror-impl", +] [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "thiserror-impl" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "vec_map" -version = "0.8.2" +name = "unicode-ident" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - -[[package]] -name = "which" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe" -dependencies = [ - "either", - "libc", -] +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wildmatch" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0" +checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86" [[package]] name = "winapi" @@ -338,8 +354,60 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" diff --git a/Cargo.toml b/Cargo.toml index 1db5808..13657bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "enjoy" -version = "0.2.0" +version = "0.3.0" authors = ["Tuncay D "] description = "Play any game rom with associated emulator in RetroArch" readme = "README.md" @@ -8,21 +8,22 @@ repository = "https://github.com/thingsiplay/enjoy/" license-file = "LICENSE" keywords = ["retroarch", "libretro", "emulation", "run", "launcher"] categories = ["command-line-utilities", "emulators"] -edition = "2018" +edition = "2021" resolver = "2" publish = false [dependencies] -configparser = "2.0.1" -structopt = "0.3.21" -atty = "0.2.14" -shellexpand = "2.1.0" -wildmatch = "2.1.0" -open = "1.7.0" -shlex = "1.0.0" +configparser = "3.0" +clap = {version = "3.2", features = ["derive"]} +atty = "0.2" +shellexpand = "2.1" +wildmatch = "2.1" +open = "3.0" +shlex = "1.1" [profile.release] opt-level = "z" codegen-units = 1 lto = true panic = "abort" +strip = "symbols" diff --git a/Makefile b/Makefile index 6cfcb24..1d25cb2 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SHELL = /bin/bash # Will strip strings from distribution binary. # 0=disable # 1=enable -STRIP_DIST_BIN:=1 +STRIP_DIST_BIN:=0 # Will compress distribution binary. # 0=disable @@ -15,9 +15,9 @@ COMPRESS_DIST_BIN:=0 # 1=enable BUILD_DEBUG:=1 -APP_NAME=$(shell grep -E 'name\s*=' Cargo.toml | grep -o '".*"' | tr -d '"') +APP_NAME=$(shell grep -E '^\s*name\s*=' Cargo.toml | grep -o '".*"' | tr -d '"') APP_DEBUG_NAME=$(APP_NAME)d -APP_VERSION=$(shell grep -E 'version\s*=' Cargo.toml | grep -o '".*"' | tr -d '"') +APP_VERSION=$(shell grep -E '^\s*version\s*=' Cargo.toml | grep -o '".*"' | tr -d '"') PACKAGE_SRC_DIR:=./build PACKAGE_DEST_DIR:=./dist @@ -30,7 +30,8 @@ CLIPPY_FLAGS:=-D warnings CLIPPY_PEDANTIC_FLAGS:=-W clippy::pedantic BUILD_RELEASE_FLAGS:= BUILD_DEBUG_FLAGS:= -DOC_FLAGS:=--open +# DOC_FLAGS:=--open +DOC_FLAGS:= .DEFAULT_GOAL := default diff --git a/README.md b/README.md index 7e10aae..1d8b969 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,21 @@ runs, it will lookup these settings to determine the correct core for each given extension. It can be even used to launch games directly within your favorite file manager by double clicking the ROM file. +Use option `-h` for short help and `--help` for detailed help. + ### Example ```bash $ enjoy '~/roms/snes/Super Mario World (U) [!].smc' + +$ ls -1 $(readlink -f ~/Emulatoren/games/gb)/* | enjoy --filter 'mario' -xWn ``` +Depending on your shell, you might need to escape the `!` in example. When +multiple ROMs are given, then the first one will be loaded. There are many +options available, including filtering such a game list or output the entire +command to run RetroArch. + ### Features - run *RetroArch* games directly from your terminal or file manager diff --git a/build/example-config.ini b/build/example-config.ini index 839d777..88c9fa2 100644 --- a/build/example-config.ini +++ b/build/example-config.ini @@ -1,3 +1,5 @@ +# ~/.config/enjoy/default.ini + # Example INI config for enjoy. # Comments start with a hash-symbol "#" and are ignored. diff --git a/rustfmt.toml b/rustfmt.toml index ad54acc..1d90db4 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,2 @@ -edition = "2018" +edition = "2021" max_width = 79 diff --git a/src/main.rs b/src/main.rs index 32c0b88..608f365 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use crate::settings::RunCommand; use crate::settings::Settings; use std::error::Error; +use std::process::Command; fn main() -> Result<(), Box> { // The flow of the program is build around the idea of creating a main settings structure from @@ -14,7 +15,7 @@ fn main() -> Result<(), Box> { // structure. The last step would be to actually execute the command and finish up the final // work. - let argument_options = Settings::new_from_cmdline(None)?; + let argument_options = Settings::new_from_cmdline(None); // If option `open-config` is set, then open the file and exit program. if argument_options.open_config()? { @@ -29,35 +30,57 @@ fn main() -> Result<(), Box> { let mut app_settings = Settings::new(); // Overwrite fields in app_settings only, if new fields are Some(). - app_settings.update_from(user_config)?; - app_settings.update_from(stdin_games)?; - app_settings.update_from(argument_options)?; + app_settings.update_from(user_config); + app_settings.update_from(stdin_games); + app_settings.update_from(argument_options); - // Without a game entry there is no point in running the program. - if !app_settings.is_game_available() { - return Err("A path to game is required.".into()); - } - - let mut defaults = Settings::new_from_defaults()?; + let mut defaults = Settings::new_from_defaults(); if !app_settings.is_libretro_path_available() { // Extract keys and values from `retroarch.cfg` only if the path to `libretro` installation // directory in `RetroArch` is unknown. let raconfig = Settings::new_from_retroarch_config( app_settings.get_retroarch_config(), )?; - defaults.update_from(raconfig)?; + defaults.update_from(raconfig); } // Overwrite only those keys in `app_settings`, which their values are currently `None`. - app_settings.update_defaults_from(defaults)?; + app_settings.update_defaults_from(defaults); + + if app_settings.is_game_available() { + let mut run: RunCommand = app_settings.build_command()?; - // Build the final commandline for RetroArch and execute it, if permitted. - let mut run: RunCommand = app_settings.build_command()?; - if app_settings.there_can_only_be_one() { - eprintln!("retroarch process already running. There Can Be Only One!"); + if app_settings.there_can_only_be_one() { + eprintln!( + "retroarch process already running. There Can Be Only One!" + ); + } else { + run.output = app_settings.run(&mut run.cmdline); + } + if app_settings.is_list_cores() { + for core in app_settings.find_core_match(&run.libretro) { + println!("{core}"); + } + } + if app_settings.is_which_command() { + print_cmdline(&run.cmdline); + } else { + app_settings.print_which(run.game); + } + } else if app_settings.is_list_cores() { + app_settings.print_cores(); } else { - run.output = app_settings.run(&mut run.cmdline); + return Err("A path to game is required.".into()); } - app_settings.print_which(run.game); Ok(()) } + +// Prints program name and each commandline arguments exactly the same as it is used to run +// RetroArch. +fn print_cmdline(command: &Command) { + print!("{:?}", command.get_program()); + for arg in command.get_args() { + print!(" {:?}", arg); + } + println!(); +} diff --git a/src/settings.rs b/src/settings.rs index 08e9510..d645997 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -7,16 +7,18 @@ use arguments::Opt; use std::collections::HashMap; use std::collections::HashSet; -use std::error::Error; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::process::Output; +use clap::Parser; use configparser::ini; -use structopt::StructOpt; use wildmatch::WildMatch; +type Result> = + std::result::Result; + /// The final `process::Command` to execute and run `retroarch`. It bundles related information /// such as paths and the `output` from stdout. The additional path data should be manually set /// when building the main `cmdline`, to stay in sync. The `output` must be set manually after @@ -45,6 +47,8 @@ pub struct Settings { core: Option, filter: Option, which: Option, + which_command: Option, + list_cores: Option, fullscreen: Option, highlander: Option, open_config: Option, @@ -76,6 +80,8 @@ impl Settings { core: None, filter: None, which: None, + which_command: None, + list_cores: None, fullscreen: None, highlander: None, open_config: None, @@ -90,36 +96,33 @@ impl Settings { /// Read each line from stdin stream and convert it to paths. Create a new struct with games /// out of it. - pub fn new_from_stdin(nostdin: bool) -> Result> { + pub fn new_from_stdin(nostdin: bool) -> Result { let mut settings: Settings = Settings::new(); if !nostdin { - if let Ok(list) = inoutput::list_from_stdin() { - settings.games = list.iter().map(PathBuf::from).collect(); - } + let list = inoutput::list_from_stdin()?; + settings.games = list.iter().map(PathBuf::from).collect(); } Ok(settings) } /// Create a new Settings struct with a few default data. - pub fn new_from_defaults() -> Result> { + pub fn new_from_defaults() -> Settings { let mut settings: Settings = Settings::new(); settings.retroarch = Some(PathBuf::from("retroarch")); - Ok(settings) + settings } /// Parse own commandline arguments and create a new Settings struct out of it. - pub fn new_from_cmdline( - options: Option>, - ) -> Result> { + pub fn new_from_cmdline(options: Option>) -> Settings { let mut settings: Settings = Settings::new(); let args: Opt = match options { - Some(from) => Opt::from_iter(from.iter()), - None => Opt::from_args(), + Some(opt) => Opt::from_iter(opt.iter()), + None => Opt::parse(), }; // default_value @@ -143,40 +146,46 @@ impl Settings { // bool // Only set it to `true`, if the option is found in arguments. if args.which { - settings.which = Some(true) + settings.which = Some(true); + } + if args.which_command { + settings.which_command = Some(true); + } + if args.list_cores { + settings.list_cores = Some(true); } if args.fullscreen { - settings.fullscreen = Some(true) + settings.fullscreen = Some(true); } if args.highlander { - settings.highlander = Some(true) + settings.highlander = Some(true); } if args.open_config { - settings.open_config = Some(true) + settings.open_config = Some(true); } if args.noconfig { - settings.noconfig = Some(true) + settings.noconfig = Some(true); } if args.norun { - settings.norun = Some(true) + settings.norun = Some(true); } if args.nostdin { - settings.nostdin = Some(true) + settings.nostdin = Some(true); } - Ok(settings) + settings } /// Parse `retroarch.cfg` the own configuration file of `RetroArch` itself and create a new /// `Settings` struct out of it. pub fn new_from_retroarch_config( file: &Option, - ) -> Result> { + ) -> Result { let mut settings: Settings = Settings::new(); // If no file was given, then search at `RetroArch` default locations for the file `retroarch.cfg`. settings.retroarch_config = match file { - Some(p) => file::to_fullpath(&p), + Some(p) => file::to_fullpath(p), None => retroarch::search_default_config(), }; @@ -221,9 +230,7 @@ impl Settings { /// [.md, .gen] /// libretro = genesis_plus_gx /// ``` - pub fn new_from_config( - file: &Option, - ) -> Result> { + pub fn new_from_config(file: &Option) -> Result { let mut settings: Settings = Settings::new(); let path: PathBuf = match file { @@ -236,7 +243,7 @@ impl Settings { None => { return Err(format!( "User config ini file not found: {}", - path.display().to_string() + path.display() ) .into()); } @@ -316,7 +323,7 @@ impl Settings { settings: &mut Settings, ini: &ini::Ini, section_names: &[String], - ) -> Result<(), Box> { + ) -> Result<()> { if section_names.contains(&String::from("options")) { if let Some(value) = ini.get("options", "game") { settings.games.push(PathBuf::from(value)); @@ -346,6 +353,14 @@ impl Settings { if let Some(value) = ini.getboolcoerce("options", "which")? { settings.which = Some(value); } + if let Some(value) = + ini.getboolcoerce("options", "which_command")? + { + settings.which = Some(value); + } + if let Some(value) = ini.getboolcoerce("options", "list_cores")? { + settings.list_cores = Some(value); + } if let Some(value) = ini.getboolcoerce("options", "fullscreen")? { settings.fullscreen = Some(value); } @@ -417,13 +432,12 @@ impl Settings { { // libretro = snes9x // Take libretro path directly. - if let Some(path) = ini.get(&pattern_group, "libretro") { + if let Some(path) = ini.get(pattern_group, "libretro") { extension_rules.insert(ext_pattern, PathBuf::from(path)); } // core = snes // Lookup matching libretro path from rules. - else if let Some(core_alias) = - ini.get(&pattern_group, "core") + else if let Some(core_alias) = ini.get(pattern_group, "core") { // [cores] // snes = snes9x @@ -493,10 +507,7 @@ impl Settings { /// Merge current `Settings` with a new one. Overwrite values only, if the new value is /// `Some`. The `games` key is different, as the new list in `games` will be prepended to /// current existing list. - pub fn update_from( - &mut self, - overwrite: Settings, - ) -> Result<(), Box> { + pub fn update_from(&mut self, overwrite: Settings) { if !overwrite.games.is_empty() { if self.games.is_empty() { self.games = overwrite.games; @@ -540,6 +551,12 @@ impl Settings { if overwrite.which.is_some() { self.which = overwrite.which; } + if overwrite.which_command.is_some() { + self.which_command = overwrite.which_command; + } + if overwrite.list_cores.is_some() { + self.list_cores = overwrite.list_cores; + } if overwrite.fullscreen.is_some() { self.fullscreen = overwrite.fullscreen; } @@ -570,17 +587,12 @@ impl Settings { if overwrite.directory_rules.is_some() { self.directory_rules = overwrite.directory_rules; } - - Ok(()) } /// Update current Settings from new Settings. Replace the content only, if the old value is /// `None`. Only a few keys are affected, currently `retroarch`, `retroarch_config`, /// `libretro` and `libretro_directory`. - pub fn update_defaults_from( - &mut self, - overwrite: Settings, - ) -> Result<(), Box> { + pub fn update_defaults_from(&mut self, overwrite: Settings) { if self.retroarch.is_none() { self.retroarch = overwrite.retroarch; } @@ -593,8 +605,6 @@ impl Settings { if self.libretro_directory.is_none() { self.libretro_directory = overwrite.libretro_directory; } - - Ok(()) } /// Build up the final `RetroArch` run command from the current Settings. This is the command @@ -607,14 +617,25 @@ impl Settings { Command::new(&file::to_str(self.retroarch.as_ref())); // `game` + // Get first entry of all games in the list, make it a full path and check if file exists. let game: Option = match self.select_game() { - Some(selected) => file::to_fullpath(&selected), + Some(selected) => { + let path = file::to_fullpath(&selected); + match path { + Some(ref p) => command.arg(p), + None => { + let message = format!( + "game file not found: {}", + selected.display() + ); + return Err(message); + } + }; + + path + } None => return Err("No matching game available".into()), }; - match &game { - Some(path) => command.arg(path), - None => return Err("game file not found.".into()), - }; // `--libretro` let mut libretro: Option = self.libretro.clone(); @@ -635,7 +656,7 @@ impl Settings { // Lookup and resolve from `[/directory]` rules if libretro.is_none() && self.directory_rules.is_some() { libretro = self.libretro_from_dir( - &game + game .as_ref() .expect("game required when building libretro path from directory rules."), ); @@ -643,7 +664,7 @@ impl Settings { // Lookup and resolve from `[.ext]` rules if libretro.is_none() && self.extension_rules.is_some() { libretro = self.libretro_from_ext( - &game + game .as_ref() .expect("game required when building libretro path from extension rules."), ); @@ -701,6 +722,31 @@ impl Settings { Ok(run) } + /// Find core matching the libretro to list of cores. + pub fn find_core_match(&self, libretro: &Path) -> Vec { + let mut core_match: Vec = vec![]; + + if let Some(rules) = &self.cores_rules { + let libretro_string = libretro + .to_path_buf() + .file_stem() + .unwrap() + .to_string_lossy() + .to_string(); + for (core, path) in rules { + let path_string = + path.file_stem().unwrap().to_string_lossy().to_string(); + if path_string.trim_end_matches("_libretro") + == libretro_string.trim_end_matches("_libretro") + { + core_match.push(core.to_string()); + } + } + } + + core_match + } + /// Extract extension from game path and lookup the corresponding extension rule in current /// settings to get the `libretro` path. fn libretro_from_ext(&self, game: &Path) -> Option { @@ -766,14 +812,20 @@ impl Settings { } /// Opens the current `config` file with the associated default application. - pub fn open_config(&self) -> Result> { + pub fn open_config(&self) -> Result { if self.open_config.unwrap_or(false) { let config_path: &PathBuf = self .config .as_ref() .expect("Path to config ini file required."); - file::open_with_default(config_path)?; + match file::to_fullpath(config_path) { + Some(ref path) => { + println!("{}", path.display()); + file::open_with_default(path)?; + } + None => (), + } return Ok(true); } @@ -828,6 +880,27 @@ impl Settings { } } + /// Check if option to print entire command is set. + pub fn is_which_command(&self) -> bool { + self.which_command.unwrap_or(false) + } + + /// Check if option to print cores is set. + pub fn is_list_cores(&self) -> bool { + self.list_cores.unwrap_or(false) + } + + /// Print all name of cores defined in the section [cores] in the config file. + pub fn print_cores(&self) { + if let Some(rules) = self.cores_rules.as_ref() { + let mut keys: Vec = rules.clone().into_keys().collect(); + keys.sort_unstable(); + for core in keys { + println!("{core}"); + } + } + } + /// Check if an instance of `RetroArch` is already running, if the single instance mode /// `highlander` is active. Otherwise its always `false`. #[must_use] @@ -844,8 +917,9 @@ impl Settings { } else { let output: Output = command.output().expect("Error! Could not run RetroArch."); - if output.status.to_string() != *"exit code: 0" { - eprintln!("Could not run RetroArch. {}", output.status) + // if output.status.to_string() != *"exit code: 0" { + if output.status.to_string() != *"exit status: 0" { + eprintln!("Could not run RetroArch. {}", output.status); } Some(output) @@ -857,11 +931,13 @@ impl Settings { mod tests { use std::collections::HashMap; - use std::error::Error; use std::path::PathBuf; use configparser::ini; + type Result> = + std::result::Result; + // Untested: // - Settings::new_from_stdin() // - Settings::new_from_retroarch_config() @@ -876,11 +952,14 @@ mod tests { // - Settings::is_game_available() // - Settings::is_nostdin() // - Settings::print_which() + // - Settings::is_which_command() + // - Settings::list_cores() + // - Settings::print_cores() // - Settings::there_can_only_be_one() // - Settings::run() #[test] - fn new_from_defaults_retroarch() -> Result<(), Box> { + fn new_from_defaults_retroarch() -> Result<()> { let settings = super::Settings { games: vec![], retroarch_arguments: vec![], @@ -892,6 +971,8 @@ mod tests { core: None, filter: None, which: None, + which_command: None, + list_cores: None, fullscreen: None, highlander: None, open_config: None, @@ -903,7 +984,7 @@ mod tests { directory_rules: None, }; - let defaults = super::Settings::new_from_defaults()?; + let defaults = super::Settings::new_from_defaults(); assert_eq!(settings.retroarch, defaults.retroarch); @@ -911,13 +992,13 @@ mod tests { } #[test] - fn new_from_cmdline_default_config() -> Result<(), Box> { + fn new_from_cmdline_default_config() -> Result<()> { let mut options: Vec = vec![]; options.push("enjoy".to_string()); let test_config = Some(PathBuf::from("~/.config/enjoy/default.ini")); - let args = super::Settings::new_from_cmdline(Some(options))?; + let args = super::Settings::new_from_cmdline(Some(options)); assert_eq!(test_config, args.config); assert_eq!(None, args.norun); @@ -926,15 +1007,14 @@ mod tests { } #[test] - fn new_from_cmdline_emptygame_then_retroarch() -> Result<(), Box> - { + fn new_from_cmdline_emptygame_then_retroarch() -> Result<()> { let mut options: Vec = vec![]; options.push("enjoy".to_string()); options.push("".to_string()); options.push("--retroarch".to_string()); options.push("/usr/bin/retroarch".to_string()); - let args = super::Settings::new_from_cmdline(Some(options))?; + let args = super::Settings::new_from_cmdline(Some(options)); assert_eq!(Some(PathBuf::from("/usr/bin/retroarch")), args.retroarch); assert_eq!(vec![PathBuf::from("")], args.games); @@ -943,7 +1023,7 @@ mod tests { } #[test] - fn new_from_cmdline_game() -> Result<(), Box> { + fn new_from_cmdline_game() -> Result<()> { let mut options: Vec = vec![]; options.push("enjoy".to_string()); options.push("mario.smc".to_string()); @@ -953,7 +1033,7 @@ mod tests { test_games.push(PathBuf::from("mario.smc")); test_games.push(PathBuf::from("")); - let args = super::Settings::new_from_cmdline(Some(options))?; + let args = super::Settings::new_from_cmdline(Some(options)); assert_eq!(test_games, args.games); @@ -1021,8 +1101,7 @@ mod tests { } #[test] - fn read_config_options_retroarch_arguments() -> Result<(), Box> - { + fn read_config_options_retroarch_arguments() -> Result<()> { let mut settings = super::Settings::new(); let ini = test_ini_template(); @@ -1041,7 +1120,7 @@ mod tests { } #[test] - fn read_config_options_path() -> Result<(), Box> { + fn read_config_options_path() -> Result<()> { let mut settings = super::Settings::new(); let ini = test_ini_template(); @@ -1062,7 +1141,7 @@ mod tests { } #[test] - fn read_config_options_bool() -> Result<(), Box> { + fn read_config_options_bool() -> Result<()> { let mut settings = super::Settings::new(); let ini = test_ini_template(); @@ -1079,7 +1158,7 @@ mod tests { } #[test] - fn read_config_cores_rules() -> Result<(), Box> { + fn read_config_cores_rules() -> Result<()> { let ini = test_ini_template(); let rules = super::Settings::read_config_cores_rules(&ini); @@ -1131,7 +1210,7 @@ mod tests { } #[test] - fn update_from() -> Result<(), Box> { + fn update_from() -> Result<()> { let mut old = super::Settings::new(); let new = super::Settings { games: vec![], @@ -1144,6 +1223,8 @@ mod tests { core: None, filter: Some("[!]".to_string()), which: None, + which_command: None, + list_cores: None, fullscreen: None, highlander: Some(true), open_config: None, @@ -1155,7 +1236,7 @@ mod tests { directory_rules: None, }; - old.update_from(new)?; + old.update_from(new); let updated = old; assert_eq!(Some(PathBuf::from("retroarch")), updated.retroarch); @@ -1185,6 +1266,8 @@ mod tests { core: None, filter: None, which: None, + which_command: None, + list_cores: None, fullscreen: None, highlander: None, open_config: None, diff --git a/src/settings/arguments.rs b/src/settings/arguments.rs index 5ad026f..ac46f9d 100644 --- a/src/settings/arguments.rs +++ b/src/settings/arguments.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use structopt::StructOpt; +use clap::Parser; /// Play any game ROM with associated emulator in `"RetroArch"`. /// @@ -15,9 +15,13 @@ use structopt::StructOpt; /// /// $ enjoy '~/roms/snes/Super Mario World (U) [!].smc' /// +/// $ enjoy 'Super Mario World (U) [!].smc' -w +/// /// $ ls -1 ./snes/* | enjoy --filter '[!]' --core snes --which --highlander +/// +/// $ ls -1 $(readlink -f ~/roms/gb)/* | enjoy -xWn #[allow(clippy::struct_excessive_bools)] -#[derive(Debug, StructOpt)] +#[derive(Debug, Parser)] pub(crate) struct Opt { /// Path to ROM file /// @@ -27,7 +31,7 @@ pub(crate) struct Opt { /// and expanded. /// /// Example: "~/roms/snes/Super Mario World (U) [\!].smc" - #[structopt(parse(from_os_str))] + #[clap(parse(from_os_str))] pub(crate) games: Vec, /// Bypass additional arguments to `retroarch` @@ -38,7 +42,7 @@ pub(crate) struct Opt { /// ensure correctness. /// /// Example: "-- --set-shader ''" - #[structopt(last = true)] + #[clap(last = true)] pub(crate) retroarch_arguments: Vec, /// Path to the user settings @@ -48,8 +52,8 @@ pub(crate) struct Opt { /// higher priority over the individual settings in this file. /// /// Example: "/home/user/.config/enjoy/alternative.ini" - #[structopt( - short = "c", + #[clap( + short = 'c', long, parse(from_os_str), value_name = "FILE", @@ -64,8 +68,8 @@ pub(crate) struct Opt { /// filename without directory part, then the systems `$PATH` is searched. /// /// Example: "/usr/bin/retroarch" [default: retroarch] - #[structopt( - short = "A", + #[clap( + short = 'A', long, parse(from_os_str), value_name = "APP", @@ -81,8 +85,8 @@ pub(crate) struct Opt { /// `$HOME/.config/retroarch/retroarch.cfg`, `$HOME/.retroarch.cfg`. /// /// Example: "/home/user/.config/retroarch/retroarch.cfg" - #[structopt( - short = "B", + #[clap( + short = 'B', long, parse(from_os_str), value_name = "FILE", @@ -100,8 +104,8 @@ pub(crate) struct Opt { /// `/home/user/.config/retroarch/cores/snes9x_libretro.so`. /// /// Example: "snes9x" - #[structopt( - short = "L", + #[clap( + short = 'L', long, parse(from_os_str), value_name = "FILE", @@ -117,8 +121,8 @@ pub(crate) struct Opt { /// configuration file `retroarch.cfg`. /// /// Example: "/home/user/.config/retroarch/cores" - #[structopt( - short = "D", + #[clap( + short = 'D', long, parse(from_os_str), value_name = "DIR", @@ -133,7 +137,7 @@ pub(crate) struct Opt { /// `[cores]` as `alias=libretro_path`. /// /// Example: "snes" - #[structopt(short = "C", long, value_name = "ALIAS", display_order = 3)] + #[clap(short = 'C', long, value_name = "ALIAS", display_order = 3)] pub(crate) core: Option, /// Apply simple wildcard to filter list of games @@ -146,32 +150,47 @@ pub(crate) struct Opt { /// useful if more than one game entry is given to the program. /// /// Example: "mario*[\!]" - #[structopt(short = "f", long, value_name = "PATTERN", display_order = 2)] + #[clap(short = 'f', long, value_name = "PATTERN", display_order = 2)] pub(crate) filter: Option, /// Print selected game ROM /// /// Writes the full filepath of the selected game to stdout. - #[structopt(short = "w", long, display_order = 1)] + #[clap(short = 'w', long, display_order = 1)] pub(crate) which: bool, + /// Print RetroArch commandline + /// + /// Writes full command with all arguments used to run RetroArch to stdout. Has higher priority + /// than option --which. + #[clap(short = 'W', long, display_order = 1)] + pub(crate) which_command: bool, + + /// Print all core names + /// + /// Lists all core names on the left side of the user configuration under section "[cores]". + /// Will output matching cores to the libretro core that would be used with the game. Without + /// a game, all cores are listed. + #[clap(short = 'n', long, display_order = 2)] + pub(crate) list_cores: bool, + /// Force fullscreen mode /// /// Runs the emulator and `RetroArch` UI in fullscreen, regardless of any other setting. - #[structopt(short = "F", long, display_order = 2)] + #[clap(short = 'F', long, display_order = 2)] pub(crate) fullscreen: bool, /// There Can Only Be One! /// /// Prevents running another `retroarch` process, if one is already active. In this case the /// final command of the emulator will not execute. - #[structopt(short = "1", long, display_order = 2)] + #[clap(short = '1', long, display_order = 2)] pub(crate) highlander: bool, /// Show user settings /// /// Opens the user config INI file with it's associated default application. - #[structopt(short = "o", long, display_order = 3)] + #[clap(short = 'o', long, display_order = 3)] pub(crate) open_config: bool, /// Ignore user settings @@ -179,8 +198,8 @@ pub(crate) struct Opt { /// The config INI file of this program will be ignored and not loaded up. The entire /// application relies on commandline options and environmental variables. Therefore any /// predefined rules and aliases from that file are ignored. - #[structopt( - short = "i", + #[clap( + short = 'i', long, display_order = 4, conflicts_with_all = &["config", "open-config", "core"] @@ -191,7 +210,7 @@ pub(crate) struct Opt { /// /// The `retroarch` run command to play ROMs will not be executed. Internally the process is /// still simulated, up until to the point of running the emulator. - #[structopt(short = "x", long, display_order = 4)] + #[clap(short = 'x', long, display_order = 4)] pub(crate) norun: bool, /// Dismiss reading from stdin @@ -199,6 +218,6 @@ pub(crate) struct Opt { /// Ignores the `stdin` and do not test or read any data from it. Normally the program will /// look and read all lines from `stdin` as additional game entries. This option will disable /// that. - #[structopt(short = "z", long, display_order = 4)] + #[clap(short = 'z', long, display_order = 4)] pub(crate) nostdin: bool, } diff --git a/src/settings/file.rs b/src/settings/file.rs index 3cabef2..daff4e7 100644 --- a/src/settings/file.rs +++ b/src/settings/file.rs @@ -14,7 +14,7 @@ pub(crate) fn open_with_default(file: &Path) -> Result<(), Box> { } else { return Err(format!( "Path to config is not accessible or a file: {}", - fullpath.display().to_string() + fullpath.display() ) .into()); }