`init` planned every file on every run and refused if any of them existed, so attaching a new monitor could not be configured without --force — which reallocated global workspace numbers by i3's current output order and renumbered the monitors already in use. - allocate_global_starts() keeps the block each known output already holds and gives new outputs the lowest free block; blocks of inactive outputs stay reserved. - read_allocations()/parse_allocation() read those blocks back out of the generated files (inverse of render_workspaces_for_output). - --sync writes only the missing files, --prune drops configs for outputs that are gone and frees their numbers. - Files without the auto-generated header are never written or deleted, --force included. Co-Authored-By: Claude <noreply@anthropic.com>
221 lines
7.0 KiB
Markdown
221 lines
7.0 KiB
Markdown
# pi3-smart-workspace
|
|
|
|
A small CLI helper for the [i3 window manager](https://i3wm.org/). Bound to
|
|
`$mod+<n>` keys, it switches (or moves a container to) the **N-th workspace
|
|
declared for the output your mouse cursor is currently on**, rather than
|
|
i3's default behaviour of jumping to a globally-numbered workspace.
|
|
|
|
This makes multi-monitor i3 setups feel a lot more seamless: pressing
|
|
`$mod+1` always lands you on the first workspace of whichever screen you're
|
|
pointing at.
|
|
|
|
## Usage
|
|
|
|
```
|
|
usage: pi3-smart-workspace [-h] [-d] -i INDEX [-s] [-k]
|
|
|
|
Switch (or shift) to the N-th workspace on the output your cursor is on.
|
|
|
|
options:
|
|
-h, --help show this help message and exit
|
|
-d, --debug Print parsed state and re-raise exceptions.
|
|
|
|
Required:
|
|
-i INDEX, --index INDEX
|
|
The 1-based index into the workspaces declared for
|
|
the cursor's output.
|
|
|
|
Shift:
|
|
manipulate the active window
|
|
|
|
-s, --shift Move the focused container to the indexed workspace.
|
|
-k, --keep-with-it With --shift, also follow the container to the new
|
|
workspace.
|
|
```
|
|
|
|
## Installation
|
|
|
|
Install from PyPI:
|
|
|
|
```sh
|
|
pip install pi3-smart-workspace
|
|
```
|
|
|
|
For local development, clone the repo and install in editable mode with the
|
|
dev extras:
|
|
|
|
```sh
|
|
pip install -e ".[dev]"
|
|
pytest
|
|
```
|
|
|
|
Requires Python 3.14+ and a running i3 session.
|
|
|
|
## i3 configuration
|
|
|
|
`pi3-smart-workspace` parses your live i3 config and looks for three
|
|
specific patterns:
|
|
|
|
1. `set $<var> <output-name>` — output aliases
|
|
2. `set $<var> <digit…>` — workspace-name aliases (quoted or unquoted)
|
|
3. `workspace $<var> output $<var>` — bindings of workspaces to outputs
|
|
|
|
The order in which `workspace … output …` lines appear in your config
|
|
determines the N-th workspace for that output.
|
|
|
|
### Bootstrap with `init` (recommended)
|
|
|
|
Instead of writing the config by hand, run:
|
|
|
|
```sh
|
|
pi3-smart-workspace init
|
|
```
|
|
|
|
This detects every active monitor via i3 and writes:
|
|
|
|
```
|
|
~/.config/i3/pi3-smart-workspace/
|
|
├── bindings.conf # $mod+N, $mod+Shift+N, $mod+Ctrl+N bindings
|
|
├── eDP-1/
|
|
│ └── workspaces.conf # workspaces 1..8 bound to eDP-1
|
|
├── HDMI-A-0/
|
|
│ └── workspaces.conf # workspaces 9..16 bound to HDMI-A-0
|
|
└── DP-1/
|
|
└── workspaces.conf # workspaces 17..24 bound to DP-1
|
|
```
|
|
|
|
Each output gets a contiguous range of global workspace numbers; the
|
|
local index (the number after the colon, e.g. `9:1`) tells you "first
|
|
workspace on this monitor". Then add the include lines `init` prints to
|
|
your main i3 config and reload (`$mod+Shift+r`):
|
|
|
|
```i3config
|
|
include ~/.config/i3/pi3-smart-workspace/bindings.conf
|
|
include ~/.config/i3/pi3-smart-workspace/*/workspaces.conf
|
|
```
|
|
|
|
i3's `include` directive needs i3 ≥ 4.20.
|
|
|
|
Useful flags:
|
|
|
|
| Flag | Default | Effect |
|
|
| --- | --- | --- |
|
|
| `-n N` / `--count N` | 8 | Workspaces per output |
|
|
| `--config-dir PATH` | `~/.config/i3/pi3-smart-workspace` | Where to write |
|
|
| `--dry-run` | off | Print to stdout, don't touch the filesystem |
|
|
| `--sync` | off | Only write configs for outputs that don't have one yet |
|
|
| `--force` | off | Regenerate every file from scratch (renumbers workspaces) |
|
|
| `--prune` | off | Delete generated configs for outputs that are no longer active |
|
|
|
|
### Plugging in a new monitor
|
|
|
|
Run:
|
|
|
|
```sh
|
|
pi3-smart-workspace init --sync
|
|
```
|
|
|
|
`--sync` writes a `workspaces.conf` only for the outputs that don't have
|
|
one yet, and gives the newcomer the lowest block of global workspace
|
|
numbers that isn't already taken. The monitors you already use keep the
|
|
numbers they have, so your muscle memory survives — which a plain
|
|
`--force` regeneration would not guarantee, since that allocates numbers
|
|
by i3's current output order and can shuffle everything.
|
|
|
|
Configs for monitors that are merely switched off are left in place and
|
|
their numbers stay reserved. Add `--prune` to delete them and free those
|
|
numbers for a new screen:
|
|
|
|
```sh
|
|
pi3-smart-workspace init --sync --prune
|
|
```
|
|
|
|
Files that don't carry the `# Auto-generated by pi3-smart-workspace init`
|
|
header are never overwritten or deleted, not even with `--force` — so
|
|
anything you hand-tune in that directory is safe.
|
|
|
|
### Example config (manual)
|
|
|
|
```i3config
|
|
# Displays
|
|
set $primary eDP
|
|
set $top HDMI-A-0
|
|
set $bottom HDMI2
|
|
|
|
# Workspaces
|
|
set $ws1 1:1
|
|
# ... and so on
|
|
set $ws{n} {n}:{n}
|
|
|
|
set $TopWs1 {n+1}:1
|
|
# ... and so on
|
|
set $TopWs{k} {n+1+k}:{k}
|
|
|
|
set $BottomWs1 {k+1}:1
|
|
# ... and so on
|
|
set $BottomWs{q} {k+1+q}:{q}
|
|
|
|
workspace $ws1 output $primary
|
|
# ... and so on
|
|
workspace $ws{n} output $primary
|
|
|
|
workspace $TopWs1 output $top
|
|
# ... and so on
|
|
workspace $TopWs{k} output $top
|
|
|
|
workspace $BottomWs1 output $bottom
|
|
# ... and so on
|
|
workspace $BottomWs{q} output $bottom
|
|
|
|
# Switch to workspace
|
|
bindsym $mod+1 exec --no-startup-id pi3-smart-workspace -i 1
|
|
bindsym $mod+2 exec --no-startup-id pi3-smart-workspace -i 2
|
|
bindsym $mod+3 exec --no-startup-id pi3-smart-workspace -i 3
|
|
bindsym $mod+4 exec --no-startup-id pi3-smart-workspace -i 4
|
|
bindsym $mod+5 exec --no-startup-id pi3-smart-workspace -i 5
|
|
bindsym $mod+6 exec --no-startup-id pi3-smart-workspace -i 6
|
|
bindsym $mod+7 exec --no-startup-id pi3-smart-workspace -i 7
|
|
bindsym $mod+8 exec --no-startup-id pi3-smart-workspace -i 8
|
|
|
|
# Move focused container to workspace
|
|
bindsym $mod+Shift+1 exec --no-startup-id pi3-smart-workspace -i 1 -s
|
|
bindsym $mod+Shift+2 exec --no-startup-id pi3-smart-workspace -i 2 -s
|
|
bindsym $mod+Shift+3 exec --no-startup-id pi3-smart-workspace -i 3 -s
|
|
bindsym $mod+Shift+4 exec --no-startup-id pi3-smart-workspace -i 4 -s
|
|
bindsym $mod+Shift+5 exec --no-startup-id pi3-smart-workspace -i 5 -s
|
|
bindsym $mod+Shift+6 exec --no-startup-id pi3-smart-workspace -i 6 -s
|
|
bindsym $mod+Shift+7 exec --no-startup-id pi3-smart-workspace -i 7 -s
|
|
bindsym $mod+Shift+8 exec --no-startup-id pi3-smart-workspace -i 8 -s
|
|
|
|
# Move to workspace with focused container
|
|
bindsym $mod+Ctrl+1 exec --no-startup-id pi3-smart-workspace -i 1 -sk
|
|
bindsym $mod+Ctrl+2 exec --no-startup-id pi3-smart-workspace -i 2 -sk
|
|
bindsym $mod+Ctrl+3 exec --no-startup-id pi3-smart-workspace -i 3 -sk
|
|
bindsym $mod+Ctrl+4 exec --no-startup-id pi3-smart-workspace -i 4 -sk
|
|
bindsym $mod+Ctrl+5 exec --no-startup-id pi3-smart-workspace -i 5 -sk
|
|
bindsym $mod+Ctrl+6 exec --no-startup-id pi3-smart-workspace -i 6 -sk
|
|
bindsym $mod+Ctrl+7 exec --no-startup-id pi3-smart-workspace -i 7 -sk
|
|
bindsym $mod+Ctrl+8 exec --no-startup-id pi3-smart-workspace -i 8 -sk
|
|
```
|
|
|
|
## Development
|
|
|
|
```sh
|
|
pip install -e ".[dev]"
|
|
pytest # run the unit tests
|
|
python -m build # build sdist + wheel into dist/
|
|
twine check dist/* # validate the distributions
|
|
```
|
|
|
|
The unit tests cover the two pure functions (`parse_workspaces_per_output`
|
|
and `find_output_at_cursor`) and run without an X display, so they work in
|
|
CI. The i3-dispatch path needs a real multi-monitor session and is only
|
|
exercised manually.
|
|
|
|
## Credits
|
|
|
|
Thanks to Michał Wieluński for the inspiration
|
|
([pi3-switch](https://github.com/landmaj/pi3-switch)) and Tony Crisci for
|
|
the easy-to-use i3 Python library
|
|
([i3ipc-python](https://github.com/acrisci/i3ipc-python)).
|