mirror of https://github.com/razor-ai/soup.git
110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""v0.44.0 Part B — Desktop / Start Menu / .desktop shortcut creator.
|
|
|
|
Pure-Python: builds the shortcut file content for the host platform.
|
|
Returns the rendered text + suggested filename without writing — the caller
|
|
(`soup install-shortcut`) does the actual write under cwd containment.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import platform
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ShortcutSpec:
|
|
"""Rendered shortcut content + suggested filename."""
|
|
|
|
filename: str
|
|
content: str
|
|
platform: str
|
|
|
|
|
|
# `Exec=` and `--name` may contain user-controlled paths in the future; we
|
|
# disallow shell metacharacters defensively even though today the inputs are
|
|
# fixed strings. Any change to those callers must keep the allowlist policy.
|
|
_NAME_DISALLOWED = frozenset("`'\"\\\n\r\x00$;&|<>")
|
|
|
|
|
|
def _validate_name(name: str) -> str:
|
|
if not isinstance(name, str):
|
|
raise TypeError("name must be str")
|
|
if not name:
|
|
raise ValueError("name must be non-empty")
|
|
if len(name) > 64:
|
|
raise ValueError("name must be <= 64 chars")
|
|
if any(char in _NAME_DISALLOWED for char in name):
|
|
raise ValueError("name contains disallowed character")
|
|
return name
|
|
|
|
|
|
def _validate_command(command: str) -> str:
|
|
if not isinstance(command, str):
|
|
raise TypeError("command must be str")
|
|
if not command:
|
|
raise ValueError("command must be non-empty")
|
|
if "\x00" in command or "\n" in command or "\r" in command:
|
|
raise ValueError("command contains control char")
|
|
if len(command) > 1024:
|
|
raise ValueError("command must be <= 1024 chars")
|
|
return command
|
|
|
|
|
|
def detect_platform() -> str:
|
|
"""Return one of {linux, darwin, windows, unknown}."""
|
|
system = platform.system().lower()
|
|
if system in ("linux", "darwin", "windows"):
|
|
return system
|
|
return "unknown"
|
|
|
|
|
|
def build_linux_desktop_entry(*, name: str, command: str) -> ShortcutSpec:
|
|
"""Build a freedesktop.org `.desktop` entry."""
|
|
name = _validate_name(name)
|
|
command = _validate_command(command)
|
|
body = (
|
|
"[Desktop Entry]\n"
|
|
f"Type=Application\n"
|
|
f"Name={name}\n"
|
|
f"Exec={command}\n"
|
|
"Terminal=true\n"
|
|
"Categories=Development;\n"
|
|
)
|
|
safe_filename = name.lower().replace(" ", "-") + ".desktop"
|
|
return ShortcutSpec(filename=safe_filename, content=body, platform="linux")
|
|
|
|
|
|
def build_macos_command_file(*, name: str, command: str) -> ShortcutSpec:
|
|
"""Build a `.command` script (double-clickable on macOS Finder)."""
|
|
name = _validate_name(name)
|
|
command = _validate_command(command)
|
|
body = "#!/usr/bin/env bash\n" f"exec {command}\n"
|
|
safe_filename = name.lower().replace(" ", "-") + ".command"
|
|
return ShortcutSpec(filename=safe_filename, content=body, platform="darwin")
|
|
|
|
|
|
def build_windows_cmd(*, name: str, command: str) -> ShortcutSpec:
|
|
"""Build a Windows `.cmd` launcher.
|
|
|
|
Note: a true Start-Menu .lnk needs `pywin32` or `winshell`, which we keep
|
|
out of our deps. The `.cmd` file is a portable alternative the user can
|
|
pin to taskbar/Start menu manually.
|
|
"""
|
|
name = _validate_name(name)
|
|
command = _validate_command(command)
|
|
body = "@echo off\r\n" f"{command} %*\r\n"
|
|
safe_filename = name.lower().replace(" ", "-") + ".cmd"
|
|
return ShortcutSpec(filename=safe_filename, content=body, platform="windows")
|
|
|
|
|
|
def build_for_current_platform(*, name: str, command: str) -> ShortcutSpec:
|
|
"""Build the right shortcut for the host. Raises on `unknown`."""
|
|
plat = detect_platform()
|
|
if plat == "linux":
|
|
return build_linux_desktop_entry(name=name, command=command)
|
|
if plat == "darwin":
|
|
return build_macos_command_file(name=name, command=command)
|
|
if plat == "windows":
|
|
return build_windows_cmd(name=name, command=command)
|
|
raise NotImplementedError(f"Shortcut creation not supported on platform: {plat}")
|