import asyncio
import re
import ipykernel.connect
from IPython.core.getipython import get_ipython
from ipylab import JupyterFrontEnd
from .comm import ggb_comm
from .construction import ggb_construction
from .parser import ggb_parser
[docs]
class GeoGebra:
"""Main interface for controlling GeoGebra applets from Python.
This class implements a singleton pattern to ensure only one GeoGebra
instance per kernel session. It provides async methods for sending
commands and calling GeoGebra API functions.
The communication uses a dual-channel architecture:
- IPython Comm: Primary control channel
- Unix socket/TCP WebSocket: Out-of-band response delivery during cell execution
Attributes:
construction (ggb_construction): File loader/saver for .ggb files
parser (ggb_parser): Dependency graph parser
comm (ggb_comm): Communication layer (initialized after init())
kernel_id (str): Current Jupyter kernel ID
app (JupyterFrontEnd): ipylab frontend interface
Example:
>>> ggb = GeoGebra()
>>> await ggb.init()
>>> await ggb.command("A=(0,0)")
>>> result = await ggb.function("getValue", ["A"])
"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.initialized = False
self.construction = ggb_construction()
self.parser = ggb_parser()
[docs]
async def init(self):
"""Initialize the GeoGebra widget and communication channels.
This method:
1. Starts the out-of-band socket server (Unix socket on POSIX, TCP WebSocket on Windows)
2. Registers the IPython Comm target ('ggblab-comm')
3. Opens the GeoGebra widget panel via ipylab with communication settings
The widget is launched programmatically to pass kernel-specific settings
(Comm target, socket path) before initialization, avoiding the limitations
of fixed arguments from Launcher/Command Palette.
Returns:
GeoGebra: Self reference for method chaining.
Example:
>>> ggb = await GeoGebra().init()
>>> # GeoGebra panel opens in split-right position
"""
if not self.initialized:
self.comm = ggb_comm()
self.comm.start()
while self.comm.socketPath is None:
await asyncio.sleep(.01)
self.comm.register_target()
_connection_file = ipykernel.connect.get_connection_file()
self.kernel_id = re.search(r'kernel-(.*)\.json', _connection_file).group(1)
self.app = JupyterFrontEnd()
self.app.commands.execute('ggblab:create', {
'kernelId': self.kernel_id,
'commTarget': 'ggblab-comm',
'insertMode': 'split-right',
'socketPath': self.comm.socketPath,
# 'wsPort': self.comm.wsPort,
})
self._initialized = True
return self
[docs]
async def function(self, f, args=None):
"""Call a GeoGebra API function.
Args:
f (str): GeoGebra API function name (e.g., "getValue", "getXML").
args (list, optional): Function arguments. Defaults to None.
Returns:
Any: Function return value from GeoGebra.
Example:
>>> value = await ggb.function("getValue", ["A"])
>>> xml = await ggb.function("getXML", ["A"])
>>> all_objs = await ggb.function("getAllObjectNames")
"""
r = await self.comm.send_recv({
"type": "function",
"payload": {
"name": f,
"args": args
}
})
return r['value']
[docs]
async def command(self, c):
"""Execute a GeoGebra command.
Args:
c (str): GeoGebra command string (e.g., "A=(0,0)", "Circle(A, 2)").
Returns:
dict: Response from GeoGebra (typically includes object label).
Example:
>>> await ggb.command("A=(0,0)")
>>> await ggb.command("B=(3,4)")
>>> await ggb.command("Circle(A, Distance(A, B))")
"""
return await self.comm.send_recv({
"type": "command",
"payload": c
})