Skip to content

Commit

Permalink
#441: Add '--fuzzy' and '--multiple' options for 'find' command
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Jan 3, 2025
1 parent 875f2fe commit cbda45a
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 89 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
which includes the serialized registry content in the output.
This may be useful if you're consuming the `--api` output to back up with another tool,
but don't have a good way to check the registry keys directly.
* CLI: The `find` command now supports `--fuzzy` and `--multiple` options.
This is also available for the `api` command's `findTitle` request.
* Changed:
* When the game list is filtered,
the summary line (e.g., "1 of 10 games") now reflects the filtered totals.
Expand All @@ -32,6 +34,8 @@
Previously, Ludusavi would make a backup folder for the game including the space,
but the OS (namely Windows) would remove the space from the folder title,
causing unpredictable behavior when Ludusavi couldn't find the expected folder name.
* CLI: `find --normalized` now better prioritizes the closest match
when multiple manifest entries have the same normalized title.

## v0.27.0 (2024-11-19)

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ sha1 = "0.10.6"
shlex = "1.3.0"
signal-hook = "0.3.17"
steamlocate = "2.0.0"
strsim = "0.11.1"
tokio = { version = "1.40.0", features = ["macros", "time"] }
typed-path = "0.9.2"
unic-langid = "0.9.5"
Expand Down
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,13 +597,15 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
}
Subcommand::Find {
api,
multiple,
path,
backup,
restore,
steam_id,
gog_id,
lutris_id,
normalized,
fuzzy,
disabled,
partial,
names,
Expand All @@ -623,11 +625,13 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)

