Files
dotfiles/ClipCascade/core/constants.py
T
2026-07-23 16:48:58 +02:00

263 lines
8.1 KiB
Python
Executable File

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\\<Username>
On macOS and Linux, this typically resolves to /Users/<Username> or /home/<Username>, 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\\<Username>\\Downloads
- macOS: /Users/<Username>/Downloads
- Linux: /home/<Username>/Downloads
"""
return os.path.join(get_user_home_directory(), "Downloads")