Change X11 window title

A typical use case is when an editor is setting its window title to the full long path name such that the actual important part, the file name, falls off the edge on the right and becomes invisible. Unfortunately the X11 window title is normally set internally by the application and its format is hard coded. A solution is to monitor the editor window title and modify it if its becomes too long.

There are command line tools (xprop, xdotool,etc.) that could be used in a bash script to query and modify X11 properties, but they are rather unwieldy to use. A short Python programme is probably a better solution:

sudo apt install python3-xlib
cat << EOF > ~/.local/bin/update_window_title_v1.0.py
#!/usr/bin/python3

"""
Rename X11 windows if they have too long names

Written by Z J Laczik

Modify the following defines to fine tune behaviour :
src_pattern = r"/.+/"
replacement = "/.../"
limit = 16
period = 2

N.B. Be careful modifying src_pattern and replacement!
Wrong choice may result in the modification of all window names, 
setting window names to '', or ending up with infinitely long
window names ptoentially crashing your window manager.

Change log

Date: 20220609	Version: 1.0
	First release
"""

import time
import re
from Xlib import display

# change debug to True to enable debug messages
debug = False
# debug = True

def get_window_name( window ) :
	# try and get utf-8 window name first, if that fails, try and get legacy name
	for ( atom, prop_type ) in ( ( NET_WM_NAME, UTF8_STRING), ( WM_NAME, STRING ) ) :
		try :
			prop = window.get_full_property( atom, prop_type )
		except UnicodeDecodeError :
			window_name = "<could not decode characters>"
		else :
			# check whether get_full_property was succesful
			# if not, set default name and try next atom
			if prop :
				# extract value data from property object
				window_name = prop.value
				# at this point window_name should be a string or a byte array
				# it it is bytes then convert to string
				if isinstance( window_name, bytes ) :
					if ( atom == NET_WM_NAME ) :
						# decode bytes as UTF-8
						window_name = window_name.decode( 'utf-8', 'replace' )
					else :
						# decode bytes as ASCII
						window_name = window_name.decode( 'ascii', 'replace' )
				return window_name
			else :
				# set default return value
				window_name = "<unnamed window>"
	# we should only get here if neither atoms worked, hence return default
	return window_name

def set_window_name( window, name ) :
	try :
		# set UTF-8 window name
		window.change_property( NET_WM_NAME, UTF8_STRING, 8, name.encode( 'utf-8', 'replace' ) )
		# set legacy ASCII name
		window.change_property( WM_NAME, STRING, 8, name.encode( 'ascii', 'replace' ) )
	except :
		pass

def check( window, src_pattern = r"/.+/", replacement = "/.../", limit = 16 ) :
	# get list of child windows
	window_list = window.query_tree().children
	# check each child window
	for window in window_list :
		# get current window name
		old_name = get_window_name( window )
		# check if length is above limit and if it matches replacement regex
		if ( len( old_name ) > limit ) :
			( new_name, matches ) = re.subn( src_pattern, replacement, old_name );
			if ( matches > 0 and new_name != old_name ) :
				set_window_name( window, new_name )
				if debug :
					print( "> Replaced '%s' with '%s'" % ( old_name, new_name ) )
		# recursively check child windows below current window
		check( window, src_pattern, replacement, limit )

# get display and root window
dsp = display.Display()
root_window = dsp.screen().root
# get various atoms
UTF8_STRING = dsp.get_atom( 'UTF8_STRING' )		# property type utf-8
STRING = dsp.get_atom( 'STRING' )				# property type string
NET_WM_NAME = dsp.get_atom( '_NET_WM_NAME' )	# utf-8 encoded window name
WM_NAME = dsp.get_atom( 'WM_NAME' )				# legacy ascii encoded window name

# define regex pattern to use for window name matching
src_pattern = r"/.+/"
# define replacement string
replacement = "/.../"
# length limit, replace window name only if longer than limit
limit = 16
# repeat check every 'period' seconds
period = 2

# start main loop checking all window names and
# changing them if there is a match and they are too long
while True :
	# check() starts with the root window and then recursively traverses all child windows
	check( root_window, src_pattern, replacement, limit )
	# wait until next check
	time.sleep( period )
EOF
chmod +x ~/.local/bin/update_window_title_v1.0.py
update_window_title_v1.0.py

The above Python code can also be downloaded here.

Examples for using the standard command line tools:

xprop | grep -i id
xprop | grep -i name
xdotool  set_window --name ZJL 79698557
xprop -format WM_NAME 8s -set WM_NAME "new name"
xprop -format _NET_WM_NAME 8u -set _NET_WM_NAME "new name"

These commands can also be handy when testing the Python code.

Leave a Reply

Your email address will not be published. Required fields are marked *