about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xbash/acmetodo145
-rwxr-xr-xbash/acmetodo-add25
-rwxr-xr-xbash/acmetodo-all17
-rwxr-xr-xbash/acmetodo-done19
-rw-r--r--bash/acmetodo-filter22
-rwxr-xr-xbash/acmetodo-inprogress19
-rwxr-xr-xbash/acmetodo-todo22
-rwxr-xr-xbash/acmetodo-toggle56
-rw-r--r--elm/cost-of-meeting/src/Main.elm1
-rw-r--r--html/playground/index.html435
-rw-r--r--ts/thinking-about-unions/left-pad.ts27
11 files changed, 559 insertions, 229 deletions
diff --git a/bash/acmetodo b/bash/acmetodo
new file mode 100755
index 0000000..0c0b72f
--- /dev/null
+++ b/bash/acmetodo
@@ -0,0 +1,145 @@
+#!/bin/sh
+# acmetodo: Main script to open and manage the todo list in Acme.
+
+PLAN9=${PLAN9:-"/Users/eli/plan9"} # Ensure this is your correct PLAN9 path
+TODO_FILE="$PLAN9/lib/todo"
+ACME_BIN="$PLAN9/bin/acme"
+STARTPLUMB_BIN="$PLAN9/bin/startplumb"
+ACME_FS_ROOT="/acme" # Standard Plan 9 /acme FUSE mount point
+
+# --- DEBUGGING START ---
+DEBUG_LOG="/tmp/acmetodo_debug.log"
+rm -f "$DEBUG_LOG" # Clear previous log on each run
+
+log_debug() {
+    echo "$(date '+%Y-%m-%d %H:%M:%S') [DEBUG] $@" >> "$DEBUG_LOG"
+    echo "$(date '+%H:%M:%S') [DEBUG] $@" >/dev/stderr
+}
+
+log_error() {
+    echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $@" >> "$DEBUG_LOG"
+    echo "$(date '+%H:%M:%S') [ERROR] $@" >/dev/stderr
+}
+
+log_debug "Script started."
+log_debug "PLAN9 is $PLAN9"
+log_debug "TODO_FILE is $TODO_FILE"
+log_debug "ACME_BIN is $ACME_BIN"
+log_debug "STARTPLUMB_BIN is $STARTPLUMB_BIN"
+log_debug "Current \$winid (from Acme environment) is '$winid'"
+# --- DEBUGGING END ---
+
+# --- PLUMBER CHECK AND LAUNCH ---
+log_debug "Checking if plumber is running..."
+if ! ps aux | grep -q "[p]lumber"; then
+    log_debug "Plumber not found. Attempting to start plumber..."
+    if [ -x "$STARTPLUMB_BIN" ]; then
+        "$STARTPLUMB_BIN" & # Launch plumber in the background
+        log_debug "Plumber launch command issued. Waiting for plumber to initialize..."
+        sleep 1 # Give plumber a moment to fully start
+    else
+        log_error "startplumb executable not found at '$STARTPLUMB_BIN'. Cannot start plumber."
+        exit 1
+    fi
+else
+    log_debug "Plumber is already running."
+fi
+# --- END PLUMBER CHECK ---
+
+
+# Ensure the lib directory exists
+mkdir -p "$(dirname "$TODO_FILE")"
+# Ensure the todo file exists
+touch "$TODO_FILE"
+
+# Function to safely get window ID by name, suppressing stderr from acme -w
+get_existing_win_id() {
+    # Pipe stderr of acme -w to /dev/null to suppress the "usage" message
+    "$ACME_BIN" -w 2>/dev/null | grep "^$TODO_FILE " | cut -d' ' -f1 | head -n 1
+}
+
+
+# Function to update a window's content, name, and tag
+update_todo_window() {
+    local win_id="$1"
+    log_debug "Function update_todo_window called for win_id: $win_id"
+
+    # 1. Set the window's name
+    log_debug "Attempting to set window $win_id name to '$TODO_FILE'"
+    echo "name $TODO_FILE" | "$ACME_BIN" -a "$win_id"
+    if [ $? -ne 0 ]; then
+        log_error "Failed to send 'name' command to window $win_id."
+    fi
+    sleep 0.05 # Small delay to ensure command is processed
+
+    # 2. Set the window's tag
+    log_debug "Attempting to set window $win_id tag."
+    echo 'tag Get Put | Add | Todo InProgress Done All | Quit' | "$ACME_BIN" -a "$win_id"
+    if [ $? -ne 0 ]; then
+        log_error "Failed to send 'tag' command to window $win_id."
+    fi
+    sleep 0.05 # Small delay
+
+    # 3. Load content directly into the window's body
+    log_debug "Attempting to write content to window $win_id body."
+    if [ -f "$TODO_FILE" ]; then
+        log_debug "Reading content from '$TODO_FILE' and piping to $win_id body."
+        cat "$TODO_FILE" | "$ACME_BIN" -a "$win_id" body
+        if [ $? -ne 0 ]; then
+            log_error "Failed to write content to window $win_id body from $TODO_FILE."
+        fi
+    else
+        log_debug "TODO_FILE '$TODO_FILE' does not exist or is not readable. Writing initial content."
+        echo "## Acme To-Do List" | "$ACME_BIN" -a "$win_id" body
+        if [ $? -ne 0 ]; then
+            log_error "Failed to write initial content to window $win_id body."
+        fi
+    fi
+    echo 'clean' | "$ACME_BIN" -a "$win_id" # Mark window as clean after setting content
+    log_debug "Finished updating window $win_id."
+}
+
+
+# --- MAIN LOGIC ---
+if [ -n "$winid" ]; then
+    log_debug "Running in Case 1: Script called from an existing Acme window."
+    update_todo_window "$winid"
+else
+    # Always try to find existing window first
+    EXISTING_TODO_WINID=$(get_existing_win_id)
+    log_debug "EXISTING_TODO_WINID (found by checking existing Acme windows) is '$EXISTING_TODO_WINID'"
+
+    if [ -n "$EXISTING_TODO_WINID" ]; then
+        log_debug "Running in Case 2: Script called from terminal, existing todo window found."
+        update_todo_window "$EXISTING_TODO_WINID"
+    else
+        log_debug "Running in Case 3: Script called from terminal, new todo window needed."
+
+        # Capture the highest window ID *before* opening a new one
+        PRE_NEW_WIN_ID=$(ls -d "$ACME_FS_ROOT"/[0-9]* 2>/dev/null | sort -V | tail -n 1 | xargs basename)
+        log_debug "Highest existing Acme window ID before new creation: '$PRE_NEW_WIN_ID'"
+
+        # 1. Open a new, empty Acme window. Don't try to capture its ID directly, as it's unreliable.
+        log_debug "Attempting to create a new Acme window with '$ACME_BIN -l /dev/null'."
+        "$ACME_BIN" -l /dev/null 2>/dev/null & # Run in background and suppress stderr
+        
+        # Give Acme a moment to open the window
+        sleep 0.5
+
+        # 2. Find the ID of the newly created window (should be the highest now)
+        NEW_WIN_ID=$(ls -d "$ACME_FS_ROOT"/[0-9]* 2>/dev/null | sort -V | tail -n 1 | xargs basename)
+        log_debug "Highest existing Acme window ID after new creation: '$NEW_WIN_ID'"
+
+        if [ -z "$NEW_WIN_ID" ] || [ "$NEW_WIN_ID" = "$PRE_NEW_WIN_ID" ]; then
+            log_error "Failed to create a new Acme window or get its ID correctly."
+            log_error "Possible causes: Acme not running, or its communication with plumber failed, or the FUSE mount is inaccessible."
+            exit 1
+        fi
+
+        # 3. Call the update function for the new window
+        log_debug "Calling update_todo_window for the newly created ID: $NEW_WIN_ID"
+        update_todo_window "$NEW_WIN_ID"
+    fi
+fi
+
+log_debug "Script finished."
\ No newline at end of file
diff --git a/bash/acmetodo-add b/bash/acmetodo-add
new file mode 100755
index 0000000..b40663d
--- /dev/null
+++ b/bash/acmetodo-add
@@ -0,0 +1,25 @@
+#!/bin/sh
+# acmetodo-add: Adds a new todo item.
+
+PLAN9=${PLAN9:-"/usr/local/plan9"}
+TODO_FILE="$PLAN9/lib/todo"
+ACME_BIN="$PLAN9/bin/acme"
+
+# Prompt for subject in Acme's current window (where the command was issued)
+# Output to stderr so it doesn't interfere with potential stdout pipe.
+echo -n "Subject: " >/dev/stderr
+read subject
+
+if [ -n "$subject" ]; then
+    echo "[ ] $subject" >> "$TODO_FILE"
+    # Find the winid of the main acmetodo window to refresh it.
+    # This assumes the main acmetodo window's name is the TODO_FILE path.
+    MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1)
+    if [ -n "$MAIN_TODO_WINID" ]; then
+        echo 'Get' | $ACME_BIN -a "$MAIN_TODO_WINID"
+    else
+        echo "Warning: Main acmetodo window not found to refresh." >/dev/stderr
+    fi
+else
+    echo "No subject provided. Item not added." >/dev/stderr
+fi
\ No newline at end of file
diff --git a/bash/acmetodo-all b/bash/acmetodo-all
new file mode 100755
index 0000000..c00bb9b
--- /dev/null
+++ b/bash/acmetodo-all
@@ -0,0 +1,17 @@
+#!/bin/sh
+# acmetodo-all: Shows all items in the todo list.
+
+PLAN9=${PLAN9:-"/usr/local/plan9"}
+TODO_FILE="$PLAN9/lib/todo"
+ACME_BIN="$PLAN9/bin/acme"
+
+MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1)
+
+if [ -z "$MAIN_TODO_WINID" ]; then
+    echo "Error: Main acmetodo window not found." >/dev/stderr
+    exit 1
+fi
+
+# Simply get (reload) the content of the todo file
+echo 'Get' | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID"
\ No newline at end of file
diff --git a/bash/acmetodo-done b/bash/acmetodo-done
new file mode 100755
index 0000000..4829331
--- /dev/null
+++ b/bash/acmetodo-done
@@ -0,0 +1,19 @@
+#!/bin/sh
+# acmetodo-done: Filters the todo list to show only 'done' items.
+
+PLAN9=${PLAN9:-"/usr/local/plan9"}
+TODO_FILE="$PLAN9/lib/todo"
+ACME_BIN="$PLAN9/bin/acme"
+
+MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1)
+
+if [ -z "$MAIN_TODO_WINID" ]; then
+    echo "Error: Main acmetodo window not found." >/dev/stderr
+    exit 1
+fi
+
+filtered_content=$(grep '^[x] ' "$TODO_FILE")
+
+echo 'data' | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo "$filtered_content" | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID"
\ No newline at end of file
diff --git a/bash/acmetodo-filter b/bash/acmetodo-filter
new file mode 100644
index 0000000..6149207
--- /dev/null
+++ b/bash/acmetodo-filter
@@ -0,0 +1,22 @@
+#!/bin/sh
+# acmetodo-todo: Filters the todo list to show only 'to-do' items.
+
+PLAN9=${PLAN9:-"/usr/local/plan9"}
+TODO_FILE="$PLAN9/lib/todo"
+ACME_BIN="$PLAN9/bin/acme"
+
+# Find the winid of the main acmetodo window
+MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1)
+
+if [ -z "$MAIN_TODO_WINID" ]; then
+    echo "Error: Main acmetodo window not found." >/dev/stderr
+    exit 1
+fi
+
+# Filter the content and send it back to the window
+filtered_content=$(grep '^[ ] ' "$TODO_FILE")
+
+# Clear current window content and then write the filtered content
+echo 'data' | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo "$filtered_content" | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID" # Mark window as clean after update
\ No newline at end of file
diff --git a/bash/acmetodo-inprogress b/bash/acmetodo-inprogress
new file mode 100755
index 0000000..d5ea505
--- /dev/null
+++ b/bash/acmetodo-inprogress
@@ -0,0 +1,19 @@
+#!/bin/sh
+# acmetodo-inprogress: Filters the todo list to show only 'in progress' items.
+
+PLAN9=${PLAN9:-"/usr/local/plan9"}
+TODO_FILE="$PLAN9/lib/todo"
+ACME_BIN="$PLAN9/bin/acme"
+
+MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1)
+
+if [ -z "$MAIN_TODO_WINID" ]; then
+    echo "Error: Main acmetodo window not found." >/dev/stderr
+    exit 1
+fi
+
+filtered_content=$(grep '^[>] ' "$TODO_FILE")
+
+echo 'data' | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo "$filtered_content" | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID"
\ No newline at end of file
diff --git a/bash/acmetodo-todo b/bash/acmetodo-todo
new file mode 100755
index 0000000..6149207
--- /dev/null
+++ b/bash/acmetodo-todo
@@ -0,0 +1,22 @@
+#!/bin/sh
+# acmetodo-todo: Filters the todo list to show only 'to-do' items.
+
+PLAN9=${PLAN9:-"/usr/local/plan9"}
+TODO_FILE="$PLAN9/lib/todo"
+ACME_BIN="$PLAN9/bin/acme"
+
+# Find the winid of the main acmetodo window
+MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1)
+
+if [ -z "$MAIN_TODO_WINID" ]; then
+    echo "Error: Main acmetodo window not found." >/dev/stderr
+    exit 1
+fi
+
+# Filter the content and send it back to the window
+filtered_content=$(grep '^[ ] ' "$TODO_FILE")
+
+# Clear current window content and then write the filtered content
+echo 'data' | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo "$filtered_content" | $ACME_BIN -a "$MAIN_TODO_WINID"
+echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID" # Mark window as clean after update
\ No newline at end of file
diff --git a/bash/acmetodo-toggle b/bash/acmetodo-toggle
new file mode 100755
index 0000000..bffccec
--- /dev/null
+++ b/bash/acmetodo-toggle
@@ -0,0 +1,56 @@
+#!/bin/sh
+# acmetodo-toggle: Toggles the status of a selected todo item.
+
+PLAN9=${PLAN9:-"/usr/local/plan9"}
+TODO_FILE="$PLAN9/lib/todo"
+ACME_BIN="$PLAN9/bin/acme"
+
+# Read the selected line(s) from standard input (Acme pipes the selection)
+selected_lines=$(cat)
+
+if [ -z "$selected_lines" ]; then
+    echo "No line selected to toggle." >/dev/stderr
+    exit 1
+fi
+
+# Process only the first line of the selection for toggling
+line_to_toggle=$(echo "$selected_lines" | head -n 1)
+
+new_line=""
+case "$line_to_toggle" in
+    '[] '*)
+        new_line="[>] ${line_to_toggle:3}" # Extract content after '[ ]'
+        ;;
+    '[>] '*)
+        new_line="[x] ${line_to_toggle:3}" # Extract content after '[>]'
+        ;;
+    '[x] '*)
+        new_line="[ ] ${line_to_toggle:3}" # Extract content after '[x]'
+        ;;
+    *)
+        echo "Warning: Selected line does not match a known todo item format: $line_to_toggle" >/dev/stderr
+        exit 0 # Exit gracefully if not a todo item
+        ;;
+esac
+
+if [ -n "$new_line" ]; then
+    # Use awk for a robust in-place replacement.
+    # It reads the entire file, replaces the first exact match, then prints.
+    awk -v old_line="$line_to_toggle" -v new_line="$new_line" '
+        BEGIN { replaced = 0 }
+        $0 == old_line && !replaced {
+            print new_line
+            replaced = 1
+            next
+        }
+        { print }
+    ' "$TODO_FILE" > "${TODO_FILE}.tmp" && mv "${TODO_FILE}.tmp" "$TODO_FILE"
+
+    # Find the winid of the main acmetodo window to refresh it.
+    MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1)
+    if [ -n "$MAIN_TODO_WINID" ]; then
+        echo 'Get' | $ACME_BIN -a "$MAIN_TODO_WINID"
+    else
+        echo "Warning: Main acmetodo window not found to refresh." >/dev/stderr
+    fi
+fi
\ No newline at end of file
diff --git a/elm/cost-of-meeting/src/Main.elm b/elm/cost-of-meeting/src/Main.elm
index 864515b..fdc16d5 100644
--- a/elm/cost-of-meeting/src/Main.elm
+++ b/elm/cost-of-meeting/src/Main.elm
@@ -6,7 +6,6 @@ import Html.Attributes as Attr
 import Html.Events exposing (onInput)
 import String
 
