Monorepo for Aesthetic.Computer
aesthetic.computer
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¬ebook=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¬ebook=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¬ebook=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¬ebook=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¬ebook=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¬ebook=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¬ebook=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¬ebook=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']