diff --git a/.vscode/launch.json b/.vscode/launch.json
index 5e9f299..c5b91fc 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,18 +5,15 @@
"version": "0.2.0",
"configurations": [
{
- "name": ".NET Core Launch (console)",
+ "name": "Argument Parsing",
"type": "coreclr",
"request": "launch",
- "preLaunchTask": "build",
"program": "${workspaceFolder}/examples/Args/bin/Debug/net8.0/Args.dll",
- "args": ["a", "--bar", "a", "a"],
+ "args": ["argument", "-ab", "-c"],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
- "console": "externalTerminal",
+ "console": "integratedTerminal",
+ "preLaunchTask": "build Argument parsing Example",
}
-
-
-
]
}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 8fe9072..cae51f2 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,15 +1,14 @@
{
- "version": "2.0.0",
- "tasks": [
- {
- "type": "dotnet",
- "task": "build",
- "group": {
- "kind": "build",
- "isDefault": true
- },
- "problemMatcher": [],
- "label": "build"
- },
- ]
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build Argument parsing Example",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/examples/Args/Args.csproj"
+ ],
+ }
+ ]
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 826e739..f39a6fe 100644
--- a/README.md
+++ b/README.md
@@ -8,5 +8,6 @@ It also supports logging.
- [Colors](https://github.com/dedouwe26/Terminal/tree/main/examples/Colors/Program.cs) (colors and text decoration)
- [Keypresses](https://github.com/dedouwe26/Terminal/tree/main/examples/Keypresses/Program.cs) (waiting for keypress, reading keypresses)
- [Logging](https://github.com/dedouwe26/Terminal/tree/main/examples/Logging/Program.cs) (also with sub-loggers.)
-- [Window](https://github.com/dedouwe26/Terminal/tree/main/examples/Window/Program.cs) (Creates a terminal window, Only works in GUI mode)
+- [Assertions](https://github.com/dedouwe26/Terminal/tree/main/examples/Assertion/Program.cs) (one-liners)
- [Argument parsing](https://github.com/dedouwe26/Terminal/tree/main/examples/Args/Program.cs)
+- [Window](https://github.com/dedouwe26/Terminal/tree/main/examples/Window/Program.cs) (doesn't work)
diff --git a/Terminal.sln b/Terminal.sln
index 8732767..5a755ce 100644
--- a/Terminal.sln
+++ b/Terminal.sln
@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Window", "examples\Window\W
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Args", "examples\Args\Args.csproj", "{3F6DC96D-963F-443B-8ABC-FE5B07F3B72D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assertion", "examples\Assertion\Assertion.csproj", "{87F6A6F3-7365-4DA5-8A9B-46F7B24327DD}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -50,6 +52,10 @@ Global
{3F6DC96D-963F-443B-8ABC-FE5B07F3B72D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F6DC96D-963F-443B-8ABC-FE5B07F3B72D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F6DC96D-963F-443B-8ABC-FE5B07F3B72D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {87F6A6F3-7365-4DA5-8A9B-46F7B24327DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {87F6A6F3-7365-4DA5-8A9B-46F7B24327DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {87F6A6F3-7365-4DA5-8A9B-46F7B24327DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {87F6A6F3-7365-4DA5-8A9B-46F7B24327DD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{70C53AE0-A9EB-4551-807A-B4D31599C92E} = {5363993F-0762-4307-9658-82BF54A21CB6}
@@ -57,5 +63,6 @@ Global
{61D6E9FD-4FDA-4386-AF0A-B54DB90E8210} = {5363993F-0762-4307-9658-82BF54A21CB6}
{1C553047-F904-48E8-A84D-7273D71C79DD} = {5363993F-0762-4307-9658-82BF54A21CB6}
{3F6DC96D-963F-443B-8ABC-FE5B07F3B72D} = {5363993F-0762-4307-9658-82BF54A21CB6}
+ {87F6A6F3-7365-4DA5-8A9B-46F7B24327DD} = {5363993F-0762-4307-9658-82BF54A21CB6}
EndGlobalSection
EndGlobal
diff --git a/Terminal/ANSI.cs b/Terminal/ANSI.cs
index efdadfa..c7a0c14 100644
--- a/Terminal/ANSI.cs
+++ b/Terminal/ANSI.cs
@@ -5,11 +5,11 @@ namespace OxDED.Terminal;
///
public static class ANSI {
///
- /// (1B)
+ /// (0x1B)
///
public const string ESC = "\x1B";
///
- /// CSI
+ /// CSI (ESC + [)
///
public const string CSI = ESC+"[";
@@ -24,7 +24,7 @@ public static class ANSI {
///
/// Will return as CSI{row};{column}R
/// Where CSI is CSI,
- /// {row} is the row,
+ /// {row} is the row and
/// {column} is the column.
///
public const string RequestCursorPosition = CSI+"6n";
diff --git a/Terminal/Arguments/Argument.cs b/Terminal/Arguments/Argument.cs
index ff3dc42..b6e8c45 100644
--- a/Terminal/Arguments/Argument.cs
+++ b/Terminal/Arguments/Argument.cs
@@ -1,147 +1,22 @@
+using static OxDED.Terminal.Arguments.ArgumentFormatter;
+
namespace OxDED.Terminal.Arguments;
///
-/// Represents an optional argument (-f, --foo).
+/// Represents a parsed option.
///
-public class Argument : ICloneable, IEquatable {
- ///
- /// The keys of this argument (f, foo).
- ///
- public string[] keys;
- ///
- /// The parameters of this argument.
- ///
- public ArgumentParameter[] parameters;
- ///
- /// The description of this argument.
- ///
- public string? description = null;
- ///
- /// Creates an argument.
- ///
- /// The key of this argument.
- /// The description of this argument (optional).
- /// The parameters of this argument (default: empty).
- public Argument(string key, string? description = null, IEnumerable? parameters = null) {
- keys = [key];
- this.description = description;
- this.parameters = parameters == null ? [] : [.. parameters];
- }
+public class Argument {
///
- /// Creates an argument.
+ /// The format of this argument.
///
- /// The keys of this argument.
- /// The description of this argument (optional).
- /// The parameters of this argument (default: empty).
- public Argument(IEnumerable keys, string? description = null, IEnumerable? parameters = null) {
- this.keys = [.. keys];
- this.description = description;
- this.parameters = parameters == null ? [] : [.. parameters];
- }
- ///
- /// Sets the key of this argument.
- ///
- /// The new key.
- /// This argument.
- public Argument Key(string key) {
- keys = [key];
- return this;
- }
+ public readonly ArgumentFormat Format;
///
- /// Sets the keys of this argument.
+ /// The content of this argument.
///
- /// The new keys.
- /// This argument.
- public Argument Keys(IEnumerable keys) {
- this.keys = [.. keys];
- return this;
- }
- ///
- /// Sets the description of this argument.
- ///
- /// The new description.
- /// This argument.
- public Argument Description(string? description) {
- this.description = description;
- return this;
- }
- ///
- /// Sets the parameters of this argument.
- ///
- /// The new parameters.
- /// This argument.
- public Argument Parameters(IEnumerable parameters) {
- this.parameters = [.. parameters];
- return this;
- }
- ///
- /// Adds a parameter to this argument.
- ///
- /// The parameter to add.
- /// This argument.
- public Argument AddParameter(ArgumentParameter parameter) {
- parameters = [.. parameters, parameter];
- return this;
- }
- ///
- /// If this argument's parameters have values (should be yes).
- ///
- public bool HasValue { get => parameters.All((ArgumentParameter parameter) => parameter.HasValue); }
+ public readonly string Content;
- ///
- public static bool operator ==(Argument? left, Argument? right) {
- if (left is null && right is null) {
- return true;
- } else if (left is null) {
- return false;
- }
- return left.Equals(right);
- }
- ///
- public static bool operator !=(Argument? left, Argument? right) {
- return !(left == right);
- }
- ///
- ///
- /// Checks if the that color is identical to this one.
- ///
- public bool Equals(Argument? other) {
- if (other is null) {
- return false;
- }
- if (ReferenceEquals(this, other)) {
- return true;
- }
- if (GetType() != other.GetType()) {
- return false;
- }
- return keys == other.keys;
- }
- ///
- ///
- /// Checks if the that color is identical to this one.
- ///
- public override bool Equals(object? obj) {
- return Equals(obj as Color);
- }
- ///
- public override int GetHashCode() {
- return keys.GetHashCode();
- }
- ///
- ///
- /// Calls .
- ///
- public object Clone() {
- return CloneArgument();
- }
-
- ///
- /// Clones this color.
- ///
- /// The new copy of this color.
- ///
- public Argument CloneArgument() {
- return new Argument(keys, description, parameters);
+ internal Argument(ArgumentFormat format, string content) {
+ Format = format;
+ Content = content;
}
}
\ No newline at end of file
diff --git a/Terminal/Arguments/ArgumentFormatter.cs b/Terminal/Arguments/ArgumentFormatter.cs
new file mode 100644
index 0000000..b2a7c20
--- /dev/null
+++ b/Terminal/Arguments/ArgumentFormatter.cs
@@ -0,0 +1,445 @@
+using System.Collections.ObjectModel;
+
+namespace OxDED.Terminal.Arguments;
+
+///
+/// Represents a format for arguments and options.
+///
+public class ArgumentFormatter {
+ ///
+ /// Whether the program should quit when a parsing error occured.
+ ///
+ public bool shouldExitOnError = true;
+ ///
+ /// The name of this application. Used by the version menu and help menu.
+ ///
+ public string? name;
+ ///
+ /// The description of this application. Used by the version menu and help menu.
+ ///
+ public string? description;
+ ///
+ /// The version of this application. Used by the version menu and help menu.
+ ///
+ public string? version;
+
+ ///
+ /// All the categories.
+ ///
+ public readonly List Categories = [];
+
+ internal List? allArguments;
+ ///
+ /// All the arguments in all categories.
+ ///
+ public ReadOnlyCollection AllArguments { get {
+ if (allArguments == null) {
+ allArguments = [];
+ foreach (CategoryFormat category in Categories) {
+ allArguments.AddRange(category.Arguments);
+ }
+ }
+ return allArguments.AsReadOnly();
+ } }
+
+ internal List? allOptions;
+ ///
+ /// All the arguments in all categories.
+ ///
+ public ReadOnlyCollection AllOptions { get {
+ if (allOptions == null) {
+ allOptions = [];
+ foreach (CategoryFormat category in Categories) {
+ allOptions.AddRange(category.Options);
+ }
+ }
+ return allOptions.AsReadOnly();
+ } }
+
+ private CategoryFormat? generalCategory;
+ ///
+ /// The general category. It is going to be created when you call the get method.
+ ///
+ public CategoryFormat GeneralCategory { get {
+ return generalCategory ??= new(this, "General");
+ } }
+
+ ///
+ /// Uses the general category.
+ ///
+ /// The category format of the general category.
+ public CategoryFormat General() {
+ return GeneralCategory;
+ }
+ ///
+ ///
+ ///
+ ///
+ public CategoryFormat Category() {
+ return new CategoryFormat(this);
+ }
+
+ // ///
+ // /// Creates a new argument format.
+ // ///
+ // public ArgumentFormatter() {
+ // }
+
+ ///
+ /// Sets whether the program should quit when a parsing error occured.
+ ///
+ /// True if it should quit.
+ /// This argument formatter.
+ public ArgumentFormatter ShouldExitOnError(bool shouldExit) {
+ shouldExitOnError = shouldExit;
+ return this;
+ }
+ ///
+ /// Sets the name of this application. Used by the version menu and help menu.
+ ///
+ /// The name of the application.
+ /// This argument formatter.
+ public ArgumentFormatter Name(string? name) {
+ this.name = name;
+ return this;
+ }
+ ///
+ /// Sets the description of this application. Used by the version menu and help menu.
+ ///
+ /// The description of the application.
+ /// This argument formatter.
+ public ArgumentFormatter Description(string? description) {
+ this.description = description;
+ return this;
+ }
+ ///
+ /// Sets the version of this application. Used by the version menu and help menu.
+ ///
+ /// The version of the application.
+ /// This argument formatter.
+ public ArgumentFormatter Version(string? version) {
+ this.version = version;
+ return this;
+ }
+
+ ///
+ /// The currently used help option.
+ ///
+ public HelpOptionFormat? CurrentHelpOption { get; internal set; }
+ ///
+ /// The currently used version option.
+ ///
+ public VersionOptionFormat? CurrentVersionOption { get; internal set; }
+
+ ///
+ /// Finishes formatting and creates a parser.
+ ///
+ /// A new argument parser.
+ public ArgumentParser Finish() {
+ return new(this);
+ }
+}
+
+///
+/// A argument and option category. Used in the help menu.
+///
+public class CategoryFormat {
+ ///
+ /// The name of the category.
+ ///
+ public string name;
+ ///
+ /// The arguments of this category.
+ ///
+ public readonly List Arguments = [];
+ ///
+ /// The arguments of this category.
+ ///
+ public readonly List Options = [];
+ ///
+ /// The parent format.
+ ///
+ public readonly ArgumentFormatter Parent;
+
+ internal CategoryFormat(ArgumentFormatter parent, string name = "Unnamed") {
+ Parent = parent;
+ this.name = name;
+ }
+
+ ///
+ /// Sets the name of the category.
+ ///
+ ///
+ ///
+ public CategoryFormat Name(string name) {
+ this.name = name;
+ return this;
+ }
+ ///
+ /// Creates a new option.
+ ///
+ /// The new option format.
+ public OptionFormat Option() {
+ return new(this);
+ }
+ ///
+ /// Creates a new argument.
+ ///
+ /// The new argument format.
+ public ArgumentFormat Argument() {
+ return new(this);
+ }
+
+ ///
+ /// Sets a new help option.
+ ///
+ /// The help option format.
+ public HelpOptionFormat HelpOption() {
+ return new HelpOptionFormat(this);
+ }
+ ///
+ /// Sets a new version option.
+ ///
+ /// The version option format.
+ public VersionOptionFormat VersionOption() {
+ return new VersionOptionFormat(this);
+ }
+
+ ///
+ /// Finishes and saves this category format.
+ ///
+ /// The parent.
+ public ArgumentFormatter Finish() {
+ if (!Parent.Categories.Exists((CategoryFormat c) => ReferenceEquals(this, c)))
+ Parent.Categories.Add(this);
+ return Parent;
+ }
+}
+
+///
+/// Represents a format for an argument.
+///
+public class ArgumentFormat {
+ ///
+ /// The description of this argument. Used by the help menu.
+ ///
+ public string description = "";
+
+ ///
+ /// The name of this argument. Used by the help menu.
+ ///
+ public string name = "";
+
+ ///
+ /// The parent format.
+ ///
+ public readonly CategoryFormat Parent;
+
+ internal ArgumentFormat(CategoryFormat parent) {
+ Parent = parent;
+ }
+ ///
+ /// Sets the name of this argument.
+ ///
+ /// The new name.
+ /// This argument.
+ public ArgumentFormat Name(string name) {
+ this.name = name;
+ return this;
+ }
+ ///
+ /// Sets the description of this argument.
+ ///
+ /// The new description.
+ /// This argument.
+ public ArgumentFormat Description(string description) {
+ this.description = description;
+ return this;
+ }
+
+ ///
+ /// Finishes and saves this argument format.
+ ///
+ /// The parent.
+ public CategoryFormat Finish() {
+ Parent.Arguments.Add(this);
+ Parent.Parent.allArguments = null;
+ return Parent;
+ }
+}
+
+///
+/// Represents a format for an option.
+///
+public class OptionFormat {
+ ///
+ /// All the keys of this option.
+ ///
+ public readonly List keys = [];
+ ///
+ /// The description of this argument. Used by the help menu.
+ ///
+ public string description = "";
+ ///
+ /// All the parameters of this option.
+ ///
+ public readonly List parameters = [];
+
+ ///
+ /// The parent format.
+ ///
+ public readonly CategoryFormat Parent;
+
+ internal OptionFormat(CategoryFormat parent) {
+ Parent = parent;
+ }
+
+ ///
+ /// Adds a key to check for.
+ ///
+ /// The new key.
+ /// This option.
+ public virtual OptionFormat Key(string key) {
+ keys.Add(key);
+ return this;
+ }
+ ///
+ /// Adds keys to check for.
+ ///
+ /// The new keys to add.
+ /// This option.
+ public virtual OptionFormat Keys(IEnumerable keys) {
+ this.keys.AddRange(keys);
+ return this;
+ }
+ ///
+ /// Sets the description of this option.
+ ///
+ /// The new description.
+ /// This option.
+ public virtual OptionFormat Description(string description) {
+ this.description = description;
+ return this;
+ }
+ ///
+ /// Adds a new parameter to this option.
+ ///
+ /// The parameter format.
+ public virtual ParameterFormat Parameter() {
+ return new(this);
+ }
+
+ ///
+ /// Finishes and saves this option format.
+ ///
+ /// The parent.
+ public virtual CategoryFormat Finish() {
+ Parent.Options.Add(this);
+ Parent.Parent.allOptions = null;
+ return Parent;
+ }
+ ///
+ /// Represents a format for an option's parameter.
+ ///
+ public class ParameterFormat {
+ ///
+ /// The name of this parameter. Used by the help menu.
+ ///
+ public string name = "";
+ ///
+ /// The description of this argument. Used by the help menu.
+ ///
+ public string description = "";
+
+ ///
+ /// The parent format.
+ ///
+ public readonly OptionFormat OptionFormat;
+
+ internal ParameterFormat(OptionFormat optionBuilder) {
+ OptionFormat = optionBuilder;
+ }
+
+ ///
+ /// Sets the name of this parameter.
+ ///
+ /// The new name.
+ /// This parameter.
+ public ParameterFormat Name(string name) {
+ this.name = name;
+ return this;
+ }
+ ///
+ /// Sets the description of this parameter.
+ ///
+ /// The new description.
+ /// This parameter.
+ public ParameterFormat Description(string description) {
+ this.description = description;
+ return this;
+ }
+
+ ///
+ /// Finishes and saves this parameter format.
+ ///
+ /// The parent.
+ public OptionFormat Finish() {
+ OptionFormat.parameters.Add(this);
+ return OptionFormat;
+ }
+ }
+}
+
+///
+/// Represents a configurable help option.
+///
+public class HelpOptionFormat : OptionFormat {
+ internal HelpOptionFormat(CategoryFormat argumentFormatter) : base(argumentFormatter) {
+ Keys(["h", "help"]).Description("Displays this help page.");
+ }
+ ///
+ /// Whether the program should quit when this option is used or not.
+ ///
+ public bool quit = true;
+ ///
+ /// Sets whether the program should quit when this option is used or not.
+ ///
+ /// Whether the program should quit when this option is used or not.
+ /// This option.
+ public HelpOptionFormat Quit(bool quit) {
+ this.quit = quit;
+ return this;
+ }
+ ///
+ public override CategoryFormat Finish() {
+ Parent.Parent.CurrentHelpOption = this;
+ return base.Finish();
+ }
+}
+
+///
+/// Represents a configurable version option.
+///
+public class VersionOptionFormat : OptionFormat {
+ internal VersionOptionFormat(CategoryFormat argumentFormatter) : base(argumentFormatter) {
+ Keys(["v", "version"]).Description("Displays the version and description of this program.");
+ }
+ ///
+ /// Whether the program should quit when this option is used or not.
+ ///
+ public bool quit = true;
+ ///
+ /// Sets whether the program should quit when this option is used or not.
+ ///
+ /// Whether the program should quit when this option is used or not.
+ /// This option.
+ public VersionOptionFormat Quit(bool quit) {
+ this.quit = quit;
+ return this;
+ }
+ ///
+ public override CategoryFormat Finish() {
+ Parent.Parent.CurrentVersionOption = this;
+ return base.Finish();
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Arguments/ArgumentParameter.cs b/Terminal/Arguments/ArgumentParameter.cs
deleted file mode 100644
index c23555e..0000000
--- a/Terminal/Arguments/ArgumentParameter.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-namespace OxDED.Terminal.Arguments;
-
-///
-/// A parameter for an .
-///
-public class ArgumentParameter {
- ///
- /// The name of this argument parameter.
- ///
- public string name;
- ///
- /// The description of this argument parameter.
- ///
- public string? description;
- internal string? value;
- ///
- /// If this argument parameter has a value (should be yes).
- ///
- public bool HasValue { get => value != null; }
- ///
- /// The value of this argument parameter (error if it isn't parsed).
- ///
- ///
- public string Value { get {
- if (HasValue) {
- return value!;
- } else {
- throw new InvalidOperationException("This argument parameter has not been parsed.");
- }
- } }
- ///
- /// Creates an argument parameter.
- ///
- /// The name of this parameter.
- /// The description of this parameter (optional).
- public ArgumentParameter(string name, string? description = null) {
- this.name = name;
- this.description = description;
- }
- ///
- /// Sets the name of this parameter.
- ///
- /// The new name of this parameter.
- /// This parameter.
- public ArgumentParameter Name(string name) {
- this.name = name;
- return this;
- }
- ///
- /// Sets the description of this parameter.
- ///
- /// The new description of this parameter.
- /// This parameter.
- public ArgumentParameter Description(string? description) {
- this.description = description;
- return this;
- }
-}
\ No newline at end of file
diff --git a/Terminal/Arguments/ArgumentParser.cs b/Terminal/Arguments/ArgumentParser.cs
index a7d1224..c3b3a92 100644
--- a/Terminal/Arguments/ArgumentParser.cs
+++ b/Terminal/Arguments/ArgumentParser.cs
@@ -1,420 +1,405 @@
namespace OxDED.Terminal.Arguments;
-// TODO: add docs
-
///
-/// Helps you with parsing arguments.
+/// The final stage of argument parsing.
///
-public class ArgumentParser
-{
- ///
- /// This is the name of the application.
- ///
- public string? name = null;
- ///
- /// The description of the application.
- ///
- public string? description = null;
- private Argument? versionArgument = null;
- private Argument? helpArgument = null;
- ///
- /// The version of the application.
- ///
- public string? version = null;
- ///
- /// The arguments of the parser.
- ///
- public Dictionary arguments = [];
+public class ArgumentParser {
///
- /// The positional arguments of the parser.
+ /// A parser that parses the arguments.
///
- public List positionalArguments = [];
- ///
- /// Sets the help argument of the application parser.
- ///
- ///
- ///
- ///
- ///
- public ArgumentParser Help(IEnumerable? keys = null, bool showDescription = true, bool showVersion = false, bool shouldExit = true)
- {
- if (helpArgument != null)
- {
- RemoveArgument(helpArgument);
+ public class Parser {
+ ///
+ /// The identifier that identifies the beginning of an option.
+ ///
+ public const char Identifier = '-';
+ private readonly ArgumentFormatter format;
+ ///
+ /// Creates a new argument parser.
+ ///
+ /// The arguments used for parsing.
+ /// The format of the arguments.
+ public Parser(string[] arguments, ArgumentFormatter format) {
+ this.arguments = arguments;
+ this.format = format;
}
- helpArgument = new Argument(keys ?? ["h", "help"], "Shows all the available arguments.");
- AddArgument(helpArgument, (Argument arg) =>
- {
- WriteHelp(showDescription, showVersion);
- if (shouldExit)
- {
- Environment.Exit(0);
+
+ private int argumentIndex = 0;
+ private int ArgumentIndex {
+ get {
+ return argumentIndex;
}
- });
- return this;
- }
- ///
- /// Sets the version of the application parser. And adds an argument.
- ///
- /// The version of the application.
- /// The keys for the parameter (default: v, version).
- ///
- ///
-
- public ArgumentParser Version(string version, IEnumerable? keys = null, bool shouldExit = true)
- {
- if (versionArgument != null)
- {
- RemoveArgument(versionArgument);
+ set {
+ if (argumentIndex != value) {
+ argumentIndex = value;
+ stream = null;
+ }
+ }
+ }
+ private StringReader? stream;
+ private StringReader Stream {
+ get {
+ return stream ??= new StringReader(arguments[ArgumentIndex]);
+ }
+ }
+ private readonly string[] arguments;
+
+ ///
+ /// The parsed options.
+ ///
+ public readonly List Options = [];
+ ///
+ /// The parsed arguments.
+ ///
+ public readonly List Arguments = [];
+
+ ///
+ /// Starts parsing the
+ ///
+ ///
+ public void Parse() {
+ while (HasNext()) {
+ char peeked = Peek(); // The should be a next available.
+ if (peeked == Identifier) {
+ ReadOption();
+ } else {
+ ReadArgument();
+ }
+ }
+ }
+
+ private char Read(string failMsg = "Could not read next character.") {
+ int read;
+ try {
+ read = Stream.Read();
+ } catch (IndexOutOfRangeException) {
+ throw new ArgumentParserException(failMsg);
+ }
+
+ if (read == -1) { ArgumentIndex++; return Read(failMsg); }
+ return (char)read;
+ }
+ private char Peek(string failMsg = "Could not read next character.") {
+ int peeked;
+ try {
+ peeked = Stream.Peek();
+ } catch (IndexOutOfRangeException) {
+ throw new ArgumentParserException(failMsg);
+ }
+
+ if (peeked == -1) { ArgumentIndex++; return Peek(failMsg); }
+ return (char)peeked;
+ }
+ private bool HasNext() {
+ int peeked;
+ try {
+ peeked = Stream.Peek();
+ } catch (IndexOutOfRangeException) {
+ return false;
+ }
+
+ if (peeked == -1) { ArgumentIndex++; return HasNext(); }
+ return true;
+ }
+ private bool HasNextInPart() {
+ int peeked;
+ try {
+ peeked = Stream.Peek();
+ } catch (IndexOutOfRangeException) {
+ return false;
+ }
+ if (peeked == -1) { return false; }
+ return true;
}
- versionArgument = new Argument(keys ?? ["v", "version"], name == null ? "Shows the version of this application." : $"Shows the version of {name}."); // TODO: add documentation to first assign a name
- AddArgument(versionArgument, (Argument arg) =>
- {
- WriteVersion();
- if (shouldExit)
- {
- Environment.Exit(0);
+
+ private string ReadPart(string failMsg = "Not enough parameters given.") {
+ string part;
+ try {
+ part = Stream.ReadToEnd();
+ } catch (IndexOutOfRangeException) {
+ throw new ArgumentParserException(failMsg);
+ }
+ if (part == "") {
+ ArgumentIndex++;
+ return ReadPart(failMsg);
}
- });
- this.version = version;
- return this;
+ ArgumentIndex++;
+ return part;
+ }
+
+ private void ReadOption() {
+ Read();
+
+ if (Peek("Could not read next option.") == Identifier) {
+ Read();
+ // Read long option (--... ???)
+ string key = ReadPart($"No option name found after '{Identifier}{Identifier}'.");
+
+ OptionFormat format = this.format.AllOptions.FirstOrDefault((OptionFormat f) => f.keys.Contains(key))
+ ?? throw new ArgumentParserException($"No option found with name: '{key}'.");
+
+ if (format.parameters.Count > 0) {
+ ReadOptionParameters(format, key);
+ } else {
+ Options.Add(new Option(format, key, null));
+ }
+ } else {
+ // Read short option (-.??? ???)
+ do {
+ char key = Read($"No option name found after '{Identifier}'.");
+ OptionFormat format = this.format.AllOptions.FirstOrDefault((OptionFormat f) => f.keys.Contains(key.ToString()))
+ ?? throw new ArgumentParserException($"No option found with name: '{key}'.");
+
+ if (format.parameters.Count > 0) {
+ ReadOptionParameters(format, key.ToString());
+ break;
+ }
+ Options.Add(new Option(format, key.ToString(), null));
+ } while (HasNextInPart());
+ }
+ }
+ private void ReadOptionParameters(OptionFormat format, string usedKey) {
+ List parameters = [];
+ while (parameters.Count < format.parameters.Count) {
+ string read = ReadPart($"Not enough option parameters for '{usedKey}'. Requires {format.parameters.Count}, only got {parameters.Count}");
+
+ parameters.Add(read);
+ }
+ Options.Add(new Option(format, usedKey, [.. parameters]));
+ }
+ private void ReadArgument() {
+ if (Arguments.Count >= format.AllArguments.Count) throw new ArgumentParserException($"Too many arguments. Requires {format.AllArguments.Count}, got {Arguments.Count+1}.");
+ string content = ReadPart($"Not enough arguments. Requires {format.AllArguments.Count}, only got {Arguments.Count}.");
+
+ Arguments.Add(new Argument(format.AllArguments[Arguments.Count], content));
+ }
}
+
///
- /// Sets the description of the application parser.
+ /// The format used for parsing.
///
- public ArgumentParser Description(string? description)
- {
- this.description = description;
- return this;
- }
+ public readonly ArgumentFormatter Format;
+
///
- /// Sets the name of the application parser.
+ /// The parsed options.
///
- public ArgumentParser Name(string? name)
- {
- this.name = name;
- return this;
- }
+ public Option[] Options { get; private set; }
///
- /// Removes a positional argument.
+ /// The parsed arguments.
///
- public ArgumentParser RemovePositionalArgument(int position) {
- positionalArguments.RemoveAt(position);
- return this;
- }
+ public Argument[] Arguments { get; private set; }
///
- /// Removes a positional argument.
+ /// Creates a new argument parser.
///
- public ArgumentParser RemovePositionalArgument(PositionalArgument argument) {
- positionalArguments.Remove(argument);
- return this;
+ ///
+ public ArgumentParser(ArgumentFormatter format) {
+ Format = format;
+ Options = [];
+ Arguments = [];
}
///
- /// Removes an argument.
+ /// Parses the arguments.
///
- public ArgumentParser RemoveArgument(Argument argument)
- {
- foreach (string key in argument.keys)
- {
- arguments.Remove(key);
+ /// The arguments to parse.
+ public void Parse(string[] args) {
+ Parser parser = new(args, Format);
+ try {
+ parser.Parse();
+ } catch (ArgumentParserException e) {
+ ShowError(e.Message);
+ if (Format.shouldExitOnError) Environment.Exit(1);
}
- return this;
- }
- ///
- /// Adds an argument.
- ///
- public ArgumentParser AddArgument(Argument argument, ArgumentCallback? callback = null)
- {
- foreach (string key in argument.keys)
- {
- arguments.Add(key, argument);
+
+ Options = [.. parser.Options];
+ Arguments = [.. parser.Arguments];
+
+ if (Format.CurrentHelpOption != null && HasOption(Format.CurrentHelpOption)) {
+ ShowHelp();
+ if (Format.CurrentHelpOption.quit) Environment.Exit(0);
}
- if (callback != null)
- {
- OnArgument += (Argument arg) =>
- {
- if (arg.keys == argument.keys)
- {
- callback?.Invoke(arg);
- }
- };
+ if (Format.CurrentVersionOption != null && HasOption(Format.CurrentVersionOption)) {
+ ShowVersion();
+ if (Format.CurrentVersionOption.quit) Environment.Exit(0);
+ }
+
+ if (Arguments.Length < Format.AllArguments.Count) {
+ ShowError($"Too few arguments. Requires {Format.AllArguments.Count}, only got {Arguments.Length}.");
+ if (Format.shouldExitOnError) Environment.Exit(1);
}
- return this;
}
+
///
- /// Adds a positional argument.
+ /// Checks if the corresponding option is used.
///
- public ArgumentParser AddPositionalArgument(PositionalArgument argument, PositionalArgumentCallback? callback = null)
- {
- positionalArguments.Add(argument);
- if (callback != null)
- {
- OnPositionalArgument += (PositionalArgument arg) =>
- {
- if (arg.name == argument.name)
- {
- callback?.Invoke(arg);
- }
- };
+ /// The key of the option of which the usage will be checked.
+ /// True the corresponding option is used.
+ public bool HasOption(string key) {
+ foreach (Option option in Options) {
+ if (option.Format.keys.Contains(key)) {
+ return true;
+ }
}
- return this;
+ return false;
}
///
- /// Writes the help menu to the terminal.
+ /// Checks if this key has been used.
///
- public void WriteHelp(bool showDescription = true, bool showVersion = false)
- {
- Terminal.WriteLine(GetHelp(showDescription, showVersion));
- }
+ /// The key to check for.
+ /// True if the key has been found.
+ public bool HasKeyBeenUsed(string key) {
+ foreach (Option option in Options) {
+ if (option.Key == key) {
+ return true;
+ }
+ }
+ return false;
+ }
///
- /// Writes the version to the terminal.
+ /// Checks if the corresponding option is used.
///
- public void WriteVersion()
- {
- Terminal.WriteLine(GetVersion());
- }
- private static string GetArgumentHelp(PositionalArgument arg, bool isRed = false)
- {
- return $"{(isRed ? Color.LightRed.ToForegroundANSI() : Color.Orange.ToForegroundANSI())}\u2520{ANSI.Styles.ResetAll} {arg.name}{(arg.description == null ? "" : ": "+arg.description)}\n";
- }
- private static string GetArgumentHelpName(string[] keys)
- {
- string result = "";
- for (int i = 0; i < keys.Length; i++)
- {
- string key = keys[i];
- result += key.Length == 1 ? "-" : "--";
- result += key;
- if (i != keys.Length - 1)
- {
- result += ", ";
+ /// The format of the option of which the usage will be checked.
+ ///
+ public bool HasOption(OptionFormat format) {
+ foreach (Option option in Options) {
+ if (format.keys.Contains(option.Key)) {
+ return true;
}
}
- return result;
- }
- private static string GetArgumentHelp(Argument arg)
- {
- string result = $"{Color.DarkGreen.ToForegroundANSI()}\u2520{ANSI.Styles.ResetAll} {GetArgumentHelpName(arg.keys)}{(arg.description == null ? "" : ": "+arg.description)}\n";
- foreach (ArgumentParameter para in arg.parameters)
- {
- result += $"{Color.DarkGreen.ToForegroundANSI()}\u2503 \u2560{ANSI.Styles.ResetAll} {para.name}{(para.description == null ? "" : ": "+para.description)}\n";
- }
- return result;
+ return false;
}
- private string GetHelp(bool showDescription = true, bool showVersion = false, int positionalIndex = -1)
- {
- string result = $"{ANSI.Styles.Bold}{name}{ANSI.Styles.ResetBold} {((showVersion && version != null) ? version : "")}{((showDescription && description != null) ? '\n' + description : "")}\n\n";
-
- if (positionalArguments.Count > 0)
- {
- result += $"{Color.Orange.ToForegroundANSI()}\u250E\u2500\u2500{ANSI.Styles.ResetAll} Required Arguments\n";
- for (int i = 0; i < positionalArguments.Count; i++)
- {
- PositionalArgument arg = positionalArguments[i];
- if (positionalIndex <= i && positionalIndex != -1)
- {
- result += GetArgumentHelp(arg, true);
- }
- else
- {
- result += GetArgumentHelp(arg, false);
- }
-
- }
- result += "\n";
- }
- if (arguments.Count > 0 || helpArgument != null || versionArgument != null)
- {
- result += $"{Color.DarkGreen.ToForegroundANSI()}\u250E\u2500\u2500{ANSI.Styles.ResetAll} Arguments\n";
- if (helpArgument != null)
- {
- result += $"\u2520 {GetArgumentHelpName(helpArgument.keys)}: {helpArgument.description}\n";
- }
- if (versionArgument != null)
- {
- result += $"\u2520 {GetArgumentHelpName(versionArgument.keys)}: {versionArgument.description}\n";
- }
- if (arguments.Count > 0)
- {
- foreach (Argument argument in arguments.Values.Distinct())
- {
- if (argument == versionArgument || argument == helpArgument)
- {
- continue;
- }
- result += GetArgumentHelp(argument);
- }
+ ///
+ /// Gets the option from that key (like ).
+ ///
+ /// The key of the option that it will search for.
+ /// The option, null if it was not used.
+ public Option? GetOption(string key) {
+ foreach (Option option in Options) {
+ if (option.Format.keys.Contains(key)) {
+ return option;
}
}
- return result;
- }
- private string GetVersion()
- {
- return $"{ANSI.Styles.Bold}{name}{ANSI.Styles.ResetBold} {version ?? ""}{(description != null ? '\n' + description : "")}";
+ return null;
}
///
- /// An event that is called when an argument is parsed.
+ /// Gets the option from that format (like ).
///
- public event ArgumentCallback? OnArgument;
- ///
- /// An event that is called when a positional argument is parsed.
- ///
- public event PositionalArgumentCallback? OnPositionalArgument;
+ /// The format of the option that it will search for.
+ /// The option, null if it was not used.
+ public Option? GetOption(OptionFormat format) {
+ foreach (Option option in Options) {
+ if (format.keys.Contains(option.Key)) {
+ return option;
+ }
+ }
+ return null;
+ }
///
- /// An event that is called when the format is invalid.
+ /// Gets the argument at that index.
///
- public event InvalidFormatCallback? OnInvalidFormatCallback;
- private void WriteInvalid(string message)
- {
- WriteHelp(false);
- OnInvalidFormatCallback?.Invoke(message);
- Terminal.WriteLine("\n" + message, new Style { ForegroundColor = Color.Red });
- Environment.Exit(1);
- }
- private void WriteNoArgument(string message, int positionalIndex)
- {
- Terminal.WriteLine(GetHelp(false, false, positionalIndex));
- OnInvalidFormatCallback?.Invoke(message);
- Terminal.WriteLine("\n" + message, new Style { ForegroundColor = Color.Red });
- Environment.Exit(1);
+ /// The index of that argument.
+ /// The argument, null if is out of range.
+ public Argument? GetArgument(int index) {
+ if (index >= Arguments.Length) return null;
+ if (index < 0) return null;
+ return Arguments[index];
}
+
///
- /// Parses the arguments.
+ /// Shows an argument parsing error in the terminal with help (see: ).
///
- public bool Parse(string arguments)
- {
- return Parse(arguments.Split(' '));
+ /// The error message that occured while argument parsing.
+ public void ShowError(string message) {
+ Terminal.WriteLine(message);
+ ShowHelp(false, false);
}
+
///
- /// Parses the arguments.
+ /// Shows the options and arguments to the screen.
///
- public bool Parse(string[] arguments)
- {
- int positionalArgumentIndex = 0;
- Argument? parsingArgument = null;
- List parameters = [];
- bool isParsingArgument = false;
- foreach (string argument in arguments)
- {
- if (!isParsingArgument)
- {
- if (argument.StartsWith("--") && argument.Length > 2)
- {
- parsingArgument = GetArgument(argument[2..]);
- if (parsingArgument == null)
- {
- WriteInvalid("No such argument as: --" + argument[2..] + ".");
- return false;
- }
- else
- {
- isParsingArgument = true;
- }
- }
- else if (argument.StartsWith('-') && argument.Length > 1)
- {
- if (argument.Length >= 3)
- {
- WriteInvalid("Invalid symbol usage (-): " + argument + ".\n Should it be (--)?");
- }
- parsingArgument = GetArgument(argument[1].ToString());
- if (parsingArgument == null)
- {
- WriteInvalid("No such argument as: -" + argument[1] + ".");
- return false;
- }
- else
- {
- isParsingArgument = true;
- }
+ /// If the name and version should be shown.
+ /// If the description of the program should be shown.
+ public void ShowHelp(bool showName = true, bool showDescription = true) {
+ // Name and description.
+ ShowVersion(showName, showDescription);
- }
- else
- {
- PositionalArgument? positionalArgument = GetPositionalArgument(positionalArgumentIndex);
- if (positionalArgument != null)
- {
- positionalArgument.value = argument;
- OnPositionalArgument?.Invoke(positionalArgument);
- positionalArgumentIndex++;
- }
- else
- {
- WriteInvalid("Too much positional arguments.");
- return false;
- }
- }
+ string msg = "\n";
+
+ foreach (CategoryFormat category in Format.Categories) {
+ msg+=new StyleBuilder().Bold().Text(category.name+":").Bold(false).NewLine();
+
+ foreach (ArgumentFormat argument in category.Arguments) {
+ msg+="\t"+new StyleBuilder().Foreground(Color.Cyan).Text(argument.name).ResetForeground().Text(": "+argument.description).NewLine().ToString();
}
- if (isParsingArgument)
- {
- if (!(parameters.Count - 1 >= parsingArgument!.parameters.Length))
- {
- parameters.Add(argument);
- }
- if (parameters.Count - 1 >= parsingArgument!.parameters.Length)
- {
- for (int j = 0; j < parsingArgument!.parameters.Length; j++)
- {
- parsingArgument!.parameters[j].value = parameters[j + 1];
+
+ foreach (OptionFormat option in category.Options) {
+ msg+="\t";
+ StyleBuilder builder = new StyleBuilder().Foreground(Color.Green).Text("[");
+ for (int i = 0; i < option.keys.Count; i++) {
+ string key = option.keys[i];
+ builder.Bold().Text((key.Length > 1 ? "--" : "-")+key).Bold(false);
+ if (i < option.keys.Count-1) {
+ builder.Text(", ");
}
- OnArgument?.Invoke(parsingArgument);
- isParsingArgument = false;
- parameters = [];
- parsingArgument = null;
}
+ builder.Text("]").ResetForeground().Text(": "+option.description).NewLine();
+ foreach (OptionFormat.ParameterFormat parameter in option.parameters) {
+ builder.Text("\t\t").Foreground(Color.Orange).Text(parameter.name).ResetForeground().Text(": "+parameter.description).NewLine();
+ }
+ msg+=builder;
}
+ msg+="\n";
}
- if (isParsingArgument)
- {
- WriteInvalid("Invalid argument parameters for " + parameters[0] + ".");
- return false;
- }
- if (positionalArguments.Count != positionalArgumentIndex)
- {
- WriteNoArgument("Not enough positional arguments.", positionalArgumentIndex);
- return false;
- }
+ // if (Format.AllArguments.Count > 0) msg += "Required arguments:\n";
+ // foreach (ArgumentFormat argument in Format.AllArguments) {
+ // msg += $"\t[{argument.name}]: {argument.description}\n";
+ // }
+
+ // if (Format.AllOptions.Count > 0) msg += "Options:\n";
+ // foreach (OptionFormat option in Format.AllOptions) {
+ // msg += "\t[";
+ // for (int i = 0; i < option.keys.Count; i++) {
+ // string key = option.keys[i];
+ // msg += (key.Length > 1 ? "--" : "-") + key;
+ // if (i < option.keys.Count-1) {
+ // msg += ", ";
+ // }
+ // }
+ // msg += $"]: {option.description}\n";
+ // foreach (OptionFormat.ParameterFormat parameter in option.parameters) {
+ // msg += $"\t\t[{parameter.name}]: {parameter.description}\n";
+ // }
+ // }
- return true;
+ Terminal.Write(msg);
}
+
///
- /// Gets an argument that is registered (null if it isn't registered).
+ /// Displays the version of the program to the screen.
///
- public Argument? GetArgument(string key)
- {
- if (!arguments.TryGetValue(key, out Argument? value))
- {
- return null;
+ /// If the name and version of the program should be shown.
+ /// if the description of the program should be shown.
+ public void ShowVersion(bool showName = true, bool showDescription = true) {
+ string msg = "";
+ if (showName) {
+ string name = GetName();
+ msg = name == "" ? "" : name+"\n";
}
- return value;
- }
- ///
- /// Gets a positional argument at a location (in order as registered) (if there is one).
- ///
- public PositionalArgument? GetPositionalArgument(int pos)
- {
- return positionalArguments.Count > pos ? positionalArguments[pos] : null;
- }
- ///
- /// True if an argument is registered (not used).
- ///
- public bool HasArgument(string key)
- {
- return GetArgument(key) != null;
- }
- ///
- /// Gets all the registered arguments (not used).
- ///
- public Argument[] GetArguments()
- {
- return [.. arguments.Values.Distinct()];
- }
- ///
- /// Gets all the registered positional arguments.
- ///
- public PositionalArgument[] GetPositionalArguments()
- {
- return [.. positionalArguments];
+ // Description
+ if (showDescription) {
+ if (Format.description != null) msg += "\n"+Format.description;
+ }
+
+ if (msg != "") Terminal.WriteLine(msg);
}
+ private string GetName() {
+ string name = "";
+ if (Format.name != null) name+=Format.name;
+ if (Format.version != null) {
+ if (name != "") name+=" ";
+ name+=Format.version;
+ }
+
+ return name;
+ }
+
}
\ No newline at end of file
diff --git a/Terminal/Arguments/ArgumentParserException.cs b/Terminal/Arguments/ArgumentParserException.cs
new file mode 100644
index 0000000..f0bb74d
--- /dev/null
+++ b/Terminal/Arguments/ArgumentParserException.cs
@@ -0,0 +1,17 @@
+namespace OxDED.Terminal.Arguments;
+
+///
+/// An exception that occures when the parser catches an error.
+///
+[Serializable]
+public class ArgumentParserException : Exception {
+ ///
+ /// Creates a new argument parser exception.
+ ///
+ public ArgumentParserException() { }
+ ///
+ /// Creates a new argument parser exception with message.
+ ///
+ /// The message to give.
+ public ArgumentParserException(string message) : base(message) { }
+}
\ No newline at end of file
diff --git a/Terminal/Arguments/Category.cs b/Terminal/Arguments/Category.cs
new file mode 100644
index 0000000..cc0b5d2
--- /dev/null
+++ b/Terminal/Arguments/Category.cs
@@ -0,0 +1,2 @@
+namespace OxDED.Terminal.Arguments;
+
diff --git a/Terminal/Arguments/Delegates.cs b/Terminal/Arguments/Delegates.cs
deleted file mode 100644
index 58d6c9d..0000000
--- a/Terminal/Arguments/Delegates.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace OxDED.Terminal.Arguments;
-
-///
-/// An callback for when an argument has been parsed.
-///
-/// The argument that has been parsed.
-public delegate void ArgumentCallback(Argument argument);
-///
-/// An callback for when an postitional argument has been parsed.
-///
-/// The positional argument that has been parsed.
-public delegate void PositionalArgumentCallback(PositionalArgument argument);
-///
-/// An callback for when the parsing has failed.
-///
-/// The message to describe what failed.
-public delegate void InvalidFormatCallback(string message);
\ No newline at end of file
diff --git a/Terminal/Arguments/Option.cs b/Terminal/Arguments/Option.cs
new file mode 100644
index 0000000..4ecd8b9
--- /dev/null
+++ b/Terminal/Arguments/Option.cs
@@ -0,0 +1,27 @@
+using static OxDED.Terminal.Arguments.ArgumentFormatter;
+
+namespace OxDED.Terminal.Arguments;
+
+///
+/// Represents a parsed option.
+///
+public class Option {
+ ///
+ /// The format of this option.
+ ///
+ public readonly OptionFormat Format;
+ ///
+ /// The key used to declare this option.
+ ///
+ public readonly string Key;
+ ///
+ /// The parameters of this option, null if this option cannot have parameters.
+ ///
+ public readonly string[]? Parameters;
+
+ internal Option(OptionFormat format, string key, string[]? parameters) {
+ Format = format;
+ Key = key;
+ Parameters = parameters;
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Arguments/PositionalArgument.cs b/Terminal/Arguments/PositionalArgument.cs
deleted file mode 100644
index a152f96..0000000
--- a/Terminal/Arguments/PositionalArgument.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-namespace OxDED.Terminal.Arguments;
-
-///
-/// Represents a required argument in a specific order.
-///
-public class PositionalArgument : ICloneable {
- ///
- /// The name (key) of this positional argument.
- ///
- public string name;
- ///
- /// The description of this positional argument.
- ///
- public string? description;
- internal string? value;
- ///
- /// If this argument has a value (should be yes).
- ///
- public bool HasValue { get => value != null; }
- ///
- /// The value of this positional argument (error if it isn't parsed).
- ///
- ///
- public string Value { get {
- if (HasValue) {
- return value!;
- } else {
- throw new InvalidOperationException("This positional argument has not been parsed.");
- }
- } }
- ///
- /// Creates a positional argument.
- ///
- /// The name of this positional argument.
- /// The description of this positional argument (optional).
- public PositionalArgument(string name, string? description = null) {
- this.name = name;
- this.description = description;
- }
- ///
- /// Sets the name of this positional argument.
- ///
- /// The new name of this positional argument.
- /// This positional argument.
- public PositionalArgument Name(string name) {
- this.name = name;
- return this;
- }
- ///
- /// Sets the description of this positional argument.
- ///
- /// The new description of this positional argument.
- /// This positional argument.
- public PositionalArgument Description(string? description) {
- this.description = description;
- return this;
- }
-
- ///
- ///
- /// Calls .
- ///
-
- public object Clone() {
- return ClonePositionalArgument();
- }
-
- ///
- /// Clones this color.
- ///
- /// The new copy of this color.
- ///
- public PositionalArgument ClonePositionalArgument() {
- return new PositionalArgument(name, description);
- }
-}
\ No newline at end of file
diff --git a/Terminal/Assertion/Assert.cs b/Terminal/Assertion/Assert.cs
new file mode 100644
index 0000000..6831179
--- /dev/null
+++ b/Terminal/Assertion/Assert.cs
@@ -0,0 +1,138 @@
+namespace OxDED.Terminal.Assertion;
+
+///
+/// The base plate of an assertion.
+///
+public static class Assert {
+ ///
+ /// Checks if is .
+ ///
+ /// Type of .
+ /// Type of .
+ /// The first value.
+ /// The second value.
+ /// A new value assertion.
+ public static ValueAssertion Is(TA a, TB b) {
+ return ValueAssertion.Create(a, b);
+ }
+ ///
+ /// Checks if is .
+ ///
+ /// The first value.
+ /// The second value.
+ /// A new value assertion.
+ public static ValueAssertion Is(object? a, object? b) {
+ return ValueAssertion.Create(a, b);
+ }
+ ///
+ /// Checks if is true.
+ ///
+ /// The boolean that should be true.
+ /// A new value assertion.
+ public static ValueAssertion IsTrue(bool boolean) {
+ return new ValueAssertion(boolean, boolean, true);
+ }
+ ///
+ /// Checks if is false.
+ ///
+ /// The boolean that should be false.
+ /// A new value assertion.
+ public static ValueAssertion IsFalse(bool boolean) {
+ return new ValueAssertion(!boolean, boolean, false);
+ }
+
+ ///
+ /// Checks if is null.
+ ///
+ /// The type of .
+ /// The object to check.
+ /// A new reference assertion.
+ public static ReferenceAssertion IsNull(T? value) {
+ return ReferenceAssertion.Create(value, shouldBeNull:true);
+ }
+ ///
+ /// Checks if is not null.
+ ///
+ /// The type of .
+ /// The object to check.
+ /// A new reference assertion.
+ public static ReferenceAssertion IsNotNull(T? value) {
+ return ReferenceAssertion.Create(value, shouldBeNull:false);
+ }
+
+ ///
+ /// Checks if is of type .
+ ///
+ /// The type of .
+ /// The type should be.
+ /// The object to check.
+ /// A new type assertion.
+ public static TypeAssertion IsType(TValue value) {
+ return TypeAssertion.Create(value);
+ }
+ ///
+ /// Checks if is of type .
+ ///
+ /// The type of .
+ /// The type object should be.
+ /// The object to check.
+ /// A new type assertion.
+ public static TypeAssertion IsType(TValue obj, Type type) {
+ return TypeAssertion.Create(obj, type);
+ }
+ ///
+ /// Checks if is of type .
+ ///
+ /// The type should be.
+ /// The object to check.
+ /// A new type assertion.
+ public static TypeAssertion IsType(object? value) {
+ return TypeAssertion.Create(value);
+ }
+ ///
+ /// Checks if is of type .
+ ///
+ /// The object to check.
+ /// The type object should be.
+ /// A new type assertion.
+ public static TypeAssertion IsType(object? value, Type type) {
+ return TypeAssertion.Create(value, type);
+ }
+
+ ///
+ /// Checks if throws .
+ ///
+ /// The return value of .
+ /// The exception to catch.
+ /// The emitter to check.
+ /// A new exception assertion.
+ public static ExceptionAssertion Throws(Func emitter) where TException : Exception {
+ return ExceptionAssertion.Create(emitter);
+ }
+ ///
+ /// Checks if throws .
+ ///
+ /// The exception to catch.
+ /// The emitter to check.
+ /// A new exception assertion.
+ public static ExceptionAssertion Throws(Action emitter) where TException : Exception {
+ return ExceptionAssertion.Create(emitter);
+ }
+ ///
+ /// Checks if throws an exception.
+ ///
+ /// The return value of .
+ /// The emitter to check.
+ /// A new exception assertion.
+ public static ExceptionAssertion Throws(Func emitter) {
+ return ExceptionAssertion.Create(emitter);
+ }
+ ///
+ /// Checks if throws an exception.
+ ///
+ /// The emitter to check.
+ /// A new exception assertion.
+ public static ExceptionAssertion Throws(Action emitter) {
+ return ExceptionAssertion.Create(emitter);
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Assertion/AssertException.cs b/Terminal/Assertion/AssertException.cs
new file mode 100644
index 0000000..fdcf4a5
--- /dev/null
+++ b/Terminal/Assertion/AssertException.cs
@@ -0,0 +1,23 @@
+namespace OxDED.Terminal.Assertion;
+
+///
+/// Represents an exception thrown when an assertion failed.
+///
+[Serializable]
+public class AssertException : Exception where T : Assertion {
+ ///
+ /// Creates a new assertion exception without any message.
+ ///
+ public AssertException() { }
+ ///
+ /// Creates a new assertion exception with a message.
+ ///
+ /// The message of this exception.
+ public AssertException(string message) : base(message) { }
+ ///
+ /// Creates a new assertion exception with a message and result.
+ ///
+ /// The message of this exception.
+ /// The assertion that occured.
+ public AssertException(string message, T assertion) : base(message+$" ({assertion})") { }
+}
\ No newline at end of file
diff --git a/Terminal/Assertion/Asserter.cs b/Terminal/Assertion/Asserter.cs
new file mode 100644
index 0000000..eacd4d6
--- /dev/null
+++ b/Terminal/Assertion/Asserter.cs
@@ -0,0 +1,176 @@
+using OxDED.Terminal.Logging;
+
+namespace OxDED.Terminal.Assertion;
+
+///
+/// Can assert a value that is contained in this class.
+///
+/// The type of the value to assert.
+public class Asserter {
+ ///
+ /// The value to assert.
+ ///
+ public readonly T value;
+ ///
+ /// Creates a new asserter.
+ ///
+ /// The value to assert.
+ public Asserter(T value) {
+ this.value = value;
+ }
+
+ ///
+ /// Gets the value to assert.
+ ///
+ /// The value to assert.
+ public T? GetValue() {
+ return value;
+ }
+
+ ///
+ /// Checks if is
+ ///
+ /// The second value.
+ /// A new value assertion.
+ public ValueAssertion Is(object? b) {
+ return Assert.Is(value, b);
+ }
+ ///
+ /// Checks if is
+ ///
+ /// Type of .
+ /// The second value.
+ /// A new value assertion.
+ public ValueAssertion Is(TMatch b) {
+ return Assert.Is(value, b);
+ }
+
+ ///
+ /// Checks if is null.
+ ///
+ /// A new reference assertion.
+ public ReferenceAssertion IsNull() {
+ return Assert.IsNull(value);
+ }
+ ///
+ /// Checks if is not null.
+ ///
+ /// A new reference assertion.
+ public ReferenceAssertion IsNotNull() {
+ return Assert.IsNotNull(value);
+ }
+
+ ///
+ /// Checks if is of type .
+ ///
+ /// The type should be.
+ /// A new type assertion.
+ public TypeAssertion IsType() {
+ return Assert.IsType(value);
+ }
+ ///
+ /// Checks if is of type .
+ ///
+ /// The type object should be.
+ /// A new type assertion.
+ public TypeAssertion IsType(Type type) {
+ return Assert.IsType(value, type);
+ }
+
+ ///
+ /// Tries to cast to a new type.
+ ///
+ /// The new type to try to cast to.
+ /// A new asserter where the value is null if the cast failed ( ).
+ public Asserter As() {
+ try {
+ return new Asserter((TNew?)(object?)value);
+ } catch (InvalidCastException) {
+ return new Asserter(default);
+ }
+ }
+
+ ///
+ /// Applies a function on .
+ ///
+ /// The new type of the value.
+ /// The function to apply.
+ /// The new asserter where the function is applied on the .
+ public Asserter Do(Func function) {
+ return new Asserter(function(value));
+ }
+
+ ///
+ /// Applies a function on .
+ ///
+ /// The function to apply.
+ /// The same asserter.
+ public Asserter Do(Action function) {
+ function(value);
+ return this;
+ }
+
+ ///
+ /// Logs the .
+ ///
+ /// The logger to log to.
+ /// The severity to log.
+ /// The same asserter.
+ public Asserter Log(Logger logger, Severity severity = Severity.Debug) {
+ logger.Log(severity, value);
+ return this;
+ }
+}
+
+///
+/// Asserter exstensions for assertions with specific types.
+///
+public static class AsserterExtension {
+ ///
+ /// Checks if is true.
+ ///
+ /// A new value assertion.
+ public static ValueAssertion IsTrue(this Asserter asserter) {
+ return Assert.IsTrue(asserter.value);
+ }
+ ///
+ /// Checks if is true.
+ ///
+ /// A new value assertion.
+ public static ValueAssertion IsFalse(this Asserter asserter) {
+ return Assert.IsFalse(asserter.value);
+ }
+
+ ///
+ /// Checks if throws .
+ ///
+ /// The return value of .
+ /// The exception to catch.
+ /// A new exception assertion.
+ public static ExceptionAssertion Throws(this Asserter> asserter) where TException : Exception {
+ return Assert.Throws(asserter.value);
+ }
+ ///
+ /// Checks if throws .
+ ///
+ /// The exception to catch.
+ /// A new exception assertion.
+ public static ExceptionAssertion Throws(this Asserter asserter) where TException : Exception {
+ return Assert.Throws(asserter.value);
+ }
+ ///
+ /// Checks if throws an exception.
+ ///
+ /// The return value of .
+ /// A new exception assertion.
+ public static ExceptionAssertion Throws(this Asserter> asserter) {
+ return Assert.Throws(asserter.value);
+ }
+ ///
+ /// Checks if throws an exception.
+ ///
+ /// A new exception assertion.
+ public static ExceptionAssertion Throws(this Asserter asserter) {
+ return Assert.Throws(asserter.value);
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Assertion/Assertion.cs b/Terminal/Assertion/Assertion.cs
new file mode 100644
index 0000000..282992f
--- /dev/null
+++ b/Terminal/Assertion/Assertion.cs
@@ -0,0 +1,148 @@
+using OxDED.Terminal.Logging;
+
+namespace OxDED.Terminal.Assertion;
+
+///
+/// Represents a base assertion with a result.
+///
+public class Assertion {
+ ///
+ /// The result of this assertion.
+ ///
+ public readonly AssertionResult result;
+ ///
+ /// Creates a new assertion.
+ ///
+ /// The result of this assertion.
+ public Assertion(AssertionResult result) {
+ this.result = result;
+ }
+ ///
+ /// Gets the result of this assertion.
+ ///
+ /// The result of this assertion.
+ public AssertionResult GetResult() {
+ return result;
+ }
+ ///
+ /// Creates an asserter of the result.
+ ///
+ /// The result asserter.
+ public Asserter ResultAsserter() {
+ return new Asserter(result);
+ }
+ ///
+ /// Checks if this assertion succeeded.
+ ///
+ /// True if the result equals to .
+ public bool IsSuccess() {
+ return result == AssertionResult.Success;
+ }
+ ///
+ /// Checks if this assertion failed.
+ ///
+ /// True if the result does not equal to .
+ public bool IsFailure() {
+ return result != AssertionResult.Success;
+ }
+
+ ///
+ /// Calls if this assertion succeeded.
+ ///
+ /// The callback to call, if this assertion succeeded.
+ /// This assertion.
+ public Assertion OnSuccess(Action callback) {
+ if (IsSuccess()) callback.Invoke(this);
+ return this;
+ }
+ ///
+ /// Calls if this assertion failed.
+ ///
+ /// The callback to call, if this assertion failed.
+ /// This assertion.
+ public Assertion OnFailure(Action callback) {
+ if (IsFailure()) callback.Invoke(this);
+ return this;
+ }
+
+ ///
+ /// Logs this assertion.
+ ///
+ /// The logger to log to.
+ /// The severity to log with.
+ /// This assertion.
+ public Assertion Log(Logger logger, Severity severity = Severity.Error) {
+ logger.LogTrace($"Assertion done: {ToString()}");
+ return OnFailure(failedAssertion => {
+ logger.Log(severity, $"Assertion failed with result: {result}");
+ });
+ }
+
+ ///
+ /// Throws or upon failing.
+ ///
+ /// The exception to throw (defaults to ).
+ /// This assertion.
+ public Assertion Throw(Exception? exception = null) {
+ return OnFailure(failedAssertion => {
+ throw exception ?? new AssertException("Assertion failed", this);
+ });
+ }
+
+ ///
+ /// Converts back this assertion to another assertion.
+ ///
+ /// The new assertion type.
+ /// The new assertion if the cast was successful.
+ public TNew? As() where TNew : Assertion {
+ return this as TNew;
+ }
+
+ ///
+ /// Converts this assertion to a string.
+ ///
+ /// The string the assertion converted to.
+ public override string ToString() {
+ return $"{GetType().Name}: result = {result}";
+ }
+}
+
+///
+/// Represents a base assertion with a result and a value.
+///
+/// The type of the value.
+public class Assertion : Assertion {
+ ///
+ /// The value of this assertion.
+ ///
+ public readonly T value;
+ ///
+ /// Creates a new assertion.
+ ///
+ /// The result of the assertion.
+ /// The value of the assertion.
+ public Assertion(AssertionResult result, T value) : base(result) {
+ this.value = value;
+ }
+
+ ///
+ /// Gets the value of the assertion.
+ ///
+ /// The value of this assertion.
+ public T GetValue() {
+ return value;
+ }
+
+ ///
+ /// Creates an asserter of the .
+ ///
+ /// The asserter.
+ public Asserter Asserter() {
+ return new Asserter(value);
+ }
+
+ ///
+ public override string ToString() {
+ return $"{GetType().Name}: result = {result}, value = {value}";
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Assertion/AssertionResult.cs b/Terminal/Assertion/AssertionResult.cs
new file mode 100644
index 0000000..0d0554f
--- /dev/null
+++ b/Terminal/Assertion/AssertionResult.cs
@@ -0,0 +1,27 @@
+namespace OxDED.Terminal.Assertion;
+
+///
+/// The possible results of an assertion.
+///
+public enum AssertionResult {
+ ///
+ /// The assertion succceeded.
+ ///
+ Success,
+ ///
+ /// The value assertion failed.
+ ///
+ UnexpectedValue,
+ ///
+ /// The type assertion failed.
+ ///
+ UnexpectedType,
+ ///
+ /// The reference assertion failed.
+ ///
+ UnexpectedReference,
+ ///
+ /// The exception assertion failed.
+ ///
+ ExceptionCaught
+}
\ No newline at end of file
diff --git a/Terminal/Assertion/ExceptionAssertion.cs b/Terminal/Assertion/ExceptionAssertion.cs
new file mode 100644
index 0000000..37eefa1
--- /dev/null
+++ b/Terminal/Assertion/ExceptionAssertion.cs
@@ -0,0 +1,116 @@
+using OxDED.Terminal.Logging;
+
+namespace OxDED.Terminal.Assertion;
+
+///
+/// Represents an assertion that catches exceptions.
+///
+/// The type of the exception to catch.
+public class ExceptionAssertion : Assertion where TException : Exception {
+ ///
+ /// Creates a new exception assertion.
+ ///
+ /// The possible emitter of an exception.
+ /// The created exception assertion.
+ public static ExceptionAssertion Create(Action emitter) {
+ try {
+ emitter.Invoke();
+ } catch (TException e) {
+ return new ExceptionAssertion(false, e);
+ }
+ return new ExceptionAssertion(true);
+ }
+
+ ///
+ /// The exception thrown.
+ ///
+ public readonly TException? exception;
+
+ ///
+ /// Creates a new exception assertion.
+ ///
+ /// If this assertion has succeeded.
+ /// The optional exception that occured.
+ public ExceptionAssertion(bool success, TException? exception = null) : base(success ? AssertionResult.Success : AssertionResult.ExceptionCaught) {
+ this.exception = exception;
+ }
+
+ ///
+ /// Gets the exception of this exception assertion.
+ ///
+ /// The exception of this exception assertion.
+ public TException? GetException() {
+ return exception;
+ }
+
+ ///
+ /// Creates an asserter of the exception.
+ ///
+ /// The exception asserter.
+ public Asserter ExceptionAsserter() {
+ return new Asserter(exception);
+ }
+
+ ///
+ public override string ToString() {
+ return $"{GetType().Name}: result = {result}, exception = {exception}";
+ }
+}
+
+///
+/// Represents an assertion that catches exceptions.
+///
+/// The return type of the emitter.
+/// The type of the exception to catch.
+public class ExceptionAssertion : Assertion where TException : Exception {
+ ///
+ /// Creates a new exception assertion.
+ ///
+ /// The possible emitter of an exception.
+ /// The created exception assertion.
+ public static ExceptionAssertion Create(Func emitter) {
+ T value;
+ try {
+ value = emitter.Invoke();
+ } catch (TException e) {
+ return new ExceptionAssertion(false, exception:e);
+ }
+ return new ExceptionAssertion(true, value:value);
+ }
+
+ ///
+ /// The exception thrown.
+ ///
+ public readonly TException? exception;
+
+ ///
+ /// Creates a new exception assertion.
+ ///
+ /// If this assertion succeeded.
+ /// The optional return value of the emitter.
+ /// The optional exception that occured.
+ public ExceptionAssertion(bool success, T? value = default, TException? exception = null) : base(success ? AssertionResult.Success : AssertionResult.ExceptionCaught, value) {
+ this.exception = exception;
+ }
+
+ ///
+ /// Gets the exception of this exception assertion.
+ ///
+ /// The exception of this exception assertion.
+ public TException? GetException() {
+ return exception;
+ }
+
+ ///
+ /// Creates an asserter of the exception.
+ ///
+ /// The exception asserter.
+ public Asserter ExceptionAsserter() {
+ return new Asserter(exception);
+ }
+
+ ///
+ public override string ToString() {
+ return $"{GetType().Name}: result = {result}, value = {value}, exception = {exception}";
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Assertion/ReferenceAssertion.cs b/Terminal/Assertion/ReferenceAssertion.cs
new file mode 100644
index 0000000..75bdc33
--- /dev/null
+++ b/Terminal/Assertion/ReferenceAssertion.cs
@@ -0,0 +1,48 @@
+namespace OxDED.Terminal.Assertion;
+
+///
+/// Represents an assertion that checks null references.
+///
+/// The type of the possible null reference.
+public class ReferenceAssertion : Assertion {
+ ///
+ /// Creates a new reference assertion.
+ ///
+ /// The object that can be null.
+ /// If the should be null.
+ /// The created reference assertion.
+ public static ReferenceAssertion Create(T? value, bool shouldBeNull = true) {
+ return new ReferenceAssertion(shouldBeNull ? (value is null) : (value is not null), value, shouldBeNull);
+ }
+
+ ///
+ /// If should be null.
+ ///
+ public readonly bool shouldBeNull;
+
+ ///
+ /// Creates a new reference assertion.
+ ///
+ /// If this assertion has succeeded.
+ /// The nullable object that has been checked.
+ /// If should be null.
+ public ReferenceAssertion(bool success, T? value, bool shouldBeNull) : base(success ? AssertionResult.Success : AssertionResult.UnexpectedReference, value) {
+ this.shouldBeNull = shouldBeNull;
+ }
+
+ ///
+ /// Checks if should be null.
+ ///
+ /// True, if should be null.
+ public bool ShouldBeNull() {
+ return shouldBeNull;
+ }
+
+ ///
+ /// Creates an asserter of .
+ ///
+ /// The asserter.
+ public Asserter ShouldBeNullAsserter() {
+ return new Asserter(shouldBeNull);
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Assertion/TypeAssertion.cs b/Terminal/Assertion/TypeAssertion.cs
new file mode 100644
index 0000000..a68e4a4
--- /dev/null
+++ b/Terminal/Assertion/TypeAssertion.cs
@@ -0,0 +1,77 @@
+namespace OxDED.Terminal.Assertion;
+
+///
+/// Represents an assertion that checks the types.
+///
+/// The type of value to check.
+public class TypeAssertion : Assertion {
+ ///
+ /// Creates a new type assertion.
+ ///
+ /// The object to check.
+ /// The type should be.
+ /// The created type assertion.
+ public static TypeAssertion Create(T obj, Type type) {
+ return new TypeAssertion(obj?.GetType() == type, type, obj);
+ }
+
+ ///
+ /// The type that should have been.
+ ///
+ public readonly Type matchedType;
+
+ ///
+ /// Creates a new type assertion.
+ ///
+ /// If this assertion has succeeded.
+ /// The type that should have been.
+ /// The value that should have been .
+ public TypeAssertion(bool success, Type matchedType, T value) : base(success ? AssertionResult.Success : AssertionResult.UnexpectedType, value) {
+
+ this.matchedType = matchedType;
+ }
+
+ ///
+ /// Gets the type that the object should have been.
+ ///
+ /// The type that the object should have been.
+ public Type GetMatchedType() {
+ return matchedType;
+ }
+
+ ///
+ /// Creates an asserter of the type that should have been.
+ ///
+ /// The type asserter.
+ public Asserter TypeAsserter() {
+ return new Asserter(matchedType);
+ }
+
+ ///
+ public override string ToString() {
+ return $"{GetType().Name}: result = {result}, value = {value}, matchedType = {matchedType}";
+ }
+}
+
+///
+/// Represents an assertion that checks the types.
+///
+/// The type of value to check.
+/// The type should have been.
+public class TypeAssertion : TypeAssertion {
+ ///
+ /// Creates a new type assertion.
+ ///
+ /// The object to check.
+ /// The created type assertion.
+ public static TypeAssertion Create(TValue obj) {
+ return new TypeAssertion(obj is TMatch, obj);
+ }
+
+ ///
+ /// Creates a new type assertion.
+ ///
+ /// If this assertion has succeeded.
+ /// The value that should have been .
+ public TypeAssertion(bool success, TValue value) : base(success, typeof(TMatch), value) { }
+}
\ No newline at end of file
diff --git a/Terminal/Assertion/ValueAssertion.cs b/Terminal/Assertion/ValueAssertion.cs
new file mode 100644
index 0000000..fb935f2
--- /dev/null
+++ b/Terminal/Assertion/ValueAssertion.cs
@@ -0,0 +1,67 @@
+using System.Runtime.CompilerServices;
+
+namespace OxDED.Terminal.Assertion;
+
+///
+/// Represents an assertion that checks two values.
+///
+/// The type of the first value.
+/// The type of the second value.
+public class ValueAssertion : Assertion {
+ private static bool P_Is(object? a, object? b) {
+ if (a is null && b is null) return true;
+ if (a is null ^ b is null) return false;
+
+ if (a?.GetType() != b?.GetType()) return false;
+ if (ReferenceEquals(a, b)) return true;
+
+ // NOTE: A should not be possibly null here... Right?
+ return a?.Equals(b) ?? (b is null);
+ }
+ ///
+ /// Creates a new value assertion.
+ ///
+ /// The value to check.
+ /// The value should have been.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ValueAssertion Create(T a, TSecond b) {
+ return new ValueAssertion(P_Is(a, b), a, b);
+ }
+
+ ///
+ /// The value should have been.
+ ///
+ public readonly TSecond secondValue;
+
+ ///
+ /// Creates a new value assertion.
+ ///
+ /// If this assertion succeeded.
+ /// The checked value.
+ /// The value should have been.
+ public ValueAssertion(bool success, T value, TSecond secondValue) : base(success ? AssertionResult.Success : AssertionResult.UnexpectedValue, value) {
+ this.secondValue = secondValue;
+ }
+
+ ///
+ /// Gets the value should have been.
+ ///
+ /// The value should have been.
+ public TSecond? GetSecondValue() {
+ return secondValue;
+ }
+
+ ///
+ /// Creates an asserter of the value should have been.
+ ///
+ /// The asserter of .
+ public Asserter SecondAsserter() {
+ return new Asserter(secondValue);
+ }
+
+ ///
+ public override string ToString() {
+ return $"{GetType().Name}: result = {result}, value = {value}, secondValue = {secondValue}";
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Backend/WindowsBackend.cs b/Terminal/Backend/WindowsBackend.cs
deleted file mode 100644
index 834ef8c..0000000
--- a/Terminal/Backend/WindowsBackend.cs
+++ /dev/null
@@ -1,283 +0,0 @@
-using System.ComponentModel;
-using System.Runtime.InteropServices;
-using System.Text;
-using Microsoft.Win32.SafeHandles;
-
-namespace OxDED.Terminal.Backend;
-
-internal static class Utility {
- internal static Stream GetStream(WinAPI.StandardType type) {
- SafeFileHandle fileHandle = new(WinAPI.GetStdHandle((int)type), false);
-
- if (fileHandle.IsInvalid) {
- fileHandle.SetHandleAsInvalid();
- return Stream.Null;
- }
-
- FileStream stream = new(fileHandle, type != WinAPI.StandardType.Input ? FileAccess.Write : FileAccess.Read);
-
- return stream;
- }
- internal static WinAPI.CONSOLE_SCREEN_BUFFER_INFO GetBufferInfo(WindowsBackend backend) {
- if (backend.outHandle == WinAPI.INVALID_HANDLE_VALUE) {
- throw new Win32Exception("Invalid standard console output handle.");
- }
-
- bool succeeded = WinAPI.GetConsoleScreenBufferInfo(backend.outHandle, out WinAPI.CONSOLE_SCREEN_BUFFER_INFO csbi);
- if (!succeeded) {
- succeeded = WinAPI.GetConsoleScreenBufferInfo(backend.errHandle, out csbi);
- if (!succeeded)
- succeeded = WinAPI.GetConsoleScreenBufferInfo(backend.inHandle, out csbi);
-
- if (!succeeded) {
- int errorCode = Marshal.GetLastWin32Error();
- throw new Win32Exception(errorCode, "Tried to get the console screen buffer info.");
- }
- }
-
- return csbi;
- }
-}
-
-internal static partial class WinAPI {
- internal enum StandardType : int {
- Input = -10,
- Output = -11,
- Error = -12
- }
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
- internal struct KEY_EVENT_RECORD {
- [MarshalAs(UnmanagedType.Bool)]
- internal bool bKeyDown;
- internal ushort wRepeatCount;
- internal ushort wVirtualKeyCode;
- internal ushort wVirtualScanCode;
- private ushort _uChar;
- internal uint dwControlKeyState;
- internal readonly char UChar => (char)_uChar;
- }
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
- internal struct INPUT_RECORD {
- internal ushort EventType;
- internal KEY_EVENT_RECORD keyEvent;
- }
- [StructLayout(LayoutKind.Sequential)]
- internal struct COORD {
- internal short X;
- internal short Y;
- }
- [StructLayout(LayoutKind.Sequential)]
- internal struct SMALL_RECT {
- internal short Left;
- internal short Top;
- internal short Right;
- internal short Bottom;
- }
- [StructLayout(LayoutKind.Sequential)]
- internal struct CONSOLE_SCREEN_BUFFER_INFO {
- internal COORD dwSize;
- internal COORD dwCursorPosition;
- internal short wAttributes;
- internal SMALL_RECT srWindow;
- internal COORD dwMaximumWindowSize;
- }
- internal const string KERNEL = "kernel32.dll";
- internal const nint INVALID_HANDLE_VALUE = -1;
-
- [LibraryImport(KERNEL, SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static partial bool AllocConsole();
-
- [LibraryImport(KERNEL, SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static partial bool FreeConsole();
-
- [LibraryImport(KERNEL)]
- internal static partial nint GetConsoleWindow();
- [LibraryImport(KERNEL, SetLastError = true)]
- internal static partial nint GetStdHandle(int nStdHandle);
-
- [LibraryImport(KERNEL, SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static partial bool CloseHandle(nint handle);
-
- [LibraryImport(KERNEL, SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static partial bool SetStdHandle(int nStdHandle, nint hConsoleOutput);
-
- [DllImport(KERNEL, CharSet = CharSet.Auto, BestFitMapping = false, SetLastError = true)]
- internal static extern SafeFileHandle CreateFile(string fileName, uint desiredAccess, int shareMode, nint securityAttributes, int creationDisposition, int flagsAndAttributes, nint templateFile);
-
- [DllImport(KERNEL, SetLastError = true, BestFitMapping = true, CharSet = CharSet.Unicode)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static extern bool SetConsoleTitle(string title);
-
- [DllImport(KERNEL, SetLastError = true, BestFitMapping = true, CharSet = CharSet.Unicode)]
- internal static extern uint GetConsoleTitle(out string lpConsoleTitle, uint nSize);
-
- [DllImport(KERNEL, CharSet=CharSet.Auto, SetLastError=true)]
- internal static extern bool ReadConsoleInput(nint hConsoleInput, out INPUT_RECORD buffer, int numInputRecords_UseOne, out int numEventsRead);
-
- [LibraryImport(KERNEL, SetLastError=true)]
- internal static partial uint GetConsoleOutputCP();
-
- [LibraryImport(KERNEL, SetLastError=true)]
- internal static partial uint GetConsoleCP();
-
- [LibraryImport(KERNEL, SetLastError=true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static partial bool SetConsoleOutputCP(uint codePage);
-
- [LibraryImport(KERNEL, SetLastError=true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static partial bool SetConsoleCP(uint codePage);
-
- [LibraryImport(KERNEL, SetLastError =true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static partial bool GetConsoleScreenBufferInfo(nint hConsoleOutput, out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
-}
-
-///
-/// A wrapper for on Windows.
-///
-public class WindowsBackend : TerminalBackend {
- ///
- /// Creates a new Windows terminal window.
- ///
- ///
- public WindowsBackend() {
- outEnc = Encoding.UTF8;
- inEnc = Encoding.UTF8;
- outHandle = WinAPI.GetStdHandle((int)WinAPI.StandardType.Output);
- inHandle = WinAPI.GetStdHandle((int)WinAPI.StandardType.Input);
- errHandle = WinAPI.GetStdHandle((int)WinAPI.StandardType.Error);
- TextWriter outStream = TextWriter.Synchronized(new StreamWriter(Utility.GetStream(WinAPI.StandardType.Output), outEnc, 256, true));
- TextReader inStream = TextReader.Synchronized(new StreamReader(Utility.GetStream(WinAPI.StandardType.Input), inEnc, false, 256, true));
- TextWriter errStream = TextWriter.Synchronized(new StreamWriter(Utility.GetStream(WinAPI.StandardType.Error), outEnc, 256, true));
- }
- private TextWriter outStream;
- private Encoding outEnc;
- private TextReader inStream;
- private Encoding inEnc;
- private TextWriter errStream;
-
- internal nint outHandle;
- internal nint inHandle;
- internal nint errHandle;
-
- ///
- public override (int Width, int Height) Size {
- get {
- WinAPI.CONSOLE_SCREEN_BUFFER_INFO csbi = Utility.GetBufferInfo(this);
- return (csbi.srWindow.Right - csbi.srWindow.Left + 1, csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
- } set {
- throw new NotImplementedException();
- }
- }
-
- ///
- public override Encoding InputEncoding { get => inEnc;
- set {
- if (inEnc == value) return;
-
- if (!WinAPI.SetConsoleCP((uint)value.CodePage)) {
- throw new Win32Exception("Failed to set console output code page.");
- }
- inEnc = value;
- }
- }
- ///
- public override Encoding OutputEncoding { get => outEnc;
- set {
- if (outEnc == value) return;
-
- outStream.Flush();
- errStream.Flush();
-
- if (!WinAPI.SetConsoleOutputCP((uint)value.CodePage)) {
- throw new Win32Exception("Failed to set console output code page.");
- }
- outEnc = value;
- }
- }
- ///
- public override Encoding ErrorEncoding { get => outEnc; set => OutputEncoding = value; }
-
- ///
- public override TextReader StandardInput => inStream;
-
- ///
- public override TextWriter StandardOutput => outStream;
-
- ///
- public override TextWriter StandardError => errStream;
-
- ///
- /// An event for when a key is released.
- ///
- public event KeyPressCallback? OnKeyRelease;
-
- // ///
- // ///
- // /// Gets the first 300 chars of the title.
- // public override string Title {
- // get {
- // _ = WinAPI.GetConsoleTitle(out string title, 300);
- // return title;
- // }
- // set {
- // if (!WinAPI.SetConsoleTitle(value)) {
- // throw new Win32Exception("Failed to set the title: "+Marshal.GetLastWin32Error());
- // }
- // }
- // }
-
- ///
- ///
- public override void Dispose() {
-
- }
-
- ///
- ///
- protected override void ListenForKeysMethod() {
- while (listenForKeys) {
- if (!WinAPI.ReadConsoleInput(consoleIn, out WinAPI.INPUT_RECORD ev, 1, out int eventsRead)) {
- throw new Win32Exception("Failed to read console inputs: "+Marshal.GetLastWin32Error());
- }
-
- bool isKeyDown = ev.EventType == 0x0001 && ev.keyEvent.bKeyDown != false;
- char ch = ev.keyEvent.UChar;
- ushort keyCode = ev.keyEvent.wVirtualKeyCode;
-
- if (!isKeyDown) {
- if (keyCode != 0x12)
- continue;
- }
- if (ch == 0) {
- if ((keyCode >= 0x10 && keyCode <= 0x12) || keyCode == 0x14 || keyCode == 0x90 || keyCode == 0x91)
- continue;
- }
- ControlKeyState state = (ControlKeyState)ev.keyEvent.dwControlKeyState;
- bool shift = (state & ControlKeyState.ShiftPressed) != 0;
- bool alt = (state & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0;
- bool control = (state & (ControlKeyState.LeftCtrlPressed | ControlKeyState.RightCtrlPressed)) != 0;
- if (isKeyDown) {
- KeyPress((ConsoleKey)keyCode, ch, alt, shift, control);
- } else {
- OnKeyRelease?.Invoke((ConsoleKey)keyCode, ch, alt, shift, control);
- }
- }
- }
- ///
- public override (int x, int y) GetCursorPosition() {
- throw new NotImplementedException();
- }
- ///
- ///
- public override void WaitForKeyPress() {
- if (!WinAPI.ReadConsoleInput(consoleIn, out _, 1, out _)) {
- throw new Win32Exception("Failed to read console inputs: "+Marshal.GetLastWin32Error());
- }
- }
-}
\ No newline at end of file
diff --git a/Terminal/Color.cs b/Terminal/Color.cs
index e41ee9a..a5872e7 100644
--- a/Terminal/Color.cs
+++ b/Terminal/Color.cs
@@ -7,21 +7,21 @@ namespace OxDED.Terminal;
/// These are TERMINAL-DEFINED colors.
///
public enum Colors : byte {
- ///
+ ///
Black = 30,
- ///
+ ///
Red = 31,
- ///
+ ///
Green = 32,
- ///
+ ///
Yellow = 33,
- ///
+ ///
Blue = 34,
- ///
+ ///
Magenta = 35,
- ///
+ ///
Cyan = 36,
- ///
+ ///
White = 37,
///
diff --git a/Terminal/Logging/ITarget.cs b/Terminal/Logging/ITarget.cs
index 6bdb7d3..d3e725d 100644
--- a/Terminal/Logging/ITarget.cs
+++ b/Terminal/Logging/ITarget.cs
@@ -7,10 +7,9 @@ public interface ITarget : IDisposable {
///
/// The method to write to output.
///
- /// The type of the text.
/// The severity of the message.
/// The time when it has been logged.
/// The logger.
/// The text to write ( ).
- public void Write(Severity severity, DateTime time, Logger logger, T? text);
+ public void Write(Severity severity, DateTime time, Logger logger, object? text);
}
\ No newline at end of file
diff --git a/Terminal/Logging/Logger.cs b/Terminal/Logging/Logger.cs
index 9898bca..31d0b03 100644
--- a/Terminal/Logging/Logger.cs
+++ b/Terminal/Logging/Logger.cs
@@ -1,4 +1,6 @@
+using System.Collections;
using System.Collections.ObjectModel;
+using OxDED.Terminal.Logging.Targets;
namespace OxDED.Terminal.Logging;
@@ -7,21 +9,18 @@ namespace OxDED.Terminal.Logging;
///
public class Logger : IDisposable, IEquatable {
///
- /// The ID of this logger.
+ /// The ID of this logger (if it is registered).
///
- public readonly string ID;
+ public readonly string? ID;
///
/// The name of this logger.
///
public readonly string Name;
+
///
- /// True if this logger is a sub logger.
- ///
- public bool IsSubLogger { get { return ParentLogger!=null; } }
- ///
- /// The parent logger if this logger is a sub logger.
+ /// Checks if this is a sub logger.
///
- public Logger? ParentLogger { get; private set; }
+ public bool IsSubLogger => this is SubLogger;
///
/// All the sub loggers of this logger.
@@ -31,9 +30,9 @@ public class Logger : IDisposable, IEquatable {
private readonly Dictionary subLoggers = [];
///
- /// All the targets, key MUST BE typeof(...Target) or ITarget.GetType() only when using .
+ /// All the targets.
///
- public Dictionary Targets;
+ public volatile List<(ITarget target, bool enabled)> Targets;
///
/// The current log severity max.
///
@@ -43,34 +42,35 @@ public class Logger : IDisposable, IEquatable {
///
public event LogCallback? OnLog;
- private Logger(Logger parentLogger, string name, string id, Severity severity, Dictionary targets) {
- ParentLogger = parentLogger;
- ID = id;
- Name = name;
- logLevel = severity;
- Targets = targets ?? new Dictionary { { typeof(TerminalTarget), (new TerminalTarget(), true) }, { typeof(FileTarget), (new FileTarget("./latest.log"), true) } };
- if (!Loggers.Register(this)) {
- throw new ArgumentException("A logger with this ID has already been registered.", nameof(id));
- }
- }
+
///
/// Creates a logger.
///
- /// The ID to identify this logger, like 'me.0xDED.MyProject' (if this ID is already registered it will throw an error).
+ /// The optional ID to identify this logger, like 'me.0xDED.MyProject'. It won't register if the ID is null (if this ID is already registered it will throw an error).
/// The name of the logger, used in the log files and terminal.
/// The log level of this logger.
/// The targets to add and enable (default: , with path "./latest.log").
///
- public Logger(string id, string name, Severity severity = Severity.Info, Dictionary? targets = null) {
+ public Logger(string name = "Logger", string? id = null, Severity severity = Severity.Info, List? targets = null) {
ID = id;
Name = name;
logLevel = severity;
- Targets = targets != null ? targets.Select(target => new KeyValuePair(target.Key, (target.Value, true))).ToDictionary() : new Dictionary{{typeof(TerminalTarget), (new TerminalTarget(), true)}, {typeof(FileTarget), (new FileTarget("./latest.log"), true)}};
- if (!Loggers.Register(this)) {
- throw new ArgumentException("A logger with this ID has already been registered.", nameof(id));
+ Targets = targets != null ? targets.Select(target => (target, true)).ToList() : [ (new TerminalTarget(), true), (new FileTarget("./latest.log"), true) ];
+ if (id != null) {
+ if (!Loggers.Register(this)) {
+ throw new ArgumentException("A logger with this ID has already been registered.", nameof(id));
+ }
}
}
+ ///
+ /// Checks if this logger is registered.
+ ///
+ ///
+ public bool IsRegistered() {
+ return ID is not null;
+ }
+
///
/// Sets the log level.
///
@@ -80,67 +80,147 @@ public void SetLevel(Severity maxSeverity) {
}
///
- /// Checks if this logger has a target of that type.
+ /// Checks if this logger has the target.
///
- /// The target type.
- /// True if it has a target of that type.
- public bool HasTarget(Type type) {
- return Targets.ContainsKey(type);
+ /// The target.
+ /// True if it has that target.
+ public bool HasTarget(ITarget target) {
+ foreach ((ITarget t, _) in Targets) {
+ if (ReferenceEquals(t, target)) {
+ return true;
+ }
+ }
+ return false;
}
///
- /// Checks if this logger has a target of that type.
+ /// Checks if this logger has a target with that type.
///
/// The type of the target.
- /// True if it has a target of that type.
+ /// True if it has that target type.
public bool HasTarget() where T : ITarget {
- return HasTarget(typeof(T));
+ foreach ((ITarget t, _) in Targets) {
+ if (t is T) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Gets the target's index.
+ ///
+ /// The target which index to get.
+ /// The index of the target or -1 if the target could not be found.
+ public int GetTargetIndex(ITarget target) {
+ for (int i = 0; i < Targets.Count; i++) {
+ if (ReferenceEquals(Targets[i].target, target)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ ///
+ /// Gets the first-matching target's index, which type is .
+ ///
+ /// The type of the target.
+ /// The index of the target or -1 if the target could not be found.
+ public int GetTargetIndex() where T : ITarget {
+ for (int i = 0; i < Targets.Count; i++) {
+ if (Targets[i].target is T) {
+ return i;
+ }
+ }
+ return -1;
}
///
- /// Gets the target from type.
+ /// Gets the target from its index.
///
- /// The target type.
+ /// The target index.
/// The target if there is one.
- public ITarget? GetTarget(Type type) {
- if (!Targets.TryGetValue(type, out (ITarget target, bool enabled) target)) {
+ public ITarget? GetTarget(int index) {
+ try {
+ return Targets[index].target;
+ } catch (IndexOutOfRangeException) {
return null;
}
- return target.target;
}
///
- /// Gets a target.
+ /// Gets the target from its index or default.
///
/// The type of the target.
- /// The target if there is one.
- ///
- public T GetTarget() where T : ITarget {
- ITarget? target = GetTarget(typeof(T)) ?? throw new ArgumentException("No target found.", nameof(T));
- return (T)target!;
+ /// The target index.
+ /// The target if there is one and if it is a .
+ public T? GetTarget(int index) where T : class, ITarget {
+ try {
+ return Targets[index].target as T;
+ } catch (IndexOutOfRangeException) {
+ return default;
+ }
+ }
+ ///
+ /// Gets the first target with that type or default.
+ ///
+ /// The type of the target.
+ /// The first found target with that type, if there is one and if it is a .
+ public T? GetTarget() where T : ITarget {
+ foreach ((ITarget target, _) in Targets) {
+ if (target is T t) {
+ return t;
+ }
+ }
+ return default;
}
+
///
- /// Sets if a target is enabled.
+ /// Sets whether a target is enabled.
///
- /// The type of the Target (e.g. TerminalTarget).
- /// True if enabled.
- /// True if there is a Target with that type.
+ /// The target to enable or disable.
+ /// True if it should be enabled.
+ /// True if it could find that target.
+ public bool SetTarget(ITarget target, bool enabled) {
+ int index = GetTargetIndex(target);
+ if (index == -1) {
+ return false;
+ }
+ Targets[index] = (target, enabled);
+ return true;
+ }
+
+ ///
+ /// Sets whether the first-matching target is enabled.
+ ///
+ /// The type of the target.
+ /// True if it should be enabled.
+ /// True if it could find a matching target.
public bool SetTarget(bool enabled) where T : ITarget {
- return SetTarget(typeof(T), enabled);
+ int index = GetTargetIndex();
+ if (index == -1) {
+ return false;
+ }
+ Targets[index] = (Targets[index].target, enabled);
+ return true;
}
///
- /// Sets if a target is enabled.
+ /// Sets whether a target is enabled.
///
- /// The type of the Target (e.g. typeof(TerminalTarget)).
- /// True if enabled.
- /// True if there is a Target with that type.
- public bool SetTarget(Type type, bool enabled) {
- if (Targets.TryGetValue(type, out (ITarget target, bool enabled) value)) {
- value.enabled = enabled;
- Targets[type] = value;
- return true;
+ /// The index of the target.
+ /// True if it should be enabled.
+ /// True if it was a valid index.
+ public bool SetTarget(int index, bool enabled) {
+ ITarget? target = GetTarget(index);
+ if (target == null) {
+ return false;
}
- return false;
+ try {
+ Targets[index] = (target, enabled);
+ } catch (IndexOutOfRangeException) {
+ return false;
+ }
+ return true;
}
///
@@ -149,42 +229,56 @@ public bool SetTarget(Type type, bool enabled) {
/// The target to add (e.g. typeof(TerminalTarget)).
/// If it is enabled.
public void AddTarget(ITarget target, bool enabled = true) {
- Targets.Add(target.GetType(), (target, enabled));
+ Targets.Add((target, enabled));
}
///
- /// Removes a Target.
+ /// Removes a target at that index.
///
- /// The type of the target
- /// True if there was a target with that type.
- public bool RemoveTarget() where T : ITarget {
- return RemoveTarget(typeof(T));
+ /// The index of the target to remove.
+ /// True if the target could be found.
+ public bool RemoveTargetAt(int index) {
+ try {
+ Targets.RemoveAt(index);
+ return true;
+ } catch (IndexOutOfRangeException) {
+ return false;
+ }
}
///
- /// Removes a Target.
+ /// Removes a target.
///
- /// The type of the target
- /// True if there was a target with that type.
- public bool RemoveTarget(Type type) {
- return Targets.Remove(type);
+ /// The target to remove.
+ /// True if the target could be found.
+ public bool RemoveTarget(ITarget target) {
+ return RemoveTargetAt(GetTargetIndex(target));
+ }
+
+ ///
+ /// Removes the first target with that type.
+ ///
+ /// The type of the target.
+ /// True if the target could be found.
+ public bool RemoveTarget() where T : ITarget {
+ return RemoveTargetAt(GetTargetIndex());
}
///
/// Checks if this logger has a sub logger with that ID.
///
- /// The ID of the sub logger.
+ /// The child ID of the sub logger.
/// True if this logger has a sub logger with that ID.
- public bool HasSubLogger(string ID) {
- return subLoggers.ContainsKey(ID);
+ public bool HasSubLogger(string childID) {
+ return subLoggers.ContainsKey(childID);
}
///
- /// Checks if this logger has a sub logger with that ID.
+ /// Checks if this logger has this logger as a sub logger.
///
/// The sub logger.
/// True if this logger has that sub logger.
- public bool HasSubLogger(Logger logger) {
- return subLoggers.ContainsKey(logger.ID);
+ public bool HasSubLogger(SubLogger logger) {
+ return ReferenceEquals(this, logger.ParentLogger);
}
///
@@ -194,96 +288,146 @@ public bool HasSubLogger(Logger logger) {
/// Please use , if you want to get a logger with that ID.
/// Or use if you want to know if it has a sub logger with that ID.
///
- /// The ID of the sub logger.
+ /// The child ID of the sub logger.
/// The sub logger.
- public Logger? GetSubLogger(string ID) {
- subLoggers.TryGetValue(ID, out Logger? logger);
+ public Logger? GetSubLogger(string childID) {
+ subLoggers.TryGetValue(childID, out Logger? logger);
return logger;
}
+
+ private bool handlingUnhandledExceptions;
+
+ private void HandleException(object sender, UnhandledExceptionEventArgs args) {
+ Exception e = (Exception) args.ExceptionObject;
+ LogException(e, args.IsTerminating ? Severity.Fatal : Severity.Error, true);
+ }
+
+ ///
+ /// Logs unhandled exceptions in the current app domain to this logger.
+ ///
+ public bool HandleUnhandledExceptions {
+ get {
+ return handlingUnhandledExceptions;
+ }
+ set {
+ if (value) {
+ AppDomain.CurrentDomain.UnhandledException += HandleException;
+ } else {
+ AppDomain.CurrentDomain.UnhandledException -= HandleException;
+ }
+ handlingUnhandledExceptions = value;
+ }
+ }
+
///
/// Logs something ( ).
///
- /// The type of the data.
/// The severity of the text.
/// The text to write ( ).
- public void Log(Severity severity, T? text) {
+ public void Log(Severity severity, object? text) {
DateTime time = DateTime.Now;
OnLog?.Invoke(this, text?.ToString()??"", severity, time);
if (((byte)severity) > ((byte)logLevel)) { return; }
- foreach (KeyValuePair target in Targets) {
- if (target.Value.enabled) {
- target.Value.target.Write(severity, time, this, text);
+ foreach ((ITarget target, bool enabled) target in Targets) {
+ if (target.enabled) {
+ target.target.Write(severity, time, this, text);
}
}
}
+
///
- /// Logs something with Info severity.
+ /// Logs something with Trace severity.
///
- /// The type of the data.
/// The text to write ( ).
- public void LogInfo(T? text) {
- Log(Severity.Info, text);
+ public void LogTrace(object? text) {
+ Log(Severity.Trace, text);
}
///
- /// Logs something with Error severity.
+ /// Logs something with Debug severity.
///
- /// The type of the data.
/// The text to write ( ).
- public void LogError(T? text) {
- Log(Severity.Error, text);
+ public void LogDebug(object? text) {
+ Log(Severity.Debug, text);
}
///
- /// Logs something with Fatal severity.
+ /// Logs something with Info severity.
///
- /// The type of the data.
/// The text to write ( ).
- public void LogFatal(T? text) {
- Log(Severity.Fatal, text);
+ public void LogInfo(object? text) {
+ Log(Severity.Info, text);
+ }
+ ///
+ /// Logs something with Message severity.
+ ///
+ /// The text to write ( ).
+ public void LogMessage(object? text) {
+ Log(Severity.Message, text);
}
///
/// Logs something with Warning severity.
///
- /// The type of the data.
/// The text to write ( ).
- public void LogWarning(T? text) {
+ public void LogWarning(object? text) {
Log(Severity.Warning, text);
}
///
- /// Logs something with Debug severity.
+ /// Logs something with Error severity.
///
- /// The type of the data.
/// The text to write ( ).
- public void LogDebug(T? text) {
- Log(Severity.Debug, text);
+ public void LogError(object? text) {
+ Log(Severity.Error, text);
}
///
- /// Logs something with Message severity.
+ /// Logs something with Fatal severity.
///
- /// The type of the data.
/// The text to write ( ).
- public void LogMessage(T? text) {
- Log(Severity.Message, text);
+ public void LogFatal(object? text) {
+ Log(Severity.Fatal, text);
+ }
+
+ private static string GetStacktrace(Exception e) {
+ string stackTrace = e.StackTrace ?? " (Unknown)";
+ if (e.InnerException != null) {
+ Exception inner = e.InnerException;
+ string source = inner.Source == null ? "" : $" (in: {inner.Source})";
+ return stackTrace + $"\nCaused by {inner.GetType().FullName} : '{inner.Message}'{source}: \n{GetStacktrace(inner)}";
+ } else {
+ return stackTrace;
+ }
}
///
- /// Logs something with Trace severity.
+ /// Logs an exception with a custom format.
///
- /// The type of the data.
- /// The text to write ( ).
- public void LogTrace(T? text) {
- Log(Severity.Trace, text);
+ /// The exception to log.
+ /// The severity of that exception.
+ public void LogException(Exception e, Severity severity = Severity.Error) {
+ LogException(e, severity, false);
+ }
+ private void LogException(Exception e, Severity severity, bool unhandled) {
+ string source = e.Source == null ? "" : $" (in: {e.Source})";
+ string unhandledStr = unhandled?"Unhandled Exception : " : "";
+ string message = $"{unhandledStr}{e.GetType().Name} ({e.GetType().FullName}) : '{e.Message}'\nTrace{source}:\n{GetStacktrace(e)}";
+
+ if (e.HelpLink != null) {
+ message += "\n\nHelp: "+e.HelpLink;
+ }
+
+ Log(severity, message);
}
///
/// Creates a sub logger.
///
/// The sub name of the logger.
- /// The sub ID (parent ID + '.' + ID = child ID).
+ /// The sub ID. Full ID will be: parent ID + '.' + subID = child ID).
+ /// If the sublogger should be registered, if the parent logger is also registered.
/// The log level of the new sub logger.
/// The targets of the new sub logger (default: Targets of parent).
/// The created sub logger.
- public Logger CreateSubLogger(string name, string id, Severity severity = Severity.Info, Dictionary? targets = null) {
- Logger subLogger = new(this, name, ID+'.'+id, severity, targets == null ? Targets : targets.Select(target => new KeyValuePair(target.Key, (target.Value, true))).ToDictionary());
- subLoggers.Add(subLogger.ID, subLogger);
+ ///
+ public SubLogger CreateSubLogger(string id, string name = "Sublogger", bool shouldRegister = true, Severity severity = Severity.Info, List? targets = null) {
+ SubLogger subLogger = new(this, id, name, (ID != null && shouldRegister) ? ID+'.'+id : null, severity, targets);
+ subLoggers.Add(id, subLogger);
return subLogger;
}
@@ -293,17 +437,17 @@ public Logger CreateSubLogger(string name, string id, Severity severity = Severi
///
public void Dispose() {
Loggers.UnRegister(this);
- foreach (KeyValuePair target in Targets) {
- target.Value.target.Dispose();
+
+ foreach ((ITarget target, _) in Targets) {
+ target.Dispose();
}
+
+ foreach (KeyValuePair subLogger in subLoggers) {
+ subLogger.Value.Dispose();
+ }
+
GC.SuppressFinalize(this);
}
- ///
- /// Disposes this logger.
- ///
- ~Logger() {
- Dispose();
- }
///
public static bool operator ==(Logger? left, Logger? right) {
@@ -332,7 +476,10 @@ public bool Equals(Logger? other) {
if (GetType() != other.GetType()) {
return false;
}
- return ID == other.ID;
+ if (ID!=null) {
+ return ID == other.ID;
+ }
+ return false;
}
///
///
@@ -343,6 +490,6 @@ public override bool Equals(object? obj) {
}
///
public override int GetHashCode() {
- return ID.GetHashCode();
+ return ID == null ? Name.GetHashCode() ^ subLoggers.GetHashCode() ^ Targets.GetHashCode() : ID.GetHashCode();
}
}
\ No newline at end of file
diff --git a/Terminal/Logging/Loggers.cs b/Terminal/Logging/Loggers.cs
index 2b6be9f..6934cbe 100644
--- a/Terminal/Logging/Loggers.cs
+++ b/Terminal/Logging/Loggers.cs
@@ -9,8 +9,9 @@ public static class Loggers {
/// Registers a logger.
///
/// The logger to register.
- /// False if there already is a logger with that ID.
+ /// False if there already is a logger with that ID or if the ID of that logger is null.
public static bool Register(Logger logger) {
+ if (logger.ID == null) return false;
if (registeredLoggers.ContainsKey(logger.ID)) { return false; }
registeredLoggers.Add(logger.ID, logger);
return true;
@@ -27,15 +28,16 @@ public static bool UnRegister(string ID) {
/// Unregisters a logger.
///
/// The logger to unregister.
- /// True if it was successful, false if that logger isn't registered or doesn't exist.
+ /// True if it was successful, false if that logger isn't registered.
public static bool UnRegister(Logger logger) {
+ if (logger.ID == null) return false;
return registeredLoggers.Remove(logger.ID);
}
///
/// Gets the logger to the corresponding ID if there is one.
///
/// The ID of the logger.
- /// The logger (if there is one).
+ /// The logger with that ID (if there is one registered).
public static Logger? Get(string ID) {
registeredLoggers.TryGetValue(ID, out Logger? logger);
return logger;
diff --git a/Terminal/Logging/Severity.cs b/Terminal/Logging/Severity.cs
index e4f7feb..95c2517 100644
--- a/Terminal/Logging/Severity.cs
+++ b/Terminal/Logging/Severity.cs
@@ -1,7 +1,7 @@
namespace OxDED.Terminal.Logging;
///
-/// Logger severity.
+/// Logger severity (from high severity to low severity).
///
public enum Severity : byte {
///
diff --git a/Terminal/Logging/SubLogger.cs b/Terminal/Logging/SubLogger.cs
new file mode 100644
index 0000000..a328397
--- /dev/null
+++ b/Terminal/Logging/SubLogger.cs
@@ -0,0 +1,21 @@
+namespace OxDED.Terminal.Logging;
+
+///
+/// Represents a child of another logger.
+///
+public sealed class SubLogger : Logger {
+ ///
+ /// The parent logger if this logger is a sub logger.
+ ///
+ public Logger ParentLogger { get; private set; }
+
+ ///
+ /// The ID of the child.
+ ///
+ public readonly string childID;
+
+ internal SubLogger(Logger parentLogger, string id = "Sublogger", string name = "Sublogger", string? registeredId = null, Severity severity = Severity.Info, List? targets = null) : base(name, registeredId, severity, targets) {
+ ParentLogger = parentLogger;
+ childID = id;
+ }
+}
\ No newline at end of file
diff --git a/Terminal/Logging/Targets/FileTarget.cs b/Terminal/Logging/Targets/FileTarget.cs
index 945f25d..fdda9a6 100644
--- a/Terminal/Logging/Targets/FileTarget.cs
+++ b/Terminal/Logging/Targets/FileTarget.cs
@@ -1,33 +1,14 @@
-namespace OxDED.Terminal.Logging;
+namespace OxDED.Terminal.Logging.Targets;
///
/// A Logger Target for a log file.
///
-public class FileTarget : ITarget {
+public class FileTarget : FormattedTarget {
///
/// The output stream to the file.
///
public readonly TextWriter FileOut;
- ///
- /// The format to use for writing to the terminal (0: logger name, 1: logger ID, 2: time, 3: severity, 4: message).
- ///
- ///
- /// Default:
- /// [{0}][{2}][{3}]: {4}
- ///
- public string Format = "[{0}][{2}][{3}]: {4}";
-
- ///
- /// The format to use for creating names (0: parent name, 1: own name).
- /// Example for default:
- /// {{App}: Sublogger}: sub-sublogger
- ///
- ///
- /// default:
- /// {0}: {1}
- ///
- public string NameFormat = "{0}: {1}";
-
+
///
/// Creates a target that targets a log file.
///
@@ -37,27 +18,18 @@ public FileTarget(string path, string? format = null) {
if (format != null) {
Format = format;
}
- FileOut = new StreamWriter(File.OpenWrite(path));
- }
- ///
- public void Dispose() {
- FileOut.Close();
- GC.SuppressFinalize(this);
- }
-
- private string GetName(Logger logger) {
- if (!logger.IsSubLogger) {
- return logger.Name;
- } else {
- return string.Format(NameFormat, GetName(logger.ParentLogger!), logger.Name);
- }
+ FileOut = new StreamWriter(File.Open(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite));
}
-
///
///
/// Writes a line.
///
- public void Write(Severity severity, DateTime time, Logger logger, T? text) {
- FileOut.WriteLine(string.Format(Format, GetName(logger), logger.ID, time.ToString(), severity.ToString(), text?.ToString()??""));
+ public override void Write(Severity severity, DateTime time, Logger logger, object? text) {
+ FileOut.WriteLine(GetText(logger, time, severity, text?.ToString() ?? "(Null)"));
+ }
+ ///
+ public override void Dispose() {
+ FileOut.Close();
+ GC.SuppressFinalize(this);
}
}
\ No newline at end of file
diff --git a/Terminal/Logging/Targets/FormattedTarget.cs b/Terminal/Logging/Targets/FormattedTarget.cs
new file mode 100644
index 0000000..257f518
--- /dev/null
+++ b/Terminal/Logging/Targets/FormattedTarget.cs
@@ -0,0 +1,58 @@
+
+namespace OxDED.Terminal.Logging.Targets;
+
+///
+/// Represents a target with formattable logs.
+///
+public abstract class FormattedTarget : ITarget {
+ ///
+ /// The format to use for writing to the terminal (0: logger name, 1: logger ID, 2: time, 3: severity, 4: message).
+ ///
+ ///
+ /// Default:
+ /// [{0}][{2}][{3}]: {4}
+ ///
+ public string Format = "[{0}][{2}][{3}]: {4}";
+
+ ///
+ /// The format to use for creating names (0: parent name, 1: own name).
+ /// Example for default:
+ /// {{App}: Sublogger}: sub-sublogger
+ ///
+ ///
+ /// default:
+ /// {0}: {1}
+ ///
+ public string NameFormat = "{0}: {1}";
+
+ ///
+ /// Generates the name of a logger with a format.
+ ///
+ /// The logger which name to generate.
+ /// The generated name.
+ protected string GetName(Logger logger) {
+ if (!logger.IsSubLogger) {
+ return logger.Name;
+ } else {
+ return string.Format(NameFormat, GetName((logger as SubLogger)!.ParentLogger), logger.Name);
+ }
+ }
+
+ ///
+ /// Generates the log with a format.
+ ///
+ /// The logger which name and ID to use.
+ /// The time of the log.
+ /// The severity of the log.
+ /// The text of the log.
+ /// The generated log.
+ protected string GetText(Logger logger, DateTime time, Severity severity, string text) {
+ return string.Format(Format, GetName(logger), logger.ID, time.ToString(), severity.ToString(), text);
+ }
+
+ ///
+ public abstract void Write(Severity severity, DateTime time, Logger logger, object? text);
+
+ ///
+ public abstract void Dispose();
+}
\ No newline at end of file
diff --git a/Terminal/Logging/Targets/TerminalTarget.cs b/Terminal/Logging/Targets/TerminalTarget.cs
index 372e087..c5154af 100644
--- a/Terminal/Logging/Targets/TerminalTarget.cs
+++ b/Terminal/Logging/Targets/TerminalTarget.cs
@@ -1,9 +1,9 @@
-namespace OxDED.Terminal.Logging;
+namespace OxDED.Terminal.Logging.Targets;
///
/// A Logger Target for the terminal.
///
-public class TerminalTarget : ITarget {
+public class TerminalTarget : FormattedTarget {
///
/// The out stream to the terminal.
///
@@ -13,23 +13,13 @@ public class TerminalTarget : ITarget {
///
public TextWriter Error;
///
- /// The format to use for writing to the terminal (0: name see , 1: logger ID, 2: time, 3: severity, 4: message, 5: color ANSI).
+ /// The format to use for writing to the terminal (0: name see , 1: logger ID, 2: time, 3: severity, 4: message, 5: color ANSI).
///
///
/// Default:
/// {5}[{0}][{2}][BOLD{3}RESETBOLD]: {4}RESETALL
///
- public string Format = "{5}[{0}][{2}]["+ANSI.Styles.Bold+"{3}"+ANSI.Styles.ResetBold+"]: {4}"+ANSI.Styles.ResetAll;
- ///
- /// The format to use for creating names (0: parent name, 1: own name).
- /// Example for default:
- /// {{App}: Sublogger}: sub-sublogger
- ///
- ///
- /// default:
- /// {0}: {1}
- ///
- public string NameFormat = "{0}: {1}";
+ public new string Format = "{5}[{0}][{2}]["+ANSI.Styles.Bold+"{3}"+ANSI.Styles.ResetBold+"]: {4}"+ANSI.Styles.ResetAll;
///
/// The colors of the severities (index: 0: Fatal, 1: Error, 2: Warning, 3: Message, 4: Info, 5: Debug, 6: Trace).
///
@@ -48,27 +38,23 @@ public TerminalTarget(string? format = null, TextWriter? terminalOut = null, Tex
Error = terminalError ?? Terminal.Error;
}
///
- public void Dispose() {
+ public override void Dispose() {
GC.SuppressFinalize(this);
}
- private string GetName(Logger logger) {
- if (!logger.IsSubLogger) {
- return logger.Name;
- } else {
- return string.Format(NameFormat, GetName(logger.ParentLogger!), logger.Name);
- }
+ private string GetText(Logger logger, DateTime time, Severity severity, string text, string color) {
+ return string.Format(Format, GetName(logger), logger.ID, time.ToString(), severity.ToString(), text, color);
}
///
///
/// Writes a line.
///
- public void Write(Severity severity, DateTime time, Logger logger, T? text) {
+ public override void Write(Severity severity, DateTime time, Logger logger, object? text) {
if (((byte)severity) < 2) {
- Error.WriteLine(string.Format(Format, GetName(logger), logger.ID, time.ToString(), severity.ToString(), text?.ToString()??"", SeverityColors[(byte)severity].ToForegroundANSI()));
+ Error.WriteLine(GetText(logger, time, severity, text?.ToString() ?? "(Null)", SeverityColors[(byte)severity].ToForegroundANSI()));
} else {
- Out.WriteLine(string.Format(Format, GetName(logger), logger.ID, time.ToString(), severity.ToString(), text?.ToString()??"", SeverityColors[(byte)severity].ToForegroundANSI()));
+ Out.WriteLine(GetText(logger, time, severity, text?.ToString() ?? "(Null)", SeverityColors[(byte)severity].ToForegroundANSI()));
}
}
diff --git a/Terminal/Style.cs b/Terminal/Style.cs
index b714361..58c71f3 100644
--- a/Terminal/Style.cs
+++ b/Terminal/Style.cs
@@ -12,18 +12,17 @@ public class Style : IEquatable