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