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