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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devspace
3
- Version: 0.1.0
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.0"
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,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "devspace"
9
- version = "0.1.0"
9
+ version = "0.1.1"
10
10
  description = "A clean workflow and context orchestrator for minimalist Linux setups."
11
11
  dependencies = [
12
12
  "customtkinter>=5.2.0",
@@ -1,4 +1,4 @@
1
1
  """devspace package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.1.0"
4
+ __version__ = "0.1.1"
@@ -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 launch_workspace, quick_run, run_session_launch, status_ribbon
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.0
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.0"
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",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devspace"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "A clean workflow and context orchestrator for minimalist Linux setups."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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