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