diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..789b271
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,28 @@
+FROM node:22.14.0-alpine
+
+WORKDIR /app
+
+# Install pnpm
+RUN npm install -g pnpm
+
+# Copy package.json and pnpm-lock.yaml
+COPY package.json pnpm-lock.yaml* ./
+
+# Install dependencies
+RUN pnpm install
+
+# Copy the rest of the application code
+COPY . .
+
+# Create a volume mount point for sensitive data
+VOLUME /app/data
+
+# Expose the port the app runs on
+#EXPOSE 5000
+
+# Create a startup script that initializes the database and starts the application
+COPY docker-entrypoint.sh /app/
+RUN chmod +x /app/docker-entrypoint.sh
+
+# Command to run the startup script
+CMD ["/app/docker-entrypoint.sh"]
diff --git a/data/nextreg_bare.txt b/data/nextreg_records.txt
similarity index 99%
rename from data/nextreg_bare.txt
rename to data/nextreg_records.txt
index 187bff6..563a543 100644
--- a/data/nextreg_bare.txt
+++ b/data/nextreg_records.txt
@@ -1,10 +1,10 @@
0x00 (00) => Machine ID
(R)
0000 1000 = EMULATORS
-
+//
0000 1010 = ZX Spectrum Next
1111 1010 = ZX Spectrum Next Anti-brick
-
+//
1001 1010 = ZX Spectrum Next Core on UnAmiga Reloaded
1010 1010 = ZX Spectrum Next Core on UnAmiga
1011 1010 = ZX Spectrum Next Core on SiDi
@@ -40,7 +40,7 @@
** These signals are ignored if the multiface, divmmc, dma or external nmi master is active
** Copper cannot clear these bits
** An i/o trap could occur at the same time as mf / divmmc cause; always check this bit in nmi isr if important
-
+
0x03 (03) => Machine Type
(R)
bit 7 = nextreg 0x44 second byte indicator
@@ -1234,6 +1234,4 @@ progress is made in the main program.
bits 7:0 = MSB data connected to XADC DRP data bus D15:8
* DRP reads store result here, DRP writes take value from here
---
-
0xFF (255) => Reserved for internal use
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
new file mode 100644
index 0000000..916e503
--- /dev/null
+++ b/docker-entrypoint.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -e
+
+# Start the application
+echo "Starting the application..."
+exec pnpm start
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
Binary files a/src/app/favicon.ico and /dev/null differ
diff --git a/src/app/globals.css b/src/app/globals.css
index a6f1d74..a3416ee 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -46,4 +46,13 @@ a {
color: blue;
margin-left: 4px;
font-weight: bold;
+}
+
+.bits-table th:first-child,
+.bits-table td:first-child {
+ width: 120px;
+}
+
+.bits-table td:last-child {
+ white-space: pre-wrap;
}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 29dee48..b01ab5a 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
+import Link from 'next/link';
import "./globals.css";
import "bootstrap/dist/css/bootstrap.min.css";
@@ -14,8 +15,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Spectrum Next Registers",
+ description: "A platform for exploring the Spectrum Next registers",
};
export default function RootLayout({
@@ -26,7 +27,27 @@ export default function RootLayout({
return (
- {children}
+
+
+
Next Explorer
+
+
+
+
+
+
+ Home
+
+
+ Registers
+
+
+
+
+
+
+ {children}
+
);
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 627d25d..86aeddb 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,98 +1,15 @@
-import Image from "next/image";
import styles from "./page.module.css";
+import Link from 'next/link';
export default function Home() {
return (
-
-
-
- Get started by editing src/app/page.tsx.
-
- Save and see your changes instantly.
-
- Explore the Spectrum Next Registers .
-
-
-
+
+ Register Explorer →
+
-
);
}
diff --git a/src/app/registers/RegisterBrowser.tsx b/src/app/registers/RegisterBrowser.tsx
index 7ec30b8..b6f6b9f 100644
--- a/src/app/registers/RegisterBrowser.tsx
+++ b/src/app/registers/RegisterBrowser.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
+import Link from 'next/link';
import { Register, RegisterAccess, Note } from './types';
import { Form, Card, Container, Row, Col, Tabs, Tab, Table, OverlayTrigger, Tooltip } from 'react-bootstrap';
@@ -8,7 +9,7 @@ interface RegisterBrowserProps {
registers: Register[];
}
-function renderAccess(access: RegisterAccess) {
+export function renderAccess(access: RegisterAccess) {
const renderTooltip = (notes: Note[]) => (
{notes.map((note, index) => (
@@ -19,32 +20,35 @@ function renderAccess(access: RegisterAccess) {
return (
<>
-
-
-
- Bits
- Description
-
-
-
- {access.operations.map((op, index) => {
- const notes = access.notes.filter(note => note.ref === op.footnoteRef);
- return (
-
- {op.bits}
-
- {op.description}
- {op.footnoteRef && notes.length > 0 && (
-
- {op.footnoteRef}
-
- )}
-
-
- );
- })}
-
-
+ {access.description && {access.description} }
+ {access.operations.length > 0 &&
+
+
+
+ Bits
+ Description
+
+
+
+ {access.operations.map((op, index) => {
+ const notes = access.notes.filter(note => note.ref === op.footnoteRef);
+ return (
+
+ {op.bits}
+
+ {op.description}
+ {op.footnoteRef && notes.length > 0 && (
+
+ {op.footnoteRef}
+
+ )}
+
+
+ );
+ })}
+
+
+ }
{access.notes.map((note, index) => (
{note.ref} {note.text}
))}
@@ -84,17 +88,25 @@ export default function RegisterBrowser({ registers }: RegisterBrowserProps) {
- {register.name} ({register.hex_address} / {register.dec_address})
+ {register.hex_address} ( {register.dec_address} )
+ {register.name} {register.issue_4_only && Issue 4 Only }
+
- {register.common && {renderAccess(register.common)} }
+ {register.common && {renderAccess(register.common)} }
{register.read && {renderAccess(register.read)} }
{register.write && {renderAccess(register.write)} }
{register.notes.map((note, index) => (
{note.ref} {note.text}
))}
+ {register.text && register.text.length > 0 && (
+
+
Notes:
+
{register.text}
+
+ )}
diff --git a/src/app/registers/RegisterDetailClient.tsx b/src/app/registers/RegisterDetailClient.tsx
new file mode 100644
index 0000000..2ebd692
--- /dev/null
+++ b/src/app/registers/RegisterDetailClient.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { Container, Row, Col, Card, Tabs, Tab } from 'react-bootstrap';
+import { Register } from './types';
+import { renderAccess } from './RegisterBrowser';
+
+export default function RegisterDetailClient({
+ register,
+ defaultActiveKey,
+}: {
+ register: Register;
+ defaultActiveKey?: string;
+}) {
+ return (
+
+
+
+
+
+ {register.name} ({register.hex_address} / {register.dec_address}){' '}
+ {register.issue_4_only && Issue 4 Only }
+
+
+ {defaultActiveKey ? (
+
+ {register.common && (
+
+ {renderAccess(register.common)}
+
+ )}
+ {register.read && (
+
+ {renderAccess(register.read)}
+
+ )}
+ {register.write && (
+
+ {renderAccess(register.write)}
+
+ )}
+
+ ) : null}
+ {register.notes.map((note, index) => (
+
+ {note.ref} {note.text}
+
+ ))}
+ {register.text && register.text.length > 0 && (
+
+
Notes:
+
{register.text}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/app/registers/[hex]/page.tsx b/src/app/registers/[hex]/page.tsx
new file mode 100644
index 0000000..e148db4
--- /dev/null
+++ b/src/app/registers/[hex]/page.tsx
@@ -0,0 +1,183 @@
+import { promises as fs } from 'fs';
+import path from 'path';
+import { notFound } from 'next/navigation';
+import Link from 'next/link';
+import { Register, RegisterAccess } from '../../registers/types';
+import RegisterDetailClient from '../../registers/RegisterDetailClient';
+
+async function parseNextReg(fileContent: string): Promise {
+ const registers: Register[] = [];
+ const paragraphs = fileContent.split(/\n\s*\n/);
+
+ for (const paragraph of paragraphs) {
+ if (!paragraph.trim()) {
+ continue;
+ }
+ processRegisterBlock(paragraph, registers);
+ }
+
+ return registers;
+}
+
+function processRegisterBlock(paragraph: string, registers: Register[]) {
+ const lines = paragraph.trim().split('\n');
+ const firstLine = lines[0];
+
+ const registerMatch = firstLine.match(/([0-9a-fA-F,x]+)\s*\((.*?)\)\s*=>\s*(.*)/);
+
+ if (!registerMatch) {
+ return;
+ }
+
+ const hexAddresses = registerMatch[1].trim();
+ const decAddresses = registerMatch[2].trim();
+ const name = registerMatch[3] ? registerMatch[3].trim() : '';
+ const description = lines.slice(1).join('\n').trim();
+
+ const hexList = hexAddresses.split(',').map(h => h.trim());
+ const decList = decAddresses.includes('-') ? decAddresses.split('-') : decAddresses.split(',').map(d => d.trim());
+
+ const createRegister = (hex: string, dec: string | number, regName: string): Register => {
+ const reg: Register = {
+ hex_address: hex,
+ dec_address: dec,
+ name: regName,
+ description: description,
+ notes: [],
+ text: "",
+ issue_4_only: false
+ };
+
+ const descriptionLines = description.split('\n');
+ let currentAccess: 'read' | 'write' | 'common' | null = null;
+ let accessData: RegisterAccess = { operations: [], notes: [] };
+
+ for (const line of descriptionLines) {
+ if(line.includes('Issue 4 Only')) reg.issue_4_only = true;
+
+ const trimmedLine = line.trim();
+
+ if (trimmedLine.startsWith('//')) continue;
+
+ if (trimmedLine.startsWith('(R)')) {
+ if (currentAccess) reg[currentAccess] = accessData;
+ accessData = { operations: [], notes: [] };
+ currentAccess = 'read';
+ continue;
+ }
+ if (trimmedLine.startsWith('(W)')) {
+ if (currentAccess) reg[currentAccess] = accessData;
+ accessData = { operations: [], notes: [] };
+ currentAccess = 'write';
+ continue;
+ }
+ if (trimmedLine.startsWith('(R/W')) {
+ if (currentAccess) reg[currentAccess] = accessData;
+ accessData = { operations: [], notes: [] };
+ currentAccess = 'common';
+ continue;
+ }
+ if (line.startsWith(trimmedLine)) {
+ if (currentAccess) reg[currentAccess] = accessData;
+ accessData = { operations: [], notes: [] };
+ currentAccess = null;
+ }
+
+ if (currentAccess) {
+ const bitMatch = trimmedLine.match(/^(bits?|bit)\s+([\d:-]+)\s*=\s*(.*)/);
+ const valueMatch = !line.match(/^\s+/) && trimmedLine.match(/^([01\s]+)\s*=\s*(.*)/);
+
+ if (bitMatch) {
+ let bitDescription = bitMatch[3];
+ const footnoteMatch = bitDescription.match(/(\*+)$/);
+ let footnoteRef: string | undefined = undefined;
+ if (footnoteMatch) {
+ footnoteRef = footnoteMatch[1];
+ bitDescription = bitDescription.substring(0, bitDescription.length - footnoteRef.length).trim();
+ }
+ accessData.operations.push({
+ bits: bitMatch[2],
+ description: bitDescription,
+ footnoteRef: footnoteRef,
+ });
+ } else if (valueMatch) {
+ accessData.operations.push({
+ bits: valueMatch[1].trim().replace(/\s/g, ''),
+ description: valueMatch[2].trim(),
+ });
+ } else if (trimmedLine.startsWith('*')) {
+ const noteMatch = trimmedLine.match(/^(\*+)\s*(.*)/);
+ if (noteMatch) {
+ accessData.notes.push({
+ ref: noteMatch[1],
+ text: noteMatch[2],
+ });
+ }
+ } else if (trimmedLine) {
+ if (line.match(/^\s+/) && accessData.operations.length > 0) {
+ accessData.operations[accessData.operations.length - 1].description += `\n${line}`;
+ } else {
+
+ if (!accessData.description) {
+ accessData.description = '';
+ }
+ accessData.description += `\n${trimmedLine}`;
+ }
+ }
+ } else {
+ if (trimmedLine.startsWith('*')) {
+ const noteMatch = trimmedLine.match(/^(\*+)\s*(.*)/);
+ if (noteMatch) {
+ (reg as Register).notes.push({
+ ref: noteMatch[1],
+ text: noteMatch[2],
+ });
+ }
+ }
+ else {
+ reg.text += `${line}\n`;
+ }
+ }
+ }
+ if (currentAccess) {
+ (reg as Register)[currentAccess] = accessData;
+ }
+
+ return reg;
+ };
+
+ if (hexList.length > 1) {
+ for (let i = 0; i < hexList.length; i++) {
+ const hexAddr = hexList[i];
+ const decAddr = (decList as string[])[i] || decAddresses;
+ const dec = isNaN(parseInt(decAddr, 10)) ? (decAddr) : parseInt(decAddr, 10);
+ registers.push(createRegister(hexAddr, dec, `${name} (${hexAddr})`));
+ }
+ } else {
+ const dec = isNaN(parseInt(decAddresses, 10)) ? (decAddresses) : parseInt(decAddresses, 10);
+ registers.push(createRegister(hexAddresses, dec, name));
+ }
+}
+
+export default async function RegisterDetailPage({ params }: { params: { hex: string } }) {
+ const filePath = path.join(process.cwd(), 'data', 'nextreg_records.txt');
+ const fileContent = await fs.readFile(filePath, 'utf8');
+ const registers = await parseNextReg(fileContent);
+
+ const targetHex = decodeURIComponent((await params).hex).toLowerCase();
+
+ const register = registers.find(r => r.hex_address.toLowerCase() === targetHex);
+
+ if (!register) return notFound();
+
+ const defaultActiveKey = register.common ? 'common' : (register.read ? 'read' : (register.write ? 'write' : undefined));
+
+ return (
+
+
+ ← Back to Registers
+
+
+
+ );
+}
diff --git a/src/app/registers/page.tsx b/src/app/registers/page.tsx
index 07f87ce..e1651f5 100644
--- a/src/app/registers/page.tsx
+++ b/src/app/registers/page.tsx
@@ -42,6 +42,8 @@ function processRegisterBlock(paragraph: string, registers: Register[]) {
name: regName,
description: description,
notes: [],
+ text: "",
+ issue_4_only: false
};
const descriptionLines = description.split('\n');
@@ -49,28 +51,40 @@ function processRegisterBlock(paragraph: string, registers: Register[]) {
let accessData: RegisterAccess = { operations: [], notes: [] };
for (const line of descriptionLines) {
+ if(line.includes('Issue 4 Only')) reg.issue_4_only = true;
+
const trimmedLine = line.trim();
- if (trimmedLine === '(R)') {
+
+ if (trimmedLine.startsWith('//')) continue;
+
+ if (trimmedLine.startsWith('(R)')) {
if (currentAccess) reg[currentAccess] = accessData;
accessData = { operations: [], notes: [] };
currentAccess = 'read';
continue;
}
- if (trimmedLine === '(W)') {
+ if (trimmedLine.startsWith('(W)')) {
if (currentAccess) reg[currentAccess] = accessData;
accessData = { operations: [], notes: [] };
currentAccess = 'write';
continue;
}
- if (trimmedLine === '(R/W)') {
+ if (trimmedLine.startsWith('(R/W')) {
if (currentAccess) reg[currentAccess] = accessData;
accessData = { operations: [], notes: [] };
currentAccess = 'common';
continue;
}
+ if (line.startsWith(trimmedLine)) {
+ if (currentAccess) reg[currentAccess] = accessData;
+ accessData = { operations: [], notes: [] };
+ currentAccess = null;
+ }
if (currentAccess) {
const bitMatch = trimmedLine.match(/^(bits?|bit)\s+([\d:-]+)\s*=\s*(.*)/);
+ const valueMatch = !line.match(/^\s+/) && trimmedLine.match(/^([01\s]+)\s*=\s*(.*)/);
+
if (bitMatch) {
let bitDescription = bitMatch[3];
const footnoteMatch = bitDescription.match(/(\*+)$/);
@@ -84,6 +98,11 @@ function processRegisterBlock(paragraph: string, registers: Register[]) {
description: bitDescription,
footnoteRef: footnoteRef,
});
+ } else if (valueMatch) {
+ accessData.operations.push({
+ bits: valueMatch[1].trim().replace(/\s/g, ''),
+ description: valueMatch[2].trim(),
+ });
} else if (trimmedLine.startsWith('*')) {
const noteMatch = trimmedLine.match(/^(\*+)\s*(.*)/);
if (noteMatch) {
@@ -92,10 +111,16 @@ function processRegisterBlock(paragraph: string, registers: Register[]) {
text: noteMatch[2],
});
}
- } else if(trimmedLine) {
- if(accessData.operations.length > 0) {
- accessData.operations[accessData.operations.length-1].description += `\n${trimmedLine}`;
+ } else if (trimmedLine) {
+ if (line.match(/^\s+/) && accessData.operations.length > 0) {
+ accessData.operations[accessData.operations.length - 1].description += `\n${line}`;
+ } else {
+
+ if (!accessData.description) {
+ accessData.description = '';
}
+ accessData.description += `\n${trimmedLine}`;
+ }
}
} else {
if (trimmedLine.startsWith('*')) {
@@ -107,6 +132,9 @@ function processRegisterBlock(paragraph: string, registers: Register[]) {
});
}
}
+ else {
+ reg.text += `${line}\n`;
+ }
}
}
if (currentAccess) {
@@ -130,18 +158,17 @@ function processRegisterBlock(paragraph: string, registers: Register[]) {
}
-import { Container } from 'react-bootstrap';
export default async function RegistersPage() {
- const filePath = path.join(process.cwd(), 'data', 'nextreg_bare.txt');
+ const filePath = path.join(process.cwd(), 'data', 'nextreg_records.txt');
const fileContent = await fs.readFile(filePath, 'utf8');
const registers = await parseNextReg(fileContent);
return (
-
+
Spectrum Next Registers
-
+
);
}
diff --git a/src/app/registers/types.ts b/src/app/registers/types.ts
index 89eb6fb..5ff646a 100644
--- a/src/app/registers/types.ts
+++ b/src/app/registers/types.ts
@@ -11,10 +11,10 @@
}
export interface RegisterAccess {
+ description?: string;
operations: BitwiseOperation[];
notes: Note[];
}
-
export interface Register {
hex_address: string;
dec_address: number | string;
@@ -23,5 +23,7 @@
read?: RegisterAccess;
write?: RegisterAccess;
common?: RegisterAccess;
+ text: string;
notes: Note[];
+ issue_4_only: boolean;
}
diff --git a/src/app/utils/parser.ts b/src/app/utils/parser.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/middleware.js b/src/middleware.js
new file mode 100644
index 0000000..f77cc69
--- /dev/null
+++ b/src/middleware.js
@@ -0,0 +1,12 @@
+import { NextResponse } from 'next/server'
+
+export function middleware(request) {
+ const { method, nextUrl } = request
+
+ // Filter out internal Next.js assets if desired
+ if (!nextUrl.pathname.startsWith('/_next')) {
+ console.log(`${method} ${nextUrl.pathname}`)
+ }
+
+ return NextResponse.next()
+}
\ No newline at end of file