Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/BizHawk.Client.Common/ArgParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string?> OptionWMClass = new ("--wmclass")
{
Description = "set a custom WM_CLASS for this Bizhawk initiation, Linux only.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a CLI flag? When should a user be changing it? If it's for vendoring, we already have VersionInfo.CustomBuildString.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial reasoning for looking into adding WM_CLASS was to help improve BizHawk's integration with the DE (I use KDE). By defining a WM_CLASS we can hint StartupWMClass for example in .desktop files.
In allowing WM_CLASS to be set in a CLI arg it is then feasible to distinguish between different installs of Bizhawk (for example, a Kaizo Ironmon setup and a regular Bizhawk install)

This comment was marked as resolved.

This comment was marked as resolved.

@YoshiRulz YoshiRulz Jun 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I should really get around to making a textbox to set the window title thing. Then you could pull from that, and your .desktop files could pass --config. Or maybe it could use the rom name and you wouldn't need any extra flags?
edit: fbdb128

};

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}=<end>` where `<end>` is the highest frame listed";
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 5 additions & 1 deletion src/BizHawk.Client.Common/ParsedCLIFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public readonly struct ParsedCLIFlags

public readonly string? cmdRom;

public readonly string? wmClassName;

public ParsedCLIFlags(
int? cmdLoadSlot,
string? cmdLoadState,
Expand All @@ -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;
Expand All @@ -97,6 +100,7 @@ public ParsedCLIFlags(
SocketProtocol = socketProtocol;
UserdataUnparsedPairs = userdataUnparsedPairs;
this.cmdRom = cmdRom;
this.wmClassName = wmClassName;
}
}
}
10 changes: 10 additions & 0 deletions src/BizHawk.Client.EmuHawk/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,8 @@ private void CheckMayCloseAndCleanup(object/*?*/ closingSender, CancelEventArgs
/// </summary>
private bool _skipNextAltRelease = true;

private bool _runWMSetOnce = true;

public int ProgramRunLoop()
{
// needs to be done late, after the log console snaps on top
Expand Down Expand Up @@ -1015,6 +1017,14 @@ public int ProgramRunLoop()
Render();
StepRunLoop_Throttle();

//Code to set WM_CLASS only need be run once, but must be run after the application has rendered at least once
if(_runWMSetOnce && OSTailoredCode.CurrentOS==OSTailoredCode.DistinctOS.Linux)
Comment thread
Leopardly marked this conversation as resolved.
Outdated
{
string wmclass = _argParser.wmClassName is not null ? _argParser.wmClassName : "BizHawk";
OSTailoredCode.SetWMClass(_x11Display,wmclass);
Comment thread
Leopardly marked this conversation as resolved.
Outdated
_runWMSetOnce = false;
}

// HACK: RAIntegration might peek at memory during messages
// we need this to allow memory access here, otherwise it will deadlock
var raMemHack = (RA as RAIntegration)?.ThisIsTheRAMemHack();
Expand Down
20 changes: 20 additions & 0 deletions src/BizHawk.Common/LSB/XlibImports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -715,5 +715,25 @@ 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);
}
}
30 changes: 30 additions & 0 deletions src/BizHawk.Common/OSTailoredCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,5 +292,35 @@ public static string SimpleSubshell(string cmd, string args, string noOutputMsg)
if (stdout.EndOfStream) throw new Exception($"{noOutputMsg} ({cmd} wrote nothing to stdout)");
return stdout.ReadLine()!;
}

public static void SetWMClass(IntPtr x11Display, string className)
Comment thread
Leopardly marked this conversation as resolved.
Outdated
{
//Setup the ClassHint
var wmSet = new XlibImports.XClassHint{res_name = Marshal.StringToCoTaskMemAnsi("BizHawk"), res_class = Marshal.StringToCoTaskMemAnsi(className)};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why the same string needs to be passed twice?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WM_CLASS property is made up of two parts (name and class) - both need to be provided - they can be different/unique however it is somewhat common to see application name used in both fields (as I have here as the default behaviour).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incidentally, I saw this in Mono's issue tracker: https://gitlab.winehq.org/mono/mono/-/work_items/32

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I can't say I'd seen that happening (although I'm just on whatever mono is in Cachy's repos) - Picking through Keepass's implementation it looks like their is a neater way to be picking up the Window frame to assign WM_CLASS to - so I may take a look at that next week (away all weekend)

IntPtr point = XlibImports.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
XlibImports.XQueryTree(x11Display, XlibImports.XDefaultRootWindow(x11Display), out _, out _, out _, out int windowCount);
IntPtr[] windowList = new IntPtr[windowCount];
XlibImports.XQueryTree(x11Display, XlibImports.XDefaultRootWindow(x11Display), out _, out _, out windowList, out windowCount);
for(int windowIterator = 0; windowIterator < windowCount; windowIterator++)
{
XlibImports.XFetchName(x11Display, windowList[windowIterator], out string name);
if(name?.Contains("BizHawk") == true)
{
//Interogate if a WM_CLASS is already set
XlibImports.XGetClassHint(x11Display, windowList[windowIterator], out var classHint);
if(classHint == IntPtr.Zero)
{
//Assign+Retrieve - doesn't seem to set properly without retrieving it after)
XlibImports.XSetClassHint(x11Display, windowList[windowIterator], point);
XlibImports.XGetClassHint(x11Display, windowList[windowIterator],out _);
}
//Reset classHint after checking, doesn't seem to overwrite otherwise.
classHint = IntPtr.Zero;
}
}
}
}
}
Loading