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 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
+ ```
@@ -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,2 @@
1
+ [console_scripts]
2
+ i3-glue = i3_glue.main:main
@@ -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",]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+