i3a 2.3.0__tar.gz → 2.4.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.
i3a-2.4.0/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ Pipfile.lock
2
+
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ build/
8
+ *.egg
9
+ *.egg-info/
10
+ .eggs
11
+
12
+ .tox/
13
+ .cache/
14
+ .coverage
15
+ coverage.xml
16
+ htmlcov/
17
+
18
+ src/i3a/_version.py
19
+
20
+ tags
21
+ .tags
22
+
23
+ poetry.lock
24
+ dist/*
@@ -1,30 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: i3a
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Automatic manager for i3 tiling
5
- Home-page: https://git.goral.net.pl/i3a.git
6
- License: GPL-3.0-or-later
7
- Author: Michal Goral
8
- Author-email: dev@goral.net.pl
9
- Requires-Python: >=3.7,<4.0
5
+ Author-email: Michal Goral <dev@goral.net.pl>
6
+ License-Expression: GPL-3.0-or-later
7
+ License-File: LICENSE
10
8
  Classifier: Development Status :: 5 - Production/Stable
11
9
  Classifier: Environment :: Console
12
10
  Classifier: Intended Audience :: End Users/Desktop
13
11
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
12
  Classifier: Natural Language :: English
15
13
  Classifier: Operating System :: POSIX
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.7
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
- Classifier: Programming Language :: Python :: 3.10
21
- Classifier: Programming Language :: Python :: 3.11
22
- Classifier: Programming Language :: Python :: 3.12
23
14
  Classifier: Programming Language :: Python :: 3 :: Only
24
15
  Classifier: Topic :: Utilities
25
- Requires-Dist: i3ipc (>=2.2.1,<3.0.0)
26
- Requires-Dist: importlib-metadata (>=4.0,<5.0) ; python_version < "3.8"
27
- Project-URL: Repository, https://git.goral.net.pl/i3a.git
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: i3ipc==2.2.1
28
18
  Description-Content-Type: text/markdown
29
19
 
30
20
  # i3a
@@ -153,8 +143,29 @@ will cycle so it must be escaped in configuration file:
153
143
  bindsym $mod+s exec "i3a-scale-cycle -f 1.0,1.5,2.0 -o HDMI-1 -o HDMI-2 --next"
154
144
  ```
155
145
 
146
+ ### i3a-trail
147
+
148
+ Dynamically toggle trailmarks (a kind of special marks) on windows and use them
149
+ to quickly cycle between trailmarks in the same groups. It's possible to have
150
+ many different groups of trailmarks (trails) and windows may belong to many
151
+ different groups.
152
+
153
+ Use `i3a-trail new-trail|delete-trail|next-trail|previous-trail` create and
154
+ activate trails. Use `i3a-trail mark` to add and remove trailmarks. Use
155
+ `i3a-trail next|previous` to traverse marked windows.
156
+
157
+ ```
158
+ bindsym $mod+grave exec i3a-trail mark
159
+ bindsym $mod+Tab exec i3a-trail next
160
+ bindsym $mod+Shift+Tab exec i3a-trail previous
161
+
162
+ bindsym $mod+n exec i3a-trail new-trail
163
+ bindsym $mod+Shift+n exec i3a-trail delete-trail
164
+ bindsym $mod+bracketright exec i3a-trail next-trail
165
+ bindsym $mod+bracketleft exec i3a-trail previous-trail
166
+ ```
167
+
156
168
  ## Installation
157
169
 
158
170
  - [PyPI](https://pypi.org/project/i3a/)
159
171
  - [AUR](https://aur.archlinux.org/packages/i3a/) (Arch Linux - unofficial)
160
-
@@ -124,6 +124,28 @@ will cycle so it must be escaped in configuration file:
124
124
  bindsym $mod+s exec "i3a-scale-cycle -f 1.0,1.5,2.0 -o HDMI-1 -o HDMI-2 --next"
125
125
  ```
126
126
 
127
+ ### i3a-trail
128
+
129
+ Dynamically toggle trailmarks (a kind of special marks) on windows and use them
130
+ to quickly cycle between trailmarks in the same groups. It's possible to have
131
+ many different groups of trailmarks (trails) and windows may belong to many
132
+ different groups.
133
+
134
+ Use `i3a-trail new-trail|delete-trail|next-trail|previous-trail` create and
135
+ activate trails. Use `i3a-trail mark` to add and remove trailmarks. Use
136
+ `i3a-trail next|previous` to traverse marked windows.
137
+
138
+ ```
139
+ bindsym $mod+grave exec i3a-trail mark
140
+ bindsym $mod+Tab exec i3a-trail next
141
+ bindsym $mod+Shift+Tab exec i3a-trail previous
142
+
143
+ bindsym $mod+n exec i3a-trail new-trail
144
+ bindsym $mod+Shift+n exec i3a-trail delete-trail
145
+ bindsym $mod+bracketright exec i3a-trail next-trail
146
+ bindsym $mod+bracketleft exec i3a-trail previous-trail
147
+ ```
148
+
127
149
  ## Installation
128
150
 
129
151
  - [PyPI](https://pypi.org/project/i3a/)
i3a-2.4.0/justfile ADDED
@@ -0,0 +1,9 @@
1
+ help:
2
+ just --list
3
+
4
+ publish:
5
+ uv build
6
+ uv publish
7
+
8
+ clean:
9
+ rm -rf build
@@ -1,12 +1,10 @@
1
- [build-system]
2
- requires = ["poetry-core>=1.0"]
3
- build-backend = "poetry.core.masonry.api"
4
-
5
- [tool.poetry]
1
+ [project]
6
2
  name = "i3a"
7
- version = "2.3.0"
3
+ version = "2.4.0"
8
4
  description = "Automatic manager for i3 tiling"
9
- authors = ["Michal Goral <dev@goral.net.pl>"]
5
+ authors = [
6
+ { name = "Michal Goral", email = "dev@goral.net.pl" }
7
+ ]
10
8
  license = "GPL-3.0-or-later"
11
9
  classifiers = [
12
10
  "Development Status :: 5 - Production/Stable",
@@ -19,17 +17,14 @@ classifiers = [
19
17
  "Topic :: Utilities",
20
18
  ]
21
19
  readme = "README.md"
22
- packages = [
23
- { include = "i3a", from = "src" },
24
- ]
25
20
  repository = "https://git.goral.net.pl/i3a.git"
26
21
 
27
- [tool.poetry.dependencies]
28
- python = "^3.7"
29
- i3ipc = "^2.2.1"
30
- importlib-metadata = {version = "^4.0", python = "<3.8"}
22
+ requires-python = ">=3.11"
23
+ dependencies = [
24
+ "i3ipc==2.2.1",
25
+ ]
31
26
 
32
- [tool.poetry.scripts]
27
+ [project.scripts]
33
28
  i3a-master-stack = "i3a.master_stack:main"
34
29
  i3a-swallow = "i3a.swallow:main"
35
30
  i3a-move-to-empty = "i3a.move_to_empty:main"
@@ -37,3 +32,11 @@ i3a-swap = "i3a.swap:main"
37
32
  i3a-resize-compass = "i3a.resize_compass:main"
38
33
  i3a-scale-cycle = "i3a.scale_cycle:main"
39
34
  i3a-cycle-focus = "i3a.cycle_focus:main"
35
+ i3a-trail = "i3a.trail:main"
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/i3a"]
@@ -105,13 +105,21 @@ class Autotiler:
105
105
  master, stack = master_stack(tree, tree.find_focused())
106
106
 
107
107
  if master and not stack:
108
- await self.cmd('[con_id="{}"] split horizontal'.format(master.id))
108
+ # move up: we have only 1 window, which previously could be a
109
+ # stack, but now we "reinterpret" it as a master. It might still be
110
+ # inside its parent virtual split-container and we don't want
111
+ # master to be inside any virtual container, because that could
112
+ # result in accumulating these over time. 'move up' breaks free
113
+ # from it.
114
+ await self.cmd('[con_id="{}"] move up, split horizontal'.format(master.id))
109
115
  return
110
116
 
111
117
  if not master and stack:
112
118
  await self.intermove(stack[0], 'move left')
113
119
  if len(stack) > 1: # stack[0] becomes a new master, resize remains
114
120
  await self.cmd('[con_id="{}"] resize set {} ppt 0 ppt'.format(stack[-1].id, self.args.stack_size))
121
+ elif len(stack) == 1:
122
+ await self.cmd('[con_id="{}"] move up'.format(stack[0]))
115
123
 
116
124
  async def _node_added(self, tree, e):
117
125
  master, stack = master_stack(tree, e.container, exclude_node=True)
@@ -124,7 +132,13 @@ class Autotiler:
124
132
  return
125
133
 
126
134
  if not stack:
127
- await self.cmd('[con_id="{}"] split horizontal'.format(master.id))
135
+ # Split master.parent instead of master? Easy explanation: we don't
136
+ # want to split the container itself because this would create
137
+ # unnecessary parent split-container. Containers aren't
138
+ # automatically garbage collected so after closing a stack and
139
+ # creating a new one, yet another parent container would be
140
+ # created. Long sessions might accumulate quite a lot of them.
141
+ await self.cmd('[con_id="{}"] split horizontal'.format(master.parent.id))
128
142
  await self.make_stack(curr)
129
143
  await self.cmd('[con_id="{}"] resize set {} ppt 0 ppt'.format(curr.id, self.args.stack_size))
130
144
  else:
@@ -0,0 +1,322 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ # Copyright (C) 2025 Michał Góral.
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ from contextlib import suppress
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Iterator
13
+
14
+ from i3ipc import Connection
15
+
16
+ from i3a import __version__ as version
17
+
18
+ TRAYMARK = "__i3a_trail.{trailid}.{winid}"
19
+ TRAYMARK_RE = r"^__i3a_trail\.{trailid}\.{winid}$"
20
+
21
+
22
+ @dataclass
23
+ class Trail:
24
+ _id: int
25
+ _winid: int = 0
26
+ _end: int = 0
27
+
28
+ @property
29
+ def id(self):
30
+ return self._id
31
+
32
+ @property
33
+ def winid(self):
34
+ return self._winid
35
+
36
+ @winid.setter
37
+ def winid(self, val: int):
38
+ assert val >= 0
39
+ self._end = max(self._end, val + 1)
40
+ self._winid = val
41
+
42
+ @property
43
+ def end(self):
44
+ return self._end
45
+
46
+ @end.setter
47
+ def end(self, val: int):
48
+ assert val == 0 or val > self._winid
49
+ self._end = val
50
+ self._winid = min(self._winid, val)
51
+
52
+ @property
53
+ def deleted(self):
54
+ return self._end == 0
55
+
56
+ def __iter__(self):
57
+ i = self.winid + 1
58
+ yield from range(i, self.end)
59
+ yield from range(0, i)
60
+
61
+ def __reversed__(self):
62
+ i = self.winid - 1
63
+ yield from range(i, -1, -1)
64
+ yield from range(self.end - 1, i, -1)
65
+
66
+
67
+ # trail format:
68
+ # <current_trail>
69
+ # <trail-0-winid><space><trail-0-end>
70
+ # <trail-1-winid><space><trail-1-end>
71
+ # ...
72
+ @dataclass
73
+ class Db:
74
+ _tid: int = 0
75
+ _trails: list[Trail] = field(default_factory=list)
76
+
77
+ def __post_init__(self):
78
+ if self._tid > len(self._trails):
79
+ self._tid = 0
80
+
81
+ def trail(self, idx: int | None = None) -> Trail | None:
82
+ if idx is None:
83
+ idx = self._tid
84
+ if len(self._trails) <= idx:
85
+ return None
86
+ return self._trails[idx]
87
+
88
+ def add_trail(self, *args, **kwargs) -> Trail:
89
+ trail_id = len(self._trails)
90
+ self._trails.append(Trail(trail_id, *args, **kwargs))
91
+ return self._trails[-1]
92
+
93
+ def delete_trail(self, idx: int | None = None):
94
+ trail = self.trail(idx)
95
+ if trail:
96
+ trail.winid = 0
97
+ trail.end = 0
98
+
99
+ def seek(self, idx: int) -> Trail:
100
+ assert idx >= 0
101
+
102
+ for _ in range(idx - len(self._trails) + 1):
103
+ self.add_trail(0, 0)
104
+
105
+ self._tid = idx
106
+ return self._trails[self._tid]
107
+
108
+ def __iter__(self) -> Iterator[Trail]:
109
+ i = self._tid + 1
110
+ yield from self._trails[i:]
111
+ yield from self._trails[:i]
112
+
113
+ def __reversed__(self) -> Iterator[Trail]:
114
+ i = self._tid
115
+ yield from reversed(self._trails[:i])
116
+ yield from reversed(self._trails[i:])
117
+
118
+ def save(self, path: Path | None = None):
119
+ lines = [str(self._tid)]
120
+ for i, trail in enumerate(self._trails):
121
+ if trail.end == 0 and i > self._tid:
122
+ continue
123
+ lines.append(f"{trail.winid} {trail.end}")
124
+
125
+ text = "\n".join(lines) + "\n"
126
+ path = path or Db.dbpath()
127
+ path.write_text(text)
128
+
129
+ @staticmethod
130
+ def dbpath() -> Path:
131
+ tmpdir = tempfile.gettempdir()
132
+ uid = os.getuid()
133
+ sock = get_sockname()
134
+ return Path(tmpdir) / f"i3a-trail-{uid}-{sock}"
135
+
136
+ @classmethod
137
+ def from_file(cls, path: Path | None = None) -> "Db":
138
+ path = path or Db.dbpath()
139
+ lines = path.read_text().splitlines()
140
+ db = Db()
141
+ for line in lines[1:]:
142
+ winid, _, end = line.strip().partition(" ")
143
+ db.add_trail(int(winid), int(end))
144
+ db.seek(int(lines[0].strip()))
145
+ return db
146
+
147
+
148
+ def get_sockname():
149
+ path = os.environ.get("I3SOCK", os.environ.get("SWAYSOCK", ""))
150
+ return os.path.splitext(os.path.basename(path))[0]
151
+
152
+
153
+ def seek_next_trail(db: Db, iter_fn) -> Trail | None:
154
+ dbit = iter_fn(db)
155
+ for trail in dbit:
156
+ if not trail.deleted:
157
+ return db.seek(trail.id)
158
+ return None
159
+
160
+
161
+ def find_mark_with_prefix(marks: list[str], prefix: str) -> str | None:
162
+ for m in marks:
163
+ if m.startswith(prefix):
164
+ return m
165
+ return None
166
+
167
+
168
+ def get_winid_of_trail_from_marks(marks: list[str], trailid: int) -> int | None:
169
+ prefix = TRAYMARK.format(trailid=trailid, winid="")
170
+ mark = find_mark_with_prefix(marks, prefix)
171
+ if mark:
172
+ with suppress(ValueError):
173
+ return int(mark.split(".")[-1])
174
+ return None
175
+
176
+
177
+ def jump_to_next_trailmark(db: Db, iter_fn) -> bool:
178
+ curr = db.trail()
179
+ if curr is None:
180
+ return False
181
+
182
+ i3 = Connection()
183
+ tree = i3.get_tree()
184
+
185
+ # set winid from mark of currently focused window to use it as a starting point
186
+ if focused := tree.find_focused():
187
+ focused_winid = get_winid_of_trail_from_marks(focused.marks, curr.id)
188
+ if focused_winid is not None:
189
+ curr.winid = focused_winid
190
+
191
+ trail_iterator = iter_fn(curr)
192
+
193
+ for winid in trail_iterator:
194
+ mk_re = TRAYMARK_RE.format(trailid=curr.id, winid=winid)
195
+ for container in tree.find_marked(mk_re):
196
+ if container.focused:
197
+ continue
198
+ repl = container.command("focus")
199
+ if repl[0].success:
200
+ curr.winid = winid
201
+ return True
202
+ return False
203
+
204
+
205
+ def cmd_new_trail(args, db: Db):
206
+ trail = db.add_trail(0, 0)
207
+ db.seek(trail.id)
208
+ db.save(args.db)
209
+
210
+
211
+ def cmd_delete_trail(args, db: Db):
212
+ curr = db.trail()
213
+ if curr:
214
+ i3 = Connection()
215
+ tree = i3.get_tree()
216
+ mk = TRAYMARK.format(trailid=curr.id, winid="")
217
+ mk_re = TRAYMARK_RE.format(trailid=curr.id, winid=".*")
218
+ for container in tree.find_marked(mk_re):
219
+ for mark in container.marks:
220
+ if mark.startswith(mk):
221
+ container.command(f"mark --add --toggle {mark}")
222
+
223
+ db.delete_trail()
224
+ next_trail = seek_next_trail(db, reversed)
225
+ if not next_trail:
226
+ db.seek(0)
227
+
228
+ db.save(args.db)
229
+
230
+
231
+ def cmd_next_trail(args, db: Db):
232
+ seek_next_trail(db, iter)
233
+ db.save(args.db)
234
+
235
+
236
+ def cmd_prev_trail(args, db: Db):
237
+ seek_next_trail(db, reversed)
238
+ db.save(args.db)
239
+
240
+
241
+ def cmd_mark(args, db: Db):
242
+ trail = db.trail()
243
+ if trail is None:
244
+ trail = db.add_trail()
245
+
246
+ i3 = Connection()
247
+ focused = i3.get_tree().find_focused()
248
+ if not focused:
249
+ sys.exit(1)
250
+
251
+ prefix = TRAYMARK.format(trailid=trail.id, winid="")
252
+ if to_remove := find_mark_with_prefix(focused.marks, prefix):
253
+ focused.command(f"mark --add --toggle {to_remove}")
254
+ else:
255
+ newmark = TRAYMARK.format(trailid=trail.id, winid=trail.end)
256
+ repl = focused.command(f"mark --add {newmark}")
257
+ if not repl[0].success:
258
+ sys.exit(1)
259
+ trail.winid = trail.end
260
+ db.save(args.db)
261
+
262
+
263
+ def cmd_next(args, db: Db):
264
+ if not jump_to_next_trailmark(db, iter):
265
+ sys.exit(1)
266
+ db.save(args.db)
267
+
268
+
269
+ def cmd_previous(args, db: Db):
270
+ if not jump_to_next_trailmark(db, reversed):
271
+ sys.exit(1)
272
+ db.save(args.db)
273
+
274
+
275
+ def prepare_args():
276
+ parser = argparse.ArgumentParser(
277
+ description="i3a-trail - quick jump between window trailmarks"
278
+ )
279
+
280
+ sp = parser.add_subparsers(required=True)
281
+
282
+ new_trail_sp = sp.add_parser("new-trail", help="create new trail and use it")
283
+ new_trail_sp.set_defaults(func=cmd_new_trail)
284
+
285
+ delete_trail_sp = sp.add_parser(
286
+ "delete-trail", help="delete current trail and switch to the previous one"
287
+ )
288
+ delete_trail_sp.set_defaults(func=cmd_delete_trail)
289
+
290
+ next_trail_sp = sp.add_parser("next-trail", help="use next trail")
291
+ next_trail_sp.set_defaults(func=cmd_next_trail)
292
+
293
+ prev_trail_sp = sp.add_parser("previous-trail", help="use previous trail")
294
+ prev_trail_sp.set_defaults(func=cmd_prev_trail)
295
+
296
+ mark_sp = sp.add_parser(
297
+ "mark", help="toggle a trail mark on currently focused window"
298
+ )
299
+ mark_sp.set_defaults(func=cmd_mark)
300
+
301
+ next_sp = sp.add_parser("next", help="jump to the next window in current trail")
302
+ next_sp.set_defaults(func=cmd_next)
303
+
304
+ previous_sp = sp.add_parser(
305
+ "previous", help="jump to the previous window in current trail"
306
+ )
307
+ previous_sp.set_defaults(func=cmd_previous)
308
+
309
+ parser.add_argument("--db", help="path to trail marks database")
310
+ parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
311
+
312
+ return parser.parse_args()
313
+
314
+
315
+ def main():
316
+ args = prepare_args()
317
+ try:
318
+ db = Db.from_file(args.db)
319
+ except (IndexError, FileNotFoundError):
320
+ db = Db()
321
+
322
+ args.func(args, db)
@@ -0,0 +1,2 @@
1
+ import i3a.trail as trail
2
+
i3a-2.4.0/uv.lock ADDED
@@ -0,0 +1,47 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "i3a"
7
+ version = "2.4.0"
8
+ source = { editable = "." }
9
+ dependencies = [
10
+ { name = "i3ipc" },
11
+ ]
12
+
13
+ [package.metadata]
14
+ requires-dist = [{ name = "i3ipc", specifier = "==2.2.1" }]
15
+
16
+ [[package]]
17
+ name = "i3ipc"
18
+ version = "2.2.1"
19
+ source = { registry = "https://pypi.org/simple" }
20
+ dependencies = [
21
+ { name = "python-xlib" },
22
+ ]
23
+ sdist = { url = "https://files.pythonhosted.org/packages/61/f3/dfab70c888d85d3e933ff4d6b351aaed0ae137a29c896e03e364de3bec94/i3ipc-2.2.1.tar.gz", hash = "sha256:e880d7d7147959ead5cb34764f08b97b41385b36eb8256e8af1ce163dbcccce8", size = 47760 }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/de/30/294b07ddeccb58855c890c3ef3a951c3b0c1e2d089666d548b6a9edc39fb/i3ipc-2.2.1-py3-none-any.whl", hash = "sha256:c0b898223d50d42c90c818deb5033d1304c582755547dee7d15df3e3781bc690", size = 26591 },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "python-xlib"
30
+ version = "0.33"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ dependencies = [
33
+ { name = "six" },
34
+ ]
35
+ sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068 }
36
+ wheels = [
37
+ { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185 },
38
+ ]
39
+
40
+ [[package]]
41
+ name = "six"
42
+ version = "1.17.0"
43
+ source = { registry = "https://pypi.org/simple" }
44
+ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
47
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes