diff --git a/src/BizHawk.Client.Common/ArgParser.cs b/src/BizHawk.Client.Common/ArgParser.cs index c7e8393ac56..0eae84f733c 100644 --- a/src/BizHawk.Client.Common/ArgParser.cs +++ b/src/BizHawk.Client.Common/ArgParser.cs @@ -117,6 +117,11 @@ public static class ArgParser Description = "pairs in the format `k1:v1;k2:v2` (mind your shell escape sequences); if the value is `true`/`false` it's interpreted as a boolean, if it's a valid 32-bit signed integer e.g. `-1234` it's interpreted as such, if it's a valid 32-bit float e.g. `12.34` it's interpreted as such, else it's interpreted as a string", }; + private static readonly Option OptionWMClass = new ("--wmclass") + { + Description = "set a custom WM_CLASS for this Bizhawk initiation, Linux only.", + }; + static ArgParser() { OptionAVDumpFrameList.Description = $"comma-separated list of integers, indices of frames which should be included in the A/V dump (encoding); implies `{OptionAVDumpEndAtFrame.Name}=` where `` is the highest frame listed"; @@ -162,6 +167,7 @@ private static RootCommand GetRootCommand() root.Add(/* --url-post */ OptionHTTPClientURIPOST); root.Add(/* --userdata */ OptionUserdataUnparsedPairs); root.Add(/* --version */ OptionQueryAppVersion); + root.Add(/* --wmclass */ OptionWMClass); return root; } @@ -268,7 +274,8 @@ private static void EnsureConsole() openExtToolDll: result.GetValue(OptionOpenExternalTool), socketProtocol: result.GetValue(OptionSocketServerUseUDP) ? ProtocolType.Udp : ProtocolType.Tcp, userdataUnparsedPairs: userdataUnparsedPairs, - cmdRom: result.GetValue(ArgumentRomFilePath) + cmdRom: result.GetValue(ArgumentRomFilePath), + wmClassName: result.GetValue(OptionWMClass) ); return null; } diff --git a/src/BizHawk.Client.Common/ParsedCLIFlags.cs b/src/BizHawk.Client.Common/ParsedCLIFlags.cs index 81dd4aa8b97..083bc8c01c2 100644 --- a/src/BizHawk.Client.Common/ParsedCLIFlags.cs +++ b/src/BizHawk.Client.Common/ParsedCLIFlags.cs @@ -51,6 +51,8 @@ public readonly struct ParsedCLIFlags public readonly string? cmdRom; + public readonly string? wmClassName; + public ParsedCLIFlags( int? cmdLoadSlot, string? cmdLoadState, @@ -73,7 +75,8 @@ public ParsedCLIFlags( string? openExtToolDll, ProtocolType socketProtocol, IReadOnlyList<(string Key, string Value)>? userdataUnparsedPairs, - string? cmdRom) + string? cmdRom, + string? wmClassName) { this.cmdLoadSlot = cmdLoadSlot; this.cmdLoadState = cmdLoadState; @@ -97,6 +100,7 @@ public ParsedCLIFlags( SocketProtocol = socketProtocol; UserdataUnparsedPairs = userdataUnparsedPairs; this.cmdRom = cmdRom; + this.wmClassName = wmClassName; } } } diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 13b08dd0b01..22bf715fe8d 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -295,6 +295,13 @@ private void MainForm_Load(object sender, EventArgs e) else Console.WriteLine($"requested ext. tool dll {requestedExtToolDll} could not be loaded"); } + //Code to set WM_CLASS + if(OSTailoredCode.CurrentOS==OSTailoredCode.DistinctOS.Linux) + { + string wmclass = _argParser.wmClassName is not null ? _argParser.wmClassName : "BizHawk"; + XlibImports.SetWMClass(_x11Display, wmclass); + } + #if DEBUG AddDebugMenu(); #endif diff --git a/src/BizHawk.Common/LSB/XlibImports.cs b/src/BizHawk.Common/LSB/XlibImports.cs index 23cdcbb297f..1df38aab24d 100644 --- a/src/BizHawk.Common/LSB/XlibImports.cs +++ b/src/BizHawk.Common/LSB/XlibImports.cs @@ -715,5 +715,61 @@ public struct XkbDescRec [DllImport(XLIB)] [return: MarshalAs(UnmanagedType.SysUInt)] public static extern Keysym XkbKeycodeToKeysym(IntPtr display, uint keycode, int group, int level); + + [DllImport(XLIB)] + public static extern IntPtr XAllocClassHint(); + + [DllImport(XLIB)] + public static extern Status XSetClassHint(IntPtr display, IntPtr window, IntPtr classHint); + + [DllImport(XLIB)] + public static extern Status XGetClassHint(IntPtr display, IntPtr window, out IntPtr classHint); + + public struct XClassHint + { + public IntPtr res_name; + public IntPtr res_class; + } + + [DllImport(XLIB)] + public static extern Status XQueryTree(IntPtr display, IntPtr window, out IntPtr rootReturn, out IntPtr parentReturn, out IntPtr[] childrenReturn, out int childCount); + [DllImport(XLIB)] + public static extern Status XFetchName(IntPtr display, IntPtr window, out string returnedName); + + public static void SetWMClass(IntPtr x11Display, string className) + { + //Establish reference to xDisplay + if(x11Display == IntPtr.Zero) + { + x11Display = XOpenDisplay(null); + } + //Setup the ClassHint + var wmSet = new XClassHint{res_name = Marshal.StringToCoTaskMemAnsi("BizHawk"), res_class = Marshal.StringToCoTaskMemAnsi(className)}; + IntPtr point = XAllocClassHint(); + Marshal.StructureToPtr(wmSet, point, true); + + //To find the window we must query twice, once to get the array length and the second to populate + XQueryTree(x11Display, XDefaultRootWindow(x11Display), out _, out _, out _, out int windowCount); + IntPtr[] windowList = new IntPtr[windowCount]; + XQueryTree(x11Display, XDefaultRootWindow(x11Display), out _, out _, out windowList, out windowCount); + for(int windowIterator = 0; windowIterator < windowCount; windowIterator++) + { + XFetchName(x11Display, windowList[windowIterator], out string name); + if(name?.Contains("BizHawk") == true) + { + //Interogate if a WM_CLASS is already set + XGetClassHint(x11Display, windowList[windowIterator], out var classHint); + if(classHint == IntPtr.Zero) + { + //Assign+Retrieve - doesn't seem to set properly without retrieving it after) + XSetClassHint(x11Display, windowList[windowIterator], point); + XGetClassHint(x11Display, windowList[windowIterator],out _); + } + //Reset classHint after checking, doesn't seem to overwrite otherwise. + classHint = IntPtr.Zero; + } + } + _ = XFlush(x11Display); + } } }