i3-glue 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- i3_glue-1.0.0/PKG-INFO +43 -0
- i3_glue-1.0.0/README.md +34 -0
- i3_glue-1.0.0/i3_glue/__init__.py +1 -0
- i3_glue-1.0.0/i3_glue/main.py +262 -0
- i3_glue-1.0.0/i3_glue.egg-info/PKG-INFO +43 -0
- i3_glue-1.0.0/i3_glue.egg-info/SOURCES.txt +10 -0
- i3_glue-1.0.0/i3_glue.egg-info/dependency_links.txt +1 -0
- i3_glue-1.0.0/i3_glue.egg-info/entry_points.txt +2 -0
- i3_glue-1.0.0/i3_glue.egg-info/requires.txt +1 -0
- i3_glue-1.0.0/i3_glue.egg-info/top_level.txt +1 -0
- i3_glue-1.0.0/pyproject.toml +18 -0
- i3_glue-1.0.0/setup.cfg +4 -0
i3_glue-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: i3-glue
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Glue open i3 windows together into stacked containers by WM class
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: i3ipc>=2.2
|
|
9
|
+
|
|
10
|
+
# i3-glue
|
|
11
|
+
Glue windows opened in i3wm together creating a tabbed container based on wm classes.
|
|
12
|
+
|
|
13
|
+
AI-generated and unreviewed.
|
|
14
|
+
|
|
15
|
+
## Motivation
|
|
16
|
+
I am grizzled and grumpy. I don't want to move windows areound all the time, but I have too many windows to fit on my screen in fixed locations. I can just about handle putting similar windows together and toggling them.
|
|
17
|
+
|
|
18
|
+
## Alternatives
|
|
19
|
+
Use desktops. Manually arrange your windows into containers.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
Find a the class of thel windows you want to with `xprop WM_CLASS` (the second string) or
|
|
23
|
+
`i3-msg -t get_tree`.
|
|
24
|
+
|
|
25
|
+
Run i3-glue as a daemon.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
i3-glue --glue obsidian,neovim
|
|
29
|
+
i3-glue --glue obsidian,neovim --glue qutebrowser,signal
|
|
30
|
+
i3-glue --glue obsidian,neovim --layout tabbed
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Each `--glue` is a comma-separated list of X11 WM classes. Every open window whose
|
|
34
|
+
|
|
35
|
+
## Flags
|
|
36
|
+
|
|
37
|
+
- `--glue A,B[,C...]` — a group of WM classes to glue (repeatable, required)
|
|
38
|
+
- `--layout` — `stacking` (default), `tabbed`, `splith`, `splitv`
|
|
39
|
+
|
|
40
|
+
## Install (editable, via pipx)
|
|
41
|
+
```sh
|
|
42
|
+
pipx install --editable i3-glue
|
|
43
|
+
```
|
i3_glue-1.0.0/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# i3-glue
|
|
2
|
+
Glue windows opened in i3wm together creating a tabbed container based on wm classes.
|
|
3
|
+
|
|
4
|
+
AI-generated and unreviewed.
|
|
5
|
+
|
|
6
|
+
## Motivation
|
|
7
|
+
I am grizzled and grumpy. I don't want to move windows areound all the time, but I have too many windows to fit on my screen in fixed locations. I can just about handle putting similar windows together and toggling them.
|
|
8
|
+
|
|
9
|
+
## Alternatives
|
|
10
|
+
Use desktops. Manually arrange your windows into containers.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
Find a the class of thel windows you want to with `xprop WM_CLASS` (the second string) or
|
|
14
|
+
`i3-msg -t get_tree`.
|
|
15
|
+
|
|
16
|
+
Run i3-glue as a daemon.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
i3-glue --glue obsidian,neovim
|
|
20
|
+
i3-glue --glue obsidian,neovim --glue qutebrowser,signal
|
|
21
|
+
i3-glue --glue obsidian,neovim --layout tabbed
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Each `--glue` is a comma-separated list of X11 WM classes. Every open window whose
|
|
25
|
+
|
|
26
|
+
## Flags
|
|
27
|
+
|
|
28
|
+
- `--glue A,B[,C...]` — a group of WM classes to glue (repeatable, required)
|
|
29
|
+
- `--layout` — `stacking` (default), `tabbed`, `splith`, `splitv`
|
|
30
|
+
|
|
31
|
+
## Install (editable, via pipx)
|
|
32
|
+
```sh
|
|
33
|
+
pipx install --editable i3-glue
|
|
34
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
i3-glue — keep similar i3 windows stacked together, by WM class.
|
|
4
|
+
|
|
5
|
+
AI-generated. You probably don't want to use it. And yet it exists.
|
|
6
|
+
|
|
7
|
+
Runs as a daemon: on start it stacks whatever matching windows are already
|
|
8
|
+
open, then it watches the i3 event stream and folds each newly-opened window
|
|
9
|
+
into its group as it appears. Similar windows pile into one stacked container
|
|
10
|
+
so you can toggle between them instead of tiling them all on screen.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
i3-glue --glue obsidian,neovim
|
|
14
|
+
i3-glue --glue obsidian,neovim --glue qutebrowser,signal
|
|
15
|
+
i3-glue --glue obsidian,neovim --layout tabbed
|
|
16
|
+
i3-glue --glue obsidian,neovim --once # stack existing, then exit
|
|
17
|
+
|
|
18
|
+
Each --glue takes a comma-separated list of WM classes. A window whose class
|
|
19
|
+
is in a group is moved to sit with the other open members of that group.
|
|
20
|
+
|
|
21
|
+
Flags:
|
|
22
|
+
--glue A,B[,C...] a group of WM classes to keep together (repeatable)
|
|
23
|
+
--layout LAYOUT tabbed (default), stacking, splith, splitv
|
|
24
|
+
--once stack currently-open windows, then exit (no daemon)
|
|
25
|
+
--debug print matched windows and the i3 commands sent
|
|
26
|
+
|
|
27
|
+
Runtime deps: i3 (via i3ipc).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
import i3ipc
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
DEBUG = False
|
|
37
|
+
MARK = "__i3_glue__"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def die(msg, code=1):
|
|
41
|
+
print(f"i3-glue: {msg}", file=sys.stderr)
|
|
42
|
+
sys.exit(code)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def info(msg):
|
|
46
|
+
print(f"[i3-glue] {msg}", file=sys.stderr, flush=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dbg(msg):
|
|
50
|
+
if DEBUG:
|
|
51
|
+
print(f"[i3-glue:debug] {msg}", file=sys.stderr, flush=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def command(conn, cmd):
|
|
55
|
+
"""Send an i3 command, log it in --debug, surface any failure."""
|
|
56
|
+
dbg(f"i3-msg: {cmd}")
|
|
57
|
+
for reply in conn.command(cmd):
|
|
58
|
+
if not reply.success:
|
|
59
|
+
info(f"command failed: {cmd!r}: {reply.error}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_groups(raw_groups):
|
|
63
|
+
"""Turn ['obsidian,neovim', ...] into [['obsidian', 'neovim'], ...] of
|
|
64
|
+
lowercased class names. Groups with fewer than 2 classes are dropped."""
|
|
65
|
+
groups = []
|
|
66
|
+
for raw in raw_groups:
|
|
67
|
+
classes = [c.strip().lower() for c in raw.split(",") if c.strip()]
|
|
68
|
+
if len(classes) < 2:
|
|
69
|
+
info(f"skip {raw!r}: a glue group needs at least 2 classes")
|
|
70
|
+
continue
|
|
71
|
+
groups.append(classes)
|
|
72
|
+
return groups
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def group_for_class(groups, wm_class):
|
|
76
|
+
wc = (wm_class or "").lower()
|
|
77
|
+
if not wc:
|
|
78
|
+
return None
|
|
79
|
+
for group in groups:
|
|
80
|
+
if wc in group:
|
|
81
|
+
return group
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def members_of(tree, group):
|
|
86
|
+
"""Open leaf windows whose class is in `group`, ordered so the listed class
|
|
87
|
+
order becomes the stack order."""
|
|
88
|
+
found = []
|
|
89
|
+
for con in tree.leaves():
|
|
90
|
+
wc = (con.window_class or "").lower()
|
|
91
|
+
if wc in group:
|
|
92
|
+
found.append((group.index(wc), con))
|
|
93
|
+
found.sort(key=lambda t: t[0])
|
|
94
|
+
return [con for _, con in found]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def stack_together(conn, cons, layout):
|
|
98
|
+
"""Move every con onto the first one and stack the shared container.
|
|
99
|
+
Targets containers by con_id, so it never steals focus."""
|
|
100
|
+
anchor = cons[0]
|
|
101
|
+
command(conn, f"[con_id={anchor.id}] mark --add {MARK}")
|
|
102
|
+
for con in cons[1:]:
|
|
103
|
+
command(conn, f"[con_id={con.id}] move container to mark {MARK}")
|
|
104
|
+
command(conn, f"[con_id={anchor.id}] layout {layout}")
|
|
105
|
+
command(conn, f"unmark {MARK}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def stack_existing(conn, groups, layout):
|
|
109
|
+
"""First-run pass: stack whatever is already open, group by group."""
|
|
110
|
+
tree = conn.get_tree()
|
|
111
|
+
for group in groups:
|
|
112
|
+
cons = members_of(tree, group)
|
|
113
|
+
if DEBUG:
|
|
114
|
+
for con in cons:
|
|
115
|
+
dbg(f" existing {','.join(group)}: con_id={con.id} "
|
|
116
|
+
f"class={con.window_class!r} name={con.name!r}")
|
|
117
|
+
if len(cons) < 2:
|
|
118
|
+
continue
|
|
119
|
+
stack_together(conn, cons, layout)
|
|
120
|
+
info(f"glued {','.join(group)}: {len(cons)} windows -> {layout}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def window_class_of(conn, con):
|
|
124
|
+
"""Class of a freshly-opened con. WM_CLASS is occasionally not set yet at
|
|
125
|
+
window::new time, so fall back to a fresh tree lookup by id."""
|
|
126
|
+
if con.window_class:
|
|
127
|
+
return con.window_class
|
|
128
|
+
found = conn.get_tree().find_by_id(con.id)
|
|
129
|
+
return found.window_class if found else None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def make_on_new(conn, groups, layout):
|
|
133
|
+
def on_new(conn, event):
|
|
134
|
+
con = event.container
|
|
135
|
+
wm_class = window_class_of(conn, con)
|
|
136
|
+
group = group_for_class(groups, wm_class)
|
|
137
|
+
if group is None:
|
|
138
|
+
return
|
|
139
|
+
# Find already-open members of this group, other than the new window.
|
|
140
|
+
others = [c for c in members_of(conn.get_tree(), group) if c.id != con.id]
|
|
141
|
+
if not others:
|
|
142
|
+
dbg(f"new {wm_class!r}: first member of its group, leaving it")
|
|
143
|
+
return
|
|
144
|
+
dbg(f"new {wm_class!r} (con_id={con.id}): merging into "
|
|
145
|
+
f"con_id={others[0].id}")
|
|
146
|
+
stack_together(conn, [others[0], con], layout)
|
|
147
|
+
info(f"merged {wm_class} into {','.join(group)} group")
|
|
148
|
+
|
|
149
|
+
return on_new
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def cmd_daemon(argv):
|
|
153
|
+
global DEBUG
|
|
154
|
+
parser = argparse.ArgumentParser(
|
|
155
|
+
prog="i3-glue",
|
|
156
|
+
description="Keep similar i3 windows stacked together, by WM class.",
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--glue", action="append", metavar="A,B", default=[],
|
|
160
|
+
help="comma-separated WM classes to keep together (repeatable)",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--layout", default="tabbed",
|
|
164
|
+
choices=["tabbed", "stacking", "splith", "splitv"],
|
|
165
|
+
help="layout for the glued container (default: tabbed)",
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--once", action="store_true",
|
|
169
|
+
help="stack currently-open windows, then exit (no daemon)",
|
|
170
|
+
)
|
|
171
|
+
parser.add_argument("--debug", action="store_true", help="verbose output")
|
|
172
|
+
args = parser.parse_args(argv)
|
|
173
|
+
|
|
174
|
+
DEBUG = args.debug
|
|
175
|
+
groups = parse_groups(args.glue)
|
|
176
|
+
if not groups:
|
|
177
|
+
die("nothing to glue: pass at least one --glue A,B")
|
|
178
|
+
|
|
179
|
+
conn = i3ipc.Connection()
|
|
180
|
+
stack_existing(conn, groups, args.layout)
|
|
181
|
+
if args.once:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
conn.on(i3ipc.Event.WINDOW_NEW, make_on_new(conn, groups, args.layout))
|
|
185
|
+
info("watching for new windows (ctrl-c to stop)")
|
|
186
|
+
try:
|
|
187
|
+
conn.main()
|
|
188
|
+
except KeyboardInterrupt:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------- group-aware navigation ----------
|
|
193
|
+
|
|
194
|
+
def glued_group(con):
|
|
195
|
+
"""Nearest tabbed/stacked ancestor of `con` (the glued group it lives in),
|
|
196
|
+
or None if it isn't inside one."""
|
|
197
|
+
node = con
|
|
198
|
+
while node is not None:
|
|
199
|
+
if node.layout in ("tabbed", "stacked"):
|
|
200
|
+
return node
|
|
201
|
+
node = node.parent
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def visible_leaf(con):
|
|
206
|
+
"""A focusable leaf for `con`: itself if it's a window, else its first leaf."""
|
|
207
|
+
if con.window:
|
|
208
|
+
return con
|
|
209
|
+
leaves = con.leaves()
|
|
210
|
+
return leaves[0] if leaves else con
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def child_holding(group, target_id):
|
|
214
|
+
"""Index of the group's child that contains the focused window."""
|
|
215
|
+
for i, child in enumerate(group.nodes):
|
|
216
|
+
if child.id == target_id or any(d.id == target_id for d in child.descendants()):
|
|
217
|
+
return i
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def cmd_toggle(argv):
|
|
222
|
+
"""Flip focus to the next window within the current glued group (wraps)."""
|
|
223
|
+
conn = i3ipc.Connection()
|
|
224
|
+
focused = conn.get_tree().find_focused()
|
|
225
|
+
group = glued_group(focused)
|
|
226
|
+
if group is None or len(group.nodes) < 2:
|
|
227
|
+
return
|
|
228
|
+
i = child_holding(group, focused.id)
|
|
229
|
+
nxt = group.nodes[(i + 1) % len(group.nodes)]
|
|
230
|
+
command(conn, f"[con_id={visible_leaf(nxt).id}] focus")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def cmd_focus(argv):
|
|
234
|
+
"""Directional focus that treats a glued group as one unit. By focusing the
|
|
235
|
+
whole group container first, i3's `focus <dir>` steps over its inner tabs."""
|
|
236
|
+
if not argv or argv[0] not in ("left", "right", "up", "down"):
|
|
237
|
+
die("usage: i3-glue focus {left|right|up|down}")
|
|
238
|
+
direction = argv[0]
|
|
239
|
+
conn = i3ipc.Connection()
|
|
240
|
+
focused = conn.get_tree().find_focused()
|
|
241
|
+
group = glued_group(focused)
|
|
242
|
+
if group is not None:
|
|
243
|
+
command(conn, f"[con_id={group.id}] focus")
|
|
244
|
+
command(conn, f"focus {direction}")
|
|
245
|
+
# If nothing moved we may have left a non-leaf container focused; drop back
|
|
246
|
+
# into the group's visible window so focus is always on a real window.
|
|
247
|
+
now = conn.get_tree().find_focused()
|
|
248
|
+
if now is not None and not now.window:
|
|
249
|
+
command(conn, f"[con_id={visible_leaf(now).id}] focus")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def main():
|
|
253
|
+
argv = sys.argv[1:]
|
|
254
|
+
if argv and argv[0] == "toggle":
|
|
255
|
+
return cmd_toggle(argv[1:])
|
|
256
|
+
if argv and argv[0] == "focus":
|
|
257
|
+
return cmd_focus(argv[1:])
|
|
258
|
+
return cmd_daemon(argv)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__":
|
|
262
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: i3-glue
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Glue open i3 windows together into stacked containers by WM class
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: i3ipc>=2.2
|
|
9
|
+
|
|
10
|
+
# i3-glue
|
|
11
|
+
Glue windows opened in i3wm together creating a tabbed container based on wm classes.
|
|
12
|
+
|
|
13
|
+
AI-generated and unreviewed.
|
|
14
|
+
|
|
15
|
+
## Motivation
|
|
16
|
+
I am grizzled and grumpy. I don't want to move windows areound all the time, but I have too many windows to fit on my screen in fixed locations. I can just about handle putting similar windows together and toggling them.
|
|
17
|
+
|
|
18
|
+
## Alternatives
|
|
19
|
+
Use desktops. Manually arrange your windows into containers.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
Find a the class of thel windows you want to with `xprop WM_CLASS` (the second string) or
|
|
23
|
+
`i3-msg -t get_tree`.
|
|
24
|
+
|
|
25
|
+
Run i3-glue as a daemon.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
i3-glue --glue obsidian,neovim
|
|
29
|
+
i3-glue --glue obsidian,neovim --glue qutebrowser,signal
|
|
30
|
+
i3-glue --glue obsidian,neovim --layout tabbed
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Each `--glue` is a comma-separated list of X11 WM classes. Every open window whose
|
|
34
|
+
|
|
35
|
+
## Flags
|
|
36
|
+
|
|
37
|
+
- `--glue A,B[,C...]` — a group of WM classes to glue (repeatable, required)
|
|
38
|
+
- `--layout` — `stacking` (default), `tabbed`, `splith`, `splitv`
|
|
39
|
+
|
|
40
|
+
## Install (editable, via pipx)
|
|
41
|
+
```sh
|
|
42
|
+
pipx install --editable i3-glue
|
|
43
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
i3_glue/__init__.py
|
|
4
|
+
i3_glue/main.py
|
|
5
|
+
i3_glue.egg-info/PKG-INFO
|
|
6
|
+
i3_glue.egg-info/SOURCES.txt
|
|
7
|
+
i3_glue.egg-info/dependency_links.txt
|
|
8
|
+
i3_glue.egg-info/entry_points.txt
|
|
9
|
+
i3_glue.egg-info/requires.txt
|
|
10
|
+
i3_glue.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
i3ipc>=2.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
i3_glue
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "setuptools>=68.0",]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "i3-glue"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Glue open i3 windows together into stacked containers by WM class"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = [ "i3ipc>=2.2",]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
i3-glue = "i3_glue.main:main"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools]
|
|
18
|
+
packages = [ "i3_glue",]
|
i3_glue-1.0.0/setup.cfg
ADDED