Skip to content

Commit 689b915

Browse files
✨ Support Window Swallow
1 parent 879e55f commit 689b915

11 files changed

Lines changed: 1363 additions & 4 deletions

File tree

README.en.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Piri is a high-performance [Niri](https://github.com/YaLTeR/niri) extension tool
1414
- 🔄 **Autofill**: Layout auto-alignment. Automatically aligns remaining windows when a window is closed or layout changes, keeping your interface clean (see [Autofill Docs](docs/en/plugins/autofill.md))
1515
- 🔒 **Singleton**: Single-instance assurance. Ensures specific applications remain globally unique, supporting quick focus or automatic process launching (see [Singleton Docs](docs/en/plugins/singleton.md))
1616
- 📋 **Window Order**: Intelligent reordering. Automatically reorders tiled windows based on configured weights, preserving relative positions for identical weights to minimize movement (see [Window Order Docs](docs/en/plugins/window_order.md))
17+
- 🍽️ **Swallow**: Window swallowing mechanism. Automatically hides parent windows when child windows are opened, allowing child windows to replace parent windows in the layout (see [Swallow Docs](docs/en/plugins/swallow.md))
1718

1819

1920
## Quick Start
@@ -306,6 +307,44 @@ piri window_order toggle
306307

307308
For detailed documentation, please refer to the [Window Order documentation](docs/en/plugins/window_order.md).
308309

310+
### Swallow
311+
312+
![Swallow](./assets/autofill_1.mp4)
313+
314+
Automatically hides parent windows when child windows are opened, allowing child windows to replace parent windows in the layout. This is useful for scenarios like terminals spawning image viewers or media players.
315+
316+
**Configuration Example**:
317+
```toml
318+
[piri.plugins]
319+
swallow = true
320+
321+
[piri.swallow]
322+
use_pid_matching = true # Enable PID-based parent-child process matching (default: true)
323+
324+
# Global exclude rule (optional)
325+
[piri.swallow.exclude]
326+
app_id = [".*dialog.*"]
327+
328+
# Rules list
329+
[[swallow]]
330+
parent_app_id = [".*terminal.*", ".*alacritty.*", ".*foot.*", ".*ghostty.*"]
331+
child_app_id = [".*mpv.*", ".*imv.*", ".*feh.*"]
332+
exclude_child_app_id = [".*dialog.*", ".*error.*"]
333+
334+
[[swallow]]
335+
parent_app_id = ["code", "nvim-qt"]
336+
child_app_id = [".*preview.*", ".*markdown.*"]
337+
```
338+
339+
**Features**:
340+
- Supports PID-based parent-child process matching (enabled by default)
341+
- Supports rule-based matching (via `app_id`, `title`, or `pid` patterns)
342+
- Supports global and rule-level exclude rules
343+
- Intelligent focus window queue for automatic parent window discovery
344+
- Automatically handles workspace movement and floating window conversion
345+
346+
For detailed documentation, please refer to the [Swallow documentation](docs/en/plugins/swallow.md).
347+
309348
## Documentation
310349

311350
- [Architecture](docs/en/architecture.md) - Project architecture and how it works

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Piri 是基于 Rust 的 [Niri](https://github.com/YaLTeR/niri) 高性能功能
1414
- 🔄 **Autofill**: 布局自动对齐。在窗口关闭或布局变动时自动对齐剩余窗口,时刻保持界面整洁(详见 [Autofill 文档](docs/zh/plugins/autofill.md)
1515
- 🔒 **Singleton**: 单实例保障。确保特定应用全局唯一,支持快速聚焦现有实例或自动拉起新进程(详见 [Singleton 文档](docs/zh/plugins/singleton.md)
1616
- 📋 **Window Order**: 智能窗口排序。根据配置权重自动重排平铺窗口,相同权重窗口保持相对位置以最小化移动损耗(详见 [Window Order 文档](docs/zh/plugins/window_order.md)
17+
- 🍽️ **Swallow**: 窗口吞噬机制。当子窗口打开时自动隐藏父窗口,让子窗口在布局中替换父窗口的位置(详见 [Swallow 文档](docs/zh/plugins/swallow.md)
1718

1819
## 窗口匹配机制
1920

@@ -316,6 +317,43 @@ piri window_order toggle
316317

317318
详细说明请参考 [Window Order 文档](docs/zh/plugins/window_order.md)
318319

320+
### Swallow
321+
322+
![Swallow](./assets/autofill_1.mp4)
323+
324+
当子窗口打开时自动隐藏父窗口,让子窗口在布局中替换父窗口的位置。这对于终端启动图片查看器或媒体播放器等场景非常有用。
325+
326+
**配置示例**
327+
```toml
328+
[piri.plugins]
329+
swallow = true
330+
331+
[piri.swallow]
332+
use_pid_matching = true # 启用基于 PID 的父子进程匹配(默认:true)
333+
334+
# 全局排除规则(可选)
335+
[piri.swallow.exclude]
336+
app_id = [".*dialog.*"]
337+
338+
# 规则列表
339+
[[swallow]]
340+
parent_app_id = [".*terminal.*", ".*alacritty.*", ".*foot.*", ".*ghostty.*"]
341+
child_app_id = [".*mpv.*", ".*imv.*", ".*feh.*"]
342+
343+
[[swallow]]
344+
parent_app_id = ["code", "nvim-qt"]
345+
child_app_id = [".*preview.*", ".*markdown.*"]
346+
```
347+
348+
**特性**
349+
- 支持基于 PID 的父子进程匹配(默认启用)
350+
- 支持基于规则的匹配(通过 `app_id``title``pid` 模式)
351+
- 支持全局和规则级别的排除规则
352+
- 智能聚焦窗口队列,自动查找父窗口
353+
- 自动处理工作空间移动和浮动窗口转换
354+
355+
详细说明请参考 [Swallow 文档](docs/zh/plugins/swallow.md)
356+
319357
## 文档
320358

321359
- [架构设计](docs/zh/architecture.md) - 项目架构和工作原理

assets/swallow_pid.mp4

2.25 MB
Binary file not shown.

assets/swallow_rule.mp4

2 MB
Binary file not shown.

config.example.toml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ window_rule = true
1515
autofill = true
1616
singleton = true
1717
window_order = true
18+
swallow = true
1819

1920
[piri.scratchpad]
2021
default_size = "40% 60%"
@@ -117,4 +118,18 @@ default_weight = 0
117118
# Window-specific weights
118119
google-chrome = 100
119120
code = 80
120-
ghostty = 70
121+
ghostty = 70
122+
123+
[piri.swallow]
124+
use_pid_matching=false
125+
126+
[piri.swallow.exclude]
127+
app_id = ".*mpv*."
128+
129+
[[swallow]]
130+
child_app_id='.*google-chrome.*'
131+
parent_app_id='.*ghostty.*'
132+
133+
[[swallow]]
134+
child_app_id='.*firefox*.'
135+
parent_app_id='.*ghostty.*'

docs/en/plugins/swallow.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Swallow Plugin
2+
3+
The Swallow plugin automatically hides parent windows when child windows are spawned. This is useful for scenarios like terminals spawning image viewers or media players, where you want the child window to replace the parent window in the layout.
4+
5+
## How It Works
6+
7+
When a child window is opened:
8+
9+
1. **Child Window Matching**: The plugin checks if the new window matches any rule's child window criteria
10+
2. **Parent Window Discovery**: It finds the parent window using two methods (in priority order):
11+
- **PID-based matching** (default): Traces the process tree to find if the child process was spawned from a parent process
12+
- **Rule-based matching**: Matches parent windows by `app_id`, `title`, or `pid` patterns
13+
3. **Swallow Operation**: If a matching parent is found, the child window is "swallowed" into the parent's column position, effectively replacing it
14+
15+
## Configuration
16+
17+
Use the `[[swallow]]` format to configure rules (each rule is a separate configuration block), and use `[piri.swallow]` to configure plugin global settings:
18+
19+
```toml
20+
[piri.plugins]
21+
swallow = true
22+
23+
# Plugin global configuration
24+
[piri.swallow]
25+
# Enable PID-based parent-child process matching (default: true)
26+
use_pid_matching = true
27+
28+
# Global exclude rule: windows matching these conditions will never be swallowed
29+
[piri.swallow.exclude]
30+
app_id = [".*dialog.*"]
31+
title = [".*error.*"]
32+
33+
# Rules list (each rule is a separate configuration block)
34+
# Example 1: Terminal swallows media players
35+
[[swallow]]
36+
parent_app_id = [".*terminal.*", ".*alacritty.*", ".*foot.*", ".*ghostty.*"]
37+
child_app_id = [".*mpv.*", ".*imv.*", ".*feh.*"]
38+
39+
# Example 2: Editor swallows preview windows
40+
[[swallow]]
41+
parent_app_id = ["code", "nvim-qt"]
42+
child_app_id = [".*preview.*", ".*markdown.*"]
43+
```
44+
45+
### Global Configuration Parameters
46+
47+
The following global parameters can be configured in `[piri.swallow]`:
48+
49+
| Parameter | Type | Description |
50+
| :--- | :--- | :--- |
51+
| `use_pid_matching` | `bool` | Enable PID-based parent-child process matching (default: `true`) |
52+
| `exclude` | `SwallowExclude` | Global exclude rule, windows matching these conditions will never be swallowed (optional) |
53+
54+
### Rule Configuration Parameters
55+
56+
Each rule supports the following optional parameters:
57+
58+
| Parameter | Type | Description |
59+
| :--- | :--- | :--- |
60+
| `parent_app_id` | `Vec<String>` | Regex patterns to match parent window `app_id` |
61+
| `parent_title` | `Vec<String>` | Regex patterns to match parent window `title` |
62+
| `child_app_id` | `Vec<String>` | Regex patterns to match child window `app_id` |
63+
| `child_title` | `Vec<String>` | Regex patterns to match child window `title` |
64+
65+
### Matching Logic
66+
67+
1. **Global Exclude Check**: First check if the child window matches the global `exclude` rule. If matched, skip immediately without performing any swallow operations.
68+
69+
2. **PID Matching** (when `use_pid_matching = true`, default, highest priority):
70+
- Traces the child process's process tree to find ancestor processes
71+
- Matches parent windows whose PID is an ancestor of the child process
72+
- If parent criteria (`parent_app_id`, `parent_title`) are specified, they are also checked
73+
- If no parent criteria are specified, any ancestor window will match
74+
75+
3. **Rule-based Matching** (fallback when PID matching fails or is disabled):
76+
- Matches parent windows using `app_id`, `title`, or `pid` patterns
77+
- Only used if PID matching fails or `use_pid_matching = false`
78+
- **Parent Window Discovery Mechanism**:
79+
- If the currently focused window is not the child window, use the currently focused window as the candidate parent window
80+
- If the currently focused window is the child window itself, search for a matching parent window from the focus window queue (maintains the last 5 focused windows)
81+
- The focus window queue is automatically updated when windows gain focus
82+
83+
4. **Exclude Rules**: Exclude patterns take precedence - if a window matches an exclude pattern, it will not be matched even if it matches include patterns
84+
85+
5. **Pattern Lists**: When multiple patterns are provided (e.g., `parent_app_id = ["pattern1", "pattern2"]`), the rule matches if ANY pattern matches (OR logic)
86+
87+
## Examples
88+
89+
### PID-based Matching Example
90+
91+
![Swallow - PID-based Matching](./assets/swallow_pid.mp4)
92+
93+
Using the default PID matching (`use_pid_matching = true`), the plugin automatically traces the process tree to find parent-child relationships.
94+
95+
```toml
96+
[piri.swallow]
97+
use_pid_matching = true
98+
99+
[[swallow]]
100+
parent_app_id = [".*ghostty.*"]
101+
child_app_id = [".*mpv.*"]
102+
```
103+
104+
### Rule-based Matching Example
105+
106+
![Swallow - Rule-based Matching](./assets/swallow_rule.mp4)
107+
108+
Using `app_id` and `title` patterns to match parent windows.
109+
110+
```toml
111+
[piri.swallow]
112+
use_pid_matching = true
113+
114+
[[swallow]]
115+
child_app_id = '.*google-chrome.*'
116+
parent_app_id = '.*ghostty.*'
117+
118+
[[swallow]]
119+
child_app_id = '.*firefox*.'
120+
parent_app_id = '.*ghostty.*'
121+
```
122+
123+
### Basic Example: Terminal Swallows Media Players
124+
125+
```toml
126+
[[swallow]]
127+
parent_app_id = ["ghostty", "alacritty", "foot"]
128+
child_app_id = ["mpv", "imv", "feh"]
129+
```
130+
131+
When you launch `mpv` or `imv` from a terminal, the terminal window will be hidden and replaced by the media player.
132+
133+
134+
### Global Exclude Example
135+
136+
```toml
137+
[piri.swallow]
138+
# Globally exclude all dialog windows
139+
[piri.swallow.exclude]
140+
app_id = [".*dialog.*", ".*error.*"]
141+
142+
[[swallow]]
143+
parent_app_id = [".*terminal.*"]
144+
child_app_id = [".*mpv.*"]
145+
```
146+
147+
This way all dialog windows will never be swallowed, even if rules match.
148+
149+
### Disable PID Matching
150+
151+
```toml
152+
[piri.swallow]
153+
use_pid_matching = false
154+
155+
[[swallow]]
156+
parent_app_id = [".*terminal.*"]
157+
child_app_id = [".*mpv.*"]
158+
```
159+
160+
This uses rule-based matching only, without checking process relationships.
161+
162+
### Match by Title
163+
164+
```toml
165+
[[swallow]]
166+
parent_title = [".*Terminal.*"]
167+
child_title = [".*Video Player.*"]
168+
```
169+
170+
### Complex Example: Multiple Patterns
171+
172+
```toml
173+
[[swallow]]
174+
parent_app_id = ["ghostty", "alacritty", "foot", "kitty"]
175+
child_app_id = ["mpv", "imv", "feh", "sxiv"]
176+
```
177+
178+
## Default Behavior
179+
180+
- If no rules are specified, the plugin is enabled but won't match any windows
181+
- `use_pid_matching` defaults to `true` if not specified
182+
- If `exclude` is not specified, no global exclusion is performed
183+
- If no child conditions are specified, the rule will match any child window and look for parents
184+
- If no parent conditions are specified (with PID matching enabled), any ancestor window will match
185+
- The focus window queue maintains at most the last 5 focused windows, used to find parent windows when child windows are focused
186+
187+
## Technical Details
188+
189+
### Process Tree Tracing
190+
191+
When PID matching is enabled, the plugin:
192+
1. Finds the PID of the child window's process
193+
2. Traces up the process tree (up to PID 1) to find ancestor PIDs
194+
3. Matches windows whose process PID is in the ancestor chain
195+
196+
### Focus Window Queue
197+
198+
The plugin maintains a focus queue of at most 5 windows to track recently focused windows:
199+
- When a window gains focus (`WindowFocusTimestampChanged` event), the window ID is added to the end of the queue
200+
- When a new window opens (`WindowOpenedOrChanged` event), the window ID is also added to the queue
201+
- When a child window opens and the currently focused window is the child window itself, the plugin searches for a matching parent window from the queue (newest to oldest)
202+
- The queue size is limited to 5, removing the oldest window ID when exceeded
203+
204+
### Window Matching
205+
206+
The plugin uses the same window matching mechanism as other plugins. For details, see [Window Matching Mechanism](../window_matching.md).
207+
208+
### IPC Calls
209+
210+
The plugin performs the following operations when swallowing:
211+
1. Focus the parent window
212+
2. Ensures child window is not floating (converts to tiling if needed)
213+
3. Moves child window to parent's workspace (if different)
214+
4. Executes `ConsumeOrExpelWindowLeft` action to swallow the child into parent's column
215+
5. Focuses the child window
216+
217+
All operations are performed in a single batch for better performance and atomicity.
218+
219+
## Use Cases
220+
221+
- **Terminals spawning media players**: Hide terminal when launching `mpv`, `imv`, or `feh`
222+
- **Editors spawning previews**: Hide editor window when preview window opens
223+
- **Applications with launcher windows**: Hide launcher when main application starts
224+
- **Nested application workflows**: Automatically manage parent-child window relationships
225+
226+
## Limitations
227+
228+
- Floating windows cannot be swallowed (will be converted to tiling first)
229+
- Parent and child windows must be in the same workspace (plugin handles this automatically)
230+
- Process tree tracing goes all the way up to PID 1, which may impact performance if the process tree is very deep
231+
- PID matching requires processes to have a parent-child relationship
232+
- The focus window queue maintains at most 5 windows. If the parent window is not among the last 5 focused windows, rule-based matching may not find the parent window
233+

0 commit comments

Comments
 (0)