| """ |
| vscode_controller.py |
| ==================== |
| Handles VS Code-specific automation: |
| - Create / open files and folders |
| - Write, append, or prepend code into files |
| - Run commands in the integrated terminal |
| - Install / list extensions |
| |
| Dependencies: code CLI, wmctrl, xdotool |
| """ |
|
|
| import os |
| import time |
| import subprocess |
| from pathlib import Path |
|
|
|
|
| |
|
|
| def open_in_vscode(path: str, new_window: bool = False) -> dict: |
| """Open a file or folder in VS Code.""" |
| expanded = os.path.expanduser(path) |
| if not os.path.exists(expanded): |
| return {"success": False, "message": f"Path not found: '{path}'", "action": "error"} |
|
|
| cmd = ["code"] + (["--new-window"] if new_window else []) + [expanded] |
| subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, |
| start_new_session=True) |
| return {"success": True, "message": f"Opened '{path}' in VS Code.", |
| "action": "opened", "path": expanded} |
|
|
|
|
| def create_file(filepath: str, content: str = "", open_after: bool = True) -> dict: |
| |
| if not filepath or not filepath.strip(): |
| return {"success": False, "message": "No file path provided.", "action": "error"} |
|
|
| expanded = os.path.expanduser(filepath) |
|
|
| |
| if not os.path.isabs(expanded): |
| return {"success": False, |
| "message": f"Could not resolve path: '{filepath}'. Please provide a full path.", |
| "action": "error"} |
|
|
| try: |
| Path(expanded).parent.mkdir(parents=True, exist_ok=True) |
| Path(expanded).write_text(content, encoding="utf-8") |
|
|
| |
| if not Path(expanded).exists(): |
| return {"success": False, "message": f"File write failed: '{expanded}'", "action": "error"} |
|
|
| if open_after: |
| subprocess.Popen( |
| ["code", "--goto", expanded], |
| stdout=subprocess.DEVNULL, |
| stderr=subprocess.DEVNULL, |
| start_new_session=True, |
| ) |
|
|
| return {"success": True, "message": f"Created '{expanded}'.", |
| "action": "created", "path": expanded} |
|
|
| except Exception as e: |
| return {"success": False, "message": f"Failed to create file: {e}", "action": "error"} |
|
|
|
|
| def create_folder(folderpath: str, open_after: bool = True) -> dict: |
| """Create a folder and open it as a VS Code workspace.""" |
| expanded = os.path.expanduser(folderpath) |
| try: |
| Path(expanded).mkdir(parents=True, exist_ok=True) |
| if open_after: |
| subprocess.Popen(["code", expanded], |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, |
| start_new_session=True) |
| return {"success": True, "message": f"Created folder '{folderpath}'.", |
| "action": "folder_created", "path": expanded} |
| except Exception as e: |
| return {"success": False, "message": f"Failed to create folder: {e}", "action": "error"} |
|
|
|
|
| def write_to_file(filepath: str, content: str, mode: str = "overwrite") -> dict: |
| """ |
| Write content to an existing file. |
| mode: 'overwrite' | 'append' | 'prepend' |
| """ |
| expanded = os.path.expanduser(filepath) |
| p = Path(expanded) |
| if not p.exists(): |
| return {"success": False, "message": f"File not found: '{filepath}'", "action": "error"} |
|
|
| try: |
| if mode == "overwrite": |
| p.write_text(content, encoding="utf-8") |
| elif mode == "append": |
| with open(expanded, "a", encoding="utf-8") as f: |
| f.write("\n" + content) |
| elif mode == "prepend": |
| existing = p.read_text(encoding="utf-8") |
| p.write_text(content + "\n" + existing, encoding="utf-8") |
| else: |
| return {"success": False, "message": f"Unknown write mode: '{mode}'", "action": "error"} |
|
|
| return {"success": True, "message": f"Written to '{filepath}' ({mode}).", |
| "action": "written", "path": expanded, "mode": mode} |
| except Exception as e: |
| return {"success": False, "message": f"Failed to write: {e}", "action": "error"} |
|
|
|
|
| |
|
|
| def run_in_terminal(command: str) -> dict: |
| """ |
| Focus VS Code, open the integrated terminal, and run a command. |
| Uses wmctrl + xdotool. |
| """ |
| try: |
| |
| subprocess.run(["wmctrl", "-x", "-a", "code.Code"], |
| capture_output=True, timeout=5) |
| time.sleep(0.6) |
|
|
| |
| subprocess.run(["xdotool", "key", "ctrl+grave"], timeout=5) |
| time.sleep(0.8) |
|
|
| |
| subprocess.run( |
| ["xdotool", "type", "--clearmodifiers", "--delay", "40", command], |
| timeout=15 |
| ) |
| time.sleep(0.2) |
| subprocess.run(["xdotool", "key", "Return"], timeout=5) |
|
|
| return {"success": True, "message": f"Ran in terminal: `{command}`", |
| "action": "terminal_run", "command": command} |
| except Exception as e: |
| return {"success": False, "message": f"Terminal run failed: {e}", "action": "error"} |
|
|
|
|
| |
|
|
| def install_extension(extension_id: str) -> dict: |
| """Install a VS Code extension by its marketplace ID (e.g. 'ms-python.python').""" |
| try: |
| result = subprocess.run( |
| ["code", "--install-extension", extension_id, "--force"], |
| capture_output=True, text=True, timeout=120 |
| ) |
| if result.returncode == 0: |
| return {"success": True, |
| "message": f"Installed extension '{extension_id}'.", |
| "action": "extension_installed", "extension": extension_id} |
| return {"success": False, "message": result.stderr.strip() or "Install failed.", |
| "action": "error"} |
| except Exception as e: |
| return {"success": False, "message": f"Extension install failed: {e}", "action": "error"} |
|
|
|
|
| def list_extensions() -> dict: |
| """List all installed VS Code extensions.""" |
| try: |
| result = subprocess.run(["code", "--list-extensions"], |
| capture_output=True, text=True, timeout=15) |
| exts = [e.strip() for e in result.stdout.strip().splitlines() if e.strip()] |
| return {"success": True, "extensions": exts, "count": len(exts)} |
| except Exception as e: |
| return {"success": False, "message": str(e), "extensions": []} |
| |
|
|
| def get_open_vscode_windows() -> list[dict]: |
| """ |
| Returns list of open VS Code windows with their workspace names. |
| Each: {"id": "0x04000004", "title": "AI OS", "full_title": "main_agent.py - AI OS - Visual Studio Code"} |
| """ |
| try: |
| result = subprocess.run( |
| ["wmctrl", "-l", "-x"], |
| capture_output=True, text=True, timeout=5 |
| ) |
| windows = [] |
| for line in result.stdout.strip().splitlines(): |
| parts = line.split(None, 4) |
| if len(parts) < 5: |
| continue |
| win_class = parts[2].lower() |
| title = parts[4].strip() |
| if "code.code" in win_class or "visual studio code" in title.lower(): |
| |
| |
| segments = [s.strip() for s in title.split(" - ")] |
| workspace = segments[-2] if len(segments) >= 2 else title |
| windows.append({ |
| "id": parts[0], |
| "title": workspace, |
| "full_title": title, |
| }) |
| return windows |
| except Exception as e: |
| print(f"[VSCodeController] Window list failed: {e}") |
| return [] |
|
|
|
|
| def focus_vscode_window(window_id: str) -> dict: |
| """Focus a specific VS Code window by its wmctrl ID.""" |
| try: |
| subprocess.run( |
| ["wmctrl", "-i", "-a", window_id], |
| capture_output=True, timeout=5 |
| ) |
| return {"success": True, "message": f"Focused window {window_id}"} |
| except Exception as e: |
| return {"success": False, "message": str(e)} |