import logging import platform import os import sys # platform constants WINDOWS = "Windows" MACOS = "macOS" LINUX = "Linux" LINUX_X11 = f"{LINUX}_X11" LINUX_WAYLAND = f"{LINUX}_Wayland" LINUX_HEADLESS = f"{LINUX}_Headless" # OS detection def get_os_and_display_server(): system = platform.system().lower().strip() if system == "windows": return WINDOWS elif system == "darwin": return MACOS elif system == "linux": session_type = os.environ.get("XDG_SESSION_TYPE", "").lower().strip() if session_type == "wayland": return LINUX_WAYLAND elif session_type == "x11": return LINUX_X11 else: # Could be a headless session or another type of session return LINUX_HEADLESS else: return "Unknown OS" def detect_linux_display_server(): session_type = os.environ.get("XDG_SESSION_TYPE", "").lower().strip() session_desktop = os.environ.get("XDG_SESSION_DESKTOP", "").lower().strip() wayland_display = os.environ.get("WAYLAND_DISPLAY") x_display = os.environ.get("DISPLAY") # Priority detection order: X11 > XWayland > Hyprland > Wayland > Unknown if session_type == "x11": return "X11" if session_type == "wayland" and x_display and wayland_display: return "XWayland" if session_type == "wayland" and session_desktop == "hyprland": return "Hyprland" if session_type == "wayland": return "Wayland" return "Unknown" def _parse_linux_bool_flag_token(raw): if raw is None or not str(raw).strip(): raise ValueError("empty value") s = str(raw).strip().lower() if s == "true": return True if s == "false": return False raise ValueError(raw) def _parse_linux_positive_float(raw): if raw is None or not str(raw).strip(): raise ValueError("empty value") try: value = float(str(raw).strip()) except ValueError: raise ValueError(f"not a number: {raw!r}") if value <= 0: raise ValueError("must be a positive number") return value def _strip_linux_cli_overrides(argv): """ Linux-only: parse --gui / --xmode (true|false), --polling (seconds), remove them from argv. Last occurrence wins if a flag is repeated. """ gui_override = None xmode_override = None polling_override = None i = 1 kept = [argv[0]] if argv else [] while i < len(argv): item = argv[i] if item.startswith("--gui="): gui_override = _parse_linux_bool_flag_token(item.split("=", 1)[1]) i += 1 continue if item == "--gui": if i + 1 >= len(argv): raise ValueError("--gui requires true or false") gui_override = _parse_linux_bool_flag_token(argv[i + 1]) i += 2 continue if item.startswith("--xmode="): xmode_override = _parse_linux_bool_flag_token(item.split("=", 1)[1]) i += 1 continue if item == "--xmode": if i + 1 >= len(argv): raise ValueError("--xmode requires true or false") xmode_override = _parse_linux_bool_flag_token(argv[i + 1]) i += 2 continue if item.startswith("--polling="): polling_override = _parse_linux_positive_float(item.split("=", 1)[1]) i += 1 continue if item == "--polling": if i + 1 >= len(argv): raise ValueError("--polling requires a positive number (seconds)") polling_override = _parse_linux_positive_float(argv[i + 1]) i += 2 continue kept.append(item) i += 1 argv[:] = kept return gui_override, xmode_override, polling_override PLATFORM = get_os_and_display_server() # Linux: CLI UI vs GTK tray — False on other platforms (unused except behind LINUX checks). LINUX_USE_CLI_UI = False # Linux: optional override for xclip/wl-paste polling sleep (seconds); None = use built-ins (0.3 / 3). LINUX_CLIPBOARD_POLL_INTERVAL_SEC = None if PLATFORM.startswith(LINUX): if ( detect_linux_display_server() == "X11" or detect_linux_display_server() == "XWayland" or detect_linux_display_server() == "Unknown" ): XMODE = True else: XMODE = False try: gui_override, xmode_override, polling_override = _strip_linux_cli_overrides( sys.argv ) except ValueError as e: print(f"clipcascade (Linux CLI): {e}", file=sys.stderr) sys.exit(2) if xmode_override is not None: XMODE = xmode_override if gui_override is not None: LINUX_USE_CLI_UI = not gui_override else: LINUX_USE_CLI_UI = not XMODE if polling_override is not None: LINUX_CLIPBOARD_POLL_INTERVAL_SEC = polling_override # App version if PLATFORM == WINDOWS: APP_VERSION = "3.2.0" elif PLATFORM == MACOS: APP_VERSION = "3.2.0" elif PLATFORM.startswith(LINUX): if XMODE: APP_VERSION = "3.2.0" # gui version else: APP_VERSION = "3.2.0" # non-gui(cli) version # core constants RECONNECT_WS_TIMER = 10 # seconds WEBSOCKET_TIMEOUT = 3000 # milliseconds # P2P signaling WebSocket keepalive (RFC 6455 ping/pong). P2P_WS_PING_INTERVAL_SEC = 25 P2P_WS_PING_TIMEOUT_SEC = 20 # Data-channel keepalive (JSON envelope with _cc_keepalive); helps idle sessions and mobile radios. P2P_DC_HEARTBEAT_INTERVAL_SEC = 20 # After sleep, aiortc RTCPeerConnection.close() can block; cap wait so the asyncio # thread does not stall (which would also block processing ASSIGNED_ID / PEER_LIST). P2P_PC_CLOSE_TIMEOUT_SEC = 5.0 LOG_FILE_NAME = "clipcascade_log.log" LOG_LEVEL = logging.INFO # Use valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL DATA_FILE_NAME = "DATA" MAX_SIZE = 1048576 # 1 MiB FRAGMENT_SIZE = 15360 # 15 KiB SUBSCRIPTION_DESTINATION = "/user/queue/cliptext" SEND_DESTINATION = "/app/cliptext" LOGIN_URL = "/login" LOGOUT_URL = "/logout" MAXSIZE_URL = "/max-size" CSRF_URL = "/csrf-token" SERVER_MODE_URL = "/server-mode" WEBSOCKET_ENDPOINT = "/clipsocket" WEBSOCKET_ENDPOINT_P2P = "/p2psignaling" STUN_URL = "/stun-url" VERSION_URL = "https://raw.githubusercontent.com/Sathvik-Rao/ClipCascade/main/version.json" RELEASE_URL = "https://github.com/Sathvik-Rao/ClipCascade/releases/latest" GITHUB_URL = "https://github.com/Sathvik-Rao/ClipCascade" APP_NAME = "ClipCascade" HELP_URL = f"{GITHUB_URL}/blob/main/README.md" METADATA_URL = "https://raw.githubusercontent.com/Sathvik-Rao/ClipCascade/main/metadata.json" if PLATFORM == WINDOWS: MUTEX_NAME = "Global\\ClipCascade_Mutex_PSSR" elif PLATFORM == MACOS or PLATFORM.startswith(LINUX): MUTEX_NAME = "clipcascade.lock" # helper functions def get_user_home_directory(): """ Get the user's home directory in a cross-platform manner. On Windows, this resolves to something like C:\\Users\\ On macOS and Linux, this typically resolves to /Users/ or /home/, respectively. """ return os.path.expanduser("~") def get_program_files_directory(): """ Get the directory containing the program files. """ if PLATFORM == MACOS: app_dir = os.path.join( get_user_home_directory(), "Library", "Application Support", "ClipCascade" ) if not os.path.exists(app_dir): try: os.makedirs(app_dir) except Exception as e: raise e return app_dir else: if getattr(sys, "frozen", False): # Running as a PyInstaller executable return os.path.dirname(sys.executable) else: # Running as a regular Python script running_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(running_dir) # Go one folder up return parent_dir def get_downloads_folder(): """ Get the path to the user's Downloads folder in a cross-platform manner. By default, the Downloads directory is commonly located as: - Windows: C:\\Users\\\\Downloads - macOS: /Users//Downloads - Linux: /home//Downloads """ return os.path.join(get_user_home_directory(), "Downloads")