exl100

Émulateur Exelvision EXL 100 (1984) dans le navigateur : dsp-cpp en WebAssembly, cartouches et cassettes BASIC
git clone https://git.schurger.org/exl100.git
Log | Files | Refs | README

commit 1e850bb69f183c92f85215c1b54c98df59d11fa9
parent c1ebc5406989d2568924320349ef79851c1f54b7
Author: Jean Schurger <jean@schurger.org>
Date:   Thu,  5 Nov 2026 16:30:06 -0500

Bibliothèque : une ligne par programme à plusieurs cassettes, faces en boutons en ligne

Le lien des faces porte un titre de groupe (« Arcade », « Initiation à l'Exelbasic »…) ; la liste n'affiche
plus « Titre (1) », « Titre (2) » mais une ligne avec un bouton par face/cassette (démarre dessus). Le bouton
de la barre reprend le titre seul (il concaténait le badge et « ⏏ insérer »). Recherche sur les titres des
faces.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyJB9yqUAYm3wRQi4uytj9

Diffstat:
Mtest/tape-sides.mjs | 8++++----
Mtools/get_dcexel.py | 6++++++
Mweb/shell.html.in | 39++++++++++++++++++++++++++++++---------
3 files changed, 40 insertions(+), 13 deletions(-)

diff --git a/test/tape-sides.mjs b/test/tape-sides.mjs @@ -9,11 +9,11 @@ const page = await open(base + '/', { width: 900, height: 900 }); await page.evaluate('location.reload()'); await sleep(7000); const row = await page.evaluate(`(() => { - const b = [...document.querySelectorAll('.pg')].find((b) => b.dataset.type === 'tape' && b.dataset.search.includes(${JSON.stringify(want)}) && b.querySelector('.pg-b')); + const b = [...document.querySelectorAll('.pg')].find((b) => b.dataset.type === 'tape' && b.dataset.search.includes(${JSON.stringify(want)}) && b.querySelector('.pg-side')); if (!b) return null; - const badge = b.querySelector('.pg-b').textContent; + const sides = [...b.querySelectorAll('.pg-side')].map((x) => x.textContent); b.click(); - return { title: b.querySelector('.pg-t').firstChild.textContent, badge }; + return { title: b.dataset.title, sides, rows: [...document.querySelectorAll('.pg')].filter((r) => r.dataset.search.includes(${JSON.stringify(want)})).length }; })()`); console.log('entrée :', JSON.stringify(row)); await sleep(6000); @@ -21,7 +21,7 @@ const buttons = await page.evaluate("[...document.querySelectorAll('#tape-sides console.log('boutons de la barre :', JSON.stringify(buttons)); await page.evaluate("document.querySelector('#tape-sides button').click()"); await sleep(500); -const after = await page.evaluate("({ status: document.getElementById('status').textContent, buttons: [...document.querySelectorAll('#tape-sides button')].map((b) => b.textContent) })"); +const after = await page.evaluate("({ status: document.getElementById('status').textContent, pick: document.getElementById('pick').textContent, buttons: [...document.querySelectorAll('#tape-sides button')].map((b) => b.textContent) })"); console.log('après insertion :', JSON.stringify(after)); console.log(buttons.length && /insérée/.test(after.status) && after.buttons.join() !== buttons.join() ? 'OK' : 'PROBLÈME'); page.close(); diff --git a/tools/get_dcexel.py b/tools/get_dcexel.py @@ -139,8 +139,14 @@ def link_sides(tapes): if len(sides) < 2: continue sides.sort(key=lambda x: (re.sub(r"\d+", lambda m: m.group(0).zfill(3), x["l"]))) + # Titre du programme (une seule ligne dans la bibliothèque) : celui de la première face, sans son + # numéro (« Arcade (1) » -> « Arcade », « Initiation au Basic 1 » -> « Initiation au Basic », + # « Initiation a l'EXELBASIC - Volume 2 v1.1 » -> « Initiation a l'EXELBASIC »). + first = tapes[sides[0]["f"]]["t"] + group = re.sub(r"\s*\(\d+\)$|\s*-\s*(Volume|Tome|Cassette|Partie)\s*\d+.*$|\s+\d+$", "", first, flags=re.I).strip() for sd in sides: tapes[sd["f"]]["sides"] = sides + tapes[sd["f"]]["group"] = group or first linked += 1 print(f"DCExel : {linked} programmes à plusieurs faces/cassettes reliés", file=sys.stderr) diff --git a/web/shell.html.in b/web/shell.html.in @@ -62,6 +62,8 @@ #disk-files tr.run:hover td { background: #2b2d35; color: #fff; } #disk-files td.go { color: var(--accent); } .pg-b { color: var(--accent); font-size: .78rem; margin-left: 8px; } + .pg-side { margin-left: 6px; background: #1f3f3f; color: var(--accent); border: 1px solid #2b5c5c; border-radius: 6px; font-size: .75rem; padding: 1px 7px; cursor: pointer; } + .pg-side:hover { background: #2a5555; color: #fff; } .pg-ins { margin-left: 8px; background: #2b2d35; color: #ddd; border: 1px solid #40434e; border-radius: 6px; font-size: .75rem; padding: 1px 6px; cursor: pointer; } .pg-ins:hover { background: #3a3d48; } #tape-sides { display: inline; } @@ -496,9 +498,9 @@ function markActive(value) { let title = ''; for (const el of programsEl.querySelectorAll('.pg')) { - const on = el.dataset.value === value; + const on = el.dataset.value === value || (el.dataset.sides || '').split(' ').includes(value); el.classList.toggle('active', on); - if (on && value) title = el.querySelector('.pg-t').textContent; + if (on && value) title = el.dataset.title; } $('pick').textContent = title ? '📂 ' + title : '📂 Choisir un programme…'; } @@ -673,14 +675,20 @@ b.dataset.value = value; b.dataset.cat = cat || ''; b.dataset.type = value.startsWith('cart:') ? 'cart' : value.startsWith('tape:') ? 'tape' : value.startsWith('cram:') ? 'cram' : value.startsWith('disk:') ? 'disk' : ''; - b.dataset.search = (title + ' ' + source + (m && m.sides ? ' ' + m.sides.map((x) => x.l).join(' ') : '')).toLowerCase(); + b.dataset.search = (title + ' ' + source + (m && m.sides ? ' ' + m.sides.map((x) => x.l).join(' ') + ' ' + m.sides.map((x) => titlesCache[x.f] || '').join(' ') : '')).toLowerCase(); + b.dataset.title = title; const t = document.createElement('span'); t.className = 'pg-t'; t.textContent = title; if (m && m.sides && m.sides.length > 1) { - // Programme sur plusieurs faces/cassettes : on le dit, la suite s'insère avec les boutons 📼 de la barre. - const b = document.createElement('span'); b.className = 'pg-b'; - b.textContent = m.sides.length + ' ' + (m.sides.every((x) => /^Face /.test(x.l)) ? 'faces' : 'cassettes') + ' : ' + m.sides.map((x) => x.l).join(' / '); - b.title = 'Ce programme demande une autre face ou cassette en cours de route : insérez-la alors avec le bouton 📼 de la barre, sans redémarrer.'; - t.appendChild(b); + // Programme sur plusieurs faces/cassettes : une seule ligne, chaque face en bouton (démarre dessus) ; + // en cours de route, la suite s'insère avec les boutons 📼 de la barre, sans redémarrer. + b.dataset.sides = m.sides.map((x) => 'tape:' + x.f).join(' '); + for (const sd of m.sides) { + const sb = document.createElement('button'); sb.type = 'button'; sb.className = 'pg-side'; + sb.textContent = sd.l; + sb.title = 'Démarrer sur « ' + sd.l + ' » (en cours de programme, insérez-la plutôt avec le bouton 📼 de la barre)'; + sb.onclick = (e) => { e.stopPropagation(); bootProgram('tape:' + sd.f); }; + t.appendChild(sb); + } } if (value.startsWith('tape:')) { const ins = document.createElement('button'); ins.type = 'button'; ins.className = 'pg-ins'; ins.textContent = '⏏ insérer'; @@ -693,14 +701,27 @@ b.onclick = () => bootProgram(value); programsEl.appendChild(b); } + const titlesCache = {}; // fichier -> titre, pour la recherche sur les faces function addGroup(label, path, prefix) { let titles; try { titles = JSON.parse(fs().readFile(path, { encoding: 'utf8' })); } catch (e) { return; } + for (const [f, m] of Object.entries(titles)) titlesCache[f] = m.t; const entries = Object.entries(titles).sort((a, b) => a[1].t.localeCompare(b[1].t, 'fr')); if (!entries.length) return; const h = document.createElement('div'); h.className = 'pg-group'; h.textContent = label; programsEl.appendChild(h); - for (const [file, m] of entries) addRow(prefix + file, m.t, sourceOf(m), m.cat, m); + const shown = new Set(); + for (const [file, m] of entries) { + if (m.sides && m.sides.length > 1) { + const first = m.sides[0].f; + if (shown.has(first)) continue; // les autres faces sont sur la même ligne + shown.add(first); + const fm = titles[first] || m; + addRow(prefix + first, m.group || fm.t, sourceOf(fm), fm.cat, fm); + continue; + } + addRow(prefix + file, m.t, sourceOf(m), m.cat, m); + } } function populatePrograms() { programsEl.innerHTML = '';