Some cleanup

This commit is contained in:
Iván Izaguirre committed 2026-02-08 18:04:40 +01:00
1 parent 784bd7baee
commit 635cce512d
21 files changed
+284 -2366

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
# Dependencies
node_modules/
# Build outputs
dist/
public/wasm/*
!public/wasm/.gitkeep
# Development
.vite/
*.log
npm-debug.log*
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# TypeScript
*.tsbuildinfo
+252 -158
View File
@@ -1,166 +1,235 @@
# izapple2 WebAssembly Frontend
A web browser frontend for the izapple2 Apple II emulator, compiled to WebAssembly.
A modern React + TypeScript frontend for the izapple2 Apple II emulator, compiled to WebAssembly.
## Features
- ✅ Runs Apple II emulation in any modern web browser
- ✅ Full keyboard support
- ✅ Modern React + TypeScript + Material-UI interface
- ✅ Full keyboard support (letters, numbers, special keys, arrows)
- ✅ Audio support (speaker clicks and beeps)
- ✅ Multiple disk loading methods:
- File picker (local files)
- Drag and drop
- Load from URLs
- Sample disk library
- ✅ Screenshot download
- ✅ Responsive design (desktop, tablet, mobile)
- ✅ Reset and pause controls
- ✅ Emulator controls:
- Pause/Resume
- Reset
- Speed toggle
- Screen mode selection (NTSC, Plain, Green, Amber)
- Screenshot download
- ✅ Real-time status display (CPU frequency, disk status)
- ✅ Responsive design
- ✅ No installation required
## Technology Stack
- **Frontend**: React 18 + TypeScript + Vite
- **UI Framework**: Material-UI (MUI) 5
- **Emulation Core**: Go + Ebiten (compiled to WebAssembly)
- **Build Tools**: Vite, Go 1.23+
## Requirements
### For Building
- Go 1.16 or later
- Standard build tools (make, bash)
- Go 1.23 or later
- Node.js 18+ and npm
- Bash (for build script)
### For Running (Browser)
- Modern web browser with WebAssembly support:
- Chrome/Edge 57+
- Chrome/Edge 79+
- Firefox 52+
- Safari 11+
- Opera 44+
## Quick Start
### Build
### Initial Setup
```bash
cd frontend/a2wasm
./build.sh
# Install npm dependencies
npm install
# Build the WASM binary
./build-wasm.sh
```
This will:
1. Compile Go code to WebAssembly (`web/izapple2.wasm`)
2. Copy the Go WASM runtime (`web/wasm_exec.js`)
3. Create a compressed version (`web/izapple2.wasm.gz`)
### Run Locally
### Development
```bash
./serve.sh
# Start Vite dev server (with hot reload)
npm run dev
```
Then open http://localhost:8080 in your browser.
Then open http://localhost:5173 in your browser.
The emulator will boot to Applesoft BASIC. Use the file picker or drag-and-drop to load disk images.
The emulator will boot to the `]` prompt. You can then load disk images or start typing commands.
### Production Build
```bash
# Build WASM
./build-wasm.sh
# Build React app for production
npm run build
# Preview production build
npm run preview
```
The production files will be in `dist/`.
## Using the Emulator
### Loading Disk Images
**Method 1: File Picker**
1. Click "Load Disk 1" or "Load Disk 2"
1. Click "Choose File" under Drive 1 or Drive 2
2. Select a disk image file (.dsk, .woz, .nib, .po, .2mg, .zip, .gz)
3. The disk will be loaded and the emulator will boot from it
3. The disk will be loaded into the selected drive
**Method 2: Drag and Drop**
1. Drag a disk image file from your computer
2. Drop it on the emulator screen
3. Multiple files will load into Drive 1 and Drive 2 respectively
2. Drop it on the disk manager panel
3. The first file loads into Drive 1, second into Drive 2
**Method 3: URL Loading**
1. Use URL parameters:
```
http://localhost:8080?disk1=https://example.com/disk.dsk
```
2. Or load from JavaScript:
```javascript
window.loadDiskFromURL(1, "https://example.com/disk.dsk");
```
**Method 4: Sample Disks**
- Click sample disk buttons in the control panel
- These load pre-configured disk images
### Keyboard Shortcuts
| Key | Action |
|-----|--------|
| F1 | Show/Hide help |
| Ctrl+F2 | Reset emulator |
| F4 | Toggle CPU trace |
| F5 | Fast/Normal speed |
| Ctrl+F5 | Show speed/FPS |
| F6 | Next screen mode (NTSC, Green, etc.) |
| F7 | Show/Hide screen pages |
| F10 | Next character set |
| Ctrl+F10 | Show/Hide character generator |
| Shift+F10 | Show/Hide alternate text |
| F12 | Save screenshot |
| Pause | Pause/Resume emulation |
| Left Alt | Open-Apple |
| Right Alt | Closed-Apple |
- Enter a URL in the disk manager (feature available via UI)
- Or use JavaScript:
```javascript
window.wasmAPI.loadDiskFromURL(1, "https://example.com/disk.dsk");
```
### Controls
**Reset Button**: Resets the Apple II (same as Ctrl+F2)
**Top Control Panel**:
- **Pause/Resume**: Freeze/unfreeze emulation
- **Reset**: Reboot the Apple II
- **Toggle Speed**: Switch between normal and fast speed
- **Screenshot**: Download current screen as PNG
- **Screen Mode**: Select display mode (NTSC, Plain, Green, Amber)
**Pause Button**: Pauses/resumes emulation
**Status Bar** (bottom):
- Shows CPU frequency in MHz
- Displays pause state
- Shows keyboard help text
**Screenshot Button**: Captures current screen as PNG and downloads it
### Keyboard
All standard Apple II keys work:
- Letters, numbers, symbols
- Enter, Escape, Backspace
- Arrow keys (Up, Down, Left, Right)
- Tab, Delete
The emulator uses Ebiten's native keyboard handling for accurate input.
## File Structure
```
frontend/a2wasm/
├── main.go # Entry point and Ebiten game loop
├── keyboard.go # Keyboard input handling
├── speaker.go # Audio output
├── disk_loader.go # JavaScript bridge for disk loading (WASM-only)
├── build.sh # Build script
├── serve.sh # Development server script
├── README.md # This file
└── web/ # Web assets
├── index.html # Main HTML page
├── loader.js # WASM initialization and UI logic
├── styles.css # Styling
├── izapple2.wasm # Compiled WASM binary (after build)
└── wasm_exec.js # Go WASM runtime (after build)
├── go/ # Go source code
│ ├── main.go # Ebiten game loop, keyboard handling
│ ├── api.go # JavaScript API exports
│ ├── speaker.go # Audio output
│ └── disk_loader.go # Disk loading (placeholder)
├── src/ # React/TypeScript source
│ ├── main.tsx # React entry point
│ ├── App.tsx # Main app component
│ ├── api/ # WASM API client
│ │ ├── types.ts # TypeScript type definitions
│ │ ├── emulator.ts # Emulator API wrapper
│ │ └── wasm-loader.ts # WASM initialization
│ ├── components/ # React components
│ │ ├── EmulatorScreen.tsx # Canvas container
│ │ ├── ControlPanel.tsx # Top controls
│ │ ├── DiskManager.tsx # Disk operations UI
│ │ ├── StatusBar.tsx # Bottom status display
│ │ └── KeyboardHandler.tsx # Keyboard event capture
│ ├── hooks/ # Custom React hooks
│ │ ├── useEmulator.ts # Emulator state management
│ │ └── useDiskLoader.ts # Disk loading logic
│ └── styles/ # Styling
│ └── theme.ts # MUI theme customization
├── public/ # Static assets
│ └── wasm/ # WASM build output
│ ├── izapple2.wasm # Compiled WASM binary
│ ├── izapple2.wasm.gz # Compressed version
│ └── wasm_exec.js # Go WASM runtime
├── index.html # HTML entry point
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
├── package.json # npm dependencies
├── build-wasm.sh # WASM build script
└── README.md # This file
```
## Architecture
```
┌─────────────────────────────────────────┐
│ React + TypeScript UI │
│ (Controls, Disk Manager, Status Bar) │
├─────────────────────────────────────────┤
│ TypeScript API Client │
│ (wasmAPI wrapper) │
├─────────────────────────────────────────┤
│ Go/WASM Bridge │
│ (Exported JS functions) │
├─────────────────────────────────────────┤
│ Go Emulation + Ebiten Rendering │
│ (Apple II core + Canvas output) │
└─────────────────────────────────────────┘
```
## Deployment
### Static Hosting
The `web/` directory contains everything needed for deployment:
The built application is fully static and can be deployed anywhere:
1. Build the WASM binary:
1. **Build everything**:
```bash
./build.sh
./build-wasm.sh
npm run build
```
2. Upload the `web/` directory to any static hosting service:
- GitHub Pages
2. **Upload the `dist/` directory** to any static hosting:
- Netlify
- Vercel
- GitHub Pages
- AWS S3 + CloudFront
- Your own web server
- Any web server
### Server Configuration
**MIME Types**
Ensure your server is configured with the correct MIME type for WASM:
Ensure your server serves the correct MIME type for WASM:
```
.wasm -> application/wasm
```
**Headers for WASM**
Vite automatically configures these in development:
```
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
```
For production, configure your server/CDN similarly.
**Compression**
Enable gzip or brotli compression for better performance:
Enable gzip or brotli compression:
```nginx
# Nginx example
@@ -170,16 +239,28 @@ location ~ \.wasm$ {
}
```
**CORS**
### Example: Netlify
If loading disk images from external URLs, ensure CORS is properly configured on the source server.
1. Create `netlify.toml`:
```toml
[build]
command = "./build-wasm.sh && npm run build"
publish = "dist"
### Example: GitHub Pages
[[headers]]
for = "/*"
[headers.values]
Cross-Origin-Embedder-Policy = "require-corp"
Cross-Origin-Opener-Policy = "same-origin"
1. Copy `web/` contents to `docs/` in your repository
2. Enable GitHub Pages in repository settings
3. Set source to `docs/` folder
4. Access at: `https://yourusername.github.io/yourrepo/`
[[headers]]
for = "*.wasm"
[headers.values]
Content-Type = "application/wasm"
```
2. Connect your repository to Netlify
3. Deploy!
## Supported Disk Formats
@@ -188,44 +269,62 @@ If loading disk images from external URLs, ensure CORS is properly configured on
- `.nib` - Nibble format
- `.po` - ProDOS order disk image
- `.2mg` - 2IMG format (with header)
- `.zip` - Compressed disk images (will extract first .dsk file)
- `.zip` - Compressed disk images
- `.gz` - Gzipped disk images
## Browser Compatibility
## WASM API Reference
### Desktop
The Go code exports the following API to JavaScript:
| Browser | Version | Status |
|---------|---------|--------|
| Chrome | 57+ | ✅ Fully supported |
| Firefox | 52+ | ✅ Fully supported |
| Safari | 11+ | ✅ Supported |
| Edge | 79+ | ✅ Fully supported |
| Opera | 44+ | ✅ Supported |
```typescript
interface EmulatorAPI {
// Core control
reset(): void;
pause(): void;
resume(): void;
### Mobile
// Disk operations
loadDisk(drive: number, data: Uint8Array, filename: string): Promise<string | null>;
loadDiskFromURL(drive: number, url: string): Promise<string | null>;
| Browser | Status | Notes |
|---------|--------|-------|
| Chrome Android | ✅ Supported | No virtual keyboard yet |
| Safari iOS | ✅ Supported | No virtual keyboard yet |
| Firefox Android | ⚠️ Limited | May have audio issues |
// Input (for advanced use - keyboard is handled automatically)
sendKey(keyCode: number): void;
sendText(text: string): void;
// State queries
isPaused(): boolean;
getFrequency(): number;
getDiskInfo(drive: number): DiskInfo | null;
// Configuration
toggleSpeed(): void;
setScreenMode(mode: 'ntsc' | 'plain' | 'green' | 'amber'): void;
// Screenshot
screenshot(): void;
}
// Access via:
window.wasmAPI.reset();
window.wasmAPI.loadDisk(1, data, "game.dsk");
```
## Performance
- **WASM Size**: ~8-12 MB uncompressed, ~2-3 MB gzipped
- **WASM Size**: ~26 MB uncompressed, ~7 MB gzipped
- **Load Time**: 2-5 seconds on broadband
- **CPU Usage**: ~50-70% of one core on modern hardware
- **Emulation Speed**: 90-100% of native speed
- **Audio Latency**: ~100-200ms (acceptable for emulation)
- **Emulation Speed**: 100% of native speed
- **Frame Rate**: 60 FPS (Ebiten rendering)
## Troubleshooting
### "Error loading WASM"
### "Failed to load emulator"
- Check that `izapple2.wasm` exists in the `web/` directory
- Run `./build.sh` to rebuild
- Check that WASM files exist in `public/wasm/`
- Run `./build-wasm.sh` to rebuild
- Check browser console for detailed errors
- Verify CORS headers are set correctly
### "Disk loading failed"
@@ -235,88 +334,83 @@ If loading disk images from external URLs, ensure CORS is properly configured on
### No audio
- Check browser console for audio context errors
- Some browsers require user interaction before playing audio
- Click the emulator screen to enable audio
### Slow performance
- Close other browser tabs
- Try Chrome or Edge for best WebAssembly performance
- Check CPU usage in browser task manager
- Click the emulator screen to enable audio context
- Check browser console for audio errors
### Keys not working
- Click on the emulator canvas to focus it
- Check that browser extensions aren't intercepting keys
- Try using on-screen buttons as fallback
- Keyboard is handled by Ebiten's native input system
- Ensure no browser extensions are intercepting keys
### Build errors
```bash
# Clean and rebuild
rm -rf node_modules package-lock.json
npm install
./build-wasm.sh
npm run build
```
## Development
### Building for Development
### Project Scripts
```bash
# Build
./build.sh
# Development server with hot reload
npm run dev
# Serve locally
./serve.sh
# Build WASM binary
./build-wasm.sh
# Or use wasmserve (auto-rebuilds)
go run github.com/hajimehoshi/wasmserve@latest .
```
# Build React app for production
npm run build
### JavaScript Bridge API
# Type checking
npm run type-check
The Go code exports these functions to JavaScript:
```javascript
// Load disk from bytes
window.loadDisk(drive, uint8Array, filename)
// Returns: null on success, error string on failure
// Load disk from URL
window.loadDiskFromURL(drive, url)
// Returns: null on success, error string on failure
// Reset emulator
window.resetEmulator()
// Toggle pause
window.togglePause()
// Download screenshot
window.downloadScreenshot()
# Preview production build
npm run preview
```
### Modifying the UI
- Edit `web/index.html` for structure
- Edit `web/styles.css` for styling
- Edit `web/loader.js` for JavaScript logic
- Rebuild not needed for web file changes (just refresh browser)
- Edit files in `src/` - Vite will auto-reload
- React components use Material-UI
- TypeScript ensures type safety
- WASM rebuild only needed for Go changes
### Modifying the Emulator
- Edit files in `go/`
- Run `./build-wasm.sh` to rebuild
- Refresh browser to load new WASM
## Known Limitations
1. **No virtual keyboard** - Mobile devices need external keyboard
2. **No save states** - Page refresh loses state (planned for future)
3. **No gamepad** - Only keyboard and mouse supported
4. **File system access** - Can't modify loaded disks (read-only)
2. **No save states** - Page refresh loses state
3. **Read-only disks** - Can't write to loaded disk images
4. **No gamepad** - Only keyboard supported
## Future Enhancements
- Virtual keyboard overlay for mobile
- Touch joystick support
- Virtual keyboard overlay for mobile devices
- Save/load state with IndexedDB
- PWA support (offline capability)
- Multiplayer over WebRTC
- GamePad API support
- Disk write support
- Multiple screen sizes/zoom levels
- More sample disk library
## Credits
- [izapple2](https://github.com/ivanizag/izapple2) by Ivan Izaguirre
- [Ebiten](https://ebiten.org/) game engine
- Go WebAssembly support
- [React](https://react.dev/) UI library
- [Material-UI](https://mui.com/) component library
- [Vite](https://vitejs.dev/) build tool
## License
-111
View File
@@ -1,111 +0,0 @@
#!/bin/bash
# Build script for izapple2 WebAssembly frontend
set -e
echo "========================================="
echo " izapple2 WebAssembly Build Script"
echo "========================================="
echo ""
# Check if Go is installed
if ! command -v go &> /dev/null; then
echo "Error: Go is not installed or not in PATH"
exit 1
fi
# Check Go version (need 1.16+ for embed)
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
echo "Go version: $GO_VERSION"
# Get GOROOT for wasm_exec.js
GOROOT=$(go env GOROOT)
WASM_EXEC_JS="$GOROOT/misc/wasm/wasm_exec.js"
# Check if wasm_exec.js exists (try alternate locations)
if [ ! -f "$WASM_EXEC_JS" ]; then
WASM_EXEC_JS="$GOROOT/lib/wasm/wasm_exec.js"
fi
if [ ! -f "$WASM_EXEC_JS" ]; then
# Try standard installation location
WASM_EXEC_JS="/usr/local/go/misc/wasm/wasm_exec.js"
fi
if [ ! -f "$WASM_EXEC_JS" ]; then
# Try homebrew location on macOS
WASM_EXEC_JS="/opt/homebrew/opt/go/libexec/misc/wasm/wasm_exec.js"
fi
if [ ! -f "$WASM_EXEC_JS" ]; then
echo "Error: Could not find wasm_exec.js in Go installation"
echo "Searched:"
echo " - $GOROOT/misc/wasm/wasm_exec.js"
echo " - $GOROOT/lib/wasm/wasm_exec.js"
echo " - /usr/local/go/misc/wasm/wasm_exec.js"
echo " - /opt/homebrew/opt/go/libexec/misc/wasm/wasm_exec.js"
exit 1
fi
echo "GOROOT: $GOROOT"
echo ""
# Build WASM binary
echo "Compiling Go to WebAssembly..."
echo "Command: GOOS=js GOARCH=wasm go build -ldflags=\"-s -w\" -o web/izapple2.wasm"
echo ""
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o web/izapple2.wasm
if [ $? -ne 0 ]; then
echo ""
echo "Error: Build failed"
exit 1
fi
# Get file size
WASM_SIZE=$(ls -lh web/izapple2.wasm | awk '{print $5}')
echo ""
echo "✓ Build successful!"
echo " Output: web/izapple2.wasm ($WASM_SIZE)"
echo ""
# Copy wasm_exec.js
echo "Copying wasm_exec.js from Go installation..."
cp "$WASM_EXEC_JS" web/wasm_exec.js
if [ $? -ne 0 ]; then
echo "Error: Failed to copy wasm_exec.js"
exit 1
fi
echo "✓ Copied wasm_exec.js"
echo ""
# Optional: Compress with gzip
if command -v gzip &> /dev/null; then
echo "Compressing WASM binary with gzip..."
gzip -k -f web/izapple2.wasm
GZIP_SIZE=$(ls -lh web/izapple2.wasm.gz | awk '{print $5}')
echo "✓ Created web/izapple2.wasm.gz ($GZIP_SIZE)"
echo ""
fi
# Display file listing
echo "Build output files:"
ls -lh web/ | grep -E '\.(html|js|css|wasm)' || true
echo ""
echo "========================================="
echo " Build Complete!"
echo "========================================="
echo ""
echo "To test locally:"
echo " ./serve.sh"
echo ""
echo "Or manually:"
echo " cd web"
echo " python3 -m http.server 8080"
echo ""
echo "Then open: http://localhost:8080"
echo ""
-5
View File
@@ -55,12 +55,7 @@ func setupAPI(a *izapple2.Apple2, game *Game) {
return nil
}
text := args[0].String()
fmt.Printf("API sendText: '%s' (len=%d)\n", text, len(text))
for i, ch := range text {
fmt.Printf(" char[%d]: '%c' (rune=%d)\n", i, ch, ch)
}
game.keyChannel.PutText(text)
fmt.Println("API sendText: done")
return nil
})
-6
View File
@@ -1,6 +0,0 @@
//go:build js
package main
// Disk loading functionality is now integrated into api.go
// This file kept for potential future expansion
+4 -16
View File
@@ -19,9 +19,9 @@ type Game struct {
speaker *wasmSpeaker
fontSource *text.GoTextFaceSource
updates uint64
screenMode int
keyChannel *izapple2.KeyboardChannel
updates uint64
screenMode int
keyChannel *izapple2.KeyboardChannel
}
const (
@@ -33,9 +33,7 @@ func (g *Game) Update() error {
// Handle keyboard input from Ebiten (AppendInputChars for printable chars)
runes := ebiten.AppendInputChars(nil)
if len(runes) > 0 {
text := string(runes)
fmt.Printf("Ebiten keyboard: '%s'\n", text)
g.keyChannel.PutText(text)
g.keyChannel.PutText(string(runes))
}
// Handle special keys (Enter, arrows, etc.)
@@ -120,13 +118,6 @@ func main() {
}
func ebitenRun(a *izapple2.Apple2) {
// Note: Window functions don't apply in WASM - canvas size is controlled by CSS
// ebiten.SetWindowSize(virtualWidth/2, virtualHeight/2)
// ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
// ebiten.SetWindowTitle("iz-" + a.Name)
fmt.Printf("ebitenRun: Apple2 instance at %p\n", a)
game := &Game{
a: a,
speaker: newWasmSpeaker(),
@@ -134,11 +125,8 @@ func ebitenRun(a *izapple2.Apple2) {
screenMode: a_screen.ScreenModeNTSC,
}
fmt.Printf("ebitenRun: keyChannel at %p\n", game.keyChannel)
// Set up providers
a.SetSpeakerProvider(game.speaker)
// Don't set keyboard provider - NewKeyboardChannel already did it
var err error
game.fontSource, err = text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
-561
View File
@@ -1,561 +0,0 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();
-59
View File
@@ -1,59 +0,0 @@
#!/bin/bash
# Development server for izapple2 WebAssembly frontend
set -e
PORT=8080
echo "========================================="
echo " izapple2 WebAssembly Dev Server"
echo "========================================="
echo ""
# Check if WASM file exists
if [ ! -f "web/izapple2.wasm" ]; then
echo "Error: web/izapple2.wasm not found"
echo ""
echo "Please run ./build.sh first"
exit 1
fi
echo "Starting HTTP server on port $PORT..."
echo ""
echo "Open in browser:"
echo " http://localhost:$PORT"
echo ""
echo "Press Ctrl+C to stop the server"
echo ""
# Try different server options in order of preference
if command -v python3 &> /dev/null; then
# Python 3 HTTP server
echo "Using Python 3 HTTP server"
cd web
python3 -m http.server $PORT
elif command -v python &> /dev/null; then
# Python 2 HTTP server (fallback)
echo "Using Python 2 HTTP server"
cd web
python -m SimpleHTTPServer $PORT
elif command -v go &> /dev/null; then
# Use Go's wasmserve if available
if go list github.com/hajimehoshi/wasmserve &> /dev/null; then
echo "Using wasmserve"
go run github.com/hajimehoshi/wasmserve@latest .
else
echo "Error: No suitable HTTP server found"
echo ""
echo "Please install one of:"
echo " - Python 3 (recommended)"
echo " - Python 2"
echo " - Or run: go install github.com/hajimehoshi/wasmserve@latest"
exit 1
fi
else
echo "Error: No suitable HTTP server found"
echo ""
echo "Please install Python 3 or Go"
exit 1
fi
-5
View File
@@ -20,24 +20,19 @@ async function initWASM(
reject: (error: Error) => void
): Promise<void> {
try {
console.log('Starting WASM initialization...');
const go = new window.Go!();
console.log('Fetching WASM binary...');
const result = await WebAssembly.instantiateStreaming(
fetch('/wasm/izapple2.wasm'),
go.importObject
);
console.log('Running Go WASM module...');
// Run the Go WASM module (this starts the emulator)
go.run(result.instance);
console.log('Waiting for WASM API to be exported...');
// Wait a bit for the emulator to initialize and export functions
setTimeout(() => {
if (window.wasmAPI) {
console.log('WASM initialized successfully, API available:', Object.keys(window.wasmAPI));
resolve();
} else {
console.error('WASM API not found on window object');
@@ -8,10 +8,8 @@ export function EmulatorScreen() {
// Move Ebiten canvas into our container
const checkCanvas = setInterval(() => {
// Look for canvas in body (where Ebiten creates it)
const canvas = document.querySelector('body > canvas');
const canvas = document.querySelector('body > canvas') as HTMLCanvasElement | null;
if (canvas && containerRef.current) {
console.log('Found Ebiten canvas, moving to container');
// Remove canvas from body
if (canvas.parentNode) {
canvas.parentNode.removeChild(canvas);
@@ -39,10 +37,8 @@ export function EmulatorScreen() {
// Make canvas focusable and focus it
canvas.setAttribute('tabindex', '0');
canvas.focus();
console.log('Canvas focused');
clearInterval(checkCanvas);
console.log('Canvas moved and styled successfully');
}
}, 100);
+1 -1
View File
@@ -4,7 +4,7 @@ import App from './App';
// Setup download trigger function for screenshots
window.triggerDownload = (data: Uint8Array, filename: string) => {
const blob = new Blob([data], { type: 'application/octet-stream' });
const blob = new Blob([data as BlobPart], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
-133
View File
@@ -1,133 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>izapple2 - Apple II Emulator</title>
<link rel="stylesheet" href="styles.css">
<script src="wasm_exec.js"></script>
</head>
<body>
<div id="loading-screen">
<div class="loading-content">
<h1>izapple2</h1>
<p>Loading Apple II Emulator...</p>
<div class="spinner"></div>
<p id="loading-status">Initializing WebAssembly...</p>
</div>
</div>
<div id="app-container" style="display: none;">
<header>
<h1>izapple2</h1>
<div class="header-info">
<span id="emulator-status">Ready</span>
</div>
</header>
<div class="main-container">
<div class="control-panel">
<div class="control-section">
<h3>Disk Drives</h3>
<div class="disk-controls">
<button id="load-disk-1" class="btn btn-primary">
<span>📀</span> Load Disk 1
</button>
<button id="load-disk-2" class="btn btn-primary">
<span>📀</span> Load Disk 2
</button>
<input type="file" id="file-input" accept=".dsk,.woz,.nib,.po,.2mg,.zip,.gz" style="display:none"/>
</div>
<div class="disk-status">
<div id="disk-1-status" class="disk-slot">Drive 1: Empty</div>
<div id="disk-2-status" class="disk-slot">Drive 2: Empty</div>
</div>
</div>
<div class="control-section">
<h3>Emulator Control</h3>
<div class="emulator-controls">
<button id="reset-btn" class="btn">
<span>🔄</span> Reset
</button>
<button id="pause-btn" class="btn">
<span id="pause-icon">⏸</span> <span id="pause-text">Pause</span>
</button>
<button id="screenshot-btn" class="btn">
<span>📷</span> Screenshot
</button>
</div>
</div>
<div class="control-section">
<h3>Sample Disks</h3>
<div class="sample-disks">
<button class="btn btn-small sample-disk" data-url="<internal>/dos33.dsk" data-drive="1">
DOS 3.3
</button>
<p class="help-text">Click to load sample disk</p>
</div>
</div>
<div class="control-section help-section">
<h3>Keyboard Shortcuts</h3>
<div class="shortcuts">
<div class="shortcut">
<kbd>F1</kbd>
<span>Show/Hide Help</span>
</div>
<div class="shortcut">
<kbd>Ctrl+F2</kbd>
<span>Reset</span>
</div>
<div class="shortcut">
<kbd>F5</kbd>
<span>Toggle Speed</span>
</div>
<div class="shortcut">
<kbd>F6</kbd>
<span>Screen Mode</span>
</div>
<div class="shortcut">
<kbd>F12</kbd>
<span>Screenshot</span>
</div>
<div class="shortcut">
<kbd>Pause</kbd>
<span>Pause/Resume</span>
</div>
</div>
</div>
</div>
<div class="emulator-container">
<div id="emulator-canvas-container">
<!-- Ebiten canvas will be inserted here -->
</div>
<div id="drop-zone" class="drop-zone">
<div class="drop-zone-content">
<span class="drop-icon">📁</span>
<p>Drop disk image here</p>
<p class="drop-hint">Supports .dsk, .woz, .nib, .po, .2mg, .zip, .gz</p>
</div>
</div>
</div>
</div>
<footer>
<div class="footer-links">
<a href="https://github.com/ivanizag/izapple2" target="_blank">GitHub</a>
<span>•</span>
<a href="#" id="show-help">Keyboard Shortcuts</a>
<span>•</span>
<span id="fps-counter">FPS: --</span>
</div>
<div class="footer-info">
<p>Press <kbd>F1</kbd> in emulator for help</p>
</div>
</footer>
</div>
<script src="loader.js"></script>
</body>
</html>
Binary file not shown.
Binary file not shown.
-315
View File
@@ -1,315 +0,0 @@
// izapple2 WebAssembly Loader and JavaScript Bridge
let wasmReady = false;
let currentDrive = 1;
// Initialize WASM when page loads
window.addEventListener('load', async () => {
console.log('Initializing izapple2 WebAssembly...');
await initWASM();
});
// Initialize the WebAssembly module
async function initWASM() {
const go = new Go();
updateLoadingStatus('Downloading WASM module...');
try {
const result = await WebAssembly.instantiateStreaming(
fetch('izapple2.wasm'),
go.importObject
);
updateLoadingStatus('Starting emulator...');
// Run the Go WASM module
go.run(result.instance);
// Wait a moment for initialization
setTimeout(() => {
wasmReady = true;
hideLoading();
initUI();
// Move Ebiten canvas into our container
moveEbitenCanvas();
console.log('izapple2 WebAssembly ready!');
// Check for URL parameters to load disks
checkURLParameters();
}, 500);
} catch (err) {
console.error('Failed to load WASM:', err);
updateLoadingStatus('Error loading emulator: ' + err.message);
}
}
// Update loading screen status
function updateLoadingStatus(message) {
const statusEl = document.getElementById('loading-status');
if (statusEl) {
statusEl.textContent = message;
}
}
// Hide loading screen and show app
function hideLoading() {
document.getElementById('loading-screen').style.display = 'none';
document.getElementById('app-container').style.display = 'block';
}
// Move Ebiten canvas into our container
function moveEbitenCanvas() {
// Ebiten creates a canvas element and appends it to body
// We need to move it into our container
const checkCanvas = setInterval(() => {
const canvas = document.querySelector('canvas');
if (canvas) {
const container = document.getElementById('emulator-canvas-container');
if (container) {
// Remove canvas from body and add to container
canvas.parentNode.removeChild(canvas);
container.appendChild(canvas);
// Style the canvas to fit properly
canvas.style.maxWidth = '100%';
canvas.style.maxHeight = '100%';
canvas.style.display = 'block';
canvas.style.margin = 'auto';
console.log('Moved Ebiten canvas into container');
clearInterval(checkCanvas);
}
}
}, 100);
// Stop checking after 5 seconds
setTimeout(() => clearInterval(checkCanvas), 5000);
}
// Initialize UI event handlers
function initUI() {
// Disk loading buttons
document.getElementById('load-disk-1').addEventListener('click', () => {
currentDrive = 1;
document.getElementById('file-input').click();
});
document.getElementById('load-disk-2').addEventListener('click', () => {
currentDrive = 2;
document.getElementById('file-input').click();
});
// File input change handler
document.getElementById('file-input').addEventListener('change', handleFileSelect);
// Control buttons
document.getElementById('reset-btn').addEventListener('click', () => {
if (wasmReady && window.resetEmulator) {
window.resetEmulator();
}
});
document.getElementById('pause-btn').addEventListener('click', () => {
if (wasmReady && window.togglePause) {
window.togglePause();
// Note: We'd need to track pause state for UI update
}
});
document.getElementById('screenshot-btn').addEventListener('click', () => {
if (wasmReady && window.downloadScreenshot) {
const result = window.downloadScreenshot();
if (result) {
alert('Screenshot error: ' + result);
}
}
});
// Sample disk buttons
document.querySelectorAll('.sample-disk').forEach(btn => {
btn.addEventListener('click', () => {
const url = btn.dataset.url;
const drive = parseInt(btn.dataset.drive);
loadDiskFromURL(drive, url);
});
});
// Drag and drop setup
setupDragAndDrop();
}
// Handle file selection from file picker
async function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
await loadDiskFile(currentDrive, file);
// Reset file input
event.target.value = '';
}
// Load a disk file into the specified drive
async function loadDiskFile(drive, file) {
if (!wasmReady) {
alert('Emulator not ready yet');
return;
}
console.log(`Loading ${file.name} into drive ${drive}...`);
updateDiskStatus(drive, `Loading ${file.name}...`);
try {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
if (window.loadDisk) {
const result = window.loadDisk(drive, bytes, file.name);
if (result) {
alert('Error loading disk: ' + result);
updateDiskStatus(drive, 'Empty');
} else {
updateDiskStatus(drive, file.name);
console.log(`Successfully loaded disk ${drive}: ${file.name}`);
}
} else {
alert('Error: loadDisk function not available');
updateDiskStatus(drive, 'Empty');
}
} catch (err) {
console.error('Error reading file:', err);
alert('Error reading file: ' + err.message);
updateDiskStatus(drive, 'Empty');
}
}
// Load a disk from URL
function loadDiskFromURL(drive, url) {
if (!wasmReady) {
alert('Emulator not ready yet');
return;
}
console.log(`Loading disk from URL into drive ${drive}: ${url}`);
updateDiskStatus(drive, 'Loading from URL...');
if (window.loadDiskFromURL) {
const result = window.loadDiskFromURL(drive, url);
if (result) {
alert('Error loading disk: ' + result);
updateDiskStatus(drive, 'Empty');
} else {
const filename = url.substring(url.lastIndexOf('/') + 1);
updateDiskStatus(drive, filename);
console.log(`Successfully loaded disk ${drive} from URL`);
}
} else {
alert('Error: loadDiskFromURL function not available');
updateDiskStatus(drive, 'Empty');
}
}
// Update disk status display
function updateDiskStatus(drive, status) {
const statusEl = document.getElementById(`disk-${drive}-status`);
if (statusEl) {
statusEl.textContent = `Drive ${drive}: ${status}`;
}
}
// Setup drag and drop functionality
function setupDragAndDrop() {
const dropZone = document.getElementById('drop-zone');
const emulatorContainer = document.getElementById('emulator-canvas-container');
// Prevent default drag behaviors on the whole page
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight drop zone when dragging over
['dragenter', 'dragover'].forEach(eventName => {
emulatorContainer.addEventListener(eventName, () => {
dropZone.classList.add('drag-over');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
emulatorContainer.addEventListener(eventName, () => {
dropZone.classList.remove('drag-over');
}, false);
});
// Handle dropped files
emulatorContainer.addEventListener('drop', handleDrop, false);
async function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
// Load first file into drive 1, second into drive 2
for (let i = 0; i < Math.min(files.length, 2); i++) {
await loadDiskFile(i + 1, files[i]);
}
}
}
}
// Check URL parameters for disk loading
function checkURLParameters() {
const params = new URLSearchParams(window.location.search);
const disk1 = params.get('disk1');
if (disk1) {
console.log('Loading disk 1 from URL parameter:', disk1);
loadDiskFromURL(1, disk1);
}
const disk2 = params.get('disk2');
if (disk2) {
console.log('Loading disk 2 from URL parameter:', disk2);
loadDiskFromURL(2, disk2);
}
}
// Trigger download of a file (called from Go)
window.triggerDownload = function(data, filename) {
const blob = new Blob([data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log(`Downloaded: ${filename}`);
};
// FPS counter (optional)
setInterval(() => {
const fpsEl = document.getElementById('fps-counter');
if (fpsEl && wasmReady) {
// Note: Actual FPS would need to be exposed from Go
// For now, just show that we're running
fpsEl.textContent = 'Running';
}
}, 1000);
// Log when WASM functions become available
const checkFunctions = setInterval(() => {
if (window.loadDisk && window.loadDiskFromURL) {
console.log('WASM bridge functions available');
clearInterval(checkFunctions);
}
}, 100);
-421
View File
@@ -1,421 +0,0 @@
/* izapple2 WebAssembly Interface Styles */
:root {
--bg-dark: #1a1a1a;
--bg-medium: #2a2a2a;
--bg-light: #3a3a3a;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent-green: #00ff00;
--accent-amber: #ffb000;
--accent-blue: #4a9eff;
--border-color: #444;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
}
/* Loading Screen */
#loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-dark);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-content {
text-align: center;
}
.loading-content h1 {
font-size: 3rem;
color: var(--accent-green);
margin-bottom: 1rem;
font-family: 'Courier New', monospace;
}
.loading-content p {
color: var(--text-secondary);
margin: 0.5rem 0;
}
.spinner {
width: 50px;
height: 50px;
margin: 2rem auto;
border: 4px solid var(--bg-light);
border-top: 4px solid var(--accent-green);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Header */
header {
background: var(--bg-medium);
border-bottom: 2px solid var(--accent-green);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 2rem;
color: var(--accent-green);
font-family: 'Courier New', monospace;
}
.header-info {
color: var(--text-secondary);
}
#emulator-status {
padding: 0.25rem 0.75rem;
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--border-color);
}
/* Main Container */
.main-container {
display: flex;
height: calc(100vh - 120px);
padding: 1rem;
gap: 1rem;
}
/* Control Panel */
.control-panel {
width: 300px;
background: var(--bg-medium);
border-radius: 8px;
padding: 1.5rem;
overflow-y: auto;
border: 1px solid var(--border-color);
}
.control-section {
margin-bottom: 2rem;
}
.control-section h3 {
color: var(--accent-amber);
font-size: 1rem;
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Buttons */
.btn {
width: 100%;
padding: 0.75rem 1rem;
background: var(--bg-light);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.btn:hover {
background: var(--bg-dark);
border-color: var(--accent-green);
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background: linear-gradient(135deg, #2a4a2a, #1a3a1a);
border-color: var(--accent-green);
}
.btn-primary:hover {
background: linear-gradient(135deg, #3a5a3a, #2a4a2a);
}
.btn-small {
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
}
.btn span:first-child {
font-size: 1.2rem;
}
/* Disk Controls */
.disk-controls {
margin-bottom: 1rem;
}
.disk-status {
margin-top: 1rem;
}
.disk-slot {
padding: 0.5rem;
background: var(--bg-dark);
border-radius: 4px;
border: 1px solid var(--border-color);
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: var(--text-secondary);
}
/* Emulator Controls */
.emulator-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.emulator-controls .btn {
margin-bottom: 0;
}
/* Sample Disks */
.sample-disks {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.help-text {
font-size: 0.8rem;
color: var(--text-secondary);
margin-top: 0.5rem;
font-style: italic;
}
/* Keyboard Shortcuts */
.shortcuts {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.shortcut {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
background: var(--bg-dark);
border-radius: 4px;
}
kbd {
padding: 0.25rem 0.5rem;
background: var(--bg-light);
border: 1px solid var(--border-color);
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
min-width: 60px;
text-align: center;
}
.shortcut span {
font-size: 0.85rem;
color: var(--text-secondary);
}
/* Emulator Container */
.emulator-container {
flex: 1;
background: #000;
border-radius: 8px;
position: relative;
overflow: hidden;
border: 2px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
}
#emulator-canvas-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#emulator-canvas-container canvas {
max-width: 100%;
max-height: 100%;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
/* Drop Zone */
.drop-zone {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: none;
align-items: center;
justify-content: center;
pointer-events: none;
}
.drop-zone.drag-over {
display: flex;
pointer-events: all;
}
.drop-zone-content {
text-align: center;
color: var(--accent-green);
}
.drop-icon {
font-size: 4rem;
display: block;
margin-bottom: 1rem;
}
.drop-zone-content p {
font-size: 1.5rem;
margin: 0.5rem 0;
}
.drop-hint {
font-size: 1rem !important;
color: var(--text-secondary);
}
/* Footer */
footer {
background: var(--bg-medium);
border-top: 1px solid var(--border-color);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-links {
display: flex;
gap: 1rem;
align-items: center;
font-size: 0.9rem;
}
.footer-links a {
color: var(--accent-blue);
text-decoration: none;
transition: color 0.2s;
}
.footer-links a:hover {
color: var(--accent-green);
}
.footer-info {
font-size: 0.85rem;
color: var(--text-secondary);
}
#fps-counter {
color: var(--accent-amber);
}
/* Responsive Design */
@media (max-width: 1024px) {
.main-container {
flex-direction: column;
height: auto;
}
.control-panel {
width: 100%;
max-height: 400px;
}
.emulator-container {
min-height: 500px;
}
}
@media (max-width: 768px) {
header {
padding: 0.75rem 1rem;
}
header h1 {
font-size: 1.5rem;
}
.main-container {
padding: 0.5rem;
}
.control-panel {
padding: 1rem;
}
.emulator-controls {
grid-template-columns: 1fr;
}
footer {
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
.shortcuts {
display: none;
}
}
/* Scrollbar Styling */
.control-panel::-webkit-scrollbar {
width: 8px;
}
.control-panel::-webkit-scrollbar-track {
background: var(--bg-dark);
}
.control-panel::-webkit-scrollbar-thumb {
background: var(--bg-light);
border-radius: 4px;
}
.control-panel::-webkit-scrollbar-thumb:hover {
background: var(--border-color);
}
-561
View File
@@ -1,561 +0,0 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();
-9
View File
@@ -37,8 +37,6 @@ var macOptionSubst = []rune("!·$%/()=qwertyopasdfghjkxcvbm,.<>cnQWETPGJKLZXVNM\
// PutRune sends a rune to the emulator if it is valid printable ASCII
func (k *KeyboardChannel) PutRune(ch rune) {
println("PutRune called with ch:", ch, "char:", string(ch))
// Some substitutions useful for Macs that transform chars with the option key
pos := slices.Index(macOptionChars, ch)
if pos >= 0 {
@@ -47,22 +45,16 @@ func (k *KeyboardChannel) PutRune(ch rune) {
// We will use computed text only for printable ASCII chars
if ch >= ' ' && ch <= '~' {
println(" ch is printable ASCII, forceCaps:", k.a.IsForceCaps())
if k.a.IsForceCaps() && ch >= 'a' && ch <= 'z' {
ch = unicode.ToUpper(ch)
}
println(" calling PutChar with:", uint8(ch))
k.PutChar(uint8(ch))
} else {
println(" ch NOT printable ASCII, skipping")
}
}
// PutChar sends a character to the emulator
func (k *KeyboardChannel) PutChar(ch uint8) {
println("PutChar: sending", ch, "to channel, buffer:", len(k.keyChannel), "/", cap(k.keyChannel))
k.keyChannel <- ch
println("PutChar: sent")
}
// GetKey returns a pressed key if available
@@ -70,7 +62,6 @@ func (k *KeyboardChannel) GetKey(_ bool) (key uint8, ok bool) {
select {
case key = <-k.keyChannel:
ok = true
println("GetKey: returning key", key, "char:", string(key))
default:
ok = false
}