let title_finder = TitleFinder::new(&config, &manifest, layout.restorable_game_set());
let found = title_finder.find(TitleQuery {
multiple,
names: names.clone(),
steam_id,
gog_id,
lutris_id,
normalized,
fuzzy,
backup,
restore,
disabled,
Expand Down
32 changes: 24 additions & 8 deletions src/cli/api.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::io::Read;

use itertools::Itertools;

use crate::{
lang::TRANSLATOR,
path::StrictPath,
prelude::Error,
resource::{config::Config, manifest::Manifest},
scan::{layout::BackupLayout, TitleFinder, TitleQuery},
scan::{compare_ranked_titles, layout::BackupLayout, TitleFinder, TitleQuery},
};

/// The full input to the `api` command.
Expand Down Expand Up @@ -63,14 +65,19 @@ pub mod request {
///
/// Precedence: Steam ID -> GOG ID -> Lutris ID -> exact names -> normalized names.
/// Once a match is found for one of these options,
/// Ludusavi will stop looking and return that match.
/// Ludusavi will stop looking and return that match,
/// unless you set `multiple: true`, in which case,
/// the results will be sorted by how well they match.
///
/// Depending on the options chosen, there may be multiple matches, but the default is a single match.
///
/// Aliases will be resolved to the target title.
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct FindTitle {
/// Keep looking for all potential matches,
/// instead of stopping at the first match.
pub multiple: bool,
/// Ensure the game is recognized in a backup context.
pub backup: bool,
/// Ensure the game is recognized in a restore context.
Expand All @@ -85,6 +92,9 @@ pub mod request {
/// Ignores capitalization, "edition" suffixes, year suffixes, and some special symbols.
/// This may find multiple games for a single input.
pub normalized: bool,
/// Look up games with fuzzy matching.
/// This may find multiple games for a single input.
pub fuzzy: bool,
/// Select games that are disabled.
pub disabled: bool,
/// Select games that have some saves disabled.
Expand All @@ -101,8 +111,6 @@ pub mod request {
}

pub mod response {
use std::collections::BTreeSet;

#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct Error {
Expand All @@ -114,7 +122,7 @@ pub mod response {
#[serde(default, rename_all = "camelCase")]
pub struct FindTitle {
/// Any matching titles found.
pub titles: BTreeSet<String>,
pub titles: Vec<String>,
}

#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
Expand Down Expand Up @@ -185,28 +193,38 @@ pub fn process(input: Option<String>, config: &Config, manifest: &Manifest) -> R
for request in input.requests {
match request {
Request::FindTitle(request::FindTitle {
multiple,
backup,
restore,
steam_id,
gog_id,
lutris_id,
normalized,
fuzzy,
disabled,
partial,
names,
}) => {
let titles = title_finder.find(TitleQuery {
multiple,
names,
steam_id,
gog_id,
lutris_id,
normalized,
fuzzy,
backup,
restore,
disabled,
partial,
});

let titles: Vec<_> = titles
.into_iter()
.sorted_by(compare_ranked_titles)
.map(|(name, _info)| name)
.collect();

responses.push(Response::FindTitle(response::FindTitle { titles }));
}
Request::CheckAppUpdate(request::CheckAppUpdate {}) => match crate::metadata::Release::fetch_sync() {
Expand All @@ -230,8 +248,6 @@ pub fn process(input: Option<String>, config: &Config, manifest: &Manifest) -> R

#[cfg(test)]
mod tests {
use std::collections::BTreeSet;

use super::*;
use pretty_assertions::assert_eq;

Expand Down Expand Up @@ -270,7 +286,7 @@ mod tests {
pub fn serialize_output() {
let output = Output::Success {
responses: vec![Response::FindTitle(response::FindTitle {
titles: BTreeSet::from(["foo".to_string()]),
titles: vec!["foo".to_string()],
})],
};
let serialized = serde_json::to_string_pretty(&output).unwrap();
Expand Down
20 changes: 19 additions & 1 deletion src/cli/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,9 @@ pub enum Subcommand {
///
/// Precedence: Steam ID -> GOG ID -> Lutris ID -> exact names -> normalized names.
/// Once a match is found for one of these options,
/// Ludusavi will stop looking and return that match.
/// Ludusavi will stop looking and return that match,
/// unless you set `--multiple`, in which case,
/// the results will be sorted by how well they match.
///
/// If there are no matches, Ludusavi will exit with an error.
/// Depending on the options chosen, there may be multiple matches, but the default is a single match.
Expand All @@ -315,6 +317,11 @@ pub enum Subcommand {
#[clap(long)]
api: bool,

/// Keep looking for all potential matches,
/// instead of stopping at the first match.
#[clap(long)]
multiple: bool,

/// Directory in which to find backups.
/// When unset, this defaults to the restore path from the config file.
#[clap(long, value_parser = parse_strict_path)]
Expand Down Expand Up @@ -346,6 +353,11 @@ pub enum Subcommand {
#[clap(long)]
normalized: bool,

/// Look up games with fuzzy matching.
/// This may find multiple games for a single input.
#[clap(long)]
fuzzy: bool,

/// Select games that are disabled.
#[clap(long)]
disabled: bool,
Expand Down Expand Up @@ -1153,13 +1165,15 @@ mod tests {
try_manifest_update: false,
sub: Some(Subcommand::Find {
api: false,
multiple: false,
path: None,
backup: false,
restore: false,
steam_id: None,
gog_id: None,
lutris_id: None,
normalized: false,
fuzzy: false,
disabled: false,
partial: false,
names: vec![],
Expand All @@ -1175,6 +1189,7 @@ mod tests {
"ludusavi",
"find",
"--api",
"--multiple",
"--path",
"tests/backup",
"--backup",
Expand All @@ -1186,6 +1201,7 @@ mod tests {
"--lutris-id",
"slug",
"--normalized",
"--fuzzy",
"--disabled",
"--partial",
"game1",
Expand All @@ -1197,13 +1213,15 @@ mod tests {
try_manifest_update: false,
sub: Some(Subcommand::Find {
api: true,
multiple: true,
path: Some(StrictPath::relative(s("tests/backup"), Some(repo_raw()))),
backup: true,
restore: true,
steam_id: Some(101),
gog_id: Some(102),
lutris_id: Some("slug".to_string()),
normalized: true,
fuzzy: true,
disabled: true,
partial: true,
names: vec![s("game1"), s("game2")],
Expand Down
22 changes: 15 additions & 7 deletions src/cli/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use crate::{
prelude::StrictPath,
resource::manifest::Os,
scan::{
layout::Backup, registry, BackupError, BackupInfo, DuplicateDetector, OperationStatus, OperationStepDecision,
ScanChange, ScanInfo,
compare_ranked_titles_ref, layout::Backup, registry, BackupError, BackupInfo, DuplicateDetector,
OperationStatus, OperationStepDecision, ScanChange, ScanInfo, TitleMatch,
},
};

Expand Down Expand Up @@ -177,7 +177,11 @@ enum ApiGame {
backups: Vec<ApiBackup>,
},
/// Used by the `find` command.
Found {},
Found {
/// How well the title matches the query.
/// Range: 0.0 to 1.0 (higher is better).
score: Option<f64>,
},
}

#[derive(Debug, serde::Serialize, schemars::JsonSchema)]
Expand Down Expand Up @@ -599,16 +603,20 @@ impl Reporter {
}
}

pub fn add_found_titles(&mut self, names: &BTreeSet<String>) {
pub fn add_found_titles(&mut self, games: &BTreeMap<String, TitleMatch>) {
match self {
Self::Standard { parts, .. } => {
for name in names {
let games: Vec<_> = games.iter().sorted_by(compare_ranked_titles_ref).collect();

for (name, _info) in games {
parts.push(name.to_owned());
}
}
Self::Json { output } => {
for name in names {
output.games.insert(name.to_owned(), ApiGame::Found {});
for (name, info) in games {
output
.games
.insert(name.to_owned(), ApiGame::Found { score: info.score });
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub use self::{
preview::ScanInfo,
saves::{ScannedFile, ScannedRegistry, ScannedRegistryValue, ScannedRegistryValues},
steam::{SteamShortcut, SteamShortcuts},
title::{TitleFinder, TitleQuery},
title::{compare_ranked_titles, compare_ranked_titles_ref, TitleFinder, TitleMatch, TitleQuery},
};

use crate::{
Expand Down
Loading

0 comments on commit cbda45a

Please sign in to comment.