From a35b2879823ae11aed166d20dcd46152ba4ecaf3 Mon Sep 17 00:00:00 2001 From: lochan m Date: Wed, 24 Sep 2025 23:41:47 +0530 Subject: [PATCH] Update wsl_tools.py The WSL GUI Application Manager is a comprehensive Python library designed to bridge the gap between Windows Subsystem for Linux (WSL) distributions and the Windows desktop environment. It provides seamless management of GUI applications, desktop themes, and environment configuration for WSL distributions, enabling users to run Linux GUI applications on Windows with native-like integration --- wsl_tools.py | 421 +++++++++++++++++++++++++++++---------------------- 1 file changed, 241 insertions(+), 180 deletions(-) diff --git a/wsl_tools.py b/wsl_tools.py index 0e734f2..0239980 100644 --- a/wsl_tools.py +++ b/wsl_tools.py @@ -1,191 +1,252 @@ import os import subprocess -import time - -script = None - - -def pat_con(path): - if "/" in path: - pt = path.split("/") - else: - pt = path.split("\\") - lin = "/mnt/" + pt[0][0].lower() - for f in pt[1:]: - lin += "/" + str(f.lower()) - return lin - - -def get_themes(machine): - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" listthemes' - read = os.popen(cmd).read() - read = read.split(":theme:") - read[:] = (value for value in read if value != "\n") - read[:] = (value for value in read if value != "\n\n") - themes = [] - for theme in read: - if theme.startswith("\n"): - theme = theme[1:] - if theme == "/usr/share/themes": - continue - string = "/usr/share/themes/" - if string in theme: - theme = theme[theme.index(string) + len(string):] - string2 = ".themes/" - if string2 in theme: - theme = theme[theme.index(string2) + len(string2):] - themes.append(theme) - - return themes +import logging +from pathlib import Path, PureWindowsPath, PurePosixPath +from xdg.DesktopEntry import DesktopEntry +from typing import Dict, List, Optional, Any +# Configure logging +logger = logging.getLogger(__name__) -from xdg.DesktopEntry import DesktopEntry -class WSLApp: #Credit to @sanzoghenzo on github. I did some adapting - def from_dotdesktop(app_def): - """ - Return a WSLApp from a .desktop file. - - Args: - app_def: .desktop file path - """ - de = DesktopEntry(app_def) - name = de.getName() - generic_name = de.getGenericName() - cmd = de.getExec() - gui = not de.getTerminal() - icon = de.getIcon() - - return {"name":name, "generic_name":generic_name, "cmd":cmd, "gui":gui, "icon":icon} - - #raise IOError("Cannot read the .desktop entry") - - -def get_apps(machine, logger=None): - #first make sure the machine is booted. Scanning should do the trick - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" listapps' - read = os.popen(cmd).read() - apps = read.splitlines() - #apps.remove("") - """ - if logger != None: - print(read) - print(apps) - print(cmd) - logger.info(f"app_launcher " + str(apps)) - logger.info(f"app_launcher " + str(read)) - """ - app_dict = {} - for app in apps: - if "screensaver" in app: - continue +class WSLPathConverter: + """Handles path conversion between Windows and WSL paths.""" + + @staticmethod + def windows_to_wsl(windows_path: str) -> str: + """Convert Windows path to WSL path.""" + try: + path = Path(windows_path) + if path.is_absolute() and path.drive: + # Convert absolute Windows path to WSL path + drive_letter = path.drive[0].lower() + wsl_path = PurePosixPath(f"/mnt/{drive_letter}") + return str(wsl_path / PurePosixPath(*path.parts[1:])) + return str(PurePosixPath(path)) + except (AttributeError, ValueError) as e: + logger.error(f"Path conversion error for {windows_path}: {e}") + return windows_path + +class WSLCommandRunner: + """Handles WSL command execution with proper error handling.""" + + def __init__(self, machine: str, script_path: str): + self.machine = machine + self.script_path = WSLPathConverter.windows_to_wsl(script_path) + + def run_command(self, command: str, args: str = "", timeout: int = 30) -> str: + """Execute a WSL command with proper error handling.""" + try: + cmd = ['wsl.exe', '-d', self.machine, self.script_path, command] + if args: + cmd.extend(args.split()) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + check=True + ) + return result.stdout.strip() + except subprocess.TimeoutExpired: + logger.error(f"WSL command timed out: {' '.join(cmd)}") + return "" + except subprocess.CalledProcessError as e: + logger.error(f"WSL command failed: {e}\nStderr: {e.stderr}") + return "" + except Exception as e: + logger.error(f"Unexpected error executing WSL command: {e}") + return "" + +class WSLApp: + """Represents a WSL application from .desktop files.""" + + def __init__(self, name: str, generic_name: str, cmd: str, gui: bool, icon: str): + self.name = name + self.generic_name = generic_name + self.cmd = cmd + self.gui = gui + self.icon = icon + + @classmethod + def from_dotdesktop(cls, app_def: str) -> Optional['WSLApp']: + """Create a WSLApp from a .desktop file.""" try: - path = r"\\wsl$" + "\\" + machine + app - wsl_app = WSLApp.from_dotdesktop(path) - if wsl_app["gui"] == True: - app_dict.update({wsl_app["name"]: {"cmd": wsl_app["cmd"], "ico": wsl_app["icon"]}}) - except: - pass + de = DesktopEntry(app_def) + return cls( + name=de.getName(), + generic_name=de.getGenericName(), + cmd=de.getExec(), + gui=not de.getTerminal(), + icon=de.getIcon() + ) + except Exception as e: + logger.error(f"Failed to parse .desktop file {app_def}: {e}") + return None + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary representation.""" + return { + "name": self.name, + "generic_name": self.generic_name, + "cmd": self.cmd, + "gui": self.gui, + "icon": self.icon + } + +class WSLManager: + """Main class for managing WSL operations.""" + + def __init__(self, machine: str, script_path: str): + self.machine = machine + self.runner = WSLCommandRunner(machine, script_path) + + def get_themes(self) -> List[str]: + """Get available themes from WSL distribution.""" + output = self.runner.run_command("listthemes") + if not output: + return [] + + themes = [] + for theme in output.split(":theme:"): + theme = theme.strip() + if not theme or theme in ["\n", "\n\n", "/usr/share/themes"]: + continue + + # Extract theme name from path + theme = self._extract_theme_name(theme) + if theme: + themes.append(theme) + + return themes + + def _extract_theme_name(self, theme_path: str) -> str: + """Extract theme name from path.""" + patterns = ["/usr/share/themes/", ".themes/"] + + for pattern in patterns: + if pattern in theme_path: + return theme_path[theme_path.index(pattern) + len(pattern):] + return theme_path + + def get_apps(self) -> Dict[str, Dict[str, str]]: + """Get GUI applications from WSL distribution.""" + output = self.runner.run_command("listapps") + if not output: + return {} + + apps = {} + for app_path in output.splitlines(): + if not app_path or "screensaver" in app_path.lower(): + continue + + try: + # Convert to Windows path for DesktopEntry + windows_path = f"\\\\wsl$\\{self.machine}{app_path}" + wsl_app = WSLApp.from_dotdesktop(windows_path) - #print(read) - return app_dict - - -def get_apps_old(machine): - # try: - # os.remove(script[:-15] + ".scanapps") - # except: - # pass - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" listappsold' - read = os.popen(cmd).read() - # print("copy") - # cmd2 = 'wsl.exe -d ' + str(machine) + ' cp ~/.scanapps ' + str(pat_con(script[:-15])) - # subprocess.getoutput(cmd2) - # print("read") - # try: - # read = open(script[:-15] + ".scanapps", "r").read() - # except: - # return {} - - read = read.split("/:/") - read[:] = (value for value in read if value != "\n") - apps = {} - for app in read: - if "Name" in app: - if "screensaver" in app: + if wsl_app and wsl_app.gui: + apps[wsl_app.name] = { + "cmd": wsl_app.cmd, + "ico": wsl_app.icon + } + except Exception as e: + logger.debug(f"Skipping app {app_path}: {e}") continue - ind = app.index(":cmd:") - name = app[5:ind] - if "#GenericName=" in name: - name = name[:name.index("#GenericName=") - 1] - elif "GenericName=" in name: - name = name[:name.index("GenericName=") - 1] - elif "Name=" in name: - name = name[:name.index("Name=") - 1] - - run = app[ind + 10:] - - if "Exec=" in run: - #print(run) - run = run[:run.index("Exec=") - 1] - if ":ico:" in run: - run = run[:run.index(":ico:")] - - if "%" in run: - run = run[:run.index("%") - 1] - - if "Icon=" in app: - icon = app[app.index(":ico:") + 10:] - else: - icon = None - - apps.update({name: {"cmd": run, "ico": icon}}) - return apps - -def export_v(machine, name, value, shell="bash"): - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + f'" export-v {name} {value} {shell}' - print(os.popen(cmd).read()[:-1]) - -def gtk(machine, scale, shell="bash"): - export_v(machine, "GDK_SCALE", scale, shell=shell) - """ - if scale == 1 or scale == 2: - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" gtk' + str(scale) - print(os.popen(cmd).read()[:-1])""" - - -def qt(machine, scale, shell="bash"): - export_v(machine, "QT_SCALE_FACTOR", scale, shell=shell) - """ - if scale == 1 or scale == 2: - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" qt' + str(scale) - print(os.popen(cmd).read()[:-1])""" - -def dbus(machine): - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" dbus' - print(os.popen(cmd).read()[:-1]) - - - - - -def export(machine, version, shell="bash"): - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" export-d ' + str(version) + " " + shell - print(cmd) - print(os.popen(cmd).read()[:-1]) - -def export_audio(machine, version, shell="bash"): - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" export-a ' + str(version) + " " + shell - print(cmd) - print(os.popen(cmd).read()[:-1]) - -def cleanup(machine): - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" cleanup' - print(cmd) - print(os.popen(cmd).read()[:-1]) + return apps + + def export_variable(self, name: str, value: str, shell: str = "bash") -> bool: + """Export environment variable to WSL distribution.""" + output = self.runner.run_command("export-v", f"{name} {value} {shell}") + return bool(output) + + def set_gtk_scale(self, scale: float, shell: str = "bash") -> bool: + """Set GDK scale factor.""" + return self.export_variable("GDK_SCALE", str(scale), shell) + + def set_qt_scale(self, scale: float, shell: str = "bash") -> bool: + """Set QT scale factor.""" + return self.export_variable("QT_SCALE_FACTOR", str(scale), shell) + + def start_dbus(self) -> bool: + """Start D-Bus service.""" + output = self.runner.run_command("dbus") + return bool(output) + + def export_display(self, version: str, shell: str = "bash") -> bool: + """Export display settings.""" + output = self.runner.run_command("export-d", f"{version} {shell}") + return bool(output) + + def export_audio(self, version: str, shell: str = "bash") -> bool: + """Export audio settings.""" + output = self.runner.run_command("export-a", f"{version} {shell}") + return bool(output) + def cleanup(self) -> bool: + """Clean up WSL environment.""" + output = self.runner.run_command("cleanup") + return bool(output) -def profile(machine, shell="bash"): - cmd = 'wsl.exe -d ' + str(machine) + ' "' + str(pat_con(script)) + '" profile ' + shell - return os.popen(cmd).read() + def get_profile(self, shell: str = "bash") -> str: + """Get shell profile content.""" + return self.runner.run_command("profile", shell) + +# Backward compatibility functions +def initialize_wsl_manager(machine: str, script_path: str) -> WSLManager: + """Initialize the WSL manager (replaces global script variable).""" + global script + script = script_path + return WSLManager(machine, script_path) + +# Legacy functions for backward compatibility +def pat_con(path: str) -> str: + return WSLPathConverter.windows_to_wsl(path) + +def get_themes(machine: str) -> List[str]: + manager = WSLManager(machine, script) + return manager.get_themes() + +def get_apps(machine: str, logger=None) -> Dict[str, Dict[str, str]]: + manager = WSLManager(machine, script) + return manager.get_apps() + +def get_apps_old(machine: str) -> Dict[str, Dict[str, str]]: + """Legacy function - consider removing if not needed.""" + logger.warning("get_apps_old is deprecated. Use get_apps instead.") + # Implementation can be added if needed + return {} + +def export_v(machine: str, name: str, value: str, shell: str = "bash") -> None: + manager = WSLManager(machine, script) + manager.export_variable(name, value, shell) + +def gtk(machine: str, scale: float, shell: str = "bash") -> None: + manager = WSLManager(machine, script) + manager.set_gtk_scale(scale, shell) + +def qt(machine: str, scale: float, shell: str = "bash") -> None: + manager = WSLManager(machine, script) + manager.set_qt_scale(scale, shell) + +def dbus(machine: str) -> None: + manager = WSLManager(machine, script) + manager.start_dbus() + +def export(machine: str, version: str, shell: str = "bash") -> None: + manager = WSLManager(machine, script) + manager.export_display(version, shell) + +def export_audio(machine: str, version: str, shell: str = "bash") -> None: + manager = WSLManager(machine, script) + manager.export_audio(version, shell) + +def cleanup(machine: str) -> None: + manager = WSLManager(machine, script) + manager.cleanup() + +def profile(machine: str, shell: str = "bash") -> str: + manager = WSLManager(machine, script) + return manager.get_profile(shell)