-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathscript.js
More file actions
230 lines (201 loc) · 8.73 KB
/
script.js
File metadata and controls
230 lines (201 loc) · 8.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
// TextEncoder and TextDecoder for handling string encoding/decoding
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// Buffers for storing terminal output and console logs
let terminalBuffer = "";
let consoleLogBuffer = "";
// WASM module interface
const WASM = {
instance: null, // Holds the WebAssembly instance
async initialize() {
try {
// Fetch the WASM binary
const response = await fetch("./zig-out/bin/tinylisp.wasm");
const buffer = await response.arrayBuffer();
// Instantiate the WASM module
const wasmModule = await WebAssembly.instantiate(buffer, this.importObject);
this.instance = wasmModule.instance;
// Initialize the Lisp interpreter
this.instance.exports.tinylisp_init();
} catch (error) {
console.error("Failed to initialize WASM:", error);
}
},
// Get a string from WASM memory
getString(ptr, len) {
const memory = this.instance.exports.memory;
return textDecoder.decode(new Uint8Array(memory.buffer, ptr, len));
},
// Import object for WASM, providing JavaScript functions to the module
importObject: {
env: {
// Write to the terminal buffer
jsTerminalWriteBuffer: (ptr, len) => {
terminalBuffer += WASM.getString(ptr, len);
},
// Write to the console log buffer
jsConsoleLogWrite: (ptr, len) => {
consoleLogBuffer += WASM.getString(ptr, len);
},
// Flush the console log buffer to the browser console
jsConsoleLogFlush: () => {
console.log(consoleLogBuffer);
consoleLogBuffer = "";
},
},
},
};
// Terminal emulator class
class Terminal {
constructor() {
// DOM elements
this.output = document.getElementById('output');
this.input = document.getElementById('input');
this.prompt = document.getElementById('prompt');
// Command history and index
this.commandHistory = [];
this.historyIndex = -1;
// Initialize event listeners
this.initializeInputListener();
this.initializeAutoResize();
}
// Set up the input event listener
initializeInputListener() {
this.input.addEventListener('keydown', (e) => this.handleInput(e));
}
// Set up auto-resize for the input textarea
initializeAutoResize() {
this.input.addEventListener('input', () => this.autoResizeTextarea());
}
// Adjust the height of the input textarea based on its content
autoResizeTextarea() {
// Reset the height to auto to recalculate the height
this.input.style.height = 'auto';
// Set the height to the scrollHeight (content height)
this.input.style.height = `${this.input.scrollHeight}px`;
}
// Handle keyboard input
handleInput(event) {
if (event.key === 'Enter') {
// Check if Shift is pressed
if (!event.shiftKey) {
// Enter (no shift): Add a new line inside the textarea
const cursorPosition = this.input.selectionStart;
const value = this.input.value;
this.input.value = value.slice(0, cursorPosition) + '\n' + value.slice(cursorPosition);
this.input.setSelectionRange(cursorPosition + 1, cursorPosition + 1); // Move cursor to the new line
this.autoResizeTextarea(); // Resize the textarea
event.preventDefault();
} else {
// Shift + Enter: Execute the command
this.processCommand(this.input.value.trim());
this.input.value = ''; // Clear the input field
this.autoResizeTextarea(); // Reset the textarea height
event.preventDefault(); // Prevent default behavior (e.g., form submission)
}
} else if (event.key === 'Tab') {
// Tab: Insert a tab character at the cursor position
const cursorPosition = this.input.selectionStart;
const value = this.input.value;
this.input.value = value.slice(0, cursorPosition) + '\t' + value.slice(cursorPosition);
this.input.setSelectionRange(cursorPosition + 1, cursorPosition + 1); // Move cursor after the tab
event.preventDefault(); // Prevent the browser's default behavior
} else if (event.key === 'ArrowUp') {
// Navigate command history (up)
this.navigateHistory(-1);
} else if (event.key === 'ArrowDown') {
// Navigate command history (down)
this.navigateHistory(1);
} else if (event.ctrlKey && event.key === 'c') {
// Clear the line input
this.input.value = '';
}
}
// Process a command entered by the user
processCommand(input) {
if (!input || input.trim() === "") return;
// Add the command to history
this.commandHistory.push(input);
this.historyIndex = this.commandHistory.length;
// Display the command in green
this.appendOutput(`λ ${input}`, 'output-command');
// Handle meta commands
if (input === '?help') {
window.open("https://github.com/daneelsan/tinylisp/blob/main/README.md", "_blank");
this.appendOutput("", 'output-result');
} else if (input === '?clear') {
this.clearTerminal();
} else if (input === '?commands') {
this.appendOutput("Available meta commands:", 'output-result');
this.appendOutput("?help - Open the documentation", 'output-result');
this.appendOutput("?clear - Clear the terminal output", 'output-result');
this.appendOutput("?commands - List available meta commands", 'output-result');
this.appendOutput("", 'output-result');
} else {
this.executeWasmCommand(input);
}
this.scrollToBottom();
}
// Execute a command in the WASM module
executeWasmCommand(input) {
// Null-terminate the input and encode it
const nullTerminatedInput = input + "\0";
const encodedInput = textEncoder.encode(nullTerminatedInput);
// Allocate memory in the WASM module for the input
const inputAddress = WASM.instance.exports._wasm_alloc(encodedInput.length);
const inputArray = new Uint8Array(WASM.instance.exports.memory.buffer, inputAddress);
inputArray.set(encodedInput);
// Run the command in the WASM module
WASM.instance.exports.tinylisp_run(inputAddress, encodedInput.length);
// Append the output to the terminal
// TODO: Could change the color of the output to red it was ERR
this.appendOutput(terminalBuffer, 'output-result');
terminalBuffer = "";
// Free allocated memory
WASM.instance.exports._wasm_free(inputAddress);
}
// Append output to the terminal
appendOutput(text, className = 'output-result') {
const outputLine = document.createElement('div');
// Replace tabs with 4 spaces
// TODO: Make these spaces configurable
outputLine.textContent = text.replace(/\t/g, ' ');
outputLine.className = className; // Apply the specified class
this.output.appendChild(outputLine);
// Ensure the terminal scrolls to the bottom after appending new content
this.scrollToBottom();
}
// Clear the terminal output (but preserve the banner)
clearTerminal() {
// Clear the terminal output but preserve the banner
this.output.innerHTML = '<div id="banner">Welcome to <a href="https://github.com/daneelsan/tinylisp/blob/main/README.md" target="_blank">TINYLISP</a>!</div>';
}
// Navigate through command history
navigateHistory(direction) {
if (direction === -1 && this.historyIndex > 0) {
// Move up in history
this.historyIndex--;
} else if (direction === 1 && this.historyIndex < this.commandHistory.length - 1) {
// Move down in history
this.historyIndex++;
} else if (direction === 1) {
// Reset to the end of history
this.historyIndex = this.commandHistory.length;
}
// Update the input field with the selected command
this.input.value = this.commandHistory[this.historyIndex] || '';
}
// Scroll the terminal to the bottom
scrollToBottom() {
setTimeout(() => {
this.output.scrollTop = this.output.scrollHeight;
}, 0);
}
}
// Bootstrap function to initialize the WASM module and terminal
async function bootstrap() {
await WASM.initialize();
new Terminal();
}
// Start the application
bootstrap();