diff options
-rwxr-xr-x | bash/acmetodo | 145 | ||||
-rwxr-xr-x | bash/acmetodo-add | 25 | ||||
-rwxr-xr-x | bash/acmetodo-all | 17 | ||||
-rwxr-xr-x | bash/acmetodo-done | 19 | ||||
-rw-r--r-- | bash/acmetodo-filter | 22 | ||||
-rwxr-xr-x | bash/acmetodo-inprogress | 19 | ||||
-rwxr-xr-x | bash/acmetodo-todo | 22 | ||||
-rwxr-xr-x | bash/acmetodo-toggle | 56 | ||||
-rw-r--r-- | elm/cost-of-meeting/src/Main.elm | 1 | ||||
-rw-r--r-- | html/playground/index.html | 435 | ||||
-rw-r--r-- | ts/thinking-about-unions/left-pad.ts | 27 |
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)); |