Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save CurtisAccelerate/e56ec3944b48a41937aa1f4a6c83f08d to your computer and use it in GitHub Desktop.
Save CurtisAccelerate/e56ec3944b48a41937aa1f4a6c83f08d to your computer and use it in GitHub Desktop.
ChatGPT C64 Run Code
// ==UserScript==
// @name ChatGPT Code Block Processor C64 Edition
// @namespace http://tampermonkey.net/
// @version 3.0.0
// @description Add "Run HTML" and "Run C64" buttons as hover actions on code blocks in ChatGPT responses with a draggable panel.
// @match https://chatgpt.com/*
// @grant GM_addElement
// @grant GM_addStyle
// @grant unsafeWindow
// ==/UserScript==
(function() {
'use strict';
let panel;
let isDragging = false;
let startX;
let startWidth;
// Define variables and functions
let c64Iframe = null;
function sendToC64(text) {
if (!c64Iframe) {
console.error('C64 iframe not initialized');
return false;
}
console.log('Sending to C64:', text);
try {
c64Iframe.contentWindow.postMessage({ type: 'typePetText', text: text + "\r\n" }, '*');
return true;
} catch (error) {
console.error('Error sending message to C64:', error);
return false;
}
}
function checkC64IframeStatus() {
console.log('c64Iframe:', c64Iframe);
console.log('c64Iframe.contentWindow:', c64Iframe ? c64Iframe.contentWindow : 'N/A');
if (c64Iframe && c64Iframe.contentWindow) {
console.log('C64 iframe is ready to receive messages');
return true;
} else {
console.log('C64 iframe is not ready');
return false;
}
}
function waitForIframeReady(callback) {
const checkInterval = 5; // Check every 100 milliseconds
const maxAttempts = 10; // Maximum attempts before giving up
let attempts = 0;
const interval = setInterval(() => {
if (checkC64IframeStatus()) {
clearInterval(interval);
callback();
} else if (attempts >= maxAttempts) {
clearInterval(interval);
console.error('C64 iframe did not become ready in time');
}
attempts++;
}, checkInterval);
}
// Expose variables and functions to the global scope
unsafeWindow.sendToC64 = sendToC64;
unsafeWindow.checkC64IframeStatus = checkC64IframeStatus;
function createSlideOutPanel(codeBlock, isC64 = false) {
if (panel) {
panel.remove();
}
panel = document.createElement('div');
panel.style.cssText = `
position: fixed;
top: 0;
right: 0;
width: 600px;
height: 100%;
background: #f7f7f8;
box-shadow: -2px 0 5px rgba(0,0,0,0.3);
z-index: 1000;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
`;
const header = document.createElement('div');
header.style.cssText = `
padding: 10px;
background: #282c34;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
`;
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.style.cssText = `
background: #ff5f57;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
color: white;
`;
closeButton.onclick = () => panel.style.transform = 'translateX(100%)';
header.appendChild(closeButton);
panel.appendChild(header);
const contentContainer = document.createElement('div');
contentContainer.style.cssText = `
padding: 10px;
overflow-y: auto;
flex-grow: 1;
`;
const iframe = document.createElement('iframe');
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
margin: 0;
padding: 0;
`;
contentContainer.appendChild(iframe);
panel.appendChild(contentContainer);
document.body.appendChild(panel);
if (isC64) {
iframe.src = 'http://localhost:8080'; // Adjust to your C64 emulator URL
c64Iframe = iframe; // Store the iframe globally
iframe.onload = () => {
console.log('C64 iframe loaded');
const basicCode = codeBlock.querySelector('code').textContent.trim();
console.log('BASIC code to be sent:', basicCode);
// Ensure the iframe is ready before sending the code
setTimeout(() => {
waitForIframeReady(() => {
console.log('Sending BASIC code to C64 emulator');
sendToC64(basicCode);
});
}, 1000); // Give some time for the iframe to initialize
};
} else {
const cleanCodeBlock = codeBlock.cloneNode(true);
const runDemoButton = cleanCodeBlock.querySelector('.run-demo-hover-button');
if (runDemoButton) {
runDemoButton.remove();
}
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(cleanCodeBlock.textContent);
doc.close();
}
setTimeout(() => panel.style.transform = 'translateX(0)', 0);
header.addEventListener('mousedown', startDragging);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDragging);
document.addEventListener('click', function(event) {
if (!panel.contains(event.target) && !event.target.closest('.run-demo-hover-button')) {
panel.style.transform = 'translateX(100%)';
}
}, { once: true });
}
function startDragging(e) {
isDragging = true;
startX = e.clientX;
startWidth = parseInt(document.defaultView.getComputedStyle(panel).width, 10);
}
function drag(e) {
if (!isDragging) return;
const width = startWidth - (e.clientX - startX);
panel.style.width = `${width}px`;
}
function stopDragging() {
isDragging = false;
}
function addHoverButton(codeBlock) {
if (!codeBlock.querySelector('.run-demo-hover-button')) {
const htmlButton = document.createElement('button');
htmlButton.textContent = 'Run HTML';
htmlButton.className = 'run-demo-hover-button';
htmlButton.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
display: none;
z-index: 1000;
`;
htmlButton.onclick = (e) => {
e.stopPropagation();
createSlideOutPanel(codeBlock);
};
const c64Button = document.createElement('button');
c64Button.textContent = 'Run C64';
c64Button.className = 'run-demo-hover-button';
c64Button.style.cssText = `
position: absolute;
top: 10px;
right: 90px;
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
display: none;
z-index: 1000;
`;
c64Button.onclick = (e) => {
e.stopPropagation();
createSlideOutPanel(codeBlock, true);
};
codeBlock.style.position = 'relative';
codeBlock.appendChild(htmlButton);
codeBlock.appendChild(c64Button);
codeBlock.addEventListener('mouseenter', () => {
htmlButton.style.display = 'block';
c64Button.style.display = 'block';
});
codeBlock.addEventListener('mouseleave', () => {
htmlButton.style.display = 'none';
c64Button.style.display = 'none';
});
}
}
function processCodeBlocks() {
const codeBlocks = document.querySelectorAll('.overflow-y-auto');
codeBlocks.forEach(codeBlock => {
if (codeBlock.closest('.markdown')) {
addHoverButton(codeBlock);
}
});
}
function observeChat() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const codeBlocks = node.querySelectorAll('.overflow-y-auto');
codeBlocks.forEach(codeBlock => {
if (codeBlock.closest('.markdown')) {
addHoverButton(codeBlock);
}
});
}
});
}
});
});
const chatContainer = document.querySelector('main');
if (chatContainer) {
observer.observe(chatContainer, { childList: true, subtree: true });
}
}
function addIndicator() {
const indicator = document.createElement('div');
indicator.textContent = 'Code Block Processor Active';
indicator.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
z-index: 1000;
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border-radius: 5px;
font-size: 12px;
`;
document.body.appendChild(indicator);
setTimeout(() => indicator.style.display = 'none', 3000);
}
function initializeProcessor() {
addIndicator();
processCodeBlocks();
observeChat();
}
// Use MutationObserver to wait for the chat interface to load
const bodyObserver = new MutationObserver((mutations) => {
if (document.querySelector('main')) {
bodyObserver.disconnect();
initializeProcessor();
}
});
bodyObserver.observe(document.body, { childList: true, subtree: true });
})();
Project:
Run c64 Basic code directly in ChatGPT.
Instructions
1. Install tampermokey script
2. Run CSP unblock. CSP policy is designed to protect you. Caution: https://chromewebstore.google.com/detail/csp-unblock/lkbelpgpclajeekijigjffllhigbhobd?hl=en
3. install and run this emulator locally https://github.com/luxocrates/viciious
4. Replace the webdev.js and runloop.js. Ensure the server is set correctly
5. Play the examples https://chatgpt.com/share/b11d2824-bed2-4b37-8e3f-bdeafbe8eac8
6. Check out some of these basic programs
https://github.com/mad4j/c64-codeart
https://retrogamestart.com/answers/learn-commodore-64-basic-programming-type-text-based-games
Architecture:
1. Tamper moneky script monitors chat and adds button to code blocks.
2. Press the send c64 opens iframe with address of local running emulator
3. Host process for emulator watches for commands to send text
4. Updated with a sendPet to receive text
Recap quick/start
1. install tampermonkey script
2. get viciious c64 emulator
3. update the files
4. start the c64 emulator
5. use the extension script to disable security policy for the chatgpt
6. click button to run the c64 demo code
/*
runloop: performs operations like tick, serialize and deserialize on the
target system as a whole, and runs the simulation in a time-throttled loop,
with an optional hook for breakpoints
*/
import {
KEYBOARD_BUFFER_ADDR,
KEYBOARD_BUFFER_INDEX,
KEYBOARD_BUFFER_LENGTH,
} from "../tools/romLocations";
// Node.js doesn't have `performance` loaded by default. Not that we make use
// of it in that version, but the runloop expects it to be there.
if (!globalThis.performance) {
globalThis.performance = { now: () => 0 };
}
// How many frames to run for a single frames-per-second sample
const FRAMES_PER_WAYPOINT = 50;
// Bound by attach
let c64;
// configure by setDevices
let wires;
let cpu;
let vic;
let cias;
let sid;
let tape;
let state;
let masterStop = false;
let frameStop = false;
export function attach(nascentC64) {
c64 = nascentC64;
wires = c64.wires;
cpu = c64.cpu;
vic = c64.vic;
cias = c64.cias;
sid = c64.sid;
tape = c64.tape;
reset();
c64.runloop = {
// Control
run,
stop,
stopAfterFrame,
isRunning,
type,
untilPc,
reset,
serialize,
deserialize,
typePet,
// Debug
getState,
};
}
function reset() {
state = {
cycle: 0,
};
c64.wires.reset();
c64.ram .reset();
c64.vic .reset();
c64.sid .reset();
c64.cpu .reset();
c64.cias .reset();
c64.tape .reset();
if (c64.hooks.setTitle) {
c64.hooks.setTitle("");
}
}
function getState() {
return state;
}
function stop() {
masterStop = true;
}
function stopAfterFrame() {
frameStop = true;
}
function isRunning() {
return !masterStop;
}
let timer;
function run(profile) {
// Apply default run profile
profile = {
tick: () => false,
fps: 50,
...profile,
}
let resolveBreakPromise;
const breakPromise = new Promise(resolve => { resolveBreakPromise = resolve; });
if (timer !== undefined) {
masterStop = true;
clearInterval(timer);
}
masterStop = false;
frameStop = false;
const cleanUpOnBreak = () => {
clearInterval(timer);
if (c64.hooks.didStop) c64.hooks.didStop();
timer = undefined;
resolveBreakPromise();
};
let timeAtWaypoint = performance.now();
let framesSinceWaypoint = 0;
timer = setInterval(
() => {
try {
// We'll loop for one video frame at a time. That is,
// 312 rows of 63 cycles per row
// (Which would be different if we support NTSC in future)
for (let i = 0; i < (63 * 312); i++) {
state.cycle++;
cpu .tick();
vic .tick();
cias.tick();
sid .tick();
tape.tick();
if (masterStop || profile.tick()) {
cleanUpOnBreak();
break;
}
}
// Frames-per-second counter
if (++framesSinceWaypoint === FRAMES_PER_WAYPOINT) {
const now = performance.now();
if (c64.hooks.updateFps) {
c64.hooks.updateFps(
Math.round((1000 * FRAMES_PER_WAYPOINT) / (now - timeAtWaypoint))
);
}
timeAtWaypoint = now;
framesSinceWaypoint = 0;
}
// Frame stop
if (frameStop) cleanUpOnBreak();
}
catch (e) {
console.error("Caught exception:", e);
cleanUpOnBreak();
}
},
1000 / profile.fps
);
if (c64.hooks.didStart) c64.hooks.didStart();
return breakPromise;
}
async function untilPc(pc, fast = false) {
const regs = c64.cpu.getState();
if (pc === undefined) {
// TODO: throw instead?
console.error("Missing argument: PC address");
return;
}
// If the PC was currently at the address we were waiting for,
// advance past it. You want to be able to call this function
// multple times to re-run.
await run({
tick: () => regs.pc !== pc,
});
const profile = {
tick: () => regs.pc === pc,
};
if (fast) profile.fps = Infinity;
return run(profile);
}
function type(str) {
let bufLen = c64.wires.cpuRead(KEYBOARD_BUFFER_INDEX);
for (let char of str) {
if (bufLen >= KEYBOARD_BUFFER_LENGTH) {
throw new Error("Overflow for Kernal keyboard buffer");
}
c64.wires.cpuWrite(KEYBOARD_BUFFER_ADDR + bufLen, char.charCodeAt(0));
c64.wires.cpuWrite(KEYBOARD_BUFFER_INDEX, ++bufLen);
}
}
async function typePet(str) {
let bufLen = c64.wires.cpuRead(KEYBOARD_BUFFER_INDEX);
for (let char of str) {
while (bufLen >= KEYBOARD_BUFFER_LENGTH) {
// Wait for space in the buffer
await new Promise(resolve => setTimeout(resolve, 10));
bufLen = c64.wires.cpuRead(KEYBOARD_BUFFER_INDEX);
}
let petsciiChar = convertToPetscii(char);
c64.wires.cpuWrite(KEYBOARD_BUFFER_ADDR + bufLen, petsciiChar);
c64.wires.cpuWrite(KEYBOARD_BUFFER_INDEX, ++bufLen);
}
}
function convertToPetscii(char) {
const asciiCode = char.charCodeAt(0);
let petsciiCode;
// Convert ASCII to PETSCII
if (asciiCode >= 65 && asciiCode <= 90) { // A-Z
petsciiCode = asciiCode - 64 + 192; // Convert A-Z to PETSCII uppercase
} else if (asciiCode >= 97 && asciiCode <= 122) { // a-z
petsciiCode = asciiCode - 96 + 64; // Convert a-z to PETSCII lowercase
} else if (asciiCode === 13) { // Carriage return
petsciiCode = 13;
} else if (asciiCode === 10) { // Newline
petsciiCode = 13; // Convert newline to carriage return in PETSCII
} else {
petsciiCode = asciiCode; // Default case for other characters
}
return petsciiCode;
}
function serialize() {
return JSON.stringify(
{
version: {
creator: "viciious",
major: 0,
minor: 1,
},
runloop: JSON.stringify(state),
wires: c64.wires.serialize(),
ram: c64.ram .serialize(),
vic: c64.vic .serialize(),
sid: c64.sid .serialize(),
cpu: c64.cpu .serialize(),
cias: c64.cias .serialize(),
tape: c64.tape .serialize(),
}
);
}
function deserialize(json) {
const obj = JSON.parse(json);
state = JSON.parse(obj.runloop);
c64.wires.deserialize(obj.wires);
c64.ram .deserialize(obj.ram );
c64.vic .deserialize(obj.vic );
c64.sid .deserialize(obj.sid );
c64.cpu .deserialize(obj.cpu );
c64.cias .deserialize(obj.cias );
c64.tape .deserialize(obj.tape );
}
// Host interfaces
import { attach as video } from "../host/video-canvas";
import { attach as audio } from "../host/audio-OscillatorNode";
import { attach as joystick } from "../host/joystick-KeyboardEvent";
import { attach as keyboard } from "../host/keyboard-KeyboardEvent";
// Target devices
import { attach as wires } from "../target/wires";
import { attach as ram } from "../target/ram";
import { attach as vic } from "../target/vic";
import { attach as sid } from "../target/sid";
import { attach as cias } from "../target/cias";
import { attach as cpu } from "../target/cpu";
import { attach as tape } from "../target/tape";
// ROMs
import basic from "../target/rom/basic";
import kernal from "../target/rom/skipRamTest";
import character from "../target/rom/character";
// Bringup
import { bringup } from "../target/bringup";
// Everything else
import { attach as monitor } from "../monitor";
import { attach as webFrontEnd } from "../host/webFrontEnd";
import { attach as dragAndDrop } from "../host/dragAndDrop";
const c64 = bringup({
host: { audio, video, keyboard, joystick },
target: { wires, ram, vic, sid, cpu, cias, tape, basic, kernal, character },
attachments: [
monitor,
dragAndDrop,
webFrontEnd,
],
});
c64.runloop.run();
// Make the c64 object globally accessible
globalThis.c64 = c64;
// Add event listener for messages
window.addEventListener('message', (event) => {
// Ensure the message is coming from a trusted source
const command = event.data;
// Execute the command
if (command.type === 'startRunloop') {
c64.runloop.run();
} else if (command.type === 'stopRunloop') {
c64.runloop.stop();
} else if (command.type === 'typeText') {
c64.runloop.type(command.text);
} else if (command.type === 'typePetText') {
c64.runloop.typePet(command.text);
}
// Add more commands as needed
});
// To run a test program on load, uncomment the below:
/*
import { ingest } from "../host/ingest";
import prg from "../tests/tod-prg.js";
ingest(c64, ".prg", prg);
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment