|
| 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 | + |
| 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 | + |
| 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