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())