Skip to content

Commit deef8bf

Browse files
committed
docs(topics): add QueryList filtering topic
why: Following libvcs pattern, document the Django-style filtering API that powers session/window/pane collections. what: - Add comprehensive filtering.md topic with working doctests - Cover all lookup operators: exact, contains, startswith, endswith, regex, in/nin, and case-insensitive variants - Show chaining filters, get() with defaults, list membership - Include real-world examples (editor windows, naming conventions) - Add to topics index after traversal - All 12 doctests pass
1 parent 76aa8d7 commit deef8bf

File tree

2 files changed

+264
-0
lines changed

2 files changed

+264
-0
lines changed

docs/topics/filtering.md

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
(querylist-filtering)=
2+
3+
# QueryList Filtering
4+
5+
libtmux uses `QueryList` to enable Django-style filtering on tmux objects.
6+
Every collection (`server.sessions`, `session.windows`, `window.panes`) returns
7+
a `QueryList`, letting you filter sessions, windows, and panes with a fluent,
8+
chainable API.
9+
10+
## Basic Filtering
11+
12+
The `filter()` method accepts keyword arguments with optional lookup suffixes:
13+
14+
```python
15+
>>> server.sessions # doctest: +ELLIPSIS
16+
[Session($... ...)]
17+
```
18+
19+
### Exact Match
20+
21+
The default lookup is `exact`:
22+
23+
```python
24+
>>> # These are equivalent
25+
>>> server.sessions.filter(session_name=session.session_name) # doctest: +ELLIPSIS
26+
[Session($... ...)]
27+
>>> server.sessions.filter(session_name__exact=session.session_name) # doctest: +ELLIPSIS
28+
[Session($... ...)]
29+
```
30+
31+
### Contains and Startswith
32+
33+
Use suffixes for partial matching:
34+
35+
```python
36+
>>> # Create windows for this example
37+
>>> w1 = session.new_window(window_name="api-server")
38+
>>> w2 = session.new_window(window_name="api-worker")
39+
>>> w3 = session.new_window(window_name="web-frontend")
40+
41+
>>> # Windows containing 'api'
42+
>>> api_windows = session.windows.filter(window_name__contains='api')
43+
>>> len(api_windows) >= 2
44+
True
45+
46+
>>> # Windows starting with 'web'
47+
>>> web_windows = session.windows.filter(window_name__startswith='web')
48+
>>> len(web_windows) >= 1
49+
True
50+
51+
>>> # Clean up
52+
>>> w1.kill()
53+
>>> w2.kill()
54+
>>> w3.kill()
55+
```
56+
57+
## Available Lookups
58+
59+
| Lookup | Description |
60+
|--------|-------------|
61+
| `exact` | Exact match (default) |
62+
| `iexact` | Case-insensitive exact match |
63+
| `contains` | Substring match |
64+
| `icontains` | Case-insensitive substring |
65+
| `startswith` | Prefix match |
66+
| `istartswith` | Case-insensitive prefix |
67+
| `endswith` | Suffix match |
68+
| `iendswith` | Case-insensitive suffix |
69+
| `in` | Value in list |
70+
| `nin` | Value not in list |
71+
| `regex` | Regular expression match |
72+
| `iregex` | Case-insensitive regex |
73+
74+
## Getting a Single Item
75+
76+
Use `get()` to retrieve exactly one matching item:
77+
78+
```python
79+
>>> window = session.windows.get(window_id=session.active_window.window_id)
80+
>>> window # doctest: +ELLIPSIS
81+
Window(@... ..., Session($... ...))
82+
```
83+
84+
If no match or multiple matches are found, `get()` raises an exception:
85+
86+
- `ObjectDoesNotExist` - no matching object found
87+
- `MultipleObjectsReturned` - more than one object matches
88+
89+
You can provide a default value to avoid the exception:
90+
91+
```python
92+
>>> session.windows.get(window_name="nonexistent", default=None) is None
93+
True
94+
```
95+
96+
## Chaining Filters
97+
98+
Filters can be chained for complex queries:
99+
100+
```python
101+
>>> # Create windows for this example
102+
>>> w1 = session.new_window(window_name="feature-login")
103+
>>> w2 = session.new_window(window_name="feature-signup")
104+
>>> w3 = session.new_window(window_name="bugfix-typo")
105+
106+
>>> # Multiple conditions in one filter (AND)
107+
>>> session.windows.filter(
108+
... window_name__startswith='feature',
109+
... window_name__endswith='signup'
110+
... ) # doctest: +ELLIPSIS
111+
[Window(@... ...:feature-signup, Session($... ...))]
112+
113+
>>> # Chained filters (also AND)
114+
>>> session.windows.filter(
115+
... window_name__contains='feature'
116+
... ).filter(
117+
... window_name__contains='login'
118+
... ) # doctest: +ELLIPSIS
119+
[Window(@... ...:feature-login, Session($... ...))]
120+
121+
>>> # Clean up
122+
>>> w1.kill()
123+
>>> w2.kill()
124+
>>> w3.kill()
125+
```
126+
127+
## Case-Insensitive Filtering
128+
129+
Use `i` prefix variants for case-insensitive matching:
130+
131+
```python
132+
>>> # Create windows with mixed case
133+
>>> w1 = session.new_window(window_name="MyApp-Server")
134+
>>> w2 = session.new_window(window_name="myapp-worker")
135+
136+
>>> # Case-insensitive contains
137+
>>> myapp_windows = session.windows.filter(window_name__icontains='MYAPP')
138+
>>> len(myapp_windows) >= 2
139+
True
140+
141+
>>> # Case-insensitive startswith
142+
>>> session.windows.filter(window_name__istartswith='myapp') # doctest: +ELLIPSIS
143+
[Window(@... ...:MyApp-Server, Session($... ...)), Window(@... ...:myapp-worker, Session($... ...))]
144+
145+
>>> # Clean up
146+
>>> w1.kill()
147+
>>> w2.kill()
148+
```
149+
150+
## Regex Filtering
151+
152+
For complex patterns, use regex lookups:
153+
154+
```python
155+
>>> # Create windows with version-like names
156+
>>> w1 = session.new_window(window_name="app-v1.0")
157+
>>> w2 = session.new_window(window_name="app-v2.0")
158+
>>> w3 = session.new_window(window_name="app-beta")
159+
160+
>>> # Match version pattern
161+
>>> versioned = session.windows.filter(window_name__regex=r'v\d+\.\d+$')
162+
>>> len(versioned) >= 2
163+
True
164+
165+
>>> # Case-insensitive regex
166+
>>> session.windows.filter(window_name__iregex=r'BETA') # doctest: +ELLIPSIS
167+
[Window(@... ...:app-beta, Session($... ...))]
168+
169+
>>> # Clean up
170+
>>> w1.kill()
171+
>>> w2.kill()
172+
>>> w3.kill()
173+
```
174+
175+
## Filtering by List Membership
176+
177+
Use `in` and `nin` (not in) for list-based filtering:
178+
179+
```python
180+
>>> # Create test windows
181+
>>> w1 = session.new_window(window_name="dev")
182+
>>> w2 = session.new_window(window_name="staging")
183+
>>> w3 = session.new_window(window_name="prod")
184+
185+
>>> # Filter windows in a list of names
186+
>>> target_envs = ["dev", "prod"]
187+
>>> session.windows.filter(window_name__in=target_envs) # doctest: +ELLIPSIS
188+
[Window(@... ...:dev, Session($... ...)), Window(@... ...:prod, Session($... ...))]
189+
190+
>>> # Filter windows NOT in a list
191+
>>> non_prod = session.windows.filter(window_name__nin=["prod"])
192+
>>> any(w.window_name == "prod" for w in non_prod)
193+
False
194+
195+
>>> # Clean up
196+
>>> w1.kill()
197+
>>> w2.kill()
198+
>>> w3.kill()
199+
```
200+
201+
## Filtering Across the Hierarchy
202+
203+
Filter at any level of the tmux hierarchy:
204+
205+
```python
206+
>>> # All panes across all windows in the server
207+
>>> server.panes # doctest: +ELLIPSIS
208+
[Pane(%... Window(@... ..., Session($... ...)))]
209+
210+
>>> # Filter panes by their window's name
211+
>>> pane = session.active_pane
212+
>>> pane # doctest: +ELLIPSIS
213+
Pane(%... Window(@... ..., Session($... ...)))
214+
```
215+
216+
## Real-World Examples
217+
218+
### Find all editor windows
219+
220+
```python
221+
>>> # Create sample windows
222+
>>> w1 = session.new_window(window_name="vim-main")
223+
>>> w2 = session.new_window(window_name="nvim-config")
224+
>>> w3 = session.new_window(window_name="shell")
225+
226+
>>> # Find vim/nvim windows
227+
>>> editors = session.windows.filter(window_name__iregex=r'n?vim')
228+
>>> len(editors) >= 2
229+
True
230+
231+
>>> # Clean up
232+
>>> w1.kill()
233+
>>> w2.kill()
234+
>>> w3.kill()
235+
```
236+
237+
### Find windows by naming convention
238+
239+
```python
240+
>>> # Create windows following a naming convention
241+
>>> w1 = session.new_window(window_name="project:frontend")
242+
>>> w2 = session.new_window(window_name="project:backend")
243+
>>> w3 = session.new_window(window_name="logs")
244+
245+
>>> # Find all project windows
246+
>>> project_windows = session.windows.filter(window_name__startswith='project:')
247+
>>> len(project_windows) >= 2
248+
True
249+
250+
>>> # Get specific project window
251+
>>> backend = session.windows.get(window_name='project:backend')
252+
>>> backend.window_name
253+
'project:backend'
254+
255+
>>> # Clean up
256+
>>> w1.kill()
257+
>>> w2.kill()
258+
>>> w3.kill()
259+
```
260+
261+
## API Reference
262+
263+
See {class}`~libtmux._internal.query_list.QueryList` for the complete API.

docs/topics/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Explore libtmux's core functionalities and underlying principles at a high level
99
```{toctree}
1010
1111
traversal
12+
filtering
1213
pane_interaction
1314
workspace_setup
1415
automation_patterns

0 commit comments

Comments
 (0)