linux-desktop-manager 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,29 @@
1
+ """
2
+ Linux Desktop Manager by Shashan Lumbhani.
3
+ All-In-One Linux Desktop Environment & Remote Ingress Orchestrator.
4
+ """
5
+
6
+ from .server_env import ServerEnv
7
+ from .desktop_env import DesktopEnvManager
8
+ from .remote_services import RemoteServiceManager
9
+ from .tunnels import TunnelManager
10
+ from .users import UserManager
11
+ from .firewall import FirewallManager
12
+ from .interactive import run_interactive_menu
13
+ from .cli import main
14
+
15
+ __version__ = "1.0.0"
16
+ __author__ = "Shashan Lumbhani"
17
+ __license__ = "MIT"
18
+
19
+ __all__ = [
20
+ "ServerEnv",
21
+ "DesktopEnvManager",
22
+ "RemoteServiceManager",
23
+ "TunnelManager",
24
+ "UserManager",
25
+ "FirewallManager",
26
+ "run_interactive_menu",
27
+ "main",
28
+ "__version__"
29
+ ]
@@ -0,0 +1,7 @@
1
+ """
2
+ Executable module entry point for `python3 -m linux_desktop_manager`.
3
+ """
4
+ from linux_desktop_manager.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,216 @@
1
+ """
2
+ Command-Line Interface (CLI) entry point for linux-desktop-manager.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import shutil
9
+ import argparse
10
+ import sys
11
+ from .formatting import console, print_banner, print_services_table, print_desktops_table, print_connection_card
12
+ from .server_env import ServerEnv
13
+ from .desktop_env import DesktopEnvManager
14
+ from .remote_services import RemoteServiceManager
15
+ from .tunnels import TunnelManager
16
+ from .firewall import FirewallManager
17
+ from .users import UserManager
18
+ from .interactive import run_interactive_menu
19
+
20
+
21
+ def ensure_system_symlinks() -> None:
22
+ """Ensure /usr/local/bin/ldm and /usr/local/bin/linux-desktop-manager wrappers exist so 'sudo ldm' works directly."""
23
+ if os.geteuid() != 0:
24
+ return
25
+
26
+ wrapper_content = f"""#!/usr/bin/env bash
27
+ # Auto-generated by Linux Desktop Manager Orchestrator
28
+ # Preserves user site-packages and editable installs when running under sudo.
29
+ if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then
30
+ USER_HOME=$(getent passwd "$SUDO_USER" 2>/dev/null | cut -d: -f6)
31
+ [ -z "$USER_HOME" ] && USER_HOME="/home/$SUDO_USER"
32
+ for pdir in "$USER_HOME"/.local/lib/python*/site-packages; do
33
+ if [ -d "$pdir" ]; then
34
+ export PYTHONPATH="$pdir:$PYTHONPATH"
35
+ for pth in "$pdir"/*.pth; do
36
+ if [ -f "$pth" ]; then
37
+ while IFS= read -r line || [ -n "$line" ]; do
38
+ [[ "$line" =~ ^[#] ]] && continue
39
+ [ -d "$line" ] && export PYTHONPATH="$line:$PYTHONPATH"
40
+ done < "$pth"
41
+ fi
42
+ done
43
+ fi
44
+ done
45
+ fi
46
+
47
+ exec {sys.executable} -m linux_desktop_manager "$@"
48
+ """
49
+
50
+ for name in ["ldm", "linux-desktop-manager"]:
51
+ usr_bin_target = f"/usr/local/bin/{name}"
52
+ try:
53
+ needs_write = True
54
+ if os.path.exists(usr_bin_target) and not os.path.islink(usr_bin_target):
55
+ try:
56
+ with open(usr_bin_target, "r") as f:
57
+ content = f.read()
58
+ if "linux_desktop_manager" in content and "pth" in content:
59
+ needs_write = False
60
+ except Exception:
61
+ pass
62
+
63
+ if needs_write:
64
+ if os.path.islink(usr_bin_target) or os.path.exists(usr_bin_target):
65
+ os.remove(usr_bin_target)
66
+ with open(usr_bin_target, "w") as f:
67
+ f.write(wrapper_content)
68
+ os.chmod(usr_bin_target, 0o755)
69
+ except Exception:
70
+ pass
71
+
72
+
73
+ def check_elevation() -> None:
74
+ """Auto-elevates with sudo preserving user site-packages and editable paths if not root."""
75
+ if any(arg in sys.argv for arg in ["-h", "--help"]):
76
+ return
77
+
78
+ # If already root, ensure system wrappers exist in /usr/local/bin
79
+ if os.geteuid() == 0:
80
+ ensure_system_symlinks()
81
+ return
82
+
83
+ # Non-root user: Auto-elevate via sudo preserving all non-system sys.path entries
84
+ if shutil.which("sudo"):
85
+ console.print("[dim cyan]⚡ Elevated privileges required. Elevating with sudo...[/dim cyan]")
86
+ try:
87
+ user_paths = [p for p in sys.path if p and not p.startswith("/usr/lib") and os.path.isdir(p)]
88
+ pythonpath = ":".join(user_paths) if user_paths else ""
89
+
90
+ cmd = ["sudo", "env"]
91
+ if pythonpath:
92
+ cmd.append(f"PYTHONPATH={pythonpath}")
93
+ cmd.extend([sys.executable, "-m", "linux_desktop_manager"] + sys.argv[1:])
94
+
95
+ os.execvp("sudo", cmd)
96
+ except Exception as e:
97
+ console.print(f"[bold red]Elevation failed: {e}. Please run with sudo.[/bold red]")
98
+ sys.exit(1)
99
+ else:
100
+ console.print("[bold red]Root privileges are required to manage desktop environments and services.[/bold red]")
101
+ sys.exit(1)
102
+
103
+
104
+ def main() -> None:
105
+ check_elevation()
106
+ parser = argparse.ArgumentParser(
107
+ prog="linux-desktop-manager",
108
+ description="⚡ Linux Desktop Environment & Remote Access Manager (CLI & Interactive TUI)",
109
+ epilog="""
110
+ Examples:
111
+ sudo linux-desktop-manager # Launch full interactive Arrow-Key TUI menu
112
+ sudo linux-desktop-manager --status # View current status of services and ports
113
+ sudo linux-desktop-manager --install-de xfce4
114
+ sudo linux-desktop-manager --start xrdp
115
+ sudo linux-desktop-manager --tunnel cf # Expose Browser Desktop via TunnelMate
116
+ """
117
+ )
118
+ parser.add_argument("--tui", action="store_true", help="Launch interactive Arrow-Key TUI menu (default if no flags)")
119
+ parser.add_argument("--status", action="store_true", help="Print live service, desktop, and port status table")
120
+ parser.add_argument("--install-wizard", action="store_true", help="Launch step-by-step installation wizard")
121
+ parser.add_argument("--install-de", metavar="NAME", help="Install a desktop environment (e.g. xfce4, plasma, i3, gnome)")
122
+ parser.add_argument("--uninstall-de", metavar="NAME", help="Uninstall a desktop environment")
123
+ parser.add_argument("--set-de", metavar="NAME", help="Set default desktop session for target user")
124
+ parser.add_argument("--start", metavar="SERVICE", help="Start service (xrdp, openssh, tigervnc, novnc, x2go, cockpit)")
125
+ parser.add_argument("--stop", metavar="SERVICE", help="Stop service")
126
+ parser.add_argument("--restart", metavar="SERVICE", help="Restart service")
127
+ parser.add_argument("--install-service", metavar="SERVICE", help="Install and configure service")
128
+ parser.add_argument("--uninstall-service", metavar="SERVICE", help="Uninstall service")
129
+ parser.add_argument("--tunnel", choices=["cf", "rdp", "stop"], help="Manage public tunnels: cf (browser), rdp (TunnelMate TCP RDP), stop")
130
+ parser.add_argument("--allow-ports", action="store_true", help="Allow all remote desktop ports in firewall")
131
+ parser.add_argument("--purge-all", action="store_true", help="Purge all remote desktop services and configs")
132
+ parser.add_argument("--user", metavar="USERNAME", help="Specify target user (default: current/sudo user)")
133
+
134
+ args = parser.parse_args()
135
+
136
+ # If no flags passed or --tui, run the interactive arrow-key TUI menu
137
+ if len(sys.argv) == 1 or args.tui or args.install_wizard:
138
+ run_interactive_menu()
139
+ return
140
+
141
+ # CLI automated actions
142
+ server_env = ServerEnv()
143
+ desktop_mgr = DesktopEnvManager(server_env)
144
+ service_mgr = RemoteServiceManager(server_env)
145
+ tunnel_mgr = TunnelManager()
146
+ firewall_mgr = FirewallManager()
147
+ target_user = args.user or server_env.get_invoking_user()
148
+
149
+ if args.status:
150
+ print_banner()
151
+ print_services_table(service_mgr.get_all_services_status())
152
+ cf_url = tunnel_mgr.get_cloudflare_url()
153
+ rdp_tcp_url = tunnel_mgr.get_rdp_tcp_url()
154
+ local_ips = server_env.get_local_ips()
155
+ pub_ip = server_env.get_public_ip()
156
+ active_de = desktop_mgr.get_active_session(target_user) or "None"
157
+ print_connection_card(
158
+ user=target_user,
159
+ de_name=active_de,
160
+ local_ip=local_ips[0] if local_ips else "127.0.0.1",
161
+ public_ip=pub_ip,
162
+ cf_url=cf_url,
163
+ rdp_tcp_url=rdp_tcp_url
164
+ )
165
+ elif args.install_de:
166
+ console.print(f"[bold cyan]Installing desktop environment: {args.install_de}...[/bold cyan]")
167
+ desktop_mgr.install_desktop(args.install_de, target_user, set_as_default=True, live_callback=console.print)
168
+ elif args.uninstall_de:
169
+ console.print(f"[bold red]Uninstalling desktop environment: {args.uninstall_de}...[/bold red]")
170
+ desktop_mgr.uninstall_desktop(args.uninstall_de, live_callback=console.print)
171
+ elif args.set_de:
172
+ if desktop_mgr.set_default_session(args.set_de, target_user):
173
+ console.print(f"[bold green]✔ Default session for '{target_user}' set to {args.set_de}.[/bold green]")
174
+ else:
175
+ console.print("[bold red]Failed to set session.[/bold red]")
176
+ elif args.start:
177
+ service_mgr.start_service(args.start, target_user=target_user)
178
+ console.print(f"[bold green]✔ Started {args.start}.[/bold green]")
179
+ elif args.stop:
180
+ service_mgr.stop_service(args.stop)
181
+ console.print(f"[bold yellow]Stopped {args.stop}.[/bold yellow]")
182
+ elif args.restart:
183
+ service_mgr.restart_service(args.restart, target_user=target_user)
184
+ console.print(f"[bold green]✔ Restarted {args.restart}.[/bold green]")
185
+ elif args.install_service:
186
+ console.print(f"[bold cyan]Installing remote service: {args.install_service}...[/bold cyan]")
187
+ service_mgr.install_service(args.install_service, target_user=target_user, live_callback=console.print)
188
+ elif args.uninstall_service:
189
+ service_mgr.uninstall_service(args.uninstall_service, purge_configs=True, live_callback=console.print)
190
+ elif args.tunnel:
191
+ if args.tunnel == "cf":
192
+ url = tunnel_mgr.start_cloudflare_tunnel(port=6080, live_callback=console.print)
193
+ if url:
194
+ console.print(f"[bold green]✔ Cloudflare Browser URL: {url}/vnc.html[/bold green]")
195
+ elif args.tunnel == "rdp":
196
+ url = tunnel_mgr.start_rdp_tcp_tunnel(port=3389, live_callback=console.print)
197
+ if url:
198
+ clean_host = url.replace("https://", "").replace("http://", "").strip("/")
199
+ console.print(f"[bold green]✔ TunnelMate Cloudflare TCP RDP Assigned: {url}[/bold green]")
200
+ console.print(f"[dim cyan]Client Command: cloudflared access rdp --hostname {clean_host} --url localhost:3389[/dim cyan]")
201
+ console.print(f"[dim]Then open Windows mstsc to: localhost:3389[/dim]")
202
+ elif args.tunnel == "stop":
203
+ tunnel_mgr.stop_all_tunnels()
204
+ console.print("[yellow]All tunnels stopped.[/yellow]")
205
+ elif args.allow_ports:
206
+ firewall_mgr.allow_all_remote_ports()
207
+ console.print("[bold green]✔ All remote desktop ports allowed in firewall.[/bold green]")
208
+ elif args.purge_all:
209
+ for sid in ["novnc", "tigervnc", "xrdp", "x2go", "cockpit"]:
210
+ service_mgr.uninstall_service(sid, purge_configs=True, live_callback=console.print)
211
+ tunnel_mgr.stop_all_tunnels()
212
+ console.print("[bold green]✔ Clean wipe completed.[/bold green]")
213
+
214
+
215
+ if __name__ == "__main__":
216
+ main()
@@ -0,0 +1,420 @@
1
+ """
2
+ Desktop Environment and Window Manager Manager for Linux.
3
+ Supports 14 full desktop environments and lightweight tiling/floating window managers.
4
+ Handles installation, uninstallation, active session switching, and ~/.xsession configuration.
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ from typing import Dict, List, Optional, Callable
11
+ from .server_env import ServerEnv
12
+
13
+ class DesktopEnvManager:
14
+ """Manages 14 Linux Desktop Environments and Window Managers across multiple distributions."""
15
+
16
+ DESKTOPS = {
17
+ "xfce4": {
18
+ "name": "XFCE4",
19
+ "category": "Full Desktop",
20
+ "recommended": True,
21
+ "cpu": "1 Core (1.0+ GHz)",
22
+ "ram": "~400MB",
23
+ "desc": "Fast, lightweight, rock-solid stability over remote RDP & VNC",
24
+ "session_bin": "startxfce4",
25
+ "session_cmd": "startxfce4",
26
+ "xsession_rc": "export XDG_SESSION_DESKTOP=xfce\nexport XDG_CURRENT_DESKTOP=XFCE",
27
+ "pkgs": {
28
+ "apt": ["xfce4", "xfce4-goodies", "xfce4-terminal", "dbus-x11"],
29
+ "dnf": ["@xfce-desktop-environment", "xfce4-session", "xfce4-panel", "xfce4-terminal"],
30
+ "pacman": ["xfce4", "xfce4-goodies"],
31
+ "zypper": ["xfce4-session", "xfce4-panel", "xfce4-terminal"],
32
+ "apk": ["xfce4", "xfce4-terminal"]
33
+ }
34
+ },
35
+ "plasma": {
36
+ "name": "KDE Plasma",
37
+ "category": "Full Desktop",
38
+ "recommended": False,
39
+ "cpu": "2+ Cores (2.0+ GHz)",
40
+ "ram": "~700MB",
41
+ "desc": "Modern, sleek, Windows-like layout with deep customization",
42
+ "session_bin": "startplasma-x11",
43
+ "session_cmd": "startplasma-x11",
44
+ "xsession_rc": "export XDG_SESSION_DESKTOP=KDE\nexport DESKTOP_SESSION=plasma",
45
+ "pkgs": {
46
+ "apt": ["kde-plasma-desktop", "plasma-workspace-x11", "konsole"],
47
+ "dnf": ["plasma-workspace-x11", "konsole"],
48
+ "pacman": ["plasma-meta", "plasma-x11-session", "konsole"],
49
+ "zypper": ["plasma5-session"],
50
+ "apk": ["plasma-desktop", "konsole"]
51
+ }
52
+ },
53
+ "gnome": {
54
+ "name": "GNOME Shell",
55
+ "category": "Full Desktop",
56
+ "recommended": False,
57
+ "cpu": "2+ Cores (2.0+ GHz)",
58
+ "ram": "~900MB",
59
+ "desc": "Default Ubuntu/Fedora workstation desktop, modern & feature-complete",
60
+ "session_bin": "gnome-session",
61
+ "session_cmd": "gnome-session",
62
+ "xsession_rc": "export XDG_SESSION_TYPE=x11\nexport XDG_CURRENT_DESKTOP=GNOME\nexport GNOME_SHELL_SESSION_MODE=ubuntu",
63
+ "pkgs": {
64
+ "apt": ["gnome-core", "gnome-session", "gnome-terminal"],
65
+ "dnf": ["gnome-shell", "gnome-session", "gnome-terminal"],
66
+ "pacman": ["gnome", "gnome-extra"],
67
+ "zypper": ["gnome-session", "gnome-terminal"],
68
+ "apk": ["gnome", "gnome-terminal"]
69
+ }
70
+ },
71
+ "mate": {
72
+ "name": "MATE",
73
+ "category": "Full Desktop",
74
+ "recommended": False,
75
+ "cpu": "1 Core (1.2+ GHz)",
76
+ "ram": "~450MB",
77
+ "desc": "Classic GNOME 2 layout, traditional and very responsive over RDP",
78
+ "session_bin": "mate-session",
79
+ "session_cmd": "mate-session",
80
+ "xsession_rc": "export XDG_SESSION_DESKTOP=mate\nexport XDG_CURRENT_DESKTOP=MATE",
81
+ "pkgs": {
82
+ "apt": ["mate-desktop-environment", "mate-desktop-environment-extras"],
83
+ "dnf": ["@mate-desktop"],
84
+ "pacman": ["mate", "mate-extra"],
85
+ "zypper": ["mate-session"],
86
+ "apk": ["mate-desktop-environment"]
87
+ }
88
+ },
89
+ "cinnamon": {
90
+ "name": "Cinnamon",
91
+ "category": "Full Desktop",
92
+ "recommended": False,
93
+ "cpu": "2 Cores (1.5+ GHz)",
94
+ "ram": "~600MB",
95
+ "desc": "Linux Mint flagship desktop, polished Windows 7/10 feel",
96
+ "session_bin": "cinnamon-session",
97
+ "session_cmd": "cinnamon-session",
98
+ "xsession_rc": "export XDG_SESSION_DESKTOP=cinnamon\nexport XDG_CURRENT_DESKTOP=X-Cinnamon",
99
+ "pkgs": {
100
+ "apt": ["cinnamon-desktop-environment"],
101
+ "dnf": ["@cinnamon-desktop", "cinnamon"],
102
+ "pacman": ["cinnamon", "nemo-fileroller", "gnome-terminal"],
103
+ "zypper": ["cinnamon"],
104
+ "apk": ["cinnamon"]
105
+ }
106
+ },
107
+ "lxqt": {
108
+ "name": "LXQt",
109
+ "category": "Full Desktop",
110
+ "recommended": False,
111
+ "cpu": "1 Core (800+ MHz)",
112
+ "ram": "~300MB",
113
+ "desc": "Lightweight Qt-based desktop, ideal for low-RAM cloud VMs",
114
+ "session_bin": "startlxqt",
115
+ "session_cmd": "startlxqt",
116
+ "xsession_rc": "export XDG_SESSION_DESKTOP=LXQt\nexport XDG_CURRENT_DESKTOP=LXQt",
117
+ "pkgs": {
118
+ "apt": ["lxqt", "openbox", "qterminal"],
119
+ "dnf": ["@lxqt-desktop", "lxqt-session", "openbox"],
120
+ "pacman": ["lxqt", "openbox", "breeze-icons", "qterminal"],
121
+ "zypper": ["lxqt-session", "openbox"],
122
+ "apk": ["lxqt-desktop", "openbox"]
123
+ }
124
+ },
125
+ "lxde": {
126
+ "name": "LXDE",
127
+ "category": "Full Desktop",
128
+ "recommended": False,
129
+ "cpu": "1 Core (600+ MHz)",
130
+ "ram": "~250MB",
131
+ "desc": "Classic GTK lightweight desktop, minimal overhead",
132
+ "session_bin": "startlxde",
133
+ "session_cmd": "startlxde",
134
+ "xsession_rc": "export XDG_SESSION_DESKTOP=LXDE\nexport XDG_CURRENT_DESKTOP=LXDE",
135
+ "pkgs": {
136
+ "apt": ["lxde"],
137
+ "dnf": ["@lxde-desktop"],
138
+ "pacman": ["lxde"],
139
+ "zypper": ["lxde-common"],
140
+ "apk": ["lxde"]
141
+ }
142
+ },
143
+ "budgie": {
144
+ "name": "Budgie",
145
+ "category": "Full Desktop",
146
+ "recommended": False,
147
+ "cpu": "1-2 Cores (1.5+ GHz)",
148
+ "ram": "~550MB",
149
+ "desc": "Sleek modern desktop with Raven notification sidebar",
150
+ "session_bin": "budgie-desktop",
151
+ "session_cmd": "budgie-desktop",
152
+ "xsession_rc": "export XDG_CURRENT_DESKTOP=Budgie:GNOME",
153
+ "pkgs": {
154
+ "apt": ["budgie-desktop", "gnome-terminal"],
155
+ "dnf": ["budgie-desktop", "gnome-terminal"],
156
+ "pacman": ["budgie-desktop", "gnome-terminal", "nemo"],
157
+ "zypper": ["budgie-desktop"],
158
+ "apk": ["budgie-desktop"]
159
+ }
160
+ },
161
+ "deepin": {
162
+ "name": "Deepin (DDE)",
163
+ "category": "Full Desktop",
164
+ "recommended": False,
165
+ "cpu": "2+ Cores (2.0+ GHz)",
166
+ "ram": "~800MB",
167
+ "desc": "Elegant macOS-style interface with fluid dock and blur effects",
168
+ "session_bin": "startdde",
169
+ "session_cmd": "startdde",
170
+ "xsession_rc": "export XDG_CURRENT_DESKTOP=Deepin",
171
+ "pkgs": {
172
+ "apt": ["ubuntudde-dde", "startdde", "deepin-terminal"],
173
+ "dnf": ["deepin-desktop"],
174
+ "pacman": ["deepin", "deepin-extra"],
175
+ "zypper": ["deepin-session-ui"],
176
+ "apk": ["deepin-desktop"]
177
+ }
178
+ },
179
+ "enlightenment": {
180
+ "name": "Enlightenment",
181
+ "category": "Full Desktop",
182
+ "recommended": False,
183
+ "cpu": "1 Core (1.0+ GHz)",
184
+ "ram": "~300MB",
185
+ "desc": "Fast compositing desktop with modular gadgets and low memory footprint",
186
+ "session_bin": "enlightenment_start",
187
+ "session_cmd": "enlightenment_start",
188
+ "xsession_rc": "export XDG_CURRENT_DESKTOP=Enlightenment",
189
+ "pkgs": {
190
+ "apt": ["enlightenment", "terminology"],
191
+ "dnf": ["enlightenment", "terminology"],
192
+ "pacman": ["enlightenment", "terminology"],
193
+ "zypper": ["enlightenment"],
194
+ "apk": ["enlightenment"]
195
+ }
196
+ },
197
+ "i3": {
198
+ "name": "i3wm",
199
+ "category": "Tiling WM",
200
+ "recommended": False,
201
+ "cpu": "1 Core (500+ MHz)",
202
+ "ram": "~150MB",
203
+ "desc": "Keyboard-driven tiling window manager for power users and programmers",
204
+ "session_bin": "i3",
205
+ "session_cmd": "i3",
206
+ "xsession_rc": "export XDG_SESSION_DESKTOP=i3",
207
+ "pkgs": {
208
+ "apt": ["i3", "i3status", "dmenu", "rxvt-unicode", "xterm"],
209
+ "dnf": ["i3", "i3status", "dmenu", "rxvt-unicode", "xterm"],
210
+ "pacman": ["i3-wm", "i3status", "dmenu", "rxvt-unicode", "xterm"],
211
+ "zypper": ["i3", "i3status", "dmenu", "xterm"],
212
+ "apk": ["i3wm", "i3status", "dmenu", "xterm"]
213
+ }
214
+ },
215
+ "openbox": {
216
+ "name": "Openbox",
217
+ "category": "Floating WM",
218
+ "recommended": False,
219
+ "cpu": "1 Core (500+ MHz)",
220
+ "ram": "~120MB",
221
+ "desc": "Ultra-lightweight floating window manager, highly extensible",
222
+ "session_bin": "openbox-session",
223
+ "session_cmd": "openbox-session",
224
+ "xsession_rc": "export XDG_SESSION_DESKTOP=openbox",
225
+ "pkgs": {
226
+ "apt": ["openbox", "obconf", "tint2", "pcmanfm", "xterm"],
227
+ "dnf": ["openbox", "obconf", "tint2", "pcmanfm", "xterm"],
228
+ "pacman": ["openbox", "obconf", "tint2", "pcmanfm", "xterm"],
229
+ "zypper": ["openbox", "tint2", "pcmanfm", "xterm"],
230
+ "apk": ["openbox", "tint2", "pcmanfm", "xterm"]
231
+ }
232
+ },
233
+ "icewm": {
234
+ "name": "IceWM",
235
+ "category": "Lightweight WM",
236
+ "recommended": False,
237
+ "cpu": "1 Core (400+ MHz)",
238
+ "ram": "~100MB",
239
+ "desc": "Classic Windows 95 look, blazing fast with minimal CPU/RAM",
240
+ "session_bin": "icewm-session",
241
+ "session_cmd": "icewm-session",
242
+ "xsession_rc": "export XDG_SESSION_DESKTOP=icewm",
243
+ "pkgs": {
244
+ "apt": ["icewm", "xterm", "pcmanfm"],
245
+ "dnf": ["icewm", "xterm", "pcmanfm"],
246
+ "pacman": ["icewm", "xterm", "pcmanfm"],
247
+ "zypper": ["icewm", "xterm", "pcmanfm"],
248
+ "apk": ["icewm", "xterm", "pcmanfm"]
249
+ }
250
+ },
251
+ "fluxbox": {
252
+ "name": "Fluxbox",
253
+ "category": "Lightweight WM",
254
+ "recommended": False,
255
+ "cpu": "1 Core (400+ MHz)",
256
+ "ram": "~100MB",
257
+ "desc": "Super-fast stacking window manager with tabs and taskbar",
258
+ "session_bin": "startfluxbox",
259
+ "session_cmd": "startfluxbox",
260
+ "xsession_rc": "export XDG_SESSION_DESKTOP=fluxbox",
261
+ "pkgs": {
262
+ "apt": ["fluxbox", "xterm", "pcmanfm"],
263
+ "dnf": ["fluxbox", "xterm", "pcmanfm"],
264
+ "pacman": ["fluxbox", "xterm", "pcmanfm"],
265
+ "zypper": ["fluxbox", "xterm", "pcmanfm"],
266
+ "apk": ["fluxbox", "xterm", "pcmanfm"]
267
+ }
268
+ }
269
+ }
270
+
271
+ def __init__(self, server_env: ServerEnv):
272
+ self.env = server_env
273
+
274
+ def get_desktop_info(self, de_id: str) -> Optional[Dict]:
275
+ return self.DESKTOPS.get(de_id)
276
+
277
+ def is_installed(self, de_id: str) -> bool:
278
+ info = self.DESKTOPS.get(de_id)
279
+ if not info:
280
+ return False
281
+ return shutil.which(info["session_bin"]) is not None
282
+
283
+ def get_user_home(self, username: str) -> str:
284
+ if username == "root":
285
+ return "/root"
286
+ import pwd
287
+ try:
288
+ return pwd.getpwnam(username).pw_dir
289
+ except Exception:
290
+ return f"/home/{username}"
291
+
292
+ def get_active_session(self, username: str) -> Optional[str]:
293
+ """Detects which Desktop Environment is currently set in target user's ~/.xsession."""
294
+ home = self.get_user_home(username)
295
+ xsession_path = os.path.join(home, ".xsession")
296
+ if not os.path.exists(xsession_path):
297
+ return None
298
+ try:
299
+ with open(xsession_path, "r", encoding="utf-8", errors="ignore") as f:
300
+ content = f.read()
301
+ for de_id, info in self.DESKTOPS.items():
302
+ if info["session_cmd"] in content:
303
+ return de_id
304
+ except Exception:
305
+ pass
306
+ return None
307
+
308
+ def list_desktops(self, target_user: Optional[str] = None) -> List[Dict]:
309
+ """Returns all 14 desktop environments with installed and active status flags."""
310
+ active_de = self.get_active_session(target_user) if target_user else None
311
+ res = []
312
+ for de_id, data in self.DESKTOPS.items():
313
+ installed = self.is_installed(de_id)
314
+ res.append({
315
+ "id": de_id,
316
+ "name": data["name"],
317
+ "category": data["category"],
318
+ "recommended": data.get("recommended", False),
319
+ "cpu": data.get("cpu", "1 Core (1.0+ GHz)"),
320
+ "ram": data["ram"],
321
+ "desc": data["desc"],
322
+ "installed": installed,
323
+ "is_active": (de_id == active_de)
324
+ })
325
+ return res
326
+
327
+ def set_default_session(self, de_id: str, username: str) -> bool:
328
+ """Configures target user's ~/.xsession and ~/.xsessionrc to launch the specified DE."""
329
+ info = self.DESKTOPS.get(de_id)
330
+ if not info:
331
+ return False
332
+ home = self.get_user_home(username)
333
+ os.makedirs(home, exist_ok=True)
334
+ xsession_file = os.path.join(home, ".xsession")
335
+ xsessionrc_file = os.path.join(home, ".xsessionrc")
336
+
337
+ try:
338
+ with open(xsession_file, "w", encoding="utf-8") as f:
339
+ f.write(f"{info['session_cmd']}\n")
340
+ os.chmod(xsession_file, 0o755)
341
+
342
+ with open(xsessionrc_file, "w", encoding="utf-8") as f:
343
+ f.write(f"{info['xsession_rc']}\n")
344
+
345
+ import pwd
346
+ user_entry = pwd.getpwnam(username)
347
+ os.chown(xsession_file, user_entry.pw_uid, user_entry.pw_gid)
348
+ os.chown(xsessionrc_file, user_entry.pw_uid, user_entry.pw_gid)
349
+ return True
350
+ except Exception:
351
+ return False
352
+
353
+ def install_desktop(
354
+ self,
355
+ de_id: str,
356
+ target_user: str,
357
+ set_as_default: bool = True,
358
+ live_callback: Optional[Callable[[str], None]] = None
359
+ ) -> bool:
360
+ """Installs selected desktop environment, applies fallbacks if missing, and configures user session."""
361
+ info = self.DESKTOPS.get(de_id)
362
+ if not info:
363
+ if live_callback:
364
+ live_callback(f"[ERROR] Unknown desktop ID: {de_id}")
365
+ return False
366
+
367
+ # Ensure Base X11 and DBus
368
+ self.env.install_base_x11(live_callback=live_callback)
369
+
370
+ # Distro specific packages
371
+ pkgs = info["pkgs"].get(self.env.pkg_mgr, info["pkgs"].get("apt", []))
372
+ if live_callback:
373
+ live_callback(f"Installing {info['name']} ({', '.join(pkgs)})...")
374
+
375
+ self.env.install_packages(pkgs, live_callback=live_callback)
376
+
377
+ # Fallback check
378
+ session_bin = info["session_bin"]
379
+ if not shutil.which(session_bin):
380
+ if live_callback:
381
+ live_callback(f"[WARN] '{session_bin}' not found in distro repositories! Installing rock-solid fallback XFCE4...")
382
+ fallback_pkgs = self.DESKTOPS["xfce4"]["pkgs"].get(self.env.pkg_mgr, ["xfce4", "xfce4-terminal"])
383
+ self.env.install_packages(fallback_pkgs, live_callback=live_callback)
384
+ de_id = "xfce4"
385
+
386
+ if set_as_default:
387
+ if live_callback:
388
+ live_callback(f"Configuring default session for user '{target_user}'...")
389
+ self.set_default_session(de_id, target_user)
390
+
391
+ self.env.setup_user_runtime_dir(target_user)
392
+ self.env.ensure_dbus()
393
+
394
+ if live_callback:
395
+ live_callback(f"[SUCCESS] {self.DESKTOPS[de_id]['name']} desktop is ready.")
396
+ return True
397
+
398
+ def uninstall_desktop(
399
+ self,
400
+ de_id: str,
401
+ live_callback: Optional[Callable[[str], None]] = None
402
+ ) -> bool:
403
+ """Removes packages for the specified desktop environment."""
404
+ info = self.DESKTOPS.get(de_id)
405
+ if not info:
406
+ return False
407
+
408
+ pkgs = info["pkgs"].get(self.env.pkg_mgr, [])
409
+ if not pkgs:
410
+ return True
411
+
412
+ if live_callback:
413
+ live_callback(f"Uninstalling {info['name']} packages...")
414
+ success = self.env.remove_packages(pkgs, purge=True, live_callback=live_callback)
415
+ if live_callback:
416
+ if success:
417
+ live_callback(f"[SUCCESS] Uninstalled {info['name']}.")
418
+ else:
419
+ live_callback(f"[WARN] Completed uninstallation process with warnings.")
420
+ return success