diff --git a/SrcMod/Shell/GlobalUsings.cs b/SrcMod/Shell/GlobalUsings.cs index 1d90052..a26ef43 100644 --- a/SrcMod/Shell/GlobalUsings.cs +++ b/SrcMod/Shell/GlobalUsings.cs @@ -3,6 +3,7 @@ global using SharpCompress.Archives.Rar; global using SharpCompress.Archives.SevenZip; global using SharpCompress.Readers; global using SrcMod.Shell; +global using SrcMod.Shell.Interop; global using SrcMod.Shell.Modules.ObjectModels; global using System; global using System.Collections.Generic; diff --git a/SrcMod/Shell/Interop/Winmm.cs b/SrcMod/Shell/Interop/Winmm.cs new file mode 100644 index 0000000..58a6d67 --- /dev/null +++ b/SrcMod/Shell/Interop/Winmm.cs @@ -0,0 +1,25 @@ +namespace SrcMod.Shell.Interop; + +internal static partial class Winmm +{ + [LibraryImport("winmm.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool PlaySound([MarshalAs(UnmanagedType.LPStr)] string pszSound, nint hMod, uint fdwSound); + + public enum PlaySoundFlags : uint + { + SND_SYNC = 0x00000000, + SND_ASYNC = 0x00000001, + SND_NODEFAULT = 0x00000002, + SND_MEMORY = 0x00000004, + SND_LOOP = 0x00000008, + SND_NOSTOP = 0x00000010, + SND_PURGE = 0x00000040, + SND_APPLICATION = 0x00000080, + SND_NOWAIT = 0x00002000, + SND_ALIAS = 0x00010000, + SND_FILENAME = 0x00020000, + SND_RESOURCE = 0x00040000, + SND_ALIAS_ID = 0x00100000, + } +} diff --git a/SrcMod/Shell/Modules/ClipboardModule.cs b/SrcMod/Shell/Modules/ClipboardModule.cs index d69b42b..14a01c2 100644 --- a/SrcMod/Shell/Modules/ClipboardModule.cs +++ b/SrcMod/Shell/Modules/ClipboardModule.cs @@ -1,6 +1,4 @@ -using SrcMod.Shell.Interop; - -namespace SrcMod.Shell.Modules; +namespace SrcMod.Shell.Modules; [Module("clipboard")] public static class ClipboardModule diff --git a/SrcMod/Shell/Shell.cs b/SrcMod/Shell/Shell.cs index e62b406..02c25e1 100644 --- a/SrcMod/Shell/Shell.cs +++ b/SrcMod/Shell/Shell.cs @@ -16,6 +16,9 @@ public class Shell public List History; public string WorkingDirectory; + private bool lastCancel; + private bool printedCancel; + public Shell() { Console.CursorVisible = false; @@ -79,6 +82,9 @@ public class Shell Write(" by ", ConsoleColor.White, false); Write($"{Author}", ConsoleColor.DarkYellow); + lastCancel = false; + Console.CancelKeyPress += HandleCancel; + ActiveGame = null; ReloadDirectoryInfo(); @@ -106,8 +112,6 @@ public class Shell public string ReadLine() { - Console.CursorVisible = true; - Write($"\n{WorkingDirectory}", ConsoleColor.DarkGreen, false); if (ActiveGame is not null) Write($" {ActiveGame}", ConsoleColor.DarkYellow, false); if (ActiveMod is not null) Write($" {ActiveMod}", ConsoleColor.Magenta, false); @@ -116,17 +120,55 @@ public class Shell Write($" {Name}", ConsoleColor.DarkCyan, false); Write(" > ", ConsoleColor.White, false); + bool printed = false; + + if (lastCancel && !printedCancel) + { + // Print the warning. A little bit of mess because execution must + // continue without funny printing errors but it's alright I guess. + + int originalLeft = Console.CursorLeft; + + Console.CursorTop -= 3; + Write("Press ^C again to exit the shell.", ConsoleColor.Red); + // Send a warning sound. + + Winmm.PlaySound("SystemAsterisk", nint.Zero, + (uint)(Winmm.PlaySoundFlags.SND_ALIAS | Winmm.PlaySoundFlags.SND_ASYNC)); + + printedCancel = true; + Console.CursorTop += 2; + + Console.CursorLeft = originalLeft; + printed = true; + } + Console.ForegroundColor = ConsoleColor.White; + Console.CursorVisible = true; string message = Console.ReadLine()!; + Console.CursorVisible = false; Console.ResetColor(); - Console.CursorVisible = false; + if (!printed) + { + lastCancel = false; + printedCancel = false; + } return message; } public void InvokeCommand(string cmd) { + if (cmd is null) + { + // This usually won't happen, but might if for example + // the shell cancel interrupt is called. This probably + // happens for other shell interrupts are called. + Write(null); + return; + } + List parts = new(); string active = string.Empty; @@ -205,4 +247,22 @@ public class Shell if (ActiveMod is not null) title += $" - {ActiveMod.Name}"; Console.Title = title; } + + private void HandleCancel(object? sender, ConsoleCancelEventArgs args) + { + // Due to some funny multithreading issues, we want to make the warning label + // single-threaded on the shell. + if (!lastCancel) + { + // Enable the warning. The "ReadLine" method will do the rest. + lastCancel = true; + args.Cancel = true; // "Cancel" referring to the cancellation of the cancel operation. + return; + } + + // Actually kill the shell. We do still have to worry about some multithreaded + // nonsense, but a bearable amount of it. + Console.ResetColor(); + Environment.Exit(0); + } }