diff --git a/CHANGELOG.md b/CHANGELOG.md index 198565b9f9..7b583fbd58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - [GUI] ru-RU translation (#3383 by: nt0g; reviewed: HebaruSan) - [GUI] Japanese Localization (#3394 by: utah239; reviewed: HebaruSan) - [Multiple] Match underscore in DLL to dash in identifier (#3412 by: HebaruSan; reviewed: DasSkelett) +- [CLI] Add versions table option to ckan show command (#3414 by: HebaruSan; reviewed: DasSkelett) ### Bugfixes diff --git a/Cmdline/Action/Search.cs b/Cmdline/Action/Search.cs index 4064480f01..5fc4d40c95 100644 --- a/Cmdline/Action/Search.cs +++ b/Cmdline/Action/Search.cs @@ -122,6 +122,8 @@ public int RunCommand(CKAN.GameInstance ksp, object raw_options) /// List of matching modules. /// The KSP instance to perform the search for. /// The search term. Case insensitive. + /// Name of author to find + /// True to look for incompatible modules, false (default) to look for compatible public List PerformSearch(CKAN.GameInstance ksp, string term, string author = null, bool searchIncompatible = false) { // Remove spaces and special characters from the search term. @@ -134,29 +136,25 @@ public List PerformSearch(CKAN.GameInstance ksp, string term, string { return registry .CompatibleModules(ksp.VersionCriteria()) - .Where((module) => - { // Look for a match in each string. - return (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1) - && module.SearchableAuthors.Any((auth) => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1); - }).ToList(); + .Where(module => (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1) + && module.SearchableAuthors.Any((auth) => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1)) + .ToList(); } else { return registry .IncompatibleModules(ksp.VersionCriteria()) - .Where((module) => - { // Look for a match in each string. - return (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 - || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1) - && module.SearchableAuthors.Any((auth) => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1); - }).ToList(); + .Where(module => (module.SearchableName.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableIdentifier.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableAbstract.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 + || module.SearchableDescription.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1) + && module.SearchableAuthors.Any((auth) => auth.IndexOf(author, StringComparison.OrdinalIgnoreCase) > -1)) + .ToList(); } } diff --git a/Cmdline/Action/Show.cs b/Cmdline/Action/Show.cs index e7c55d6ec9..635bfcf12a 100644 --- a/Cmdline/Action/Show.cs +++ b/Cmdline/Action/Show.cs @@ -3,12 +3,12 @@ using System.Linq; using System.Text; +using CKAN.Versioning; + namespace CKAN.CmdLine { public class Show : ICommand { - public IUser user { get; set; } - public Show(IUser user) { this.user = user; @@ -17,80 +17,103 @@ public Show(IUser user) public int RunCommand(CKAN.GameInstance ksp, object raw_options) { ShowOptions options = (ShowOptions) raw_options; - - if (options.Modname == null) + if (options.modules == null || options.modules.Count < 1) { // empty argument user.RaiseMessage("show - module name argument missing, perhaps you forgot it?"); return Exit.BADOPT; } + int combined_exit_code = Exit.OK; // Check installed modules for an exact match. var registry = RegistryManager.Instance(ksp).registry; - var installedModuleToShow = registry.InstalledModule(options.Modname); - - if (installedModuleToShow != null) - { - // Show the installed module. - return ShowMod(installedModuleToShow); - } - - // Module was not installed, look for an exact match in the available modules, - // either by "name" (the user-friendly display name) or by identifier - CkanModule moduleToShow = registry - .CompatibleModules(ksp.VersionCriteria()) - .SingleOrDefault( - mod => mod.name == options.Modname - || mod.identifier == options.Modname - ); - - if (moduleToShow == null) + foreach (string modName in options.modules) { - // No exact match found. Try to look for a close match for this KSP version. - user.RaiseMessage("{0} not found or installed.", options.Modname); - user.RaiseMessage("Looking for close matches in mods compatible with KSP {0}.", ksp.Version()); - - Search search = new Search(user); - var matches = search.PerformSearch(ksp, options.Modname); - - // Display the results of the search. - if (!matches.Any()) - { - // No matches found. - user.RaiseMessage("No close matches found."); - return Exit.BADOPT; - } - else if (matches.Count() == 1) + var installedModuleToShow = registry.InstalledModule(modName); + if (installedModuleToShow != null) { - // If there is only 1 match, display it. - user.RaiseMessage("Found 1 close match: {0}", matches[0].name); + // Show the installed module. + combined_exit_code = CombineExitCodes( + combined_exit_code, + ShowMod(installedModuleToShow) + ); + if (options.with_versions) + { + ShowVersionTable(ksp, registry.AvailableByIdentifier(installedModuleToShow.identifier).ToList()); + } user.RaiseMessage(""); - - moduleToShow = matches[0]; + continue; } - else - { - // Display the found close matches. - string[] strings_matches = new string[matches.Count]; - for (int i = 0; i < matches.Count; i++) + // Module was not installed, look for an exact match in the available modules, + // either by "name" (the user-friendly display name) or by identifier + CkanModule moduleToShow = registry + .CompatibleModules(ksp.VersionCriteria()) + .SingleOrDefault( + mod => mod.name == modName + || mod.identifier == modName + ); + if (moduleToShow == null) + { + // No exact match found. Try to look for a close match for this KSP version. + user.RaiseMessage( + "{0} not installed or compatible with {1} {2}.", + modName, + ksp.game.ShortName, + string.Join(", ", ksp.VersionCriteria().Versions.Select(v => v.ToString())) + ); + user.RaiseMessage("Looking for close matches in compatible mods..."); + + Search search = new Search(user); + var matches = search.PerformSearch(ksp, modName); + + // Display the results of the search. + if (!matches.Any()) { - strings_matches[i] = matches[i].name; + // No matches found. + user.RaiseMessage("No close matches found."); + combined_exit_code = CombineExitCodes(combined_exit_code, Exit.BADOPT); + user.RaiseMessage(""); + continue; } + else if (matches.Count() == 1) + { + // If there is only 1 match, display it. + user.RaiseMessage("Found 1 close match: {0}", matches[0].name); + user.RaiseMessage(""); - int selection = user.RaiseSelectionDialog("Close matches", strings_matches); - - if (selection < 0) + moduleToShow = matches[0]; + } + else { - return Exit.BADOPT; + // Display the found close matches. + int selection = user.RaiseSelectionDialog( + "Close matches:", + matches.Select(m => m.name).ToArray() + ); + user.RaiseMessage(""); + if (selection < 0) + { + combined_exit_code = CombineExitCodes(combined_exit_code, Exit.BADOPT); + continue; + } + + // Mark the selection as the one to show. + moduleToShow = matches[selection]; } + } - // Mark the selection as the one to show. - moduleToShow = matches[selection]; + combined_exit_code = CombineExitCodes( + combined_exit_code, + ShowMod(moduleToShow) + ); + if (options.with_versions) + { + ShowVersionTable(ksp, registry.AvailableByIdentifier(moduleToShow.identifier).ToList()); } + user.RaiseMessage(""); } - - return ShowMod(moduleToShow); + return combined_exit_code; } /// @@ -103,16 +126,20 @@ public int ShowMod(InstalledModule module) // Display the basic info. int return_value = ShowMod(module.Module); - // Display InstalledModule specific information. - ICollection files = module.Files as ICollection; - if (files == null) throw new InvalidCastException(); - if (!module.Module.IsDLC) { - user.RaiseMessage("\r\nShowing {0} installed files:", files.Count); + // Display InstalledModule specific information. + ICollection files = module.Files as ICollection; + if (files == null) + { + throw new InvalidCastException(); + } + + user.RaiseMessage(""); + user.RaiseMessage("Showing {0} installed files:", files.Count); foreach (string file in files) { - user.RaiseMessage("- {0}", file); + user.RaiseMessage(" - {0}", file); } } @@ -138,89 +165,124 @@ public int ShowMod(CkanModule module) if (!string.IsNullOrEmpty(module.description)) { - user.RaiseMessage("\r\n{0}\r\n", module.description); + user.RaiseMessage(""); + user.RaiseMessage("{0}", module.description); } #endregion #region General info (author, version...) - user.RaiseMessage("\r\nModule info:"); - user.RaiseMessage("- version:\t{0}", module.version); + user.RaiseMessage(""); + user.RaiseMessage("Module info:"); + user.RaiseMessage(" Version:\t{0}", module.version); if (module.author != null) { - user.RaiseMessage("- authors:\t{0}", string.Join(", ", module.author)); + user.RaiseMessage(" Authors:\t{0}", string.Join(", ", module.author)); } else { // Did you know that authors are optional in the spec? // You do now. #673. - user.RaiseMessage("- authors:\tUNKNOWN"); + user.RaiseMessage(" Authors:\tUNKNOWN"); } - user.RaiseMessage("- status:\t{0}", module.release_status); - user.RaiseMessage("- license:\t{0}", string.Join(", ", module.license)); + if (module.release_status != null) + { + user.RaiseMessage(" Status:\t{0}", module.release_status); + } + user.RaiseMessage(" License:\t{0}", string.Join(", ", module.license)); + if (module.Tags != null && module.Tags.Count > 0) + { + // Need an extra space before the tab to line up with other fields + user.RaiseMessage(" Tags: \t{0}", string.Join(", ", module.Tags)); + } + if (module.localizations != null && module.localizations.Length > 0) + { + user.RaiseMessage(" Languages:\t{0}", string.Join(", ", module.localizations.OrderBy(l => l))); + } #endregion #region Relationships if (module.depends != null && module.depends.Count > 0) { - user.RaiseMessage("\r\nDepends:"); + user.RaiseMessage(""); + user.RaiseMessage("Depends:"); foreach (RelationshipDescriptor dep in module.depends) - user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + { + user.RaiseMessage(" - {0}", RelationshipToPrintableString(dep)); + } } if (module.recommends != null && module.recommends.Count > 0) { - user.RaiseMessage("\r\nRecommends:"); + user.RaiseMessage(""); + user.RaiseMessage("Recommends:"); foreach (RelationshipDescriptor dep in module.recommends) - user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + { + user.RaiseMessage(" - {0}", RelationshipToPrintableString(dep)); + } } if (module.suggests != null && module.suggests.Count > 0) { - user.RaiseMessage("\r\nSuggests:"); + user.RaiseMessage(""); + user.RaiseMessage("Suggests:"); foreach (RelationshipDescriptor dep in module.suggests) - user.RaiseMessage("- {0}", RelationshipToPrintableString(dep)); + { + user.RaiseMessage(" - {0}", RelationshipToPrintableString(dep)); + } } - if (module.ProvidesList != null && module.ProvidesList.Count > 0) + if (module.provides != null && module.provides.Count > 0) { - user.RaiseMessage("\r\nProvides:"); - foreach (string prov in module.ProvidesList) - user.RaiseMessage("- {0}", prov); + user.RaiseMessage(""); + user.RaiseMessage("Provides:"); + foreach (string prov in module.provides) + { + user.RaiseMessage(" - {0}", prov); + } } #endregion - user.RaiseMessage("\r\nResources:"); if (module.resources != null) { - if (module.resources.bugtracker != null) + user.RaiseMessage(""); + user.RaiseMessage("Resources:"); + if (module.resources.homepage != null) { - user.RaiseMessage("- bugtracker: {0}", Uri.EscapeUriString(module.resources.bugtracker.ToString())); + user.RaiseMessage(" Home page:\t{0}", Uri.EscapeUriString(module.resources.homepage.ToString())); } - if (module.resources.homepage != null) + if (module.resources.manual != null) { - user.RaiseMessage("- homepage: {0}", Uri.EscapeUriString(module.resources.homepage.ToString())); + user.RaiseMessage(" Manual: {0}", Uri.EscapeUriString(module.resources.manual.ToString())); } if (module.resources.spacedock != null) { - user.RaiseMessage("- spacedock: {0}", Uri.EscapeUriString(module.resources.spacedock.ToString())); + user.RaiseMessage(" SpaceDock:\t{0}", Uri.EscapeUriString(module.resources.spacedock.ToString())); } if (module.resources.repository != null) { - user.RaiseMessage("- repository: {0}", Uri.EscapeUriString(module.resources.repository.ToString())); + user.RaiseMessage(" Repository:\t{0}", Uri.EscapeUriString(module.resources.repository.ToString())); + } + if (module.resources.bugtracker != null) + { + user.RaiseMessage(" Bug tracker:\t{0}", Uri.EscapeUriString(module.resources.bugtracker.ToString())); } if (module.resources.curse != null) { - user.RaiseMessage("- curse: {0}", Uri.EscapeUriString(module.resources.curse.ToString())); + user.RaiseMessage(" Curse: {0}", Uri.EscapeUriString(module.resources.curse.ToString())); } if (module.resources.store != null) { - user.RaiseMessage("- store: {0}", Uri.EscapeUriString(module.resources.store.ToString())); + user.RaiseMessage(" Store:\t{0}", Uri.EscapeUriString(module.resources.store.ToString())); } if (module.resources.steamstore != null) { - user.RaiseMessage("- steamstore: {0}", Uri.EscapeUriString(module.resources.steamstore.ToString())); + user.RaiseMessage(" Steam store:\t{0}", Uri.EscapeUriString(module.resources.steamstore.ToString())); + } + if (module.resources.remoteAvc != null) + { + user.RaiseMessage(" Version file: {0}", Uri.EscapeUriString(module.resources.remoteAvc.ToString())); } } @@ -230,12 +292,52 @@ public int ShowMod(CkanModule module) string file_uri_hash = NetFileCache.CreateURLHash(module.download); string file_name = CkanModule.StandardName(module.identifier, module.version); - user.RaiseMessage("\r\nFilename: {0}", file_uri_hash + "-" + file_name); + user.RaiseMessage(""); + user.RaiseMessage("Filename: {0}", file_uri_hash + "-" + file_name); } return Exit.OK; } + private static int CombineExitCodes(int a, int b) + { + // Failures should dominate, keep whichever one isn't OK + return a == Exit.OK ? b : a; + } + + private void ShowVersionTable(CKAN.GameInstance inst, List modules) + { + var versions = modules.Select(m => m.version.ToString()).ToList(); + var gameVersions = modules.Select(m => + { + GameVersion minKsp = null, maxKsp = null; + Registry.GetMinMaxVersions(new List() { m }, out _, out _, out minKsp, out maxKsp); + return GameVersionRange.VersionSpan(inst.game, minKsp, maxKsp); + }).ToList(); + string[] headers = new string[] { "Version", "Game Versions" }; + int versionLength = Math.Max(headers[0].Length, versions.Max(v => v.Length)); + int gameVersionLength = Math.Max(headers[1].Length, gameVersions.Max(v => v.Length)); + user.RaiseMessage(""); + user.RaiseMessage( + "{0} {1}", + headers[0].PadRight(versionLength), + headers[1].PadRight(gameVersionLength) + ); + user.RaiseMessage( + "{0} {1}", + new string('-', versionLength), + new string('-', gameVersionLength) + ); + for (int row = 0; row < versions.Count; ++row) + { + user.RaiseMessage( + "{0} {1}", + versions[row].PadRight(versionLength), + gameVersions[row].PadRight(gameVersionLength) + ); + } + } + /// /// Formats a RelationshipDescriptor into a user-readable string: /// Name, version: x, min: x, max: x @@ -246,5 +348,7 @@ private static string RelationshipToPrintableString(RelationshipDescriptor dep) sb.Append(dep.ToString()); return sb.ToString(); } + + private IUser user { get; set; } } } diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs index 0e88b1a902..1ce6a70238 100644 --- a/Cmdline/Options.cs +++ b/Cmdline/Options.cs @@ -516,7 +516,11 @@ internal class ImportOptions : InstanceSpecificOptions internal class ShowOptions : InstanceSpecificOptions { - [ValueOption(0)] public string Modname { get; set; } + [Option("with-versions")] + public bool with_versions { get; set; } + + [ValueList(typeof(List))] + public List modules { get; set; } } internal class SearchOptions : InstanceSpecificOptions