diff --git a/pkg/freecad/InitGui.py b/pkg/freecad/InitGui.py index a0f6da5..ae2ea67 100644 --- a/pkg/freecad/InitGui.py +++ b/pkg/freecad/InitGui.py @@ -16,24 +16,12 @@ class SiloWorkbench(FreeCADGui.Workbench): Icon = "" def __init__(self): - # Find icon path - try: - locations = [ - os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "Silo"), - os.path.join(FreeCAD.getResourceDir(), "Mod", "Silo"), - os.path.expanduser( - "~/.var/app/org.freecad.FreeCAD/data/FreeCAD/Mod/Silo" - ), - os.path.expanduser("~/.FreeCAD/Mod/Silo"), - ] - for silo_dir in locations: - icon_path = os.path.join(silo_dir, "resources", "icons", "silo.svg") - if os.path.exists(icon_path): - self.__class__.Icon = icon_path - FreeCAD.Console.PrintMessage("Silo icon: " + icon_path + "\n") - break - except Exception as e: - FreeCAD.Console.PrintWarning("Silo icon error: " + str(e) + "\n") + # Resolve icon relative to this file so it works regardless of install location + icon_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "resources", "icons", "silo.svg" + ) + if os.path.exists(icon_path): + self.__class__.Icon = icon_path def Initialize(self): """Called when workbench is first activated.""" @@ -47,6 +35,7 @@ class SiloWorkbench(FreeCADGui.Workbench): "Silo_Pull", "Silo_Push", "Silo_Info", + "Silo_Settings", ] self.appendToolbar("Silo", self.toolbar_commands) @@ -55,12 +44,6 @@ class SiloWorkbench(FreeCADGui.Workbench): def Activated(self): """Called when workbench is activated.""" FreeCAD.Console.PrintMessage("Kindred Silo workbench activated\n") - FreeCAD.Console.PrintMessage( - " API: SILO_API_URL (default: http://localhost:8080/api)\n" - ) - FreeCAD.Console.PrintMessage( - " Projects: SILO_PROJECTS_DIR (default: ~/projects)\n" - ) self._show_shortcut_recommendations() def Deactivated(self): diff --git a/pkg/freecad/silo_commands.py b/pkg/freecad/silo_commands.py index 033ed4c..97fec2c 100644 --- a/pkg/freecad/silo_commands.py +++ b/pkg/freecad/silo_commands.py @@ -3,6 +3,7 @@ import json import os import re +import ssl import urllib.error import urllib.parse import urllib.request @@ -12,12 +13,41 @@ from typing import Any, Dict, List, Optional, Tuple import FreeCAD import FreeCADGui -# Configuration -SILO_API_URL = os.environ.get("SILO_API_URL", "http://localhost:8080/api") +# Preference group for Kindred Silo settings +_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo" + +# Configuration - preferences take priority over env vars SILO_PROJECTS_DIR = os.environ.get( "SILO_PROJECTS_DIR", os.path.expanduser("~/projects") ) + +def _get_api_url() -> str: + """Get Silo API URL from preferences, falling back to env var then default.""" + param = FreeCAD.ParamGet(_PREF_GROUP) + url = param.GetString("ApiUrl", "") + if url: + return url + return os.environ.get("SILO_API_URL", "http://localhost:8080/api") + + +def _get_ssl_verify() -> bool: + """Get SSL verification setting from preferences.""" + param = FreeCAD.ParamGet(_PREF_GROUP) + return param.GetBool("SslVerify", True) + + +def _get_ssl_context() -> ssl.SSLContext: + """Build an SSL context based on the current SSL verification preference.""" + if _get_ssl_verify(): + return ssl.create_default_context() + else: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + return ctx + + # Category name mapping for folder structure # Format: CCC -> "descriptive_name" CATEGORY_NAMES = { @@ -203,23 +233,10 @@ CATEGORY_NAMES = { } -# Icon directory -def _get_icon_dir(): - """Get the icons directory path.""" - locations = [ - os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "Silo", "resources", "icons"), - os.path.expanduser( - "~/.var/app/org.freecad.FreeCAD/data/FreeCAD/Mod/Silo/resources/icons" - ), - os.path.expanduser("~/.FreeCAD/Mod/Silo/resources/icons"), - ] - for loc in locations: - if os.path.isdir(loc): - return loc - return "" - - -_ICON_DIR = _get_icon_dir() +# Icon directory - resolve relative to this file so it works regardless of install location +_ICON_DIR = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "resources", "icons" +) def _icon(name): @@ -241,8 +258,14 @@ def get_projects_dir() -> Path: class SiloClient: """HTTP client for Silo API.""" - def __init__(self, base_url: str = SILO_API_URL): - self.base_url = base_url.rstrip("/") + def __init__(self, base_url: str = None): + self._explicit_url = base_url + + @property + def base_url(self) -> str: + if self._explicit_url: + return self._explicit_url.rstrip("/") + return _get_api_url().rstrip("/") def _request( self, method: str, path: str, data: Optional[Dict] = None @@ -254,7 +277,7 @@ class SiloClient: req = urllib.request.Request(url, data=body, headers=headers, method=method) try: - with urllib.request.urlopen(req) as resp: + with urllib.request.urlopen(req, context=_get_ssl_context()) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as e: error_body = e.read().decode() @@ -268,7 +291,7 @@ class SiloClient: req = urllib.request.Request(url, method="GET") try: - with urllib.request.urlopen(req) as resp: + with urllib.request.urlopen(req, context=_get_ssl_context()) as resp: with open(dest_path, "wb") as f: while True: chunk = resp.read(8192) @@ -330,7 +353,7 @@ class SiloClient: req = urllib.request.Request(url, data=body, headers=headers, method="POST") try: - with urllib.request.urlopen(req) as resp: + with urllib.request.urlopen(req, context=_get_ssl_context()) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as e: raise RuntimeError(f"Upload error {e.code}: {e.read().decode()}") @@ -1785,6 +1808,104 @@ class Silo_SetStatus: return FreeCAD.ActiveDocument is not None +class Silo_Settings: + """Configure Silo connection settings.""" + + def GetResources(self): + return { + "MenuText": "Settings", + "ToolTip": "Configure Silo API URL and SSL settings", + "Pixmap": _icon("info"), + } + + def Activated(self): + from PySide import QtCore, QtGui + + param = FreeCAD.ParamGet(_PREF_GROUP) + + dialog = QtGui.QDialog() + dialog.setWindowTitle("Silo Settings") + dialog.setMinimumWidth(450) + + layout = QtGui.QVBoxLayout(dialog) + + # URL + url_label = QtGui.QLabel("Silo API URL:") + layout.addWidget(url_label) + + url_input = QtGui.QLineEdit() + url_input.setPlaceholderText("http://localhost:8080/api") + current_url = param.GetString("ApiUrl", "") + if current_url: + url_input.setText(current_url) + else: + env_url = os.environ.get("SILO_API_URL", "") + if env_url: + url_input.setText(env_url) + layout.addWidget(url_input) + + url_hint = QtGui.QLabel( + "Leave empty to use SILO_API_URL environment variable " + "or default (http://localhost:8080/api)" + ) + url_hint.setWordWrap(True) + url_hint.setStyleSheet("color: #888; font-size: 11px;") + layout.addWidget(url_hint) + + layout.addSpacing(10) + + # SSL + ssl_checkbox = QtGui.QCheckBox("Verify SSL certificates") + ssl_checkbox.setChecked(param.GetBool("SslVerify", True)) + layout.addWidget(ssl_checkbox) + + ssl_hint = QtGui.QLabel( + "Disable only for internal servers with self-signed certificates." + ) + ssl_hint.setWordWrap(True) + ssl_hint.setStyleSheet("color: #888; font-size: 11px;") + layout.addWidget(ssl_hint) + + layout.addSpacing(10) + + # Current effective values (read-only) + status_label = QtGui.QLabel( + f"Active URL: {_get_api_url()}
" + f"SSL verification: {'enabled' if _get_ssl_verify() else 'disabled'}" + ) + status_label.setTextFormat(QtCore.Qt.RichText) + layout.addWidget(status_label) + + layout.addStretch() + + # Buttons + btn_layout = QtGui.QHBoxLayout() + save_btn = QtGui.QPushButton("Save") + cancel_btn = QtGui.QPushButton("Cancel") + btn_layout.addStretch() + btn_layout.addWidget(save_btn) + btn_layout.addWidget(cancel_btn) + layout.addLayout(btn_layout) + + def on_save(): + url = url_input.text().strip() + param.SetString("ApiUrl", url) + param.SetBool("SslVerify", ssl_checkbox.isChecked()) + FreeCAD.Console.PrintMessage( + f"Silo settings saved. URL: {_get_api_url()}, " + f"SSL verify: {_get_ssl_verify()}\n" + ) + dialog.accept() + + save_btn.clicked.connect(on_save) + cancel_btn.clicked.connect(dialog.reject) + + dialog.exec_() + + def IsActive(self): + return True + + # Register commands FreeCADGui.addCommand("Silo_Open", Silo_Open()) FreeCADGui.addCommand("Silo_New", Silo_New()) @@ -1796,3 +1917,4 @@ FreeCADGui.addCommand("Silo_Info", Silo_Info()) FreeCADGui.addCommand("Silo_TagProjects", Silo_TagProjects()) FreeCADGui.addCommand("Silo_Rollback", Silo_Rollback()) FreeCADGui.addCommand("Silo_SetStatus", Silo_SetStatus()) +FreeCADGui.addCommand("Silo_Settings", Silo_Settings())