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