-
 -- MODEL
 
 type alias Model =
diff --git a/html/playground/index.html b/html/playground/index.html
index 680f022..a236d2f 100644
--- a/html/playground/index.html
+++ b/html/playground/index.html
@@ -6,238 +6,243 @@
     <title>JavaScript Playground</title>
     <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles.">
     <style>
-        body {
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-            background-color: #ddd;
-            padding: 10px;
-            height: 100vh;
-            margin: 0;
-        }
-
-        textarea {
-            width: 100%;
-            height: 64%;
-            font-family: monospace;
-            background-color: #FFFEEC;
-            border: 2px solid #000;
-            scrollbar-width: none;            
-            font-size: 1rem;
-            margin-bottom: 10px;
-            padding: 10px;
-            box-sizing: border-box;
-            resize: none;
-            border-bottom: 12px solid teal;
-            -webkit-user-modify: read-write-plaintext-only;
-        }
-
-        textarea::-webkit-scrollbar {
-            display: none;
-        }
-
-        textarea::selection {
-            background-color: #EFECA7;
-        }
-
-        textarea:focus {
-            outline: none;
-        }
-
-        #console {
-            width: 100%;
-            height: 22%;
-            background-color: #000;
-            color: #0fc;
-            font-family: monospace;
-            font-size: 1rem;
-            overflow-y: auto;
-            padding: 10px;
-            box-sizing: border-box;
-            white-space: pre-wrap;
-        }
-
-        .button-container {
-            width: 100%;
-            display: flex;
-            justify-content: flex-end;
-            margin-bottom: 10px;
-        }
-
-        button {
-            padding: 10px 20px;
-            font-size: 1rem;
-            margin-left: 10px;
-            cursor: pointer;
-            border: none;
-            transition: background-color 0.3s ease;
-        }
-
-        button:hover, button:focus {
-            outline: none; 
-        }
-
-        button.run {
-            background-color: teal;
-            color: #FFFFFF;
-        }
+        html, body { height: 100%; margin: 0; padding: 0; }
+        body { display: flex; flex-direction: column; background-color: #ddd; }
+        #app { display: flex; flex-direction: column; padding: 10px; height: 100%; box-sizing: border-box; }
+        .button-container { width: 100%; display: flex; justify-content: flex-end; margin-bottom: 10px; flex-shrink: 0; }
+        button { padding: 10px 20px; font-size: 1rem; margin-left: 10px; cursor: pointer; border: none; transition: background-color 0.3s ease; }
+        button:hover, button:focus { outline: none; }
+        button:disabled { background-color: #999; cursor: not-allowed; }
+        button.run { background-color: teal; color: #FFFFFF; }
+        textarea { width: 100%; height: 64%; font-family: monospace; background-color: #FFFEEC; border: 2px solid #000; scrollbar-width: none; font-size: 1rem; margin-bottom: 10px; padding: 10px; box-sizing: border-box; resize: none; border-bottom: 12px solid teal; -webkit-user-modify: read-write-plaintext-only; flex-shrink: 0; }
+        textarea::-webkit-scrollbar { display: none; }
+        textarea::selection { background-color: #EFECA7; }
+        textarea:focus { outline: none; }
+        #console { width: 100%; height: 22%; background-color: #000; color: #0fc; font-family: monospace; font-size: 1rem; overflow-y: auto; padding: 10px; box-sizing: border-box; white-space: pre-wrap; flex-grow: 1; }
+        #console .log-item { margin-bottom: 5px; border-bottom: 1px solid #333; }
+        #console details { cursor: pointer; }
+        #console summary { outline: none; }
     </style>
 </head>
 <body>
+    <div id="app"></div>
 
-    <div class="playground" id="playground">    
-        <div class="seesaw" id="seesaw"></div>
-        <div class="slide" id="slide"></div>
-    </div>
-
-    <div class="button-container">
-        <button onclick="clearEverything()">Clear</button>
-        <button onclick="downloadCodeAndEditor()">Download</button>
-        <button onclick="shareCode()">Share</button>
-        <button onclick="evaluateCode()" class="run">Run Code</button>
-    </div>
-    <textarea id="codeInput" placeholder="Enter JavaScript..." spellcheck="false"></textarea>
-    <div id="console"></div>
+    <iframe id="sandbox" style="display: none;" title="JavaScript Sandbox"></iframe>
 
     <script>
-        function evaluateCode() {
-            const code = document.getElementById('codeInput').value;
-            const consoleDiv = document.getElementById('console');
-            consoleDiv.innerHTML = '';
-
-            // Custom console.log function to output to the console div
-            const originalConsoleLog = console.log;
-            console.log = function(...args) {
-                args.forEach(arg => {
-                    const output = document.createElement('div');
-                    output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg;
-                    consoleDiv.appendChild(output);
-                });
-                originalConsoleLog.apply(console, args);
-            };
-
-            try {
-                eval(code);
-            } catch (error) {
-                const errorOutput = document.createElement('div');
-                errorOutput.textContent = error;
-                errorOutput.style.color = 'red';
-                consoleDiv.appendChild(errorOutput);
+        /**
+         * ----------------------------------------------------------------
+         * Functional Utilities
+         * ----------------------------------------------------------------
+         */
+
+        const pipe = (...fns) => (initialValue) => fns.reduce((acc, fn) => fn(acc), initialValue);
+        const compose = (...fns) => (initialValue) => fns.reduceRight((acc, fn) => fn(acc), initialValue);
+
+        /**
+         * ----------------------------------------------------------------
+         * The Elm Architecture (Model, View, Update) Implementation
+         * ----------------------------------------------------------------
+         */
+
+        const init = {
+            code: `
+            const add = (a, b) => a + b;
+            const multiply = (a, b) => a * b;
+
+            const addAndMultiply = pipe(add, multiply);
+
+            console.log(addAndMultiply(2, 3));
+`,
+            consoleOutput: [],
+            sandboxReady: false,
+        };
+
+        const view = (model) => `
+            <div class="button-container">
+                <button onclick="dispatch({ type: 'CLEAR' })">Clear</button>
+                <button onclick="dispatch({ type: 'DOWNLOAD' })">Download</button>
+                <button onclick="dispatch({ type: 'SHARE' })">Share</button>
+                <button onclick="dispatch({ type: 'RUN_CODE' })" class="run" ${!model.sandboxReady ? 'disabled title="Sandbox is loading..."' : ''}>Run Code</button>
+            </div>
+            <textarea id="codeInput" oninput="dispatch({ type: 'CODE_CHANGED', payload: this.value })" onkeydown="handleKeyDown(event)">${model.code}</textarea>
+            <div id="console">
+                ${model.consoleOutput.map(log => {
+                    if (typeof log === 'object' && log !== null) {
+                        return `<div class="log-item"><details><summary>${Object.prototype.toString.call(log)}</summary><pre>${JSON.stringify(log, null, 2)}</pre></details></div>`;
+                    }
+                    return `<div class="log-item">${log}</div>`;
+                }).join('')}
+            </div>
+        `;
+
+        const update = (msg, model) => {
+            switch (msg.type) {
+                case 'CODE_CHANGED':
+                    return { ...model, code: msg.payload };
+                
+                case 'RUN_CODE':
+                    return model;
+
+                case 'CONSOLE_LOG':
+                    return { ...model, consoleOutput: [...model.consoleOutput, ...msg.payload] };
+
+                case 'CLEAR':
+                    if (confirm('Are you sure you want to reset the playground?')) {
+                        window.location.hash = '';
+                        window.location.reload();
+                    }
+                    return model;
+
+                case 'CLEAR_CONSOLE':
+                     return { ...model, consoleOutput: [] };
+                
+                case 'SHARE':
+                case 'DOWNLOAD':
+                    return model;
+
+                case 'SANDBOX_READY':
+                    return { ...model, sandboxReady: true };
+
+                default:
+                    return model;
             }
-
-            // Restore browser's console.log
-            console.log = originalConsoleLog;
-        }
-
-        function downloadCodeAndEditor() {
-            const codeInput = document.getElementById('codeInput').value;
-            const htmlContent = document.documentElement.outerHTML.replace(
-                /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
-                `<textarea id="codeInput">${codeInput}</textarea>`
-            );
-
-            const blob = new Blob([htmlContent], { type: 'text/html' });
-            const url = URL.createObjectURL(blob);
-            const a = document.createElement('a');
-            a.href = url;
-            a.download = 'code_editor.html';
-            document.body.appendChild(a);
-            a.click();
-            document.body.removeChild(a);
-            URL.revokeObjectURL(url);
-        }
-
-        function shareCode() {
-            const code = document.getElementById('codeInput').value;
-            const encodedCode = btoa(encodeURIComponent(code));
-            window.location.hash = encodedCode;
-            window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href);
-        }
-
-        function clearEverything() {
-            if (!confirm('Are you sure you want to reset the playground?')) {
-                return;
-            } else {               
-                window.location.hash = '';
-                window.location.reload();
+        };
+
+        /**
+         * ----------------------------------------------------------------
+         * Application Core & Side-Effect Handling
+         * ----------------------------------------------------------------
+         */
+
+        let model = null;
+        const appContainer = document.getElementById('app');
+        const sandbox = document.getElementById('sandbox');
+
+        const dispatch = (msg) => {
+            const newModel = update(msg, model);
+            handleSideEffects(msg, newModel);
+            model = newModel;
+            render(model);
+        };
+
+        const handleSideEffects = (msg, model) => {
+             if (msg.type === 'RUN_CODE') {
+                if (!model.sandboxReady) return;
+                dispatch({ type: 'CLEAR_CONSOLE' });
+                sandbox.contentWindow.postMessage({ code: model.code }, '*');
+            } else if (msg.type === 'SHARE') {
+                const encodedCode = btoa(encodeURIComponent(model.code));
+                window.location.hash = encodedCode;
+                window.prompt("Copy the URL to share.", window.location.href);
+            } else if (msg.type === 'DOWNLOAD') {
+                const htmlContent = document.documentElement.outerHTML.replace(
+                    /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
+                    `<textarea id="codeInput">${model.code}</textarea>`
+                );
+                const blob = new Blob([htmlContent], { type: 'text/html' });
+                const url = URL.createObjectURL(blob);
+                const a = document.createElement('a');
+                a.href = url;
+                a.download = 'playground.html';
+                a.click();
+                URL.revokeObjectURL(url);
             }
-        }
-
-        function loadCodeFromHash() {
-            const hash = window.location.hash.substring(1);
-            if (hash) {
-                try {
-                    const decodedCode = decodeURIComponent(atob(hash));
-                    document.getElementById('codeInput').value = decodedCode;
-                } catch (error) {
-                    console.error('Failed to decode the URL hash:', error);
+        };
+
+        const render = (model) => {
+            const focusedElementId = document.activeElement?.id;
+            const selectionStart = document.activeElement?.selectionStart;
+            const selectionEnd = document.activeElement?.selectionEnd;
+
+            appContainer.innerHTML = view(model);
+
+            if (focusedElementId === 'codeInput') {
+                const focusedElement = document.getElementById(focusedElementId);
+                if (focusedElement) {
+                    focusedElement.focus();
+                    if (typeof selectionStart === 'number') {
+                       focusedElement.selectionStart = selectionStart;
+                       focusedElement.selectionEnd = selectionEnd;
+                    }
                 }
             }
-        }
+        };
 
-        function help() {
-            const helpText = `
-            Welcome to the JavaScript Playground! Here are some tips to get you started:
-
-            1. Enter your JavaScript code in the textarea.
-            2. Click the "Run Code" button to execute your code.
-            3. The console output will be displayed below the textarea.
-            4. Click the "Clear" button to reset the playground.
-            5. Click the "Download" button to save your code and editor as an HTML file.
-            6. Click the "Share" button to generate a URL to share your code with others.
-            7. You can also press "Cmd + Enter" to run your code.
-            8. There's an empty div above the buttons with the id "playground"
-            9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()"
-            10. You can use the "clear()" function to clear the content's of the console.
-
-            Go nuts! Share scrappy fiddles!
-            `;
-            console.log(helpText);
-        }
-
-        function clear() {
-            document.getElementById('console').innerHTML = '';
-        }
-
-        function mountHelp() {
-            console.log(`
-            The mount function is used to mount stuff to the playground div.
-            It takes a function as an argument, which in turn receives the playground div as an argument.
-            Before mounting, it clears the playground div.
-            Here's an example of how to use the mount function:
-
-            mount(playground => {
-                const h1 = document.createElement('h1');
-                h1.textContent = 'Hell is empty and all the devils are here.';
-                playground.appendChild(h1);
-            });
-
-            This will add an h1 element to the playground div.
-            `);
-        }
-
-        function mount(mountFunction) {
-            const playground = document.getElementById('playground');
-            if (!playground) {
-                console.error("Couldn't find a div with the id 'playground'! You may need to reload the page.");
-                return;
+        const handleKeyDown = (event) => {
+            if (event.key === 'Tab') {
+                event.preventDefault();
+                const start = event.target.selectionStart;
+                const end = event.target.selectionEnd;
+                event.target.value = event.target.value.substring(0, start) + "  " + event.target.value.substring(end);
+                dispatch({ type: 'CODE_CHANGED', payload: event.target.value });
+                event.target.selectionStart = event.target.selectionEnd = start + 2;
             }
+            if (event.metaKey && event.key === 'Enter') {
+                event.preventDefault();
+                dispatch({ type: 'RUN_CODE' });
+            }
+        };
+        
+        /**
+         * ----------------------------------------------------------------
+         * Sandbox Initialization
+         * ----------------------------------------------------------------
+         */
+         
+        const sandboxSrc = `
+            <script>
+                const pipe = ${pipe.toString()};
+                const compose = ${compose.toString()};
+            
+                const originalConsoleLog = console.log;
+                console.log = (...args) => {
+                    parent.postMessage({ type: 'CONSOLE_LOG', payload: args }, '*');
+                };
+
+                window.addEventListener('message', (event) => {
+                    try {
+                        eval(event.data.code);
+                    } catch (e) {
+                        console.log(e.toString());
+                    }
+                });
+            <\/script>
+        `;
 
-            if (playground.innerHTML.trim() !== "") {
-                playground.innerHTML = "";
+        window.addEventListener('message', (event) => {
+            if (event.source === sandbox.contentWindow && event.data.type === 'CONSOLE_LOG') {
+                dispatch(event.data);
             }
-            mountFunction(playground);
+        });
+        
+        /**
+         * ----------------------------------------------------------------
+         * Application Entry Point
+         * ----------------------------------------------------------------
+         */
+        
+        const hash = window.location.hash.substring(1);
+        if (hash) {
+            try {
+                const decodedCode = decodeURIComponent(atob(hash));
+                model = { ...init, code: decodedCode };
+            } catch (error) {
+                console.error('Failed to decode the URL hash:', error);
+                model = init;
+            }
+        } else {
+            model = init;
         }
 
+        render(model);
 
-        document.getElementById('codeInput').addEventListener('keydown', function(event) {
-            if (event.metaKey && event.key === 'Enter') {
-                event.preventDefault();
-                evaluateCode();
-            }
-        });
+        sandbox.onload = () => {
+            dispatch({ type: 'SANDBOX_READY' });
+        };
+
+        sandbox.srcdoc = sandboxSrc;
 
-        window.onload = loadCodeFromHash;
     </script>
 </body>
-</html>
+</html>
\ No newline at end of file
diff --git a/ts/thinking-about-unions/left-pad.ts b/ts/thinking-about-unions/left-pad.ts
index e75e38d..95f6d51 100644
--- a/ts/thinking-about-unions/left-pad.ts
+++ b/ts/thinking-about-unions/left-pad.ts
@@ -1,9 +1,9 @@
-/* 
+/*
 
 A stupidly simple example of unions.
 
 Unions can be used to describe a type that is actually several different types.
-Here, the Padding type is a union of either a number or a string. 
+Here, the Padding type is a union of either a number or a string.
 Then, leftPad uses the union type so that it can accept either sort of type.
 
 */
@@ -11,16 +11,17 @@ Then, leftPad uses the union type so that it can accept either sort of type.
 type Padding = number | string;
 
 const leftPad = (value: string, padding: Padding) => {
-    if (typeof padding === 'number') {
-        return Array(padding + 1).join(' ') + value; // 0 indexing is for computers, this function is for people.
-    }
-    if (typeof padding === 'string') {
-        return padding + value;
-    }
-    throw new Error(`Expected number or string, got '${padding}'.`);
-}
+  switch (typeof padding) {
+    case "number":
+      return Array(padding + 1).join(" ") + value; // 0 indexing is for computers, this function is for people.
+    case "string":
+      return padding + value;
+    default:
+      throw new Error(`Expected number or string, got '${padding}'.`);
+  }
+};
 
-const marioMsg = 'It is I, Mario!';
+const marioMsg = "It is I, Mario!";
 console.log(leftPad(marioMsg, 4));
-console.log(leftPad(marioMsg, "****"));
-console.log(leftPad(marioMsg, true));
\ No newline at end of file
+console.log(leftPad(marioMsg, "*** "));
+// console.log(leftPad(marioMsg, true));