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