├── README.md └── pxdo.py /README.md: -------------------------------------------------------------------------------- 1 | Python script for querying X-server information and manipulating X-windows 2 | 3 | # Actions 4 | 5 | ## --move-\-\x\+\+\ 6 | 7 | move an X window to the specified position 8 | 9 | ## --print-window-info 10 | 11 | print information on X windows 12 | format: ` : ` 13 | separated by tabs 14 | 15 | ## --print-active-window-info 16 | 17 | print information on the active X window 18 | 19 | ## --print-active-window-id 20 | 21 | print the id of the active X window 22 | 23 | ## --print-monitor-info 24 | 25 | print information on active monitors 26 | format: `<name> <id> <geometry>` 27 | separated by tabs 28 | 29 | ## --version 30 | 31 | show version 32 | 33 | 34 | # Dependencies 35 | 36 | -python-xlib (can be out of date in some distro repos - see [#2](https://github.com/xhsdf/pxdo/issues/2)) 37 | -------------------------------------------------------------------------------- /pxdo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | 4 | NAME = "pxdo" 5 | VERSION = "0.13" 6 | 7 | 8 | from sys import exc_info, argv 9 | from re import match, compile 10 | from Xlib.display import Display 11 | from Xlib.error import BadWindow 12 | from Xlib.ext.randr import get_screen_resources, get_output_info 13 | from Xlib.protocol.event import ClientMessage 14 | from Xlib.X import AnyPropertyType, SubstructureRedirectMask, SubstructureNotifyMask 15 | 16 | display = None 17 | MOVE_REGEX = '--move-(0x[0-9a-fA-F]+)-(\\d+)x(\\d+)\\+(\\d+)\\+(\\d+)' 18 | 19 | def main(argv): 20 | globals()['display'] = Display() 21 | 22 | for arg in argv: 23 | try: 24 | if arg == '--version': 25 | print("%s v%s" % (NAME, VERSION)) 26 | elif arg == '--print-active-window-id': 27 | print("0x%x" % Window.get_active_window_id()) 28 | elif arg == '--print-active-window-info': 29 | print_active_window_info() 30 | elif arg == '--print-window-info': 31 | print_window_info() 32 | elif arg == '--print-monitor-info': 33 | print_monitor_info() 34 | elif match(MOVE_REGEX, arg): 35 | p = compile(MOVE_REGEX) 36 | wid, width, height, x, y = p.findall(arg)[0] 37 | w = Window.from_id(int(wid, 16), False) 38 | w.move(int(x), int(y), int(width), int(height)) 39 | except: 40 | if '--debug' in argv: 41 | raise 42 | else: 43 | print("Unexpected error:", exc_info()[0]) 44 | 45 | display.sync() 46 | display.flush() 47 | display.close() 48 | 49 | 50 | def print_window_info(): 51 | window_ids = Window.get_window_ids() 52 | for w in Window.get_windows(): 53 | if w.type == None or display.intern_atom("_NET_WM_WINDOW_TYPE_NORMAL") in w.type: 54 | print("WINDOW: " + w.get_info_string()) 55 | 56 | 57 | def print_monitor_info(): 58 | for monitor in Monitor.get_monitors(): 59 | print("MONITOR: " + monitor.get_info_string()) 60 | 61 | 62 | def print_active_window_info(): 63 | w = Window.from_id(int(Window.get_active_window_id())) 64 | w.active = True 65 | print("WINDOW: " + w.get_info_string()) 66 | 67 | class Monitor: 68 | def __init__(self, mon_id, x, y, width, height): 69 | self.id = mon_id 70 | self.x, self.y, self.width, self.height = x, y, width, height 71 | self.name = "" 72 | 73 | 74 | # <name> <id> <geometry> 75 | def get_info_string(self): 76 | return "%s\t%d\t%dx%d+%d+%d" % (self.name, self.id, self.width, self.height, self.x, self.y) 77 | 78 | 79 | @staticmethod 80 | def get_monitors(): 81 | resources = get_screen_resources(display.screen().root) 82 | monitors = {} 83 | for crtc in resources.crtcs: 84 | crtc_info = display.xrandr_get_crtc_info(crtc, resources.config_timestamp) 85 | for output_id in crtc_info.outputs: 86 | monitor = Monitor(output_id, crtc_info.x, crtc_info.y, crtc_info.width, crtc_info.height) 87 | monitors[output_id] = monitor 88 | 89 | output_ids = resources.outputs 90 | for monitor in monitors.values(): 91 | output_info = get_output_info(display.screen().root, monitor.id, 0) 92 | monitor.name = output_info.name 93 | return monitors.values() 94 | 95 | 96 | class Window: 97 | def __init__(self, win, win_id, full = True): 98 | self.win = win 99 | self.id = win_id 100 | self.active = False 101 | self.type = None 102 | self.name = self.wm_class = "Unknown" 103 | self.workspace = -1 104 | self.hidden = False 105 | self.fullscreen = False 106 | self.allowed_actions = [] 107 | self.x, self.y, self.width, self.height = 0, 0, 0, 0 108 | self.extents_left, self.extents_right, self.extents_top, self.extents_bottom = 0, 0, 0, 0 109 | extents = self.get_property('_NET_FRAME_EXTENTS') 110 | if extents != None: 111 | self.extents_left, self.extents_right, self.extents_top, self.extents_bottom = extents 112 | if full: 113 | self.name = self.get_property('WM_NAME') 114 | if self.name is None: 115 | self.name = 'unknown' 116 | else: 117 | self.name = self.name.decode("ascii", errors='ignore').replace("\t", " ") 118 | self.workspace = int(self.get_property('_NET_WM_DESKTOP')[0]) 119 | self.allowed_actions = self.get_property('_NET_WM_ALLOWED_ACTIONS') 120 | self.type = self.get_property('_NET_WM_WINDOW_TYPE') 121 | try: 122 | self.state = self.get_property('_NET_WM_STATE') 123 | if display.intern_atom("_NET_WM_STATE_HIDDEN") in self.state: 124 | self.hidden = True 125 | if display.intern_atom("_NET_WM_STATE_FULLSCREEN") in self.state: 126 | self.fullscreen = True 127 | except: 128 | pass 129 | if win.get_wm_class() != None: 130 | self.wm_class = win.get_wm_class()[1] 131 | geometry = win.query_tree().parent.get_geometry() 132 | self.x = geometry.x 133 | self.y = geometry.y 134 | self.width = geometry.width 135 | self.height = geometry.height 136 | 137 | 138 | @staticmethod 139 | def get_windows(): 140 | windows = [] 141 | active_window_id = Window.get_active_window_id() 142 | for id in Window.get_window_ids(): 143 | try: 144 | w = Window.from_id(int(id)) 145 | if id == active_window_id: 146 | w.active = True 147 | if w.allowed_actions == None: 148 | if w.type != None and display.intern_atom("_NET_WM_WINDOW_TYPE_NORMAL") in w.type: 149 | windows.append(w) 150 | elif display.intern_atom("_NET_WM_ACTION_MOVE") in w.allowed_actions or display.intern_atom("_NET_WM_ACTION_RESIZE") in w.allowed_actions: 151 | windows.append(w) 152 | except BadWindow: 153 | pass 154 | return windows 155 | 156 | 157 | @staticmethod 158 | def from_id(wid, full = True): 159 | return Window(display.create_resource_object('window', wid), wid, full) 160 | 161 | 162 | # <id> <geometry> <frame_extents> <window_workspace>:<current_workspace> <tags> <class> <title> 163 | def get_info_string(self): 164 | tags = "," 165 | if self.hidden: 166 | tags += "hidden," 167 | if self.fullscreen: 168 | tags += "fullscreen," 169 | if self.active: 170 | tags += "active," 171 | current_workspace = Window.get_current_workspace(); 172 | return "0x%x\t%s\t%s\t%d:%d\t%s\t%s\t%s" % (self.id, "%dx%d+%d+%d" % (self.width, self.height, self.x, self.y), "%d,%d,%d,%d" % (self.extents_top, self.extents_bottom, self.extents_left, self.extents_right), self.workspace, current_workspace, tags, self.wm_class, self.name) 173 | 174 | 175 | def move(self, x, y, width, height): 176 | window = self.win 177 | 178 | width -= self.extents_left + self.extents_right 179 | height -= self.extents_top + self.extents_bottom 180 | 181 | flags = 0 | 0b0000000010000000 # ??? 182 | flags = flags | 0b0000000100000000 # x 183 | flags = flags | 0b0000001000000000 # y 184 | flags = flags | 0b0000010000000000 # w 185 | flags = flags | 0b0000100000000000 # h 186 | wm_normal_hints = window.get_wm_normal_hints() 187 | if wm_normal_hints != None: 188 | wm_normal_hints["width_inc"] = 0 189 | wm_normal_hints["height_inc"] = 0 190 | window.set_wm_normal_hints(wm_normal_hints) 191 | self.set_property('_NET_MOVERESIZE_WINDOW', [flags, int(x), int(y), int(width), int(height)]) 192 | 193 | 194 | def get_property(self, prop): 195 | atom = self.win.get_full_property(display.intern_atom(prop), AnyPropertyType) 196 | if atom: 197 | return atom.value 198 | else: 199 | return None 200 | 201 | 202 | def set_property(self, prop, data, mask=None): 203 | if isinstance(data, str): 204 | datasize = 8 205 | else: 206 | data = (data+[0]*(5-len(data)))[:5] 207 | datasize = 32 208 | 209 | e = ClientMessage(window=self.win, client_type=display.intern_atom(prop), data=(datasize, data)) 210 | 211 | if not mask: 212 | mask = (SubstructureRedirectMask | SubstructureNotifyMask) 213 | display.screen().root.send_event(e, event_mask=mask) 214 | 215 | 216 | @staticmethod 217 | def get_current_workspace(): 218 | return Window(display.screen().root, 0, False).get_property('_NET_CURRENT_DESKTOP')[0] 219 | 220 | 221 | @staticmethod 222 | def get_active_window_id(): 223 | return Window(display.screen().root, 0, False).get_property('_NET_ACTIVE_WINDOW')[0] 224 | 225 | 226 | @staticmethod 227 | def get_window_ids(): 228 | return Window(display.screen().root, 0, False).get_property('_NET_CLIENT_LIST_STACKING') 229 | 230 | 231 | if __name__ == "__main__": 232 | main(argv[1:]) 233 | --------------------------------------------------------------------------------