[SKIP_DEPLOY] Support account alias in configs

This commit is contained in:
BruceChen 2022-11-06 16:20:38 +08:00
parent c0266685a8
commit 0b5a562f7f
17 changed files with 213 additions and 157 deletions

View file

@ -9,7 +9,7 @@ body:
attributes:
label: Prerequisites
options:
- label: I made sure I am running the latest [development build](https://ci.appveyor.com/project/ORelio/minecraft-console-client/build/artifacts)
- label: I made sure I am running the latest [development build](https://github.com/MCCTeam/Minecraft-Console-Client/releases/latest)
required: true
- label: I tried to [look for similar issues](https://github.com/MCCTeam/Minecraft-Console-Client/issues?q=is%3Aissue) before opening a new one
required: true
@ -95,4 +95,4 @@ body:
- type: markdown
id: credit
attributes:
value: Thank you for filling the bug report. Feel free to submit the report to us.
value: Thank you for filling the bug report. Feel free to submit the report to us.

View file

@ -10,9 +10,9 @@ body:
attributes:
label: Prerequisites
options:
- label: I have read and understood the [user manual](https://github.com/MCCTeam/Minecraft-Console-Client/tree/master/MinecraftClient/config)
- label: I have read and understood the [user manual](https://mccteam.github.io/guide/)
required: true
- label: I made sure I am running the latest [development build](https://ci.appveyor.com/project/ORelio/Minecraft-Console-Client/build/artifacts)
- label: I made sure I am running the latest [development build](https://github.com/MCCTeam/Minecraft-Console-Client/releases/latest)
required: true
- label: I tried to [look for similar feature requests](https://github.com/MCCTeam/Minecraft-Console-Client/issues?q=is%3Aissue) before opening a new one
required: true
@ -69,4 +69,4 @@ body:
- type: markdown
id: credit
attributes:
value: Thank you for filling the request form. Feel free to submit the request to us.
value: Thank you for filling the request form. Feel free to submit the request to us.

View file

@ -111,84 +111,79 @@ namespace MinecraftClient
}
//Process ini configuration file
bool loadSucceed, needWriteDefaultSetting, newlyGenerated = false;
if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini")
{
(loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile(args[0]);
settingsIniPath = args[0];
//remove ini configuration file from arguments array
List<string> args_tmp = args.ToList<string>();
args_tmp.RemoveAt(0);
args = args_tmp.ToArray();
}
else if (File.Exists("MinecraftClient.ini"))
{
(loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile("MinecraftClient.ini");
}
else
{
loadSucceed = true;
needWriteDefaultSetting = true;
newlyGenerated = true;
}
if (needWriteDefaultSetting)
{
Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage();
WriteBackSettings(false);
if (newlyGenerated)
ConsoleIO.WriteLineFormatted(Translations.mcc_settings_generated);
ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings);
}
else if (!loadSucceed)
{
ConsoleInteractive.ConsoleReader.StopReadThread();
string command = " ";
while (command.Length > 0)
bool loadSucceed, needWriteDefaultSetting, newlyGenerated = false;
if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini")
{
ConsoleIO.WriteLine(string.Empty);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_invaild_config, Config.Main.Advanced.InternalCmdChar.ToLogString()));
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim();
if (command.Length > 0)
{
if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' '
&& command[0] == Config.Main.Advanced.InternalCmdChar.ToChar())
command = command[1..];
(loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile(args[0]);
settingsIniPath = args[0];
if (command.StartsWith("exit") || command.StartsWith("quit"))
{
return;
}
else if (command.StartsWith("new"))
{
Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage();
WriteBackSettings(true);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_gen_new_config, settingsIniPath));
return;
}
}
else
{
return;
}
//remove ini configuration file from arguments array
List<string> args_tmp = args.ToList<string>();
args_tmp.RemoveAt(0);
args = args_tmp.ToArray();
}
else if (File.Exists("MinecraftClient.ini"))
{
(loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile("MinecraftClient.ini");
}
else
{
loadSucceed = true;
needWriteDefaultSetting = true;
newlyGenerated = true;
}
return;
}
else
{
//Load external translation file. Should be called AFTER settings loaded
if (!Config.Main.Advanced.Language.StartsWith("en"))
ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl));
WriteBackSettings(true); // format
}
bool needPromptUpdate = true;
if (Settings.CheckUpdate(Config.Head.CurrentVersion, Config.Head.LatestVersion))
{
needPromptUpdate = false;
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_has_update, Settings.GithubReleaseUrl));
if (needWriteDefaultSetting)
{
Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage();
WriteBackSettings(false);
if (newlyGenerated)
ConsoleIO.WriteLineFormatted(Translations.mcc_settings_generated);
ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings);
}
else if (!loadSucceed)
{
ConsoleInteractive.ConsoleReader.StopReadThread();
string command = " ";
while (command.Length > 0)
{
ConsoleIO.WriteLine(string.Empty);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_invaild_config, Config.Main.Advanced.InternalCmdChar.ToLogString()));
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim();
if (command.Length > 0)
{
if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' '
&& command[0] == Config.Main.Advanced.InternalCmdChar.ToChar())
command = command[1..];
if (command.StartsWith("exit") || command.StartsWith("quit"))
{
return;
}
else if (command.StartsWith("new"))
{
Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage();
WriteBackSettings(true);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_gen_new_config, settingsIniPath));
return;
}
}
else
{
return;
}
}
return;
}
else
{
//Load external translation file. Should be called AFTER settings loaded
if (!Config.Main.Advanced.Language.StartsWith("en"))
ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl));
WriteBackSettings(true); // format
}
}
//Other command-line arguments
@ -280,7 +275,60 @@ namespace MinecraftClient
Console.WriteLine(string.Format(Translations.mcc_generator_done, dataGenerator, dataPath));
return;
}
}
if (Config.Main.Advanced.ConsoleTitle != "")
{
InternalConfig.Username = "New Window";
Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle);
}
// Check for updates
{
bool needPromptUpdate = true;
if (Settings.CheckUpdate(Config.Head.CurrentVersion, Config.Head.LatestVersion))
{
needPromptUpdate = false;
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_has_update, Settings.GithubReleaseUrl));
}
Task.Run(() =>
{
HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = false };
HttpClient httpClient = new(httpClientHandler);
Task<HttpResponseMessage>? httpWebRequest = null;
try
{
httpWebRequest = httpClient.GetAsync(Settings.GithubLatestReleaseUrl, HttpCompletionOption.ResponseHeadersRead);
httpWebRequest.Wait();
HttpResponseMessage res = httpWebRequest.Result;
if (res.Headers.Location != null)
{
Match match = Regex.Match(res.Headers.Location.ToString(), Settings.GithubReleaseUrl + @"/tag/(\d{4})(\d{2})(\d{2})-(\d+)");
if (match.Success && match.Groups.Count == 5)
{
string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value;
string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day);
if (needPromptUpdate)
if (Settings.CheckUpdate(Config.Head.CurrentVersion, Config.Head.LatestVersion))
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_has_update, Settings.GithubReleaseUrl));
if (latestVersion != Config.Head.LatestVersion)
{
Config.Head.LatestVersion = latestVersion;
WriteBackSettings(false);
}
}
}
}
catch (Exception) { }
finally { httpWebRequest?.Dispose(); }
httpClient.Dispose();
httpClientHandler.Dispose();
});
}
// Load command-line arguments
if (args.Length >= 1)
{
try
{
Settings.LoadArguments(args);
@ -293,47 +341,6 @@ namespace MinecraftClient
}
}
// Check for updates
Task.Run(() =>
{
HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = false };
HttpClient httpClient = new(httpClientHandler);
Task<HttpResponseMessage>? httpWebRequest = null;
try
{
httpWebRequest = httpClient.GetAsync(Settings.GithubLatestReleaseUrl, HttpCompletionOption.ResponseHeadersRead);
httpWebRequest.Wait();
HttpResponseMessage res = httpWebRequest.Result;
if (res.Headers.Location != null)
{
Match match = Regex.Match(res.Headers.Location.ToString(), Settings.GithubReleaseUrl + @"/tag/(\d{4})(\d{2})(\d{2})-(\d+)");
if (match.Success && match.Groups.Count == 5)
{
string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value;
string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day);
if (needPromptUpdate)
if (Settings.CheckUpdate(Config.Head.CurrentVersion, Config.Head.LatestVersion))
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_has_update, Settings.GithubReleaseUrl));
if (latestVersion != Config.Head.LatestVersion)
{
Config.Head.LatestVersion = latestVersion;
WriteBackSettings(false);
}
}
}
}
catch (Exception) { }
finally { httpWebRequest?.Dispose(); }
httpClient.Dispose();
httpClientHandler.Dispose();
});
if (Config.Main.Advanced.ConsoleTitle != "")
{
InternalConfig.Username = "New Window";
Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle);
}
//Test line to troubleshoot invisible colors
if (Config.Logging.DebugMessages)
{
@ -382,26 +389,22 @@ namespace MinecraftClient
//Asking the user to type in missing data such as Username and Password
bool useBrowser = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser;
if (string.IsNullOrEmpty(InternalConfig.Login) && !useBrowser)
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !useBrowser)
{
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login);
InternalConfig.Login = ConsoleIO.ReadLine().Trim();
if (string.IsNullOrEmpty(InternalConfig.Login))
InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim();
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login))
{
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
return;
}
}
InternalConfig.Username = InternalConfig.Login;
if (string.IsNullOrEmpty(Config.Main.General.Account.Password) && string.IsNullOrEmpty(InternalConfig.Password) && !useBrowser &&
(Config.Main.Advanced.SessionCache == CacheType.none || !SessionCache.Contains(Settings.ToLowerIfNeed(InternalConfig.Login))))
InternalConfig.Username = InternalConfig.Account.Login;
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !useBrowser &&
(Config.Main.Advanced.SessionCache == CacheType.none || !SessionCache.Contains(ToLowerIfNeed(InternalConfig.Account.Login))))
{
RequestPassword();
}
else if (string.IsNullOrEmpty(InternalConfig.Password))
{
InternalConfig.Password = Config.Main.General.Account.Password;
}
startupargs = args;
InitializeClient();
@ -412,10 +415,12 @@ namespace MinecraftClient
/// </summary>
private static void RequestPassword()
{
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Login) + "\n" : Translations.mcc_password);
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password);
string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword();
if (password == null || password == string.Empty) { password = "-"; }
InternalConfig.Password = password;
if (string.IsNullOrWhiteSpace(password))
InternalConfig.Account.Password = "-";
else
InternalConfig.Account.Password = password;
}
/// <summary>
@ -430,8 +435,8 @@ namespace MinecraftClient
ProtocolHandler.LoginResult result = ProtocolHandler.LoginResult.LoginRequired;
string loginLower = Settings.ToLowerIfNeed(InternalConfig.Login);
if (InternalConfig.Password == "-")
string loginLower = ToLowerIfNeed(InternalConfig.Account.Login);
if (InternalConfig.Account.Password == "-")
{
ConsoleIO.WriteLineFormatted(Translations.mcc_offline, acceptnewlines: true);
result = ProtocolHandler.LoginResult.Success;
@ -463,8 +468,8 @@ namespace MinecraftClient
}
if (result != ProtocolHandler.LoginResult.Success
&& InternalConfig.Password == ""
&& Config.Main.General.AccountType == LoginType.mojang)
&& string.IsNullOrWhiteSpace(InternalConfig.Account.Password)
&& !(Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser))
RequestPassword();
}
else ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_session_valid, session.PlayerName));
@ -473,7 +478,7 @@ namespace MinecraftClient
if (result != ProtocolHandler.LoginResult.Success)
{
ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : "Microsoft"));
result = ProtocolHandler.GetLogin(InternalConfig.Login, InternalConfig.Password, Config.Main.General.AccountType, out session);
result = ProtocolHandler.GetLogin(InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType, out session);
}
if (result == ProtocolHandler.LoginResult.Success && Config.Main.Advanced.SessionCache != CacheType.none)
@ -582,8 +587,10 @@ namespace MinecraftClient
}
}
if (Config.Main.General.AccountType == LoginType.microsoft && InternalConfig.Password != "-" &&
Config.Signature.LoginWithSecureProfile && protocolversion >= 759 /* 1.19 and above */)
if (Config.Main.General.AccountType == LoginType.microsoft
&& (InternalConfig.Account.Password != "-" || Config.Main.General.Method == LoginMethod.browser)
&& Config.Signature.LoginWithSecureProfile
&& protocolversion >= 759 /* 1.19 and above */)
{
// Load cached profile key from disk if necessary
if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk)

