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