diff --git a/docker-launcher/.gitignore b/docker-launcher/.gitignore
new file mode 100644
index 000000000..3b5fccb4e
--- /dev/null
+++ b/docker-launcher/.gitignore
@@ -0,0 +1,29 @@
+# Python
+__pycache__/
+*.py[cod]
+*.so
+.Python
+venv/
+.venv/
+
+# PyInstaller
+build/
+dist/
+*.spec.bak
+
+# IDE
+.idea/
+.vscode/
+*.swp
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Docker
+*.log
+
+# Temp
+*.tmp
+*.bak
+
diff --git a/docker-launcher/Dockerfile b/docker-launcher/Dockerfile
new file mode 100644
index 000000000..3f83af961
--- /dev/null
+++ b/docker-launcher/Dockerfile
@@ -0,0 +1,164 @@
+# eSim Docker Container
+# Multi-stage build to keep the image smaller (~3.5GB vs ~5GB)
+# FOSSEE IIT Bombay
+
+# ============================================
+# Stage 1: Build dependencies
+# ============================================
+FROM ubuntu:22.04 AS builder
+
+ENV DEBIAN_FRONTEND=noninteractive
+ENV TZ=Asia/Kolkata
+
+# Install build tools
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential gcc g++ make cmake git \
+ python3 python3-pip python3-dev python3-venv \
+ autoconf automake libtool pkg-config bison flex gettext wget \
+ libgtk-3-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+# Clone eSim
+RUN git clone --depth 1 https://github.com/FOSSEE/eSim.git /build/esim
+
+# Setup Python venv with dependencies
+# Note: setuptools<58 is needed for hdlparse which uses deprecated use_2to3
+RUN python3 -m venv /build/venv \
+ && /build/venv/bin/pip install --no-cache-dir "setuptools<58.0.0" wheel \
+ && /build/venv/bin/pip install --no-cache-dir \
+ matplotlib==3.7.5 numpy==1.24.4 scipy==1.10.1 \
+ PyQt5==5.15.7 pillow==10.4.0 hdlparse==1.0.4 watchdog==4.0.2
+
+# Build GAW3 (analog waveform viewer) from source
+# gtkwave is digital only, gaw3 is what eSim actually uses
+RUN git clone --depth 1 https://github.com/StefanSchippers/xschem-gaw.git /build/gaw3 \
+ && cd /build/gaw3 \
+ && aclocal && autoheader \
+ && automake --add-missing --foreign 2>/dev/null || true \
+ && autoconf \
+ && sed -i 's/GETTEXT_MACRO_VERSION = 0.18/GETTEXT_MACRO_VERSION = 0.20/' po/Makefile.in.in \
+ && ./configure --prefix=/usr/local \
+ && make -j$(nproc) \
+ && make DESTDIR=/build/gaw3-install install
+
+
+# ============================================
+# Stage 2: Runtime image
+# ============================================
+FROM ubuntu:22.04
+
+LABEL maintainer="FOSSEE IIT Bombay"
+LABEL description="eSim EDA Tool with GUI support"
+
+ENV DEBIAN_FRONTEND=noninteractive
+ENV TZ=Asia/Kolkata
+ENV LANG=C.UTF-8
+ENV LC_ALL=C.UTF-8
+
+# eSim paths
+ENV ESIM_HOME=/usr/local/esim
+ENV PYTHONPATH="${ESIM_HOME}/src:/opt/venv/lib/python3.10/site-packages"
+
+# VNC settings
+ENV VNC_PORT=5901
+ENV NOVNC_PORT=6080
+ENV VNC_RESOLUTION=1920x1080
+ENV VNC_DEPTH=24
+
+# Desktop and font rendering settings
+ENV GTK_THEME=Adwaita
+ENV UBUNTU_MENUPROXY=0
+ENV XDG_DATA_DIRS=/usr/share:/usr/local/share:/usr/share/icons
+ENV QT_AUTO_SCREEN_SCALE_FACTOR=1
+ENV GDK_SCALE=1
+ENV FREETYPE_PROPERTIES="truetype:interpreter-version=40"
+
+# Install runtime packages
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ kicad kicad-libraries ngspice gtkwave xterm \
+ python3 python3-wxgtk4.0 \
+ libx11-6 libxext6 libxrender1 libxfixes3 libxi6 libxrandr2 \
+ libxcursor1 libxinerama1 libgl1 libgl1-mesa-glx libgl1-mesa-dri \
+ libxcb1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \
+ libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 \
+ libxcb-xkb1 libxkbcommon0 libxkbcommon-x11-0 dbus-x11 \
+ adwaita-icon-theme-full hicolor-icon-theme gnome-icon-theme \
+ oxygen-icon-theme tango-icon-theme humanity-icon-theme \
+ ca-certificates libgtk-3-0 libcanberra-gtk-module xdg-utils \
+ libglib2.0-0 libfontconfig1 libfreetype6 \
+ tigervnc-standalone-server tigervnc-common \
+ xfce4 xfce4-terminal novnc websockify \
+ xdotool wmctrl openbox tint2 \
+ && rm -rf /var/lib/apt/lists/* && apt-get clean
+
+# Copy built artifacts from builder stage
+COPY --from=builder /build/gaw3-install/usr/local /usr/local/
+COPY --from=builder /build/esim ${ESIM_HOME}
+COPY --from=builder /build/venv /opt/venv
+
+# Create user
+ARG USERNAME=esim-user
+ARG USER_UID=1000
+ARG USER_GID=1000
+
+RUN groupadd --gid ${USER_GID} ${USERNAME} \
+ && useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} \
+ && mkdir -p /home/${USERNAME}/workspace \
+ && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME} \
+ && chown -R ${USERNAME}:${USERNAME} ${ESIM_HOME}
+
+WORKDIR ${ESIM_HOME}/src/frontEnd
+
+# Create eSim config
+RUN mkdir -p /home/${USERNAME}/.esim \
+ && printf '[DEFAULT]\nworkspace=/home/%s/eSim-Workspace\n\n[eSim]\nworkspace=/home/%s/eSim-Workspace\nkicad=/usr/bin\nngspice=/usr/bin\n' \
+ ${USERNAME} ${USERNAME} > /home/${USERNAME}/.esim/config.ini \
+ && echo '{}' > /home/${USERNAME}/.esim/modelica_map.json \
+ && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.esim
+
+# Setup KiCad symbol libraries
+RUN mkdir -p /home/${USERNAME}/.config/kicad/6.0 \
+ && KICAD_CONFIG=/home/${USERNAME}/.config/kicad/6.0 \
+ && ESIM_SYMLIB=/usr/local/esim/library/kicadLibrary/eSim-symbols \
+ && KICAD_SYMLIB=/usr/share/kicad/symbols \
+ && echo '(sym_lib_table' > ${KICAD_CONFIG}/sym-lib-table \
+ && for lib in eSim_Devices eSim_Sources eSim_Analog eSim_Digital eSim_Hybrid eSim_Power eSim_Subckt eSim_Miscellaneous eSim_Plot eSim_Nghdl eSim_Ngveri eSim_SKY130 eSim_SKY130_Subckts eSim_User; do \
+ echo " (lib (name \"$lib\")(type \"KiCad\")(uri \"${ESIM_SYMLIB}/${lib}.kicad_sym\")(options \"\")(descr \"\"))" >> ${KICAD_CONFIG}/sym-lib-table; \
+ done \
+ && for lib in Device power Simulation_SPICE Connector Analog Transistor_BJT Transistor_FET Diode Amplifier_Operational; do \
+ echo " (lib (name \"$lib\")(type \"KiCad\")(uri \"${KICAD_SYMLIB}/${lib}.kicad_sym\")(options \"\")(descr \"\"))" >> ${KICAD_CONFIG}/sym-lib-table; \
+ done \
+ && echo ')' >> ${KICAD_CONFIG}/sym-lib-table \
+ && echo '(fp_lib_table)' > ${KICAD_CONFIG}/fp-lib-table \
+ && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config
+
+# Setup VNC with openbox + tint2 taskbar
+RUN mkdir -p /home/${USERNAME}/.vnc \
+ && printf '#!/bin/bash\nunset SESSION_MANAGER\nunset DBUS_SESSION_BUS_ADDRESS\nexport XDG_RUNTIME_DIR=/tmp/runtime-esim-user\nmkdir -p $XDG_RUNTIME_DIR && chmod 700 $XDG_RUNTIME_DIR\ntint2 &\nexec openbox-session\n' \
+ > /home/${USERNAME}/.vnc/xstartup \
+ && chmod +x /home/${USERNAME}/.vnc/xstartup \
+ && printf '\x9f\x87\x18\xb4\x8e\x8f\x8a\x57' > /home/${USERNAME}/.vnc/passwd \
+ && chmod 600 /home/${USERNAME}/.vnc/passwd \
+ && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.vnc
+
+# XFCE config to fix window focus issues
+RUN mkdir -p /home/${USERNAME}/.config/xfce4/xfconf/xfce-perchannel-xml \
+ && printf '\n\n \n \n \n \n \n\n' \
+ > /home/${USERNAME}/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml \
+ && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config
+
+# Font rendering config for better VNC quality
+RUN mkdir -p /home/${USERNAME}/.config/fontconfig \
+ && printf '\n\n\n true\n true\n hintslight\n rgb\n\n' \
+ > /home/${USERNAME}/.config/fontconfig/fonts.conf \
+ && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config/fontconfig
+
+# Startup script
+RUN printf '#!/bin/bash\nset -e\n\n# Ensure workspace exists\nmkdir -p /home/esim-user/eSim-Workspace\ncp -rn /usr/local/esim/Examples/* /home/esim-user/eSim-Workspace/ 2>/dev/null || true\n\nif [ "$1" = "--vnc" ] || [ "$USE_VNC" = "1" ]; then\n echo "Starting eSim in VNC mode"\n vncserver -kill :1 2>/dev/null || true\n export XDG_RUNTIME_DIR=/tmp/runtime-esim-user\n mkdir -p $XDG_RUNTIME_DIR && chmod 700 $XDG_RUNTIME_DIR\n vncserver :1 -geometry ${VNC_RESOLUTION:-1920x1080} -depth ${VNC_DEPTH:-24} -SecurityTypes None\n sleep 3\n websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6080} localhost:5901 &\n echo "VNC ready at http://localhost:${NOVNC_PORT:-6080}/vnc.html"\n export DISPLAY=:1\n sleep 2\n cd /usr/local/esim/src/frontEnd\n python3 Application.py\n tail -f /dev/null\nelse\n echo "Starting eSim in X11 mode"\n cd /usr/local/esim/src/frontEnd\n exec python3 Application.py\nfi\n' \
+ > /usr/local/bin/start-esim.sh \
+ && chmod +x /usr/local/bin/start-esim.sh
+
+USER ${USERNAME}
+
+ENTRYPOINT ["/usr/local/bin/start-esim.sh"]
+CMD []
diff --git a/docker-launcher/README.md b/docker-launcher/README.md
new file mode 100644
index 000000000..741111278
--- /dev/null
+++ b/docker-launcher/README.md
@@ -0,0 +1,154 @@
+# eSim Docker
+
+
+
+
+
+
+
+ Run eSim anywhere using Docker - No installation required!
+
+
+
+ Download Launcher •
+ Quick Start •
+ Troubleshooting
+
+
+---
+
+## About
+
+This project provides a Docker-based solution to run **eSim** (Electronic Circuit Simulation) on any operating system. eSim is developed by FOSSEE, IIT Bombay and integrates KiCad, Ngspice, and Python for circuit design and simulation.
+
+**What's included:**
+- KiCad for schematic design
+- Ngspice for SPICE simulation
+- GAW3 analog waveform viewer
+- All eSim libraries pre-configured
+
+---
+
+## Quick Start
+
+### Step 1: Get Docker
+
+Download [Docker Desktop](https://www.docker.com/products/docker-desktop) and make sure it's running.
+
+### Step 2: Download the Launcher
+
+Go to [Releases](../../releases/latest) and download:
+- Windows: `eSim-Launcher-Windows.exe`
+- Linux: `eSim-Launcher-Linux`
+- macOS: `eSim-Launcher-macOS`
+
+### Step 3: Run it
+
+**Windows:** Double-click the `.exe` file.
+
+**Linux:** Open terminal and run:
+```bash
+chmod +x eSim-Launcher-Linux
+./eSim-Launcher-Linux
+```
+
+**macOS:** Open terminal and run:
+```bash
+chmod +x eSim-Launcher-macOS
+./eSim-Launcher-macOS
+```
+
+---
+
+## Display Modes
+
+The launcher offers two display modes:
+
+| Mode | Best For | How it Works |
+|------|----------|--------------|
+| **VNC** | Windows, macOS | Opens eSim in your browser. Works everywhere, no setup needed. |
+| **X11** | Linux | Opens eSim in a native window. Best performance on Linux. |
+
+### Recommendations
+
+- **Linux** → Use X11 mode (recommended, no lag)
+- **Windows** → Use VNC mode (X11 works but KiCad may lag)
+- **macOS** → Use VNC mode (X11 requires XQuartz)
+
+Both modes work on all platforms - the launcher will guide you through any required setup.
+
+---
+
+## Command Line Usage
+
+```bash
+# Interactive menu
+python run_esim_docker.py
+
+# Direct VNC mode
+python run_esim_docker.py --vnc
+
+# Direct X11 mode
+python run_esim_docker.py --x11
+
+# Update image
+python run_esim_docker.py --pull
+```
+
+---
+
+## Workspace
+
+Your projects are saved to:
+
+| OS | Location |
+|----|----------|
+| Windows | `C:\Users\\eSim_Workspace` |
+| Linux/macOS | `~/eSim_Workspace` |
+
+This folder is mounted into the container, so your files persist.
+
+---
+
+## Troubleshooting
+
+### Docker not running
+Open Docker Desktop and wait for it to fully start.
+
+### Browser shows "localhost not found"
+Wait a few seconds and refresh. The container needs time to start.
+
+### VNC shows blank screen
+Refresh the browser page. If still blank, restart the launcher.
+
+### X11 mode: window doesn't appear (Windows)
+Make sure VcXsrv is running. The launcher auto-installs it if needed.
+
+### X11 mode: window doesn't appear (macOS)
+Install XQuartz from xquartz.org, then run `xhost +localhost` in terminal.
+
+---
+
+## Building from Source
+
+```bash
+docker build -t esim:latest .
+python run_esim_docker.py --build
+```
+
+---
+
+## Credits
+
+- **eSim** - FOSSEE Team, IIT Bombay
+- **KiCad** - KiCad Developers
+- **Ngspice** - Ngspice Team
+- **GAW3** - Hervé Quillévéré, Stefan Schippers
+
+Created as part of the FOSSEE Internship program.
+
+---
+
+## License
+
+GPL-3.0
diff --git a/docker-launcher/assets/esim_logo.icns b/docker-launcher/assets/esim_logo.icns
new file mode 100644
index 000000000..14d19d24c
Binary files /dev/null and b/docker-launcher/assets/esim_logo.icns differ
diff --git a/docker-launcher/assets/esim_logo.ico b/docker-launcher/assets/esim_logo.ico
new file mode 100644
index 000000000..53a345281
Binary files /dev/null and b/docker-launcher/assets/esim_logo.ico differ
diff --git a/docker-launcher/assets/esim_logo.png b/docker-launcher/assets/esim_logo.png
new file mode 100644
index 000000000..f3d5d7ca6
Binary files /dev/null and b/docker-launcher/assets/esim_logo.png differ
diff --git a/docker-launcher/assets/esim_text.png b/docker-launcher/assets/esim_text.png
new file mode 100644
index 000000000..fe8fc6e38
Binary files /dev/null and b/docker-launcher/assets/esim_text.png differ
diff --git a/docker-launcher/eSim.spec b/docker-launcher/eSim.spec
new file mode 100644
index 000000000..55704c968
--- /dev/null
+++ b/docker-launcher/eSim.spec
@@ -0,0 +1,33 @@
+# PyInstaller spec file for eSim Docker Launcher
+# Build: pyinstaller eSim.spec
+
+block_cipher = None
+
+a = Analysis(
+ ['run_esim_docker.py'],
+ pathex=[],
+ binaries=[],
+ datas=[],
+ hiddenimports=['socket', 'webbrowser', 'threading', 'tempfile', 'argparse'],
+ hookspath=[],
+ runtime_hooks=[],
+ excludes=['tkinter', 'matplotlib', 'numpy', 'scipy', 'PIL', 'pandas', 'pytest'],
+ cipher=block_cipher,
+)
+
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ [],
+ name='eSim-Launcher',
+ debug=False,
+ strip=False,
+ upx=True,
+ console=True, # Keep console for menu
+ icon='esim_logo.png' if __import__('os').path.exists('esim_logo.png') else None,
+)
diff --git a/docker-launcher/run_esim_docker.py b/docker-launcher/run_esim_docker.py
new file mode 100644
index 000000000..9851bb9ad
--- /dev/null
+++ b/docker-launcher/run_esim_docker.py
@@ -0,0 +1,686 @@
+"""
+eSim Docker Launcher
+FOSSEE IIT Bombay Internship Project
+
+Simple launcher to run eSim in Docker with VNC or X11 display.
+"""
+
+import os
+import sys
+import platform
+import subprocess
+import shutil
+import socket
+import webbrowser
+import time
+import tempfile
+import urllib.request
+from pathlib import Path
+
+# Docker image (GitHub Container Registry)
+DOCKER_IMAGE = "ghcr.io/barun-2005/esim-docker:latest"
+LOCAL_IMAGE = "esim:latest"
+CONTAINER_NAME = "esim-container"
+DOCKERFILE_DIR = Path(__file__).parent.resolve()
+WORKSPACE_DIR_NAME = "eSim_Workspace"
+
+# VcXsrv config for Windows X11 mode
+VCXSRV_CONFIG = """
+
+"""
+
+
+def run_cmd(cmd, capture=False, check=True, shell=False):
+ if capture:
+ return subprocess.run(cmd, capture_output=True, text=True, check=check, shell=shell)
+ return subprocess.run(cmd, check=check, shell=shell)
+
+
+def cmd_exists(cmd):
+ return shutil.which(cmd) is not None
+
+
+def clear():
+ os.system('cls' if os.name == 'nt' else 'clear')
+
+
+def show_banner():
+ clear()
+ print("""
+ ╔══════════════════════════════════════════╗
+ ║ eSim Docker Launcher ║
+ ║ FOSSEE, IIT Bombay ║
+ ╚══════════════════════════════════════════╝
+ """)
+
+
+def info(msg):
+ print(f" [i] {msg}")
+
+def ok(msg):
+ print(f" [+] {msg}")
+
+def warn(msg):
+ print(f" [!] {msg}")
+
+def err(msg):
+ print(f" [-] {msg}", file=sys.stderr)
+
+
+def find_free_port(start=6080, tries=20):
+ for port in range(start, start + tries):
+ try:
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.bind(('', port))
+ return port
+ except OSError:
+ continue
+ raise RuntimeError(f"No free port found between {start}-{start+tries}")
+
+
+def wait_for_port(port, timeout=30):
+ """Wait until noVNC HTTP server is responding."""
+ start = time.time()
+ while time.time() - start < timeout:
+ try:
+ req = urllib.request.urlopen(f"http://localhost:{port}/", timeout=2)
+ req.close()
+ return True
+ except:
+ time.sleep(1)
+ return False
+
+
+def get_os():
+ system = platform.system().lower()
+ if system == "linux":
+ try:
+ with open("/proc/version") as f:
+ if "microsoft" in f.read().lower():
+ return "wsl2"
+ except:
+ pass
+ return "linux"
+ if system == "windows":
+ return "windows"
+ if system == "darwin":
+ return "macos"
+ return system
+
+
+def is_wslg():
+ return (get_os() == "wsl2" and
+ os.environ.get("WAYLAND_DISPLAY") and
+ Path("/mnt/wslg").exists())
+
+
+# Docker installation helpers
+
+def open_url(url):
+ """Open URL in browser."""
+ try:
+ webbrowser.open(url)
+ return True
+ except:
+ return False
+
+
+def install_docker_windows():
+ """Check Docker on Windows - try to start it, or install if needed."""
+ # First check if Docker Desktop is installed but not running
+ docker_paths = [
+ Path(os.environ.get("PROGRAMFILES", "")) / "Docker" / "Docker" / "Docker Desktop.exe",
+ Path(os.environ.get("LOCALAPPDATA", "")) / "Docker" / "Docker Desktop.exe",
+ ]
+
+ docker_exe = None
+ for p in docker_paths:
+ if p.exists():
+ docker_exe = p
+ break
+
+ if docker_exe:
+ info("Docker Desktop is installed but not running.")
+ resp = input(" Start Docker Desktop now? (y/n): ").strip().lower()
+ if resp == 'y':
+ try:
+ info("Starting Docker Desktop...")
+ subprocess.Popen([str(docker_exe)], creationflags=subprocess.DETACHED_PROCESS)
+ info("Docker Desktop is starting. Please wait 30-60 seconds...")
+ info("Then run this launcher again.")
+ input("\n Press Enter to exit...")
+ return True
+ except Exception as e:
+ err(f"Failed to start: {e}")
+ return False
+
+ # Docker not installed - offer to install
+ info("Docker Desktop is not installed.")
+ print()
+ print(" Docker Desktop is required to run eSim.")
+ print()
+ resp = input(" Install Docker Desktop now? (y/n): ").strip().lower()
+ if resp != 'y':
+ print()
+ info("You can install Docker manually from:")
+ info("https://www.docker.com/products/docker-desktop")
+ return False
+
+ if not cmd_exists("winget"):
+ info("Opening Docker download page...")
+ open_url("https://www.docker.com/products/docker-desktop")
+ info("Please install Docker Desktop and restart this launcher.")
+ return False
+
+ try:
+ info("Installing Docker Desktop (this takes a few minutes)...")
+ subprocess.run(["winget", "install", "-e", "--id", "Docker.DockerDesktop",
+ "--accept-source-agreements"], check=True)
+ print()
+ ok("Docker Desktop installed!")
+ warn("Please RESTART your computer, then run this launcher again.")
+ input("\n Press Enter to exit...")
+ return True
+ except Exception as e:
+ err(f"Install failed: {e}")
+ info("Opening Docker download page...")
+ open_url("https://www.docker.com/products/docker-desktop")
+ return False
+
+
+def guide_docker_linux():
+ """Guide user to install Docker on Linux."""
+ info("Docker is not running or not installed.")
+ print()
+ print(" To install Docker on Linux, run these commands:")
+ print()
+ print(" curl -fsSL https://get.docker.com | sudo sh")
+ print(" sudo usermod -aG docker $USER")
+ print(" # Then log out and log back in")
+ print()
+ print(" After installation, start Docker:")
+ print(" sudo systemctl start docker")
+ print()
+ resp = input(" Open Docker installation guide? (y/n): ").strip().lower()
+ if resp == 'y':
+ open_url("https://docs.docker.com/engine/install/")
+
+
+def guide_docker_macos():
+ """Guide user to install Docker on macOS."""
+ info("Docker Desktop is not running or not installed.")
+ print()
+ print(" Docker Desktop is required to run eSim.")
+ print()
+ resp = input(" Open Docker download page? (y/n): ").strip().lower()
+ if resp == 'y':
+ open_url("https://www.docker.com/products/docker-desktop")
+ print()
+ info("After installing, open Docker Desktop and wait for it to start.")
+
+
+def install_vcxsrv_windows():
+ """Install VcXsrv on Windows."""
+ info("VcXsrv is needed for native window mode on Windows.")
+ resp = input(" Install VcXsrv now? (y/n): ").strip().lower()
+ if resp != 'y':
+ return False
+
+ if not cmd_exists("winget"):
+ info("Opening VcXsrv download page...")
+ open_url("https://sourceforge.net/projects/vcxsrv/")
+ return False
+
+ try:
+ info("Installing VcXsrv...")
+ subprocess.run(["winget", "install", "-e", "--id", "marha.VcXsrv",
+ "--accept-source-agreements"], check=True)
+ ok("VcXsrv installed!")
+ return True
+ except:
+ err("Install failed")
+ open_url("https://sourceforge.net/projects/vcxsrv/")
+ return False
+
+
+def start_vcxsrv():
+ """Start VcXsrv X server on Windows."""
+ paths = [
+ Path(os.environ.get("PROGRAMFILES", "")) / "VcXsrv" / "vcxsrv.exe",
+ Path(os.environ.get("PROGRAMFILES(X86)", "")) / "VcXsrv" / "vcxsrv.exe",
+ ]
+
+ vcxsrv = None
+ for p in paths:
+ if p.exists():
+ vcxsrv = p
+ break
+
+ if not vcxsrv:
+ if not install_vcxsrv_windows():
+ return False
+ for p in paths:
+ if p.exists():
+ vcxsrv = p
+ break
+ if not vcxsrv:
+ err("VcXsrv not found")
+ return False
+
+ # Check if already running
+ try:
+ result = subprocess.run(["tasklist", "/FI", "IMAGENAME eq vcxsrv.exe"],
+ capture_output=True, text=True)
+ if "vcxsrv.exe" in result.stdout.lower():
+ ok("VcXsrv already running")
+ return True
+ except:
+ pass
+
+ # Write config and launch
+ config = Path(tempfile.gettempdir()) / "esim_xserver.xlaunch"
+ config.write_text(VCXSRV_CONFIG)
+
+ info("Starting VcXsrv...")
+ try:
+ xlaunch = vcxsrv.parent / "xlaunch.exe"
+ if xlaunch.exists():
+ subprocess.Popen([str(xlaunch), "-run", str(config)],
+ creationflags=subprocess.DETACHED_PROCESS)
+ else:
+ subprocess.Popen([str(vcxsrv), ":0", "-multiwindow", "-clipboard", "-wgl", "-ac"],
+ creationflags=subprocess.DETACHED_PROCESS)
+ time.sleep(2)
+ ok("VcXsrv started")
+ return True
+ except Exception as e:
+ err(f"Failed: {e}")
+ return False
+
+
+def get_display_args(os_type):
+ """Get Docker display environment for X11 mode."""
+ if os_type == "linux":
+ display = os.environ.get("DISPLAY", ":0")
+ try:
+ subprocess.run(["xhost", "+local:docker"], capture_output=True)
+ except:
+ pass
+ return display, ["-e", f"DISPLAY={display}", "-v", "/tmp/.X11-unix:/tmp/.X11-unix:rw"]
+
+ if os_type == "wsl2":
+ if is_wslg():
+ display = os.environ.get("DISPLAY", ":0")
+ return display, ["-e", f"DISPLAY={display}", "-e", "QT_QPA_PLATFORM=xcb",
+ "-v", "/tmp/.X11-unix:/tmp/.X11-unix:rw"]
+ try:
+ with open("/etc/resolv.conf") as f:
+ for line in f:
+ if line.startswith("nameserver"):
+ host_ip = line.split()[1]
+ break
+ else:
+ host_ip = "localhost"
+ except:
+ host_ip = "localhost"
+ return f"{host_ip}:0.0", ["-e", f"DISPLAY={host_ip}:0.0", "-e", "LIBGL_ALWAYS_INDIRECT=1"]
+
+ if os_type == "windows":
+ return "host.docker.internal:0.0", [
+ "-e", "DISPLAY=host.docker.internal:0.0",
+ "-e", "QT_X11_NO_MITSHM=1", "-e", "NO_AT_BRIDGE=1", "-e", "GTK_A11Y=none"
+ ]
+
+ if os_type == "macos":
+ return "host.docker.internal:0", [
+ "-e", "DISPLAY=host.docker.internal:0", "-e", "LIBGL_ALWAYS_INDIRECT=1"
+ ]
+
+ return ":0", ["-e", "DISPLAY=:0"]
+
+
+# Docker operations
+
+def docker_ok():
+ if not cmd_exists("docker"):
+ return False
+ try:
+ run_cmd(["docker", "info"], capture=True)
+ return True
+ except:
+ return False
+
+
+def image_exists(image):
+ try:
+ result = run_cmd(["docker", "images", "-q", image], capture=True)
+ return bool(result.stdout.strip())
+ except:
+ return False
+
+
+def pull_image(image=DOCKER_IMAGE):
+ info(f"Pulling {image}...")
+ info("This may take a few minutes on first run...")
+ print()
+ try:
+ subprocess.run(["docker", "pull", image], check=True)
+ print()
+ ok("Image downloaded!")
+ return True
+ except:
+ err("Pull failed")
+ return False
+
+
+def build_image():
+ dockerfile = DOCKERFILE_DIR / "Dockerfile"
+ if not dockerfile.exists():
+ err(f"Dockerfile not found: {dockerfile}")
+ return False
+
+ info("Building from Dockerfile (10-15 min)...")
+ print()
+ try:
+ subprocess.run(["docker", "build", "-t", LOCAL_IMAGE, str(DOCKERFILE_DIR)], check=True)
+ print()
+ ok("Build complete!")
+ return True
+ except:
+ err("Build failed")
+ return False
+
+
+def stop_container():
+ run_cmd(["docker", "rm", "-f", CONTAINER_NAME], capture=True, check=False)
+
+
+def get_workspace():
+ ws = Path.home() / WORKSPACE_DIR_NAME
+ ws.mkdir(exist_ok=True)
+ return ws
+
+
+def get_image(build_local=False):
+ if build_local:
+ return LOCAL_IMAGE if build_image() else None
+
+ if image_exists(DOCKER_IMAGE):
+ return DOCKER_IMAGE
+
+ print()
+ if pull_image(DOCKER_IMAGE):
+ return DOCKER_IMAGE
+
+ if image_exists(LOCAL_IMAGE):
+ warn(f"Using local image: {LOCAL_IMAGE}")
+ return LOCAL_IMAGE
+
+ warn("Remote unavailable, trying local build...")
+ return LOCAL_IMAGE if build_image() else None
+
+
+# Launch modes
+
+def launch_vnc(image, workspace):
+ """Run eSim in VNC mode (browser)."""
+ stop_container()
+
+ try:
+ vnc_port = find_free_port(6080)
+ server_port = find_free_port(5901)
+ except RuntimeError as e:
+ err(str(e))
+ return 1
+
+ cmd = [
+ "docker", "run", "--rm", "-it", "--name", CONTAINER_NAME,
+ "--shm-size=256m", "--ipc=host",
+ "-v", f"{workspace}:/home/esim-user/eSim-Workspace:rw",
+ "-p", f"{vnc_port}:6080", "-p", f"{server_port}:5901",
+ "-e", "USE_VNC=1", image, "--vnc"
+ ]
+
+ url = f"http://localhost:{vnc_port}/vnc.html"
+
+ print()
+ print(" " + "=" * 50)
+ ok("Starting VNC Mode...")
+ print()
+ print(f" Browser URL: {url}")
+ print(f" VNC Server: localhost:{server_port}")
+ print()
+ print(" Waiting for container to start...")
+ print(" " + "=" * 50)
+ print()
+
+ # Start container in background thread and wait for port
+ import threading
+ container_started = threading.Event()
+
+ def run_container():
+ subprocess.run(cmd)
+ container_started.set()
+
+ thread = threading.Thread(target=run_container, daemon=True)
+ thread.start()
+
+ # Wait for VNC port to be ready, then open browser
+ info("Waiting for eSim to start...")
+ if wait_for_port(vnc_port, timeout=45):
+ time.sleep(5) # Wait for noVNC and eSim to fully initialize
+ ok("Opening browser...")
+ webbrowser.open(url)
+ else:
+ warn("Container is starting slowly. Please open manually:")
+ print(f" {url}")
+
+ # Wait for container to finish
+ thread.join()
+ return 0
+
+
+def launch_x11(image, workspace, os_type):
+ """Run eSim in X11 mode (native window)."""
+ if os_type == "windows":
+ if not start_vcxsrv():
+ err("X11 server not available. Try VNC mode instead.")
+ return 1
+
+ if os_type == "macos":
+ info("Make sure XQuartz is running (download from xquartz.org)")
+ info("Run 'xhost +localhost' in terminal if needed")
+ print()
+
+ display, display_args = get_display_args(os_type)
+ stop_container()
+
+ cmd = [
+ "docker", "run", "--rm", "-it", "--name", CONTAINER_NAME,
+ "--shm-size=256m", "--ipc=host",
+ "-v", f"{workspace}:/home/esim-user/eSim-Workspace:rw",
+ ] + display_args + [image]
+
+ print()
+ print(" " + "=" * 50)
+ ok("Starting X11 Mode")
+ print(f" Display: {display}")
+ if os_type in ["windows", "macos"]:
+ print(" Note: KiCad schematic editor may lag slightly")
+ print(" " + "=" * 50)
+ print()
+
+ return subprocess.run(cmd).returncode
+
+
+# Menu system
+
+def show_menu(os_type):
+ """Display interactive menu based on OS."""
+ show_banner()
+ print(f" OS: {os_type.upper()}")
+ print()
+
+ if os_type == "linux":
+ # Linux: X11 first (recommended)
+ print(" 1. Launch X11 Mode (Recommended)")
+ print(" 2. Launch VNC Mode (Browser)")
+ else:
+ # Windows/Mac: VNC first (recommended)
+ print(" 1. Launch VNC Mode (Recommended - Browser)")
+ print(" 2. Launch X11 Mode (Native Window)")
+ if os_type == "windows":
+ print(" [KiCad may lag, auto-installs VcXsrv if needed]")
+ elif os_type == "macos":
+ print(" [Requires XQuartz, KiCad may lag]")
+
+ print(" 3. Update Image")
+ print(" 4. Build from Source")
+ print(" 0. Exit")
+ print()
+ return input(" Choice: ").strip()
+
+
+def handle_docker_missing(os_type):
+ """Handle case when Docker is not available."""
+ if os_type == "windows":
+ install_docker_windows()
+ elif os_type == "linux":
+ guide_docker_linux()
+ elif os_type == "macos":
+ guide_docker_macos()
+ else:
+ err("Docker is required. Please install Docker Desktop.")
+ input("\n Press Enter to continue...")
+
+
+def run_menu():
+ """Interactive menu loop."""
+ while True:
+ os_type = get_os()
+ choice = show_menu(os_type)
+
+ if choice == "0":
+ print()
+ info("Bye!")
+ return 0
+
+ if choice not in ["1", "2", "3", "4"]:
+ err("Invalid choice")
+ input("\n Press Enter...")
+ continue
+
+ # Check Docker
+ print()
+ if not docker_ok():
+ handle_docker_missing(os_type)
+ continue
+
+ ok("Docker ready")
+ workspace = get_workspace()
+ ok(f"Workspace: {workspace}")
+
+ if choice == "3":
+ print()
+ pull_image(DOCKER_IMAGE)
+ input("\n Press Enter...")
+ continue
+
+ if choice == "4":
+ print()
+ build_image()
+ input("\n Press Enter...")
+ continue
+
+ image = get_image()
+ if not image:
+ err("No image available")
+ input("\n Press Enter...")
+ continue
+
+ # Launch based on OS and choice
+ if os_type == "linux":
+ # Linux: 1=X11, 2=VNC
+ if choice == "1":
+ return launch_x11(image, workspace, os_type)
+ else:
+ return launch_vnc(image, workspace)
+ else:
+ # Windows/Mac: 1=VNC, 2=X11
+ if choice == "1":
+ return launch_vnc(image, workspace)
+ else:
+ return launch_x11(image, workspace, os_type)
+
+ return 0
+
+
+# CLI mode
+
+def run_cli(args):
+ import argparse
+
+ parser = argparse.ArgumentParser(description="eSim Docker Launcher")
+ parser.add_argument("--vnc", "-v", action="store_true", help="VNC mode (browser)")
+ parser.add_argument("--x11", "-x", action="store_true", help="X11 mode (native)")
+ parser.add_argument("--build", "-b", action="store_true", help="Build from Dockerfile")
+ parser.add_argument("--pull", "-p", action="store_true", help="Force pull image")
+ parser.add_argument("--shell", "-s", action="store_true", help="Open shell only")
+
+ opts = parser.parse_args(args)
+ os_type = get_os()
+
+ if not docker_ok():
+ handle_docker_missing(os_type)
+ return 1
+
+ workspace = get_workspace()
+
+ if opts.pull:
+ if not pull_image():
+ return 1
+
+ image = get_image(build_local=opts.build)
+ if not image:
+ err("No image available")
+ return 1
+
+ if opts.shell:
+ stop_container()
+ cmd = ["docker", "run", "--rm", "-it", "--name", CONTAINER_NAME,
+ "-v", f"{workspace}:/home/esim-user/eSim-Workspace:rw",
+ image, "/bin/bash"]
+ return subprocess.run(cmd).returncode
+
+ if opts.x11:
+ return launch_x11(image, workspace, os_type)
+
+ # Default: VNC for Windows/Mac, X11 for Linux
+ if os_type == "linux" and not opts.vnc:
+ return launch_x11(image, workspace, os_type)
+
+ return launch_vnc(image, workspace)
+
+
+# Entry point
+
+def main():
+ if len(sys.argv) == 1:
+ try:
+ return run_menu()
+ except KeyboardInterrupt:
+ print("\n")
+ info("Cancelled")
+ return 0
+ else:
+ show_banner()
+ try:
+ return run_cli(sys.argv[1:])
+ except KeyboardInterrupt:
+ print("\n")
+ info("Cancelled")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())