View file

@ -535,7 +535,7 @@ namespace MinecraftClient.Protocol.Handlers
{
session.ServerIDhash = serverIDhash;
session.ServerPublicKey = serverPublicKey;
SessionCache.Store(InternalConfig.Login.ToLower(), session);
SessionCache.Store(InternalConfig.Account.Login.ToLower(), session);
}
else
{

View file

@ -2004,7 +2004,7 @@ namespace MinecraftClient.Protocol.Handlers
{
session.ServerIDhash = serverIDhash;
session.ServerPublicKey = serverPublicKey;
SessionCache.Store(InternalConfig.Login.ToLower(), session);
SessionCache.Store(InternalConfig.Account.Login.ToLower(), session);
}
else
{

View file

@ -570,11 +570,12 @@ namespace MinecraftClient.Protocol
Microsoft.OpenBrowser(Microsoft.SignInUrl);
else
Microsoft.OpenBrowser(Microsoft.GetSignInUrlWithHint(loginHint));
ConsoleIO.WriteLine("Your browser should open automatically. If not, open the link below in your browser.");
ConsoleIO.WriteLine(Translations.mcc_browser_open);
ConsoleIO.WriteLine("\n" + Microsoft.SignInUrl + "\n");
ConsoleIO.WriteLine("Paste your code here");
ConsoleIO.WriteLine(Translations.mcc_browser_login_code);
string code = ConsoleIO.ReadLine();
ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, "Microsoft"));
var msaResponse = Microsoft.RequestAccessToken(code);
return MicrosoftLogin(msaResponse, out session);
@ -604,7 +605,7 @@ namespace MinecraftClient.Protocol
session.PlayerID = profile.UUID;
session.ID = accessToken;
session.RefreshToken = msaResponse.RefreshToken;
InternalConfig.Login = msaResponse.Email;
InternalConfig.Account.Login = msaResponse.Email;
return LoginResult.Success;
}
else

