diff --git a/SrcMod/Shell/GlobalUsings.cs b/SrcMod/Shell/GlobalUsings.cs
index dc25d52..13ff962 100644
--- a/SrcMod/Shell/GlobalUsings.cs
+++ b/SrcMod/Shell/GlobalUsings.cs
@@ -8,5 +8,7 @@ global using System.IO;
global using System.IO.Compression;
global using System.Linq;
global using System.Reflection;
+global using System.Runtime.InteropServices;
+global using System.Text;
global using System.Threading;
global using static SrcMod.Shell.Tools;
diff --git a/SrcMod/Shell/Interop/Kernel32.cs b/SrcMod/Shell/Interop/Kernel32.cs
new file mode 100644
index 0000000..751f71c
--- /dev/null
+++ b/SrcMod/Shell/Interop/Kernel32.cs
@@ -0,0 +1,11 @@
+namespace SrcMod.Shell.Interop;
+
+internal static partial class Kernel32
+{
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ public static partial uint GetFinalPathNameByHandleA(nint hFile, [MarshalAs(UnmanagedType.LPTStr)] string lpszFilePath,
+ uint cchFilePath, uint dwFlags);
+
+ [LibraryImport("kernel32.dll")]
+ public static partial nuint GlobalSize(nint hPtr);
+}
diff --git a/SrcMod/Shell/Interop/User32.cs b/SrcMod/Shell/Interop/User32.cs
new file mode 100644
index 0000000..b11f317
--- /dev/null
+++ b/SrcMod/Shell/Interop/User32.cs
@@ -0,0 +1,29 @@
+namespace SrcMod.Shell.Interop;
+
+internal static partial class User32
+{
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool CloseClipboard();
+
+ [LibraryImport("user32.dll")]
+ public static partial uint EnumClipboardFormats(uint uFormat);
+
+ [LibraryImport("user32.dll")]
+ public static partial nint GetClipboardData(uint uFormat);
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool IsClipboardFormatAvailable(uint uFormat);
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool OpenClipboard(nint hWndNewOwner);
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool EmptyClipboard();
+
+ [LibraryImport("user32.dll")]
+ public static partial nint SetClipboardData(uint uFormat, nint hMem);
+}
diff --git a/SrcMod/Shell/Modules/ClipboardModule.cs b/SrcMod/Shell/Modules/ClipboardModule.cs
new file mode 100644
index 0000000..d69b42b
--- /dev/null
+++ b/SrcMod/Shell/Modules/ClipboardModule.cs
@@ -0,0 +1,112 @@
+using SrcMod.Shell.Interop;
+
+namespace SrcMod.Shell.Modules;
+
+[Module("clipboard")]
+public static class ClipboardModule
+{
+ [Command("clear")]
+ public static void ClearClipboard()
+ {
+ if (!ValidateUnsafe()) return;
+
+ User32.OpenClipboard(0);
+ User32.EmptyClipboard();
+ User32.CloseClipboard();
+ }
+
+ [Command("copy")]
+ public static void CopyClipboard(string text)
+ {
+ const uint format = 1;
+
+ if (!ValidateUnsafe()) return;
+
+ if (!text.EndsWith("\0")) text += "\0";
+ byte[] data = Encoding.Default.GetBytes(text);
+
+ nint hGlobal = Marshal.AllocHGlobal(data.Length + 1);
+ Marshal.Copy(data, 0, hGlobal, data.Length);
+
+ User32.OpenClipboard(0);
+ User32.EmptyClipboard();
+
+ User32.SetClipboardData(format, hGlobal);
+
+ User32.CloseClipboard();
+ }
+
+ [Command("view")]
+ public static void ViewClipboard()
+ {
+ // TODO (maybe): Make this support other formats?
+ // I spent way too long trying to make that a reality
+ // but the whole "clipboard format" thing is a nightmare.
+
+ if (!ValidateUnsafe()) return;
+
+ User32.OpenClipboard(0);
+
+ nint hClipboard;
+ uint format;
+ if (User32.IsClipboardFormatAvailable(format = (uint)ClipboardFormat.DspText) ||
+ User32.IsClipboardFormatAvailable(format = (uint)ClipboardFormat.OemText) ||
+ User32.IsClipboardFormatAvailable(format = (uint)ClipboardFormat.Text) ||
+ User32.IsClipboardFormatAvailable(format = (uint)ClipboardFormat.UnicodeText))
+ hClipboard = User32.GetClipboardData(format);
+ else throw new("Clipboard doesn't contain text data.");
+
+ nuint length = Kernel32.GlobalSize(hClipboard);
+ byte[] data = new byte[length];
+
+ Marshal.Copy(hClipboard, data, 0, (int)length);
+
+ User32.CloseClipboard();
+
+ string msg = (ClipboardFormat)format switch
+ {
+ ClipboardFormat.DspText or ClipboardFormat.OemText or ClipboardFormat.Text =>
+ Encoding.UTF8.GetString(data),
+ ClipboardFormat.UnicodeText => Encoding.Unicode.GetString(data),
+ _ => throw new("Unknown text format.")
+ };
+
+ Write(msg);
+ }
+
+ public enum ClipboardFormat
+ {
+ Biff5 = 49988,
+ Biff8 = 49986,
+ Biff12 = 50009,
+ Bitmap = 2,
+ Csv = 49989,
+ DataObject = 49161,
+ Dib = 8,
+ Dif = 5,
+ DspText = 129,
+ EmbedSource = 49163,
+ EnhancedMetafile = 14,
+ HandleDrop = 15,
+ HtmlFormat = 49381,
+ Hyperlink = 50006,
+ Link = 49985,
+ LinkSource = 49165,
+ LinkSourceDescriptor = 49167,
+ Locale = 16,
+ Max = 17,
+ MetafilePicture = 3,
+ Native = 49156,
+ ObjectDescriptor = 49166,
+ ObjectLink = 49154,
+ OemText = 7,
+ OlePrivateData = 49171,
+ OwnerLink = 49155,
+ Palette = 9,
+ RichTextFormat = 49308,
+ SYLK = 4,
+ Text = 1,
+ UnicodeText = 13,
+ XmlSpreadSheet = 50007
+ }
+}
diff --git a/SrcMod/Shell/Shell.csproj b/SrcMod/Shell/Shell.csproj
index 0fdb552..511ed2f 100644
--- a/SrcMod/Shell/Shell.csproj
+++ b/SrcMod/Shell/Shell.csproj
@@ -12,6 +12,7 @@
That_One_Nerd
false
Logo.ico
+ true
diff --git a/SrcMod/Shell/Tools.cs b/SrcMod/Shell/Tools.cs
index 8610be6..0bbeb15 100644
--- a/SrcMod/Shell/Tools.cs
+++ b/SrcMod/Shell/Tools.cs
@@ -1,4 +1,6 @@
-namespace SrcMod.Shell;
+using System.Text;
+
+namespace SrcMod.Shell;
public static class Tools
{
@@ -137,4 +139,18 @@ public static class Tools
Console.ForegroundColor = prevCol;
}
+
+ public static bool ValidateUnsafe()
+ {
+ Write("You are about to execute an unsafe command.\nProceed? > ", ConsoleColor.DarkYellow, false);
+
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.CursorVisible = true;
+ string result = Console.ReadLine()!.Trim().ToLower();
+ Console.CursorVisible = false;
+ Console.ResetColor();
+
+ return result == "y" || result == "yes" || result == "t" ||
+ result == "true" || result == "p" || result == "proceed";
+ }
}