devspace 0.1.0__tar.gz → 0.1.1__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.
- {devspace-0.1.0 → devspace-0.1.1}/PKG-INFO +2 -2
- {devspace-0.1.0 → devspace-0.1.1}/README.md +1 -1
- {devspace-0.1.0 → devspace-0.1.1}/devspace/__init__.py +1 -1
- {devspace-0.1.0 → devspace-0.1.1}/devspace/cli.py +54 -1
- {devspace-0.1.0 → devspace-0.1.1}/devspace/orchestrator.py +91 -0
- {devspace-0.1.0 → devspace-0.1.1}/devspace.egg-info/PKG-INFO +2 -2
- {devspace-0.1.0 → devspace-0.1.1}/pyproject.toml +1 -1
- {devspace-0.1.0 → devspace-0.1.1}/tests/test_devspace.py +40 -1
- {devspace-0.1.0 → devspace-0.1.1}/devspace/config.py +0 -0
- {devspace-0.1.0 → devspace-0.1.1}/devspace.egg-info/SOURCES.txt +0 -0
- {devspace-0.1.0 → devspace-0.1.1}/devspace.egg-info/dependency_links.txt +0 -0
- {devspace-0.1.0 → devspace-0.1.1}/devspace.egg-info/entry_points.txt +0 -0
- {devspace-0.1.0 → devspace-0.1.1}/devspace.egg-info/top_level.txt +0 -0
- {devspace-0.1.0 → devspace-0.1.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devspace
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A clean workflow and context orchestrator for minimalist Linux setups.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "devspace"
|
|
16
|
-
version = "0.1.
|
|
16
|
+
version = "0.1.1"
|
|
17
17
|
description = "A clean workflow and context orchestrator for minimalist Linux setups."
|
|
18
18
|
dependencies = [
|
|
19
19
|
"customtkinter>=5.2.0",
|
|
@@ -6,7 +6,16 @@ import sys
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from .config import create_space, load_project_config
|
|
9
|
-
from .orchestrator import
|
|
9
|
+
from .orchestrator import (
|
|
10
|
+
hotkey_snippet,
|
|
11
|
+
launch_workspace,
|
|
12
|
+
quick_run,
|
|
13
|
+
record_layout_loop,
|
|
14
|
+
restore_saved_layout,
|
|
15
|
+
run_session_launch,
|
|
16
|
+
save_current_layout,
|
|
17
|
+
status_ribbon,
|
|
18
|
+
)
|
|
10
19
|
|
|
11
20
|
|
|
12
21
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -16,6 +25,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
16
25
|
)
|
|
17
26
|
subparsers = parser.add_subparsers(dest="command")
|
|
18
27
|
|
|
28
|
+
if argv is None:
|
|
29
|
+
argv = sys.argv[1:]
|
|
30
|
+
if not argv:
|
|
31
|
+
project = Path.cwd()
|
|
32
|
+
try:
|
|
33
|
+
return record_layout_loop(project, "default", interval=1.0)
|
|
34
|
+
except RuntimeError as exc:
|
|
35
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
36
|
+
return 1
|
|
37
|
+
|
|
19
38
|
create_parser = subparsers.add_parser("create", help="Create a new devspace")
|
|
20
39
|
create_parser.add_argument("name", help="Name of the devspace to create")
|
|
21
40
|
|
|
@@ -32,6 +51,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
32
51
|
quick_parser.add_argument("project", nargs="?", default=".", help="Project directory or space name")
|
|
33
52
|
quick_parser.add_argument("index", nargs="?", default="0", help="Zero-based index of the quick run command")
|
|
34
53
|
|
|
54
|
+
record_parser = subparsers.add_parser("record", help="Record the current window arrangement to a layout file")
|
|
55
|
+
record_parser.add_argument("project", nargs="?", default=".", help="Project directory or space name")
|
|
56
|
+
record_parser.add_argument("name", nargs="?", default="default", help="Saved layout name")
|
|
57
|
+
|
|
58
|
+
restore_parser = subparsers.add_parser("restore", help="Restore a previously saved window arrangement")
|
|
59
|
+
restore_parser.add_argument("project", nargs="?", default=".", help="Project directory or space name")
|
|
60
|
+
restore_parser.add_argument("name", nargs="?", default="default", help="Saved layout name")
|
|
61
|
+
|
|
62
|
+
hotkey_parser = subparsers.add_parser("hotkey", help="Print a Ctrl+D+S shortcut snippet")
|
|
63
|
+
|
|
35
64
|
args = parser.parse_args(argv)
|
|
36
65
|
|
|
37
66
|
if args.command == "create":
|
|
@@ -80,6 +109,30 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
80
109
|
return 1
|
|
81
110
|
return quick_run(project, config, int(args.index))
|
|
82
111
|
|
|
112
|
+
if args.command == "record":
|
|
113
|
+
project = Path(args.project)
|
|
114
|
+
try:
|
|
115
|
+
target = save_current_layout(project, args.name)
|
|
116
|
+
print(f"Saved layout '{args.name}' to {target}")
|
|
117
|
+
return 0
|
|
118
|
+
except RuntimeError as exc:
|
|
119
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
if args.command == "restore":
|
|
123
|
+
project = Path(args.project)
|
|
124
|
+
try:
|
|
125
|
+
layout = restore_saved_layout(project, args.name)
|
|
126
|
+
print(json.dumps(layout, indent=2, sort_keys=True))
|
|
127
|
+
return 0
|
|
128
|
+
except FileNotFoundError as exc:
|
|
129
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
if args.command == "hotkey":
|
|
133
|
+
print(hotkey_snippet())
|
|
134
|
+
return 0
|
|
135
|
+
|
|
83
136
|
parser.print_help()
|
|
84
137
|
return 0
|
|
85
138
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import shutil
|
|
5
6
|
import socket
|
|
@@ -158,3 +159,93 @@ def run_session_launch(project_dir: str | Path, config: dict[str, Any]) -> None:
|
|
|
158
159
|
stderr=subprocess.DEVNULL,
|
|
159
160
|
start_new_session=True,
|
|
160
161
|
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def save_current_layout(project_dir: str | Path, name: str = "default") -> Path:
|
|
165
|
+
"""Capture the current X11 window layout and save it for later restoration."""
|
|
166
|
+
project_path = Path(project_dir)
|
|
167
|
+
if shutil.which("wmctrl") is None:
|
|
168
|
+
raise RuntimeError("wmctrl is required to record window layouts on Linux")
|
|
169
|
+
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
["wmctrl", "-lG"],
|
|
172
|
+
capture_output=True,
|
|
173
|
+
text=True,
|
|
174
|
+
check=False,
|
|
175
|
+
)
|
|
176
|
+
if result.returncode != 0:
|
|
177
|
+
raise RuntimeError(f"Unable to inspect windows: {result.stderr.strip() or result.stdout.strip()}")
|
|
178
|
+
|
|
179
|
+
rows = []
|
|
180
|
+
for line in result.stdout.splitlines():
|
|
181
|
+
parts = line.split()
|
|
182
|
+
if len(parts) < 6:
|
|
183
|
+
continue
|
|
184
|
+
win_id, desktop, x, y, width, height, *rest = parts[:7]
|
|
185
|
+
rows.append({
|
|
186
|
+
"window_id": win_id,
|
|
187
|
+
"desktop": desktop,
|
|
188
|
+
"x": int(x),
|
|
189
|
+
"y": int(y),
|
|
190
|
+
"width": int(width),
|
|
191
|
+
"height": int(height),
|
|
192
|
+
"label": " ".join(rest),
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
save_dir = project_path / ".devspace"
|
|
196
|
+
save_dir.mkdir(exist_ok=True)
|
|
197
|
+
target = save_dir / f"layout-{name}.json"
|
|
198
|
+
target.write_text(json.dumps({"name": name, "windows": rows}, indent=2), encoding="utf-8")
|
|
199
|
+
return target
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_saved_layout(project_dir: str | Path, name: str = "default") -> dict[str, Any]:
|
|
203
|
+
"""Load a saved window layout from disk."""
|
|
204
|
+
project_path = Path(project_dir)
|
|
205
|
+
target = project_path / ".devspace" / f"layout-{name}.json"
|
|
206
|
+
if not target.exists():
|
|
207
|
+
raise FileNotFoundError(f"No saved layout named '{name}' found in {project_path}")
|
|
208
|
+
return json.loads(target.read_text(encoding="utf-8"))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def restore_saved_layout(project_dir: str | Path, name: str = "default") -> dict[str, Any]:
|
|
212
|
+
"""Restore a previously saved X11 layout with wmctrl."""
|
|
213
|
+
payload = load_saved_layout(project_dir, name)
|
|
214
|
+
for window in payload.get("windows", []):
|
|
215
|
+
if shutil.which("wmctrl") is None:
|
|
216
|
+
continue
|
|
217
|
+
subprocess.run(
|
|
218
|
+
[
|
|
219
|
+
"wmctrl",
|
|
220
|
+
"-i",
|
|
221
|
+
"-r",
|
|
222
|
+
window["window_id"],
|
|
223
|
+
"-e",
|
|
224
|
+
f"0,{window['x']},{window['y']},{window['width']},{window['height']}",
|
|
225
|
+
],
|
|
226
|
+
check=False,
|
|
227
|
+
stdout=subprocess.DEVNULL,
|
|
228
|
+
stderr=subprocess.DEVNULL,
|
|
229
|
+
)
|
|
230
|
+
return payload
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def hotkey_snippet() -> str:
|
|
234
|
+
"""Return a simple xbindkeys-style binding for Ctrl + D + S."""
|
|
235
|
+
return """
|
|
236
|
+
# ~/.xbindkeysrc
|
|
237
|
+
"control + d + s": /usr/bin/env python3 -m devspace.cli record
|
|
238
|
+
""".strip()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def record_layout_loop(project_dir: str | Path, name: str = "default", interval: float = 1.0) -> int:
|
|
242
|
+
"""Continuously record the current layout while the user is working; used by the default devspace mode."""
|
|
243
|
+
project_path = Path(project_dir)
|
|
244
|
+
while True:
|
|
245
|
+
try:
|
|
246
|
+
save_current_layout(project_path, name)
|
|
247
|
+
except RuntimeError:
|
|
248
|
+
break
|
|
249
|
+
import time
|
|
250
|
+
time.sleep(interval)
|
|
251
|
+
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devspace
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A clean workflow and context orchestrator for minimalist Linux setups.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "devspace"
|
|
16
|
-
version = "0.1.
|
|
16
|
+
version = "0.1.1"
|
|
17
17
|
description = "A clean workflow and context orchestrator for minimalist Linux setups."
|
|
18
18
|
dependencies = [
|
|
19
19
|
"customtkinter>=5.2.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from devspace.cli import main
|
|
2
2
|
from devspace.config import create_space, load_project_config
|
|
3
|
-
from devspace.orchestrator import launch_workspace
|
|
3
|
+
from devspace.orchestrator import launch_workspace, save_current_layout
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def test_load_project_config(tmp_path):
|
|
@@ -87,3 +87,42 @@ command = "bash -lc 'echo command-opened'"
|
|
|
87
87
|
|
|
88
88
|
assert any("terminal-opened" in str(call[0]) for call in calls)
|
|
89
89
|
assert any("example.com" in str(call[0]) for call in calls)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_save_current_layout_writes_file(tmp_path, monkeypatch):
|
|
93
|
+
project_dir = create_space("demo", tmp_path)
|
|
94
|
+
|
|
95
|
+
def fake_run(args, **kwargs):
|
|
96
|
+
class Result:
|
|
97
|
+
returncode = 0
|
|
98
|
+
stdout = "0x1 0 10 20 200 100 terminal\n0x2 0 400 20 200 100 browser\n"
|
|
99
|
+
|
|
100
|
+
return Result()
|
|
101
|
+
|
|
102
|
+
monkeypatch.setattr("devspace.orchestrator.subprocess.run", fake_run)
|
|
103
|
+
monkeypatch.setattr("devspace.orchestrator.shutil.which", lambda name: "/usr/bin/wmctrl" if name == "wmctrl" else None)
|
|
104
|
+
|
|
105
|
+
path = save_current_layout(project_dir, "default")
|
|
106
|
+
|
|
107
|
+
assert path.exists()
|
|
108
|
+
assert "terminal" in path.read_text()
|
|
109
|
+
assert "browser" in path.read_text()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_main_starts_recording_when_run_without_command(monkeypatch, tmp_path):
|
|
113
|
+
monkeypatch.chdir(tmp_path)
|
|
114
|
+
seen = {}
|
|
115
|
+
|
|
116
|
+
def fake_record(project_dir, name="default", interval=1.0):
|
|
117
|
+
seen["project_dir"] = str(project_dir)
|
|
118
|
+
seen["name"] = name
|
|
119
|
+
seen["interval"] = interval
|
|
120
|
+
return 7
|
|
121
|
+
|
|
122
|
+
monkeypatch.setattr("devspace.cli.record_layout_loop", fake_record)
|
|
123
|
+
|
|
124
|
+
exit_code = main([])
|
|
125
|
+
|
|
126
|
+
assert exit_code == 7
|
|
127
|
+
assert seen["project_dir"] == str(tmp_path)
|
|
128
|
+
assert seen["name"] == "default"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|