Trying to get a dokku build :)

This commit is contained in:
2025-10-07 22:01:08 +01:00
parent 365bbb11f9
commit 0aae6aebfd
14 changed files with 410 additions and 135 deletions

28
Dockerfile Normal file
View File

@@ -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"]

View File

@@ -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

6
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -e
# Start the application
echo "Starting the application..."
exec pnpm start

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -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;
}

View File

@@ -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 (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
<nav className="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
<div className="container-fluid">
<Link className="navbar-brand" href="/">Next Explorer</Link>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className="nav-item">
<Link className="nav-link" href="/">Home</Link>
</li>
<li className="nav-item">
<Link className="nav-link" href="/registers">Registers</Link>
</li>
</ul>
</div>
</div>
</nav>
<div className="container-fluid py-3">
{children}
</div>
</body>
</html>
);

View File

@@ -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 (
<div className={styles.page}>
<main className={styles.main}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol>
<li>
Get started by editing <code>src/app/page.tsx</code>.
</li>
<li>Save and see your changes instantly.</li>
<li>
Explore the <a href="/registers">Spectrum Next Registers</a>.
</li>
</ol>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
className={styles.secondary}
>
Read our docs
</a>
</div>
<Link href="/registers">
Register Explorer
</Link>
</main>
<footer className={styles.footer}>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

View File

@@ -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[]) => (
<Tooltip id="tooltip">
{notes.map((note, index) => (
@@ -19,32 +20,35 @@ function renderAccess(access: RegisterAccess) {
return (
<>
<Table striped bordered hover size="sm" className="bits-table">
<thead>
<tr>
<th>Bits</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{access.operations.map((op, index) => {
const notes = access.notes.filter(note => note.ref === op.footnoteRef);
return (
<tr key={index}>
<td>{op.bits}</td>
<td>
{op.description}
{op.footnoteRef && notes.length > 0 && (
<OverlayTrigger placement="top" overlay={renderTooltip(notes)}>
<span className="footnote-ref">{op.footnoteRef}</span>
</OverlayTrigger>
)}
</td>
</tr>
);
})}
</tbody>
</Table>
{access.description && <pre>{access.description}</pre>}
{access.operations.length > 0 &&
<Table striped bordered hover size="sm" className="bits-table">
<thead>
<tr>
<th>Bits</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{access.operations.map((op, index) => {
const notes = access.notes.filter(note => note.ref === op.footnoteRef);
return (
<tr key={index}>
<td>{op.bits}</td>
<td>
{op.description}
{op.footnoteRef && notes.length > 0 && (
<OverlayTrigger placement="top" overlay={renderTooltip(notes)}>
<span className="footnote-ref">{op.footnoteRef}</span>
</OverlayTrigger>
)}
</td>
</tr>
);
})}
</tbody>
</Table>
}
{access.notes.map((note, index) => (
<p key={index} className="small text-muted">{note.ref} {note.text}</p>
))}
@@ -84,17 +88,25 @@ export default function RegisterBrowser({ registers }: RegisterBrowserProps) {
<Col key={register.hex_address} xs={12} className="mb-4">
<Card>
<Card.Header>
<strong>{register.name}</strong> ({register.hex_address} / {register.dec_address})
<code>{register.hex_address}</code> ( {register.dec_address} ) <Link href={`https://wiki.specnext.dev/${encodeURIComponent((register.name).replace(' ','_'))}_Register`} className="text-decoration-none">
<strong>{register.name}</strong> {register.issue_4_only && <span className="badge bg-danger">Issue 4 Only</span>}
</Link>
</Card.Header>
<Card.Body>
<Tabs defaultActiveKey={defaultActiveKey} id={`register-tabs-${register.hex_address}`}>
{register.common && <Tab eventKey="common" title="R/W">{renderAccess(register.common)}</Tab>}
{register.common && <Tab eventKey="common" title="Read/Write">{renderAccess(register.common)}</Tab>}
{register.read && <Tab eventKey="read" title="Read">{renderAccess(register.read)}</Tab>}
{register.write && <Tab eventKey="write" title="Write">{renderAccess(register.write)}</Tab>}
</Tabs>
{register.notes.map((note, index) => (
<p key={index} className="small text-muted">{note.ref} {note.text}</p>
))}
{register.text && register.text.length > 0 && (
<div className="mt-3">
<h5>Notes:</h5>
<pre>{register.text}</pre>
</div>
)}
</Card.Body>
</Card>
</Col>

View File

@@ -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 (
<Container fluid className="py-4">
<Row>
<Col xs={12}>
<Card>
<Card.Header>
<strong>{register.name}</strong> ({register.hex_address} / {register.dec_address}){' '}
{register.issue_4_only && <span className="badge bg-danger">Issue 4 Only</span>}
</Card.Header>
<Card.Body>
{defaultActiveKey ? (
<Tabs defaultActiveKey={defaultActiveKey} id={`register-tabs-${register.hex_address}`}>
{register.common && (
<Tab eventKey="common" title="Read/Write">
{renderAccess(register.common)}
</Tab>
)}
{register.read && (
<Tab eventKey="read" title="Read">
{renderAccess(register.read)}
</Tab>
)}
{register.write && (
<Tab eventKey="write" title="Write">
{renderAccess(register.write)}
</Tab>
)}
</Tabs>
) : null}
{register.notes.map((note, index) => (
<p key={index} className="small text-muted">
{note.ref} {note.text}
</p>
))}
{register.text && register.text.length > 0 && (
<div className="mt-3">
<h5>Notes:</h5>
<pre>{register.text}</pre>
</div>
)}
</Card.Body>
</Card>
</Col>
</Row>
</Container>
);
}

View File

@@ -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<Register[]> {
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 (
<div className="py-4">
<div className="mb-3">
<Link href="/registers" className="btn btn-secondary"> Back to Registers</Link>
</div>
<RegisterDetailClient register={register} defaultActiveKey={defaultActiveKey} />
</div>
);
}

View File

@@ -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 (
<Container fluid className="py-4">
<div className="container-fluid py-4">
<h1 className="mb-4">Spectrum Next Registers</h1>
<RegisterBrowser registers={registers} />
</Container>
</div>
);
}

View File

@@ -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;
}

0
src/app/utils/parser.ts Normal file
View File

12
src/middleware.js Normal file
View File

@@ -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()
}