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# 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¬ebook=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¬ebook=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¬ebook=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¬ebook=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¬ebook=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¬ebook=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¬ebook=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']