1import re 2import sys 3from .model import Rect, Gaps 4from . import replies 5from collections import deque 6from typing import List, Optional 7 8 9class Con: 10 """A container of a window and child containers gotten from :func:`i3ipc.Connection.get_tree()` or events. 11 12 .. seealso:: https://i3wm.org/docs/ipc.html#_tree_reply 13 14 :ivar border: 15 :vartype border: str 16 :ivar current_border_width: 17 :vartype current_border_with: int 18 :ivar floating: 19 :vartype floating: bool 20 :ivar focus: The focus stack for this container as a list of container ids. 21 The "focused inactive" is at the top of the list which is the container 22 that would be focused if this container recieves focus. 23 :vartype focus: list(int) 24 :ivar focused: 25 :vartype focused: bool 26 :ivar fullscreen_mode: 27 :vartype fullscreen_mode: int 28 :ivar ~.id: 29 :vartype ~.id: int 30 :ivar layout: 31 :vartype layout: str 32 :ivar marks: 33 :vartype marks: list(str) 34 :ivar name: 35 :vartype name: str 36 :ivar num: 37 :vartype num: int 38 :ivar orientation: 39 :vartype orientation: str 40 :ivar percent: 41 :vartype percent: float 42 :ivar scratchpad_state: 43 :vartype scratchpad_state: str 44 :ivar sticky: 45 :vartype sticky: bool 46 :ivar type: 47 :vartype type: str 48 :ivar urgent: 49 :vartype urgent: bool 50 :ivar window: 51 :vartype window: int 52 :ivar nodes: 53 :vartype nodes: list(:class:`Con <i3ipc.Con>`) 54 :ivar floating_nodes: 55 :vartype floating_nodes: list(:class:`Con <i3ipc.Con>`) 56 :ivar window_class: 57 :vartype window_class: str 58 :ivar window_instance: 59 :vartype window_instance: str 60 :ivar window_role: 61 :vartype window_role: str 62 :ivar window_title: 63 :vartype window_title: str 64 :ivar rect: 65 :vartype rect: :class:`Rect <i3ipc.Rect>` 66 :ivar window_rect: 67 :vartype window_rect: :class:`Rect <i3ipc.Rect>` 68 :ivar deco_rect: 69 :vartype deco_rect: :class:`Rect <i3ipc.Rect>` 70 :ivar geometry: 71 :vartype geometry: :class:`Rect <i3ipc.Rect>` 72 :ivar app_id: (sway only) 73 :vartype app_id: str 74 :ivar pid: (sway only) 75 :vartype pid: int 76 :ivar gaps: (gaps only) 77 :vartype gaps: :class:`Gaps <i3ipc.Gaps>` 78 :ivar representation: (sway only) 79 :vartype representation: str 80 :ivar visible: (sway only) 81 :vartype visible: bool 82 83 :ivar ipc_data: The raw data from the i3 ipc. 84 :vartype ipc_data: dict 85 """ 86 def __init__(self, data, parent, conn): 87 self.ipc_data = data 88 self._conn = conn 89 self.parent = parent 90 91 # set simple properties 92 ipc_properties = [ 93 'border', 'current_border_width', 'floating', 'focus', 'focused', 'fullscreen_mode', 94 'id', 'layout', 'marks', 'name', 'num', 'orientation', 'percent', 'scratchpad_state', 95 'sticky', 'type', 'urgent', 'window', 'pid', 'app_id', 'representation' 96 ] 97 for attr in ipc_properties: 98 if attr in data: 99 setattr(self, attr, data[attr]) 100 else: 101 setattr(self, attr, None) 102 103 # XXX in 4.12, marks is an array (old property was a string "mark") 104 if self.marks is None: 105 self.marks = [] 106 if 'mark' in data and data['mark']: 107 self.marks.append(data['mark']) 108 109 # XXX this is for compatability with 4.8 110 if isinstance(self.type, int): 111 if self.type == 0: 112 self.type = "root" 113 elif self.type == 1: 114 self.type = "output" 115 elif self.type == 2 or self.type == 3: 116 self.type = "con" 117 elif self.type == 4: 118 self.type = "workspace" 119 elif self.type == 5: 120 self.type = "dockarea" 121 122 # set complex properties 123 self.nodes = [] 124 if 'nodes' in data: 125 for n in data['nodes']: 126 self.nodes.append(self.__class__(n, self, conn)) 127 128 self.floating_nodes = [] 129 if 'floating_nodes' in data: 130 for n in data['floating_nodes']: 131 self.floating_nodes.append(self.__class__(n, self, conn)) 132 133 self.window_class = None 134 self.window_instance = None 135 self.window_role = None 136 self.window_title = None 137 if 'window_properties' in data: 138 if 'class' in data['window_properties']: 139 self.window_class = data['window_properties']['class'] 140 if 'instance' in data['window_properties']: 141 self.window_instance = data['window_properties']['instance'] 142 if 'window_role' in data['window_properties']: 143 self.window_role = data['window_properties']['window_role'] 144 if 'title' in data['window_properties']: 145 self.window_title = data['window_properties']['title'] 146 147 self.rect = Rect(data['rect']) 148 if 'window_rect' in data: 149 self.window_rect = Rect(data['window_rect']) 150 151 self.deco_rect = None 152 if 'deco_rect' in data: 153 self.deco_rect = Rect(data['deco_rect']) 154 155 self.geometry = None 156 if 'geometry' in data: 157 self.geometry = Rect(data['geometry']) 158 159 self.gaps = None 160 if 'gaps' in data: 161 self.gaps = Gaps(data['gaps']) 162 163 def __iter__(self): 164 """Iterate through the descendents of this node (breadth-first tree traversal) 165 """ 166 queue = deque(self.nodes) 167 queue.extend(self.floating_nodes) 168 169 while queue: 170 con = queue.popleft() 171 yield con 172 queue.extend(con.nodes) 173 queue.extend(con.floating_nodes) 174 175 def root(self) -> 'Con': 176 """Gets the root container. 177 178 :returns: The root container. 179 :rtype: :class:`Con` 180 """ 181 182 if not self.parent: 183 return self 184 185 con = self.parent 186 187 while con.parent: 188 con = con.parent 189 190 return con 191 192 def descendants(self) -> List['Con']: 193 """Gets a list of all child containers for the container in 194 breadth-first order. 195 196 :returns: A list of descendants. 197 :rtype: list(:class:`Con`) 198 """ 199 return [c for c in self] 200 201 def descendents(self) -> List['Con']: 202 """Gets a list of all child containers for the container in 203 breadth-first order. 204 205 .. deprecated:: 2.0.1 206 Use :func:`descendants` instead. 207 208 :returns: A list of descendants. 209 :rtype: list(:class:`Con`) 210 """ 211 print('WARNING: descendents is deprecated. Use `descendants()` instead.', file=sys.stderr) 212 return self.descendants() 213 214 def leaves(self) -> List['Con']: 215 """Gets a list of leaf child containers for this container in 216 breadth-first order. Leaf containers normally contain application 217 windows. 218 219 :returns: A list of leaf descendants. 220 :rtype: list(:class:`Con`) 221 """ 222 leaves = [] 223 224 for c in self: 225 if not c.nodes and c.type == "con" and c.parent.type != "dockarea": 226 leaves.append(c) 227 228 return leaves 229 230 def command(self, command: str) -> List[replies.CommandReply]: 231 """Runs a command on this container. 232 233 .. seealso:: https://i3wm.org/docs/userguide.html#list_of_commands 234 235 :returns: A list of replies for each command in the given command 236 string. 237 :rtype: list(:class:`CommandReply <i3ipc.CommandReply>`) 238 """ 239 return self._conn.command('[con_id="{}"] {}'.format(self.id, command)) 240 241 def command_children(self, command: str) -> List[replies.CommandReply]: 242 """Runs a command on the immediate children of the currently selected 243 container. 244 245 .. seealso:: https://i3wm.org/docs/userguide.html#list_of_commands 246 247 :returns: A list of replies for each command that was executed. 248 :rtype: list(:class:`CommandReply <i3ipc.CommandReply>`) 249 """ 250 if not len(self.nodes): 251 return 252 253 commands = [] 254 for c in self.nodes: 255 commands.append('[con_id="{}"] {};'.format(c.id, command)) 256 257 self._conn.command(' '.join(commands)) 258 259 def workspaces(self) -> List['Con']: 260 """Gets a list of workspace containers for this tree. 261 262 :returns: A list of workspace containers. 263 :rtype: list(:class:`Con`) 264 """ 265 workspaces = [] 266 267 def collect_workspaces(con): 268 if con.type == "workspace" and not con.name.startswith('__'): 269 workspaces.append(con) 270 return 271 272 for c in con.nodes: 273 collect_workspaces(c) 274 275 collect_workspaces(self.root()) 276 return workspaces 277 278 def find_focused(self) -> Optional['Con']: 279 """Finds the focused container under this container if it exists. 280 281 :returns: The focused container if it exists. 282 :rtype: :class:`Con` or :class:`None` if the focused container is not 283 under this container 284 """ 285 try: 286 return next(c for c in self if c.focused) 287 except StopIteration: 288 return None 289 290 def find_by_id(self, id: int) -> Optional['Con']: 291 """Finds a container with the given container id under this node. 292 293 :returns: The container with this container id if it exists. 294 :rtype: :class:`Con` or :class:`None` if there is no container with 295 this container id. 296 """ 297 try: 298 return next(c for c in self if c.id == id) 299 except StopIteration: 300 return None 301 302 def find_by_pid(self, pid: int) -> List['Con']: 303 """Finds all the containers under this node with this pid. 304 305 :returns: A list of containers with this pid. 306 :rtype: list(:class:`Con`) 307 """ 308 return [c for c in self if c.pid == pid] 309 310 def find_by_window(self, window: int) -> Optional['Con']: 311 """Finds a container with the given window id under this node. 312 313 :returns: The container with this window id if it exists. 314 :rtype: :class:`Con` or :class:`None` if there is no container with 315 this window id. 316 """ 317 try: 318 return next(c for c in self if c.window == window) 319 except StopIteration: 320 return None 321 322 def find_by_role(self, pattern: str) -> List['Con']: 323 """Finds all the containers under this node with a window role that 324 matches the given regex pattern. 325 326 :returns: A list of containers that have a window role that matches the 327 pattern. 328 :rtype: list(:class:`Con`) 329 """ 330 return [c for c in self if c.window_role and re.search(pattern, c.window_role)] 331 332 def find_named(self, pattern: str) -> List['Con']: 333 """Finds all the containers under this node with a name that 334 matches the given regex pattern. 335 336 :returns: A list of containers that have a name that matches the 337 pattern. 338 :rtype: list(:class:`Con`) 339 """ 340 return [c for c in self if c.name and re.search(pattern, c.name)] 341 342 def find_titled(self, pattern: str) -> List['Con']: 343 """Finds all the containers under this node with a window title that 344 matches the given regex pattern. 345 346 :returns: A list of containers that have a window title that matches 347 the pattern. 348 :rtype: list(:class:`Con`) 349 """ 350 return [c for c in self if c.window_title and re.search(pattern, c.window_title)] 351 352 def find_classed(self, pattern: str) -> List['Con']: 353 """Finds all the containers under this node with a window class that 354 matches the given regex pattern. 355 356 :returns: A list of containers that have a window class that matches the 357 pattern. 358 :rtype: list(:class:`Con`) 359 """ 360 return [c for c in self if c.window_class and re.search(pattern, c.window_class)] 361 362 def find_instanced(self, pattern: str) -> List['Con']: 363 """Finds all the containers under this node with a window instance that 364 matches the given regex pattern. 365 366 :returns: A list of containers that have a window instance that matches the 367 pattern. 368 :rtype: list(:class:`Con`) 369 """ 370 return [c for c in self if c.window_instance and re.search(pattern, c.window_instance)] 371 372 def find_marked(self, pattern: str = ".*") -> List['Con']: 373 """Finds all the containers under this node with a mark that 374 matches the given regex pattern. 375 376 :returns: A list of containers that have a mark that matches the 377 pattern. 378 :rtype: list(:class:`Con`) 379 """ 380 pattern = re.compile(pattern) 381 return [c for c in self if any(pattern.search(mark) for mark in c.marks)] 382 383 def find_fullscreen(self) -> List['Con']: 384 """Finds all the containers under this node that are in fullscreen 385 mode. 386 387 :returns: A list of fullscreen containers. 388 :rtype: list(:class:`Con`) 389 """ 390 return [c for c in self if c.type == 'con' and c.fullscreen_mode] 391 392 def workspace(self) -> Optional['Con']: 393 """Finds the workspace container for this node if this container is at 394 or below the workspace level. 395 396 :returns: The workspace container if it exists. 397 :rtype: :class:`Con` or :class:`None` if this container is above the 398 workspace level. 399 """ 400 if self.type == 'workspace': 401 return self 402 403 ret = self.parent 404 405 while ret: 406 if ret.type == 'workspace': 407 break 408 ret = ret.parent 409 410 return ret 411 412 def scratchpad(self) -> 'Con': 413 """Finds the scratchpad container. 414 415 :returns: The scratchpad container. 416 :rtype: class:`Con` 417 """ 418 for con in self.root(): 419 if con.type == 'workspace' and con.name == "__i3_scratch": 420 return con 421 422 return None 423