#if 0 #// Various window management fixes for the Linux Steam client. #// #// You can set the following environment variables to 0 to disable individual features: #// #// STEAMWM_FORCE_BORDERS Force borders on non-menu windows. #// #// STEAMWM_PREVENT_MOVE Let the WM position non-menu/tooltip windows. #// #// STEAMWM_GROUP_WINDOWS Group all steam windows. #// This helps WMs with their focus stealing preventions, #// and also prevents all Steam windows from being dimmed #// (by KWin) if any Steam window has focus (is a KWin setting). #// NOTE: Window is still dimmed when showing menus/tooltips :( #// #// STEAMWM_SET_WINDOW_TYPE Tell the WM which Steam windows are dialogs. #// This lets the window manager place them more intelligently. #// For example, the WM might center dialogs. #// NOTE: We simply treat every window with a title other than #// "Steam" or "Friends" as a dialog window. #// The startup window is also marked as a dialog. #// #// STEAMWM_MANAGE_ERRORS Steam sets error dialogs as unmanaged windows - fix that. #// #// #// Obsolete fixes (now disabled by default): #// #// STEAMWM_FIX_NET_WM_NAME Set _NET_WM_NAME to the WM_NAME value to get better window #// titles (and add " - Steam" suffix if needed). #// Steam now doesn't set WM_ICON_NAME, _NET_WM_NAME or #// _NET_WM_ICON_NAME anymore - while it would be better to set #// them, their absence is unlikely to cause problems. #// #// STEAMWM_SET_FIXED_SIZE Set fixed size hints for windows with a fixed layout. #// #// #// Requires: g++ with support for x86 targets, Xlib + headers #// #// #// Use: #// $ chmod +x steamwm.cpp #// and then #// #// #// $ DEBUGGER="$(pwd)/steamwm.cpp" steam #// #// *or* #// #// $ ./steamwm.cpp steam // Prints ld.so errors on 64-bit systems #// #// *or* #// #// $ ./steamwm.cpp // Compile #// $ LD_PRELOAD="$(pwd)/steamwm.so" steam // Prints ld.so errors on 64-bit systems #// #// #// DISCLAIMER: Use at your own risk! This is in no way endorsed by VALVE. #// #// This program is free software. It comes without any warranty, to #// the extent permitted by applicable law. You can redistribute it #// and/or modify it under the terms of the Do What The Fuck You Want #// To Public License, Version 2, as published by Sam Hocevar. See #// http://sam.zoy.org/wtfpl/COPYING for more details. #// [ -z $STEAMWM_FORCE_BORDERS ] && export STEAMWM_FORCE_BORDERS=1 [ -z $STEAMWM_PREVENT_MOVE ] && export STEAMWM_PREVENT_MOVE=1 [ -z $STEAMWM_FIX_NET_WM_NAME ] && export STEAMWM_FIX_NET_WM_NAME=0 [ -z $STEAMWM_GROUP_WINDOWS ] && export STEAMWM_GROUP_WINDOWS=1 [ -z $STEAMWM_SET_WINDOW_TYPE ] && export STEAMWM_SET_WINDOW_TYPE=1 [ -z $STEAMWM_SET_FIXED_SIZE ] && export STEAMWM_SET_FIXED_SIZE=0 [ -z $STEAMWM_MANAGE_ERRORS ] && export STEAMWM_MANAGE_ERRORS=1 self="$(readlink -f "$(which "$0")")" name="$(basename "$self" .cpp)" out="$(dirname "$self")/$name" soname="$name.so" #// On amd64 platforms, compile a dummy 64-bit steamwm.so, #// so that native 64-bit tools invoked by Steam and its #// launch script won't spam (harmless) ld.so errors. if [ -f '/lib64/ld-linux-x86-64.so.2' ] ; then dout="$out/64" mkdir -p "$dout" if ! [ -f "$dout/$soname" ] ; then echo -e "\n" | gcc -shared -fPIC -m64 -x c - -o "$dout/$soname" &> /dev/null strip "$dout/$soname" &> /dev/null #// ignore all errors - this may at worst cause warnings later fi export LD_LIBRARY_PATH="$dout:$LD_LIBRARY_PATH" fi #// Compile the LD_PRELOAD library mkdir -p "$out" if [ "$self" -nt "$out/$soname" ] ; then echo "Compiling $soname..." g++ -shared -fPIC -m32 -x c++ "$self" -o "$out/$soname" \ -lX11 -static-libgcc -static-libstdc++ \ -O3 -Wall -Wextra -DSONAME="$soname" \ || exit 1 fi #// Run the executable export LD_PRELOAD="$soname:$LD_PRELOAD" export LD_LIBRARY_PATH="$out:$LD_LIBRARY_PATH" [ -z "$1" ] || exec "$@" exit #endif // 0 #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #include #include #include #include #include #include #define STR_(x) # x #define STR(x) STR_(x) // List of window titles for windows that should not be marked as dialogs static const char * main_windows[] = { "Steam", "Friends", }; // List of window titles for windows that should be dialogs even if unmanaged static const char * dialog_windows[] = { "Untitled", "Steam - Go Online", }; static const char * fixed_size_windows[] = { "Settings", "About Steam", "Backup and Restore Programs", "Steam - Error", "Steam - Self Updater", }; static const char * fixed_size_suffixes[] = { " - Properties", " - Category", }; static bool force_borders = false; static bool prevent_move = false; static bool fix_net_wm_name = false; static bool group_windows = false; static bool set_window_type = false; static bool set_fixed_size = false; static bool manage_errors = false; extern "C" { extern char * program_invocation_short_name; // provided by glibc } static bool get_setting(const char * name) { char * setting = getenv(name); return (setting && setting[0] != '\0' && setting[0] != '0'); } void steamwm_init(void) __attribute__((constructor)); void steamwm_init(void) { // Only attach to steam! if(strcmp(program_invocation_short_name, "steam") != 0) { return; } // Prevent steamwm.so from being attached to processes started by steam const char * envname = "LD_PRELOAD"; const char * oldenv = getenv(envname); if(oldenv) { char * env = strdup(oldenv); char * pos = strstr(env, STR(SONAME)); if(pos) { size_t len1 = strlen(STR(SONAME)); size_t len2 = strlen(pos + len1); memmove(pos, pos + len1, len2); *(pos + len2) = '\0'; setenv(envname, env, 1); } free(env); } force_borders = get_setting("STEAMWM_FORCE_BORDERS"); prevent_move = get_setting("STEAMWM_PREVENT_MOVE"); fix_net_wm_name = get_setting("STEAMWM_FIX_NET_WM_NAME"); group_windows = get_setting("STEAMWM_GROUP_WINDOWS"); set_window_type = get_setting("STEAMWM_SET_WINDOW_TYPE"); set_fixed_size = get_setting("STEAMWM_SET_FIXED_SIZE"); manage_errors = get_setting("STEAMWM_MANAGE_ERRORS"); fprintf(stderr, "\n[steamwm] attached to steam:\n force_borders %d\n" " prevent_move %d\n fix_net_wm_name %d\n" " group_windows %d\n set_window_type %d\n" " set_fixed_size %d\n manage_errors %d\n\n", int(force_borders), int(prevent_move), int(fix_net_wm_name), int(group_windows), int(set_window_type), int(set_fixed_size), int(manage_errors)); } /* helper functions */ #define BASE_NAME(SymbolName) base_ ## SymbolName #define TYPE_NAME(SymbolName) SymbolName ## _t #define INTERCEPT(ReturnType, SymbolName, ...) \ typedef ReturnType (*TYPE_NAME(SymbolName))(__VA_ARGS__); \ static void * const BASE_NAME(SymbolName) = dlsym(RTLD_NEXT, STR(SymbolName)); \ ReturnType SymbolName(__VA_ARGS__) #define BASE(SymbolName) ((TYPE_NAME(SymbolName))BASE_NAME(SymbolName)) static bool is_unmanaged_window(Display * dpy, Window w); static void set_is_unmanaged_window(Display * dpy, Window w, bool is_unmanaged); static bool is_fixed_size_window_name(const char * name); static bool is_main_window_name(const char * name); static bool is_dialog_window_name(const char * name); static void set_window_desired_size(Display * dpy, Window w, int width, int height, bool set_fixed); static void set_window_property(Display * dpy, Window w, Atom property, Atom type, long value); static void set_window_group_hint(Display * dpy, Window w, XID window_group); static void set_window_is_dialog(Display * dpy, Window w, bool is_dialog); static void set_window_modal(Display * dpy, Window w); /* fix window titles and types, and add window borders & title bars */ static Window first_window = None, second_window = None; static void name_changed(Display * dpy, Window w, const unsigned char * data, int n); INTERCEPT(void, XSetWMName, Display * dpy, Window w, XTextProperty * prop ) { if(prop->format == 8) { // The libX11 pulled in with STEAM_RUNTIME=1 has XSetWMName (or XSetTextProperty?) // liked/optimized to use internal functions and not the global XChangeProperty, // so our override below won't work -> also intercept XSetWMName(). name_changed(dpy, w, prop->value, prop->nitems); } return BASE(XSetWMName)(dpy, w, prop); } INTERCEPT(int, XChangeProperty, Display * dpy, Window w, Atom property, Atom type, int format, int mode, const unsigned char * data, int n ) { if(property == XA_WM_NAME && format == 8) { name_changed(dpy, w, data, n); } if(manage_errors && property == XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False)) { if(!is_unmanaged_window(dpy, w) && type == XA_ATOM && format == 32 && n > 0) { Atom menu = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_MENU", False); Atom type = reinterpret_cast(data)[0]; if(type == menu) { // Ignore the window type Steam sets on error dialogs. return 1; } } } if(force_borders && property == XInternAtom(dpy, "_MOTIF_WM_HINTS", False)) { // Don't suppress window borders! return 1; } return BASE(XChangeProperty)(dpy, w, property, type, format, mode, data, n); } static void name_changed(Display * dpy, Window w, const unsigned char * data, int n) { char * value = (char *)data; if(fix_net_wm_name) { // Use the XA_WM_NAME as both XA_WM_NAME and _NET_WM_NAME. // Steam sets _NET_WM_NAME to just "Steam" for all windows. const unsigned char * name = data; unsigned char * buffer = NULL; int nn = n; if(n > 0 && strstr((char *)data, "Steam") == 0) { // Make sure "Steam" is in all window titles. char suffix[] = " - Steam"; nn = n + sizeof(suffix) - 1; name = buffer = (unsigned char *)malloc(nn + 1); memcpy(buffer, data, n); memcpy(buffer + n, suffix, sizeof(suffix)); } Atom net_wm_name = XInternAtom(dpy, "_NET_WM_NAME", False); Atom utf8_string = XInternAtom(dpy, "UTF8_STRING", False); BASE(XChangeProperty)(dpy, w, net_wm_name, utf8_string, 8, PropModeReplace, name, nn); if(buffer) { free(buffer); } } if(manage_errors && is_unmanaged_window(dpy, w) && is_dialog_window_name(value)) { // Error dialogs should be managed by the window manager. set_is_unmanaged_window(dpy, w, false); set_window_modal(dpy, w); set_window_desired_size(dpy, w, -1, -1, true); } if(set_window_type && !is_unmanaged_window(dpy, w) && w != first_window && w != second_window) { // Set the window type for non-menu windows. // This should probably be done *before* mapping the windows, // but then we don't have a title yet. // Try to guess the window type from the title. set_window_is_dialog(dpy, w, !is_main_window_name(value)); } if(set_fixed_size && is_fixed_size_window_name(value)) { // Set fixed size hints for windows with static layouts. set_window_desired_size(dpy, w, -1, -1, true); } } /* ignore window move requests for non-menu windows */ INTERCEPT(int, XResizeWindow, Display * dpy, Window w, unsigned int width, unsigned int height ) { if(set_fixed_size || manage_errors) { // Set fixed size hints for windows with static layouts. set_window_desired_size(dpy, w, width, height, false); } return BASE(XResizeWindow)(dpy, w, width, height); } INTERCEPT(int, XMoveResizeWindow, Display * dpy, Window w, int x, int y, unsigned int width, unsigned int height ) { if(set_fixed_size || manage_errors) { // Set fixed size hints for windows with static layouts. set_window_desired_size(dpy, w, width, height, false); } if(prevent_move && !is_unmanaged_window(dpy, w)) { // Ignore the position request for non-menu windows. return BASE(XResizeWindow)(dpy, w, width, height); } return BASE(XMoveResizeWindow)(dpy, w, x, y, width, height); } INTERCEPT(int, XMoveWindow, Display * dpy, Window w, int x, int y ) { if(prevent_move && !is_unmanaged_window(dpy, w)) { // Ignore the position request for non-menu windows. return 1; } return BASE(XMoveWindow)(dpy, w, x, y); } /* group windows and force the first and second window to be dialogs */ INTERCEPT(int, XMapWindow, Display * dpy, Window w ) { if(first_window == None) { first_window = w; } if(group_windows) { // Group all steam windows. Atom leader = XInternAtom(dpy, "WM_CLIENT_LEADER", False); set_window_property(dpy, w, leader, XA_WINDOW, first_window); set_window_group_hint(dpy, w, first_window); } if(set_window_type && (w == first_window || second_window == None)) { // Force the first and second windows to be marked as dialogs. set_window_is_dialog(dpy, w, true); if(w != first_window) { // Give the second window a proper size *now* so that the WM can center it. second_window = w; XResizeWindow(dpy, w, 384, 107); } } return BASE(XMapWindow)(dpy, w); } INTERCEPT(void, XSetWMNormalHints, Display * dpy, Window w, XSizeHints * hints ) { XSizeHints old_hints; long supplied; // Prevent steam from overriding the max size hints if((set_fixed_size || manage_errors) && XGetWMNormalHints(dpy, w, &old_hints, &supplied)) { if(!(hints->flags & PSize) && (old_hints.flags & PSize)) { hints->flags |= PSize; hints->width = old_hints.width; hints->height = old_hints.height; } if((old_hints.flags & PMinSize) && (old_hints.flags & PMaxSize) && old_hints.min_width == old_hints.max_width && old_hints.min_height == old_hints.max_height) { hints->flags |= PMinSize | PMaxSize; hints->min_width = hints->max_width = old_hints.max_width; hints->min_height = hints->max_height = old_hints.max_height; } } BASE(XSetWMNormalHints)(dpy, w, hints); } /* helper function implementations */ static bool is_unmanaged_window(Display * dpy, Window w) { XWindowAttributes xwa; if(!XGetWindowAttributes(dpy, w, &xwa)) { return false; } return xwa.override_redirect; } static void set_is_unmanaged_window(Display * dpy, Window w, bool is_unmanaged) { XSetWindowAttributes xswa; xswa.override_redirect = is_unmanaged; XChangeWindowAttributes(dpy, w, CWOverrideRedirect, &xswa); } template static bool is_in_array(const char * (&array)[N], const char * needle) { for(unsigned i = 0; i < N; i++) { if(strcmp(needle, array[i]) == 0) { return true; } } return false; } static bool is_main_window_name(const char * name) { return is_in_array(main_windows, name); } static bool is_dialog_window_name(const char * name) { return is_in_array(dialog_windows, name); } static bool is_fixed_size_window_name(const char * name) { if(is_in_array(fixed_size_windows, name)) { return true; } int len = strlen(name); for(unsigned i = 0; i < sizeof(fixed_size_suffixes)/sizeof(*fixed_size_suffixes); i++) { int plen = strlen(fixed_size_suffixes[i]); if(len > plen && strcmp(name + len - plen, fixed_size_suffixes[i]) == 0) { return true; } } return false; } static void set_window_desired_size(Display * dpy, Window w, int width, int height, bool set_fixed) { XSizeHints xsh; long supplied; if(!XGetWMNormalHints(dpy, w, &xsh, &supplied)) { xsh.flags = 0; } if(width > 0 && height > 0) { // Store the desired size. // PBaseSize (base_width and base_height) sounds more appropriate // (and is not obsolete), but some window managers won't let the window // get smaller than the base size. xsh.flags |= PSize; xsh.width = width, xsh.height = height; } else if(xsh.flags & PSize) { // Retrieve the desired size. width = xsh.width, height = xsh.height; } else { Window root; int x, y; unsigned int cwidth, cheight, border_width, depth; if(!XGetGeometry(dpy, w, &root, &x, &y, &cwidth, &cheight, &border_width, &depth)) { return; } width = cwidth, height = cheight; } if(set_fixed || ((xsh.flags & PMinSize) && (xsh.flags & PMaxSize) && xsh.min_width == xsh.max_width && xsh.min_height == xsh.max_height)) { xsh.flags |= PMinSize | PMaxSize; xsh.min_width = xsh.max_width = width; xsh.min_height = xsh.max_height = height; } BASE(XSetWMNormalHints)(dpy, w, &xsh); } static void set_window_property(Display * dpy, Window w, Atom property, Atom type, long value) { unsigned char data[sizeof(long)]; memcpy(&data, &value, sizeof(long)); BASE(XChangeProperty)(dpy, w, property, type, 32, PropModeReplace, data, 1); } static void set_window_group_hint(Display * dpy, Window w, XID window_group) { XWMHints base_hints; XWMHints * h = XGetWMHints(dpy, w); if(!h) { h = &base_hints; h->flags = 0; } h->flags |= WindowGroupHint; h->window_group = window_group; XSetWMHints(dpy, w, h); if(h != &base_hints) { XFree(h); } } static void set_window_is_dialog(Display * dpy, Window w, bool is_dialog) { Atom window_type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); if(is_dialog) { Atom dialog = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); set_window_property(dpy, w, window_type, XA_ATOM, dialog); } else { Atom normal = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_NORMAL", False); set_window_property(dpy, w, window_type, XA_ATOM, normal); } } static void set_window_modal(Display * dpy, Window w) { XWindowAttributes xwa; if(!XGetWindowAttributes(dpy, w, &xwa)) { return; } Atom state = XInternAtom(dpy, "_NET_WM_STATE", False); Atom state_modal = XInternAtom(dpy, "_NET_WM_STATE_MODAL", False); if(xwa.map_state == IsUnmapped) { set_window_property(dpy, w, state, XA_ATOM, state_modal); } else { XEvent event; event.type = ClientMessage; event.xclient.message_type = state; event.xclient.window = w; event.xclient.format = 32; event.xclient.data.l[0] = 1; // add event.xclient.data.l[1] = state_modal; event.xclient.data.l[2] = 0; event.xclient.data.l[3] = 1; event.xclient.data.l[4] = 0; XSendEvent(dpy, DefaultRootWindow(dpy), False, (SubstructureNotifyMask | SubstructureRedirectMask), &event); } }