diff --git a/sommelier-x11event-test.cc b/sommelier-x11event-test.cc index f03ef5c..3e5a528 100644 --- a/sommelier-x11event-test.cc +++ b/sommelier-x11event-test.cc @@ -149,5 +149,87 @@ TEST_F(X11EventTest, PropertyNotifyStoresSteamId) { EXPECT_EQ(window->steam_game_id, steam_game_id); } +TEST_F(X11EventTest, MapRequestParsesSteamIdFromClass) { + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + const char clazz[] = "steam_app_7890\0steam_app_7890"; + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 8, sizeof(clazz), + clazz); + + xcb_map_request_event_t event; + event.response_type = XCB_MAP_REQUEST; + event.window = window->id; + sl_handle_map_request(&ctx, &event); + + EXPECT_EQ(window->steam_game_id, 7890); +} + +TEST_F(X11EventTest, MapRequestPrefersSteamGameIdOverClass) { + uint32_t steam_game_id = 123456; + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + const char clazz[] = "steam_app_7890\0steam_app_7890"; + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 8, sizeof(clazz), + clazz); + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + ctx.atoms[ATOM_STEAM_GAME].value, XCB_ATOM_CARDINAL, 32, + 1, &steam_game_id); + + // Act: map the window + xcb_map_request_event_t event; + event.response_type = XCB_MAP_REQUEST; + event.window = window->id; + sl_handle_map_request(&ctx, &event); + + // Assert: Uses the ID from the STEAM_GAME property, not from WM_CLASS + EXPECT_EQ(window->steam_game_id, steam_game_id); +} + +TEST_F(X11EventTest, PropertyNotifyParsesSteamIdFromClassAsFallback) { + uint32_t steam_game_id = 123456; + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + + // Act: set STEAM_GAME to one ID, and WM_CLASS to specify another + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + ctx.atoms[ATOM_STEAM_GAME].value, XCB_ATOM_CARDINAL, 32, + 1, &steam_game_id); + { + xcb_property_notify_event_t event; + event.response_type = XCB_PROPERTY_NOTIFY; + event.window = window->id; + event.atom = ctx.atoms[ATOM_STEAM_GAME].value; + event.state = XCB_PROPERTY_NEW_VALUE; + sl_handle_property_notify(&ctx, &event); + } + + const char clazz[] = "steam_app_7890\0steam_app_7890"; + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 8, sizeof(clazz), + clazz); + { + xcb_property_notify_event_t event; + event.response_type = XCB_PROPERTY_NOTIFY; + event.window = window->id; + event.atom = XCB_ATOM_WM_CLASS; + event.state = XCB_PROPERTY_NEW_VALUE; + sl_handle_property_notify(&ctx, &event); + } + + // Assert: STEAM_GAME is preferred, even though WM_CLASS was changed last. + EXPECT_EQ(window->steam_game_id, steam_game_id); +} + } // namespace sommelier } // namespace vm_tools diff --git a/sommelier.cc b/sommelier.cc index 46296b5..ebe31fe 100644 --- a/sommelier.cc +++ b/sommelier.cc @@ -12,6 +12,7 @@ #include "xcb/xcb-shim.h" #include +#include #include #include #include @@ -97,6 +98,8 @@ struct sl_data_source { #define MIN_AURA_SHELL_VERSION 6 #define MAX_AURA_SHELL_VERSION 38 +static const char STEAM_APP_CLASS_PREFIX[] = "steam_app_"; + int sl_open_wayland_socket(const char* socket_name, struct sockaddr_un* addr, int* lock_fd, @@ -1125,12 +1128,24 @@ static const char* sl_decode_wm_class(struct sl_window* window, // WM_CLASS property contains two consecutive null-terminated strings. // These specify the Instance and Class names. If a global app ID is // not set then use Class name for app ID. - const char* value = static_cast(xcb_get_property_value(reply)); - int value_length = xcb_get_property_value_length(reply); + const char* value = static_cast(xcb()->get_property_value(reply)); + int value_length = xcb()->get_property_value_length(reply); int instance_length = strnlen(value, value_length); if (value_length > instance_length) { window->clazz = strndup(value + instance_length + 1, value_length - instance_length - 1); + + if (!window->steam_game_id) { + // If there's no known Steam Game ID for this window, + // attempt to parse one from the class name. + if (strncmp(window->clazz, STEAM_APP_CLASS_PREFIX, + strlen(STEAM_APP_CLASS_PREFIX)) == 0) { + // atoi() returns 0 on error, in which case steam_game_id + // simply remains effectively unset. + window->steam_game_id = + atoi(window->clazz + strlen(STEAM_APP_CLASS_PREFIX)); + } + } return window->clazz; } return nullptr;