<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ddevmem — Register Map</title>
<script>
(function() {
var t = localStorage.getItem('ddevmem-theme');
if (!t) t = matchMedia('(prefers-color-scheme: light)').matches ? 'g10' : 'g100';
document.documentElement.setAttribute('data-theme', t);
})();
</script>
<style>
:root, [data-theme="g100"] {
--background: #161616;
--layer-01: #262626;
--layer-02: #393939;
--layer-hover: #353535;
--layer-active: #525252;
--field-01: #262626;
--field-02: #393939;
--field-hover: #353535;
--border-subtle: #393939;
--border-strong: #6f6f6f;
--text-primary: #f4f4f4;
--text-secondary: #c6c6c6;
--text-helper: #a8a8a8;
--text-placeholder: #6f6f6f;
--text-on-color: #ffffff;
--link-primary: #78a9ff;
--link-hover: #a6c8ff;
--focus: #ffffff;
--button-primary: #0f62fe;
--button-primary-hover: #0353e9;
--button-primary-active: #002d9c;
--button-secondary: #6f6f6f;
--button-secondary-hover: #5e5e5e;
--button-secondary-active: #393939;
--button-tertiary: #ffffff;
--button-tertiary-hover: #f4f4f4;
--button-tertiary-active: #c6c6c6;
--button-separator: #161616;
--tag-bg-rw: #0043ce; --tag-fg-rw: #d0e2ff;
--tag-bg-ro: #4d5358; --tag-fg-ro: #dde1e6;
--tag-bg-wo: #9f1853; --tag-fg-wo: #ffd6e8;
--support-success: #42be65;
}
[data-theme="g10"] {
--background: #ffffff;
--layer-01: #f4f4f4;
--layer-02: #ffffff;
--layer-hover: #e8e8e8;
--layer-active: #c6c6c6;
--field-01: #f4f4f4;
--field-02: #ffffff;
--field-hover: #e8e8e8;
--border-subtle: #e0e0e0;
--border-strong: #8d8d8d;
--text-primary: #161616;
--text-secondary: #525252;
--text-helper: #6f6f6f;
--text-placeholder: #a8a8a8;
--text-on-color: #ffffff;
--link-primary: #0f62fe;
--link-hover: #0043ce;
--focus: #0f62fe;
--button-primary: #0f62fe;
--button-primary-hover: #0353e9;
--button-primary-active: #002d9c;
--button-secondary: #393939;
--button-secondary-hover: #4c4c4c;
--button-secondary-active: #6f6f6f;
--button-tertiary: #0f62fe;
--button-tertiary-hover: #0353e9;
--button-tertiary-active: #002d9c;
--button-separator: #e0e0e0;
--tag-bg-rw: #d0e2ff; --tag-fg-rw: #0043ce;
--tag-bg-ro: #dde1e6; --tag-fg-ro: #4d5358;
--tag-bg-wo: #ffd6e8; --tag-fg-wo: #9f1853;
--support-success: #24a148;
}
:root {
--spacing-01: .125rem;
--spacing-02: .25rem;
--spacing-03: .5rem;
--spacing-04: .75rem;
--spacing-05: 1rem;
--spacing-06: 1.5rem;
--spacing-07: 2rem;
--spacing-08: 2.5rem;
--spacing-09: 3rem;
--header-h: 3rem;
--sidenav-w: 16rem;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: var(--background); color: var(--text-primary); }
body {
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
line-height: 1.4;
padding: 0;
padding-top: var(--header-h);
-webkit-font-smoothing: antialiased;
transition: background-color 70ms, color 70ms;
}
code, kbd, .mono, input, select, .reg-offset, .hex, .bf-val {
font-family: 'IBM Plex Mono', ui-monospace, 'JetBrains Mono', 'Fira Code', monospace;
}
h1 { font-size: 1.75rem; font-weight: 300; letter-spacing: -.01em; margin-bottom: var(--spacing-02); }
h2 {
font-size: 1.25rem; font-weight: 400; margin: var(--spacing-07) 0 var(--spacing-03);
padding: var(--spacing-03) 0; border-bottom: 1px solid var(--border-subtle);
display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-05);
}
h2 .map-dump { font-size: .75rem; }
.meta { color: var(--text-helper); font-size: .75rem; margin-bottom: var(--spacing-05); letter-spacing: .02em; }
.app-header {
position: fixed; inset: 0 0 auto 0;
height: var(--header-h);
display: flex; align-items: center;
background: var(--background);
border-bottom: 1px solid var(--border-subtle);
z-index: 10;
}
.app-header .brand {
padding: 0 var(--spacing-05);
font-size: .875rem;
font-weight: 600;
letter-spacing: .01em;
color: var(--text-primary);
display: inline-flex; align-items: center; gap: var(--spacing-03);
}
.app-header .brand .brand-sub {
color: var(--text-helper);
font-weight: 400;
}
.app-header .header-actions {
margin-left: auto;
display: inline-flex;
height: 100%;
}
.app-header .header-actions > button {
height: var(--header-h);
width: var(--header-h);
border-radius: 0;
border-color: transparent;
color: var(--text-primary);
}
.app-header .header-actions > button:hover {
background: var(--layer-01);
color: var(--text-primary);
border-color: transparent;
}
.app-header .header-actions > button:focus {
border-color: var(--focus);
box-shadow: inset 0 0 0 1px var(--focus),
inset 0 0 0 2px var(--background);
}
.reg-card {
background: var(--layer-01);
border: 1px solid transparent;
padding: var(--spacing-05) var(--spacing-05);
margin-bottom: 2px;
transition: background-color 70ms, border-color 70ms;
}
.reg-header { display: flex; align-items: center; gap: var(--spacing-04); flex-wrap: wrap; margin-bottom: var(--spacing-02); }
.reg-name { font-weight: 600; font-size: 1rem; color: var(--text-primary); }
.reg-offset { color: var(--text-helper); font-size: .75rem; margin-left: auto; }
.reg-doc { color: var(--text-secondary); font-size: .8125rem; margin: var(--spacing-02) 0 var(--spacing-04); }
.badge {
display: inline-flex;
align-items: center;
height: 18px;
padding: 0 8px;
border: 0;
border-radius: 16px;
font-size: .75rem;
line-height: 1.3334;
font-weight: 400;
letter-spacing: .32px;
text-transform: uppercase;
}
.badge-rw { background: var(--tag-bg-rw); color: var(--tag-fg-rw); }
.badge-ro { background: var(--tag-bg-ro); color: var(--tag-fg-ro); }
.badge-wo { background: var(--tag-bg-wo); color: var(--tag-fg-wo); }
input[type="text"], input:not([type]), select, .bf-input {
background: var(--field-01);
color: var(--text-primary);
border: 0;
border-bottom: 1px solid var(--border-strong);
padding: 6px 10px;
font-size: .8125rem;
height: 2rem;
outline: none;
transition: background-color 70ms, border-color 70ms, outline-color 70ms;
min-width: 0;
}
input::placeholder { color: var(--text-placeholder); }
input:hover, select:hover, .bf-input:hover { background: var(--field-hover); }
input:focus, select:focus, .bf-input:focus {
outline: 2px solid var(--focus);
outline-offset: -2px;
border-bottom-color: transparent;
}
.reg-value input { width: 16ch; }
.bf-input { height: 1.75rem; padding: 4px 8px; width: 9ch; }
.bf-set { display: flex; gap: .5rem; align-items: center; }
.bf-set .bf-input { flex: 1 1 auto; width: auto; min-width: 0; }
.bf-set > button { flex: 0 0 auto; }
select, select.bf-input {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-repeat: no-repeat;
background-position: right .75rem center;
cursor: pointer;
text-overflow: ellipsis;
padding-right: 2rem;
}
select::-ms-expand { display: none; }
select.bf-input {
width: auto;
min-width: 11ch;
height: 1.75rem;
padding: 4px 2rem 4px 8px;
background-position: right .5rem center;
}
.reg-value { display: flex; align-items: center; gap: .5rem; margin: .5rem 0; flex-wrap: wrap; }
.btn-set { display: inline-flex; }
.btn-set > button:not(:first-of-type):not(:focus) {
box-shadow: -1px 0 0 0 var(--button-separator);
}
.btn-set > button:focus + button:not(:focus) { box-shadow: none; }
.reg-value .hex { color: var(--text-secondary); font-size: .8125rem; min-width: 12ch; }
button {
display: inline-flex;
align-items: center;
justify-content: space-between;
text-align: start;
background: var(--button-primary);
color: var(--text-on-color);
border: 1px solid var(--button-primary);
padding: 0 3.1875rem 0 .6875rem;
min-height: 2rem;
min-width: max-content;
max-width: 20rem;
font-family: inherit;
font-size: .875rem;
line-height: 1.28572;
font-weight: 400;
letter-spacing: .16px;
cursor: pointer;
vertical-align: top;
transition: background-color 70ms cubic-bezier(.2,0,.38,.9),
box-shadow 70ms cubic-bezier(.2,0,.38,.9),
border-color 70ms cubic-bezier(.2,0,.38,.9),
color 70ms cubic-bezier(.2,0,.38,.9);
}
button:hover { background: var(--button-primary-hover); border-color: var(--button-primary-hover); }
button:active { background: var(--button-primary-active); border-color: var(--button-primary-active); }
button:focus,
button.btn-secondary:focus,
button.btn-tertiary:focus,
button.btn-ghost:focus,
button.btn-icon-only:focus {
outline: none;
border-color: var(--focus);
box-shadow: inset 0 0 0 1px var(--focus),
inset 0 0 0 2px var(--background);
}
button.btn-secondary {
background: var(--button-secondary);
color: var(--text-on-color);
border-color: var(--button-secondary);
}
button.btn-secondary:hover { background: var(--button-secondary-hover); border-color: var(--button-secondary-hover); }
button.btn-secondary:active { background: var(--button-secondary-active); border-color: var(--button-secondary-active); }
button.btn-tertiary {
background: transparent;
color: var(--button-tertiary);
border-color: var(--button-tertiary);
}
button.btn-tertiary:hover {
background: var(--button-tertiary-hover);
color: var(--background);
border-color: var(--button-tertiary-hover);
}
button.btn-tertiary:active {
background: var(--button-tertiary-active);
color: var(--background);
border-color: var(--button-tertiary-active);
}
button.btn-ghost {
background: transparent;
color: var(--link-primary);
border-color: transparent;
padding: 0 .9375rem;
}
button.btn-ghost:hover { background: var(--layer-hover); color: var(--link-hover); border-color: transparent; }
button.btn-ghost:active { background: var(--layer-active); color: var(--link-hover); border-color: transparent; }
button.btn-icon-only {
width: 2rem; height: 2rem;
padding: 0;
justify-content: center;
}
button.btn-icon-only svg { width: 16px; height: 16px; fill: currentColor; }
.bitfields { margin-top: .75rem; }
.bf-table { width: 100%; border-collapse: collapse; font-size: .8125rem; background: var(--layer-01); }
.bf-table th, .bf-table td { padding: .5rem .75rem; text-align: left; vertical-align: middle; border: 0; }
.bf-table thead th {
background: var(--layer-02); color: var(--text-secondary);
font-weight: 600; font-size: .75rem; letter-spacing: .02em;
border-bottom: 1px solid var(--border-subtle);
}
.bf-table tbody tr { border-bottom: 1px solid var(--border-subtle); }
.bf-table tbody tr:last-child { border-bottom: 0; }
.bf-table tbody tr:hover { background: var(--layer-hover); }
.bf-val { color: var(--text-primary); font-weight: 400; }
.bf-doc { color: var(--text-helper); }
.error { color: #da1e28; font-size: .75rem; }
.status { font-size: .75rem; color: var(--text-helper); margin-left: .5rem; }
.toolbar {
display: flex; align-items: center; gap: var(--spacing-03);
margin: var(--spacing-05) 0 var(--spacing-06);
flex-wrap: wrap;
}
.toolbar .spacer { flex: 1; }
.toolbar label { font-size: .8125rem; color: var(--text-secondary); cursor: pointer; user-select: none; display: inline-flex; align-items: center; gap: var(--spacing-03); }
input[type="checkbox"] {
appearance: none; -webkit-appearance: none;
width: 1rem; height: 1rem;
background: transparent;
border: 1px solid var(--text-secondary);
margin: 0; padding: 0;
position: relative; cursor: pointer;
transition: background-color 70ms, border-color 70ms;
}
input[type="checkbox"]:hover { border-color: var(--text-primary); }
input[type="checkbox"]:checked { background: var(--text-primary); border-color: var(--text-primary); }
input[type="checkbox"]:checked::after {
content: ''; position: absolute;
left: 3px; top: 0px;
width: 5px; height: 9px;
border: solid var(--background);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
input[type="checkbox"]:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
.layout { display: block; }
.sidebar {
position: fixed;
top: var(--header-h);
left: 0;
width: var(--sidenav-w);
height: calc(100vh - var(--header-h));
overflow-y: auto;
background: var(--layer-01);
border-right: 1px solid var(--border-subtle);
padding: var(--spacing-03) 0;
display: none;
z-index: 5;
}
.sidebar.visible { display: block; }
.sidebar a {
display: flex; align-items: baseline; gap: var(--spacing-03);
padding: var(--spacing-02) var(--spacing-05);
min-height: 2rem;
color: var(--text-secondary); text-decoration: none;
font-size: .8125rem;
white-space: nowrap; overflow: hidden;
border-left: 3px solid transparent;
transition: background-color 70ms, color 70ms, border-color 70ms;
}
.sidebar a .nav-name { overflow: hidden; text-overflow: ellipsis; }
.sidebar a .nav-addr {
margin-left: auto;
font-family: 'IBM Plex Mono', ui-monospace, monospace;
font-size: .6875rem; color: var(--text-helper);
flex-shrink: 0;
}
.sidebar a:hover .nav-addr,
.sidebar a.active .nav-addr { color: var(--text-secondary); }
.sidebar a:hover { background: var(--layer-hover); color: var(--text-primary); }
.sidebar a:focus {
outline: 2px solid var(--focus);
outline-offset: -2px;
}
.sidebar a.active {
background: var(--layer-02); color: var(--text-primary);
border-left-color: var(--button-primary);
font-weight: 600;
}
.sidebar .nav-heading {
color: var(--text-helper);
font-weight: 600; font-size: .6875rem; letter-spacing: .04em; text-transform: uppercase;
padding: var(--spacing-04) var(--spacing-05) var(--spacing-01);
}
.sidebar .nav-base {
font-family: 'IBM Plex Mono', ui-monospace, monospace;
font-size: .6875rem; color: var(--text-helper);
padding: 0 var(--spacing-05) var(--spacing-02);
text-transform: none; letter-spacing: 0; font-weight: 400;
}
.sidebar::-webkit-scrollbar { width: 8px; }
.sidebar::-webkit-scrollbar-thumb { background: var(--border-subtle); }
.sidebar::-webkit-scrollbar-track { background: transparent; }
.main-content {
padding: var(--spacing-06) var(--spacing-07);
min-width: 0;
transition: margin-left 70ms;
}
body:has(.sidebar.visible) .main-content { margin-left: var(--sidenav-w); }
@media (max-width: 720px) {
.main-content { padding: var(--spacing-05); }
body:has(.sidebar.visible) .main-content { margin-left: 0; }
.sidebar.visible { display: none; }
}
a { color: var(--link-primary); }
a:hover { color: var(--link-hover); }
option {
background: var(--layer-hover);
color: var(--text-primary);
}
::selection { background: var(--button-primary); color: var(--text-on-color); }
</style>
</head>
<body>
<header class="app-header" role="banner">
<span class="brand" id="brand">
ddevmem
<span class="brand-sub">· Register Map</span>
</span>
<div class="header-actions">
<button class="btn-ghost btn-icon-only" id="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme" title="Toggle theme"></button>
</div>
</header>
<div class="layout">
<nav class="sidebar" id="sidebar"></nav>
<div class="main-content">
<h1 id="title">ddevmem — Register Map</h1>
<div class="meta" id="meta"></div>
<div class="toolbar">
<button class="btn-ghost" onclick="refreshAll()">Refresh all</button>
<button class="btn-ghost" onclick="dumpAll()">Dump all</button>
<label><input type="checkbox" id="auto-refresh"> Auto-refresh (1s)</label>
<span class="status" id="global-status"></span>
</div>
<div id="maps"></div>
</div>
</div>
<script>
const API_BASE = window.location.pathname.replace(/\/?$/, '/') + 'api';
let allMaps = []; let autoTimer = null;
const $ = id => document.getElementById(id);
const ce = tag => document.createElement(tag);
async function api(path, body) {
const opts = body !== undefined
? { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }
: {};
const r = await fetch(API_BASE + path, opts);
if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
if (r.headers.get('content-type')?.includes('json')) return r.json();
}
function hexStr(val, bits) {
const digits = Math.ceil(bits / 4);
return '0x' + BigInt(val).toString(16).toUpperCase().padStart(digits, '0');
}
function extractBits(val, lo, hi) {
const v = BigInt(val);
const width = BigInt(hi - lo + 1);
const mask = (1n << width) - 1n;
return Number((v >> BigInt(lo)) & mask);
}
function setBits(oldVal, lo, hi, newFieldVal) {
let v = BigInt(oldVal);
const width = BigInt(hi - lo + 1);
const mask = (1n << width) - 1n;
v = (v & ~(mask << BigInt(lo))) | ((BigInt(newFieldVal) & mask) << BigInt(lo));
return Number(v & ((1n << 64n) - 1n));
}
function escHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function setBrand(title) {
const el = $('brand');
if (!el) return;
el.textContent = title;
}
async function init() {
try {
let multiMode = false;
let customTitle = null;
try {
const resp = await api('/maps');
const maps = Array.isArray(resp) ? resp : resp.maps;
if (!Array.isArray(resp) && typeof resp.title === 'string') {
customTitle = resp.title;
}
const infos = await Promise.all(maps.map(m => api('/' + m.slug + '/info')));
allMaps = maps.map((m, i) => ({
slug: m.slug, name: m.name, info: infos[i],
apiPrefix: '/' + m.slug
}));
multiMode = true;
} catch (_) {
const info = await api('/info');
allMaps = [{ slug: '_', name: info.name, info: info, apiPrefix: '' }];
}
if (multiMode) {
const heading = customTitle || 'ddevmem — Register Maps';
$('title').textContent = heading;
document.title = heading;
if (customTitle) setBrand(customTitle);
$('meta').textContent = allMaps.length + ' register map(s)';
} else {
const info = allMaps[0].info;
$('title').textContent = info.name + ' — Register Map';
document.title = info.name + ' — Register Map';
$('meta').textContent = 'Base: ' + hexStr(info.base_address, 32)
+ ' | Bus width: ' + (info.bus_width * 8) + ' bit'
+ ' | ' + info.registers.length + ' register(s)';
}
renderAll(multiMode);
refreshAll();
} catch (e) {
$('maps').innerHTML = '<div class="error">Failed to load: ' + escHtml(e.message) + '</div>';
}
}
document.addEventListener('pointerdown', (e) => {
const btn = e.target.closest('button');
if (btn && !btn.disabled) btn.focus();
});
function renderAll(showHeaders) {
const container = $('maps');
container.innerHTML = '';
for (const map of allMaps) {
const info = map.info;
const section = ce('div');
section.id = 'map-' + map.slug;
let html = '';
if (showHeaders) {
html += '<h2>' + escHtml(info.name)
+ '<button class="btn-ghost map-dump" onclick="dumpMap(\'' + map.slug + '\')">Dump</button>'
+ '</h2>';
html += '<div class="meta">' + escHtml(map.slug) + ' · Base: ' + hexStr(info.base_address, 32)
+ ' · Bus: ' + (info.bus_width * 8) + '-bit · ' + info.registers.length + ' register(s)</div>';
}
for (const reg of info.registers) {
const uid = map.slug + '-' + reg.offset;
html += '<div class="reg-card" id="reg-' + uid + '">';
html += '<div class="reg-header">'
+ '<span class="reg-name">' + escHtml(reg.name) + '</span>'
+ '<span class="badge badge-' + reg.access + '">' + reg.access + '</span>'
+ '<span class="reg-offset">' + hexStr(reg.offset, 16) + ' (' + reg.width + '-bit)</span>'
+ '</div>';
if (reg.doc) html += '<div class="reg-doc">' + escHtml(reg.doc) + '</div>';
html += '<div class="reg-value">';
if (reg.access !== 'wo') {
html += '<span>Value:</span><span class="hex" id="val-' + uid + '">—</span>';
}
if (reg.access !== 'ro') {
html += '<input id="inp-' + uid + '" placeholder="hex value">';
}
const hasWrite = reg.access !== 'ro';
const hasRead = reg.access !== 'wo';
if (hasWrite || hasRead) {
html += '<div class="btn-set">';
if (hasWrite) {
html += '<button onclick="writeReg(\'' + map.slug + '\',' + reg.offset + ',' + reg.width + ')">Write</button>';
}
if (hasRead) {
html += '<button class="btn-secondary" onclick="readReg(\'' + map.slug + '\',' + reg.offset + ',' + reg.width + ')">Read</button>';
}
html += '</div>';
}
html += '<span class="status" id="st-' + uid + '"></span></div>';
if (reg.bitfields.length > 0) {
html += '<div class="bitfields"><table class="bf-table"><thead><tr>'
+ '<th>Field</th><th>Bits</th><th>Value</th>';
if (reg.access !== 'ro') html += '<th>Set</th>';
html += '<th>Doc</th></tr></thead><tbody>';
for (const bf of reg.bitfields) {
const bits = bf.lo === bf.hi ? '' + bf.lo : bf.hi + ':' + bf.lo;
const bfid = uid + '-' + bf.name;
html += '<tr><td>' + escHtml(bf.name) + '</td><td>' + bits + '</td>'
+ '<td class="bf-val" id="bf-' + bfid + '">—</td>';
if (reg.access !== 'ro') {
html += '<td><div class="bf-set">';
if (bf.variants && bf.variants.length > 0) {
html += '<select class="bf-input" id="bfi-' + bfid + '">';
for (const v of bf.variants) {
html += '<option value="' + v.value + '">' + escHtml(v.name) + ' (' + v.value + ')</option>';
}
html += '</select>';
} else {
html += '<input class="bf-input" id="bfi-' + bfid + '" placeholder="val">';
}
html += '<button onclick="writeBitfield(\'' + map.slug + '\',' + reg.offset + ',' + bf.lo + ',' + bf.hi + ',\'' + bf.name + '\',' + reg.width + ')">Set</button></div></td>';
}
html += '<td class="bf-doc">' + escHtml(bf.doc) + '</td></tr>';
}
html += '</tbody></table></div>';
}
html += '</div>';
}
section.innerHTML = html;
container.appendChild(section);
}
const sidebar = $('sidebar');
let navHtml = '';
for (const map of allMaps) {
const info = map.info;
if (allMaps.length > 1 || map.slug !== '_') {
navHtml += '<div class="nav-heading">' + escHtml(info.name) + '</div>';
}
navHtml += '<div class="nav-base" title="Base address">Base ' + hexStr(info.base_address, 32) + '</div>';
for (const reg of info.registers) {
const uid = map.slug + '-' + reg.offset;
navHtml += '<a href="#reg-' + uid + '" title="' + escHtml(reg.name) + ' ' + hexStr(reg.offset, 16) + '">'
+ '<span class="nav-name">' + escHtml(reg.name) + '</span>'
+ '<span class="nav-addr">' + hexStr(reg.offset, 16) + '</span>'
+ '</a>';
}
}
sidebar.innerHTML = navHtml;
if (allMaps.reduce((s, m) => s + m.info.registers.length, 0) > 1) {
sidebar.classList.add('visible');
}
setupScrollSpy();
}
let scrollSpyObserver = null;
let visibleCards = new Set();
function setupScrollSpy() {
if (scrollSpyObserver) scrollSpyObserver.disconnect();
visibleCards = new Set();
const cards = document.querySelectorAll('.reg-card');
if (!cards.length) return;
scrollSpyObserver = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) visibleCards.add(e.target.id);
else visibleCards.delete(e.target.id);
}
const atBottom = window.innerHeight + window.scrollY >=
document.documentElement.scrollHeight - 2;
if (atBottom && visibleCards.size > 0) {
let bottomId = null, bottomY = -Infinity;
for (const id of visibleCards) {
const el = document.getElementById(id);
if (!el) continue;
const y = el.getBoundingClientRect().top;
if (y > bottomY) { bottomY = y; bottomId = id; }
}
if (bottomId) { setActiveLink(bottomId); return; }
}
let topId = null, topY = Infinity;
for (const id of visibleCards) {
const el = document.getElementById(id);
if (!el) continue;
const y = el.getBoundingClientRect().top;
if (y < topY) { topY = y; topId = id; }
}
if (topId) setActiveLink(topId);
}, {
rootMargin: '-10% 0px -70% 0px',
threshold: 0,
});
cards.forEach(c => scrollSpyObserver.observe(c));
document.querySelectorAll('.sidebar a').forEach(a => {
a.addEventListener('click', () => {
const href = a.getAttribute('href') || '';
if (href.startsWith('#')) setActiveLink(href.slice(1));
});
});
}
function setActiveLink(cardId) {
const links = document.querySelectorAll('.sidebar a');
let activeLink = null;
links.forEach(a => {
const isActive = a.getAttribute('href') === '#' + cardId;
a.classList.toggle('active', isActive);
if (isActive) activeLink = a;
});
if (activeLink) {
const sb = $('sidebar');
if (sb) {
const lr = activeLink.getBoundingClientRect();
const sr = sb.getBoundingClientRect();
if (lr.top < sr.top || lr.bottom > sr.bottom) {
activeLink.scrollIntoView({ block: 'nearest' });
}
}
if (location.hash !== '#' + cardId) {
history.replaceState(null, '', '#' + cardId);
}
}
}
function getMap(slug) {
return allMaps.find(m => m.slug === slug);
}
async function readReg(slug, offset, width) {
const map = getMap(slug);
if (!map) return;
const uid = slug + '-' + offset;
const st = $('st-' + uid);
try {
const resp = await api(map.apiPrefix + '/read', { offset });
const el = $('val-' + uid);
if (el) el.textContent = hexStr(resp.value, width);
updateBitfields(slug, offset, resp.value);
if (st) st.textContent = '';
} catch (e) {
if (st) st.textContent = e.message;
}
}
function updateBitfields(slug, offset, value) {
const map = getMap(slug);
if (!map) return;
const reg = map.info.registers.find(r => r.offset === offset);
if (!reg) return;
for (const bf of reg.bitfields) {
const el = $('bf-' + slug + '-' + offset + '-' + bf.name);
if (!el) continue;
const rawVal = extractBits(value, bf.lo, bf.hi);
if (bf.variants && bf.variants.length > 0) {
const v = bf.variants.find(v => v.value === rawVal);
el.textContent = v ? v.name + ' (' + rawVal + ')' : String(rawVal);
const sel = $('bfi-' + slug + '-' + offset + '-' + bf.name);
if (sel && sel.tagName === 'SELECT') sel.value = String(rawVal);
} else {
el.textContent = rawVal;
}
}
}
async function writeReg(slug, offset, width) {
const map = getMap(slug);
if (!map) return;
const uid = slug + '-' + offset;
const inp = $('inp-' + uid);
const st = $('st-' + uid);
if (!inp) return;
try {
const value = Number(inp.value.trim());
if (isNaN(value)) throw new Error('invalid number');
await api(map.apiPrefix + '/write', { offset, value });
if (st) st.textContent = 'written';
await readReg(slug, offset, width);
} catch (e) {
if (st) st.textContent = e.message;
}
}
async function writeBitfield(slug, offset, lo, hi, name, width) {
const map = getMap(slug);
if (!map) return;
const uid = slug + '-' + offset;
const inp = $('bfi-' + uid + '-' + name);
const st = $('st-' + uid);
if (!inp) return;
try {
const fieldVal = Number(inp.value.trim());
if (isNaN(fieldVal)) throw new Error('invalid number');
const resp = await api(map.apiPrefix + '/read', { offset });
const newVal = setBits(resp.value, lo, hi, fieldVal);
await api(map.apiPrefix + '/write', { offset, value: newVal });
if (st) st.textContent = name + ' updated';
await readReg(slug, offset, width);
} catch (e) {
if (st) st.textContent = e.message;
}
}
async function refreshAll() {
for (const map of allMaps) {
for (const reg of map.info.registers) {
if (reg.access !== 'wo') readReg(map.slug, reg.offset, reg.width);
}
}
}
async function dumpAll() { return dumpMaps(allMaps, 'global-status'); }
async function dumpMap(slug) {
const map = getMap(slug);
if (!map) return;
return dumpMaps([map], 'global-status');
}
async function dumpMaps(maps, statusId) {
if (!maps.length) return;
const gs = statusId ? $(statusId) : null;
if (gs) gs.textContent = 'Dumping...';
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const multi = maps.length > 1 || maps[0].slug !== '_';
let lines = [];
lines.push('# ddevmem register dump');
lines.push('# Date: ' + new Date().toISOString());
if (multi) lines.push('# Maps: ' + maps.length);
lines.push('');
let fileName = multi ? 'ddevmem-all' : null;
for (const map of maps) {
const info = map.info;
if (multi) {
lines.push('## ' + info.name + ' (' + map.slug + ')');
} else {
lines[0] = '# ' + info.name + ' register dump';
}
if (!fileName) {
fileName = info.name + '@' + hexStr(info.base_address, 32);
}
lines.push('# Base address: ' + hexStr(info.base_address, 32));
lines.push('# Bus width: ' + (info.bus_width * 8) + ' bit');
lines.push('');
for (const reg of info.registers) {
if (reg.access === 'wo') {
lines.push(hexStr(reg.offset, 16) + ' ' + reg.name.padEnd(20) + ' [write-only]');
continue;
}
try {
const resp = await api(map.apiPrefix + '/read', { offset: reg.offset });
const val = resp.value;
lines.push(hexStr(reg.offset, 16) + ' ' + reg.name.padEnd(20) + ' ' + hexStr(val, reg.width) + ' (' + reg.access + ')');
for (const bf of reg.bitfields) {
const raw = extractBits(val, bf.lo, bf.hi);
const bits = bf.lo === bf.hi ? 'bit ' + bf.lo : 'bits ' + bf.hi + ':' + bf.lo;
let valStr = String(raw);
if (bf.variants && bf.variants.length > 0) {
const v = bf.variants.find(v => v.value === raw);
if (v) valStr = v.name + ' (' + raw + ')';
}
lines.push(' ' + bf.name.padEnd(20) + ' [' + bits + '] = ' + valStr);
}
} catch (e) {
lines.push(hexStr(reg.offset, 16) + ' ' + reg.name.padEnd(20) + ' [read error: ' + e.message + ']');
}
}
lines.push('');
}
const blob = new Blob([lines.join('\n') + '\n'], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (fileName || 'regdump') + '_' + ts + '.txt';
a.click();
URL.revokeObjectURL(a.href);
if (gs) gs.textContent = 'Dump saved';
} catch (e) {
if (gs) gs.textContent = 'Dump failed: ' + e.message;
}
}
$('auto-refresh').addEventListener('change', function() {
if (this.checked) {
autoTimer = setInterval(refreshAll, 1000);
} else {
clearInterval(autoTimer);
autoTimer = null;
}
});
function updateThemeButton() {
const t = document.documentElement.getAttribute('data-theme');
const btn = $('theme-toggle');
if (!btn) return;
const dark = t !== 'g10';
const moon = '<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'
+ '<path d="M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194 11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1 1 0 0 0-.175.016 13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555 1.01 1.01 0 0 0-.783-1.582 13.08 13.08 0 0 1-11.107-19.302A1.005 1.005 0 0 0 14.98 3z"/></svg>';
const sun = '<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'
+ '<path d="M16 12.005a4 4 0 1 1-4 4 4.005 4.005 0 0 1 4-4m0-2a6 6 0 1 0 6 6 6 6 0 0 0-6-6zM5.394 6.813 6.81 5.399 9.64 8.227 8.226 9.643zM2 15.005h4v2H2zM5.394 25.196 8.222 22.367 9.636 23.781 6.808 26.61zM15 26.005h2v4h-2zM22.362 23.79l1.414-1.414 2.828 2.827-1.414 1.415zM26 15.005h4v2h-4zM22.359 8.227 25.187 5.399 26.601 6.813 23.773 9.641zM15 2.005h2v4h-2z"/></svg>';
btn.innerHTML = dark ? moon : sun;
btn.title = dark ? 'Switch to light theme' : 'Switch to dark theme';
btn.setAttribute('aria-label', btn.title);
}
function toggleTheme() {
const cur = document.documentElement.getAttribute('data-theme');
const next = cur === 'g10' ? 'g100' : 'g10';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('ddevmem-theme', next); } catch (_) {}
updateThemeButton();
}
updateThemeButton();
init();
</script>
</body>
</html>