Monorepo for Aesthetic.Computer aesthetic.computer
at main 977 lines 38 kB view raw
1""" 2Aesthetic Computer Python Library for Jupyter Notebooks 3 4Usage: 5 import aesthetic 6 7 # Toggle between production and localhost: 8 aesthetic.USE_PRODUCTION = True # Use aesthetic.computer (production) 9 aesthetic.USE_PRODUCTION = False # Use localhost:8888 (default) 10 11Examples: 12 # Use localhost (default) 13 %%ac 14 (ink red) (line 0 0 100 100) 15 16 # Switch to production 17 aesthetic.USE_PRODUCTION = True 18 19 # Now all pieces load from aesthetic.computer 20 %%ac 21 prompt 22 23Note: All URLs include ?notebook=true for custom boot animation 24""" 25 26import sys 27import importlib 28import urllib.parse 29import hashlib 30import subprocess 31import os 32 33# Ensure notebooks/ dir is on sys.path so this module can be imported 34# regardless of kernel working directory (VS Code runs from workspace root) 35_notebooks_dir = os.path.dirname(os.path.abspath(__file__)) 36if _notebooks_dir not in sys.path: 37 sys.path.insert(0, _notebooks_dir) 38 39# Check if the module is already loaded 40if "aesthetic" in sys.modules: 41 importlib.reload(sys.modules["aesthetic"]) 42 43from IPython.display import display, IFrame, HTML 44from IPython.core.magic import Magics, cell_magic, line_magic, magics_class 45 46DEFAULT_DENSITY = 2 47 48# Global configuration for production vs localhost 49# Set to True to use aesthetic.computer (production), False for localhost:8888 (default) 50USE_PRODUCTION = False 51 52def _get_base_url(): 53 """Get the base URL based on USE_PRODUCTION setting""" 54 return "https://aesthetic.computer" if USE_PRODUCTION else "https://localhost:8888" 55 56def _get_ipython_context(): 57 try: 58 from IPython import get_ipython 59 ip = get_ipython() 60 if ip is not None: 61 return ip.user_ns.copy() 62 except Exception: 63 pass 64 return {} 65 66def _safe_eval(expr, context=None): 67 """Safely evaluate math expressions with access to IPython variables""" 68 if context is None: 69 context = {} 70 71 if expr == "100%" or (isinstance(expr, str) and expr.endswith('%')): 72 return expr 73 74 try: 75 return int(expr) 76 except (ValueError, TypeError): 77 pass 78 79 try: 80 return float(expr) 81 except (ValueError, TypeError): 82 pass 83 84 import math 85 safe_dict = { 86 '__builtins__': {}, 87 'abs': abs, 'min': min, 'max': max, 'round': round, 88 'int': int, 'float': float, 'sum': sum, 'len': len, 89 'pow': pow, 90 'math': math, 'pi': math.pi, 'e': math.e, 91 'sin': math.sin, 'cos': math.cos, 'tan': math.tan, 92 'sqrt': math.sqrt, 'log': math.log, 'log10': math.log10, 93 'exp': math.exp, 'floor': math.floor, 'ceil': math.ceil, 94 **context, 95 } 96 97 try: 98 result = eval(expr, safe_dict) 99 if isinstance(result, float) and result.is_integer(): 100 return int(result) 101 return result 102 except Exception as e: 103 print(f"Warning: Could not evaluate '{expr}': {e}") 104 return expr 105 106def _normalize_density(value, context=None, default=DEFAULT_DENSITY): 107 if value is None: 108 return default 109 if isinstance(value, (int, float)): 110 return value 111 if isinstance(value, str): 112 evaluated = _safe_eval(value, context) 113 if isinstance(evaluated, (int, float)): 114 return evaluated 115 return default 116 117def _scale_dimension(value, density): 118 if isinstance(value, (int, float)) and isinstance(density, (int, float)): 119 scaled = value * density 120 if isinstance(scaled, float) and scaled.is_integer(): 121 return int(scaled) 122 return scaled 123 return value 124 125def _compute_iframe_dimensions(width, height, density): 126 return _scale_dimension(width, density), _scale_dimension(height, density) 127 128def show(piece, width="100%", height=54, density=None): 129 importlib.reload(importlib.import_module('aesthetic')) 130 base_url = _get_base_url() 131 url = f"{base_url}/{piece}?nolabel=true&nogap=true&notebook=true" 132 density_value = _normalize_density(density, _get_ipython_context()) 133 iframe_width, iframe_height = _compute_iframe_dimensions(width, height, density_value) 134 if density_value is not None: 135 url += f"&density={density_value}" 136 display(IFrame(src=url, width=iframe_width, height=iframe_height, frameborder="0")) 137 138def encode_kidlisp_with_node(code): 139 """ 140 Use Node.js to encode kidlisp code using the same function as kidlisp.mjs 141 Falls back to Python implementation if Node.js is not available 142 """ 143 try: 144 # Get the directory of this script 145 script_dir = os.path.dirname(os.path.abspath(__file__)) 146 encoder_script = os.path.join(script_dir, 'encode_kidlisp.mjs') 147 148 # Run the Node.js encoder 149 result = subprocess.run( 150 ['node', encoder_script, code], 151 capture_output=True, 152 text=True, 153 timeout=5 154 ) 155 156 if result.returncode == 0: 157 return result.stdout.strip() 158 else: 159 print(f"Node.js encoder failed: {result.stderr}") 160 raise subprocess.CalledProcessError(result.returncode, 'node') 161 162 except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e: 163 print(f"Falling back to Python encoder: {e}") 164 # Fallback to Python implementation with full URL 165 base_url = _get_base_url() 166 encoded = code.replace(' ', '_').replace('\n', '~') 167 return f"{base_url}/{encoded}?nolabel=true&nogap=true&notebook=true" 168 169def _highlight_kidlisp(code): 170 """Generate syntax-highlighted HTML for KidLisp code""" 171 # Simple KidLisp syntax highlighting 172 highlighted = code 173 174 # KidLisp keywords and functions (green) 175 keywords = [ 176 'ink', 'wipe', 'line', 'rect', 'circle', 'box', 'paste', 'write', 'text', 177 'sin', 'cos', 'tan', 'sqrt', 'abs', 'floor', 'ceil', 'round', 'max', 'min', 178 'random', 'noise', 'if', 'let', 'loop', 'for', 'while', 'define', 'defn', 179 'lambda', 'map', 'filter', 'reduce', 'quote', 'eval', 'quote', 'true', 'false' 180 ] 181 182 # Build regex pattern for keywords 183 import re 184 for kw in keywords: 185 pattern = r'\b' + kw + r'\b' 186 highlighted = re.sub(pattern, f'<span style="color: #4CAF50; font-weight: bold;">{kw}</span>', highlighted, flags=re.IGNORECASE) 187 188 # Numbers (blue) 189 highlighted = re.sub(r'\b(\d+\.?\d*)\b', r'<span style="color: #2196F3;">\1</span>', highlighted) 190 191 # Strings (orange) 192 highlighted = re.sub(r'"([^"]*)"', r'<span style="color: #FF9800;">"\1"</span>', highlighted) 193 194 # Comments (gray) 195 highlighted = re.sub(r';[^\n]*', lambda m: f'<span style="color: #999;">%s</span>' % m.group(0), highlighted) 196 197 # Parentheses (purple) 198 highlighted = highlighted.replace('(', '<span style="color: #9C27B0;">(</span>') 199 highlighted = highlighted.replace(')', '<span style="color: #9C27B0;">)</span>') 200 201 return highlighted 202 203def _display_kidlisp_source(code): 204 """Display KidLisp source code with syntax highlighting below the iframe""" 205 if not code or code.startswith('$'): 206 # External code reference - show a note 207 display(HTML(f''' 208 <div style="margin-top: 12px; padding: 8px 12px; background: #f5f5f5; border-left: 3px solid #999; font-family: monospace; font-size: 12px; color: #666;"> 209 <span style="color: #999;">📌 External code reference: <code>{code}</code></span> 210 </div> 211 ''')) 212 else: 213 # Inline code - show with syntax highlighting 214 highlighted_code = _highlight_kidlisp(code) 215 display(HTML(f''' 216 <pre style="margin-top: 12px; padding: 12px; background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.5; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"> 217 {highlighted_code} 218 </pre> 219 ''')) 220 221def kidlisp(code, width="100%", height=500, auto_scale=False, tv_mode=False, density=None, show_source=True): 222 """ 223 Run kidlisp code in Aesthetic Computer. 224 225 Loads a blank AC iframe and sends the full source via postMessage 226 (same protocol as the kidlisp.com editor), avoiding URL-encoding issues 227 with commas, newlines, and special characters. 228 229 Args: 230 code (str): The kidlisp code to execute 231 width: Virtual width in AC pixels (default: "100%") 232 height: Virtual height in AC pixels (default: 400) 233 auto_scale (bool): If True, iframe will scale to fit content (default: False) 234 tv_mode (bool): If True, disables touch and keyboard input for TV display (default: False) 235 density (number|str): Virtual pixel density (default: 2) 236 show_source (bool): If True, display syntax-highlighted source below iframe (default: True) 237 """ 238 importlib.reload(importlib.import_module('aesthetic')) 239 240 clean_code = code.strip() 241 base_url = _get_base_url() 242 243 # Load prompt piece as a blank canvas, then send code via postMessage 244 url = f"{base_url}/prompt?nolabel=true&nogap=true&notebook=true" 245 246 if tv_mode: 247 url += "&tv=true" 248 249 density_value = _normalize_density(density, _get_ipython_context()) 250 if density_value is not None: 251 url += f"&density={density_value}" 252 253 content_hash = hashlib.md5(f"{clean_code}{width}{height}{tv_mode}{density_value}".encode()).hexdigest()[:8] 254 iframe_id = f"ac-iframe-{content_hash}" 255 256 iframe_width, iframe_height = _compute_iframe_dimensions(width, height, density_value) 257 258 # Escape the code for embedding in JavaScript 259 import json 260 escaped_code = json.dumps(clean_code) 261 262 style = "background: transparent; margin: 0; padding: 0; border: none; display: block;" 263 if auto_scale: 264 style += " transform-origin: top left; max-width: 100%; height: auto; aspect-ratio: 1/1;" 265 266 # Embed iframe + JS that sends code via postMessage once the iframe is ready 267 iframe_html = f''' 268 <div style="margin: -8px -8px 0 -8px; padding: 0; overflow: hidden;"> 269 <iframe id="{iframe_id}" src="{url}" 270 width="{iframe_width}" 271 height="{iframe_height}" 272 frameborder="0" 273 style="{style}"> 274 </iframe> 275 </div> 276 <script> 277 (function() {{ 278 var iframe = document.getElementById("{iframe_id}"); 279 var code = {escaped_code}; 280 var origin = "{base_url}"; 281 var sent = false; 282 function trySend() {{ 283 if (sent) return; 284 if (iframe && iframe.contentWindow) {{ 285 iframe.contentWindow.postMessage({{ 286 type: "kidlisp-reload", 287 code: code 288 }}, origin); 289 sent = true; 290 }} 291 }} 292 // Listen for ready signal from the iframe 293 window.addEventListener("message", function handler(e) {{ 294 if (e.source === iframe.contentWindow && e.data && e.data.type === "ready") {{ 295 trySend(); 296 window.removeEventListener("message", handler); 297 }} 298 }}); 299 // Also try after a delay as fallback 300 iframe.addEventListener("load", function() {{ 301 setTimeout(trySend, 500); 302 setTimeout(trySend, 1500); 303 }}); 304 }})(); 305 </script> 306 ''' 307 308 display(HTML(iframe_html)) 309 310 # Display source code if requested 311 if show_source: 312 _display_kidlisp_source(clean_code) 313 314def kidlisp_display(code, width="100%", height=400, auto_scale=False, density=None): 315 """ 316 Run kidlisp code with minimal visual footprint - designed for display-only cells. 317 318 Args: 319 code (str): The kidlisp code to execute 320 width: Width of the iframe (default: "100%") 321 height: Height of the iframe (default: 400) 322 auto_scale (bool): If True, iframe will scale to fit content (default: False) 323 density (number|str): Virtual pixel density (default: 2) 324 """ 325 # This function is identical to kidlisp() but with a different name 326 # The "display-only" aspect comes from how you use it in your notebook 327 return kidlisp(code, width, height, auto_scale, False, density) 328 329def show_side_by_side(*pieces_and_options): 330 """ 331 Display multiple pieces side by side in a horizontal layout. 332 333 Args: 334 *pieces_and_options: Can be either: 335 - Just piece names as strings: show_side_by_side("clock:4", "clock:5") 336 - Tuples of (piece, width, height): show_side_by_side(("clock:4", 200, 100), ("clock:5", 150, 75)) 337 - Mix of both 338 """ 339 importlib.reload(importlib.import_module('aesthetic')) 340 341 base_url = _get_base_url() 342 iframes_html = [] 343 344 for item in pieces_and_options: 345 if isinstance(item, tuple): 346 piece, width, height = item 347 else: 348 piece = item 349 width = 200 350 height = 100 351 352 url = f"{base_url}/{piece}?nolabel=true&nogap=true&notebook=true" 353 iframe_html = f'<iframe src="{url}" width="{width}" height="{height}" frameborder="0" style="margin: 0; padding: 0; border: none;"></iframe>' 354 iframes_html.append(iframe_html) 355 356 # Create a container div with flexbox layout and no gaps 357 container_html = f''' 358 <div style="display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0; margin: 0; padding: 0;"> 359 {"".join(iframes_html)} 360 </div> 361 ''' 362 363 display(HTML(container_html)) 364 365# Short aliases for kidlisp function 366def k(*args, **kwargs): 367 """Ultra-short alias for kidlisp function""" 368 return λ(*args, **kwargs) 369 370def _(*args, **kwargs): 371 """Single underscore alias for kidlisp function""" 372 return λ(*args, **kwargs) 373 374# Even shorter - single character functions 375def l(*args, **kwargs): 376 """Single letter 'l' for lisp""" 377 return λ(*args, **kwargs) 378 379# Lambda symbol alias - perfect for functional programming! 380def λ(*args, **kwargs): 381 """ 382 Lambda symbol alias for kidlisp function - λ() 383 384 Usage: 385 λ("kidlisp code") # Default: 100% width, 32px height 386 λ("kidlisp code", 300) # Single number = height (width stays 100%) 387 λ("kidlisp code", 800, 600) # Two numbers = width, height 388 λ("kidlisp code", (800, 600)) # Resolution tuple 389 λ(("kidlisp code", 500, 300)) # Tuple format: (code, width, height) 390 λ("kidlisp code", resolution=(800, 600)) # Named resolution tuple 391 """ 392 # Default values 393 code = None 394 width = "100%" 395 height = 30 396 auto_scale = kwargs.get('auto_scale', False) 397 density = kwargs.get('density', None) 398 resolution = kwargs.get('resolution', None) 399 400 # Parse positional arguments 401 if len(args) >= 1: 402 code = args[0] 403 if len(args) >= 2: 404 # Check if second argument is a resolution tuple 405 if isinstance(args[1], tuple) and len(args[1]) == 2: 406 resolution = args[1] 407 elif isinstance(args[1], (int, float)): 408 # Single number = height only (width stays 100%) 409 height = args[1] 410 else: 411 width = args[1] 412 if len(args) >= 3: 413 # Third argument could be height or resolution tuple 414 if isinstance(args[2], tuple) and len(args[2]) == 2: 415 resolution = args[2] 416 else: 417 # If we have 3 args and second was a number, then this is width, height 418 if isinstance(args[1], (int, float)): 419 width = args[1] 420 height = args[2] 421 else: 422 height = args[2] 423 if len(args) >= 4: 424 auto_scale = args[3] 425 426 # Handle tuple input for resolution in first parameter 427 if isinstance(code, tuple): 428 if len(code) == 3: 429 # Format: ("code", width, height) 430 code, width, height = code 431 elif len(code) == 2: 432 # Format: ("code", height) - width stays 100% 433 code, height = code 434 else: 435 raise ValueError("Tuple must have 2 or 3 elements: (code, height[, width])") 436 437 # Handle resolution tuple parameter (overrides other width/height settings) 438 if resolution is not None: 439 if isinstance(resolution, tuple) and len(resolution) == 2: 440 width, height = resolution 441 else: 442 raise ValueError("Resolution must be a tuple of (width, height)") 443 444 return kidlisp(code, width, height, auto_scale, False, density) 445 446def _is_numeric_like(s): 447 """Check if a string looks numeric without evaluation (avoids warnings)""" 448 if not isinstance(s, str): 449 return False 450 s = s.strip() 451 if not s: 452 return False 453 try: 454 float(s) 455 return True 456 except (ValueError, TypeError): 457 return False 458 459# IPython Magic Commands for Native Kidlisp Syntax 460@magics_class 461class AestheticComputerMagics(Magics): 462 """IPython magic commands for native kidlisp syntax in Jupyter notebooks""" 463 464 @cell_magic 465 def ac(self, line, cell): 466 """ 467 Cell magic to execute kidlisp code or piece invocations from PRODUCTION (aesthetic.computer). 468 Uppercase %%AC loads from development localhost instead. 469 470 Usage (Production): 471 %%ac 472 (ink red) 473 (line 0 0 100 100) 474 475 Or for piece invocations: 476 %%ac 477 clock cdefg 478 479 With size options: 480 %%ac 400 # Height only (width = 100%) 481 %%ac 800 600 # Width and height 482 %%ac 320 240*2 # Math expressions supported 483 (your kidlisp code here) 484 485 Development (use %%AC uppercase for localhost:8888): 486 %%AC 487 prompt 488 489 Note: Width/height are virtual AC pixels. Iframe size is scaled by density. 490 491 Parameters support math expressions and variables from the current namespace. 492 Examples: 493 %%ac 400*2 # Height = 800, width = 100% (production) 494 %%AC 400*2 # Height = 800, width = 100% (development) 495 %%ac my_width my_height 496 %%ac int(800/2) int(600*1.5) # Width = 400, height = 900 497 %%ac 320 240 density=2 # Virtual size with explicit density 498 """ 499 # Parse and evaluate arguments with math support 500 args = line.strip().split() if line.strip() else [] 501 width = "100%" 502 height = 30 503 tv_mode = False 504 density = None 505 506 # Look for tv_mode parameter 507 filtered_args = [] 508 for arg in args: 509 if arg.startswith('tv_mode='): 510 tv_mode = arg.split('=')[1].lower() in ('true', '1', 'yes') 511 elif arg.startswith('density=') or arg.startswith('d='): 512 density = arg.split('=', 1)[1] 513 else: 514 filtered_args.append(arg) 515 516 args = filtered_args 517 518 context = _get_ipython_context() 519 520 if len(args) >= 1: 521 # If only one argument, treat it as height and keep width as "100%" 522 if len(args) == 1: 523 height_expr = args[0] 524 height = _safe_eval(height_expr, context) 525 # width stays as "100%" (already set above) 526 else: 527 # If two or more arguments, first is width, second is height 528 width_expr = args[0] 529 width = _safe_eval(width_expr, context) 530 if len(args) >= 2: 531 height_expr = args[1] 532 height = _safe_eval(height_expr, context) 533 534 density_value = _normalize_density(density, context) 535 iframe_width, iframe_height = _compute_iframe_dimensions(width, height, density_value) 536 537 # Check if the cell content is a piece invocation or kidlisp code 538 cell_content = cell.strip() 539 540 # Detect if this is a piece invocation vs kidlisp code 541 # Piece invocations: 542 # - Don't start with ( or ; 543 # - Don't contain newlines (single line piece calls) 544 # - First word is likely a piece name (not a kidlisp function) 545 is_piece_invocation = ( 546 cell_content and 547 not cell_content.startswith('(') and 548 not cell_content.startswith(';') and 549 '\n' not in cell_content and 550 not cell_content.startswith('~') and # kidlisp can start with ~ 551 len(cell_content.split()) >= 1 552 ) 553 554 # Force PRODUCTION mode for lowercase %ac 555 global USE_PRODUCTION 556 old_production = USE_PRODUCTION 557 try: 558 USE_PRODUCTION = True 559 560 if is_piece_invocation: 561 # Handle as piece invocation - construct URL directly 562 # Replace spaces with ~ for piece parameters 563 piece_url = cell_content.replace(' ', '~') 564 base_url = _get_base_url() 565 url = f"{base_url}/{piece_url}?nolabel=true&nogap=true&notebook=true" 566 567 # Add TV mode parameter if requested 568 if tv_mode: 569 url += "&tv=true" 570 571 if density_value is not None: 572 url += f"&density={density_value}" 573 574 # Create iframe directly 575 content_hash = hashlib.md5(f"{cell_content}{width}{height}{tv_mode}{density_value}".encode()).hexdigest()[:8] 576 iframe_id = f"ac-iframe-{content_hash}" 577 578 iframe_html = f''' 579 <div style="margin: -8px -8px 0 -8px; padding: 0; overflow: hidden;"> 580 <iframe id="{iframe_id}" src="{url}" 581 width="{iframe_width}" 582 height="{iframe_height}" 583 frameborder="0" 584 style="background: transparent; margin: 0; padding: 0; border: none; display: block;"> 585 </iframe> 586 </div> 587 ''' 588 589 display(HTML(iframe_html)) 590 # Display source code 591 _display_kidlisp_source(cell_content) 592 else: 593 # Handle as kidlisp code 594 return kidlisp(cell_content, width, height, False, tv_mode, density_value) 595 finally: 596 USE_PRODUCTION = old_production 597 598 @cell_magic 599 def AC(self, line, cell): 600 """ 601 Cell magic to execute kidlisp code or piece invocations from DEVELOPMENT (localhost:8888). 602 Lowercase %%ac loads from production aesthetic.computer instead. 603 604 Usage (Development): 605 %%AC 606 prompt 607 608 Or for piece invocations: 609 %%AC 610 clock cdefg 611 612 With size options: 613 %%AC 400 # Height only (width = 100%) 614 %%AC 800 600 # Width and height 615 616 Production (use %%ac lowercase for aesthetic.computer): 617 %%ac prompt 618 619 Note: Uppercase AC = localhost/dev, lowercase ac = production domain 620 """ 621 # Parse and evaluate arguments with math support 622 args = line.strip().split() if line.strip() else [] 623 width = "100%" 624 height = 30 625 tv_mode = False 626 density = None 627 628 # Look for tv_mode parameter 629 filtered_args = [] 630 for arg in args: 631 if arg.startswith('tv_mode='): 632 tv_mode = arg.split('=')[1].lower() in ('true', '1', 'yes') 633 elif arg.startswith('density=') or arg.startswith('d='): 634 density = arg.split('=', 1)[1] 635 else: 636 filtered_args.append(arg) 637 638 args = filtered_args 639 640 context = _get_ipython_context() 641 642 if len(args) >= 1: 643 # If only one argument, treat it as height and keep width as "100%" 644 if len(args) == 1: 645 height_expr = args[0] 646 height = _safe_eval(height_expr, context) 647 # width stays as "100%" (already set above) 648 else: 649 # If two or more arguments, first is width, second is height 650 width_expr = args[0] 651 width = _safe_eval(width_expr, context) 652 if len(args) >= 2: 653 height_expr = args[1] 654 height = _safe_eval(height_expr, context) 655 656 density_value = _normalize_density(density, context) 657 iframe_width, iframe_height = _compute_iframe_dimensions(width, height, density_value) 658 659 # Check if the cell content is a piece invocation or kidlisp code 660 cell_content = cell.strip() 661 662 # Detect if this is a piece invocation vs kidlisp code 663 is_piece_invocation = ( 664 cell_content and 665 not cell_content.startswith('(') and 666 not cell_content.startswith(';') and 667 '\n' not in cell_content and 668 not cell_content.startswith('~') and 669 len(cell_content.split()) >= 1 670 ) 671 672 # Force DEVELOPMENT mode (localhost) for uppercase AC 673 global USE_PRODUCTION 674 old_production = USE_PRODUCTION 675 try: 676 USE_PRODUCTION = False 677 678 if is_piece_invocation: 679 # Handle as piece invocation - construct URL directly 680 piece_url = cell_content.replace(' ', '~') 681 base_url = _get_base_url() # This will now use localhost 682 url = f"{base_url}/{piece_url}?nolabel=true&nogap=true&notebook=true" 683 684 # Add TV mode parameter if requested 685 if tv_mode: 686 url += "&tv=true" 687 688 if density_value is not None: 689 url += f"&density={density_value}" 690 691 # Create iframe directly 692 content_hash = hashlib.md5(f"{cell_content}{width}{height}{tv_mode}{density_value}".encode()).hexdigest()[:8] 693 iframe_id = f"ac-iframe-{content_hash}" 694 695 iframe_html = f''' 696 <div style="margin: -8px -8px 0 -8px; padding: 0; overflow: hidden;"> 697 <iframe id="{iframe_id}" src="{url}" 698 width="{iframe_width}" 699 height="{iframe_height}" 700 frameborder="0" 701 style="background: transparent; margin: 0; padding: 0; border: none; display: block;"> 702 </iframe> 703 </div> 704 ''' 705 706 display(HTML(iframe_html)) 707 # Display source code 708 _display_kidlisp_source(cell_content) 709 else: 710 # Handle as kidlisp code 711 return kidlisp(cell_content, width, height, False, tv_mode, density_value) 712 finally: 713 USE_PRODUCTION = old_production 714 715 @line_magic 716 def ac_line(self, line): 717 """ 718 Line magic to execute a single line of kidlisp code or piece invocation. 719 720 Usage: 721 %ac (ink red) (circle 25 25 10) 722 %ac clock cdefg 723 %ac 100 clock cdefg # Height = 100, width = 100% 724 %ac 400 200 clock cdefg # Width = 400, height = 200 725 %ac 320 240 density=2 clock # Virtual size with explicit density 726 727 Note: Width/height are virtual AC pixels. Iframe size is scaled by density. 728 729 Note: For piece invocations with special characters like {}, use cell magic %%ac instead 730 to avoid Python syntax parsing issues. 731 """ 732 line_content = line.strip() 733 734 # Parse potential size parameters from the beginning of the line 735 parts = line_content.split() 736 width = "100%" 737 height = 30 738 density = None 739 content_start_index = 0 740 741 context = _get_ipython_context() 742 743 filtered_parts = [] 744 for part in parts: 745 if part.startswith('density=') or part.startswith('d='): 746 density = part.split('=', 1)[1] 747 else: 748 filtered_parts.append(part) 749 750 parts = filtered_parts 751 752 # Check if first part(s) are numeric parameters (use _is_numeric_like to avoid warning) 753 if len(parts) >= 1 and _is_numeric_like(parts[0]): 754 # First part looks numeric - parse it 755 first_num = _safe_eval(parts[0], context) 756 if isinstance(first_num, (int, float)): 757 height = first_num 758 content_start_index = 1 759 760 # Check if second part is also numeric (width) 761 if len(parts) >= 3 and _is_numeric_like(parts[1]): 762 second_num = _safe_eval(parts[1], context) 763 if isinstance(second_num, (int, float)): 764 width = first_num 765 height = second_num 766 content_start_index = 2 767 768 # Extract the actual content (after size parameters) 769 if content_start_index > 0: 770 actual_content = ' '.join(parts[content_start_index:]) 771 else: 772 actual_content = line_content 773 774 # Detect if this is a piece invocation vs kidlisp code 775 is_piece_invocation = ( 776 actual_content and 777 not actual_content.startswith('(') and 778 not actual_content.startswith(';') and 779 not actual_content.startswith('~') and 780 len(actual_content.split()) >= 1 781 ) 782 783 density_value = _normalize_density(density, context) 784 iframe_width, iframe_height = _compute_iframe_dimensions(width, height, density_value) 785 786 if is_piece_invocation: 787 # Handle as piece invocation - construct URL directly 788 # Replace spaces with ~ for piece parameters 789 piece_url = actual_content.replace(' ', '~') 790 base_url = _get_base_url() 791 url = f"{base_url}/{piece_url}?nolabel=true&nogap=true&notebook=true" 792 793 if density_value is not None: 794 url += f"&density={density_value}" 795 796 # Create iframe directly 797 content_hash = hashlib.md5(f"{actual_content}{width}{height}{density_value}".encode()).hexdigest()[:8] 798 iframe_id = f"ac-iframe-{content_hash}" 799 800 iframe_html = f''' 801 <div style="margin: -8px -8px 0 -8px; padding: 0; overflow: hidden;"> 802 <iframe id="{iframe_id}" src="{url}" 803 width="{iframe_width}" 804 height="{iframe_height}" 805 frameborder="0" 806 style="background: transparent; margin: 0; padding: 0; border: none; display: block;"> 807 </iframe> 808 </div> 809 ''' 810 811 display(HTML(iframe_html)) 812 else: 813 # Handle as kidlisp code 814 return kidlisp(actual_content, width, height, False, False, density_value) 815 816 @line_magic 817 def AC(self, line): 818 """ 819 Line magic to execute a single line of kidlisp code or piece invocation from DEVELOPMENT. 820 821 Usage: 822 %AC (ink red) (circle 25 25 10) 823 %AC clock cdefg 824 %AC 100 clock cdefg # Height = 100, width = 100% 825 %AC 400 200 clock cdefg # Width = 400, height = 200 826 827 Note: Uppercase AC = localhost/dev, lowercase ac = production domain 828 Note: Width/height are virtual AC pixels. Iframe size is scaled by density. 829 """ 830 line_content = line.strip() 831 832 # Parse potential size parameters from the beginning of the line 833 parts = line_content.split() 834 width = "100%" 835 height = 30 836 density = None 837 content_start_index = 0 838 839 context = _get_ipython_context() 840 841 filtered_parts = [] 842 for part in parts: 843 if part.startswith('density=') or part.startswith('d='): 844 density = part.split('=', 1)[1] 845 else: 846 filtered_parts.append(part) 847 848 parts = filtered_parts 849 850 # Check if first part(s) are numeric parameters (use _is_numeric_like to avoid warning) 851 if len(parts) >= 1 and _is_numeric_like(parts[0]): 852 # First part looks numeric - parse it 853 first_num = _safe_eval(parts[0], context) 854 if isinstance(first_num, (int, float)): 855 height = first_num 856 content_start_index = 1 857 858 # Check if second part is also numeric (width) 859 if len(parts) >= 3 and _is_numeric_like(parts[1]): 860 second_num = _safe_eval(parts[1], context) 861 if isinstance(second_num, (int, float)): 862 width = first_num 863 height = second_num 864 content_start_index = 2 865 866 # Extract the actual content (after size parameters) 867 if content_start_index > 0: 868 actual_content = ' '.join(parts[content_start_index:]) 869 else: 870 actual_content = line_content 871 872 # Detect if this is a piece invocation vs kidlisp code 873 is_piece_invocation = ( 874 actual_content and 875 not actual_content.startswith('(') and 876 not actual_content.startswith(';') and 877 not actual_content.startswith('~') and 878 len(actual_content.split()) >= 1 879 ) 880 881 density_value = _normalize_density(density, context) 882 iframe_width, iframe_height = _compute_iframe_dimensions(width, height, density_value) 883 884 # Force DEVELOPMENT mode (localhost) for uppercase AC 885 global USE_PRODUCTION 886 old_production = USE_PRODUCTION 887 try: 888 USE_PRODUCTION = False 889 890 if is_piece_invocation: 891 # Handle as piece invocation - construct URL directly 892 # Replace spaces with ~ for piece parameters 893 piece_url = actual_content.replace(' ', '~') 894 base_url = _get_base_url() # This will now use localhost 895 url = f"{base_url}/{piece_url}?nolabel=true&nogap=true&notebook=true" 896 897 if density_value is not None: 898 url += f"&density={density_value}" 899 900 # Create iframe directly 901 content_hash = hashlib.md5(f"{actual_content}{width}{height}{density_value}".encode()).hexdigest()[:8] 902 iframe_id = f"ac-iframe-{content_hash}" 903 904 iframe_html = f''' 905 <div style="margin: -8px -8px 0 -8px; padding: 0; overflow: hidden;"> 906 <iframe id="{iframe_id}" src="{url}" 907 width="{iframe_width}" 908 height="{iframe_height}" 909 frameborder="0" 910 style="background: transparent; margin: 0; padding: 0; border: none; display: block;"> 911 </iframe> 912 </div> 913 ''' 914 915 display(HTML(iframe_html)) 916 else: 917 # Handle as kidlisp code 918 return kidlisp(actual_content, width, height, False, False, density_value) 919 finally: 920 USE_PRODUCTION = old_production 921 922# Automatic IPython/Jupyter setup - makes λ globally available 923def _setup_ipython(): 924 """Automatically setup λ and magic commands in IPython/Jupyter environment""" 925 try: 926 from IPython import get_ipython 927 ip = get_ipython() 928 if ip is not None: 929 # Add λ to the user namespace 930 ip.user_ns['λ'] = λ 931 932 # Also add to the global namespace for pylance 933 ip.user_global_ns['λ'] = λ 934 935 # Register as a built-in so it's available everywhere 936 import builtins 937 builtins.λ = λ 938 939 # Register the magic commands class 940 magic_instance = AestheticComputerMagics(ip) 941 ip.register_magics(AestheticComputerMagics) 942 943 # Create and register line magics with production/dev modes forced 944 def ac_line_prod(line): 945 """Line magic for production (%ac)""" 946 global USE_PRODUCTION 947 old_production = USE_PRODUCTION 948 try: 949 USE_PRODUCTION = True 950 return magic_instance.ac_line(line) 951 finally: 952 USE_PRODUCTION = old_production 953 954 def AC_line_dev(line): 955 """Line magic for development (%AC)""" 956 global USE_PRODUCTION 957 old_production = USE_PRODUCTION 958 try: 959 USE_PRODUCTION = False 960 return magic_instance.ac_line(line) 961 finally: 962 USE_PRODUCTION = old_production 963 964 # Register the line magics with correct names 965 ip.register_magic_function(ac_line_prod, 'line', 'ac') 966 ip.register_magic_function(AC_line_dev, 'line', 'AC') 967 968 return True 969 except ImportError: 970 pass 971 return False 972 973# Auto-setup when module is imported 974_setup_ipython() 975 976# Export all functions and configuration for proper IDE support 977__all__ = ['show', 'show_side_by_side', 'kidlisp', 'kidlisp_display', 'k', '_', 'l', 'λ', 'AestheticComputerMagics', 'USE_PRODUCTION']