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.
- linux_desktop_manager/__init__.py +29 -0
- linux_desktop_manager/__main__.py +7 -0
- linux_desktop_manager/cli.py +216 -0
- linux_desktop_manager/desktop_env.py +420 -0
- linux_desktop_manager/firewall.py +78 -0
- linux_desktop_manager/formatting.py +184 -0
- linux_desktop_manager/interactive.py +693 -0
- linux_desktop_manager/remote_services.py +650 -0
- linux_desktop_manager/server_env.py +441 -0
- linux_desktop_manager/tunnels.py +266 -0
- linux_desktop_manager/users.py +105 -0
- linux_desktop_manager-1.0.0.dist-info/METADATA +182 -0
- linux_desktop_manager-1.0.0.dist-info/RECORD +17 -0
- linux_desktop_manager-1.0.0.dist-info/WHEEL +5 -0
- linux_desktop_manager-1.0.0.dist-info/entry_points.txt +3 -0
- linux_desktop_manager-1.0.0.dist-info/licenses/LICENSE +21 -0
- linux_desktop_manager-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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,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
|