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 +24 -0
- {i3a-2.3.0 → i3a-2.4.0}/PKG-INFO +29 -18
- {i3a-2.3.0 → i3a-2.4.0}/README.md +22 -0
- i3a-2.4.0/justfile +9 -0
- {i3a-2.3.0 → i3a-2.4.0}/pyproject.toml +18 -15
- {i3a-2.3.0 → i3a-2.4.0}/src/i3a/master_stack.py +16 -2
- i3a-2.4.0/src/i3a/trail.py +322 -0
- i3a-2.4.0/tests/test_trail.py +2 -0
- i3a-2.4.0/uv.lock +47 -0
- {i3a-2.3.0 → i3a-2.4.0}/LICENSE +0 -0
- {i3a-2.3.0 → i3a-2.4.0}/src/i3a/__init__.py +0 -0
- {i3a-2.3.0 → i3a-2.4.0}/src/i3a/cycle_focus.py +0 -0
- {i3a-2.3.0 → i3a-2.4.0}/src/i3a/move_to_empty.py +0 -0
- {i3a-2.3.0 → i3a-2.4.0}/src/i3a/resize_compass.py +0 -0
- {i3a-2.3.0 → i3a-2.4.0}/src/i3a/scale_cycle.py +0 -0
- {i3a-2.3.0 → i3a-2.4.0}/src/i3a/swallow.py +0 -0
- {i3a-2.3.0 → i3a-2.4.0}/src/i3a/swap.py +0 -0
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/*
|
{i3a-2.3.0 → i3a-2.4.0}/PKG-INFO
RENAMED
|
@@ -1,30 +1,20 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: i3a
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Automatic manager for i3 tiling
|
|
5
|
-
|
|
6
|
-
License: GPL-3.0-or-later
|
|
7
|
-
|
|
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-
|
|
26
|
-
Requires-Dist:
|
|
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
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
[
|
|
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
|
+
version = "2.4.0"
|
|
8
4
|
description = "Automatic manager for i3 tiling"
|
|
9
|
-
authors = [
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
i3ipc
|
|
30
|
-
|
|
22
|
+
requires-python = ">=3.11"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"i3ipc==2.2.1",
|
|
25
|
+
]
|
|
31
26
|
|
|
32
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
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
|
+
]
|
{i3a-2.3.0 → i3a-2.4.0}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|