diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 18936687a..cc6f1b64c 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using CheapLoc; @@ -143,6 +144,13 @@ private DalamudCommands(CommandManager commandManager) HelpMessage = "ImGui DEBUG", ShowInHelp = false, }); + + commandManager.AddHandler("/xlcopylog", new CommandInfo(this.OnCopyLogCommand) + { + HelpMessage = Loc.Localize( + "DalamudCopyLogHelp", + "Copy the dalamud.log file to your clipboard."), + }); } private void OnUnloadCommand(string command, string arguments) @@ -406,4 +414,17 @@ private void OnOpenProfilerCommand(string command, string arguments) { Service.Get().ToggleProfilerWindow(); } + + private void OnCopyLogCommand(string command, string arguments) + { + var chatGui = Service.Get(); + var logPath = Path.Join( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "XIVLauncher", + "dalamud.log"); + var message = Util.CopyFilesToClipboard([logPath]) + ? Loc.Localize("DalamudLogCopySuccess", "Log file copied to clipboard.") + : Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard."); + chatGui.Print(message); + } } diff --git a/Dalamud/NativeMethods.txt b/Dalamud/NativeMethods.txt index c42e76c1c..d9d05e472 100644 --- a/Dalamud/NativeMethods.txt +++ b/Dalamud/NativeMethods.txt @@ -11,3 +11,13 @@ SetActiveWindow HWND_TOPMOST HWND_NOTOPMOST SET_WINDOW_POS_FLAGS + +OpenClipboard +SetClipboardData +CloseClipboard +DROPFILES +CLIPBOARD_FORMAT +GlobalAlloc +GlobalLock +GlobalUnlock +GLOBAL_ALLOC_FLAGS diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 3c3efada4..85769d2da 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -25,6 +25,11 @@ using Serilog; using TerraFX.Interop.Windows; using Windows.Win32.Storage.FileSystem; +using Windows.Win32.System.Memory; +using Windows.Win32.System.Ole; + +using HWND = Windows.Win32.Foundation.HWND; +using Win32_PInvoke = Windows.Win32.PInvoke; namespace Dalamud.Utility; @@ -476,12 +481,12 @@ public static OSPlatform GetHostPlatform() case "MacOS": return OSPlatform.OSX; case "Linux": return OSPlatform.Linux; } - + // n.b. we had some fancy code here to check if the Wine host version returned "Darwin" but apparently // *all* our Wines report Darwin if exports aren't hidden. As such, it is effectively impossible (without some // (very cursed and inaccurate heuristics) to determine if we're on macOS or Linux unless we're explicitly told // by our launcher. See commit a7aacb15e4603a367e2f980578271a9a639d8852 for the old check. - + return IsWine() ? OSPlatform.Linux : OSPlatform.Windows; } @@ -544,7 +549,7 @@ bool MoveNext() } } } - } + } finally { foreach (var enumerator in enumerators) @@ -585,7 +590,7 @@ public static void WriteAllTextSafe(string path, string text) { WriteAllTextSafe(path, text, Encoding.UTF8); } - + /// /// Overwrite text in a file by first writing it to a temporary file, and then /// moving that file to the path specified. @@ -597,7 +602,7 @@ public static void WriteAllTextSafe(string path, string text, Encoding encoding) { WriteAllBytesSafe(path, encoding.GetBytes(text)); } - + /// /// Overwrite data in a file by first writing it to a temporary file, and then /// moving that file to the path specified. @@ -607,13 +612,13 @@ public static void WriteAllTextSafe(string path, string text, Encoding encoding) public static unsafe void WriteAllBytesSafe(string path, byte[] bytes) { ArgumentException.ThrowIfNullOrEmpty(path); - + // Open the temp file var tempPath = path + ".tmp"; using var tempFile = Windows.Win32.PInvoke.CreateFile( - tempPath, - (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), + tempPath, + (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), FILE_SHARE_MODE.FILE_SHARE_NONE, null, FILE_CREATION_DISPOSITION.CREATE_ALWAYS, @@ -622,7 +627,7 @@ public static unsafe void WriteAllBytesSafe(string path, byte[] bytes) if (tempFile.IsInvalid) throw new Win32Exception(); - + // Write the data uint bytesWritten = 0; if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan(bytes), &bytesWritten, null)) @@ -633,7 +638,7 @@ public static unsafe void WriteAllBytesSafe(string path, byte[] bytes) if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile)) throw new Win32Exception(); - + tempFile.Close(); if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) @@ -736,6 +741,69 @@ internal static void PrintGameObject(GameObject actor, string tag, bool resolveG } } + /// + /// Copy files to the clipboard as if they were copied in Explorer. + /// + /// Full paths to files to be copied. + /// Returns true on success. + internal static unsafe bool CopyFilesToClipboard(IEnumerable paths) + { + var pathBytes = paths + .Select(Encoding.Unicode.GetBytes) + .ToArray(); + var pathBytesSize = pathBytes + .Select(bytes => bytes.Length) + .Sum(); + var sizeWithTerminators = pathBytesSize + (pathBytes.Length * 2); + + var dropFilesSize = sizeof(DROPFILES); + var hGlobal = Win32_PInvoke.GlobalAlloc_SafeHandle( + GLOBAL_ALLOC_FLAGS.GHND, + // struct size + size of encoded strings + null terminator for each + // string + two null terminators for end of list + (uint)(dropFilesSize + sizeWithTerminators + 4)); + var dropFiles = (DROPFILES*)Win32_PInvoke.GlobalLock(hGlobal); + + *dropFiles = default; + dropFiles->fWide = true; + dropFiles->pFiles = (uint)dropFilesSize; + + var pathLoc = (byte*)((nint)dropFiles + dropFilesSize); + foreach (var bytes in pathBytes) + { + // copy the encoded strings + for (var i = 0; i < bytes.Length; i++) + { + pathLoc![i] = bytes[i]; + } + + // null terminate + pathLoc![bytes.Length] = 0; + pathLoc[bytes.Length + 1] = 0; + pathLoc += bytes.Length + 2; + } + + // double null terminator for end of list + for (var i = 0; i < 4; i++) + { + pathLoc![i] = 0; + } + + Win32_PInvoke.GlobalUnlock(hGlobal); + + if (Win32_PInvoke.OpenClipboard(HWND.Null)) + { + Win32_PInvoke.SetClipboardData( + (uint)CLIPBOARD_FORMAT.CF_HDROP, + hGlobal); + Win32_PInvoke.CloseClipboard(); + return true; + } + + hGlobal.Dispose(); + return false; + } + private static void ShowSpanProperty(ulong addr, IList path, PropertyInfo p, object obj) { var objType = obj.GetType(); @@ -750,7 +818,7 @@ private static void ShowSpanProperty(ulong addr, IList path, PropertyInf "-", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, - null, + null, new[] { typeof(object), typeof(IList), typeof(ulong) }, obj.GetType(), true); @@ -907,7 +975,7 @@ private static unsafe void ShowValue(ulong addr, IList path, Type type, } } } - + /// /// Show a structure in an ImGui context. ///