1 #!/usr/bin/env python 2 3 """ 4 Simple desktop window enumeration for Python. 5 6 Copyright (C) 2007, 2008, 2009 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU Lesser General Public License as published by the Free 10 Software Foundation; either version 3 of the License, or (at your option) any 11 later version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 16 details. 17 18 You should have received a copy of the GNU Lesser General Public License along 19 with this program. If not, see <http://www.gnu.org/licenses/>. 20 21 -------- 22 23 Finding Open Windows on the Desktop 24 ----------------------------------- 25 26 To obtain a list of windows, use the desktop.windows.list function as follows: 27 28 windows = desktop.windows.list() 29 30 To obtain the root window, typically the desktop background, use the 31 desktop.windows.root function as follows: 32 33 root = desktop.windows.root() 34 35 Each window object can be inspected through a number of methods. For example: 36 37 name = window.name() 38 width, height = window.size() 39 x, y = window.position() 40 child_windows = window.children() 41 42 See the desktop.windows.Window class for more information. 43 """ 44 45 from desktop import _is_x11, _get_x11_vars, _readfrom, use_desktop 46 import re 47 48 # System functions. 49 50 def _xwininfo(identifier, action): 51 if identifier is None: 52 args = "-root" 53 else: 54 args = "-id " + identifier 55 56 s = _readfrom(_get_x11_vars() + "xwininfo %s -%s" % (args, action), shell=1) 57 58 # Return a mapping of keys to values for the "stats" action. 59 60 if action == "stats": 61 d = {} 62 for line in s.split("\n"): 63 fields = line.split(":") 64 if len(fields) < 2: 65 continue 66 key, value = fields[0].strip(), ":".join(fields[1:]).strip() 67 d[key] = value 68 69 return d 70 71 # Otherwise, return the raw output. 72 73 else: 74 return s 75 76 def _get_int_properties(d, properties): 77 results = [] 78 for property in properties: 79 results.append(int(d[property])) 80 return results 81 82 # Finder functions. 83 84 def find_all(name): 85 return 1 86 87 def find_named(name): 88 return name is not None 89 90 def find_by_name(name): 91 return lambda n, t=name: n == t 92 93 # Window classes. 94 # NOTE: X11 is the only supported desktop so far. 95 96 class Window: 97 98 "A window on the desktop." 99 100 _name_pattern = re.compile(r':\s+\(.*?\)\s+[-0-9x+]+\s+[-0-9+]+$') 101 _absent_names = "(has no name)", "(the root window) (has no name)" 102 103 def __init__(self, identifier): 104 105 "Initialise the window with the given 'identifier'." 106 107 self.identifier = identifier 108 109 # Finder methods (from above). 110 111 self.find_all = find_all 112 self.find_named = find_named 113 self.find_by_name = find_by_name 114 115 def __repr__(self): 116 return "Window(%r)" % self.identifier 117 118 # Methods which deal with the underlying commands. 119 120 def _get_handle_and_name(self, text): 121 fields = text.strip().split(" ") 122 handle = fields[0] 123 124 # Get the "<name>" part, stripping off the quotes. 125 126 name = " ".join(fields[1:]) 127 if len(name) > 1 and name[0] == '"' and name[-1] == '"': 128 name = name[1:-1] 129 130 if name in self._absent_names: 131 return handle, None 132 else: 133 return handle, name 134 135 def _get_this_handle_and_name(self, line): 136 fields = line.split(":") 137 return self._get_handle_and_name(":".join(fields[1:])) 138 139 def _get_descendant_handle_and_name(self, line): 140 match = self._name_pattern.search(line) 141 if match: 142 return self._get_handle_and_name(line[:match.start()].strip()) 143 else: 144 raise OSError, "Window information from %r did not contain window details." % line 145 146 def _descendants(self, s, fn): 147 handles = [] 148 adding = 0 149 for line in s.split("\n"): 150 if line.endswith("child:") or line.endswith("children:"): 151 if not adding: 152 adding = 1 153 elif adding and line: 154 handle, name = self._get_descendant_handle_and_name(line) 155 if fn(name): 156 handles.append(handle) 157 return [Window(handle) for handle in handles] 158 159 # Public methods. 160 161 def children(self, all=0): 162 163 """ 164 Return a list of windows which are children of this window. If the 165 optional 'all' parameter is set to a true value, all such windows will 166 be returned regardless of whether they have any name information. 167 """ 168 169 s = _xwininfo(self.identifier, "children") 170 return self._descendants(s, all and self.find_all or self.find_named) 171 172 def descendants(self, all=0): 173 174 """ 175 Return a list of windows which are descendants of this window. If the 176 optional 'all' parameter is set to a true value, all such windows will 177 be returned regardless of whether they have any name information. 178 """ 179 180 s = _xwininfo(self.identifier, "tree") 181 return self._descendants(s, all and self.find_all or self.find_named) 182 183 def find(self, callable): 184 185 """ 186 Return windows using the given 'callable' (returning a true or a false 187 value when invoked with a window name) for descendants of this window. 188 """ 189 190 s = _xwininfo(self.identifier, "tree") 191 return self._descendants(s, callable) 192 193 def name(self): 194 195 "Return the name of the window." 196 197 d = _xwininfo(self.identifier, "stats") 198 199 # Format is 'xwininfo: Window id: <handle> "<name>" 200 201 return self._get_this_handle_and_name(d["xwininfo"])[1] 202 203 def size(self): 204 205 "Return a tuple containing the width and height of this window." 206 207 d = _xwininfo(self.identifier, "stats") 208 return _get_int_properties(d, ["Width", "Height"]) 209 210 def position(self): 211 212 "Return a tuple containing the upper left co-ordinates of this window." 213 214 d = _xwininfo(self.identifier, "stats") 215 return _get_int_properties(d, ["Absolute upper-left X", "Absolute upper-left Y"]) 216 217 def displayed(self): 218 219 """ 220 Return whether the window is displayed in some way (but not necessarily 221 visible on the current screen). 222 """ 223 224 d = _xwininfo(self.identifier, "stats") 225 return d["Map State"] != "IsUnviewable" 226 227 def visible(self): 228 229 "Return whether the window is displayed and visible." 230 231 d = _xwininfo(self.identifier, "stats") 232 return d["Map State"] == "IsViewable" 233 234 def list(desktop=None): 235 236 """ 237 Return a list of windows for the current desktop. If the optional 'desktop' 238 parameter is specified then attempt to use that particular desktop 239 environment's mechanisms to look for windows. 240 """ 241 242 root_window = root(desktop) 243 window_list = [window for window in root_window.descendants() if window.displayed()] 244 window_list.insert(0, root_window) 245 return window_list 246 247 def root(desktop=None): 248 249 """ 250 Return the root window for the current desktop. If the optional 'desktop' 251 parameter is specified then attempt to use that particular desktop 252 environment's mechanisms to look for windows. 253 """ 254 255 # NOTE: The desktop parameter is currently ignored and X11 is tested for 256 # NOTE: directly. 257 258 if _is_x11(): 259 return Window(None) 260 else: 261 raise OSError, "Desktop '%s' not supported" % use_desktop(desktop) 262 263 def find(callable, desktop=None): 264 265 """ 266 Find and return windows using the given 'callable' for the current desktop. 267 If the optional 'desktop' parameter is specified then attempt to use that 268 particular desktop environment's mechanisms to look for windows. 269 """ 270 271 return root(desktop).find(callable) 272 273 # vim: tabstop=4 expandtab shiftwidth=4