From 6b4aabd79bf347bc9068603c9556dee39bb78ce5 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:42:28 +0100 Subject: [PATCH 01/27] Revert "quick" This reverts commit ab19baae8ea82dc462480f9239a52fb494b26a78. --- Terminal/Backend/WindowsBackend.cs | 283 ------------------ Terminal/Terminal.cs | 4 +- .../TerminalWindow.cs} | 133 ++++---- Terminal/Window/WinTerminalWindow.cs | 266 ++++++++++++++++ Terminal/Window/Window.cs | 13 - 5 files changed, 334 insertions(+), 365 deletions(-) delete mode 100644 Terminal/Backend/WindowsBackend.cs rename Terminal/{Backend/TerminalBackend.cs => Window/TerminalWindow.cs} (67%) create mode 100644 Terminal/Window/WinTerminalWindow.cs delete mode 100644 Terminal/Window/Window.cs 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/Terminal.cs b/Terminal/Terminal.cs index 27e71e7..1eb0c21 100644 --- a/Terminal/Terminal.cs +++ b/Terminal/Terminal.cs @@ -83,9 +83,9 @@ public static bool ListenForKeys {set { /// The name of the window /// /// - public static TerminalWindow CreateBackend(string title) { + public static TerminalWindow CreateWindow(string title) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return new WindowsBackend(title); + return new WinTerminalWindow(title); } else { throw new PlatformNotSupportedException("No window implementation for your platform."); } diff --git a/Terminal/Backend/TerminalBackend.cs b/Terminal/Window/TerminalWindow.cs similarity index 67% rename from Terminal/Backend/TerminalBackend.cs rename to Terminal/Window/TerminalWindow.cs index 382592b..eb8c257 100644 --- a/Terminal/Backend/TerminalBackend.cs +++ b/Terminal/Window/TerminalWindow.cs @@ -1,71 +1,54 @@ using System.Text; -namespace OxDED.Terminal.Backend; +namespace OxDED.Terminal.Window; /// -/// Represents an interface of common methods of a terminal. +/// Represents a terminal window that can be used. /// -public interface ITerminalBackend : IDisposable { +public abstract class TerminalWindow : IDisposable { /// - /// The data stream for reading from the terminal. + /// Sets default values. /// - public TextReader StandardInput { get; } + protected TerminalWindow() { + IsDisposed = true; + } + /// - /// The data stream for writing to the terminal. + /// The title of the Terminal window. /// - public TextWriter StandardOutput { get; } + public abstract string Title { get; set;} /// - /// The data stream for writing errors to the terminal. + /// The out (to terminal) stream. /// - public TextWriter StandardError { get; } + public abstract TextWriter Out {get;} /// - /// The encoding used for the stream (default: UTF-8). + /// The in (from terminal) stream. /// - public Encoding InputEncoding { get; set; } + public abstract TextReader In {get;} /// - /// The encoding used for the stream (default: UTF-8). + /// The error (to terminal) stream. /// - public Encoding OutputEncoding { get; set; } + public abstract TextWriter Error {get;} /// - /// The encoding used for the stream (default: UTF-8). + /// Hides or shows terminal cursor. /// - public Encoding ErrorEncoding { get; set; } + public abstract bool HideCursor {get; set;} /// - /// The width and the height (in characters) of the terminal. + /// The width (in characters) of the terminal. /// - public (int Width, int Height) Size { get; set; } - -} - -/// -/// Represents an interface of common methods of a terminal. -/// -public abstract class TerminalBackend : ITerminalBackend { - /// - public abstract TextReader StandardInput { get; } - - /// - public abstract TextWriter StandardOutput { get; } - - /// - public abstract TextWriter StandardError { get; } - - /// - public abstract Encoding InputEncoding { get; set; } - - /// - public abstract Encoding OutputEncoding { get; set; } - - /// - public abstract Encoding ErrorEncoding { get; set; } - - /// - public virtual (int Width, int Height) Size { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - /// - public abstract void Dispose(); - - + public abstract int Width {get;} + /// + /// The height (in characters) of the terminal. + /// + public abstract int Height {get;} + /// + /// The encoding used for the in stream (default: UTF-8). + /// + public abstract Encoding InEncoding {get; set;} + /// + /// The encoding used for the error and out streams (default: UTF-8). + /// + public abstract Encoding OutEncoding {get; set;} /// /// Writes something () to the terminal, with a style. /// @@ -73,7 +56,7 @@ public abstract class TerminalBackend : ITerminalBackend { /// The thing to write to the terminal. /// The text decoration to use. public virtual void Write(T? text, Style? style = null) { - StandardOutput.Write((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + Out.Write((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the terminal, with a style. @@ -82,7 +65,7 @@ public virtual void Write(T? text, Style? style = null) { /// The thing to write to the terminal. /// The text decoration to use. public virtual void WriteLine(T? text, Style? style = null) { - StandardOutput.WriteLine((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + Out.WriteLine((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the error stream, with a style. @@ -91,7 +74,7 @@ public virtual void WriteLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteErrorLine(T? text, Style? style = null) { - StandardError.WriteLine((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + Error.WriteLine((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the error stream, with a style. @@ -100,7 +83,7 @@ public virtual void WriteErrorLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteError(T? text, Style? style = null) { - StandardError.Write((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + Error.Write((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Sets the cursor to that position. @@ -108,11 +91,9 @@ public virtual void WriteError(T? text, Style? style = null) { /// The position. /// public virtual void Goto((int x, int y) pos) { - try { - if (pos.x >= Size.Width || pos.x < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos x is higher than the width or is lower than 0."); } - if (pos.y >= Size.Height || pos.y < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos y is higher than the height or is lower than 0."); } - } catch (NotImplementedException) { } - StandardOutput.Write(ANSI.MoveCursor(pos.x, pos.y)); + if (pos.x >= Width || pos.x < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos x is higher than the width or is lower than 0."); } + if (pos.y >= Height || pos.y < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos y is higher than the height or is lower than 0."); } + Out.Write(ANSI.MoveCursor(pos.x, pos.y)); } /// /// Gets the cursor position. @@ -122,7 +103,7 @@ public virtual void Goto((int x, int y) pos) { /// /// Sets the something () at a , with a . /// - /// The type of what to write. + /// /// The thing to set at to the terminal. /// The position to set at. /// The text decoration to use. @@ -134,7 +115,7 @@ public virtual void Set(T? text, (int x, int y) pos, Style? style = null) { /// /// Sets the something in the error stream () at a , with a . /// - /// The type of what to write. + /// /// The thing to set at to the terminal. /// The position to set at. /// The text decoration to use. @@ -147,14 +128,14 @@ public virtual void SetError(T? text, (int x, int y) pos, Style? style = null /// /// The character that has been read (-1 if everything has been read). public virtual int Read() { - return StandardInput.Read(); + return In.Read(); } /// /// Reads a line from the input stream. /// /// The line that has been read (null if everything has been read). public virtual string? ReadLine() { - return StandardInput.ReadLine(); + return In.ReadLine(); } /// /// Waits until a key is pressed. @@ -181,7 +162,7 @@ protected void KeyPress(ConsoleKey key, char keyChar, bool alt, bool shift, bool /// public virtual void Clear() { Goto((0,0)); - StandardOutput.Write(ANSI.EraseScreenFromCursor); + Out.Write(ANSI.EraseScreenFromCursor); } /// /// Clears screen from the position to end of the screen. @@ -189,7 +170,7 @@ public virtual void Clear() { /// The start position. public virtual void ClearFrom((int x, int y) pos) { Goto(pos); - StandardOutput.Write(ANSI.EraseLineFromCursor); + Out.Write(ANSI.EraseLineFromCursor); } /// /// Clears (deletes) a line. @@ -197,7 +178,7 @@ public virtual void ClearFrom((int x, int y) pos) { /// The y-axis of the line. public virtual void ClearLine(int line) { Goto((0, line)); - StandardOutput.Write(ANSI.EraseLine); + Out.Write(ANSI.EraseLine); } /// /// Clears the line from the position to the end of the line. @@ -205,7 +186,7 @@ public virtual void ClearLine(int line) { /// The start position. public virtual void ClearLineFrom((int x, int y) pos) { Goto(pos); - StandardOutput.Write(ANSI.EraseLineFromCursor); + Out.Write(ANSI.EraseLineFromCursor); } /// /// The thread that is running . @@ -229,9 +210,27 @@ public virtual bool ListenForKeys {set { } get { return listenForKeys; }} - /// /// Method in new thread that should call when a key is pressed. /// protected abstract void ListenForKeysMethod(); + + /// + /// If it already is disposed. + /// + public bool IsDisposed {get; protected set;} + /// + public virtual void Dispose() { + if (IsDisposed) { return; } + IsDisposed = true; + + GC.SuppressFinalize(this); + } + + /// + /// Disposes the window. + /// + ~TerminalWindow() { + Dispose(); + } } \ No newline at end of file diff --git a/Terminal/Window/WinTerminalWindow.cs b/Terminal/Window/WinTerminalWindow.cs new file mode 100644 index 0000000..52bf4d1 --- /dev/null +++ b/Terminal/Window/WinTerminalWindow.cs @@ -0,0 +1,266 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +namespace OxDED.Terminal.Window; + +internal static class Utils { + + internal static Stream GetStream(nint handle) { + SafeFileHandle fileHandle = new(handle, false); + FileStream stream = new(fileHandle, FileAccess.ReadWrite); + return stream; + } +} + +internal static partial class WinAPI { + [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; + } + internal const int STD_OUTPUT_HANDLE = -11; + internal const int STD_INPUT_HANDLE = -10; + internal const int STD_ERROR_HANDLE = -12; + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool AllocConsole(); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool FreeConsole(); + [LibraryImport("kernel32.dll")] + internal static partial nint GetConsoleWindow(); + [LibraryImport("kernel32.dll", SetLastError = true)] + internal static partial nint GetStdHandle(int nStdHandle); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool CloseHandle(nint handle); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetStdHandle(int nStdHandle, nint hConsoleOutput); + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern nint CreateFile(string fileName, uint desiredAccess, int shareMode, nint securityAttributes, int creationDisposition, int flagsAndAttributes, nint templateFile); + [DllImport("kernel32.dll", SetLastError = true, BestFitMapping = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SetConsoleTitle(string title); + [DllImport("kernel32.dll", SetLastError = true, BestFitMapping = true, CharSet = CharSet.Auto)] + internal static extern uint GetConsoleTitle([MarshalAs(UnmanagedType.LPTStr)]out string lpConsoleTitle, uint nSize); + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool ReadConsoleInput(nint hConsoleInput, out INPUT_RECORD lpBuffer, int nLength, out int numEventsRead); +} + +/// +/// A wrapper for on Windows. +/// +public class WinTerminalWindow : TerminalWindow { + private const string ConsoleIn = "CONIN$"; + private const string ConsoleOut = "CONOUT$"; + private const string ConsoleError = ConsoleOut; + + private nint consoleOut; + private nint consoleIn; + private nint consoleErr; + /// + /// Creates a new Windows terminal window. + /// + /// The name of the window. + /// + public WinTerminalWindow(string title) { + nint stdOut = WinAPI.GetStdHandle(WinAPI.STD_OUTPUT_HANDLE); + nint stdIn = WinAPI.GetStdHandle(WinAPI.STD_INPUT_HANDLE); + nint stdErr = WinAPI.GetStdHandle(WinAPI.STD_ERROR_HANDLE); + + if (WinAPI.GetConsoleWindow() == nint.Zero) { + if (!WinAPI.AllocConsole()) { + throw new Win32Exception("Failed to allocate a console: "+Marshal.GetLastWin32Error()); + } + } + + consoleOut = WinAPI.CreateFile(ConsoleOut, 0x80000000 | 0x40000000, 2, nint.Zero, 3, 0, nint.Zero); + consoleIn = WinAPI.CreateFile(ConsoleIn, 0x80000000 | 0x40000000, 1, nint.Zero, 3, 0, nint.Zero); + consoleErr = WinAPI.CreateFile(ConsoleError, 0x80000000 | 0x40000000, 2, nint.Zero, 3, 0, nint.Zero); + + if (!WinAPI.SetStdHandle(WinAPI.STD_OUTPUT_HANDLE, consoleOut)) { + throw new Win32Exception("Failed to set the handle for the console out stream: "+Marshal.GetLastWin32Error()); + } + + if (!WinAPI.SetStdHandle(WinAPI.STD_INPUT_HANDLE, consoleIn)) { + throw new Win32Exception("Failed to set the handle for the console in stream: "+Marshal.GetLastWin32Error()); + } + + if (!WinAPI.SetStdHandle(WinAPI.STD_ERROR_HANDLE, consoleErr)) { + throw new Win32Exception("Failed to set the handle for the console error stream: "+Marshal.GetLastWin32Error()); + } + + if (!WinAPI.CloseHandle(stdOut)) { + throw new Win32Exception("Failed to close the handle of stdOut: "+Marshal.GetLastWin32Error()); + } + if (!WinAPI.CloseHandle(stdIn)) { + throw new Win32Exception("Failed to close the handle of stdIn: "+Marshal.GetLastWin32Error()); + } + if (!WinAPI.CloseHandle(stdErr)) { + throw new Win32Exception("Failed to close the handle of stdErr: "+Marshal.GetLastWin32Error()); + } + + outStream = new StreamWriter(Utils.GetStream(consoleOut), Encoding.UTF8); + inStream = new StreamReader(Utils.GetStream(consoleIn), Encoding.UTF8); + errStream = new StreamWriter(Utils.GetStream(consoleErr), Encoding.UTF8); + + Title = title; + } + private StreamWriter outStream; + /// + public override TextWriter Out { get => outStream;} + private StreamReader inStream; + /// + public override TextReader In { get => inStream;} + private StreamWriter errStream; + /// + public override TextWriter Error { get => errStream;} + + /// + public override int Width => Console.WindowWidth; + + /// + public override int Height => Console.WindowHeight; + /// + public override Encoding InEncoding { get => inStream.CurrentEncoding; set { inStream = new StreamReader(Utils.GetStream(consoleIn), value); } } + /// + public override Encoding OutEncoding { get => outStream.Encoding; set { outStream = new StreamWriter(Utils.GetStream(consoleOut), value); errStream = new StreamWriter(Utils.GetStream(consoleErr), value); } } + /// + /// 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()); + } + } + } + + private bool isCursorHidden = false; + + /// + public override bool HideCursor { + set { + if (value) { + outStream.Write(ANSI.CursorInvisible); + } else { + outStream.Write(ANSI.CursorVisible); + } + isCursorHidden = value; + } get { + return isCursorHidden; + } + } + + /// + /// + public override void Dispose() { + if (IsDisposed) { return; } + + if (!WinAPI.CloseHandle(consoleOut)) { + throw new Win32Exception("Failed to close console out: "+Marshal.GetLastWin32Error()); + } + consoleOut = nint.Zero; + if (!WinAPI.CloseHandle(consoleIn)) { + throw new Win32Exception("Failed to close console in: "+Marshal.GetLastWin32Error()); + } + consoleIn = nint.Zero; + if (!WinAPI.CloseHandle(consoleErr)) { + throw new Win32Exception("Failed to close console err: "+Marshal.GetLastWin32Error()); + } + consoleErr = nint.Zero; + + if (!WinAPI.FreeConsole()) { + throw new Win32Exception("Failed to free the console window: "+Marshal.GetLastWin32Error()); + } + Console.SetError(new StreamWriter(Console.OpenStandardError())); + Console.SetIn(new StreamReader(Console.OpenStandardInput())); + Console.SetOut(new StreamWriter(Console.OpenStandardOutput())); + + IsDisposed = true; + GC.SuppressFinalize(this); + } + + /// + /// + 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()); + } + } + + [Flags] + internal enum ControlKeyState { + RightAltPressed = 0x0001, + LeftAltPressed = 0x0002, + RightCtrlPressed = 0x0004, + LeftCtrlPressed = 0x0008, + ShiftPressed = 0x0010, + NumLockOn = 0x0020, + ScrollLockOn = 0x0040, + CapsLockOn = 0x0080, + EnhancedKey = 0x0100 + } +} \ No newline at end of file diff --git a/Terminal/Window/Window.cs b/Terminal/Window/Window.cs deleted file mode 100644 index 08130ec..0000000 --- a/Terminal/Window/Window.cs +++ /dev/null @@ -1,13 +0,0 @@ -using OxDED.Terminal.Backend; - -namespace OxDED.Terminal.Window; - -/// -/// Represents a terminal window. -/// -public interface IWindow : ITerminalBackend { - /// - /// The title of the terminal window. - /// - public string Title { get; set; } -} \ No newline at end of file From 988bb93c9d285821d216b63893fb6e2b430882c6 Mon Sep 17 00:00:00 2001 From: 0xDED <63008025+dedouwe26@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:43:43 +0100 Subject: [PATCH 02/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 826e739..17cec9b 100644 --- a/README.md +++ b/README.md @@ -8,5 +8,5 @@ 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) +- [Window](https://github.com/dedouwe26/Terminal/tree/main/examples/Window/Program.cs) (doesn't work) - [Argument parsing](https://github.com/dedouwe26/Terminal/tree/main/examples/Args/Program.cs) From 7e4ed8229360ad86fb33efd2722e685a52661d39 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:21:08 +0100 Subject: [PATCH 03/27] Revert "Revert "quick"" This reverts commit 6b4aabd79bf347bc9068603c9556dee39bb78ce5. --- .../TerminalBackend.cs} | 133 ++++---- Terminal/Backend/WindowsBackend.cs | 283 ++++++++++++++++++ Terminal/Terminal.cs | 4 +- Terminal/Window/WinTerminalWindow.cs | 266 ---------------- Terminal/Window/Window.cs | 13 + 5 files changed, 365 insertions(+), 334 deletions(-) rename Terminal/{Window/TerminalWindow.cs => Backend/TerminalBackend.cs} (67%) create mode 100644 Terminal/Backend/WindowsBackend.cs delete mode 100644 Terminal/Window/WinTerminalWindow.cs create mode 100644 Terminal/Window/Window.cs diff --git a/Terminal/Window/TerminalWindow.cs b/Terminal/Backend/TerminalBackend.cs similarity index 67% rename from Terminal/Window/TerminalWindow.cs rename to Terminal/Backend/TerminalBackend.cs index eb8c257..382592b 100644 --- a/Terminal/Window/TerminalWindow.cs +++ b/Terminal/Backend/TerminalBackend.cs @@ -1,54 +1,71 @@ using System.Text; -namespace OxDED.Terminal.Window; +namespace OxDED.Terminal.Backend; /// -/// Represents a terminal window that can be used. +/// Represents an interface of common methods of a terminal. /// -public abstract class TerminalWindow : IDisposable { +public interface ITerminalBackend : IDisposable { /// - /// Sets default values. + /// The data stream for reading from the terminal. /// - protected TerminalWindow() { - IsDisposed = true; - } - - /// - /// The title of the Terminal window. - /// - public abstract string Title { get; set;} - /// - /// The out (to terminal) stream. - /// - public abstract TextWriter Out {get;} - /// - /// The in (from terminal) stream. - /// - public abstract TextReader In {get;} + public TextReader StandardInput { get; } /// - /// The error (to terminal) stream. + /// The data stream for writing to the terminal. /// - public abstract TextWriter Error {get;} + public TextWriter StandardOutput { get; } /// - /// Hides or shows terminal cursor. + /// The data stream for writing errors to the terminal. /// - public abstract bool HideCursor {get; set;} + public TextWriter StandardError { get; } /// - /// The width (in characters) of the terminal. + /// The encoding used for the stream (default: UTF-8). /// - public abstract int Width {get;} + public Encoding InputEncoding { get; set; } /// - /// The height (in characters) of the terminal. + /// The encoding used for the stream (default: UTF-8). /// - public abstract int Height {get;} + public Encoding OutputEncoding { get; set; } /// - /// The encoding used for the in stream (default: UTF-8). + /// The encoding used for the stream (default: UTF-8). /// - public abstract Encoding InEncoding {get; set;} + public Encoding ErrorEncoding { get; set; } /// - /// The encoding used for the error and out streams (default: UTF-8). + /// The width and the height (in characters) of the terminal. /// - public abstract Encoding OutEncoding {get; set;} + public (int Width, int Height) Size { get; set; } + +} + +/// +/// Represents an interface of common methods of a terminal. +/// +public abstract class TerminalBackend : ITerminalBackend { + /// + public abstract TextReader StandardInput { get; } + + /// + public abstract TextWriter StandardOutput { get; } + + /// + public abstract TextWriter StandardError { get; } + + /// + public abstract Encoding InputEncoding { get; set; } + + /// + public abstract Encoding OutputEncoding { get; set; } + + /// + public abstract Encoding ErrorEncoding { get; set; } + + /// + public virtual (int Width, int Height) Size { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + /// + public abstract void Dispose(); + + /// /// Writes something () to the terminal, with a style. /// @@ -56,7 +73,7 @@ protected TerminalWindow() { /// The thing to write to the terminal. /// The text decoration to use. public virtual void Write(T? text, Style? style = null) { - Out.Write((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + StandardOutput.Write((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the terminal, with a style. @@ -65,7 +82,7 @@ public virtual void Write(T? text, Style? style = null) { /// The thing to write to the terminal. /// The text decoration to use. public virtual void WriteLine(T? text, Style? style = null) { - Out.WriteLine((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + StandardOutput.WriteLine((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the error stream, with a style. @@ -74,7 +91,7 @@ public virtual void WriteLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteErrorLine(T? text, Style? style = null) { - Error.WriteLine((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + StandardError.WriteLine((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the error stream, with a style. @@ -83,7 +100,7 @@ public virtual void WriteErrorLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteError(T? text, Style? style = null) { - Error.Write((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + StandardError.Write((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Sets the cursor to that position. @@ -91,9 +108,11 @@ public virtual void WriteError(T? text, Style? style = null) { /// The position. /// public virtual void Goto((int x, int y) pos) { - if (pos.x >= Width || pos.x < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos x is higher than the width or is lower than 0."); } - if (pos.y >= Height || pos.y < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos y is higher than the height or is lower than 0."); } - Out.Write(ANSI.MoveCursor(pos.x, pos.y)); + try { + if (pos.x >= Size.Width || pos.x < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos x is higher than the width or is lower than 0."); } + if (pos.y >= Size.Height || pos.y < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos y is higher than the height or is lower than 0."); } + } catch (NotImplementedException) { } + StandardOutput.Write(ANSI.MoveCursor(pos.x, pos.y)); } /// /// Gets the cursor position. @@ -103,7 +122,7 @@ public virtual void Goto((int x, int y) pos) { /// /// Sets the something () at a , with a . /// - /// + /// The type of what to write. /// The thing to set at to the terminal. /// The position to set at. /// The text decoration to use. @@ -115,7 +134,7 @@ public virtual void Set(T? text, (int x, int y) pos, Style? style = null) { /// /// Sets the something in the error stream () at a , with a . /// - /// + /// The type of what to write. /// The thing to set at to the terminal. /// The position to set at. /// The text decoration to use. @@ -128,14 +147,14 @@ public virtual void SetError(T? text, (int x, int y) pos, Style? style = null /// /// The character that has been read (-1 if everything has been read). public virtual int Read() { - return In.Read(); + return StandardInput.Read(); } /// /// Reads a line from the input stream. /// /// The line that has been read (null if everything has been read). public virtual string? ReadLine() { - return In.ReadLine(); + return StandardInput.ReadLine(); } /// /// Waits until a key is pressed. @@ -162,7 +181,7 @@ protected void KeyPress(ConsoleKey key, char keyChar, bool alt, bool shift, bool /// public virtual void Clear() { Goto((0,0)); - Out.Write(ANSI.EraseScreenFromCursor); + StandardOutput.Write(ANSI.EraseScreenFromCursor); } /// /// Clears screen from the position to end of the screen. @@ -170,7 +189,7 @@ public virtual void Clear() { /// The start position. public virtual void ClearFrom((int x, int y) pos) { Goto(pos); - Out.Write(ANSI.EraseLineFromCursor); + StandardOutput.Write(ANSI.EraseLineFromCursor); } /// /// Clears (deletes) a line. @@ -178,7 +197,7 @@ public virtual void ClearFrom((int x, int y) pos) { /// The y-axis of the line. public virtual void ClearLine(int line) { Goto((0, line)); - Out.Write(ANSI.EraseLine); + StandardOutput.Write(ANSI.EraseLine); } /// /// Clears the line from the position to the end of the line. @@ -186,7 +205,7 @@ public virtual void ClearLine(int line) { /// The start position. public virtual void ClearLineFrom((int x, int y) pos) { Goto(pos); - Out.Write(ANSI.EraseLineFromCursor); + StandardOutput.Write(ANSI.EraseLineFromCursor); } /// /// The thread that is running . @@ -210,27 +229,9 @@ public virtual bool ListenForKeys {set { } get { return listenForKeys; }} + /// /// Method in new thread that should call when a key is pressed. /// protected abstract void ListenForKeysMethod(); - - /// - /// If it already is disposed. - /// - public bool IsDisposed {get; protected set;} - /// - public virtual void Dispose() { - if (IsDisposed) { return; } - IsDisposed = true; - - GC.SuppressFinalize(this); - } - - /// - /// Disposes the window. - /// - ~TerminalWindow() { - Dispose(); - } } \ No newline at end of file diff --git a/Terminal/Backend/WindowsBackend.cs b/Terminal/Backend/WindowsBackend.cs new file mode 100644 index 0000000..834ef8c --- /dev/null +++ b/Terminal/Backend/WindowsBackend.cs @@ -0,0 +1,283 @@ +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/Terminal.cs b/Terminal/Terminal.cs index 1eb0c21..27e71e7 100644 --- a/Terminal/Terminal.cs +++ b/Terminal/Terminal.cs @@ -83,9 +83,9 @@ public static bool ListenForKeys {set { /// The name of the window /// /// - public static TerminalWindow CreateWindow(string title) { + public static TerminalWindow CreateBackend(string title) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return new WinTerminalWindow(title); + return new WindowsBackend(title); } else { throw new PlatformNotSupportedException("No window implementation for your platform."); } diff --git a/Terminal/Window/WinTerminalWindow.cs b/Terminal/Window/WinTerminalWindow.cs deleted file mode 100644 index 52bf4d1..0000000 --- a/Terminal/Window/WinTerminalWindow.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System.ComponentModel; -using System.Diagnostics; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text; -using Microsoft.Win32.SafeHandles; - -namespace OxDED.Terminal.Window; - -internal static class Utils { - - internal static Stream GetStream(nint handle) { - SafeFileHandle fileHandle = new(handle, false); - FileStream stream = new(fileHandle, FileAccess.ReadWrite); - return stream; - } -} - -internal static partial class WinAPI { - [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; - } - internal const int STD_OUTPUT_HANDLE = -11; - internal const int STD_INPUT_HANDLE = -10; - internal const int STD_ERROR_HANDLE = -12; - - [LibraryImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool AllocConsole(); - [LibraryImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool FreeConsole(); - [LibraryImport("kernel32.dll")] - internal static partial nint GetConsoleWindow(); - [LibraryImport("kernel32.dll", SetLastError = true)] - internal static partial nint GetStdHandle(int nStdHandle); - [LibraryImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool CloseHandle(nint handle); - [LibraryImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool SetStdHandle(int nStdHandle, nint hConsoleOutput); - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - internal static extern nint CreateFile(string fileName, uint desiredAccess, int shareMode, nint securityAttributes, int creationDisposition, int flagsAndAttributes, nint templateFile); - [DllImport("kernel32.dll", SetLastError = true, BestFitMapping = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool SetConsoleTitle(string title); - [DllImport("kernel32.dll", SetLastError = true, BestFitMapping = true, CharSet = CharSet.Auto)] - internal static extern uint GetConsoleTitle([MarshalAs(UnmanagedType.LPTStr)]out string lpConsoleTitle, uint nSize); - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool ReadConsoleInput(nint hConsoleInput, out INPUT_RECORD lpBuffer, int nLength, out int numEventsRead); -} - -/// -/// A wrapper for on Windows. -/// -public class WinTerminalWindow : TerminalWindow { - private const string ConsoleIn = "CONIN$"; - private const string ConsoleOut = "CONOUT$"; - private const string ConsoleError = ConsoleOut; - - private nint consoleOut; - private nint consoleIn; - private nint consoleErr; - /// - /// Creates a new Windows terminal window. - /// - /// The name of the window. - /// - public WinTerminalWindow(string title) { - nint stdOut = WinAPI.GetStdHandle(WinAPI.STD_OUTPUT_HANDLE); - nint stdIn = WinAPI.GetStdHandle(WinAPI.STD_INPUT_HANDLE); - nint stdErr = WinAPI.GetStdHandle(WinAPI.STD_ERROR_HANDLE); - - if (WinAPI.GetConsoleWindow() == nint.Zero) { - if (!WinAPI.AllocConsole()) { - throw new Win32Exception("Failed to allocate a console: "+Marshal.GetLastWin32Error()); - } - } - - consoleOut = WinAPI.CreateFile(ConsoleOut, 0x80000000 | 0x40000000, 2, nint.Zero, 3, 0, nint.Zero); - consoleIn = WinAPI.CreateFile(ConsoleIn, 0x80000000 | 0x40000000, 1, nint.Zero, 3, 0, nint.Zero); - consoleErr = WinAPI.CreateFile(ConsoleError, 0x80000000 | 0x40000000, 2, nint.Zero, 3, 0, nint.Zero); - - if (!WinAPI.SetStdHandle(WinAPI.STD_OUTPUT_HANDLE, consoleOut)) { - throw new Win32Exception("Failed to set the handle for the console out stream: "+Marshal.GetLastWin32Error()); - } - - if (!WinAPI.SetStdHandle(WinAPI.STD_INPUT_HANDLE, consoleIn)) { - throw new Win32Exception("Failed to set the handle for the console in stream: "+Marshal.GetLastWin32Error()); - } - - if (!WinAPI.SetStdHandle(WinAPI.STD_ERROR_HANDLE, consoleErr)) { - throw new Win32Exception("Failed to set the handle for the console error stream: "+Marshal.GetLastWin32Error()); - } - - if (!WinAPI.CloseHandle(stdOut)) { - throw new Win32Exception("Failed to close the handle of stdOut: "+Marshal.GetLastWin32Error()); - } - if (!WinAPI.CloseHandle(stdIn)) { - throw new Win32Exception("Failed to close the handle of stdIn: "+Marshal.GetLastWin32Error()); - } - if (!WinAPI.CloseHandle(stdErr)) { - throw new Win32Exception("Failed to close the handle of stdErr: "+Marshal.GetLastWin32Error()); - } - - outStream = new StreamWriter(Utils.GetStream(consoleOut), Encoding.UTF8); - inStream = new StreamReader(Utils.GetStream(consoleIn), Encoding.UTF8); - errStream = new StreamWriter(Utils.GetStream(consoleErr), Encoding.UTF8); - - Title = title; - } - private StreamWriter outStream; - /// - public override TextWriter Out { get => outStream;} - private StreamReader inStream; - /// - public override TextReader In { get => inStream;} - private StreamWriter errStream; - /// - public override TextWriter Error { get => errStream;} - - /// - public override int Width => Console.WindowWidth; - - /// - public override int Height => Console.WindowHeight; - /// - public override Encoding InEncoding { get => inStream.CurrentEncoding; set { inStream = new StreamReader(Utils.GetStream(consoleIn), value); } } - /// - public override Encoding OutEncoding { get => outStream.Encoding; set { outStream = new StreamWriter(Utils.GetStream(consoleOut), value); errStream = new StreamWriter(Utils.GetStream(consoleErr), value); } } - /// - /// 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()); - } - } - } - - private bool isCursorHidden = false; - - /// - public override bool HideCursor { - set { - if (value) { - outStream.Write(ANSI.CursorInvisible); - } else { - outStream.Write(ANSI.CursorVisible); - } - isCursorHidden = value; - } get { - return isCursorHidden; - } - } - - /// - /// - public override void Dispose() { - if (IsDisposed) { return; } - - if (!WinAPI.CloseHandle(consoleOut)) { - throw new Win32Exception("Failed to close console out: "+Marshal.GetLastWin32Error()); - } - consoleOut = nint.Zero; - if (!WinAPI.CloseHandle(consoleIn)) { - throw new Win32Exception("Failed to close console in: "+Marshal.GetLastWin32Error()); - } - consoleIn = nint.Zero; - if (!WinAPI.CloseHandle(consoleErr)) { - throw new Win32Exception("Failed to close console err: "+Marshal.GetLastWin32Error()); - } - consoleErr = nint.Zero; - - if (!WinAPI.FreeConsole()) { - throw new Win32Exception("Failed to free the console window: "+Marshal.GetLastWin32Error()); - } - Console.SetError(new StreamWriter(Console.OpenStandardError())); - Console.SetIn(new StreamReader(Console.OpenStandardInput())); - Console.SetOut(new StreamWriter(Console.OpenStandardOutput())); - - IsDisposed = true; - GC.SuppressFinalize(this); - } - - /// - /// - 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()); - } - } - - [Flags] - internal enum ControlKeyState { - RightAltPressed = 0x0001, - LeftAltPressed = 0x0002, - RightCtrlPressed = 0x0004, - LeftCtrlPressed = 0x0008, - ShiftPressed = 0x0010, - NumLockOn = 0x0020, - ScrollLockOn = 0x0040, - CapsLockOn = 0x0080, - EnhancedKey = 0x0100 - } -} \ No newline at end of file diff --git a/Terminal/Window/Window.cs b/Terminal/Window/Window.cs new file mode 100644 index 0000000..08130ec --- /dev/null +++ b/Terminal/Window/Window.cs @@ -0,0 +1,13 @@ +using OxDED.Terminal.Backend; + +namespace OxDED.Terminal.Window; + +/// +/// Represents a terminal window. +/// +public interface IWindow : ITerminalBackend { + /// + /// The title of the terminal window. + /// + public string Title { get; set; } +} \ No newline at end of file From d080b462de476aef065e0975354f73868aff49ec Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Wed, 4 Dec 2024 07:27:21 +0100 Subject: [PATCH 04/27] - --- Terminal/Backend/ConsoleBackend.cs | 45 +++++++++++++++++++++++++++++ Terminal/Backend/TerminalBackend.cs | 6 +++- Terminal/Terminal.cs | 8 +++-- 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 Terminal/Backend/ConsoleBackend.cs diff --git a/Terminal/Backend/ConsoleBackend.cs b/Terminal/Backend/ConsoleBackend.cs new file mode 100644 index 0000000..de80ba0 --- /dev/null +++ b/Terminal/Backend/ConsoleBackend.cs @@ -0,0 +1,45 @@ +using System.Text; + +namespace OxDED.Terminal.Backend; + +/// +/// The terminal back-end implementation made by .NET. +/// +public class ConsoleBackend : TerminalBackend { + /// + public override TextReader StandardInput => Console.In; + /// + public override TextWriter StandardOutput => Console.Out; + /// + public override TextWriter StandardError => Console.Error; + /// + + /// + public override Encoding InputEncoding { get => Console.InputEncoding; set => Console.InputEncoding = value; } + /// + public override Encoding OutputEncoding { get => Console.OutputEncoding; set => Console.OutputEncoding = value; } + + /// + public override (int x, int y) GetCursorPosition() { + return Console.GetCursorPosition(); + } + + /// + public override void WaitForKeyPress() { + Console.ReadKey(true); + } + + /// + protected override void ListenForKeysMethod() { + while (listenForKeys) { + ConsoleKeyInfo key = Console.ReadKey(true); + if (!Console.IsInputRedirected) { + KeyPress(key.Key, key.KeyChar, key.Modifiers.HasFlag(ConsoleModifiers.Alt), key.Modifiers.HasFlag(ConsoleModifiers.Shift), key.Modifiers.HasFlag(ConsoleModifiers.Control)); + } + } + } + /// + public override void Dispose() { + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Terminal/Backend/TerminalBackend.cs b/Terminal/Backend/TerminalBackend.cs index 382592b..365a15e 100644 --- a/Terminal/Backend/TerminalBackend.cs +++ b/Terminal/Backend/TerminalBackend.cs @@ -57,7 +57,7 @@ public abstract class TerminalBackend : ITerminalBackend { public abstract Encoding OutputEncoding { get; set; } /// - public abstract Encoding ErrorEncoding { get; set; } + public virtual Encoding ErrorEncoding { get; set; } /// public virtual (int Width, int Height) Size { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } @@ -73,6 +73,7 @@ public abstract class TerminalBackend : ITerminalBackend { /// The thing to write to the terminal. /// The text decoration to use. public virtual void Write(T? text, Style? style = null) { + if (text==null) return; StandardOutput.Write((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// @@ -82,6 +83,7 @@ public virtual void Write(T? text, Style? style = null) { /// The thing to write to the terminal. /// The text decoration to use. public virtual void WriteLine(T? text, Style? style = null) { + if (text==null) return; StandardOutput.WriteLine((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// @@ -91,6 +93,7 @@ public virtual void WriteLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteErrorLine(T? text, Style? style = null) { + if (text==null) return; StandardError.WriteLine((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// @@ -100,6 +103,7 @@ public virtual void WriteErrorLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteError(T? text, Style? style = null) { + if (text==null) return; StandardError.Write((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// diff --git a/Terminal/Terminal.cs b/Terminal/Terminal.cs index 27e71e7..33849c5 100644 --- a/Terminal/Terminal.cs +++ b/Terminal/Terminal.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using System.Text; +using OxDED.Terminal.Backend; using OxDED.Terminal.Window; namespace OxDED.Terminal; @@ -83,11 +84,12 @@ public static bool ListenForKeys {set { /// The name of the window /// /// - public static TerminalWindow CreateBackend(string title) { + public static TerminalBackend CreateBackend(string title) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return new WindowsBackend(title); + return new WindowsBackend(); } else { - throw new PlatformNotSupportedException("No window implementation for your platform."); + return new ConsoleBackend(); + throw new PlatformNotSupportedException("No implementation for your platform."); } } /// From a303e4d1c5cf527d43e492ef96a7616798a3b85f Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:56:21 +0100 Subject: [PATCH 05/27] Revert "Revert "Revert "quick""" This reverts commit 7e4ed8229360ad86fb33efd2722e685a52661d39. --- Terminal/Backend/WindowsBackend.cs | 283 ------------------ Terminal/Terminal.cs | 4 +- .../{Backend => Window}/ConsoleBackend.cs | 0 .../TerminalWindow.cs} | 129 ++++---- Terminal/Window/WinTerminalWindow.cs | 266 ++++++++++++++++ Terminal/Window/Window.cs | 13 - 6 files changed, 330 insertions(+), 365 deletions(-) delete mode 100644 Terminal/Backend/WindowsBackend.cs rename Terminal/{Backend => Window}/ConsoleBackend.cs (100%) rename Terminal/{Backend/TerminalBackend.cs => Window/TerminalWindow.cs} (71%) create mode 100644 Terminal/Window/WinTerminalWindow.cs delete mode 100644 Terminal/Window/Window.cs 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/Terminal.cs b/Terminal/Terminal.cs index 33849c5..b4a69fb 100644 --- a/Terminal/Terminal.cs +++ b/Terminal/Terminal.cs @@ -84,9 +84,9 @@ public static bool ListenForKeys {set { /// The name of the window /// /// - public static TerminalBackend CreateBackend(string title) { + public static TerminalWindow CreateWindow(string title) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return new WindowsBackend(); + return new WindowsBackend(title); } else { return new ConsoleBackend(); throw new PlatformNotSupportedException("No implementation for your platform."); diff --git a/Terminal/Backend/ConsoleBackend.cs b/Terminal/Window/ConsoleBackend.cs similarity index 100% rename from Terminal/Backend/ConsoleBackend.cs rename to Terminal/Window/ConsoleBackend.cs diff --git a/Terminal/Backend/TerminalBackend.cs b/Terminal/Window/TerminalWindow.cs similarity index 71% rename from Terminal/Backend/TerminalBackend.cs rename to Terminal/Window/TerminalWindow.cs index 365a15e..20522c5 100644 --- a/Terminal/Backend/TerminalBackend.cs +++ b/Terminal/Window/TerminalWindow.cs @@ -1,71 +1,54 @@ using System.Text; -namespace OxDED.Terminal.Backend; +namespace OxDED.Terminal.Window; /// -/// Represents an interface of common methods of a terminal. +/// Represents a terminal window that can be used. /// -public interface ITerminalBackend : IDisposable { +public abstract class TerminalWindow : IDisposable { /// - /// The data stream for reading from the terminal. + /// Sets default values. /// - public TextReader StandardInput { get; } + protected TerminalWindow() { + IsDisposed = true; + } + /// - /// The data stream for writing to the terminal. + /// The title of the Terminal window. /// - public TextWriter StandardOutput { get; } + public abstract string Title { get; set;} /// - /// The data stream for writing errors to the terminal. + /// The out (to terminal) stream. /// - public TextWriter StandardError { get; } + public abstract TextWriter Out {get;} /// - /// The encoding used for the stream (default: UTF-8). + /// The in (from terminal) stream. /// - public Encoding InputEncoding { get; set; } + public abstract TextReader In {get;} /// - /// The encoding used for the stream (default: UTF-8). + /// The error (to terminal) stream. /// - public Encoding OutputEncoding { get; set; } + public abstract TextWriter Error {get;} /// - /// The encoding used for the stream (default: UTF-8). + /// Hides or shows terminal cursor. /// - public Encoding ErrorEncoding { get; set; } + public abstract bool HideCursor {get; set;} /// - /// The width and the height (in characters) of the terminal. + /// The width (in characters) of the terminal. /// - public (int Width, int Height) Size { get; set; } - -} - -/// -/// Represents an interface of common methods of a terminal. -/// -public abstract class TerminalBackend : ITerminalBackend { - /// - public abstract TextReader StandardInput { get; } - - /// - public abstract TextWriter StandardOutput { get; } - - /// - public abstract TextWriter StandardError { get; } - - /// - public abstract Encoding InputEncoding { get; set; } - - /// - public abstract Encoding OutputEncoding { get; set; } - - /// - public virtual Encoding ErrorEncoding { get; set; } - - /// - public virtual (int Width, int Height) Size { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - /// - public abstract void Dispose(); - - + public abstract int Width {get;} + /// + /// The height (in characters) of the terminal. + /// + public abstract int Height {get;} + /// + /// The encoding used for the in stream (default: UTF-8). + /// + public abstract Encoding InEncoding {get; set;} + /// + /// The encoding used for the error and out streams (default: UTF-8). + /// + public abstract Encoding OutEncoding {get; set;} /// /// Writes something () to the terminal, with a style. /// @@ -73,7 +56,6 @@ public abstract class TerminalBackend : ITerminalBackend { /// The thing to write to the terminal. /// The text decoration to use. public virtual void Write(T? text, Style? style = null) { - if (text==null) return; StandardOutput.Write((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// @@ -83,7 +65,6 @@ public virtual void Write(T? text, Style? style = null) { /// The thing to write to the terminal. /// The text decoration to use. public virtual void WriteLine(T? text, Style? style = null) { - if (text==null) return; StandardOutput.WriteLine((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// @@ -93,7 +74,6 @@ public virtual void WriteLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteErrorLine(T? text, Style? style = null) { - if (text==null) return; StandardError.WriteLine((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// @@ -103,7 +83,6 @@ public virtual void WriteErrorLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteError(T? text, Style? style = null) { - if (text==null) return; StandardError.Write((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// @@ -112,11 +91,9 @@ public virtual void WriteError(T? text, Style? style = null) { /// The position. /// public virtual void Goto((int x, int y) pos) { - try { - if (pos.x >= Size.Width || pos.x < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos x is higher than the width or is lower than 0."); } - if (pos.y >= Size.Height || pos.y < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos y is higher than the height or is lower than 0."); } - } catch (NotImplementedException) { } - StandardOutput.Write(ANSI.MoveCursor(pos.x, pos.y)); + if (pos.x >= Width || pos.x < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos x is higher than the width or is lower than 0."); } + if (pos.y >= Height || pos.y < 0) { throw new ArgumentOutOfRangeException(nameof(pos), "pos y is higher than the height or is lower than 0."); } + Out.Write(ANSI.MoveCursor(pos.x, pos.y)); } /// /// Gets the cursor position. @@ -126,7 +103,7 @@ public virtual void Goto((int x, int y) pos) { /// /// Sets the something () at a , with a . /// - /// The type of what to write. + /// /// The thing to set at to the terminal. /// The position to set at. /// The text decoration to use. @@ -138,7 +115,7 @@ public virtual void Set(T? text, (int x, int y) pos, Style? style = null) { /// /// Sets the something in the error stream () at a , with a . /// - /// The type of what to write. + /// /// The thing to set at to the terminal. /// The position to set at. /// The text decoration to use. @@ -151,14 +128,14 @@ public virtual void SetError(T? text, (int x, int y) pos, Style? style = null /// /// The character that has been read (-1 if everything has been read). public virtual int Read() { - return StandardInput.Read(); + return In.Read(); } /// /// Reads a line from the input stream. /// /// The line that has been read (null if everything has been read). public virtual string? ReadLine() { - return StandardInput.ReadLine(); + return In.ReadLine(); } /// /// Waits until a key is pressed. @@ -185,7 +162,7 @@ protected void KeyPress(ConsoleKey key, char keyChar, bool alt, bool shift, bool /// public virtual void Clear() { Goto((0,0)); - StandardOutput.Write(ANSI.EraseScreenFromCursor); + Out.Write(ANSI.EraseScreenFromCursor); } /// /// Clears screen from the position to end of the screen. @@ -193,7 +170,7 @@ public virtual void Clear() { /// The start position. public virtual void ClearFrom((int x, int y) pos) { Goto(pos); - StandardOutput.Write(ANSI.EraseLineFromCursor); + Out.Write(ANSI.EraseLineFromCursor); } /// /// Clears (deletes) a line. @@ -201,7 +178,7 @@ public virtual void ClearFrom((int x, int y) pos) { /// The y-axis of the line. public virtual void ClearLine(int line) { Goto((0, line)); - StandardOutput.Write(ANSI.EraseLine); + Out.Write(ANSI.EraseLine); } /// /// Clears the line from the position to the end of the line. @@ -209,7 +186,7 @@ public virtual void ClearLine(int line) { /// The start position. public virtual void ClearLineFrom((int x, int y) pos) { Goto(pos); - StandardOutput.Write(ANSI.EraseLineFromCursor); + Out.Write(ANSI.EraseLineFromCursor); } /// /// The thread that is running . @@ -233,9 +210,27 @@ public virtual bool ListenForKeys {set { } get { return listenForKeys; }} - /// /// Method in new thread that should call when a key is pressed. /// protected abstract void ListenForKeysMethod(); + + /// + /// If it already is disposed. + /// + public bool IsDisposed {get; protected set;} + /// + public virtual void Dispose() { + if (IsDisposed) { return; } + IsDisposed = true; + + GC.SuppressFinalize(this); + } + + /// + /// Disposes the window. + /// + ~TerminalWindow() { + Dispose(); + } } \ No newline at end of file diff --git a/Terminal/Window/WinTerminalWindow.cs b/Terminal/Window/WinTerminalWindow.cs new file mode 100644 index 0000000..52bf4d1 --- /dev/null +++ b/Terminal/Window/WinTerminalWindow.cs @@ -0,0 +1,266 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +namespace OxDED.Terminal.Window; + +internal static class Utils { + + internal static Stream GetStream(nint handle) { + SafeFileHandle fileHandle = new(handle, false); + FileStream stream = new(fileHandle, FileAccess.ReadWrite); + return stream; + } +} + +internal static partial class WinAPI { + [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; + } + internal const int STD_OUTPUT_HANDLE = -11; + internal const int STD_INPUT_HANDLE = -10; + internal const int STD_ERROR_HANDLE = -12; + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool AllocConsole(); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool FreeConsole(); + [LibraryImport("kernel32.dll")] + internal static partial nint GetConsoleWindow(); + [LibraryImport("kernel32.dll", SetLastError = true)] + internal static partial nint GetStdHandle(int nStdHandle); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool CloseHandle(nint handle); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetStdHandle(int nStdHandle, nint hConsoleOutput); + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern nint CreateFile(string fileName, uint desiredAccess, int shareMode, nint securityAttributes, int creationDisposition, int flagsAndAttributes, nint templateFile); + [DllImport("kernel32.dll", SetLastError = true, BestFitMapping = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SetConsoleTitle(string title); + [DllImport("kernel32.dll", SetLastError = true, BestFitMapping = true, CharSet = CharSet.Auto)] + internal static extern uint GetConsoleTitle([MarshalAs(UnmanagedType.LPTStr)]out string lpConsoleTitle, uint nSize); + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool ReadConsoleInput(nint hConsoleInput, out INPUT_RECORD lpBuffer, int nLength, out int numEventsRead); +} + +/// +/// A wrapper for on Windows. +/// +public class WinTerminalWindow : TerminalWindow { + private const string ConsoleIn = "CONIN$"; + private const string ConsoleOut = "CONOUT$"; + private const string ConsoleError = ConsoleOut; + + private nint consoleOut; + private nint consoleIn; + private nint consoleErr; + /// + /// Creates a new Windows terminal window. + /// + /// The name of the window. + /// + public WinTerminalWindow(string title) { + nint stdOut = WinAPI.GetStdHandle(WinAPI.STD_OUTPUT_HANDLE); + nint stdIn = WinAPI.GetStdHandle(WinAPI.STD_INPUT_HANDLE); + nint stdErr = WinAPI.GetStdHandle(WinAPI.STD_ERROR_HANDLE); + + if (WinAPI.GetConsoleWindow() == nint.Zero) { + if (!WinAPI.AllocConsole()) { + throw new Win32Exception("Failed to allocate a console: "+Marshal.GetLastWin32Error()); + } + } + + consoleOut = WinAPI.CreateFile(ConsoleOut, 0x80000000 | 0x40000000, 2, nint.Zero, 3, 0, nint.Zero); + consoleIn = WinAPI.CreateFile(ConsoleIn, 0x80000000 | 0x40000000, 1, nint.Zero, 3, 0, nint.Zero); + consoleErr = WinAPI.CreateFile(ConsoleError, 0x80000000 | 0x40000000, 2, nint.Zero, 3, 0, nint.Zero); + + if (!WinAPI.SetStdHandle(WinAPI.STD_OUTPUT_HANDLE, consoleOut)) { + throw new Win32Exception("Failed to set the handle for the console out stream: "+Marshal.GetLastWin32Error()); + } + + if (!WinAPI.SetStdHandle(WinAPI.STD_INPUT_HANDLE, consoleIn)) { + throw new Win32Exception("Failed to set the handle for the console in stream: "+Marshal.GetLastWin32Error()); + } + + if (!WinAPI.SetStdHandle(WinAPI.STD_ERROR_HANDLE, consoleErr)) { + throw new Win32Exception("Failed to set the handle for the console error stream: "+Marshal.GetLastWin32Error()); + } + + if (!WinAPI.CloseHandle(stdOut)) { + throw new Win32Exception("Failed to close the handle of stdOut: "+Marshal.GetLastWin32Error()); + } + if (!WinAPI.CloseHandle(stdIn)) { + throw new Win32Exception("Failed to close the handle of stdIn: "+Marshal.GetLastWin32Error()); + } + if (!WinAPI.CloseHandle(stdErr)) { + throw new Win32Exception("Failed to close the handle of stdErr: "+Marshal.GetLastWin32Error()); + } + + outStream = new StreamWriter(Utils.GetStream(consoleOut), Encoding.UTF8); + inStream = new StreamReader(Utils.GetStream(consoleIn), Encoding.UTF8); + errStream = new StreamWriter(Utils.GetStream(consoleErr), Encoding.UTF8); + + Title = title; + } + private StreamWriter outStream; + /// + public override TextWriter Out { get => outStream;} + private StreamReader inStream; + /// + public override TextReader In { get => inStream;} + private StreamWriter errStream; + /// + public override TextWriter Error { get => errStream;} + + /// + public override int Width => Console.WindowWidth; + + /// + public override int Height => Console.WindowHeight; + /// + public override Encoding InEncoding { get => inStream.CurrentEncoding; set { inStream = new StreamReader(Utils.GetStream(consoleIn), value); } } + /// + public override Encoding OutEncoding { get => outStream.Encoding; set { outStream = new StreamWriter(Utils.GetStream(consoleOut), value); errStream = new StreamWriter(Utils.GetStream(consoleErr), value); } } + /// + /// 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()); + } + } + } + + private bool isCursorHidden = false; + + /// + public override bool HideCursor { + set { + if (value) { + outStream.Write(ANSI.CursorInvisible); + } else { + outStream.Write(ANSI.CursorVisible); + } + isCursorHidden = value; + } get { + return isCursorHidden; + } + } + + /// + /// + public override void Dispose() { + if (IsDisposed) { return; } + + if (!WinAPI.CloseHandle(consoleOut)) { + throw new Win32Exception("Failed to close console out: "+Marshal.GetLastWin32Error()); + } + consoleOut = nint.Zero; + if (!WinAPI.CloseHandle(consoleIn)) { + throw new Win32Exception("Failed to close console in: "+Marshal.GetLastWin32Error()); + } + consoleIn = nint.Zero; + if (!WinAPI.CloseHandle(consoleErr)) { + throw new Win32Exception("Failed to close console err: "+Marshal.GetLastWin32Error()); + } + consoleErr = nint.Zero; + + if (!WinAPI.FreeConsole()) { + throw new Win32Exception("Failed to free the console window: "+Marshal.GetLastWin32Error()); + } + Console.SetError(new StreamWriter(Console.OpenStandardError())); + Console.SetIn(new StreamReader(Console.OpenStandardInput())); + Console.SetOut(new StreamWriter(Console.OpenStandardOutput())); + + IsDisposed = true; + GC.SuppressFinalize(this); + } + + /// + /// + 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()); + } + } + + [Flags] + internal enum ControlKeyState { + RightAltPressed = 0x0001, + LeftAltPressed = 0x0002, + RightCtrlPressed = 0x0004, + LeftCtrlPressed = 0x0008, + ShiftPressed = 0x0010, + NumLockOn = 0x0020, + ScrollLockOn = 0x0040, + CapsLockOn = 0x0080, + EnhancedKey = 0x0100 + } +} \ No newline at end of file diff --git a/Terminal/Window/Window.cs b/Terminal/Window/Window.cs deleted file mode 100644 index 08130ec..0000000 --- a/Terminal/Window/Window.cs +++ /dev/null @@ -1,13 +0,0 @@ -using OxDED.Terminal.Backend; - -namespace OxDED.Terminal.Window; - -/// -/// Represents a terminal window. -/// -public interface IWindow : ITerminalBackend { - /// - /// The title of the terminal window. - /// - public string Title { get; set; } -} \ No newline at end of file From c8a8a945b10decbd13bb5bbc5ee1b43e9ff43aa8 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:03:20 +0100 Subject: [PATCH 06/27] update main branch --- Terminal/Assertion/Assert.cs | 3 +++ Terminal/Terminal.cs | 4 +-- Terminal/Window/ConsoleBackend.cs | 45 ------------------------------- Terminal/Window/TerminalWindow.cs | 8 +++--- 4 files changed, 8 insertions(+), 52 deletions(-) create mode 100644 Terminal/Assertion/Assert.cs delete mode 100644 Terminal/Window/ConsoleBackend.cs diff --git a/Terminal/Assertion/Assert.cs b/Terminal/Assertion/Assert.cs new file mode 100644 index 0000000..2adc68c --- /dev/null +++ b/Terminal/Assertion/Assert.cs @@ -0,0 +1,3 @@ +namespace OxDED.Terminal.Assertion; + +public static class Assert {} \ No newline at end of file diff --git a/Terminal/Terminal.cs b/Terminal/Terminal.cs index b4a69fb..abe0a8b 100644 --- a/Terminal/Terminal.cs +++ b/Terminal/Terminal.cs @@ -1,6 +1,5 @@ using System.Runtime.InteropServices; using System.Text; -using OxDED.Terminal.Backend; using OxDED.Terminal.Window; namespace OxDED.Terminal; @@ -86,9 +85,8 @@ public static bool ListenForKeys {set { /// public static TerminalWindow CreateWindow(string title) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return new WindowsBackend(title); + return new WinTerminalWindow(title); } else { - return new ConsoleBackend(); throw new PlatformNotSupportedException("No implementation for your platform."); } } diff --git a/Terminal/Window/ConsoleBackend.cs b/Terminal/Window/ConsoleBackend.cs deleted file mode 100644 index de80ba0..0000000 --- a/Terminal/Window/ConsoleBackend.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text; - -namespace OxDED.Terminal.Backend; - -/// -/// The terminal back-end implementation made by .NET. -/// -public class ConsoleBackend : TerminalBackend { - /// - public override TextReader StandardInput => Console.In; - /// - public override TextWriter StandardOutput => Console.Out; - /// - public override TextWriter StandardError => Console.Error; - /// - - /// - public override Encoding InputEncoding { get => Console.InputEncoding; set => Console.InputEncoding = value; } - /// - public override Encoding OutputEncoding { get => Console.OutputEncoding; set => Console.OutputEncoding = value; } - - /// - public override (int x, int y) GetCursorPosition() { - return Console.GetCursorPosition(); - } - - /// - public override void WaitForKeyPress() { - Console.ReadKey(true); - } - - /// - protected override void ListenForKeysMethod() { - while (listenForKeys) { - ConsoleKeyInfo key = Console.ReadKey(true); - if (!Console.IsInputRedirected) { - KeyPress(key.Key, key.KeyChar, key.Modifiers.HasFlag(ConsoleModifiers.Alt), key.Modifiers.HasFlag(ConsoleModifiers.Shift), key.Modifiers.HasFlag(ConsoleModifiers.Control)); - } - } - } - /// - public override void Dispose() { - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/Terminal/Window/TerminalWindow.cs b/Terminal/Window/TerminalWindow.cs index 20522c5..eb8c257 100644 --- a/Terminal/Window/TerminalWindow.cs +++ b/Terminal/Window/TerminalWindow.cs @@ -56,7 +56,7 @@ protected TerminalWindow() { /// The thing to write to the terminal. /// The text decoration to use. public virtual void Write(T? text, Style? style = null) { - StandardOutput.Write((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + Out.Write((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the terminal, with a style. @@ -65,7 +65,7 @@ public virtual void Write(T? text, Style? style = null) { /// The thing to write to the terminal. /// The text decoration to use. public virtual void WriteLine(T? text, Style? style = null) { - StandardOutput.WriteLine((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + Out.WriteLine((style ?? new Style()).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the error stream, with a style. @@ -74,7 +74,7 @@ public virtual void WriteLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteErrorLine(T? text, Style? style = null) { - StandardError.WriteLine((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + Error.WriteLine((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Writes something () to the error stream, with a style. @@ -83,7 +83,7 @@ public virtual void WriteErrorLine(T? text, Style? style = null) { /// The text to write to the error output stream. /// The style to use (default: with red foreground). public virtual void WriteError(T? text, Style? style = null) { - StandardError.Write((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); + Error.Write((style ?? new Style {ForegroundColor = Colors.Red}).ToANSI()+text?.ToString()+ANSI.Styles.ResetAll); } /// /// Sets the cursor to that position. From 2943a7d4cf7555b54c11ef3c84865c142af010e4 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:48:16 +0100 Subject: [PATCH 07/27] A --- Terminal/Assertion/Assertion.cs | 1 + Terminal/Assertion/AssertionResult.cs | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 Terminal/Assertion/Assertion.cs create mode 100644 Terminal/Assertion/AssertionResult.cs diff --git a/Terminal/Assertion/Assertion.cs b/Terminal/Assertion/Assertion.cs new file mode 100644 index 0000000..baa8df7 --- /dev/null +++ b/Terminal/Assertion/Assertion.cs @@ -0,0 +1 @@ +namespace OxDED.Terminal.Assertion; \ No newline at end of file diff --git a/Terminal/Assertion/AssertionResult.cs b/Terminal/Assertion/AssertionResult.cs new file mode 100644 index 0000000..a2bf800 --- /dev/null +++ b/Terminal/Assertion/AssertionResult.cs @@ -0,0 +1,5 @@ +namespace OxDED.Terminal.Assertion; + +public enum AssertionResult { + +} \ No newline at end of file From a7b0529458095fb8b586b4f539289c0bdd4ee770 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:36:43 +0100 Subject: [PATCH 08/27] Create Assertion 4.1.0 --- Terminal.sln | 7 ++ Terminal/Assertion/Assert.cs | 48 +++++++++++- Terminal/Assertion/AssertException.cs | 23 ++++++ Terminal/Assertion/Asserter.cs | 75 ++++++++++++++++++ Terminal/Assertion/Assertion.cs | 68 +++++++++++++++- Terminal/Assertion/AssertionResult.cs | 6 +- Terminal/Assertion/ExceptionAssertion.cs | 58 ++++++++++++++ Terminal/Assertion/ReferenceAssertion.cs | 8 ++ Terminal/Assertion/TypeAssertion.cs | 31 ++++++++ Terminal/Assertion/ValueAssertion.cs | 36 +++++++++ Terminal/Logging/Logger.cs | 91 +++++++++++----------- Terminal/Logging/Loggers.cs | 8 +- Terminal/Logging/SubLogger.cs | 21 +++++ Terminal/Logging/Targets/FileTarget.cs | 4 +- Terminal/Logging/Targets/TerminalTarget.cs | 2 +- examples/Assertion/Assertion.csproj | 14 ++++ examples/Assertion/Program.cs | 76 ++++++++++++++++++ examples/Colors/Program.cs | 7 ++ examples/Keypresses/Program.cs | 8 +- examples/Logging/Program.cs | 27 ++++--- 20 files changed, 550 insertions(+), 68 deletions(-) create mode 100644 Terminal/Assertion/AssertException.cs create mode 100644 Terminal/Assertion/Asserter.cs create mode 100644 Terminal/Assertion/ExceptionAssertion.cs create mode 100644 Terminal/Assertion/ReferenceAssertion.cs create mode 100644 Terminal/Assertion/TypeAssertion.cs create mode 100644 Terminal/Assertion/ValueAssertion.cs create mode 100644 Terminal/Logging/SubLogger.cs create mode 100644 examples/Assertion/Assertion.csproj create mode 100644 examples/Assertion/Program.cs 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/Assertion/Assert.cs b/Terminal/Assertion/Assert.cs index 2adc68c..80a4a0b 100644 --- a/Terminal/Assertion/Assert.cs +++ b/Terminal/Assertion/Assert.cs @@ -1,3 +1,49 @@ namespace OxDED.Terminal.Assertion; -public static class Assert {} \ No newline at end of file +public static class Assert { + public static ValueAssertion Is(TA a, TB b) { + return ValueAssertion.Create(a, b); + } + public static ValueAssertion Is(object? a, object? b) { + return ValueAssertion.Create(a, b); + } + public static ValueAssertion IsTrue(bool boolean) { + return new ValueAssertion(boolean, boolean, true); + } + public static ValueAssertion IsFalse(bool boolean) { + return new ValueAssertion(!boolean, boolean, false); + } + + public static ReferenceAssertion IsNull(T? obj) { + return ReferenceAssertion.Create(obj, shouldBeNull:true); + } + public static ReferenceAssertion IsNotNull(T? obj) { + return ReferenceAssertion.Create(obj, shouldBeNull:false); + } + + public static TypeAssertion IsType(TValue obj) { + return TypeAssertion.Create(obj); + } + public static TypeAssertion IsType(TValue obj, Type type) { + return TypeAssertion.Create(obj, type); + } + public static TypeAssertion IsType(object? obj) { + return TypeAssertion.Create(obj); + } + public static TypeAssertion IsType(object? obj, Type type) { + return TypeAssertion.Create(obj, type); + } + + public static ExceptionAssertion Throws(Func emitter) where TException : Exception { + return ExceptionAssertion.Create(emitter); + } + public static ExceptionAssertion Throws(Action emitter) where TException : Exception { + return ExceptionAssertion.Create(emitter); + } + public static ExceptionAssertion Throws(Func emitter) { + return ExceptionAssertion.Create(emitter); + } + 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..f0345d8 --- /dev/null +++ b/Terminal/Assertion/Asserter.cs @@ -0,0 +1,75 @@ +using OxDED.Terminal.Logging; + +namespace OxDED.Terminal.Assertion; + +public class Asserter { + public readonly T value; + public Asserter(T value) { + this.value = value; + } + + public T? GetValue() { + return value; + } + + public ValueAssertion Is(object? b) { + return Assert.Is(value, b); + } + public ValueAssertion Is(TMatch? b) { + return Assert.Is(value, b); + } + + public ReferenceAssertion IsNull() { + return Assert.IsNull(value); + } + public ReferenceAssertion IsNotNull() { + return Assert.IsNotNull(value); + } + + public TypeAssertion IsType() { + return Assert.IsType(value); + } + public TypeAssertion IsType(Type type) { + return Assert.IsType(value, type); + } + + public Asserter As() { + try { + return new Asserter((TNew?)(object?)value); + } catch (InvalidCastException) { + return new Asserter(default); + } + } + + public Asserter Do(Func function) { + return new Asserter(function(value)); + } + + public Asserter Log(Logger logger, Severity severity = Severity.Debug) { + logger.Log(severity, value); + return this; + } +} + +public static class AsserterExtension { + public static ValueAssertion IsTrue(this Asserter asserter) { + return Assert.IsTrue(asserter.value); + } + public static ValueAssertion IsFalse(this Asserter asserter) { + return Assert.IsFalse(asserter.value); + } + + public static ExceptionAssertion Throws(this Asserter> asserter) where TException : Exception { + return Assert.Throws(asserter.value); + } + public static ExceptionAssertion Throws(this Asserter asserter) where TException : Exception { + return Assert.Throws(asserter.value); + } + public static ExceptionAssertion Throws(this Asserter> asserter) { + return Assert.Throws(asserter.value); + } + 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 index baa8df7..7ca1dcf 100644 --- a/Terminal/Assertion/Assertion.cs +++ b/Terminal/Assertion/Assertion.cs @@ -1 +1,67 @@ -namespace OxDED.Terminal.Assertion; \ No newline at end of file +using OxDED.Terminal.Logging; + +namespace OxDED.Terminal.Assertion; + +public class Assertion { + public readonly AssertionResult result; + public Assertion(AssertionResult result) { + this.result = result; + } + public AssertionResult GetResult() { + return result; + } + public Asserter ResultAsserter() { + return new Asserter(result); + } + public bool IsSuccess() { + return result == AssertionResult.Success; + } + public bool IsFailure() { + return result != AssertionResult.Success; + } + + public Assertion OnSuccess(Action callback) { + if (IsSuccess()) callback.Invoke(this); + return this; + } + public Assertion OnFailure(Action callback) { + if (IsFailure()) callback.Invoke(this); + return this; + } + public Assertion Log(Logger logger, string? message = null, bool fatal = false) { + logger.LogTrace($"Assertion done: {ToString()}"); + logger.Log(fatal ? Severity.Fatal : Severity.Error, message ?? $"Assertion failed with result: {result}"); + return this; + } + public Assertion Throw(Exception? exception = null) { + return OnFailure((Assertion failedAssertion) => { + throw exception ?? new AssertException("Assertion failed", this); + }); + } + + public TNew? As() where TNew : Assertion { + return this as TNew; + } + + public override string ToString() { + return $"{GetType().Name}: result = {result}"; + } +} + +public class Assertion : Assertion { + public readonly T value; + public Assertion(AssertionResult result, T value) : base(result) { + this.value = value; + } + + public T GetValue() { + return value; + } + 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 index a2bf800..bf992bb 100644 --- a/Terminal/Assertion/AssertionResult.cs +++ b/Terminal/Assertion/AssertionResult.cs @@ -1,5 +1,9 @@ namespace OxDED.Terminal.Assertion; public enum AssertionResult { - + Success, + UnexpectedValue, + UnexpectedReference, + UnexpectedType, + 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..2efab40 --- /dev/null +++ b/Terminal/Assertion/ExceptionAssertion.cs @@ -0,0 +1,58 @@ +using OxDED.Terminal.Logging; + +namespace OxDED.Terminal.Assertion; + +public class ExceptionAssertion : Assertion where TException : Exception { + public static ExceptionAssertion Create(Action emitter) { + try { + emitter.Invoke(); + } catch (TException e) { + return new ExceptionAssertion(false, e); + } + return new ExceptionAssertion(true); + } + + public readonly TException? exception; + public ExceptionAssertion(bool success, TException? exception = null) : base(success ? AssertionResult.Success : AssertionResult.ExceptionCaught) { + this.exception = exception; + } + public TException? GetException() { + return exception; + } + + public Asserter ExceptionAsserter() { + return new Asserter(exception); + } + + public override string ToString() { + return $"{GetType().Name}: result = {result}, exception = {exception}"; + } +} + +public class ExceptionAssertion : Assertion where TException : Exception { + 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); + } + + public readonly TException? exception; + public ExceptionAssertion(bool success, T? value = default, TException? exception = null) : base(success ? AssertionResult.Success : AssertionResult.ExceptionCaught, value) { + this.exception = exception; + } + public TException? GetException() { + return exception; + } + + 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..af638f1 --- /dev/null +++ b/Terminal/Assertion/ReferenceAssertion.cs @@ -0,0 +1,8 @@ +namespace OxDED.Terminal.Assertion; + +public class ReferenceAssertion : Assertion { + public static ReferenceAssertion Create(T? obj, bool shouldBeNull = true) { + return new ReferenceAssertion(shouldBeNull ? (obj is null) : (obj is not null), obj); + } + public ReferenceAssertion(bool success, T? value = default) : base(success ? AssertionResult.Success : AssertionResult.UnexpectedReference, value) { } +} \ No newline at end of file diff --git a/Terminal/Assertion/TypeAssertion.cs b/Terminal/Assertion/TypeAssertion.cs new file mode 100644 index 0000000..9cceb44 --- /dev/null +++ b/Terminal/Assertion/TypeAssertion.cs @@ -0,0 +1,31 @@ +namespace OxDED.Terminal.Assertion; + +public class TypeAssertion : Assertion { + public static TypeAssertion Create(T obj, Type type) { + return new TypeAssertion(obj?.GetType() == type, type, obj); + } + public readonly Type matchedType; + public TypeAssertion(bool success, Type matchedType, T value) : base(success ? AssertionResult.Success : AssertionResult.UnexpectedType, value) { + + this.matchedType = matchedType; + } + + public Type GetMatchedType() { + return matchedType; + } + + public Asserter TypeAsserter() { + return new Asserter(matchedType); + } + + public override string ToString() { + return $"{GetType().Name}: result = {result}, value = {value}, matchedType = {matchedType}"; + } +} + +public class TypeAssertion : TypeAssertion { + public static TypeAssertion Create(TValue obj) { + return new TypeAssertion(obj is TMatch, obj); + } + 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..3e9d503 --- /dev/null +++ b/Terminal/Assertion/ValueAssertion.cs @@ -0,0 +1,36 @@ +using System.Runtime.CompilerServices; + +namespace OxDED.Terminal.Assertion; + +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); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ValueAssertion Create(T a, TSecond b) { + return new ValueAssertion(P_Is(a, b), a, b); + } + public readonly TSecond secondValue; + public ValueAssertion(bool success, T value, TSecond secondValue) : base(success ? AssertionResult.Success : AssertionResult.UnexpectedValue, value) { + this.secondValue = secondValue; + } + + public TSecond? GetSecondValue() { + return secondValue; + } + + 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/Logging/Logger.cs b/Terminal/Logging/Logger.cs index 9898bca..34300fd 100644 --- a/Terminal/Logging/Logger.cs +++ b/Terminal/Logging/Logger.cs @@ -9,19 +9,16 @@ public class Logger : IDisposable, IEquatable { /// /// The ID of this logger. /// - 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,7 +28,7 @@ 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, key MUST BE typeof(...Target) or ITarget.GetType() only when using. /// public Dictionary Targets; /// @@ -43,32 +40,34 @@ 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, Dictionary? 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 => new KeyValuePair(target.Key, (target.Value, true))).ToDictionary() : new Dictionary { { typeof(TerminalTarget), (new TerminalTarget(), true) }, { typeof(FileTarget), (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; } /// @@ -173,18 +172,18 @@ public bool RemoveTarget(Type type) { /// /// 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,10 +193,10 @@ 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; } /// @@ -277,13 +276,15 @@ public void LogTrace(T? text) { /// 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, Dictionary? targets = null) { + SubLogger subLogger = new(this, id, name, (ID != null && shouldRegister) ? ID+'.'+id : null, severity, targets); + subLoggers.Add(id, subLogger); return subLogger; } @@ -296,14 +297,13 @@ public void Dispose() { foreach (KeyValuePair target in Targets) { target.Value.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 +332,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 +346,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/SubLogger.cs b/Terminal/Logging/SubLogger.cs new file mode 100644 index 0000000..d6705bf --- /dev/null +++ b/Terminal/Logging/SubLogger.cs @@ -0,0 +1,21 @@ +namespace OxDED.Terminal.Logging; + +/// +/// Represents a child of another logger. +/// +public 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, Dictionary? 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..c7a139d 100644 --- a/Terminal/Logging/Targets/FileTarget.cs +++ b/Terminal/Logging/Targets/FileTarget.cs @@ -37,7 +37,7 @@ public FileTarget(string path, string? format = null) { if (format != null) { Format = format; } - FileOut = new StreamWriter(File.OpenWrite(path)); + FileOut = new StreamWriter(File.Open(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)); } /// public void Dispose() { @@ -49,7 +49,7 @@ private string GetName(Logger logger) { if (!logger.IsSubLogger) { return logger.Name; } else { - return string.Format(NameFormat, GetName(logger.ParentLogger!), logger.Name); + return string.Format(NameFormat, GetName((logger as SubLogger)!.ParentLogger), logger.Name); } } diff --git a/Terminal/Logging/Targets/TerminalTarget.cs b/Terminal/Logging/Targets/TerminalTarget.cs index 372e087..4617abb 100644 --- a/Terminal/Logging/Targets/TerminalTarget.cs +++ b/Terminal/Logging/Targets/TerminalTarget.cs @@ -56,7 +56,7 @@ private string GetName(Logger logger) { if (!logger.IsSubLogger) { return logger.Name; } else { - return string.Format(NameFormat, GetName(logger.ParentLogger!), logger.Name); + return string.Format(NameFormat, GetName((logger as SubLogger)!.ParentLogger), logger.Name); } } diff --git a/examples/Assertion/Assertion.csproj b/examples/Assertion/Assertion.csproj new file mode 100644 index 0000000..326da3f --- /dev/null +++ b/examples/Assertion/Assertion.csproj @@ -0,0 +1,14 @@ + + + + + + + + Exe + net8.0 + enable + enable + + + diff --git a/examples/Assertion/Program.cs b/examples/Assertion/Program.cs new file mode 100644 index 0000000..a7028e5 --- /dev/null +++ b/examples/Assertion/Program.cs @@ -0,0 +1,76 @@ +// Using the Assertion namespace. +using OxDED.Terminal.Assertion; +using OxDED.Terminal.Logging; + +class Program { + public static void Main(string[] args) { + Logger logger = new(name:"Assertion", severity:Severity.Trace); + + /// Assert + // Assert is used to begin an assertion like this: + Assert.Is(4, 2).Log(logger); + // Here an assertion is created which checks if 4 == 2. Spoiler alert: it doesn't. + // NOTE: Log(...) is used to log an assertion to the terminal. + + /// Assertions + // There are 4 types of assertions: + // - Value assertion : Checks if values match (like 4, 2). + // - Type assertion : checks if types match. + // - Exception assertion : Checks if an unhandled exception occured. + // - Reference assertion : Can check if an object is null. + // Here is an exception assertion used: + Assert.Throws(()=>throw new Exception("Test")).Log(logger); + + // Assertions contain methods to check or handle the result: + Assert.Is(4, 2).Log(logger) + .OnFailure((Assertion failedAssertion) => { // Gets executed when the result is not Success. + logger.LogFatal("Panic!"); + }) + .IsSuccess(); // True if the result is Success. + + /// Asserters + // Asserters basically do a assert of a pre-defined value: + _ = + new Asserter(42).IsType().IsSuccess() // Asserter equivalent + == + Assert.IsType(42).IsSuccess(); // Assert equivalent + + // Different assertions have different asserters. + // Like the exception assertion has (ExceptionAssertion has another one): + ExceptionAssertion.Create(()=>{}) + .ExceptionAsserter(); + // And the value asserter has two: + ValueAssertion.Create(1, 2) + .Asserter(); // For 1. + ValueAssertion.Create(1, 2) + .SecondAsserter(); // For 2. + // And others have other asserters. + // NOTE: All assertions have ResultAsserter(), that asserts the result. + + // For all asserters in a assertion is a Get...() variant: + ValueAssertion.Create(1, 2) + .ResultAsserter(); // Asserter + ValueAssertion.Create(1, 2) + .GetResult(); // Getter + + // Asserters can also change types: + // This assertion first checks if 42 is an int then it asserts the type (int). + // After that it converts the asserter into a string asserter. + Assert.IsType(42).TypeAsserter().Do((Type type) => type.Name).Log(logger); + + /// Put together + // Assert that "1" is 1, when it fails, + // convert the failed assertion back, log the first value: "1", + // when it fails, + // convert the failed assertion back, log the second value: 1, + Assert.Is("1", 1).OnFailure((Assertion failedAssertion) => + // NOTE: We already know that failedAssertion is ValueAssertion. + failedAssertion.As>()!.Asserter().Log(logger) + ).OnFailure((Assertion failedAssertion) => + failedAssertion.As>()!.SecondAsserter().Log(logger) + ); + + Assert.IsTrue(false).Throw(); + // bye, bye. + } +} \ No newline at end of file diff --git a/examples/Colors/Program.cs b/examples/Colors/Program.cs index 594fade..9ce71f6 100644 --- a/examples/Colors/Program.cs +++ b/examples/Colors/Program.cs @@ -13,6 +13,13 @@ public static void Main(string[] args) { byte[] randomBytes = new byte[1]; Random.Shared.NextBytes(randomBytes); + // Printing out all the terminal-defined colors + foreach (Colors color in Enum.GetValues()) { + Terminal.Write(color.ToString()+' ', new Style() { ForegroundColor = new Color(color)}); + } + + Terminal.WriteLine(); + // * In string formatting. Terminal.WriteLine( @$"{Color.Green.ToForegroundANSI()}Green{defaultColor}{"\t\t"}{ANSI.Styles.Blink}Blinking{ANSI.Styles.ResetBlink} diff --git a/examples/Keypresses/Program.cs b/examples/Keypresses/Program.cs index 1b717ba..bec9a63 100644 --- a/examples/Keypresses/Program.cs +++ b/examples/Keypresses/Program.cs @@ -3,22 +3,22 @@ class Program { public static sbyte countdown = 5; - public static void Main(string[] args) { - // Will listen for keys.e + public static void Main() { + // Will listen for keys. Terminal.ListenForKeys = true; // Adds an event. Terminal.OnKeyPress += OnKey; } public static void OnKey(ConsoleKey key, char keyChar, bool alt, bool shift, bool control) { // Says what key, and countdown - Terminal.WriteLine(key.ToString() + " " + countdown.ToString()); + Terminal.WriteLine(key.ToString() + " " + countdown.ToString(), new Style() { Bold = control, Italic = alt, Underline = shift}); // lowers the countdown and checks if it is at zero. if ((--countdown)==0) { // Stops listening for keys. Terminal.ListenForKeys = false; - Terminal.WriteLine("Waiting for input...", new Style{Bold = true}); + Terminal.WriteLine("Waiting for input...", new Style{ Bold = true }); //Waits for key press. Terminal.WaitForKeyPress(); // End of program diff --git a/examples/Logging/Program.cs b/examples/Logging/Program.cs index 657bd2e..fdc46d3 100644 --- a/examples/Logging/Program.cs +++ b/examples/Logging/Program.cs @@ -5,7 +5,7 @@ class Program { public const string LoggerID = "me.0xDED.Terminal.examples"; - // Creates a logger with an ID and name and with severity Trace (lowest). + // Creates a logger with an ID (optional) and name and with severity Trace (lowest). public static Logger logger = new(LoggerID, "Logging Example", Severity.Trace); public static void Main() { logger.LogMessage("Hello, world!"); @@ -13,6 +13,7 @@ public static void Main() { logger.LogWarning("This is a warning."); logger.LogDebug("Let's debug that."); logger.LogTrace("It came from there."); + // Gets logger with logger id. Loggers.Get(LoggerID)?.LogError("It happend again."); Loggers.Get(LoggerID)?.LogFatal("Bye"); @@ -21,15 +22,24 @@ public static void Main() { Terminal.WriteLine("Sub loggers"); // Sub loggers - Logger sublogger1 = logger.CreateSubLogger("Sub 1", "sub1", Severity.Trace); + SubLogger sublogger1 = logger.CreateSubLogger("Sub 1", "sub1", severity:Severity.Trace); sublogger1.LogInfo("This is the first sub logger of "+logger.Name); - Logger sublogger2 = logger.CreateSubLogger("Sub 2", "sub2", Severity.Trace); - sublogger2.LogMessage("This is the second sub logger of "+sublogger2.ParentLogger!.Name); // Gets parent name from ParentLogger + SubLogger sublogger2 = logger.CreateSubLogger("Sub 2", "sub2", severity:Severity.Trace); + sublogger2.LogMessage("This is the second sub logger of "+sublogger2.ParentLogger.Name); // Gets parent name from ParentLogger + + SubLogger subsublogger = sublogger2.CreateSubLogger("sub-sub", "sub", severity:Severity.Trace); - Logger subsublogger = sublogger2.CreateSubLogger("sub-sub", "sub", Severity.Trace); // sublogger2.SubLoggers[sublogger2.SubLoggers.Keys.ToArray()[0]] - sublogger2.SubLoggers[subsublogger.ID].LogTrace("This is the sub logger of "+sublogger2.Name); // Gets sublogger from parent + // NOTE: The difference between child ID and ID is that the child ID is used by the parent (last bit) and the ID is used by the Loggers Register (full ID). + // - child ID : "sub-sub" + // - ID : "Logging Example.Sub 2.sub-sub" + sublogger2.SubLoggers[subsublogger.childID].LogTrace("This is the sub logger of "+sublogger2.Name); // Gets sublogger from parent + + // Tree of loggers (names of variables): + // logger + sublogger1 + // | + // + sublogger2 - subsublogger Terminal.WriteLine(); @@ -41,11 +51,6 @@ public static void Main() { sublogger2.GetTarget().Format = "<{1}>: {3}: ({2}) : {5}{4}"+ANSI.Styles.ResetAll; sublogger2.LogDebug("Wow cool new format!"); - // Tree of loggers: - // logger + sublogger1 - // | - // + sublogger2 - subsublogger - // Don't forget to dispose the logger (does happen automatically). logger.Dispose(); } From ac77225b8646528627831dc6d319f53ebfac57d1 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:26:03 +0100 Subject: [PATCH 09/27] Add docs --- Terminal/Assertion/Assert.cs | 109 ++++++++++++++++++++--- Terminal/Assertion/Asserter.cs | 104 ++++++++++++++++++++- Terminal/Assertion/Assertion.cs | 84 ++++++++++++++++- Terminal/Assertion/AssertionResult.cs | 20 ++++- Terminal/Assertion/ExceptionAssertion.cs | 62 ++++++++++++- Terminal/Assertion/ReferenceAssertion.cs | 46 +++++++++- Terminal/Assertion/TypeAssertion.cs | 46 ++++++++++ Terminal/Assertion/ValueAssertion.cs | 31 +++++++ examples/Assertion/Program.cs | 5 ++ 9 files changed, 488 insertions(+), 19 deletions(-) diff --git a/Terminal/Assertion/Assert.cs b/Terminal/Assertion/Assert.cs index 80a4a0b..6831179 100644 --- a/Terminal/Assertion/Assert.cs +++ b/Terminal/Assertion/Assert.cs @@ -1,48 +1,137 @@ 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); } - public static ReferenceAssertion IsNull(T? obj) { - return ReferenceAssertion.Create(obj, shouldBeNull:true); + /// + /// 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); } - public static ReferenceAssertion IsNotNull(T? obj) { - return ReferenceAssertion.Create(obj, shouldBeNull:false); + /// + /// 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); } - public static TypeAssertion IsType(TValue obj) { - return TypeAssertion.Create(obj); + /// + /// 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); } - public static TypeAssertion IsType(object? obj) { - return TypeAssertion.Create(obj); + /// + /// 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); } - public static TypeAssertion IsType(object? obj, Type type) { - return TypeAssertion.Create(obj, type); + /// + /// 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); } diff --git a/Terminal/Assertion/Asserter.cs b/Terminal/Assertion/Asserter.cs index f0345d8..1ee51f7 100644 --- a/Terminal/Assertion/Asserter.cs +++ b/Terminal/Assertion/Asserter.cs @@ -2,37 +2,86 @@ 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); } - public ValueAssertion Is(TMatch? 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); @@ -41,33 +90,86 @@ public TypeAssertion IsType(Type type) { } } + /// + /// 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); diff --git a/Terminal/Assertion/Assertion.cs b/Terminal/Assertion/Assertion.cs index 7ca1dcf..e9ef81b 100644 --- a/Terminal/Assertion/Assertion.cs +++ b/Terminal/Assertion/Assertion.cs @@ -2,65 +2,145 @@ 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; } - public Assertion Log(Logger logger, string? message = null, bool fatal = false) { + + /// + /// 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()}"); - logger.Log(fatal ? Severity.Fatal : Severity.Error, message ?? $"Assertion failed with result: {result}"); + logger.Log(severity, $"Assertion failed with result: {result}"); return this; } + + /// + /// Throws or upon failing. + /// + /// The exception to throw (defaults to ). + /// This assertion. public Assertion Throw(Exception? exception = null) { return OnFailure((Assertion 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}"; } diff --git a/Terminal/Assertion/AssertionResult.cs b/Terminal/Assertion/AssertionResult.cs index bf992bb..0d0554f 100644 --- a/Terminal/Assertion/AssertionResult.cs +++ b/Terminal/Assertion/AssertionResult.cs @@ -1,9 +1,27 @@ namespace OxDED.Terminal.Assertion; +/// +/// The possible results of an assertion. +/// public enum AssertionResult { + /// + /// The assertion succceeded. + /// Success, + /// + /// The value assertion failed. + /// UnexpectedValue, - UnexpectedReference, + /// + /// 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 index 2efab40..37eefa1 100644 --- a/Terminal/Assertion/ExceptionAssertion.cs +++ b/Terminal/Assertion/ExceptionAssertion.cs @@ -2,7 +2,16 @@ 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(); @@ -12,24 +21,53 @@ public static ExceptionAssertion Create(Action emitter) { 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 { @@ -40,18 +78,38 @@ public static ExceptionAssertion Create(Func emitter) { 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; } - public Asserter ExceptionAsserter() { - return new Asserter(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}"; } diff --git a/Terminal/Assertion/ReferenceAssertion.cs b/Terminal/Assertion/ReferenceAssertion.cs index af638f1..75bdc33 100644 --- a/Terminal/Assertion/ReferenceAssertion.cs +++ b/Terminal/Assertion/ReferenceAssertion.cs @@ -1,8 +1,48 @@ namespace OxDED.Terminal.Assertion; +/// +/// Represents an assertion that checks null references. +/// +/// The type of the possible null reference. public class ReferenceAssertion : Assertion { - public static ReferenceAssertion Create(T? obj, bool shouldBeNull = true) { - return new ReferenceAssertion(shouldBeNull ? (obj is null) : (obj is not null), obj); + /// + /// 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); } - public ReferenceAssertion(bool success, T? value = default) : base(success ? AssertionResult.Success : AssertionResult.UnexpectedReference, value) { } } \ No newline at end of file diff --git a/Terminal/Assertion/TypeAssertion.cs b/Terminal/Assertion/TypeAssertion.cs index 9cceb44..a68e4a4 100644 --- a/Terminal/Assertion/TypeAssertion.cs +++ b/Terminal/Assertion/TypeAssertion.cs @@ -1,31 +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 index 3e9d503..fb935f2 100644 --- a/Terminal/Assertion/ValueAssertion.cs +++ b/Terminal/Assertion/ValueAssertion.cs @@ -2,6 +2,11 @@ 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; @@ -13,23 +18,49 @@ private static bool P_Is(object? a, object? b) { // 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}"; } diff --git a/examples/Assertion/Program.cs b/examples/Assertion/Program.cs index a7028e5..54a98d3 100644 --- a/examples/Assertion/Program.cs +++ b/examples/Assertion/Program.cs @@ -35,6 +35,10 @@ public static void Main(string[] args) { == Assert.IsType(42).IsSuccess(); // Assert equivalent + // Some custom asserter functions: + new Asserter(true).IsTrue(); + new Asserter(()=>throw new Exception()).Throws(); + // Different assertions have different asserters. // Like the exception assertion has (ExceptionAssertion has another one): ExceptionAssertion.Create(()=>{}) @@ -70,6 +74,7 @@ public static void Main(string[] args) { failedAssertion.As>()!.SecondAsserter().Log(logger) ); + Assert.IsTrue(false).Throw(); // bye, bye. } From ff17e5a1e21fd914f13db0a47b141e40f717ef80 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:27:29 +0100 Subject: [PATCH 10/27] Change version --- Terminal/Terminal.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal/Terminal.csproj b/Terminal/Terminal.csproj index 258d7c3..3e7f6a4 100644 --- a/Terminal/Terminal.csproj +++ b/Terminal/Terminal.csproj @@ -7,7 +7,7 @@ true true 0xDED.Terminal - 4.0.0 + 4.1.0 0xDED MIT README.md From 7f59bc180ed7f63b2f104e0bfa67c1ba47f6d011 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:32:52 +0100 Subject: [PATCH 11/27] Edit README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 17cec9b..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) (doesn't work) +- [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) From 9931f5926705d351638e0a1ee7d4a1dc278f99ec Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:43:54 +0100 Subject: [PATCH 12/27] Added FormattedTarget, namespace --- Terminal/Logging/Targets/FileTarget.cs | 48 ++++------------- Terminal/Logging/Targets/FormattedTarget.cs | 58 +++++++++++++++++++++ Terminal/Logging/Targets/TerminalTarget.cs | 34 ++++-------- Terminal/Terminal.csproj | 2 +- examples/Logging/Program.cs | 1 + 5 files changed, 80 insertions(+), 63 deletions(-) create mode 100644 Terminal/Logging/Targets/FormattedTarget.cs diff --git a/Terminal/Logging/Targets/FileTarget.cs b/Terminal/Logging/Targets/FileTarget.cs index c7a139d..4a0f8e9 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. /// @@ -39,25 +20,16 @@ public FileTarget(string path, string? format = null) { } FileOut = new StreamWriter(File.Open(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)); } - /// - 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 as SubLogger)!.ParentLogger), logger.Name); - } - } - /// /// /// 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, T? text) where T : default { + 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..3e2e106 --- /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, T? 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 4617abb..2d293cd 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 as SubLogger)!.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, T? text) where T : default { 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/Terminal.csproj b/Terminal/Terminal.csproj index 3e7f6a4..24350b7 100644 --- a/Terminal/Terminal.csproj +++ b/Terminal/Terminal.csproj @@ -7,7 +7,7 @@ true true 0xDED.Terminal - 4.1.0 + 4.1.1 0xDED MIT README.md diff --git a/examples/Logging/Program.cs b/examples/Logging/Program.cs index fdc46d3..6eb800a 100644 --- a/examples/Logging/Program.cs +++ b/examples/Logging/Program.cs @@ -1,6 +1,7 @@ // The namespace of terminal logging. using OxDED.Terminal; using OxDED.Terminal.Logging; +using OxDED.Terminal.Logging.Targets; class Program { public const string LoggerID = "me.0xDED.Terminal.examples"; From 1dd5e5ef4323ff26a0d484c5114aba3006a317fa Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:45:44 +0100 Subject: [PATCH 13/27] hotfix --- Terminal/Logging/Logger.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Terminal/Logging/Logger.cs b/Terminal/Logging/Logger.cs index 34300fd..de57bc5 100644 --- a/Terminal/Logging/Logger.cs +++ b/Terminal/Logging/Logger.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using OxDED.Terminal.Logging.Targets; namespace OxDED.Terminal.Logging; From 29fd17b026896652460d5aca2a61fac7b1370b8a Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:05:16 +0100 Subject: [PATCH 14/27] Add Unhandled Exceptions --- Terminal/Logging/Logger.cs | 41 +++++++++++++++++++++++++++++++++++++ examples/Logging/Program.cs | 19 ++++++++++++----- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/Terminal/Logging/Logger.cs b/Terminal/Logging/Logger.cs index de57bc5..4bf46df 100644 --- a/Terminal/Logging/Logger.cs +++ b/Terminal/Logging/Logger.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Collections.ObjectModel; using OxDED.Terminal.Logging.Targets; @@ -200,6 +201,46 @@ public bool HasSubLogger(SubLogger logger) { subLoggers.TryGetValue(childID, out Logger? logger); return logger; } + private bool handlingUnhandledExceptions; + private 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; + } + } + private void HandleException(object sender, UnhandledExceptionEventArgs args) { + Exception e = (Exception) args.ExceptionObject; + string source = e.Source == null ? "" : $" (in: {e.Source})"; + string message = $"Unhandled Exception : {e.GetType().Name} ({e.GetType().FullName}) : '{e.Message}'\nTrace{source}:\n{GetStacktrace(e)}"; + + if (e.HelpLink != null) { + message += "\n\nHelp: "+e.HelpLink; + } + + Log(args.IsTerminating ? Severity.Fatal : Severity.Error, message); + } + + /// + /// 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 (). /// diff --git a/examples/Logging/Program.cs b/examples/Logging/Program.cs index 6eb800a..f686d37 100644 --- a/examples/Logging/Program.cs +++ b/examples/Logging/Program.cs @@ -7,7 +7,7 @@ class Program { public const string LoggerID = "me.0xDED.Terminal.examples"; // Creates a logger with an ID (optional) and name and with severity Trace (lowest). - public static Logger logger = new(LoggerID, "Logging Example", Severity.Trace); + public static Logger logger = new("Logging Example", LoggerID, Severity.Trace); public static void Main() { logger.LogMessage("Hello, world!"); logger.LogInfo("This is the start of the program!"); @@ -23,13 +23,13 @@ public static void Main() { Terminal.WriteLine("Sub loggers"); // Sub loggers - SubLogger sublogger1 = logger.CreateSubLogger("Sub 1", "sub1", severity:Severity.Trace); + SubLogger sublogger1 = logger.CreateSubLogger("sub1", "Sub 1", severity:Severity.Trace); sublogger1.LogInfo("This is the first sub logger of "+logger.Name); - SubLogger sublogger2 = logger.CreateSubLogger("Sub 2", "sub2", severity:Severity.Trace); + SubLogger sublogger2 = logger.CreateSubLogger("sub2", "Sub 2", severity:Severity.Trace); sublogger2.LogMessage("This is the second sub logger of "+sublogger2.ParentLogger.Name); // Gets parent name from ParentLogger - SubLogger subsublogger = sublogger2.CreateSubLogger("sub-sub", "sub", severity:Severity.Trace); + SubLogger subsublogger = sublogger2.CreateSubLogger("sub", "sub-sub", severity:Severity.Trace); // sublogger2.SubLoggers[sublogger2.SubLoggers.Keys.ToArray()[0]] // NOTE: The difference between child ID and ID is that the child ID is used by the parent (last bit) and the ID is used by the Loggers Register (full ID). @@ -52,7 +52,16 @@ public static void Main() { sublogger2.GetTarget().Format = "<{1}>: {3}: ({2}) : {5}{4}"+ANSI.Styles.ResetAll; sublogger2.LogDebug("Wow cool new format!"); - // Don't forget to dispose the logger (does happen automatically). + // Can listen for unhandled exceptions in the current app domain. + logger.HandleUnhandledExceptions = true; + + // try { + // throw new Exception("middle", new Exception("inner")); + // } catch (Exception e) { + // throw new Exception("outer", e); + // } + + // Don't forget to dispose the logger. logger.Dispose(); } } \ No newline at end of file From 5048a87bc5cfbbb0d237119eca107a541379d81e Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:05:47 +0100 Subject: [PATCH 15/27] change version --- Terminal/Terminal.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal/Terminal.csproj b/Terminal/Terminal.csproj index 24350b7..70bb0c6 100644 --- a/Terminal/Terminal.csproj +++ b/Terminal/Terminal.csproj @@ -7,7 +7,7 @@ true true 0xDED.Terminal - 4.1.1 + 4.1.2 0xDED MIT README.md From 744e6ca9337f3917ed09014e9c917f0f73f2cd7a Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:23:36 +0100 Subject: [PATCH 16/27] Changed targets, LogException --- Terminal/Logging/Logger.cs | 221 +++++++++++++++++++--------------- Terminal/Logging/SubLogger.cs | 4 +- examples/Logging/Program.cs | 4 + 3 files changed, 133 insertions(+), 96 deletions(-) diff --git a/Terminal/Logging/Logger.cs b/Terminal/Logging/Logger.cs index 4bf46df..b269311 100644 --- a/Terminal/Logging/Logger.cs +++ b/Terminal/Logging/Logger.cs @@ -9,7 +9,7 @@ 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; /// @@ -30,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. /// @@ -51,17 +51,16 @@ public class Logger : IDisposable, IEquatable { /// The log level of this logger. /// The targets to add and enable (default: , with path "./latest.log"). /// - public Logger(string name = "Logger", string? id = null, 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) } }; + 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)); } } - } /// @@ -81,67 +80,78 @@ 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. + /// Gets the target's index. /// - /// The type of the target. - /// True if it has a target of that type. - public bool HasTarget() where T : ITarget { - return HasTarget(typeof(T)); + /// 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 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. - /// - /// 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!; } + /// /// Sets if a target is enabled. /// - /// The type of the Target (e.g. TerminalTarget). + /// /// True if enabled. /// True if there is a Target with that type. - public bool SetTarget(bool enabled) where T : ITarget { - return SetTarget(typeof(T), enabled); + public bool SetTarget(int index, bool enabled) { + ITarget? target = GetTarget(index); + if (target == null) { + return false; + } + try { + Targets[index] = (target, enabled); + } catch (IndexOutOfRangeException) { + return false; + } + return true; } /// /// Sets if 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; + public bool SetTarget(ITarget target, bool enabled) { + int index = GetTargetIndex(target); + if (index == -1) { + return false; } - return false; + Targets[index] = (target, enabled); + return true; } /// @@ -150,25 +160,31 @@ 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. /// - /// 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. /// - /// 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)); + } /// @@ -201,27 +217,12 @@ public bool HasSubLogger(SubLogger logger) { subLoggers.TryGetValue(childID, out Logger? logger); return logger; } + private bool handlingUnhandledExceptions; - private 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; - } - } + private void HandleException(object sender, UnhandledExceptionEventArgs args) { Exception e = (Exception) args.ExceptionObject; - string source = e.Source == null ? "" : $" (in: {e.Source})"; - string message = $"Unhandled Exception : {e.GetType().Name} ({e.GetType().FullName}) : '{e.Message}'\nTrace{source}:\n{GetStacktrace(e)}"; - - if (e.HelpLink != null) { - message += "\n\nHelp: "+e.HelpLink; - } - - Log(args.IsTerminating ? Severity.Fatal : Severity.Error, message); + LogException(e, args.IsTerminating ? Severity.Fatal : Severity.Error, true); } /// @@ -251,35 +252,44 @@ public void Log(Severity severity, T? 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(T? 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(T? 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(T? text) { + Log(Severity.Info, text); + } + /// + /// Logs something with Message severity. + /// + /// The type of the data. + /// The text to write (). + public void LogMessage(T? text) { + Log(Severity.Message, text); } /// /// Logs something with Warning severity. @@ -290,28 +300,50 @@ public void LogWarning(T? 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(T? 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(T? text) { + Log(Severity.Fatal, text); + } + + private string GetStacktrace(Exception e) { + string stackTrace = e.StackTrace ?? " (Unknown)"; + if (e.InnerException != null) { + Exception inner = e.InnerException; + string source = inner.Source == null ? string.Empty : $" (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 ? string.Empty : $" (in: {e.Source})"; + string unhandledStr = unhandled?"Unhandled Exception : " : string.Empty; + 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); } /// @@ -324,7 +356,7 @@ public void LogTrace(T? text) { /// The targets of the new sub logger (default: Targets of parent). /// The created sub logger. /// - public SubLogger CreateSubLogger(string id, string name = "Sublogger", bool shouldRegister = true, Severity severity = Severity.Info, Dictionary? targets = null) { + 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; @@ -336,8 +368,9 @@ public SubLogger CreateSubLogger(string id, string name = "Sublogger", bool shou /// 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) { diff --git a/Terminal/Logging/SubLogger.cs b/Terminal/Logging/SubLogger.cs index d6705bf..a328397 100644 --- a/Terminal/Logging/SubLogger.cs +++ b/Terminal/Logging/SubLogger.cs @@ -3,7 +3,7 @@ namespace OxDED.Terminal.Logging; /// /// Represents a child of another logger. /// -public class SubLogger : Logger { +public sealed class SubLogger : Logger { /// /// The parent logger if this logger is a sub logger. /// @@ -14,7 +14,7 @@ public class SubLogger : Logger { /// public readonly string childID; - internal SubLogger(Logger parentLogger, string id = "Sublogger", string name = "Sublogger", string? registeredId = null, Severity severity = Severity.Info, Dictionary? targets = null) : base(name, registeredId, severity, targets) { + 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; } diff --git a/examples/Logging/Program.cs b/examples/Logging/Program.cs index f686d37..ddda808 100644 --- a/examples/Logging/Program.cs +++ b/examples/Logging/Program.cs @@ -61,6 +61,10 @@ public static void Main() { // throw new Exception("outer", e); // } + // Can also log exceptions. + logger.LogException(new Exception("outer", new Exception("middle", new Exception("inner")))); + + // Don't forget to dispose the logger. logger.Dispose(); } From f5bb5bf5cfd3ce3149fc7a186a5e67e05b24c5ed Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:30:55 +0100 Subject: [PATCH 17/27] Bump version --- Terminal/Terminal.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal/Terminal.csproj b/Terminal/Terminal.csproj index 70bb0c6..ba22d22 100644 --- a/Terminal/Terminal.csproj +++ b/Terminal/Terminal.csproj @@ -7,7 +7,7 @@ true true 0xDED.Terminal - 4.1.2 + 4.1.3 0xDED MIT README.md From 842eb95d326d9dbd6af3b5d09f65343d83d6953b Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Mon, 30 Dec 2024 23:32:49 +0100 Subject: [PATCH 18/27] a --- .vscode/launch.json | 22 -- .vscode/tasks.json | 15 - Terminal/Arguments/Argument.cs | 130 ++----- Terminal/Arguments/ArgumentFormatter.cs | 174 ++++++++++ Terminal/Arguments/ArgumentParameter.cs | 58 ---- Terminal/Arguments/ArgumentParser.cs | 418 +---------------------- Terminal/Arguments/ArgumentParser_old.cs | 348 +++++++++++++++++++ Terminal/Arguments/Delegates.cs | 4 +- Terminal/Arguments/Option.cs | 157 +++++++++ Terminal/Arguments/OptionParameter.cs | 60 ++++ Terminal/Arguments/PositionalArgument.cs | 76 ----- examples/Args/Program.cs | 21 +- 12 files changed, 787 insertions(+), 696 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/tasks.json create mode 100644 Terminal/Arguments/ArgumentFormatter.cs delete mode 100644 Terminal/Arguments/ArgumentParameter.cs create mode 100644 Terminal/Arguments/ArgumentParser_old.cs create mode 100644 Terminal/Arguments/Option.cs create mode 100644 Terminal/Arguments/OptionParameter.cs delete mode 100644 Terminal/Arguments/PositionalArgument.cs diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 5e9f299..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceFolder}/examples/Args/bin/Debug/net8.0/Args.dll", - "args": ["a", "--bar", "a", "a"], - "cwd": "${workspaceFolder}", - "stopAtEntry": false, - "console": "externalTerminal", - } - - - - ] -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 8fe9072..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "dotnet", - "task": "build", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [], - "label": "build" - }, - ] -} \ No newline at end of file diff --git a/Terminal/Arguments/Argument.cs b/Terminal/Arguments/Argument.cs index ff3dc42..90027d0 100644 --- a/Terminal/Arguments/Argument.cs +++ b/Terminal/Arguments/Argument.cs @@ -1,137 +1,67 @@ namespace OxDED.Terminal.Arguments; /// -/// Represents an optional argument (-f, --foo). +/// Represents a required argument in a specific order. /// -public class Argument : ICloneable, IEquatable { +public class Argument : ICloneable { /// - /// The keys of this argument (f, foo). + /// The name (key) of this argument. /// - public string[] keys; + public string name; /// - /// The parameters of this argument. + /// The description of this argument. /// - public ArgumentParameter[] parameters; + public string? description; + internal string? value; /// - /// The description of this argument. + /// If this argument has a value (should be yes). /// - public string? description = null; + public bool HasValue { get => value != null; } /// - /// Creates an argument. + /// The value of this argument (error if it isn't parsed). /// - /// 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 string Value { get { + if (HasValue) { + return value!; + } else { + throw new InvalidOperationException("This argument has not yet been parsed"); + } + } } + /// - /// Creates an argument. + /// Creates a argument. /// - /// The keys of this argument. + /// The name 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]; + public Argument(string name, string? description = null) { + this.name = name; this.description = description; - this.parameters = parameters == null ? [] : [.. parameters]; } /// - /// Sets the key of this argument. + /// Sets the name of this argument. /// - /// The new key. + /// The new name of this argument. /// This argument. - public Argument Key(string key) { - keys = [key]; - return this; - } - /// - /// Sets the keys of this argument. - /// - /// The new keys. - /// This argument. - public Argument Keys(IEnumerable keys) { - this.keys = [.. keys]; + public Argument Name(string name) { + this.name = name; return this; } /// /// Sets the description of this argument. /// - /// The new description. + /// The new description of this argument. /// 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 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(); } @@ -142,6 +72,6 @@ public object Clone() { /// The new copy of this color. /// public Argument CloneArgument() { - return new Argument(keys, description, parameters); + return new Argument(name, description); } } \ No newline at end of file diff --git a/Terminal/Arguments/ArgumentFormatter.cs b/Terminal/Arguments/ArgumentFormatter.cs new file mode 100644 index 0000000..b364ed0 --- /dev/null +++ b/Terminal/Arguments/ArgumentFormatter.cs @@ -0,0 +1,174 @@ +namespace OxDED.Terminal.Arguments; + +/// +/// Represents a format for arguments and options. +/// +public partial class ArgumentFormatter { + public string? name; + public string? description; + public string? version; + + /// + /// The options of this format. + /// + public List Options { get; private set; } + /// + /// The arguments of this format. + /// + public List Arguments { get; private set; } + + /// + /// Creates a new argument format. + /// + public ArgumentFormatter(List? arguments = null, List? options = null) { + Options = options ?? []; + Arguments = arguments ?? []; + } + + public OptionFormat Option() { + return new(this); + } + public ArgumentFormat Argument() { + return new(this); + } + + public ArgumentFormatter Name(string name) { + this.name = name; + return this; + } + public ArgumentFormatter Description(string? description) { + this.description = description; + return this; + } + public ArgumentFormatter Version(string? version) { + this.version = version; + return this; + } + + public ArgumentFormatter AddHelpOption(bool quit = true, IEnumerable? keys = null) { + Option() + .Keys(keys ?? ["-h", "--help"]) + .Description("Shows this help message.") + .Finish(); + return this; + } + public ArgumentFormatter AddVersionOption(bool quit = true, IEnumerable? keys = null) { + Option() + .Keys(keys ?? ["-v", "--version"]) + .Description("Shows the version of this application.") + .Finish(); + return this; + } + + // public ArgumentFormatter +} + +public partial class ArgumentFormatter { + public class ArgumentFormat { + public string description; + public string name; + + public ArgumentFormatter ArgumentFormatter { get; private set; } + + public ArgumentFormat(ArgumentFormatter argumentFormatter) { + ArgumentFormatter = argumentFormatter; + } + + public ArgumentFormat Name(string name) { + this.name = name; + return this; + } + public ArgumentFormat Description(string description) { + this.description = description; + return this; + } + + public ArgumentFormatter Finish() { + ArgumentFormatter.Arguments = [.. ArgumentFormatter.Arguments, this]; + return ArgumentFormatter; + } + } +} + +public partial class ArgumentFormatter { + public class OptionFormat { + public string[] keys; + public string description; + public ParameterFormat[] parameters; + + public ArgumentFormatter ArgumentFormatter { get; private set; } + + public OptionFormat(ArgumentFormatter argumentFormatter) { + ArgumentFormatter = argumentFormatter; + } + + public OptionFormat Key(string key) { + keys = [.. keys, key]; + return this; + } + public OptionFormat Keys(IEnumerable keys) { + this.keys = [.. this.keys, .. keys]; + return this; + } + public OptionFormat Description(string description) { + this.description = description; + return this; + } + public ParameterFormat Parameter() { + return new(this); + } + + public ArgumentFormatter Finish() { + ArgumentFormatter.Options = [.. ArgumentFormatter.Options, this]; + return ArgumentFormatter; + } + + public class ParameterFormat { + public string name { get; set; } + public string description { get; set; } + public bool required { get; set; } + + public OptionFormat OptionFormat { get; private set; } + + public ParameterFormat(OptionFormat optionBuilder) { + OptionFormat = optionBuilder; + } + + public ParameterFormat Name(string name) { + this.name = name; + return this; + } + public ParameterFormat Description(string description) { + this.description = description; + return this; + } + public ParameterFormat Required(bool required) { + this.required = required; + return this; + } + + public OptionFormat Finish() { + OptionFormat.parameters = [.. OptionFormat.parameters, this]; + return OptionFormat; + } + } + } +} + +public class HelpOptionFormat : ArgumentFormatter.OptionFormat { + public HelpOptionFormat(ArgumentFormatter argumentFormatter) : base(argumentFormatter) { } + public bool quit; + public HelpOptionFormat Quit(bool quit) { + this.quit = quit; + return this; + } +} + +public class VersionOptionFormat : ArgumentFormatter.OptionFormat { + public VersionOptionFormat(ArgumentFormatter argumentFormatter) : base(argumentFormatter) { } + public bool quit; + public VersionOptionFormat Quit(bool quit) { + this.quit = quit; + return this; + } +} \ 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..b281545 100644 --- a/Terminal/Arguments/ArgumentParser.cs +++ b/Terminal/Arguments/ArgumentParser.cs @@ -1,420 +1,12 @@ namespace OxDED.Terminal.Arguments; -// TODO: add docs - -/// -/// Helps you with parsing arguments. -/// -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 = []; - /// - /// The positional arguments of the parser. - /// - 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); - } - helpArgument = new Argument(keys ?? ["h", "help"], "Shows all the available arguments."); - AddArgument(helpArgument, (Argument arg) => - { - WriteHelp(showDescription, showVersion); - if (shouldExit) - { - Environment.Exit(0); - } - }); - 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); - } - 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); - } - }); - this.version = version; - return this; - } - /// - /// Sets the description of the application parser. - /// - public ArgumentParser Description(string? description) - { - this.description = description; - return this; - } - /// - /// Sets the name of the application parser. - /// - public ArgumentParser Name(string? name) - { - this.name = name; - return this; - } - /// - /// Removes a positional argument. - /// - public ArgumentParser RemovePositionalArgument(int position) { - positionalArguments.RemoveAt(position); - return this; - } - /// - /// Removes a positional argument. - /// - public ArgumentParser RemovePositionalArgument(PositionalArgument argument) { - positionalArguments.Remove(argument); - return this; - } - /// - /// Removes an argument. - /// - public ArgumentParser RemoveArgument(Argument argument) - { - foreach (string key in argument.keys) - { - arguments.Remove(key); - } - return this; - } - /// - /// Adds an argument. - /// - public ArgumentParser AddArgument(Argument argument, ArgumentCallback? callback = null) - { - foreach (string key in argument.keys) - { - arguments.Add(key, argument); - } - if (callback != null) - { - OnArgument += (Argument arg) => - { - if (arg.keys == argument.keys) - { - callback?.Invoke(arg); - } - }; - } - return this; - } - /// - /// Adds a positional argument. - /// - 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); - } - }; - } - return this; - } - /// - /// Writes the help menu to the terminal. - /// - public void WriteHelp(bool showDescription = true, bool showVersion = false) - { - Terminal.WriteLine(GetHelp(showDescription, showVersion)); - } - /// - /// Writes the version to the terminal. - /// - 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 += ", "; - } - } - 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; +public class ArgumentParser { + public ArgumentFormatter Register { get; private set; } + public ArgumentParser(ArgumentFormatter register) { + Register = register; } - 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); - } + public void Parse(string[] args) { - } - 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); - } - } - } - return result; } - private string GetVersion() - { - return $"{ANSI.Styles.Bold}{name}{ANSI.Styles.ResetBold} {version ?? ""}{(description != null ? '\n' + description : "")}"; - } - /// - /// An event that is called when an argument is parsed. - /// - public event ArgumentCallback? OnArgument; - /// - /// An event that is called when a positional argument is parsed. - /// - public event PositionalArgumentCallback? OnPositionalArgument; - /// - /// An event that is called when the format is invalid. - /// - 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); - } - /// - /// Parses the arguments. - /// - public bool Parse(string arguments) - { - return Parse(arguments.Split(' ')); - } - /// - /// Parses the arguments. - /// - 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; - } - - } - else - { - PositionalArgument? positionalArgument = GetPositionalArgument(positionalArgumentIndex); - if (positionalArgument != null) - { - positionalArgument.value = argument; - OnPositionalArgument?.Invoke(positionalArgument); - positionalArgumentIndex++; - } - else - { - WriteInvalid("Too much positional arguments."); - return false; - } - } - } - 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]; - } - OnArgument?.Invoke(parsingArgument); - isParsingArgument = false; - parameters = []; - parsingArgument = null; - } - } - } - if (isParsingArgument) - { - WriteInvalid("Invalid argument parameters for " + parameters[0] + "."); - return false; - } - if (positionalArguments.Count != positionalArgumentIndex) - { - WriteNoArgument("Not enough positional arguments.", positionalArgumentIndex); - return false; - } - - return true; - } - /// - /// Gets an argument that is registered (null if it isn't registered). - /// - public Argument? GetArgument(string key) - { - if (!arguments.TryGetValue(key, out Argument? value)) - { - return null; - } - 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]; - } - } \ No newline at end of file diff --git a/Terminal/Arguments/ArgumentParser_old.cs b/Terminal/Arguments/ArgumentParser_old.cs new file mode 100644 index 0000000..e439b39 --- /dev/null +++ b/Terminal/Arguments/ArgumentParser_old.cs @@ -0,0 +1,348 @@ +namespace OxDED.Terminal.Arguments; + +/// +/// Helps you with parsing arguments. +/// +public class ArgumentParserOld { + /// + /// This is the name of the application. + /// + public string? name = null; + /// + /// The description of the application. + /// + public string? description = null; + private Option? versionArgument = null; + private Option? helpArgument = null; + /// + /// The version of the application. + /// + public string? version = null; + /// + /// The arguments of the parser. + /// + public Dictionary options = []; + /// + /// The arguments of the parser. + /// + public List arguments = []; + /// + /// Sets the help argument of the application parser. + /// + /// + /// + /// + /// + public ArgumentParserOld Help(IEnumerable? keys = null, bool showDescription = true, bool showVersion = false, bool shouldExit = true) { + if (helpArgument != null) { + RemoveOption(helpArgument); + } + helpArgument = new Option(keys ?? ["h", "help"], "Shows all the available arguments."); + AddOption(helpArgument, (Option arg) => { + WriteHelp(showDescription, showVersion); + if (shouldExit) { + Environment.Exit(0); + } + }); + 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 ArgumentParserOld Version(string version, IEnumerable? keys = null, bool shouldExit = true) { + if (versionArgument != null) { + RemoveOption(versionArgument); + } + versionArgument = new Option(keys ?? ["v", "version"], name == null ? "Shows the version of this application." : $"Shows the version of {name}."); // TODO: add documentation to first assign a name + AddOption(versionArgument, (Option arg) => { + WriteVersion(); + if (shouldExit) { + Environment.Exit(0); + } + }); + this.version = version; + return this; + } + /// + /// Sets the description of the application parser. + /// + public ArgumentParserOld Description(string? description) { + this.description = description; + return this; + } + /// + /// Sets the name of the application parser. + /// + public ArgumentParserOld Name(string? name) { + this.name = name; + return this; + } + /// + /// Removes a argument. + /// + public ArgumentParserOld RemoveArgument(int position) { + arguments.RemoveAt(position); + return this; + } + /// + /// Removes a argument. + /// + public ArgumentParserOld RemoveArgument(Argument argument) { + arguments.Remove(argument); + return this; + } + /// + /// Removes an argument. + /// + public ArgumentParserOld RemoveOption(Option argument) { + foreach (string key in argument.keys) { + options.Remove(key); + } + return this; + } + /// + /// Adds an argument. + /// + public ArgumentParserOld AddOption(Option argument, OptionCallback? callback = null) { + foreach (string key in argument.keys) { + options.Add(key, argument); + } + if (callback != null) { + OnOption += (Option arg) => { + if (arg.keys == argument.keys) { + callback?.Invoke(arg); + } + }; + } + return this; + } + /// + /// Adds a argument. + /// + public ArgumentParserOld AddArgument(Argument argument, ArgumentCallback? callback = null) { + arguments.Add(argument); + if (callback != null) { + OnArgument += (Argument arg) => { + if (arg.name == argument.name) { + callback?.Invoke(arg); + } + }; + } + return this; + } + /// + /// Writes the help menu to the terminal. + /// + public void WriteHelp(bool showDescription = true, bool showVersion = false) { + Terminal.WriteLine(GetHelp(showDescription, showVersion)); + } + /// + /// Writes the version to the terminal. + /// + public void WriteVersion() { + Terminal.WriteLine(GetVersion()); + } + private static string GetArgumentHelp(Argument 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 += ", "; + } + } + return result; + } + private static string GetArgumentHelp(Option arg) { + string result = $"{Color.DarkGreen.ToForegroundANSI()}\u2520{ANSI.Styles.ResetAll} {GetArgumentHelpName(arg.keys)}{(arg.description == null ? "" : ": "+arg.description)}\n"; + foreach (OptionParameter para in arg.parameters) { + result += $"{Color.DarkGreen.ToForegroundANSI()}\u2503 \u2560{ANSI.Styles.ResetAll} {para.name}{(para.description == null ? "" : ": "+para.description)}\n"; + } + return result; + } + 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 (arguments.Count > 0) { + result += $"{Color.Orange.ToForegroundANSI()}\u250E\u2500\u2500{ANSI.Styles.ResetAll} Required Arguments\n"; + for (int i = 0; i < arguments.Count; i++) { + Argument arg = arguments[i]; + if (positionalIndex <= i && positionalIndex != -1) { + result += GetArgumentHelp(arg, true); + } + else { + result += GetArgumentHelp(arg, false); + } + + } + result += "\n"; + } + if (options.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 (options.Count > 0) { + foreach (Option argument in options.Values.Distinct()) { + if (argument == versionArgument || argument == helpArgument) { + continue; + } + result += GetArgumentHelp(argument); + } + } + } + return result; + } + private string GetVersion() { + return $"{ANSI.Styles.Bold}{name}{ANSI.Styles.ResetBold} {version ?? ""}{(description != null ? '\n' + description : "")}"; + } + /// + /// An event that is called when an option is parsed. + /// + public event OptionCallback? OnOption; + /// + /// An event that is called when a argument is parsed. + /// + public event ArgumentCallback? OnArgument; + /// + /// An event that is called when the format is invalid. + /// + 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); + } + /// + /// Parses the arguments. + /// + public bool Parse(string arguments) { + return Parse(arguments.Split(' ')); + } + /// + /// Parses the arguments. + /// + public bool Parse(string[] arguments) { + int argumentIndex = 0; + Option? parsingOption = null; + List parameters = []; + bool isParsingArgument = false; + foreach (string raw in arguments) { + if (!isParsingArgument) { + if (raw.StartsWith("--") && raw.Length > 2) { + parsingOption = GetArgument(raw[2..]); + if (parsingOption == null) { + WriteInvalid("No such argument as: --" + raw[2..] + "."); + return false; + } + else { + isParsingArgument = true; + } + } + else if (raw.StartsWith('-') && raw.Length > 1) { + if (raw.Length >= 3) { + WriteInvalid("Invalid symbol usage (-): " + raw + ".\n Should it be (--)?"); + } + parsingOption = GetArgument(raw[1].ToString()); + if (parsingOption == null) { + WriteInvalid("No such argument as: -" + raw[1] + "."); + return false; + } + else { + isParsingArgument = true; + } + + } + else { + Argument? argument = GetArgument(argumentIndex); + if (argument != null) { + argument.value = raw; + OnArgument?.Invoke(argument); + argumentIndex++; + } + else { + WriteInvalid("Too much arguments."); + return false; + } + } + } + if (isParsingArgument) { + if (!(parameters.Count - 1 >= parsingOption!.parameters.Length)) { + parameters.Add(raw); + } + if (parameters.Count - 1 >= parsingOption!.parameters.Length) { + for (int j = 0; j < parsingOption!.parameters.Length; j++) { + parsingOption!.parameters[j].value = parameters[j + 1]; + } + OnOption?.Invoke(parsingOption); + isParsingArgument = false; + parameters = []; + parsingOption = null; + } + } + } + if (isParsingArgument) { + WriteInvalid("Invalid option parameters for " + parameters[0] + "."); + return false; + } + if (this.arguments.Count != argumentIndex) { + WriteNoArgument("Not enough arguments.", argumentIndex); + return false; + } + + return true; + } + /// + /// Gets an argument that is registered (null if it isn't registered). + /// + public Option? GetArgument(string key) { + if (!options.TryGetValue(key, out Option? value)) { + return null; + } + return value; + } + /// + /// Gets a argument at a location (in order as registered) (if there is one). + /// + public Argument? GetArgument(int pos) { + return arguments.Count > pos ? arguments[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 Option[] GetOptions() { + return [.. options.Values.Distinct()]; + } + /// + /// Gets all the registered arguments. + /// + public Argument[] GetArguments() { + return [.. arguments]; + } + +} \ No newline at end of file diff --git a/Terminal/Arguments/Delegates.cs b/Terminal/Arguments/Delegates.cs index 58d6c9d..afafe01 100644 --- a/Terminal/Arguments/Delegates.cs +++ b/Terminal/Arguments/Delegates.cs @@ -4,12 +4,12 @@ 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); +public delegate void OptionCallback(Option argument); /// /// An callback for when an postitional argument has been parsed. /// /// The positional argument that has been parsed. -public delegate void PositionalArgumentCallback(PositionalArgument argument); +public delegate void ArgumentCallback(Argument argument); /// /// An callback for when the parsing has failed. /// diff --git a/Terminal/Arguments/Option.cs b/Terminal/Arguments/Option.cs new file mode 100644 index 0000000..07bfcd9 --- /dev/null +++ b/Terminal/Arguments/Option.cs @@ -0,0 +1,157 @@ +namespace OxDED.Terminal.Arguments; + +/// +/// Represents an optional argument (e.g. -f, --foo). +/// +public partial class Option : ICloneable, IEquatable public partial class ArgumentFormatter { + public bool shouldExitOnError = true; public string? name; public string? description; public string? version; @@ -11,11 +12,11 @@ public partial class ArgumentFormatter { /// /// The options of this format. /// - public List Options { get; private set; } + public readonly List Options; /// /// The arguments of this format. /// - public List Arguments { get; private set; } + public readonly List Arguments; /// /// Creates a new argument format. @@ -32,6 +33,10 @@ public ArgumentFormat Argument() { return new(this); } + public ArgumentFormatter ShouldExitOnError(bool shouldExit) { + shouldExitOnError = shouldExit; + return this; + } public ArgumentFormatter Name(string name) { this.name = name; return this; @@ -45,130 +50,252 @@ public ArgumentFormatter Version(string? version) { return this; } - public ArgumentFormatter AddHelpOption(bool quit = true, IEnumerable? keys = null) { - Option() - .Keys(keys ?? ["-h", "--help"]) - .Description("Shows this help message.") - .Finish(); - return this; + public HelpOptionFormat CurrentHelpOption { get; internal set; } + + public VersionOptionFormat CurrentVersionOption { get; internal set; } + + public HelpOptionFormat HelpOption() { + return new HelpOptionFormat(this); } - public ArgumentFormatter AddVersionOption(bool quit = true, IEnumerable? keys = null) { - Option() - .Keys(keys ?? ["-v", "--version"]) - .Description("Shows the version of this application.") - .Finish(); - return this; + public VersionOptionFormat VersionOption() { + return new VersionOptionFormat(this); } - // public ArgumentFormatter + /// + /// Finishes formatting and creates a parser. + /// + /// A new argument parser. + public ArgumentParser Finish() { + return new(this); + } } public partial class ArgumentFormatter { + /// + /// Represents a format for an argument. + /// public class ArgumentFormat { - public string description; - public string name; + /// + /// 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 ArgumentFormatter ArgumentFormatter; - public ArgumentFormatter ArgumentFormatter { get; private set; } - - public ArgumentFormat(ArgumentFormatter argumentFormatter) { + internal ArgumentFormat(ArgumentFormatter argumentFormatter) { ArgumentFormatter = argumentFormatter; } - + /// + /// 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 ArgumentFormatter Finish() { - ArgumentFormatter.Arguments = [.. ArgumentFormatter.Arguments, this]; + ArgumentFormatter.Arguments.Add(this); return ArgumentFormatter; } } } public partial class ArgumentFormatter { + /// + /// Represents a format for an option. + /// public class OptionFormat { - public string[] keys; - public string description; - public ParameterFormat[] parameters; + /// + /// 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 = []; - public ArgumentFormatter ArgumentFormatter { get; private set; } + /// + /// The parent format. + /// + public readonly ArgumentFormatter ArgumentFormatter; - public OptionFormat(ArgumentFormatter argumentFormatter) { + internal OptionFormat(ArgumentFormatter argumentFormatter) { ArgumentFormatter = argumentFormatter; } - public OptionFormat Key(string key) { - keys = [.. keys, key]; + /// + /// Adds a key to check for. + /// + /// The new key. + /// This option. + public virtual OptionFormat Key(string key) { + keys.Add(key); return this; } - public OptionFormat Keys(IEnumerable keys) { - this.keys = [.. this.keys, .. keys]; + /// + /// Adds keys to check for. + /// + /// The new keys to add. + /// This option. + public virtual OptionFormat Keys(IEnumerable keys) { + this.keys.AddRange(keys); return this; } - public OptionFormat Description(string description) { + /// + /// Sets the description of this option. + /// + /// The new description. + /// This option. + public virtual OptionFormat Description(string description) { this.description = description; return this; } - public ParameterFormat Parameter() { + /// + /// Adds a new parameter to this option. + /// + /// The parameter formatter. + public virtual ParameterFormat Parameter() { return new(this); } - public ArgumentFormatter Finish() { - ArgumentFormatter.Options = [.. ArgumentFormatter.Options, this]; + /// + /// Finishes and saves this option format. + /// + /// The parent. + public virtual ArgumentFormatter Finish() { + ArgumentFormatter.Options.Add(this); return ArgumentFormatter; } - + /// + /// Represents a format for an option's parameter. + /// public class ParameterFormat { - public string name { get; set; } - public string description { get; set; } - public bool required { get; set; } + /// + /// 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 = ""; - public OptionFormat OptionFormat { get; private set; } + /// + /// The parent format. + /// + public readonly OptionFormat OptionFormat; - public ParameterFormat(OptionFormat optionBuilder) { + 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; } - public ParameterFormat Required(bool required) { - this.required = required; - return this; - } + /// + /// Finishes and saves this parameter format. + /// + /// The parent. public OptionFormat Finish() { - OptionFormat.parameters = [.. OptionFormat.parameters, this]; + OptionFormat.parameters.Add(this); return OptionFormat; } } } } +/// +/// Represents a configurable help option. +/// public class HelpOptionFormat : ArgumentFormatter.OptionFormat { - public HelpOptionFormat(ArgumentFormatter argumentFormatter) : base(argumentFormatter) { } - public bool quit; + internal HelpOptionFormat(ArgumentFormatter 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 ArgumentFormatter Finish() { + ArgumentFormatter.CurrentHelpOption = this; + return base.Finish(); + } } +/// +/// Represents a configurable version option. +/// public class VersionOptionFormat : ArgumentFormatter.OptionFormat { - public VersionOptionFormat(ArgumentFormatter argumentFormatter) : base(argumentFormatter) { } - public bool quit; + internal VersionOptionFormat(ArgumentFormatter 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 ArgumentFormatter Finish() { + ArgumentFormatter.CurrentVersionOption = this; + return base.Finish(); + } } \ No newline at end of file diff --git a/Terminal/Arguments/ArgumentParser.cs b/Terminal/Arguments/ArgumentParser.cs index b281545..72ef071 100644 --- a/Terminal/Arguments/ArgumentParser.cs +++ b/Terminal/Arguments/ArgumentParser.cs @@ -1,12 +1,376 @@ +using System.Collections.ObjectModel; +using System.Text; +using static OxDED.Terminal.Arguments.ArgumentFormatter; +using static OxDED.Terminal.Arguments.ArgumentFormatter.OptionFormat; + namespace OxDED.Terminal.Arguments; +/// +/// The final stage of argument parsing. +/// public class ArgumentParser { - public ArgumentFormatter Register { get; private set; } - public ArgumentParser(ArgumentFormatter register) { - Register = register; + /// + /// A parser that parses the arguments. + /// + 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; + } + + private int argumentIndex = 0; + private int ArgumentIndex { + get { + return argumentIndex; + } + 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 public partial 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; /// @@ -26,37 +38,77 @@ public ArgumentFormatter(List? arguments = null, List + /// 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 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; } - public ArgumentFormatter Name(string name) { + /// + /// 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; } - public HelpOptionFormat CurrentHelpOption { get; internal set; } - - public VersionOptionFormat CurrentVersionOption { get; internal set; } + /// + /// The currently used help option. + /// + public HelpOptionFormat? CurrentHelpOption { get; internal set; } + /// + /// The currently used version option. + /// + public VersionOptionFormat? CurrentVersionOption { get; internal set; } + /// + /// 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); } @@ -180,7 +232,7 @@ public virtual OptionFormat Description(string description) { /// /// Adds a new parameter to this option. /// - /// The parameter formatter. + /// The parameter format. public virtual ParameterFormat Parameter() { return new(this); } diff --git a/Terminal/Arguments/ArgumentParser.cs b/Terminal/Arguments/ArgumentParser.cs index 72ef071..73ad9ca 100644 --- a/Terminal/Arguments/ArgumentParser.cs +++ b/Terminal/Arguments/ArgumentParser.cs @@ -208,11 +208,11 @@ public void Parse(string[] args) { Options = [.. parser.Options]; Arguments = [.. parser.Arguments]; - if (HasOption(Format.CurrentHelpOption)) { + if (Format.CurrentHelpOption != null && HasOption(Format.CurrentHelpOption)) { ShowHelp(); if (Format.CurrentHelpOption.quit) Environment.Exit(0); } - if (HasOption(Format.CurrentVersionOption)) { + if (Format.CurrentVersionOption != null && HasOption(Format.CurrentVersionOption)) { ShowVersion(); if (Format.CurrentVersionOption.quit) Environment.Exit(0); } From 9d0de0230ee49889b2e7aec787e366f09ac585d8 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:31:27 +0100 Subject: [PATCH 21/27] started with categories --- Terminal/Arguments/ArgumentCategory.cs | 5 - Terminal/Arguments/ArgumentFormatter.cs | 284 ++++++++++++------------ 2 files changed, 143 insertions(+), 146 deletions(-) delete mode 100644 Terminal/Arguments/ArgumentCategory.cs diff --git a/Terminal/Arguments/ArgumentCategory.cs b/Terminal/Arguments/ArgumentCategory.cs deleted file mode 100644 index b3b34e1..0000000 --- a/Terminal/Arguments/ArgumentCategory.cs +++ /dev/null @@ -1,5 +0,0 @@ -// namespace OxDED.Terminal.Arguments; - -// public class ArgumentCategory { - -// } \ No newline at end of file diff --git a/Terminal/Arguments/ArgumentFormatter.cs b/Terminal/Arguments/ArgumentFormatter.cs index d5dba46..d3b6a45 100644 --- a/Terminal/Arguments/ArgumentFormatter.cs +++ b/Terminal/Arguments/ArgumentFormatter.cs @@ -3,7 +3,7 @@ namespace OxDED.Terminal.Arguments; /// /// Represents a format for arguments and options. /// -public partial class ArgumentFormatter { +public class ArgumentFormatter : IFormatContainer { /// /// Whether the program should quit when a parsing error occured. /// @@ -28,11 +28,17 @@ public partial class ArgumentFormatter { /// /// The arguments of this format. /// - public readonly List Arguments; + public readonly List Arguments; + + List IFormatContainer.Options => Options; + + List IFormatContainer.Arguments => Arguments; /// /// Creates a new argument format. /// + /// The predetermined arguments (optional). + /// The predetermined options (optional). public ArgumentFormatter(List? arguments = null, List? options = null) { Options = options ?? []; Arguments = arguments ?? []; @@ -122,178 +128,174 @@ public ArgumentParser Finish() { } } -public partial class ArgumentFormatter { +/// +/// Represents a format for an argument. +/// +public class ArgumentFormat { /// - /// Represents a format for an argument. + /// The description of this argument. Used by the help menu. /// - 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 ArgumentFormatter ArgumentFormatter; + public string description = ""; + + /// + /// The name of this argument. Used by the help menu. + /// + public string name = ""; + + /// + /// The parent format. + /// + public readonly IFormatContainer Container; - internal ArgumentFormat(ArgumentFormatter argumentFormatter) { - ArgumentFormatter = argumentFormatter; - } - /// - /// 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; - } + internal ArgumentFormat(IFormatContainer container) { + Container = container; + } + /// + /// 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 ArgumentFormatter Finish() { - ArgumentFormatter.Arguments.Add(this); - return ArgumentFormatter; - } + /// + /// Finishes and saves this argument format. + /// + /// The parent. + public ArgumentFormatter Finish() { + Container.Arguments.Add(this); + return ArgumentFormatter; } } -public partial class ArgumentFormatter { +/// +/// Represents a format for an option. +/// +public class OptionFormat { + /// + /// All the keys of this option. + /// + public readonly List keys = []; /// - /// Represents a format for an option. + /// The description of this argument. Used by the help menu. /// - public class OptionFormat { + public string description = ""; + /// + /// All the parameters of this option. + /// + public readonly List parameters = []; + + /// + /// The parent format. + /// + public readonly IFormatContainer Container; + + internal OptionFormat(IFormatContainer container) { + Container = container; + } + + /// + /// 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 ArgumentFormatter Finish() { + Container.Options.Add(this); + return ArgumentFormatter; + } + /// + /// Represents a format for an option's parameter. + /// + public class ParameterFormat { /// - /// All the keys of this option. + /// The name of this parameter. Used by the help menu. /// - public readonly List keys = []; + public string name = ""; /// /// 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 ArgumentFormatter ArgumentFormatter; + public readonly OptionFormat OptionFormat; - internal OptionFormat(ArgumentFormatter argumentFormatter) { - ArgumentFormatter = argumentFormatter; + internal ParameterFormat(OptionFormat optionBuilder) { + OptionFormat = optionBuilder; } /// - /// 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. + /// Sets the name of this parameter. /// - /// The new keys to add. - /// This option. - public virtual OptionFormat Keys(IEnumerable keys) { - this.keys.AddRange(keys); + /// The new name. + /// This parameter. + public ParameterFormat Name(string name) { + this.name = name; return this; } /// - /// Sets the description of this option. + /// Sets the description of this parameter. /// /// The new description. - /// This option. - public virtual OptionFormat Description(string description) { + /// This parameter. + public ParameterFormat 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. + /// Finishes and saves this parameter format. /// /// The parent. - public virtual ArgumentFormatter Finish() { - ArgumentFormatter.Options.Add(this); - return ArgumentFormatter; - } - /// - /// 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; - } + public OptionFormat Finish() { + OptionFormat.parameters.Add(this); + return OptionFormat; } } } @@ -301,7 +303,7 @@ public OptionFormat Finish() { /// /// Represents a configurable help option. /// -public class HelpOptionFormat : ArgumentFormatter.OptionFormat { +public class HelpOptionFormat : OptionFormat { internal HelpOptionFormat(ArgumentFormatter argumentFormatter) : base(argumentFormatter) { Keys(["h", "help"]).Description("Displays this help page."); } @@ -328,7 +330,7 @@ public override ArgumentFormatter Finish() { /// /// Represents a configurable version option. /// -public class VersionOptionFormat : ArgumentFormatter.OptionFormat { +public class VersionOptionFormat : OptionFormat { internal VersionOptionFormat(ArgumentFormatter argumentFormatter) : base(argumentFormatter) { Keys(["v", "version"]).Description("Displays the version and description of this program."); } @@ -347,7 +349,7 @@ public VersionOptionFormat Quit(bool quit) { } /// public override ArgumentFormatter Finish() { - ArgumentFormatter.CurrentVersionOption = this; + Container.CurrentVersionOption = this; return base.Finish(); } } \ No newline at end of file From d720607d116178caa45089176e94749f4970c845 Mon Sep 17 00:00:00 2001 From: dedouwe26 <63008025+dedouwe26@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:58:16 +0100 Subject: [PATCH 22/27] Add StyleBuilder #5 and bugfixes --- Terminal/Color.cs | 16 ++-- Terminal/Style.cs | 19 +++-- Terminal/StyleBuilder.cs | 158 +++++++++++++++++++++++++++++++++++++ Terminal/Terminal.csproj | 2 +- examples/Colors/Program.cs | 27 +++---- 5 files changed, 190 insertions(+), 32 deletions(-) create mode 100644 Terminal/StyleBuilder.cs 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/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