View file

@ -6901,6 +6901,24 @@ namespace MinecraftClient {
}
}
/// <summary>
/// Looks up a localized string similar to Paste your code here:.
/// </summary>
internal static string mcc_browser_login_code {
get {
return ResourceManager.GetString("mcc.browser_login_code", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your browser should open automatically. If not, open the link below in your browser..
/// </summary>
internal static string mcc_browser_open {
get {
return ResourceManager.GetString("mcc.browser_open", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connecting to {0}....
/// </summary>

View file

@ -59,7 +59,7 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
@ -2670,4 +2670,10 @@ Logging in...</value>
<data name="config.Main.Advanced.temporary_fix_badpacket" xml:space="preserve">
<value>Temporary fix for Badpacket issue on some servers.</value>
</data>
<data name="mcc.browser_login_code" xml:space="preserve">
<value>Paste your code here:</value>
</data>
<data name="mcc.browser_open" xml:space="preserve">
<value>Your browser should open automatically. If not, open the link below in your browser.</value>
</data>
</root>

View file

@ -19,6 +19,7 @@ using static MinecraftClient.Settings.ChatFormatConfigHelper;
using static MinecraftClient.Settings.HeadCommentHealper;
using static MinecraftClient.Settings.LoggingConfigHealper;
using static MinecraftClient.Settings.MainConfigHealper;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
using static MinecraftClient.Settings.MCSettingsConfigHealper;
using static MinecraftClient.Settings.SignatureConfigHelper;
@ -47,12 +48,10 @@ namespace MinecraftClient
public static ushort ServerPort = 25565;
public static string Login = string.Empty;
public static AccountInfoConfig Account = new();
public static string Username = string.Empty;
public static string Password = string.Empty;
public static string MinecraftVersion = string.Empty;
public static bool InteractiveMode = true;
@ -252,6 +251,7 @@ namespace MinecraftClient
public static void LoadArguments(string[] args)
{
int positionalIndex = 0;
bool skipPassword = false;
foreach (string argument in args)
{
@ -272,11 +272,20 @@ namespace MinecraftClient
switch (positionalIndex)
{
case 0:
InternalConfig.Login = argument;
if (Config.Main.Advanced.AccountList.TryGetValue(argument, out AccountInfoConfig accountInfo))
{
InternalConfig.Account = accountInfo;
skipPassword = true;
}
else
{
InternalConfig.Account.Login = argument;
}
InternalConfig.KeepAccountSettings = true;
break;
case 1:
InternalConfig.Password = argument;
if (!skipPassword)
InternalConfig.Account.Password = argument;
break;
case 2:
Config.Main.SetServerIP(new MainConfig.ServerInfoConfig(argument), true);
@ -388,7 +397,12 @@ namespace MinecraftClient
General.Account.Login ??= string.Empty;
General.Account.Password ??= string.Empty;
if (!InternalConfig.KeepAccountSettings)
InternalConfig.Login = General.Account.Login;
{
if (Advanced.AccountList.TryGetValue(General.Account.Login, out AccountInfoConfig account))
InternalConfig.Account = account;
else
InternalConfig.Account = General.Account;
}
General.Server.Host ??= string.Empty;
@ -597,8 +611,7 @@ namespace MinecraftClient
{
if (AccountList.TryGetValue(accountAlias, out AccountInfoConfig accountInfo))
{
InternalConfig.Login = accountInfo.Login;
InternalConfig.Password = accountInfo.Password;
InternalConfig.Account = accountInfo;
return true;
}
else
@ -894,7 +907,7 @@ namespace MinecraftClient
switch (varname_lower)
{
case "username": result.Append(InternalConfig.Username); break;
case "login": result.Append(InternalConfig.Login); break;
case "login": result.Append(InternalConfig.Account.Login); break;
case "serverip": result.Append(InternalConfig.ServerIP); break;
case "serverport": result.Append(InternalConfig.ServerPort); break;
case "datetime":

View file

@ -1,4 +1,4 @@
Minecraft Console Client User Manual
======
This page has been moved to appropriate sections on our new [Documentation website](https://mccteam.github.io/docs/).
This page has been moved to appropriate sections on our new [Documentation website](https://mccteam.github.io/guide/configuration.html).

View file

@ -1,6 +1,7 @@
---
title: About & Features
---
# Introduction
- [About](#about)

View file

@ -1,7 +1,10 @@
---
title: Chat Bots
redirectFrom: ["/g/bots/index.html", "/g/bots.html"]
redirectFrom:
- "/g/bots/index.html"
- "/g/bots.html"
---
# Chat Bots
- [About](#about)

View file

@ -1,7 +1,10 @@
---
title: Configuration
redirectFrom: ["/g/conf/index.html", "/g/conf.html"]
redirectFrom:
- "/g/conf/index.html"
- "/g/conf.html"
---
# Configuration
**Minecraft Console Client** can be both configured by the [command line parameters](usage.md#command-line-parameters) and the configuration file.

View file

@ -1,6 +1,7 @@
---
title: Contributing
---
# Contributing
At this moment this page needs to be created.

View file

@ -1,6 +1,7 @@
---
title: Creating Chat Bots
---
# Creating Chat Bots
- [Notes](#notes)

View file

@ -1,6 +1,7 @@
---
title: Installation
---
# Installation
- [YouTube Tutorials](#youtube-tutorials)

View file

@ -1,6 +1,7 @@
---
title: Usage
---
# Usage
How to run the program: