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(s): 52 d = {} 53 for line in s.split("\n"): 54 fields = line.split(":") 55 if len(fields) < 2: 56 continue 57 key, value = fields[0].strip(), ":".join(fields[1:]).strip() 58 d[key] = value 59 return d 60 61 def _get_int_properties(d, properties): 62 results = [] 63 for property in properties: 64 results.append(int(d[property])) 65 return results 66 67 # Finder functions. 68 69 def find_all(name): 70 return 1 71 72 def find_named(name): 73 return name is not None 74 75 def find_by_name(name): 76 return lambda n, t=name: n == t 77 78 # Window classes. 79 # NOTE: X11 is the only supported desktop so far. 80 81 class Window: 82 83 "A window on the desktop." 84 85 _name_pattern = re.compile(r':\s+\(.*?\)\s+[-0-9x+]+\s+[-0-9+]+$') 86 _absent_names = "(has no name)", "(the root window) (has no name)" 87 88 def __init__(self, identifier): 89 90 "Initialise the window with the given 'identifier'." 91 92 self.identifier = identifier 93 94 # Finder methods (from above). 95 96 self.find_all = find_all 97 self.find_named = find_named 98 self.find_by_name = find_by_name 99 100 def __repr__(self): 101 return "Window(%r)" % self.identifier 102 103 # Methods which deal with the underlying commands. 104 105 def _get_identifier_args(self): 106 if self.identifier is None: 107 return "-root" 108 else: 109 return "-id " + self.identifier 110 111 def _get_command_output(self, action): 112 return _readfrom(_get_x11_vars() + "xwininfo %s -%s" % (self._get_identifier_args(), action), shell=1) 113 114 def _get_handle_and_name(self, text): 115 fields = text.strip().split(" ") 116 handle = fields[0] 117 118 # Get the "<name>" part, stripping off the quotes. 119 120 name = " ".join(fields[1:]) 121 if len(name) > 1 and name[0] == '"' and name[-1] == '"': 122 name = name[1:-1] 123 124 if name in self._absent_names: 125 return handle, None 126 else: 127 return handle, name 128 129 def _get_this_handle_and_name(self, line): 130 fields = line.split(":") 131 return self._get_handle_and_name(":".join(fields[2:])) 132 133 def _get_descendant_handle_and_name(self, line): 134 match = self._name_pattern.search(line) 135 if match: 136 return self._get_handle_and_name(line[:match.start()].strip()) 137 else: 138 raise OSError, "Window information from %r did not contain window details." % line 139 140 def _descendants(self, s, fn): 141 handles = [] 142 adding = 0 143 for line in s.split("\n"): 144 if line.endswith("child:") or line.endswith("children:"): 145 if not adding: 146 adding = 1 147 elif adding and line: 148 handle, name = self._get_descendant_handle_and_name(line) 149 if fn(name): 150 handles.append(handle) 151 return [Window(handle) for handle in handles] 152 153 # Public methods. 154 155 def children(self, all=0): 156 157 """ 158 Return a list of windows which are children of this window. If the 159 optional 'all' parameter is set to a true value, all such windows will 160 be returned regardless of whether they have any name information. 161 """ 162 163 s = self._get_command_output("children") 164 return self._descendants(s, all and self.find_all or self.find_named) 165 166 def descendants(self, all=0): 167 168 """ 169 Return a list of windows which are descendants of this window. If the 170 optional 'all' parameter is set to a true value, all such windows will 171 be returned regardless of whether they have any name information. 172 """ 173 174 s = self._get_command_output("tree") 175 return self._descendants(s, all and self.find_all or self.find_named) 176 177 def find(self, callable): 178 179 """ 180 Return windows using the given 'callable' (returning a true or a false 181 value when invoked with a window name) for descendants of this window. 182 """ 183 184 s = self._get_command_output("tree") 185 return self._descendants(s, callable) 186 187 def name(self): 188 189 "Return the name of the window." 190 191 s = self._get_command_output("stats") 192 for line in s.split("\n"): 193 if line.startswith("xwininfo:"): 194 195 # Format is 'xwininfo: Window id: <handle> "<name>" 196 197 return self._get_this_handle_and_name(line)[1] 198 199 return None 200 201 def size(self): 202 203 "Return a tuple containing the width and height of this window." 204 205 s = self._get_command_output("stats") 206 d = _xwininfo(s) 207 return _get_int_properties(d, ["Width", "Height"]) 208 209 def position(self): 210 211 "Return a tuple containing the upper left co-ordinates of this window." 212 213 s = self._get_command_output("stats") 214 d = _xwininfo(s) 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 s = self._get_command_output("stats") 225 d = _xwininfo(s) 226 return d["Map State"] != "IsUnviewable" 227 228 def visible(self): 229 230 "Return whether the window is displayed and visible." 231 232 s = self._get_command_output("stats") 233 d = _xwininfo(s) 234 return d["Map State"] == "IsViewable" 235 236 def list(desktop=None): 237 238 """ 239 Return a list of windows for the current desktop. If the optional 'desktop' 240 parameter is specified then attempt to use that particular desktop 241 environment's mechanisms to look for windows. 242 """ 243 244 root_window = root(desktop) 245 window_list = [window for window in root_window.descendants() if window.displayed()] 246 window_list.insert(0, root_window) 247 return window_list 248 249 def root(desktop=None): 250 251 """ 252 Return the root window for the current desktop. If the optional 'desktop' 253 parameter is specified then attempt to use that particular desktop 254 environment's mechanisms to look for windows. 255 """ 256 257 # NOTE: The desktop parameter is currently ignored and X11 is tested for 258 # NOTE: directly. 259 260 if _is_x11(): 261 return Window(None) 262 else: 263 raise OSError, "Desktop '%s' not supported" % use_desktop(desktop) 264 265 def find(callable, desktop=None): 266 267 """ 268 Find and return windows using the given 'callable' for the current desktop. 269 If the optional 'desktop' parameter is specified then attempt to use that 270 particular desktop environment's mechanisms to look for windows. 271 """ 272 273 return root(desktop).find(callable) 274 275 # vim: tabstop=4 expandtab shiftwidth=4