PageSourceSearch

https://retroapplejs.github.io/

html retroapplejs.github.io collected 2026-10-03 09:50:21 UTC 289,976 bytes, 7,615 lines download raw bytes

1<!DOCTYPE html>
2<html>
3<head>
4    <title>Apple II+ in Javascript</title>
5    <link rel="icon" type="image/x-icon" href="res/favicon.ico">
6    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7    <link rel="icon" type="image/png" sizes="16x16" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAEXRFWHRTb2Z0d2FyZQBKVEwtRGV2J4CxQ84AAAAwUExURf7///7///j4+PLy8u3t7eXl5dvb283NzcLCwrW1ta6urqenp5qamo6OjnV1dWhoaJTpFr4AAAABYktHRACIBR1IAAAACXBIWXMAAB7CAAAewgFu0HU+AAAAAXRSTlMAQObYZgAAAIFJREFUeJwtzTEKwkAQBdC9wp/NkqTcsJC0IVXKlCE3SCrBxjKI6D3sBY9g4R1EEEEbr5AD2Psn8VeP+bM7BoxkwIzhvEAe4wI3QSGnqb43RHdsr/kLRg6lt2Ek9lxIPasLkSh2+o4VCiL6ErFO3oRbA/1Wf948b5/5RBXs6n9d8wM0Ahk+0eMaYgAAAABJRU5ErkJggg==" />
8    <!--link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"-->
9    <link rel="stylesheet" href="res/COM_fontawesome/all.css">
10    <link rel="stylesheet" href="res/COM_MAIN.css">
11    <link rel="stylesheet" href="res/COM_oTERM.css">
12    <link rel="stylesheet" href="res/DBG_testbench.css">
13    
13<script type="text/javascript" src="res/COM_MAIN.js"></script>
13
14    
14<script type="text/javascript" src="res/COM_A2P_LAYOUT.js"></script>
14
15    
15<script type="text/javascript" src="res/COM_LAYOUT_CONFIG.js"></script>
15
16    
16<script type="text/javascript" src="res/COM_oTERM.js"></script>
16
17    
17<script type="text/javascript" src="res/DBG_testbench.js"></script>
17
18    
18<script type="text/javascript" src="res/COM_CONFIG.js"></script>
18  <!-- load all the help, documentation & configuration data-->
19    
19<script type="text/javascript" src="res/pako.min.js"></script>
19    <!-- compression/decompression library -->
20    <!--script type="text/javascript" src="res/purify.min.js"></script-->  <!-- safely filter out all HTML tags -->
21    <!--script type="text/javascript" src="res/ayumi.js"></script-->    <!--  AY-3-8910 chip emulator -->
22    
22<script type="text/javascript" src="https://unpkg.com/[email protected]/build/three.min.js"></script>
22
23    
23<script type="text/javascript" src="https://unpkg.com/[email protected]/examples/js/controls/OrbitControls.js"></script>
23
24    
24<script type="text/javascript" src="https://unpkg.com/[email protected]/examples/js/controls/TransformControls.js"></script>
24
25    
25<script type="text/javascript" src="res/THREEJS_scene_v0.js"></script>
25 
26    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
27    
27<script id="rendered-js">
28      
29//   ____    ____          _            
30//  |_   \  /   _|        (_)           
31//    |   \/   |   ,--.   __   _ .--.   
32//    | |\  /| |  `'_\ : [  | [ `.-. |  
33//   _| |_\/_| |_ // | |, | |  | | | |  
34//  |_____||_____|\'-;__/[___][___||__]
35//  ███████  ██████ ██████  ██ ██████  ████████ ███████ 
36//  ██      ██      ██   ██ ██ ██   ██    ██    ██      
37//  ███████ ██      ██████  ██ ██████     ██    ███████ 
38//       ██ ██      ██   ██ ██ ██         ██         ██ 
39//  ███████  ██████ ██   ██ ██ ██         ██    ███████ 
40
41    //////////////////////
42    // TAB ANIMATOR     //
43    //////////////////////  
44
45    var oTAB = null;
46    oCOM.addToEventStack("onload",COM_switchTab);
47    oCOM.addToEventStack("onhashchange",COM_switchTab);
48
49    ///////////////////////
50    // TAB HANDLER       //
51    ///////////////////////
52
53    function COM_switchTab(sel)
54    {
55        if(!oTAB)
56          oTAB = oCOM.tabSwitch({
57            menuID:"#app ul",
58            contentCL:".tabs",
59            animDuration:0.1
60          }).init();
61
62        if(typeof(sel) == "string" && location.hash.slice(1) != sel)
63          location.hash = "#" + sel;
64
65        return oTAB.switchTab(sel);        
66    }
67
68    ////////////////////
69    // ON-SCREEN HELP //
70    ////////////////////
71
72    function onhelp(txt,target)
73    {
74        if( typeof(_o.tools[target.id])=="undefined" )
75        _o.tools[target.id] = {"toggle":false,"cache":""}
76
77        var t = target.tagName;
78        switch(target.tagName.toUpperCase())
79        {
80        case "DIV":
81            _o.tools[target.id].toggle = !_o.tools[target.id].toggle
82            if(_o.tools[target.id].toggle)
83            {
84            _o.tools[target.id].cache = target.innerHTML;
85            target.innerHTML = txt;
86            }
87            else
88            target.innerHTML = _o.tools[target.id].cache;
89        break;
90        case "TEXTAREA":
91            target.innerHTML = txt;
92        break;
93        }
94    }
95</script>
95
96
97    <style id="lironSurfaceMap_style">
98      #lironSurfaceMap_popup { box-sizing:border-box; overflow:hidden; }
99      #lironSurfaceMap_popup .liron-surface-title { font-weight:bold; padding:2px 0 4px 0; }
100      #lironSurfaceMap_popup .liron-surface-meta { padding:0 0 7px 0; }
101      #lironSurfaceMap_popup .liron-surface-status { padding:12px 4px; }
102      #lironSurfaceMap_popup .liron-surface-panels { display:flex; gap:10px; width:100%; }
103      #lironSurfaceMap_popup .liron-surface-side { flex:1 1 0; min-width:0; }
104      #lironSurfaceMap_popup .liron-surface-side-title { text-align:center; padding-bottom:3px; }
105      #lironSurfaceMap_popup .liron-surface-grid { display:grid; grid-template-columns:repeat(80,minmax(3px,1fr)); grid-template-rows:repeat(12,8px); gap:1px; width:100%; overflow:hidden; }
106      #lironSurfaceMap_popup .liron-surface-cell { min-width:0; height:8px; box-sizing:border-box; border:1px solid rgba(0,0,0,0.35); }
107      #lironSurfaceMap_popup .liron-surface-cell.active { background:rgba(0,0,0,0.38); }
108      #lironSurfaceMap_popup .liron-surface-cell.inactive { opacity:0.12; }
109      #lironSurfaceMap_popup .liron-surface-cell.zone-end { border-right-width:2px; }
110    </style>
111</head>
112
113
114<!--
115   ____    ____          _            
116  |_   \  /   _|        (_)           
117    |   \/   |   ,--.   __   _ .--.   
118    | |\  /| |  `'_\ : [  | [ `.-. |  
119   _| |_\/_| |_ // | |, | |  | | | |  
120  |_____||_____|\'-;__/[___][___||__]
121  ██   ██ ████████ ███    ███ ██      
122  ██   ██    ██    ████  ████ ██      
123  ███████    ██    ██ ████ ██ ██      
124  ██   ██    ██    ██  ██  ██ ██      
125  ██   ██    ██    ██      ██ ███████ 
126-->  
127<body>
128</script>
128
129
130  <div class=appbut id=COM_popup style="position: absolute; z-index: 3; left: 0px; top: 120px; " hidden="">
131  <div class="appbut" onclick="oCOM.POPUP.toggle('COM_popup');document.getElementById('COM_popup_text').innerHTML=''" style="text-align:center;float:right;">x</div>
132  <div id=COM_popup_text></div>
133  </div>
134
135
136  <div class="appbox" id="lironSurfaceMap_popup" style="position:absolute;z-index:3;left:350px;top:120px;width:930px;max-width:calc(100vw - 370px);text-align:left;padding:7px;margin:0px" hidden="">
137    <div class="appbut" onclick="oCOM.POPUP.off('lironSurfaceMap_popup')" style="text-align:center;float:right;cursor:pointer">x</div>
138    <div id="lironSurfaceMap_popup_text"></div>
139  </div>
140
141  <table id=box style=width:450px;margin-top:0px>
142    <tr>
143      <td valign=top>
144        <!--i class="fa fa-bars" onmouseover="document.getElementById('app').style.display=''" style=float:left;></i!-->
145
146        <!-- tab menu  -->
147        <div id=app>
148          <ul style="width:350px;float:left">
149            <!-- links -->
150            <a id="tab0" href="#tab1">Emulator</a>
151            <a id="tab0" href="#tab2">Assembler</a>
152            <a id="tab0" href="#tab3">Debugger</a>
153            <div class="focus-el el--1"></div>
154            <div class="focus-el el--2"></div>
155          </ul>
156        </div>
157       
158        <!-- tab content -->
159        <div class="tabs">
160          <div id="tab1" style="height:1300px;width:1300px;user-select:none;-moz-user-select:none;-webkit-user-drag:none;-webkit-user-select:none;-ms-user-select:none;">
161            <!--form name="tab1" class="tabdata"><input name="tabdata" value='{"SYS":"A2P"}'></form!-->
162            <!--
163              ███████ ███    ███ ██    ██ ██       █████  ████████  ██████  ██████  
164              ██      ████  ████ ██    ██ ██      ██   ██    ██    ██    ██ ██   ██ 
165              █████   ██ ████ ██ ██    ██ ██      ███████    ██    ██    ██ ██████  
166              ██      ██  ██  ██ ██    ██ ██      ██   ██    ██    ██    ██ ██   ██ 
167              ███████ ██      ██  ██████  ███████ ██   ██    ██     ██████  ██   ██  
168            -->
169
170            
170<script type="text/javascript" src="res/EMU_apple2main.js"></script>
170
171            
171<script type="text/javascript" src="res/EMU_apple2hw.js"></script>
171
172            
172<script type="text/javascript" src="res/EMU_cpu6502.js"></script>
172
173            
173<script type="text/javascript" src="res/EMU_WASMcpu6502.js"></script>
173
174            
174<script type="text/javascript" src="res/EMU_apple2debug.js"></script>
174
175            
175<script type="text/javascript" src="res/EMU_DEVICE_speaker.js"></script>
175
176            
176<script type="text/javascript" src="res/EMU_DEVICE_gameport.js"></script>
176
177            
177<script type="text/javascript" src="res/EMU_apple2roms.js"></script>
177
178            
178<script type="text/javascript" src="res/gpu-browser.min.js"></script>
178
179
180            
180<script type="text/javascript" src="res/EMU_DEVICE_video_GPU.js"></script>
180<script>var Apple2VideoGPU = Apple2Video;</script>
180
181            
181<script type="text/javascript" src="res/EMU_DEVICE_video_wave.js"></script>
181<script>var Apple2VideoWave = Apple2Video; Apple2Video = Apple2VideoGPU;</script>
181
182            
182<script type="text/javascript" src="res/EMU_DEVICE_video_THREE.js"></script>
182
183            
183<script type="text/javascript" src="res/EMU_DEVICE_video_canvas.js"></script>
183<script>var Apple2VideoCanvas = Apple2Video;</script>
183
184            
184<script type="text/javascript" src="res/EMU_DEVICE_video_MUX.js"></script>
184
185            
185<script type="text/javascript" src="res/EMU_DEVICE_video_text.js"></script>
185
186            
186<script type="text/javascript" src="res/EMU_DEVICE_videx_normal.js"></script>
186
187            
187<script type="text/javascript" src="res/EMU_DEVICE_videx_MUX.js"></script>
187
188            
188<script type="text/javascript" src="res/EMU_DEVICE_pasteboard.js"></script>
188
189            
189<script type="text/javascript" src="res/EMU_DEVICE_serialpro_line.js"></script>
189
190            
190<script type="text/javascript" src="res/EMU_DEVICE_serialpro_terminal.js"></script>
190
191            
191<script type="text/javascript" src="res/EMU_DEVICE_UNIDISK35.js"></script>
191
192            
192<script type="text/javascript" src="res/EMU_DEVICE_HD20.js"></script>
192
193            
194            
194<script type="text/javascript" src="res/EMU_DEVICE_camera.js"></script>
194
195            
195<script type="text/javascript" src="res/EMU_CARD_dithertizer.js"></script>
195
196            
196<script type="text/javascript" src="res/EMU_DITHERTIZER_converthgr_worker.js"></script>
196
197            
197<script type="text/javascript" src="res/EMU_DITHERTIZER_converthgr.js"></script>
197
198            
198<script type="text/javascript" src="res/EMU_CARD_dithertizer2.js"></script>
198
199            
199<script type="text/javascript" src="res/EMU_CARD_LIRON.js"></script>
199
200            
200<script type="text/javascript" src="res/EMU_CARD_applemouse.js"></script>
200
201            
201<script type="text/javascript" src="res/EMU_CARD_appledisk2.js"></script>
201
202            
202<script type="text/javascript" src="res/EMU_CARD_ramcard.js"></script>
202
203            <!--script type="text/javascript" src="res/EMU_saturnRAM.js"></script-->
204            
204<script type="text/javascript" src="res/EMU_CARD_videx.js"></script>
204
205            
205<script type="text/javascript" src="res/EMU_CARD_mockingboard.js"></script>
205
206            <!--script type="text/javascript" src="res/EMU_CARD_superserial.js"></script-->
207            
207<script type="text/javascript" src="res/EMU_CARD_serialpro.js"></script>
207
208            
208<script type="text/javascript" src="res/EMU_CARD_serialpro_gpt.js"></script>
208
209            
209<script type="text/javascript" src="res/EMU_CARD_thunderclock.js"></script>
209
210            
210<script type="text/javascript" src="res/EMU_CARD_hostio.js"></script>
210
211
212            
212<script type="text/javascript" src="res/EMU_apple2io.js"></script>
212
213            
213<script type="text/javascript" src="res/EMU_DEVICE_keyboard.js"></script>
213
214            
214<script type="text/javascript" src="res/EMU_apple2plus.js"></script>
214
215
216            <!--  EMULATOR TAB PANE -->
217            <div style="height:115px;padding:0px 0px 0px 0px;z-index:1;">
218
219              <div style="height:30px;padding:0px 90px 0px 0px;">
220                <button class=appbut value="Settings" onclick="javascript:oCOM.POPUP.runRule('tab1.1');" title="Settings"><i class="fa fa fa-sliders-h"></i></button>
221                <button class=appbut value="Tools"    onclick="javascript:oCOM.POPUP.runRule('tab1.2');onBoardPasteRefresh();" title="Tools"><i class="fa fa-toolbox"></i></button>              </div>
222  
223              <!--  TOOLS TAB WINDOW -->
224              <div class="popupHost" id="tab1.2">
225                <!--  PASTE-BOARD TOOLBOX -->
226                <div class=toolbox>
227                <form name="EMUboard">
228                  <textarea id="EMUboardText" cols=40 rows=4 placeholder="Paste here → 0:A2KBD:text" name=boardText style="float:left"></textarea><br>
229                  <input type="button"  value="Paste"              onclick="javascript:oCOM.POPUP.toggle('tab1.2');oEMUI.pauseBtn({id:'pausebutton',class1:'fa-play',class2:'fa-pause',pause:false});onBoardPaste()">
230                  <input type="button"  value="TxtCap" id=TxtCapture onclick="javascript:onTxtCapture(this)">
231                  <input type="button"  value="MemCap" id=MemCapture onclick="javascript:onMemCapture('CapF','CapT')">
232                  <input type="text"    value="$0000" size=5 id="CapF" onkeyup="onCaptureAddr(this)"><font color="#FFFFF">►</font><input type="text" value="$FFFF" size=5 id="CapT" onkeyup="onCaptureAddr(this)">
233                  <input type="button"  value="Clear"              onclick="javascript:onEMUBoardClear()">
234                  <input type="button"  value="X"                  onclick="javascript:oCOM.POPUP.off('tab1.2')">
235                </form>
236                </div>
237                <!--  EMULATOR ACTION BUTTONS -->
238                <div class=toolbox>
239                  <button class=appbut                 onclick="javascript:oEMUI.muteBtn({id:'mutebutton',class1:'fa-volume-up',class2:'fa-volume-mute'}).muteAct()"  title="Sound"><i id="mutebutton" class="fa fa-volume-mute"></i></button>
240                  <button class=appbut                 onclick="javascript:oEMUI.pauseBtn({id:'pausebutton',class1:'fa-play',class2:'fa-pause'})" id="pausebutton"   title="Pause"><i  class="fa fa-play"></i></button>
241                  <br>
242                  <button class="appbut"
243                          value="Restart"
244                          id="restartbutton"
245                          title="Short press: warm boot / hold 1/2s: cold boot"
246                          onmousedown="EMU_audio_event_unlock();return oEMUI.restartBtnDown();"
247                          onmouseup="return oEMUI.restartBtnUp();"
248                          onmouseleave="return oEMUI.restartBtnCancel();"
249                          ontouchstart="EMU_audio_event_unlock();return oEMUI.restartBtnDown();"
250                          ontouchend="return oEMUI.restartBtnUp();"
251                          ontouchcancel="return oEMUI.restartBtnCancel();">
252                  <i class="fa fa-power-off"></i></button>
253                  <button class=appbut onclick="javascript:apple2plus.hwObj().io.deviceBtn({id:'devices'})" id="devices" title="Peripheral controls" data-slot=""></button>
254
255                </div>
256                <!--  DEVICE TOOLBOX -->
257
258                <div class=toolbox id="device_toolbox_body"></div>
259
260                <div class="toolbox">
261                  <div id=LEDS class=appbox style="text-align:left;padding:0px 0px 0px 0px;margin:0px 0px 0px 0px; z-index:0">
262                  </div>
263                </div>
264
265
266                <!--  DEBUG  -->
267                <!--div class=toolbox>
268                  <div class=appbox style="text-align:left;height:80px;padding:0px 0px 0px 0px;margin:0px 0px 0px 0px">
269                    <canvas id="scopeCanvas" width="300" height="55" style="border:0px solid #E0E0E0;padding:0px 0px 0px 0px;margin:0px 0px 0px 0px"></canvas>
270                    <div style="border:0px solid #E0E0E0;padding:0px 0px 0px 0px;margin:0px 0px 0px 0px">
271                      <input type="range" min="0" max="250" value="
2710" class="slider" onchange="slider_scroll(this,250)" style="width:250px">
272                      <i class="fa fa-arrow-circle-left"></i>932<i class="fa fa-arrow-circle-right"></i>
273                    </div>
274                  </div>
275                </div-->
276
277              </div>
278
279              <!--  SETTINGS TAB WINDOW -->
280              <div class="popupHost" id="tab1.1" hidden>
281
282                <!--  SYSTEM SETTINGS -->
283                <div class=toolbox>
284                  <div class=appbox title="SYSTEM" style="text-align:left;height:92px;padding:0px 6px 0px 6px;">
285                    SYSTEM
286                    <div style="font-family:arial;font-size:11px">
287                     <div style="
288  display:flex;
289  flex-direction:column;
290  width:110px;
291  min-width:110px;
292">
293
294  <!-- CPU load -->
295  <div style="
296    display:flex;
297    flex-flow:row nowrap;
298    align-items:center;
299    width:110px;
300    white-space:nowrap;
301  ">
302    <span id="cpu_component" style="flex:0 0 auto;"></span>
303
304    <span style="flex:0 0 auto;margin-left:3px;">load</span>
305
306    <i class="fa fa-sync-alt"
307       id="CPU_monitoring"
308       style="flex:0 0 auto;margin-left:3px;"
309       onclick="
310         oCOM.POPUP.toggle_class(this,'fa-stop-circle','fa-sync-alt');
311         oCOM.toggleRefreshEvent('CPU_monitoring');
312         document.getElementById('cpu_pct').textContent='';
313       "></i>
314
315    <span id="cpu_pct"
316          style="
317            flex:1 1 auto;
318            min-width:24px;
319            margin-left:3px;
320            text-align:right;
321            white-space:nowrap;
322          "></span>
323  </div>
324
325  <!-- CPU speed -->
326  <div style="
327    display:flex;
328    flex-flow:row nowrap;
329    align-items:center;
330    width:110px;
331  ">
332    <button type="button"
333            id="slider_1v"
334            class="appbut skinny"
335            style="
336              flex:0 0 45px;
337              width:45px;
338              padding:0;
339              font-family:arial;
340              font-size:11px;
341            "
342            onclick="oEMUI.cpuSldIndicator(event,this,'myRange','feature_box')"
343            title="CPU speed: 100%. Click to switch to 0-100%."
344            aria-label="CPU speed: 100%. Click to switch to 0-100%.">100%</button>
345
346    <input id="myRange"
347           type="range"
348           min="0"
349           max="400"
350           value="100"
351           step="20"
352           class="slider"
353           data-speed-mode="pct400"
354           onchange="oEMUI.cpuSld(this,'slider_1v')"
355           style="
356             flex:1 1 auto;
357             min-width:0;
358             width:0;
359           ">
360  </div>
361
362  <!-- FPS -->
363  <div style="
364    display:flex;
365    flex-flow:row nowrap;
366    align-items:center;
367    width:110px;
368  ">
369    <span id="slider_fps_v"
370          style="
371            flex:0 0 35px;
372            width:35px;
373          ">10fps</span>
374
375    <input id="fpsRange"
376           type="range"
377           min="10"
378           max="100"
379           value="10"
380           step="1"
381           class="slider"
382           oninput="document.getElementById('slider_fps_v').textContent=this.value+'fps'"
383           onchange="oEMUI.fpsSld(this,'slider_fps_v')"
384           title="Processing frames per second"
385           style="
386             flex:1 1 auto;
387             min-width:0;
388             width:0;
389           ">
390  </div>
391
392</div>
393
394<div id="all_components"></div>
395                        <div id="all_components"></div>
396                      
396<script>
397                        oCOM.POPUP.addRule("tab1.1", function(o) { o.toggle("tab1.1"); o.off("tab1.2") });
398                        oCOM.POPUP.addRule("tab1.2", function(o) { o.toggle("tab1.2"); o.off("tab1.1") });
399
400                        /*
401                         * Floating feature dialogs belong to one emulator
402                         * toolbox. They are suspended outside that toolbox,
403                         * but return in the open/closed state the user left.
404                         */
405                        oCOM.POPUP.addScope("slotConfig_popup","tab1.1");
406                        oCOM.POPUP.addScope("softwareCat_popup","tab1.2");
407                        oCOM.POPUP.addScope("surfaceMap_popup","tab1.2");
408                        oCOM.POPUP.addScope("lironSurfaceMap_popup","tab1.2");
409                        oCOM.POPUP.addScope("serialProTerminal_popup","tab1.2");
410
411                        function system_tab_update()
412                        {
413                            var video = apple2plus.vidObj();
414                            var charRomKey = video.getCharRomKey();
415
416                            document.getElementById("cpu_component").innerHTML =
417                                _CFG_SYSCODE.A2P.CPU;
418
419                            document.getElementById("all_components").innerHTML =
420                                  _CFG_SYSCODE.A2P.Display
421                                + " Vid "
422                                + "<div class=\"appbut skinny\" style=\"display:inline\">"
423                                +   "<i class=\"fa fa-palette\" id=\"clr\" onclick=\"this.name=(this.name?Number(this.name)+1:1);"
424                                +   "var m=apple2plus.monitor(this.name); this.style.color=m.color;this.title=m.name\" title=\"Color mode\"></i>"
425                                + "</div>"
426                                + "<div class=\"appbut skinny\" style=\"display:inline\">"
427                                +   "<i class=\"fa fa-eye\" id=\"render_mode\" title=\"GPU\""
428                                +   " onclick=\"if(typeof(apple2plus)=='object') apple2plus.vidObj().nextMode(this);\"></i>"
429                                + "</div><br>"
430                                + charRomKey + " ROM "
431                                + "<div class=\"appbut skinny\" style=\"display:inline\">"
432                                +   "<i class=\"fa fa-keyboard\" id=\"char_rom\""
433                                +   " title=\"Character ROM: " + charRomKey + "\""
434                                +   " onclick=\"apple2plus.vidObj().nextCharRom(this);"
435                                +   " system_tab_update();\"></i>"
436                                + "</div>"
437                                + "<i class=\"fa fa-cog\"></i>";
438                        }
439                      </script>
439
440                    </div>
441                  </div>
442                </div>
443
444
445                <!--  SLOT SETTINGS -->
446                <div class=toolbox>
447                  <div class=appbox style="text-align:left;height:92px;padding:0px 6px 0px 6px;">
448                    SLOTS<br>
449                    <div id="peripheral_slots"></div>
450                  </div>
451                </div>
452              
453                <div id=feature_box class=toolbox style="margin:0px 0px 0px 1px;"></div>
454                
455              </div>
456            </div>
457
458              
458<script>
459              function slider_scroll(obj,max)
460              {
461                  var pct = Math.round(1000*obj.value/max)/10;
462
463                  // Plot Line
464                  var canvas = slider_canvas;
465                  var ctx = slider_scroll_ctx;
466
467                  ctx.font = "8px Arial";
468                  //ctx.font = "8px Tahoma";
469                  //ctx.font = "8px MS Sans Serif";
470                  ctx.fillText("1A2B", 10, 50-3);
471                  ctx.fillText("3C4D", 10, 58-3);
472
473                  ctx.strokeStyle = '#ff0000';
474                  ctx.beginPath();
475                  ctx.moveTo(0, 40);
476                  ctx.lineTo(300, 40);
477                  ctx.stroke();
478
479                  //ctx.clearRect(0, 0, canvas.width, canvas.height);
480              }
481              const slider_canvas = document.getElementById("scopeCanvas");
482              if(slider_canvas) var slider_scroll_ctx = slider_canvas.getContext("2d");
483
484
485            oCOM.addToEventStack("onload",EMU_mem_map)
486            function EMU_mem_map()
487            {
488              var el = document.getElementById("EMU_mem_map");
489              if(!el) return;
490              if(typeof(apple2plus)!="object" || apple2plus==null || !apple2plus.hwObj().mem_layout) return;
491 
492              var linestep = 4096;
493              var cnf = {id_prefix:"m",digits:4};
494              // Apple slot 0 is internal slot-array index 1.
495              var ramcard = apple2plus.hwObj().io.SLOT2obj(1);
496
497              var bLC = ramcard && ramcard.id?.PCODE=="MS16K"
498                    && ramcard.state && ramcard.state.active
499                    && ramcard.MEM_grid
500                    && typeof(ramcard.build_MEM_map)=="function";
501              el.innerHTML =
502                "<div style='display:flex;'>"
503              +   oMEMGRID.build_grid(7*linestep,8,-linestep,cnf)
504              +   oMEMGRID.build_grid(15*linestep,8,-linestep,cnf)
505              +   (bLC ? ramcard.build_MEM_map(linestep) : "")
506              + "</div>";
507              oMEMGRID.paint_grid(apple2plus.hwObj().mem_layout,cnf);
508              if(bLC && typeof(ramcard.paint_MEM_map)=="function") ramcard.paint_MEM_map();
509
510            }
511
512            function isHidden(el)
513            {
514              var style = window.getComputedStyle(el);
515              return (style.display === 'none')
516            }
517
518            function onBoardPasteIO()
519            {
520              return typeof(apple2plus)=="object" && apple2plus
521                  ? apple2plus.hwObj().io
522                  : null;
523            }
524
525            function onBoardPasteDevice(io)
526            {
527              if(!io || typeof(io.pipeResolve)!="function") return null;
528
529              var source = io.pipeResolve("0:PASTEBO:text");
530              return source ? source.device : null;
531            }
532
533            function onBoardPasteTarget()
534            {
535              var io = onBoardPasteIO();
536              var pasteboard = onBoardPasteDevice(io);
537
538              return pasteboard && typeof(pasteboard.getTargetAddress)=="function"
539                  ? pasteboard.getTargetAddress(io)
540                  : "0:A2KBD:text";
541            }
542
543            function onBoardPasteRefresh()
544            {
545              var target = onBoardPasteTarget();
546              var textarea = document.getElementById("EMUboardText");
547
548              if(textarea)
549              {
550                textarea.placeholder = "Paste here → " + target;
551                textarea.title = "Pasteboard pipe: 0:PASTEBO:text → " + target;
552              }
553
554              var io = onBoardPasteIO();
555              var pasteboard = onBoardPasteDevice(io);
556              var captureButton = document.getElementById("TxtCapture");
557              var captureSource =
558                pasteboard &&
559                typeof(pasteboard.getCaptureSourceAddress)=="function"
560                    ? pasteboard.getCaptureSourceAddress(io)
561                    : null;
562
563              if(captureButton)
564                  captureButton.title =
565                      "Text capture: " +
566                      (captureSource
567                          ? captureSource + " → 0:PASTEBO:text"
568                          : "Apple II 40-column display");
569
570              return target;
571            }
572
573            var onBoardPasteRouteUnsubscribe = null;
574
575            function onBoardPasteBindRouteState()
576            {
577              var io = onBoardPasteIO();
578
579              if(onBoardPasteRouteUnsubscribe)
580              {
581                onBoardPasteRouteUnsubscribe();
582                onBoardPasteRouteUnsubscribe = null;
583              }
584
585              if(io && typeof(io.subscribePipeStateChange)=="function")
586                onBoardPasteRouteUnsubscribe =
587                    io.subscribePipeStateChange(onBoardPasteRefresh);
588
589              return onBoardPasteRefresh();
590            }
591
592            oCOM.addToEventStack("onload",onBoardPasteBindRouteState);
593
594            function onBoardPaste()
595            {
596              var io = onBoardPasteIO();
597              var pasteboard = onBoardPasteDevice(io); 
598
599              if(!io || typeof(io.pipeSend)!="function")
600              {
601                console.error("Apple2IO pipe router is not available");
602                return false;
603              }
604              if(!pasteboard || typeof(pasteboard.sendText)!="function")
605              {
606                console.error("PASTEBO device is not attached to A2BO");
607                return false;
608              }
609
610              onBoardPasteRefresh();
611
612              return pasteboard.sendText(
613                  io,
614                  EMUboard.boardText.value
615
616              );
617            }
618
619            function onTxtCapture(el)
620            {
621
622              var io = onBoardPasteIO();
623              var pasteboard = onBoardPasteDevice(io);
624
625              if(!pasteboard ||
626                 typeof(pasteboard.captureText)!="function")
627              {
628                console.error("PASTEBO text capture is not available");
629                return false;                
630              }
631              
632              /*
633               * TxtCap is now entirely device/pipe based:
634               *
635               *   0:A2VIDEOTXT:text -> 0:PASTEBO:text
636               *   n:VIDEXTXT:text   -> 0:PASTEBO:text
637               *
638               * PASTEBO selects the active semantic text source.
639               */
640              var captured = pasteboard.captureText(io);
641
642              if(captured===null)
643              {
644                console.error("No text capture source is available");
645                return false;
646              }
647              EMUboard.boardText.value = captured;
648              return true;
649            }
650
651            function onEMUBoardClear() { EMUboard.boardText.value = "" };  
652            function onCaptureBase(tag)
653            {
654              tag.value=tag.value=="hex"?"dec":"hex";
655            }
656            function EMU_safeBusRead(addr)
657            {
658              var hw = apple2plus.hwObj();
659              addr = Number(addr) & 0xFFFF;
660
661              // Prefer the hardware-level safe reader when available.
662              if(hw && typeof(hw.safe_read)=="function")
663                return hw.safe_read(addr) & 0xFF;
664
665              // Fallback for builds where Apple2Hw.safe_read() has not been added yet.
666              // This still uses the current CPU-visible RD[] callback map and raises
667              // hw.bRO so hardware dependencies can suppress soft-switch side effects.
668              if(!hw || !hw.RD || typeof(hw.lineDecode)!="function") return 0x00;
669
670              var old_bRO = hw.bRO;
671              hw.bRO = true;
672
673              try
674              {
675                var line = hw.lineDecode(addr);
676                var fn = hw.RD[line];
677                var d8 = typeof(fn)=="function" ? fn(addr) : 0x00;
678                return (d8==null ? 0x00 : d8) & 0xFF;
679              }
680              finally
681              {
682                hw.bRO = old_bRO;
683              }
684            }
685
686            function EMU_safeBusDump(from,to)
687            {
688              var hw = apple2plus.hwObj();
689              from = Number(from) & 0xFFFF;
690              to   = Number(to)   & 0xFFFF;
691
692              // Prefer the hardware-level safe dump when available.
693              if(hw && typeof(hw.safe_dump)=="function")
694                return hw.safe_dump(from,to);
695
696              var len = ((to - from) & 0xFFFF) + 1;
697              var out = new Uint8Array(len);
698
699              if(!hw || !hw.RD || typeof(hw.lineDecode)!="function") return out;
700
701              var old_bRO = hw.bRO;
702              hw.bRO = true;
703
704              try
705              {
706                for(var i=0;i<len;i++)
707                {
708                  var addr = (from + i) & 0xFFFF;
709                  var line = hw.lineDecode(addr);
710                  var fn = hw.RD[line];
711                  var d8 = typeof(fn)=="function" ? fn(addr) : 0x00;
712                  out[i] = (d8==null ? 0x00 : d8) & 0xFF;
713                }
714              }
715              finally
716              {
717                hw.bRO = old_bRO;
718              }
719
720              return out;
721            }
722
723            function onMemCapture(id1,id2)
724            {
725              var hw = apple2plus.hwObj();
726              var id1 = document.getElementById(id1).value;
727              var id2 = document.getElementById(id2).value;
728              var base = id1.charAt(0)=="$" && id2.charAt(0)=="$"?"hex":"dec";
729              var val = id1+"►"+id2;
730
731              var template = {"hex":["\\$([0-9A-F]{1,4})►\\$([0-9A-F]{1,4})",16],
732                              "dec":["([0-9]{1,5})►([0-9]{1,5})",10]};
733              // VALIDATE
734              var re = new RegExp(template[base][0],'g');
735              var addr_arr = val.replace(re,'$1,$2').split(",");
736              if(base=="hex") addr_arr = [Number(oCOM.base_convert(addr_arr[0],["0123456789ABCDEF"],["0123456789"])),Number(oCOM.base_convert(addr_arr[1],16,10))];
737
738              if(addr_arr.length==2)
739              {
740                var from = Math.max(0,Math.min(0xFFFF,addr_arr[0] & 0xFFFF));
741                var to   = Math.max(0,Math.min(0xFFFF,addr_arr[1] & 0xFFFF));
742
743                if(to < from)
744                {
745                  var tmp = from;
746                  from = to;
747                  to = tmp;
748                }
749
750                var s = [];
751                var dump = EMU_safeBusDump(from,to);
752
753                for(var i=from;i<=to;i++)
754                {
755                  var idx = Math.floor((i-from)/8);
756                  if((i-from)%8==0)
757                  {
758                    var adr = oCOM.getHexWord(i);
759                    s[idx] = adr+":";
760                  }
761
762                  var b = oCOM.getHexByte(dump[i-from]);
763                  s[idx] += " "+b;
764                }
765                EMUboard.boardText.value = s.join("\n");
766              }           
767            }
768
769            function onCaptureAddr(el)
770            {
771              var s = el.value.toUpperCase();
772              var base = s.charAt(0)=="$" ? 16 : 10;
773              if(base==16) s = s.substring(1);
774              var n = oCOM.base_convert(s,[oCOM.getsymbols(base)],[oCOM.getsymbols(10)]);
775              var bValid = n!==null && Number(n)<=0xFFFF;
776              document.getElementById('MemCapture').disabled = !bValid;
777              return bValid;
778            }
779
780            function onSnapshot(canvas)
781            {
782              var canvas = document.getElementById("applescreen");
783              const dataURL = canvas.toDataURL('image/png');
784              document.write(dataURL);
785            }
786
787            </script>
787
788            
789            <!--input id=foo type=text oninput="apple2plus.keystroke(event)"    onclick=document.getElementById('foo').focus()-->
790            <canvas class="appvid" id="applescreen" width="560" height="384" tabindex="0" style="outline: none !important;padding:0px 0px 0px 0px;" onclick="EMU_audio_event_unlock();" onkeydown="EMU_audio_event_unlock();if(keys) keys.events(event);" onkeyup="if(keys) keys.events(event);"></canvas>
791            <!--/input-->
792
793<!--button onclick="onSnapshot()">SNAP</button-->
794
795            <div id="kbd" style="position:relative; z-index:1;"></div>
796
797
798            <!--
799            <br>
800            <img src=res/appleIIplus_kbd_650.png id=kbdimg style="padding-top:0px;padding-left:105px;opacity:0.5;image-rendering:pixelated;user-drag:none;user-select:none;-moz-user-select:none;-webkit-user-drag:none;-webkit-user-select:none;-ms-user-select:none;" ontouchstart=apple2OnKeyHover_b(event) onclick=apple2OnKeyHover_b(event) onmousemove=apple2OnKeyHover_b(event) onmouseover="apple2OnKeyHover_b(event)" onmouseout="apple2OnKeyHover_b(event)">
801            -->
802            <br>
803            <input hidden id="debug" style="width:400px" value="">
804          </div>
805
806          <!--
807             █████  ███████ ███████ ███████ ███    ███ ██████  ██      ███████ ██████  
808            ██   ██ ██      ██      ██      ████  ████ ██   ██ ██      ██      ██   ██ 
809            ███████ ███████ ███████ █████   ██ ████ ██ ██████  ██      █████   ██████
810            ██   ██      ██      ██ ██      ██  ██  ██ ██   ██ ██      ██      ██   ██
811            ██   ██ ███████ ███████ ███████ ██      ██ ██████  ███████ ███████ ██   ██
812          -->
813          <div id="tab2" style="width:1300px;user-select:none;-moz-user-select:none;-webkit-user-drag:none;-webkit-user-select:none;-ms-user-select:none;">
814            <!--form name="tab2" class="tabdata"><input name="tabdata" value='{"SYS":"A2P"}'></form!-->
815            
815<script type="text/javascript" src="res/ASM_core.js"></script>
815
816            
816<script type="text/javascript" src="res/ASM_OFFLINE.js"></script>
816
817
818            <style>
819              /*
820               * ASM namespace rules
821               * Keep tab2 itself owned by the common tab system.
822               * All assembler-specific layout and styling lives below ASM_root.
823               */
824              .ASM_root {
825                --ASM_bg: #0d1117;
826                --ASM_panel: #161b22;
827                --ASM_panel2: #0b0f14;
828                --ASM_border: #30363d;
829                --ASM_text: #e6edf3;
830                --ASM_muted: #8b949e;
831                --ASM_accent: #79b8ff;
832                --ASM_ok: #8bd17c;
833                --ASM_warn: #ffd166;
834                --ASM_bad: #ff8a8a;
835                --ASM_key: #79c0ff;
836                --ASM_string: #a5d6ff;
837                --ASM_number: #f2cc60;
838                --ASM_bool: #ffab70;
839                --ASM_monoFont: "DejaVu Sans Mono","Menlo","Consolas","Courier New",monospace;
840                --ASM_monoSize: 14px;
841                --ASM_monoLineHeight: 21px;
842                --ASM_paneBodyGap: 4px;
843
844                width: 1300px;
845                margin: 0px;
846                padding: 0px 0px 0px 0px;
847                color: var(--ASM_text);
848                box-sizing: border-box;
849                font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
850              }
851
852              .ASM_root * {
853                box-sizing: border-box;
854              }
855
856
857              .ASM_paneHost {
858                display: grid;
859                grid-template-columns: repeat(2, minmax(0, 1fr));
860                align-items: start;
861                gap: 10px;
862                width: calc(100% - 10px);
863                max-width: calc(100% - 10px);
864              }
865
866              .ASM_paneHost.ASM_showInclude {
867                grid-template-columns: repeat(3, minmax(0, 1fr));
868              }
869
870              .ASM_paneHost .ASM_includePane {
871                display: none;
872              }
873
874              .ASM_paneHost.ASM_showInclude .ASM_includePane {
875                display: flex;
876              }
877
878              .ASM_pane {
879                display: flex;
880                flex-direction: column;
881                gap: var(--ASM_paneBodyGap);
882                min-width: 0;
883                max-width: 100%;
884                line-height: normal;
885                margin-top: 2px;
886                margin-bottom: 10px;
887              }
888
889              .ASM_editor,
890              .ASM_jsonMount {
891                width: 100%;
892                max-width: 100%;
893                min-height: 420px;
894                display: block;
895                border: 0;
896                outline: 0;
897                padding: 0px 30px;
898                color: #000000;
899                -webkit-text-fill-color: #000000;
900                caret-color: #000000;
901                font-family: var(--ASM_monoFont);
902                font-size: var(--ASM_monoSize);
903                font-weight: 500;
904                line-height: var(--ASM_monoLineHeight);
905                tab-size: 4;
906                white-space: pre;
907                resize: none;
908              }
909
910              .ASM_editor {
911                height: auto;
912                overflow-y: hidden;
913                overflow-x: auto;
914              }
915
916              .ASM_jsonMount {
917                height: 420px;
918                max-height: 420px;
919                overflow-y: auto;
920                overflow-x: auto;
921                resize: none;
922                scrollbar-color: rgba(0,0,0,0.30) transparent;
923                scrollbar-width: thin;
924              }
925
926              .ASM_jsonMount::placeholder {
927                color: rgba(0,0,0,0.55);
928                font: inherit;
929                line-height: inherit;
930                opacity: 1;
931              }
932
933
934              .ASM_jsonMount::-webkit-scrollbar { width: 10px; height: 10px; }
935              .ASM_jsonMount::-webkit-scrollbar-track { background: transparent; }
936              .ASM_jsonMount::-webkit-scrollbar-corner { background: transparent; }
937              .ASM_jsonMount::-webkit-scrollbar-thumb {
938                background-color: rgba(0,0,0,0.30);
939                border-radius: 10px;
940                border: 2px solid transparent;
941                background-clip: content-box;
942              }
943
944              .ASM_jsonView {
945                margin: 0;
946                padding: 0;
947                background: transparent !important;
948                white-space: pre;
949                min-width: max-content;
950                max-width: none;
951                color: inherit;
952                font: inherit;
953                line-height: inherit;
954              }
955
956              /* oCOM.JSONprettify() creates <pre class="json-view">.
957                 Keep JSON output aligned with Source and Listing. */
958              .ASM_jsonMount > pre,
959              .ASM_jsonMount .json-view,
960              .ASM_jsonMount .ASM_jsonView {
961                margin: 0;
962                padding: 0;
963                background: transparent !important;
964                color: inherit;
965                font: inherit;
966                line-height: inherit;
967                white-space: pre;
968                min-width: max-content;
969                max-width: none;
970              }
971
972
973              .ASM_jsonMount *,
974              .ASM_jsonKey,
975              .ASM_jsonString,
976              .ASM_jsonNumber,
977              .ASM_jsonBoolean,
978              .ASM_jsonNull { color: #000000 !important; }
979
980
981              .ASM_findReplaceMount {
982                display: inline-flex;
983                align-items: center;
984                min-height: 26px;
985                max-width: 430px;
986                margin: 2px 0px 0px 0px;
987                color: #000000;
988                font-family: arial;
989                font-size: 11px;
990                vertical-align: middle;
991              }
992
993              .ASM_findReplaceMount .com-pane-fr {
994                color: #000000;
995              }
996
997              .ASM_findReplaceMount input {
998                color: #000000;
999              }
1000
1001              .ASM_paneTitle {
1002                display: flex;
1003                align-items: center;
1004                justify-content: space-between;
1005                gap: 10px;
1006                min-height: 34px;
1007                margin: 0px;
1008                padding: 4px 8px;
1009                border-radius: 10px;
1010                border: 1px solid rgba(0,0,0,0.18);
1011                background: rgba(244,244,244,0.88);
1012                color: #000000;
1013                font-family: arial, sans-serif;
1014                font-size: 12px;
1015                line-height: 20px;
1016                overflow: visible;
1017                position: relative;
1018              }
1019
1020              .ASM_root .ASM_paneTitle.com-pane-title-equalized {
1021                height: var(--com-pane-title-height, auto);
1022                min-height: var(--com-pane-title-height, 34px);
1023              }
1024
1025              .ASM_paneTitle strong {
1026                color: #000000;
1027                font-size: 13px;
1028                font-weight: 700;
1029              }
1030
1031              .ASM_paneTitleMain {
1032                display: flex;
1033                align-items: center;
1034                gap: 8px;
1035                min-width: 0;
1036              }
1037
1038              .ASM_listingPresetList,
1039              .DBG_listingPresetList {
1040                display: flex;
1041                flex-flow: row wrap;
1042                align-items: flex-start;
1043                align-content: flex-start;
1044                gap: 4px;
1045                margin-top: 5px;
1046                width: 100%;
1047                min-width: 0;
1048                max-width: 100%;
1049              }
1050
1051              .ASM_listingPreset,
1052              .DBG_listingPreset {
1053                -webkit-appearance: none;
1054                appearance: none;
1055                display: inline-flex;
1056                align-items: center;
1057                justify-content: flex-start;
1058                flex: 0 0 auto;
1059                gap: 6px;
1060                min-height: 22px;
1061                padding: 2px 6px;
1062                border: 1px solid rgba(0,0,0,0.24);
1063                border-radius: 4px;               
1064                background: rgba(255,255,255,0.55);
1065                color: #000000;
1066                font: 11px/1.2 arial, sans-serif;
1067                cursor: pointer;
1068              }
1069
1070              .ASM_listingPreset:hover,
1071              .ASM_listingPreset:focus,
1072              .DBG_listingPreset:hover,
1073              .DBG_listingPreset:focus {
1074                border-color: #0078d4;
1075                outline: none;
1076              }
1077
1078              .ASM_listingPreset i,
1079              .DBG_listingPreset i {
1080                width: 14px;
1081                text-align: center;
1082              }
1083
1084              .ASM_paneInfo {
1085                color: rgba(0,0,0,0.70);
1086                font-family: arial, sans-serif;
1087                font-size: 11px;
1088                white-space: nowrap;
1089              }
1090
1091              /* Explicit source dependencies loaded for S-C .IN directives. */
1092              .ASM_includeLabelWrap {
1093                display: flex;
1094                flex-wrap: wrap;
1095                align-items: flex-start;
1096                gap: 2px 3px;
1097                width: 100%;
1098                min-width: 0;
1099                max-width: 100%;
1100                margin: 2px 0px;
1101              }
1102
1103              .ASM_includeLabelWrap .appbut.label {
1104                float: none !important;
1105                display: inline-flex;
1106                align-items: center;
1107                gap: 3px;
1108                width: auto !important;
1109                min-width: 0;
1110                height: auto;
1111                min-height: 16px;
1112                line-height: 14px;
1113                margin: 0px;
1114                border-radius: 3px;
1115                background: rgba(128,128,128,0.08);
1116                border: 1px solid black;
1117                color: #000000;
1118                font-family: arial;
1119                font-size: 11px;
1120                padding: 0px 2px;
1121                box-sizing: border-box;
1122                white-space: nowrap;
1123                cursor: pointer;
1124              }
1125
1126              .ASM_includeLabelWrap .appbut.label.ASM_includeSelected {
1127                outline: 2px solid #0078d4;
1128                outline-offset: 1px;
1129                background: rgba(0,120,212,0.10);
1130              }
1131
1132              .ASM_includeLabelWrap .ASM_includeRemove {
1133                float: none !important;
1134                display: inline-flex;
1135                align-items: center;
1136                justify-content: center;
1137                width: 14px;
1138                min-width: 14px;
1139                height: 14px;
1140                min-height: 14px;
1141                line-height: 12px;
1142                margin: 0px;
1143                padding: 0px;
1144                cursor: pointer;
1145              }
1146
1147              #ASM_diagnosticsButton[hidden],
1148              #ASM_outputPopupBack[hidden] {
1149                display: none !important;
1150              }
1151
1152              .ASM_outputPopupBack {
1153                position: fixed;
1154                inset: 0;
1155                z-index: 2200;
1156                display: flex;
1157                align-items: center;
1158                justify-content: center;
1159                padding: 28px;
1160                background: rgba(0,0,0,0.38);
1161              }
1162
1163              .ASM_outputPopup {
1164                display: flex;
1165                flex-direction: column;
1166                width: min(1100px,calc(100vw - 56px));
1167                height: min(760px,calc(100vh - 56px));
1168                min-width: 360px;
1169                min-height: 260px;
1170                border: 1px solid rgba(0,0,0,0.42);
1171                border-radius: 10px;
1172                background: #f4f4f4;
1173                box-shadow: 0px 8px 30px rgba(0,0,0,0.42);
1174                overflow: hidden;
1175              }
1176
1177              .ASM_outputPopupHeader {
1178                display: flex;
1179                align-items: center;
1180                justify-content: space-between;
1181                gap: 10px;
1182                min-height: 38px;
1183                padding: 5px 8px;
1184                border-bottom: 1px solid rgba(0,0,0,0.22);
1185                color: #000000;
1186                font: 12px/20px arial,sans-serif;
1187              }
1188
1189              .ASM_outputPopupHeader strong { font-size: 13px; }
1190
1191              .ASM_outputPopupActions {
1192                display: inline-flex;
1193                align-items: center;
1194                gap: 5px;
1195              }
1196
1197              .ASM_outputPopupText {
1198                display: block;
1199                flex: 1 1 auto;
1200                width: 100%;
1201                min-height: 0;
1202                margin: 0px;
1203                padding: 10px 14px;
1204                border: 0px;
1205                outline: 0px;
1206                resize: none;
1207                overflow: auto;
1208                background: #ffffff;
1209                color: #000000;
1210                -webkit-text-fill-color: #000000;
1211                font-family: var(--ASM_monoFont);
1212                font-size: var(--ASM_monoSize);
1213                font-weight: 500;
1214                line-height: var(--ASM_monoLineHeight);
1215                white-space: pre;
1216                tab-size: 4;
1217                user-select: text !important;
1218                -moz-user-select: text !important;
1219                -webkit-user-select: text !important;
1220                -ms-user-select: text !important;
1221              }
1222
1223              .ASM_root.ASM_paneDark .ASM_outputPopup {
1224                background: #111820;
1225                border-color: #30363d;
1226                color: #e6edf3;
1227              }
1228
1229              .ASM_root.ASM_paneDark .ASM_outputPopupHeader {
1230                color: #e6edf3;
1231                border-color: #30363d;
1232              }
1233
1234              .ASM_root.ASM_paneDark .ASM_outputPopupText {
1235                background: #000000;
1236                color: #e6edf3;
1237                -webkit-text-fill-color: #e6edf3;
1238              }
1239
1240              .ASM_paneTitleButton {
1241                -webkit-appearance: none;
1242                appearance: none;
1243                border: 1px solid rgba(0,0,0,0.35);
1244                border-radius: 4px;
1245                background: #f4f4f4;
1246                color: #000000;
1247                padding: 2px 6px;
1248                font: 11px/1.2 arial, sans-serif;
1249                cursor: pointer;
1250              }
1251
1252              .ASM_paneTitleButton:hover {
1253                border-color: #0078d4;
1254              }
1255
1256              .ASM_paneTitle .ASM_findReplaceMount {
1257                margin: 0px;
1258                max-width: 430px;
1259                min-height: 24px;
1260              }
1261
1262              .ASM_root.ASM_headerDark .ASM_paneTitle {
1263                background: #111820;
1264                border-color: #30363d;
1265                color: #e6edf3;
1266              }
1267
1268              .ASM_root.ASM_headerDark .ASM_paneTitle strong {
1269                color: #e6edf3;
1270              }
1271
1272              .ASM_root.ASM_headerDark .ASM_paneInfo {
1273                color: #8b949e;
1274              }
1275
1276              .ASM_root.ASM_headerDark {
1277                background: #0b0f14;
1278                border-color: #30363d;
1279                color: #e6edf3;
1280              }
1281
1282              .ASM_root.ASM_headerDark .ASM_paneTitleButton {
1283                background: #21262d;
1284                border-color: #30363d;
1285                color: #e6edf3;
1286              }
1287
1288              .ASM_root .com-pane-header-default-widget {
1289                gap: 4px;
1290              }
1291
1292              .ASM_root .com-pane-fr-wrap {
1293                width: 100%;
1294                max-width: 100%;
1295                min-height: 420px;
1296                padding: 0px;
1297                overflow: hidden;
1298              }
1299
1300              .ASM_root .com-pane-fr-wrap textarea {
1301                background-color: transparent !important;
1302                background-image: none !important;
1303                color: #000000 !important;
1304                -webkit-text-fill-color: #000000 !important;
1305                caret-color: #000000;
1306              }
1307
1308              #ASM_sourcePane,
1309              .ASM_root #ASM_sourcePane,
1310              .ASM_root .com-pane-fr-wrap #ASM_sourcePane {
1311                color: #000000 !important;
1312                -webkit-text-fill-color: #000000 !important;
1313                caret-color: #000000;
1314              }
1315
1316              /* Let the ASM dot-matrix wrapper background remain visible through the
1317                find/replace overlay.  Do not blanket-clear every com-pane-fr element,
1318                because the match spans also use com-pane-fr-* classes. */
1319              .ASM_root .com-pane-fr-wrap > .com-pane-fr-overlay {
1320                background-color: transparent !important;
1321                background-image: none !important;
1322              }
1323
1324              /* Keep the textarea itself transparent so the wrapper pattern shows through. */
1325              .ASM_root .com-pane-fr-wrap > textarea {
1326                background-color: transparent !important;
1327                background-image: none !important;
1328              }
1329
1330              /* Restore actual find/replace highlights. */
1331              .ASM_root .com-pane-fr-wrap .com-pane-fr-selection {
1332                background-color: rgba(190,190,190,.35) !important;
1333              }
1334
1335              .ASM_root .com-pane-fr-wrap .com-pane-fr-match {
1336                background-color: rgba(255,210,70,.28) !important;
1337              }
1338
1339              .ASM_root .com-pane-fr-wrap .com-pane-fr-current {
1340                background-color: rgba(255,185,45,.85) !important;
1341                color: #000000 !important;
1342              }
1343
1344
1345              /* Keep the source pane rendering identical to the listing pane, even after
1346                 PANE.bindFindReplace() wraps ASM_sourcePane and injects its own CSS. */
1347              .ASM_root .com-pane-fr-wrap,
1348              .ASM_root .com-pane-fr-wrap textarea,
1349              .ASM_root .com-pane-fr-wrap pre,
1350              .ASM_root .com-pane-fr-wrap code,
1351              .ASM_root .com-pane-fr-wrap .com-pane-fr-overlay,
1352              .ASM_root .com-pane-fr-wrap .com-pane-fr-layer,
1353              .ASM_root .com-pane-fr-wrap .com-pane-fr-text,
1354              .ASM_root .com-pane-fr-wrap .com-pane-fr-match-layer {
1355                font-family: var(--ASM_monoFont) !important;
1356                font-size: var(--ASM_monoSize) !important;
1357                font-weight: 500 !important;
1358                line-height: var(--ASM_monoLineHeight) !important;
1359                letter-spacing: normal !important;
1360                font-kerning: none;
1361                font-variant-ligatures: none;
1362                text-rendering: auto;
1363              }
1364
1365              .ASM_root .com-pane-fr-wrap #ASM_sourcePane {
1366                 font-family: var(--ASM_monoFont) !important;
1367                 font-size: var(--ASM_monoSize) !important;
1368                 font-weight: 500 !important;
1369                 line-height: var(--ASM_monoLineHeight) !important;
1370              }
1371
1372
1373              /* Match the effective Source pane inset to the Listing pane.
1374                 The listing textarea gets its final padding from dotmatrix_bck
1375                 (0px top/bottom, 30px left/right).  After find/replace wraps
1376                 ASM_sourcePane, the wrapper/overlay stack must not add another
1377                 inset or fall back to the generic 14px editor padding. */
1378              .ASM_root .com-pane-fr-wrap {
1379                padding: 0px !important;
1380              }
1381
1382              .ASM_root .com-pane-fr-wrap #ASM_sourcePane,
1383              .ASM_root .com-pane-fr-wrap textarea,
1384              .ASM_root .com-pane-fr-wrap .com-pane-fr-overlay,
1385              .ASM_root .com-pane-fr-wrap .com-pane-fr-layer,
1386              .ASM_root .com-pane-fr-wrap .com-pane-fr-text,
1387              .ASM_root .com-pane-fr-wrap .com-pane-fr-match-layer {
1388                padding: 0px 30px !important;
1389                box-sizing: border-box;
1390              }
1391
1392/* Source pane: PANE.bindFindReplace() adds wrapper/overlay layers.
1393   Keep those transparent so the shared dotmatrix_bck skin remains visible. */
1394.ASM_root .com-pane-fr-wrap textarea,
1395.ASM_root .com-pane-fr-wrap > .com-pane-fr-overlay,
1396.ASM_root .com-pane-fr-wrap .com-pane-fr-layer,
1397.ASM_root .com-pane-fr-wrap .com-pane-fr-text,
1398.ASM_root .com-pane-fr-wrap .com-pane-fr-match-layer {
1399  background-color: transparent !important;
1400  background-image: none !important;
1401}
1402
1403/* JSON pane: the generated <pre> must not cover ASM_jsonMount's background. */
1404.ASM_jsonMount .ASM_jsonView {
1405  background: transparent !important;
1406}
1407
1408/* Source pane light mode:
1409   PANE.bindFindReplace() wraps the source textarea in .com-pane-fr-wrap.
1410   The wrapper can otherwise keep/receive PANE's plain dark background.
1411   Put the shared COM_MAIN.css dotmatrix_bck skin on the source section and
1412   let the generated wrapper inherit that skin.  This avoids duplicating the
1413   base64 background image in index.html. */
1414.ASM_root:not(.ASM_paneDark) .ASM_pane.ASM_sourcePaneHost .com-pane-fr-wrap.dotmatrix_bck {
1415  background-color: inherit !important;
1416  background-image: inherit !important;
1417  background-size: inherit !important;
1418  background-attachment: inherit !important;
1419  background-repeat: inherit !important;
1420}
1421
1422
1423/* Theme the pane interiors, not only the pane headers.
1424   Light mode uses the shared .dotmatrix_bck pattern from COM_MAIN.css.
1425   Dark mode deliberately removes the image and uses plain black. */
1426.ASM_root.ASM_paneDark .dotmatrix_bck,
1427.ASM_root.ASM_paneDark .com-pane-fr-wrap {
1428  background-color: #000000 !important;
1429  background-image: none !important;
1430  color: #e6edf3 !important;
1431  -webkit-text-fill-color: #e6edf3 !important;
1432  caret-color: #e6edf3;
1433}
1434
1435.ASM_root.ASM_paneDark .ASM_editor,
1436.ASM_root.ASM_paneDark .ASM_jsonMount,
1437.ASM_root.ASM_paneDark .com-pane-fr-wrap textarea,
1438.ASM_root.ASM_paneDark .com-pane-fr-overlay {
1439  color: #e6edf3 !important;
1440  -webkit-text-fill-color: #e6edf3 !important;
1441  caret-color: #e6edf3;
1442}
1443
1444.ASM_root.ASM_paneDark .ASM_jsonMount::placeholder {
1445  color: rgba(230,237,243,0.58);
1446  opacity: 1;
1447}
1448
1449.ASM_root.ASM_paneDark .ASM_jsonMount *,
1450.ASM_root.ASM_paneDark .ASM_jsonKey,
1451.ASM_root.ASM_paneDark .ASM_jsonString,
1452.ASM_root.ASM_paneDark .ASM_jsonNumber,
1453.ASM_root.ASM_paneDark .ASM_jsonBoolean,
1454.ASM_root.ASM_paneDark .ASM_jsonNull {
1455  color: #e6edf3 !important;
1456}
1457
1458/* Beat the earlier #ASM_sourcePane light-mode rule, which also uses
1459   !important and otherwise keeps the source text black in dark mode. */
1460.ASM_root.ASM_paneDark #ASM_sourcePane,
1461.ASM_root.ASM_paneDark .com-pane-fr-wrap #ASM_sourcePane {
1462  color: #e6edf3 !important;
1463  -webkit-text-fill-color: #e6edf3 !important;
1464  caret-color: #e6edf3 !important;
1465}
1466
1467.ASM_root.ASM_paneDark .com-pane-fr-current {
1468  color: #000000 !important;
1469  -webkit-text-fill-color: #000000 !important;
1470}
1471
1472/*
1473 * The find/replace overlay should paint highlights only.  ASM_sourcePane is
1474 * deliberately visible so Source and Listing are both rendered by a native
1475 * textarea with exactly the same font metrics.  Leaving the overlay glyphs
1476 * visible draws every source character twice and produces the heavier,
1477 * slightly displaced text visible in the regression screenshot.
1478 */
1479.ASM_root .com-pane-fr-wrap > .com-pane-fr-overlay,
1480.ASM_root .com-pane-fr-wrap > .com-pane-fr-overlay * {
1481  color: transparent !important;
1482  -webkit-text-fill-color: transparent !important;
1483}
1484
1485            </style>
1486
1487            <div id="ASM_root" class="ASM_root">
1488
1489              <div style="height:30px;padding:0px 90px 0px 0px;">
1490                <button class=appbut value="Settings" onclick="javascript:oCOM.POPUP.runRule('tab2.1');" title="Settings"><i class="fa fa fa-sliders-h"></i></button>
1491                <button class=appbut value="Tools"    onclick="javascript:oCOM.POPUP.runRule('tab2.2');" title="Tools"><i class="fa fa-toolbox"></i></button>          
1492              </div>
1493
1494              <!--  ASSEMBLER SETTINGS -->
1495              <div class="popupHost" id="tab2.1" hidden>
1496                <div class=toolbox>
1497                  <div class=appbox title="Assembler settings" style="min-height:63px;height:auto;padding:0px 6px 0px 6px;color:black;text-align: left;">
1498                    COMPILER<br>
1499                    <label title="Legacy double-quote mode: when checked, double-quoted character literals force Apple II high-bit text. When unchecked, double quotes compile like single quotes.">
1500                      Quote legacy <input id="ASM_dQuoteLegacyInput" type="checkbox">
1501                    </label>
1502                  </div>
1503                </div>
1504
1505                <div class=toolbox>
1506                  <div class=appbox title="Assembler settings" style="min-height:63px;height:auto;padding:0px 6px 0px 6px;color:black;text-align: left;">
1507                    LISTING<br>
1508                    <label>
1509                      Columns <input id="ASM_settingsListingColumnsInput" type="text" value="{adr:0,code:6,lin:15,num:{col:23,dig:6},lbl:30,ins:40,opr:45,com:61}" title="Listing columns: address, code bytes, jump lines, num:{col,dig}, label, instruction, operand, compatible assemblers for pragmas, comment" spellcheck="false" style="font-family:monospace;font-size:11px">
1510                    </label>
1511                    <div class="ASM_listingPresetList" aria-label="Listing column presets">
1512                      <button type="button" class="ASM_listingPreset" data-listing-preset
1512="default" title="Use default listing columns and assemble">
1513                        <span>default</span>
1514                        <i class="fa fa-table"></i>
1515                      </button>
1516                      <button type="button" class="ASM_listingPreset" data-listing-preset="print" title="Use print listing columns and assemble">
1517                        <span>print</span>
1518                        <i class="fa fa-table"></i>
1519                      </button>
1520                      <button type="button" class="ASM_listingPreset" data-listing-preset="compile" title="Use compact compile listing columns and assemble">
1521                        <span>compile</span>
1522                        <i class="fa fa-table"></i>
1523                      </button>
1524                    </div>
1525                  </div>
1526                </div>
1527
1528
1529              </div>
1530
1531              <!--  ASSEMBLER TOOLS -->
1532              <div class="popupHost" id="tab2.2" hidden>
1533                <div class=toolbox>
1534                  <form name="ASMboard" onsubmit="return false">
1535                    <textarea id="ASM_boardText" cols=40 rows=4 style="float:left" placeholder="Byte-code output" name="boardText"></textarea><br>
1536                    <input title="Send byte code to emulator" id="ASM_toEmulatorButton" class="ASM_toEmulatorButton" type="button" value="to emulator">
1537                    <input title="Send byte code to debugger" id="ASM_toDebuggerButton" class="ASM_toDebuggerButton" type="button" value="to debugger">
1538                    <input title="Copy byte code to clipboard" type="button" id="ASM_copyByteCodeButton" value="copy">
1539                    <input title="Clear byte code" type="button" value="Clear" onclick="javascript:ASM_clearBoard()">
1540                    <span id="ASM_byteCodeInfo" style="color:#000000;font-family:arial;font-size:11px;padding:0px 6px;white-space:nowrap;">0 bytes</span>
1541                    <input type="button" value="X" onclick="javascript:oCOM.POPUP.off('tab2.2')">
1542                  </form>
1543                </div>
1544
1545                <!--  ASM ACTION BUTTONS -->
1546                <div class=toolbox>
1547                  <!--button id="ASM_assembleButton" class=appbut type="button" title="Assemble"><i class="fa fa-cogs"></i></button-->
1548                  <button id="ASM_toggleIncludeButton" class="appbut greyed" type="button" title="No include file loaded" disabled><i class="fa fa-file-code"></i></button>
1549                  <br>
1550                  <button id="ASM_assembleButton" class=appbut type="button" data-pane-default-act="assemble" title="Assemble"><i class="fa fa-play"></i></button>
1551                </div>
1552
1553                <!--  ASM FILE TOOLBOX -->
1554                <div class=toolbox>
1555                  <div class=appbox style="min-height:63px;height:auto;padding:0px 6px 0px 6px;">
1556                    <div class=appbut style="color:black">
1557                      load include files<input id="ASM_includeFileInput" style="display:inline-block" type="file" multiple accept=".INC,.inc,.S,.s,.ASM,.asm,.txt,.src,.a65,.mac,text/plain">
1558                    </div>
1559                    <div id="ASM_includeLabelWrap" class="ASM_includeLabelWrap"></div>
1560                    <div class=appbut style="color:black">
1561                      load assembler code<input id="ASM_fileInput" style="display:inline-block" type="file" accept=".S,.s,.ASM,.asm,.INC,.inc,.txt,text/plain">
1562                    </div>
1563                    <div class="appbut" style="color:black"
1564                          title="Start address used when byte-encoding the source pane into monitor byte code">
1565                      byte encode source
1566                      <input id="ASM_sourceByteCodeStartInput"
1567                             type="text"
1568                             value="$6000"
1569                             maxlength="6"
1570                             spellcheck="false"
1571                             style="width:64px;min-height:20px;font:12px/1.2 courier,monospace;text-transform:uppercase;box-sizing:border-box;">
1572                      <div id="ASM_encodeSourceButton"
1573                           class="appbut mini"
1574                           style="color:black"
1575                           title="Byte encode source at this address"
1576                           role="button"
1577                           tabindex="0"><i class="fa fa-scroll"></i></div>
1578                      <span id="ASM_sourceByteCodeRangeInfo"
1579                            style="color:#000000;font-family:arial;font-size:11px;padding:0px 4px;white-space:nowrap;">A$6000 L$0000</span>
1580                  </div>
1581                    <!-- Status is logged through ASM_setStatus(); find/replace lives in the Source pane header. -->
1582                  </div>
1583                </div>
1584
1585                <!--  ASM SOURCE CATALOG TOOLBOX -->
1586                <div class=toolbox>
1587                  <div class=appbox style="text-align:left;height:63px;padding:0px 6px 0px 6px;">
1588                    <button id="ASM_sourceCatalogButton"
1589                            class=appbut
1590                            type="button"
1591                            title="Source Code Catalog"
1592                            onclick="ASM_sourceCatalogOpen();event.stopPropagation();">
1593                      <i class="fa fa-cat"></i>
1594                    </button>
1595                  </div>
1596                </div>
1597
1598              </div>
1599
1600              <div id="ASM_paneHost" class="ASM_paneHost">
1601
1602                <section class="ASM_pane ASM_sourcePaneHost dotmatrix_bck">
1603                  <div class="ASM_paneTitle">
1604                    <span class="ASM_paneTitleMain">
1605                      <strong>Source</strong>
1606                      <input id="ASM_sourceFileNameInput"
1607                             type="text"
1608                             value="sample.S"
1609                             title="Source filename"
1610                             spellcheck="false"
1611                             style="width:150px;max-width:22vw;border:1px solid rgba(0,0,0,0.25);border-radius:4px;background:#ffffff;color:#000000;padding:2px 5px;font:11px/1.2 courier,monospace;">           
1612                      <button id="ASM_downloadSourceButton"
1613                              class="ASM_paneTitleButton"
1614                              type="button"
1615                              title="Download source"><i class="fa fa-cloud-download-alt"></i></button>
1616                    </span>
1617                    <span id="ASM_sourceFindReplaceMount" class="ASM_findReplaceMount pane-title-search-host"></span>
1618                  </div>
1619                  <textarea id="ASM_sourcePane" class="ASM_editor dotmatrix_bck" spellcheck="false" wrap="off" placeholder="Source code">ORG $6000
1620       LDX #0
1621Label1 TXA
1622       STA $0400,X
1623       LDA #1
1624       STA $D800,X
1625       INX
1626       BNE Label1
1627       RTS
1628.END</textarea>
1629                </section>
1630                <section class="ASM_pane">
1631                  <div class="ASM_paneTitle">
1632                    <span class="ASM_paneTitleMain">
1633                      <strong>Listing</strong>
1634                      <input id="ASM_symbolFileNameInput"
1635                             type="text"
1636                             value="sample.SYM"
1637                             title="Symbol filename"
1638                             spellcheck="false"
1639                             style="width:150px;max-width:22vw;border:1px solid rgba(0,0,0,0.25);border-radius:4px;background:#ffffff;color:#000000;padding:2px 5px;font:11px/1.2 courier,monospace;">
1640                      <button id="ASM_exportSymbolsButton"
1641                              class="ASM_paneTitleButton"
1642                              type="button"
1643                              title="Download symbols"
1644                              onclick="ASM_exportSymbols()">
1645                              <i class="fa fa-cloud-download-alt"></i>
1646                      </button>
1647                    
1648                    </span>
1649
1650
1651                    <span class="ASM_paneTitleMain">
1652                    
1653                    <div title="Send byte code to emulator" id="ASM_toEmulatorHeaderButton" class="ASM_toEmulatorButton appbut skinny"><i class="fa fa-apple-alt"></i>&nbsp;<i class="fa fa-caret-left"></i></div> | 
1654                    <div title="Send byte code to debugger" id="ASM_toDebuggerHeaderButton" class="ASM_toDebuggerButton appbut skinny"><i class="fa fa-caret-right"></i>&nbsp;<i class="fa fa-bug"></i></div>                    
1655
1656                    <button type="button" id="ASM_diagnosticsButton" class="appbut skinny" title="No assembly diagnostics" hidden disabled>
1657                      <i class="fa fa-exclamation-triangle"></i>
1658                    </button>
1659                    <button type="button" id="ASM_jsonPopupButton" class="appbut skinny" title="Show JSON output">
1660                      <i class="fa fa-stethoscope"></i>
1661                    </button>
1662
1663                      <!--button id="ASM_exportSymbolsButton"
1664                              class="ASM_paneTitleButton"
1665                              type="button"
1666                              title="Export symbols"
1667                              onclick="ASM_exportSymbols()">
1668                        <i class="fa fa-download"></i>
1669                      </button-->
1670                    </span>
1671                  </div>
1672                  <textarea id="ASM_listingPane" class="ASM_editor dotmatrix_bck" readonly="readonly" spellcheck="false" wrap="off" placeholder="Listing"></textarea>
1673                </section>
1674
1675                <section id="ASM_includePane" class="ASM_pane ASM_includePane">
1676                  <div class="ASM_paneTitle">
1677                    <strong>Include</strong>
1678                    <span class="ASM_paneTitleMain">
1679                      <span id="ASM_includeInfo" class="ASM_paneInfo"></span>
1680                      <div id="ASM_copyIncludeButton" class="ASM_paneTitleButton" title="Copy include file"><i class="fa fa-copy"></i></div>
1681                      <button type="button" id="ASM_closeIncludeButton" class="appbut skinny" title="Hide include file">
1682                        <i class="fa fa-times"></i>
1683                      </button>
1684                    </span>
1685                  </div>
1686                   <textarea id="ASM_includeMount"
1687                             class="ASM_jsonMount dotmatrix_bck"
1688                             aria-label="Include file source"
1689                             readonly="readonly"
1690                             spellcheck="false"
1691                             wrap="off"
1692                             placeholder="Include file source"></textarea>
1693                </section>
1694                
1695              </div>
1696
1697              <div id="ASM_outputPopupBack" class="ASM_outputPopupBack" hidden aria-hidden="true">
1698                <section class="ASM_outputPopup" role="dialog" aria-modal="true" aria-labelledby="ASM_outputPopupTitle">
1699                  <div class="ASM_outputPopupHeader">
1700                    <strong id="ASM_outputPopupTitle">JSON output</strong>
1701                    <span class="ASM_outputPopupActions">
1702                      <button type="button" id="ASM_outputPopupCopy" class="appbut skinny" title="Copy"><i class="fa fa-copy"></i></button>
1703                      <button type="button" id="ASM_outputPopupClose" class="appbut skinny" title="Close"><i class="fa fa-times"></i></button>
1704                    </span>
1705                  </div>
1706                  <textarea id="ASM_outputPopupText" class="ASM_outputPopupText" readonly="readonly" spellcheck="false" wrap="off"></textarea>
1707                </section>
1708              </div>
1709
1710              <section id="ASM_manualPane" class="CRT_TV DOUBL_TV" style="width:100%;max-width:100%;margin:10px 0px 0px 0px;padding:0px;border-radius:10px;overflow:hidden;">
1711                <div style="height:120px"></div>
1712                <div name="codefield" id="ASM_manual" class="markdown_mon" style="border:1px solid transparent;width:600px;height:420px;background-color:transparent;resize:none;overflow-y:auto;"></div>
1713                <div style="height:300px"></div>
1714              </section>
1715            </div>
1716
1717            
1717<script type="text/javascript">
1718            var ASM_currentFileName = "sample.S";
1719            var ASM_includeFiles = [];
1720            var ASM_nextIncludeFileId = 1;
1721            var ASM_selectedIncludeId = null;
1722
1723            /* ASM INCLUDE FILES BEGIN */
1724            function ASM_includeNameKey(name)
1725            {
1726              return String(name || "").trim().toUpperCase();
1727            }
1728
1729            function ASM_findIncludeIndexByName(name)
1730            {
1731              var key = ASM_includeNameKey(name);
1732              for(var i=0;i<ASM_includeFiles.length;i++)
1733                if(ASM_includeNameKey(ASM_includeFiles[i].name) === key) return i;
1734              return -1;
1735            }
1736
1737            function ASM_findIncludeIndexById(id)
1738            {
1739              id = String(id || "");
1740              for(var i=0;i<ASM_includeFiles.length;i++)
1741                if(String(ASM_includeFiles[i].id) === id) return i;
1742              return -1;
1743            }
1744
1745            function ASM_currentIncludeFile()
1746            {
1747              var index = ASM_findIncludeIndexById(ASM_selectedIncludeId);
1748              return index >= 0 ? ASM_includeFiles[index] : null;
1749            }
1750
1751            function ASM_buildIncludeMap()
1752            {
1753              var includes = {};
1754              for(var i=0;i<ASM_includeFiles.length;i++)
1755              {
1756                var file = ASM_includeFiles[i];
1757                includes[file.name] = {
1758                  source:String(file.source == null ? "" : file.source),
1759                  sourceName:file.sourceName || file.name
1760                };
1761              }
1762              return includes;
1763            }
1764
1765            function ASM_updateIncludePane()
1766            {
1767              var file = ASM_currentIncludeFile();
1768              var mount = ASM_el("ASM_includeMount");
1769              var info = ASM_el("ASM_includeInfo");
1770
1771              if(mount)
1772              {
1773                mount.value = file ? String(file.source == null ? "" : file.source) : "";
1774                mount.scrollTop = 0;
1775                mount.scrollLeft = 0;
1776              }
1777
1778              if(info)
1779              {
1780                info.textContent = file ? file.name : "";
1781                info.title = file ? (file.sourceName || file.name) : "";
1782              }
1783            }
1784
1785            function ASM_syncIncludeToggleButton()
1786            {
1787              var host = ASM_el("ASM_paneHost");
1788              var btn = ASM_el("ASM_toggleIncludeButton");
1789              if(!host || !btn) return;
1790
1791              var hasIncludes = ASM_includeFiles.length > 0;
1792              if(!hasIncludes) host.classList.remove("ASM_showInclude");
1793
1794              btn.disabled = !hasIncludes;
1795              btn.classList.toggle("greyed",!hasIncludes);
1796              btn.title = !hasIncludes
1797                ? "No include file loaded"
1798                : (host.classList.contains("ASM_showInclude") ? "Hide include file" : "Show include file");
1799            }
1800
1801            function ASM_selectIncludeFile(id,showPane)
1802            {
1803              var index = ASM_findIncludeIndexById(id);
1804              if(index < 0 && ASM_includeFiles.length) index = 0;
1805
1806              ASM_selectedIncludeId = index >= 0 ? String(ASM_includeFiles[index].id) : null;
1807              ASM_updateIncludePane();
1808              ASM_renderIncludeLabels();
1809
1810              if(showPane !== false && index >= 0) ASM_toggleIncludePane(true);
1811              return index >= 0;
1812            }
1813
1814            function ASM_renderIncludeLabels()
1815            {
1816              var wrap = ASM_el("ASM_includeLabelWrap");
1817              if(!wrap) return;
1818              wrap.replaceChildren();
1819
1820              for(var i=0;i<ASM_includeFiles.length;i++)
1821              {
1822                var file = ASM_includeFiles[i];
1823                var chip = document.createElement("div");
1824                chip.className = "appbut label" + (String(file.id) === String(ASM_selectedIncludeId) ? " ASM_includeSelected" : "");
1825                chip.title = "Show " + (file.sourceName || file.name);
1826                chip.setAttribute("role","button");
1827                chip.setAttribute("tabindex","0");
1828                chip.setAttribute("data-include-id",file.id);
1829                chip.appendChild(document.createTextNode(file.name + " "));
1830                chip.onclick = function(ev)
1831                {
1832                  ev.preventDefault();
1833                  ASM_selectIncludeFile(this.getAttribute("data-include-id"),true);
1834                };
1835                chip.onkeydown = function(ev)
1836                {
1837                  if(ev.key == "Enter" || ev.key == " ")
1838                  {
1839                    ev.preventDefault();
1840                    ASM_selectIncludeFile(this.getAttribute("data-include-id"),true);
1841                  }
1842                };
1843
1844                var remove = document.createElement("div");
1845                remove.className = "appbut skinny ASM_includeRemove";
1846                remove.title = "Remove include file";
1847                remove.textContent = "x";
1848                remove.setAttribute("data-include-id",file.id);
1849                remove.onclick = function(ev)
1850                {
1851                  ev.stopPropagation();
1852                  ASM_removeIncludeFile(this.getAttribute("data-include-id"));
1853                };
1854
1855                chip.appendChild(remove);
1856                wrap.appendChild(chip);
1857              }
1858
1859              ASM_syncIncludeToggleButton();
1860            }
1861
1862            function ASM_addIncludeText(name,text,sourceName)
1863            {
1864              name = String(name || "include.INC").trim() || "include.INC";
1865              var index = ASM_findIncludeIndexByName(name);
1866              var replaced = index >= 0;
1867
1868              if(replaced)
1869              {
1870                ASM_includeFiles[index].name = name;
1871                ASM_includeFiles[index].source = String(text == null ? "" : text);
1872                ASM_includeFiles[index].sourceName = sourceName || name;
1873              }
1874              else
1875              {
1876                ASM_includeFiles.push({
1877                  id:"asminc_" + (ASM_nextIncludeFileId++),
1878                  name:name,
1879                  sourceName:sourceName || name,
1880                  source:String(text == null ? "" : text)
1881                });
1882              }
1883
1884              if(!ASM_currentIncludeFile() && ASM_includeFiles.length)
1885                ASM_selectedIncludeId = String(ASM_includeFiles[0].id);
1886
1887              ASM_updateIncludePane();
1888              ASM_renderIncludeLabels();
1889              return {name:name,replaced:replaced};
1890            }
1891
1892            function ASM_removeIncludeFile(id)
1893            {
1894              id = String(id || "");
1895              var removed = null;
1896              var host = ASM_el("ASM_paneHost");
1897              var wasOpen = !!(host && host.classList.contains("ASM_showInclude"));
1898              var removedSelected = String(ASM_selectedIncludeId || "") === id;
1899
1900              ASM_includeFiles = ASM_includeFiles.filter(function(file)
1901              {
1902                if(String(file.id) === id) { removed = file; return false; }
1903                return true;
1904              });
1905
1906              if(!removed) return false;
1907
1908              if(removedSelected || !ASM_currentIncludeFile())
1909                ASM_selectedIncludeId = ASM_includeFiles.length ? String(ASM_includeFiles[0].id) : null;
1910
1911              ASM_updateIncludePane();
1912              ASM_renderIncludeLabels();
1913
1914              if(!ASM_includeFiles.length) ASM_toggleIncludePane(false);
1915              else if(wasOpen) ASM_toggleIncludePane(true);
1916
1917              ASM_clearOutputs(
1918                "Removed include " + removed.name + ". Reassemble to refresh the listing and byte code.",
1919                "warn"
1920              );
1921              return true;
1922            }
1923
1924            async function ASM_loadIncludeFiles(fileList)
1925            {
1926              var files = Array.prototype.slice.call(fileList || []);
1927              if(!files.length) return {loaded:0,replaced:0};
1928
1929              var loaded = 0;
1930              var replaced = 0;
1931
1932              for(var i=0;i<files.length;i++)
1933              {
1934                var file = files[i];
1935                var result = ASM_addIncludeText(
1936                  file.name || "include.INC",
1937                  await file.text(),
1938                  file.name || "include.INC"
1939                );
1940                if(result.replaced) replaced++; else loaded++;
1941              }
1942
1943              var message = "Loaded " + loaded + " include file" + (loaded === 1 ? "" : "s");
1944              if(replaced) message += "; replaced " + replaced + " existing include" + (replaced === 1 ? "" : "s");
1945              message += ". Reassemble to refresh the listing and byte code.";
1946              ASM_clearOutputs(message,"ok");
1947
1948              return {loaded:loaded,replaced:replaced};
1949            }
1950            /* ASM INCLUDE FILES END */
1951
1952            var ASM_currentJsonText = "";
1953            var ASM_currentDiagnosticsText = "";
1954            var ASM_currentByteCodeText = "";
1955            var ASM_currentErrorCount = 0;
1956            var ASM_currentWarningCount = 0;
1957            var ASM_outputPopupKind = "";
1958            var ASM_sourceByteCodeActive = false;
1959            var ASM_oPANE = null;
1960            var ASM_sourceFindReplace = null;
1961            var ASM_sourceHeaderWidgetManager = null;
1962            var ASM_sourceEditHistory = {
1963              undo: [],
1964              redo: [],
1965              outdent: [],
1966              max: 100
1967            };
1968            var ASM_headerThemeDark = false;
1969
1970            var ASM_listingColumnPresets = {
1971              "default":"{adr:0,code:6,lin:15,num:{col:23,dig:6},lbl:30,ins:40,opr:45,com:61}",
1972              "print":"{adr:0,code:6,lin:15,num:{col:25,dig:6},lbl:32,ins:40,opr:45,asm:61,com:70}",
1973              "compile":"{adr:0,code:6,lbl:15,ins:25,opr:30,com:55}"
1974            };
1975
1976            function ASM_el(id) { return document.getElementById(id); }
1977
1978            function ASM_applyConfigVars(vars, target, prefix)
1979            {
1980              target = target || document.documentElement;
1981              prefix = prefix || "--";
1982              for(var k in vars) target.style.setProperty(prefix + k, vars[k]);
1983            }
1984
1985            var ASM_findReplaceScheme = {
1986              "bg":"#f3f3f3",
1987              "border":"#d2d2d2",
1988              "text":"#000000",
1989              "muted":"#666666",
1990              "field-bg":"#ffffff",
1991              "field-text":"#000000",
1992              "active":"#0078d4",
1993              "active-bg":"#e9f4ff",
1994              "active-text":"#000000",
1995              "hover-bg":"#ececec",
1996              "hover-text":"#000000",
1997              "expander":"#000000"
1998            };
1999
2000            function ASM_applyFindReplaceScheme()
2001            {
2002              var mount = ASM_el("ASM_sourceFindReplaceMount");
2003              if(mount) ASM_applyConfigVars(ASM_findReplaceScheme,mount,"--com-pane-fr-");
2004            }
2005
2006            function ASM_countByteCodeText(text)
2007            {
2008              var s = String(text || "").toUpperCase();
2009
2010              // Remove common monitor/listing address prefixes such as "6000:".
2011              s = s.replace(/(^|[\r\n])\s*[0-9A-F]{4}\s*:/g,"$1");
2012
2013              var m = s.match(/\b[0-9A-F]{2}\b/g);
2014              return m ? m.length : 0;
2015            }
2016
2017            function ASM_updateByteCodeInfo(byteCount)
2018            {
2019              var info = ASM_el("ASM_byteCodeInfo");
2020              if(info) info.textContent = (byteCount || 0) + " bytes";
2021            }
2022
2023            function ASM_syncByteCodeFromBoard()
2024            {
2025              var board = ASM_el("ASM_boardText");
2026              ASM_currentByteCodeText = board ? board.value : "";
2027              ASM_updateByteCodeInfo(ASM_countByteCodeText(ASM_currentByteCodeText));
2028            }
2029
2030            function ASM_setByteCodeOutput(text, byteCount)
2031            {
2032              ASM_currentByteCodeText = text || "";
2033
2034              var board = ASM_el("ASM_boardText");
2035              if(board) board.value = ASM_currentByteCodeText;
2036
2037              ASM_updateByteCodeInfo(typeof byteCount == "number" ? byteCount : ASM_countByteCodeText(ASM_currentByteCodeText));
2038            }
2039
2040            /*
2041             * Source byte-code origin.
2042             *
2043             * This deliberately belongs to the assembler toolbox instead of the
2044             * debugger.  The debugger's "Go to PC" controls execution/listing
2045             * position; this address controls where the source text buffer will
2046             * be pasted into Apple II memory before BSAVE ...,T writes it as a
2047             * DOS text file.
2048             */
2049            function ASM_hexWord(v)
2050            {
2051              if(typeof oCOM != "undefined" && oCOM && typeof oCOM.getHexWord == "function")
2052                return oCOM.getHexWord(v & 0xFFFF);
2053              return ("000" + ((v & 0xFFFF).toString(16).toUpperCase())).slice(-4);
2054            }
2055
2056            function ASM_parseHexAddress(text)
2057            {
2058              text = String(text == null ? "" : text).trim();
2059              text = text.replace(/^\$/,"").replace(/^0x/i,"");
2060              if(!/^[0-9A-Fa-f]{1,4}$/.test(text)) return null;
2061
2062              var value = parseInt(text,16);
2063              if(isNaN(value) || value < 0 || value > 0xFFFF) return null;
2064              return value & 0xFFFF;
2065            }
2066
2067            function ASM_sourceByteCodeOrigin(reportError)
2068            {
2069              var input = ASM_el("ASM_sourceByteCodeStartInput");
2070              var raw = input ? input.value : "$6000";
2071              var origin = ASM_parseHexAddress(raw);
2072
2073              if(origin === null)
2074              {
2075                if(reportError !== false)
2076                  ASM_setStatus("Invalid source byte-code start address. Use hexadecimal $0000-$FFFF.","bad");
2077                return null;
2078              }
2079
2080              return origin;
2081            }
2082
2083            function ASM_normalizeSourceByteCodeStartInput()
2084            {
2085              var input = ASM_el("ASM_sourceByteCodeStartInput");
2086              var origin = ASM_sourceByteCodeOrigin(false);
2087              if(input && origin !== null) input.value = "$" + ASM_hexWord(origin);
2088              return origin;
2089            }      
2090
2091            function ASM_sourceTextBytes(text)
2092            {
2093              /*
2094               * Apple DOS text uses CR as its line separator.  Normalize every
2095               * browser/host newline form before encoding so CRLF does not
2096               * accidentally become two characters.
2097               */
2098              text = String(text == null ? "" : text)
2099                .replace(/\r\n/g,"\n")
2100                .replace(/\r/g,"\n")
2101                .replace(/\n/g,"\r");
2102
2103              var bytes = [];
2104              var replacements = 0;
2105
2106              for(var i = 0; i < text.length; i++)
2107              {
2108                var code = text.charCodeAt(i);
2109
2110                /*
2111                 * The memory buffer intentionally contains ordinary 7-bit
2112                 * ASCII.  BSAVE ...,T performs the Apple high-bit conversion.
2113                 *
2114                 * Replace NUL and non-ASCII UTF-16 characters with "?" rather
2115                 * than truncating them to misleading byte values.
2116                 */
2117                if(code === 0 || code > 0x7F)
2118                {
2119                  code = 0x3F;
2120                  replacements++;
2121                }
2122
2123                bytes.push(code & 0x7F);
2124              }
2125
2126              return {
2127                bytes: bytes,
2128                replacements: replacements
2129              };
2130            }
2131
2132            function ASM_formatSourceByteCode(bytes,origin)
2133            {
2134              var records = [];
2135              for(var i = 0; i < bytes.length; i++)
2136                records.push({pc:(origin + i) & 0xFFFF,val:bytes[i] & 0xFF});
2137
2138              /*
2139               * Reuse ASM_core's formatter whenever possible.  It emits the
2140               * same eight-byte Apple monitor lines as assembled byte code.
2141               */
2142              var formatter = window.oASM;
2143              if((!formatter || typeof formatter.formatByteCodeLines != "function") &&
2144                 typeof window.ASM == "function")
2145                formatter = new ASM();
2146
2147              if(formatter && typeof formatter.formatByteCodeLines == "function")
2148                return formatter.formatByteCodeLines(records,8).join("\n");
2149
2150              /*
2151               * Defensive fallback for a page where ASM_core.js failed to load.
2152               */
2153              var lines = [];
2154              for(var p = 0; p < bytes.length; p += 8)
2155              {
2156                var hex = [];
2157                for(var j = p; j < Math.min(p + 8,bytes.length); j++)
2158                  hex.push(("0" + bytes[j].toString(16).toUpperCase()).slice(-2));
2159
2160                lines.push(
2161                  ("000" + ((origin + p) & 0xFFFF).toString(16).toUpperCase()).slice(-4)
2162                  + ": "
2163                  + hex.join(" ")
2164                );
2165              }
2166              return lines.join("\n");
2167            }
2168
2169            function ASM_sourceByteCodeSaveCommand(filename,origin,length)
2170            {
2171              return "BSAVE " + filename
2172                + ",A$" + ASM_hexWord(origin)
2173                + ",L$" + ASM_hexWord(length)
2174                + ",T";
2175            }
2176
2177            function ASM_sourceByteCodePasteText(bytes,origin,filename)
2178            {
2179              /*
2180               * Build a complete Apple II paste sequence:
2181               *
2182               *   CALL-151          enter the monitor
2183               *   xxxx: .. ..       write the source buffer bytes
2184               *   E003G             return through DOS warm-start entry
2185               *   BSAVE ...,T       save the memory buffer as a DOS text file
2186               *
2187               * The final BSAVE line is intentionally not followed by an extra
2188               * newline here.  This keeps the generated board text from
2189               * auto-submitting the final command when it is copied manually.
2190               */
2191              return [
2192                "CALL-151",
2193                ASM_formatSourceByteCode(bytes,origin),
2194                "E003G",
2195                ASM_sourceByteCodeSaveCommand(filename,origin,bytes.length)
2196              ].join("\n");
2197            }
2198
2199            function ASM_setSourceByteCodeModeUI(active)
2200            {
2201              var btn = ASM_el("ASM_encodeSourceButton");
2202              if(btn)
2203              {
2204                //btn.classList.toggle("blink",!!active);
2205                btn.title = active
2206                  ? "Source byte-code mode is active; address/source changes refresh the byte code"
2207                  : "Byte encode source at this address";
2208              }
2209            }
2210
2211            function ASM_updateSourceByteCodeRangeInfo(origin,length)
2212            {
2213              var info = ASM_el("ASM_sourceByteCodeRangeInfo");
2214              if(!info) return;
2215
2216              if(origin === null)
2217              {
2218                info.textContent = "invalid A";
2219                info.title = "Invalid start address";
2220                return;
2221              }
2222
2223              length = Number(length) || 0;
2224              info.textContent = "A$" + ASM_hexWord(origin) + " L$" + ASM_hexWord(length);
2225              info.title = "Use " + ASM_sourceByteCodeSaveCommand(
2226                ASM_currentFileName || "source.S",
2227                origin,
2228                length
2229              );
2230            }
2231
2232            function ASM_renderSourceByteCode(options)
2233             {
2234              options = options || {};
2235
2236              var sourcePane = ASM_el("ASM_sourcePane");
2237              if(!sourcePane)
2238              {
2239                ASM_setStatus("Source pane is not available.","bad");
2240                return false;
2241              }
2242              var origin = ASM_sourceByteCodeOrigin(options.status !== false);
2243              if(origin === null)
2244              {
2245                ASM_updateSourceByteCodeRangeInfo(null,0);
2246                return false;
2247              }
2248
2249              if(options.normalizeInput !== false)
2250                ASM_normalizeSourceByteCodeStartInput();
2251
2252
2253              var encoded = ASM_sourceTextBytes(sourcePane.value);
2254              var bytes = encoded.bytes;
2255
2256              if(options.activate) ASM_sourceByteCodeActive = true;
2257              ASM_setSourceByteCodeModeUI(ASM_sourceByteCodeActive);
2258
2259              if(!bytes.length)
2260              {
2261                ASM_setByteCodeOutput("",0);
2262                ASM_updateSourceByteCodeRangeInfo(origin,0);
2263                if(options.status !== false)
2264                  ASM_setStatus("Source pane is empty; no bytes were generated.","warn");
2265                return false;
2266              }
2267
2268              if(bytes.length > (0x10000 - origin))
2269              {
2270                ASM_updateSourceByteCodeRangeInfo(origin,bytes.length);
2271                ASM_setStatus(
2272                  "Source text does not fit from $"
2273                  + ASM_hexWord(origin)
2274                  + " through $FFFF.",
2275                  "bad"
2276                );
2277                return false;
2278              }
2279
2280              var filename = ASM_setCurrentFileName(
2281                ASM_el("ASM_sourceFileNameInput")
2282                  ? ASM_el("ASM_sourceFileNameInput").value
2283                  : ASM_currentFileName
2284              );
2285
2286              ASM_setByteCodeOutput(
2287                ASM_sourceByteCodePasteText(bytes,origin,filename),
2288                bytes.length
2289              );
2290
2291              ASM_updateSourceByteCodeRangeInfo(origin,bytes.length);
2292
2293              if(options.status !== false)
2294              {
2295                ASM_setStatus(
2296                  "Encoded " + bytes.length + " source byte(s) at $" + ASM_hexWord(origin)
2297                  + (encoded.replacements
2298                      ? "; replaced " + encoded.replacements + " non-ASCII/NUL character(s)"
2299                      : "")
2300                  + ". Save as text with "
2301                  + ASM_sourceByteCodeSaveCommand(filename,origin,bytes.length)
2302                  + ".",
2303                  encoded.replacements ? "warn" : "ok"
2304                );
2305              }
2306
2307              return true;
2308            }
2309
2310
2311            function ASM_byteEncodeSource()
2312            {
2313              return ASM_renderSourceByteCode({activate:true,status:true});
2314            }
2315
2316            function ASM_refreshSourceByteCodeFromAddress()
2317            {
2318              var origin = ASM_sourceByteCodeOrigin(false);
2319
2320              if(!ASM_sourceByteCodeActive)
2321              {
2322                ASM_updateSourceByteCodeRangeInfo(origin,ASM_countByteCodeText(ASM_currentByteCodeText));
2323                return origin !== null;
2324              }
2325
2326              return ASM_renderSourceByteCode({
2327                status:false,
2328                normalizeInput:false
2329              });
2330            }
2331
2332            function ASM_listingColumnsValue()
2333            {
2334              var headerInput   = ASM_el("ASM_listingColumnsInput");
2335              var settingsInput = ASM_el("ASM_settingsListingColumnsInput");
2336              return headerInput ? headerInput.value : (settingsInput ? settingsInput.value : undefined);
2337            }
2338
2339            function ASM_dQuoteLegacyValue()
2340            {
2341              var input = ASM_el("ASM_dQuoteLegacyInput");
2342              return !!(input && input.checked);
2343            }
2344
2345            function ASM_syncListingColumns(sourceInput)
2346            {
2347              var headerInput   = ASM_el("ASM_listingColumnsInput");
2348              var settingsInput = ASM_el("ASM_settingsListingColumnsInput");
2349              var value = sourceInput ? sourceInput.value : ASM_listingColumnsValue();
2350
2351              if(headerInput && headerInput !== sourceInput)     headerInput.value = value;
2352              if(settingsInput && settingsInput !== sourceInput) settingsInput.value = value;
2353            }
2354
2355            function ASM_applyListingColumnPreset(name)
2356            {
2357              var spec = ASM_listingColumnPresets[name];
2358              if(!spec) return false;
2359
2360              var settingsInput = ASM_el("ASM_settingsListingColumnsInput");
2361              var headerInput   = ASM_el("ASM_listingColumnsInput");
2362
2363              if(settingsInput) settingsInput.value = spec;
2364              if(headerInput) headerInput.value = spec;
2365              ASM_syncListingColumns(settingsInput || headerInput);
2366              return ASM_assembleCurrentSource();
2367            }
2368
2369            function ASM_setPaneInfos(listingText, jsonState)
2370            {
2371              var listingInfo = ASM_el("ASM_listingInfo");
2372              var jsonInfo    = ASM_el("ASM_jsonInfo");
2373
2374              if(listingInfo)
2375              {
2376                if(listingText)
2377                {
2378                  var n = String(listingText).split(/\r\n|\r|\n/).length;
2379                  listingInfo.textContent = n + " lines";
2380                }
2381                else listingInfo.textContent = "compile()";
2382              }
2383
2384              if(jsonInfo)
2385              {
2386                jsonInfo.textContent = jsonState || "not assembled";
2387                jsonInfo.title = jsonInfo.textContent;
2388              }
2389            }
2390
2391
2392            function ASM_setErrorButtonState(errorCount,warningCount)
2393            {
2394              ASM_currentErrorCount = Number(errorCount) || 0;
2395              ASM_currentWarningCount = Number(warningCount) || 0;
2396
2397              var btn = ASM_el("ASM_diagnosticsButton");
2398              if(!btn) return;
2399
2400              var hasDiagnostics = ASM_currentErrorCount > 0 || ASM_currentWarningCount > 0;
2401
2402              btn.hidden = !hasDiagnostics;
2403              btn.disabled = !hasDiagnostics;
2404              btn.classList.remove("blink");
2405              var icon = btn.querySelector("i");
2406              if(icon) icon.classList.toggle("blink",ASM_currentErrorCount > 0);
2407
2408              if(ASM_currentErrorCount && ASM_currentWarningCount)
2409                btn.title = ASM_currentErrorCount + " error(s), " + ASM_currentWarningCount + " warning(s)";
2410              else if(ASM_currentErrorCount)
2411                btn.title = ASM_currentErrorCount + " assembly error(s)";
2412              else if(ASM_currentWarningCount)
2413                btn.title = ASM_currentWarningCount + " assembly warning(s)";
2414              else
2415                btn.title = "No assembly diagnostics";
2416
2417              if(!hasDiagnostics && ASM_outputPopupKind == "diagnostics") ASM_closeOutputPopup();
2418            }
2419
2420            function ASM_setDiagnosticsText(compiled)
2421            {
2422              compiled = compiled || {};
2423              ASM_currentDiagnosticsText = JSON.stringify({
2424                errors:Array.isArray(compiled.errors) ? compiled.errors : [],
2425                warnings:Array.isArray(compiled.warnings) ? compiled.warnings : []
2426              },null,2);
2427              ASM_refreshOutputPopup();
2428            }
2429
2430            function ASM_refreshOutputPopup()
2431            {
2432              var back = ASM_el("ASM_outputPopupBack");
2433              var text = ASM_el("ASM_outputPopupText");
2434              if(!back || !text || back.hidden) return;
2435
2436              text.value = ASM_outputPopupKind == "diagnostics"
2437                ? ASM_currentDiagnosticsText
2438                : ASM_currentJsonText;
2439            }
2440
2441            function ASM_openOutputPopup(kind)
2442            {
2443              var back = ASM_el("ASM_outputPopupBack");
2444              var text = ASM_el("ASM_outputPopupText");
2445              var title = ASM_el("ASM_outputPopupTitle");
2446              if(!back || !text || !title) return;
2447
2448              ASM_outputPopupKind = kind == "diagnostics" ? "diagnostics" : "json";
2449              title.textContent = ASM_outputPopupKind == "diagnostics" ? "Assembly diagnostics" : "JSON output";
2450              text.value = ASM_outputPopupKind == "diagnostics" ? ASM_currentDiagnosticsText : ASM_currentJsonText;
2451              text.scrollTop = 0;
2452              text.scrollLeft = 0;
2453              back.hidden = false;
2454              back.setAttribute("aria-hidden","false");
2455              text.focus();
2456            }
2457
2458            function ASM_closeOutputPopup()
2459            {
2460              var back = ASM_el("ASM_outputPopupBack");
2461              if(back)
2462              {
2463                back.hidden = true;
2464                back.setAttribute("aria-hidden","true");
2465              }
2466              ASM_outputPopupKind = "";
2467            }
2468
2469            function ASM_toggleOutputPopup(kind)
2470            {
2471              var back = ASM_el("ASM_outputPopupBack");
2472              kind = kind == "diagnostics" ? "diagnostics" : "json";
2473
2474              if(back && !back.hidden && ASM_outputPopupKind == kind) ASM_closeOutputPopup();
2475              else ASM_openOutputPopup(kind);
2476            }
2477
2478            function ASM_findJsonKeyElement(key)
2479            {
2480              var mount = ASM_el("ASM_jsonMount");
2481              if(!mount) return null;
2482
2483              /*
2484               * oCOM.JSONprettify() wraps JSON keys in .json-key spans.
2485               * Prefer the real rendered key element, because the JSON may contain
2486               * very large string values before "errors" (for example listingText).
2487               * A text-offset * line-height estimate does not survive that case.
2488               */
2489              var keys = mount.querySelectorAll(".json-key,.ASM_jsonKey");
2490              for(var i = 0; i < keys.length; i++)
2491              {
2492                var txt = String(keys[i].textContent || "").replace(/^\s+|\s+$/g,"");
2493                if(txt == '"' + key + '"' || txt == "'" + key + "'" || txt == key)
2494                  return keys[i];
2495              }
2496
2497              return null;
2498            }
2499
2500            function ASM_findJsonTextRangeForKey(key)
2501            {
2502              var mount = ASM_el("ASM_jsonMount");
2503              if(!mount) return null;
2504
2505              /*
2506               * Fallback for non-prettified JSON.  Find a key at the beginning of
2507               * a JSON line, not a random occurrence inside listingText/comments.
2508               */
2509              var full = mount.textContent || ASM_currentJsonText || "";
2510              var re = new RegExp("(^|\\n)\\s*[\\\"']" + key + "[\\\"']\\s*:", "m");
2511              var m = re.exec(full);
2512              if(!m) return null;
2513
2514              var targetOffset = m.index + (m[1] ? m[1].length : 0);
2515              var seen = 0;
2516              var walker = document.createTreeWalker(mount,NodeFilter.SHOW_TEXT,null,false);
2517              var node;
2518
2519              while((node = walker.nextNode()))
2520              {
2521                var len = node.nodeValue ? node.nodeValue.length : 0;
2522                if(seen + len >= targetOffset)
2523                {
2524                  var local = Math.max(0,targetOffset - seen);
2525                  var range = document.createRange();
2526                  range.setStart(node,local);
2527                  range.setEnd(node,Math.min(len,local + key.length + 2));
2528                  return range;
2529                }
2530                seen += len;
2531              }
2532
2533              return null;
2534            }
2535
2536            function ASM_scrollJsonTargetIntoView(target)
2537            {
2538              var mount = ASM_el("ASM_jsonMount");
2539              if(!mount || !target) return false;
2540
2541              var targetRect;
2542              if(typeof target.getBoundingClientRect == "function")
2543                targetRect = target.getBoundingClientRect();
2544              else if(typeof target.getClientRects == "function" && target.getClientRects().length)
2545                targetRect = target.getClientRects()[0];
2546
2547              if(!targetRect) return false;
2548
2549              var mountRect = mount.getBoundingClientRect();
2550              var lh = parseFloat(window.getComputedStyle(mount).lineHeight) || 21;
2551
2552              mount.scrollTop = Math.max(
2553                0,
2554                mount.scrollTop + (targetRect.top - mountRect.top) - (2 * lh)
2555              );
2556
2557              mount.focus();
2558              return true;
2559            }
2560
2561             function ASM_scrollJsonTextareaToKey(key)
2562             {
2563               var mount = ASM_el("ASM_jsonMount");
2564               if(!mount || !("value" in mount)) return false;
2565
2566               var text = mount.value || ASM_currentJsonText || "";
2567               var re = new RegExp("(^|\\n)\\s*[\\\"']" + key + "[\\\"']\\s*:", "m");
2568               var m = re.exec(text);
2569               if(!m) return false;
2570
2571               var targetOffset = m.index + (m[1] ? m[1].length : 0);
2572               var before = text.slice(0,targetOffset);
2573               var line = before.split(/\r\n|\r|\n/).length - 1;
2574               var lh = parseFloat(window.getComputedStyle(mount).lineHeight) || 21;
2575
2576               mount.scrollTop = Math.max(0,(line - 2) * lh);
2577               mount.focus();
2578
2579               try
2580               {
2581                 mount.setSelectionRange(targetOffset,Math.min(text.length,targetOffset + key.length + 2));
2582               }
2583               catch(_ignore) {}
2584
2585               return true;
2586             }
2587
2588            function ASM_scrollJsonToErrors()
2589            {
2590              ASM_toggleOutputPopup("diagnostics");
2591            }           
2592
2593            function ASM_jumpToErrors()
2594            {
2595              if(ASM_currentErrorCount <= 0 && ASM_currentWarningCount <= 0)
2596              {
2597                ASM_setErrorButtonState(0,0);
2598                return;
2599              }
2600
2601              ASM_scrollJsonToErrors();
2602            }
2603
2604            function ASM_setStatus(text, kind)
2605            {
2606              var msg = "[ASM" + (kind ? " " + kind.toUpperCase() : "") + "] " + text;
2607
2608              if(typeof console == "undefined") return;
2609
2610              if(kind == "bad" && typeof console.error == "function")
2611                console.error(msg);
2612              else if(kind == "warn" && typeof console.warn == "function")
2613                console.warn(msg);
2614              else if(typeof console.log == "function")
2615                console.log(msg);
2616            }
2617
2618            function ASM_escapeHTML(str)
2619            {
2620              return String(str)
2621                .replace(/&/g,"&amp;")
2622                .replace(/</g,"&lt;")
2623                .replace(/>/g,"&gt;")
2624                .replace(/\"/g,"&quot;")
2625                .replace(/'/g,"&#039;");
2626            }
2627
2628            function ASM_prettifyJSON()
2629            {
2630              var mountEl = ASM_el("ASM_jsonMount");
2631              if(!mountEl) return;
2632
2633               /*
2634                * The JSON output pane is now a real textarea so users can select
2635                * and copy arbitrary portions of the JSON. A textarea cannot host
2636                * syntax-highlight spans, so keep the raw pretty-printed JSON text.
2637                */
2638               if("value" in mountEl)
2639               {
2640                 mountEl.value = ASM_currentJsonText || mountEl.value || "";
2641                 return;
2642               }
2643
2644              var styleMap = {
2645                "{}": "color:#000000;font-weight:800",
2646                "[]": "color:#000000;font-weight:800"
2647              };
2648
2649              if(typeof oCOM != "undefined" && oCOM && typeof oCOM.JSONprettify == "function")
2650              {
2651                oCOM.JSONprettify(mountEl,styleMap,2);
2652                return;
2653              }
2654
2655              mountEl.innerHTML = "<pre class=\"ASM_jsonView\">" + ASM_escapeHTML(mountEl.textContent) + "</pre>";
2656            }
2657
2658             function ASM_setJsonText(text)
2659             {
2660               ASM_currentJsonText = String(text || "");
2661               ASM_refreshOutputPopup();
2662             }
2663
2664            function ASM_consoleTable(rows)
2665            {
2666              var newrows = JSON.parse(JSON.stringify(rows || []));
2667              if(!window.oASM) return newrows;
2668
2669              for(var i=0;i<newrows.length;i++)
2670              {
2671                for(var j in newrows[i])
2672                {
2673                  if(j == "pc") newrows[i][j] = window.oASM.pcHex(newrows[i][j]);
2674                  else if(j == "opc") newrows[i][j] = window.oASM.opcHex(newrows[i][j]);
2675                  else if(j == "val") newrows[i][j] = window.oASM.valHex(newrows[i][j],newrows[i].oby);
2676                  else if(j == "bytes") newrows[i][j] = window.oASM.bytesToHex(newrows[i][j]);
2677                  else if(typeof newrows[i][j] == "object" && newrows[i][j] != null)
2678                    newrows[i][j] = Array.isArray(newrows[i][j]) ? newrows[i][j].join("|") : JSON.stringify(newrows[i][j]);
2679                }
2680              }
2681              return newrows;
2682            }
2683
2684            function ASM_clearOutputs(message,kind)
2685            {
2686              ASM_setJsonText("");
2687              ASM_currentDiagnosticsText = "";
2688              ASM_sourceByteCodeActive = false;
2689              ASM_setSourceByteCodeModeUI(false);
2690              ASM_setByteCodeOutput("",0);
2691              ASM_updateSourceByteCodeRangeInfo(ASM_sourceByteCodeOrigin(false),0);
2692              window.asmTokeniseJSON = null;
2693              window.asmCompileResult = null;
2694              ASM_setErrorButtonState(0,0);
2695
2696              if(ASM_el("ASM_listingPane"))  ASM_el("ASM_listingPane").value  = "";
2697              //ASM_setPaneInfos("", "not assembled");
2698              ASM_resizeSourceListingPanes();
2699              ASM_refreshFindReplaceOverlay();
2700
2701              if(message) ASM_setStatus(message,kind || "");
2702            }
2703
2704            function ASM_assembleCurrentSource()
2705            {
2706              if(typeof window.ASM != "function")
2707              {
2708                var msg = "ASM() was not found. Check res/ASM_core.js.";
2709                var failure = {errors:[{message:msg}],warnings:[]};
2710                ASM_setStatus(msg,"bad");
2711                ASM_setJsonText(JSON.stringify(failure,null,2));
2712                ASM_setDiagnosticsText(failure);
2713                ASM_setErrorButtonState(1,0);
2714                ASM_setPaneInfos("", "error");
2715                return false;
2716              }
2717
2718              try
2719              {
2720                var sourcePane = ASM_el("ASM_sourcePane");
2721                var listingColumnsInput = ASM_el("ASM_listingColumnsInput") || ASM_el("ASM_settingsListingColumnsInput");
2722                var listingPane = ASM_el("ASM_listingPane");
2723                var jsonMount = ASM_el("ASM_jsonMount");
2724
2725                ASM_syncListingColumns(listingColumnsInput);
2726                window.oASM = new ASM({
2727                  listingColumns:listingColumnsInput ? listingColumnsInput.value : undefined,
2728                  dQuoteLegacy:ASM_dQuoteLegacyValue(),
2729                  sourceName:ASM_currentFileName,
2730                  includes:ASM_buildIncludeMap()
2731                });
2732                if(listingColumnsInput && typeof window.oASM.setListingColumns == "function")
2733                  window.oASM.setListingColumns(listingColumnsInput.value);
2734
2735                var rows = window.oASM.tokenise(sourcePane.value,ASM_currentFileName);
2736                var compiled = window.oASM.compile(rows);
2737                var coreVersion = window.oASM.version || "?";
2738
2739                window.asmTokeniseJSON = rows;
2740                window.asmCompileResult = compiled;
2741
2742                listingPane.value = compiled.listingText || "";
2743
2744                ASM_setJsonText(JSON.stringify(compiled,null,2));
2745                //ASM_setPaneInfos(listingPane.value, "interactive JSON");
2746
2747                console.table(ASM_consoleTable(compiled.rows || []));
2748
2749                var rowCount = compiled.rows && compiled.rows.length ? compiled.rows.length : 0;
2750                var byteCount = compiled.bytes && compiled.bytes.length ? compiled.bytes.length : 0;
2751                var errorCount = compiled.errors && compiled.errors.length ? compiled.errors.length : 0;
2752                var warningCount = compiled.warnings && compiled.warnings.length ? compiled.warnings.length : 0;
2753                ASM_setDiagnosticsText(compiled);
2754                ASM_setErrorButtonState(errorCount,warningCount);
2755
2756                ASM_sourceByteCodeActive = false;
2757                ASM_setSourceByteCodeModeUI(false);
2758                ASM_setByteCodeOutput(compiled.byteCodeText || "", byteCount);
2759                ASM_updateSourceByteCodeRangeInfo(ASM_sourceByteCodeOrigin(false),byteCount);                
2760                ASM_resizeSourceListingPanes();
2761                ASM_refreshFindReplaceOverlay();
2762
2763                if(errorCount || warningCount)
2764                {
2765                  var diagnosticSummary = [];
2766                  if(errorCount) diagnosticSummary.push(errorCount + " error(s)");
2767                  if(warningCount) diagnosticSummary.push(warningCount + " warning(s)");
2768                  ASM_setStatus("ASM core v" + coreVersion + ": assembled " + rowCount + " rows / " + byteCount + " bytes with " + diagnosticSummary.join(" / ") + ".","warn");
2769                }
2770                else
2771                  ASM_setStatus("ASM core v" + coreVersion + ": assembled " + rowCount + " rows / " + byteCount + " bytes from " + ASM_currentFileName + ".","ok");
2772
2773                return true;
2774              }
2775              catch(err)
2776              {
2777                console.error(err);
2778                if(ASM_el("ASM_listingPane"))  ASM_el("ASM_listingPane").value = "";
2779                ASM_resizeSourceListingPanes();
2780                ASM_refreshFindReplaceOverlay();
2781                ASM_setByteCodeOutput("",0);
2782
2783                var message = err && err.message ? err.message : 
2783String(err);
2784                var failure = {
2785                  errors:[{message:message,stack:err && err.stack ? err.stack : ""}],
2786                  warnings:[]
2787                };
2788                ASM_setJsonText(JSON.stringify(failure,null,2));
2789                ASM_setDiagnosticsText(failure);
2790                ASM_setErrorButtonState(1,0);
2791                ASM_setPaneInfos("", "error");
2792                ASM_setStatus("Error: " + message,"bad");
2793                return false;
2794              }
2795            }
2796
2797            function ASM_loadSourceText(name,text)
2798            {
2799              ASM_setCurrentFileName(name || "source.S");
2800              ASM_el("ASM_sourcePane").value = String(text || "");
2801              ASM_clearOutputs("Loaded " + ASM_currentFileName + ". Click assemble to generate the listing, JSON and byte code.","ok");
2802              ASM_resizeSourceListingPanes();
2803              ASM_refreshFindReplaceOverlay();
2804            }
2805
2806            async function ASM_loadFile(file)
2807            {
2808              if(!file) return;
2809              ASM_loadSourceText(file.name || "source.S",await file.text());
2810            }
2811
2812            function ASM_loadCatalogSource(arg)
2813            {
2814              var url = arg.download_url || oCOM.CATALOG.githubRawURL(arg);
2815              var name = arg.name || String(arg.path || "source.S").split("/").pop();
2816
2817              function finish(text)
2818              {
2819                if(/\.inc$/i.test(name))
2820                {
2821                  var result = ASM_addIncludeText(
2822                    name,
2823                    text,
2824                    arg.path || name
2825                  );
2826
2827                  ASM_clearOutputs(
2828                    (result.replaced ? "Replaced include " : "Loaded include ")
2829                    + name
2830                    + ". Reassemble to refresh the listing and byte code.",
2831                    "ok"
2832                  );
2833                }
2834                else
2835                  ASM_loadSourceText(name,text);
2836
2837                oCOM.CATALOG.close("sourceCat");
2838              }
2839
2840              function loadOffline(reason)
2841              {
2842                if(typeof ASM_OfflineSources == "undefined")
2843                  return false;
2844
2845                try   
2846                {
2847                  var text = oCOM.CATALOG.offlineData(
2848                    ASM_OfflineSources,
2849                    arg,
2850                    url,
2851                    "text"
2852                  );
2853
2854                  if(text == null)
2855                    return false;
2856
2857                  console.warn(
2858                    "Using offline source fallback for " +
2859                    arg.path + ": " + reason
2860                  );
2861
2862                  finish(text);
2863                  return true;
2864                  }
2865                  catch(err)
2866                  {
2867                    ASM_setStatus(
2868                      "Offline source decode failed: " +
2869                      (err.message || String(err)),
2870                      "bad"
2871                    );
2872                    return true;
2873                  }
2874                }
2875
2876              /*
2877               * Do not attempt raw GitHub when the browser already knows it is
2878               * offline.  This also covers the case where the directory listing
2879               * came from cache before connectivity was lost.
2880               */
2881              if(oCOM.CATALOG.isOffline())
2882              {
2883                if(loadOffline("navigator.onLine=false"))
2884                  return;
2885
2886                ASM_setStatus(
2887                  "This source file is not available offline.",
2888                  "warn"
2889                );
2890                return;
2891              }
2892
2893              oCOM.GetHTTP(
2894                url,
2895                "text",
2896                function()
2897                {
2898                  if(this.status != 200)
2899                  {
2900                    if(loadOffline("HTTP " + this.status))
2901                      return;
2902
2903                    ASM_setStatus(
2904                      "Source catalog load failed: HTTP " + this.status,
2905                      "bad"
2906                    );
2907                    return;
2908                  }
2909
2910                  finish(
2911                    this.responseText ||
2912                    this.response ||
2913                    ""
2914                  );
2915                },
2916                null,
2917                function()
2918                {
2919                  if(loadOffline("network error"))
2920                    return;
2921
2922                  ASM_setStatus(
2923                    "Source catalog load failed: network unavailable.",
2924                    "bad"
2925                  );
2926                }
2927              );
2928            }
2929
2930            function ASM_sourceCatalogOpen()
2931            {
2932              if(typeof oCOM == "undefined" || !oCOM.CATALOG)
2933              {
2934                ASM_setStatus("Shared catalog browser is unavailable.","bad");
2935                return false;
2936              }
2937
2938              var catalog = oCOM.CATALOG.create({
2939                 id:"sourceCat"
2940                ,popupID:"sourceCat_popup"
2941                ,title:"SOURCE CODE CATALOG"
2942                ,owner:"RetroAppleJS"
2943                ,repo:"RetroAppleJS.github.io"
2944                ,ref:"main"
2945                ,basepath:"asm"
2946                ,path:"asm"
2947                ,fileIcon:"fa fa-file-code"
2948                ,offlineProvider:function(url)
2949                {
2950                  if(typeof ASM_GitHubDirOffline == "undefined")
2951                    return null;
2952
2953                  var offline = oCOM.CATALOG.offlineDirectory(
2954                    ASM_GitHubDirOffline,
2955                    url
2956                  );
2957
2958                  if(offline)
2959                    console.warn("Using offline assembler source directory fallback");
2960
2961                  return offline;
2962                }
2963                ,filter:function(entry)
2964                {
2965                  return entry.type == "dir" || /\.(s|asm|inc|txt|src|a65|mac)$/i.test(entry.name || "");
2966                }
2967                ,onFile:ASM_loadCatalogSource
2968                ,onError:function(err)
2969                {
2970                  ASM_setStatus("Source catalog failed: " + (err.message || ("HTTP " + err.status)),"bad");
2971                }
2972              });
2973
2974              return catalog.open();
2975            }
2976
2977            function ASM_copyText(text,okMessage)
2978            {
2979              if(!navigator.clipboard)
2980              {
2981                ASM_setStatus("Clipboard unavailable; select and copy manually.","warn");
2982                return;
2983              }
2984
2985              navigator.clipboard.writeText(text || "")
2986                .then(function(){ ASM_setStatus(okMessage,"ok"); })
2987                .catch(function(){ ASM_setStatus("Clipboard copy failed; select manually.","warn"); });
2988            }
2989
2990            function ASM_downloadText(filename,text,mime)
2991            {
2992              filename = filename || "symbols.json";
2993              text = text || "";
2994              mime = mime || "application/json;charset=utf-8";
2995
2996              var blob = new Blob([text],{type:mime});
2997              var url = URL.createObjectURL(blob);
2998              var a = document.createElement("a");
2999
3000              a.href = url;
3001              a.download = filename;
3002              a.style.display = "none";
3003
3004              document.body.appendChild(a);
3005              a.click();
3006              document.body.removeChild(a);
3007
3008              URL.revokeObjectURL(url);
3009            }
3010
3011            function ASM_safeSourceFileName(name)
3012            {
3013              name = String(name || "").trim();
3014              name = name.replace(/[\\/:*?\"<>|]+/g,"_");
3015              return name || "source.S";
3016            }
3017
3018            function ASM_updateSourceFileNameUI()
3019            {
3020              var input = ASM_el("ASM_sourceFileNameInput");
3021              if(input && input.value !== ASM_currentFileName)
3022                input.value = ASM_currentFileName;
3023            }
3024
3025            function ASM_setCurrentFileName(name)
3026            {
3027              ASM_currentFileName = ASM_safeSourceFileName(name);
3028              ASM_updateSourceFileNameUI();
3029              ASM_updateSymbolFileNameFromSource();
3030              return ASM_currentFileName;
3031            }
3032
3033
3034            function ASM_sourceBaseName(name)
3035            {
3036              name = ASM_safeSourceFileName(name || ASM_currentFileName || "source.S");
3037              name = name.replace(/\.[^.]*$/,"");
3038              name = name.replace(/[^A-Za-z0-9_.-]+/g,"_");
3039              return name || "source";
3040            }
3041
3042            function ASM_defaultSymbolFileName(sourceName)
3043            {
3044              return ASM_sourceBaseName(sourceName) + ".SYM";
3045            }
3046
3047            function ASM_safeSymbolFileName(name)
3048            {
3049              name = String(name || "").trim();
3050              name = name.replace(/[\\/:*?\"<>|]+/g,"_");
3051              name = name.replace(/\.[^.]*$/,"");
3052              name = name.replace(/[^A-Za-z0-9_.-]+/g,"_");
3053              return (name || ASM_sourceBaseName()) + ".SYM";
3054            }
3055
3056            function ASM_updateSymbolFileNameFromSource()
3057            {
3058              var input = ASM_el("ASM_symbolFileNameInput");
3059              if(input) input.value = ASM_defaultSymbolFileName(ASM_currentFileName);
3060            }
3061
3062            function ASM_getSymbolFileName()
3063            {
3064              var input = ASM_el("ASM_symbolFileNameInput");
3065              var filename = ASM_safeSymbolFileName(input ? input.value : ASM_defaultSymbolFileName(ASM_currentFileName));
3066              if(input && input.value !== filename) input.value = filename;
3067              return filename;
3068            }
3069
3070
3071            function ASM_downloadSource()
3072            {
3073              var sourcePane = ASM_el("ASM_sourcePane");
3074              var input = ASM_el("ASM_sourceFileNameInput");
3075              var filename = ASM_setCurrentFileName(input ? input.value : ASM_currentFileName);
3076
3077              ASM_downloadText(
3078                filename,
3079                sourcePane ? sourcePane.value : "",
3080                "text/plain;charset=utf-8"
3081              );
3082
3083              ASM_setStatus("Downloaded source as " + filename + ".","ok");
3084            }
3085
3086            function ASM_symbolScopeFromBytes()
3087            {
3088              var compiled = window.asmCompileResult || {};
3089              var records = compiled.bytes || [];
3090              var ranges = [];
3091
3092              if(!records.length) return ranges;
3093
3094              var pcs = [];
3095
3096              for(var i=0;i<records.length;i++)
3097              {
3098                if(typeof records[i].pc == "number")
3099                  pcs.push(records[i].pc & 0xFFFF);
3100              }
3101
3102              if(!pcs.length) return ranges;
3103
3104              pcs.sort(function(a,b){ return a-b; });
3105
3106              var start = pcs[0];
3107              var prev  = pcs[0];
3108
3109              for(var j=1;j<pcs.length;j++)
3110              {
3111                var pc = pcs[j];
3112
3113                if(pc === prev || pc === ((prev + 1) & 0xFFFF))
3114                {
3115                  prev = pc;
3116                  continue;
3117                }
3118
3119                ranges.push([start,prev]);
3120                start = pc;
3121                prev  = pc;
3122              }
3123
3124              ranges.push([start,prev]);
3125
3126              return ranges;
3127            }
3128
3129            function ASM_scopeHex(scope)
3130            {
3131              var out = [];
3132
3133              for(var i=0;i<(scope || []).length;i++)
3134              {
3135                out.push([
3136                  ASM_symbolHex(scope[i][0],2),
3137                  ASM_symbolHex(scope[i][1],2)
3138                ]);
3139              }
3140
3141              return out;
3142            }
3143
3144            function ASM_symbolHex(value,bytes)
3145            {
3146              value = Number(value) & 0xFFFF;
3147              bytes = bytes === 1 ? 1 : 2;
3148
3149              if(window.oASM && typeof window.oASM.valHex == "function")
3150                return window.oASM.valHex(value,bytes).replace(/^0x/i,"$");
3151
3152              if(bytes === 1)
3153                return "$" + ("0" + (value & 0xFF).toString(16).toUpperCase()).slice(-2);
3154
3155              return "$" + ("000" + value.toString(16).toUpperCase()).slice(-4);
3156            }
3157
3158
3159            function ASM_cleanCommentText(comment)
3160            {
3161              comment = String(comment == null ? "" : comment).trim();
3162              return comment.replace(/^;\s*/,"");
3163            }
3164
3165            function ASM_rowComment(row,asm)
3166            {
3167              row = row || {};
3168              asm = asm || window.oASM;
3169
3170              if(asm && typeof asm.splitStatementAndComment == "function")
3171              {
3172                var split = asm.splitStatementAndComment(row.source != null ? row.source : row.statement);
3173                return split && split.comment ? String(split.comment) : "";
3174              }
3175
3176              var src = String(row.source != null ? row.source : row.statement || "");
3177              var quote = "";
3178              var escaped = false;
3179
3180              for(var i=0;i<src.length;i++)
3181              {
3182                var ch = src.charAt(i);
3183                if(escaped) { escaped = false; continue; }
3184                if(ch == "\\") { escaped = true; continue; }
3185                if(quote)
3186                {
3187                  if(ch == quote) quote = "";
3188                  continue;
3189                }
3190                if(ch == "'" || ch == '"') { quote = ch; continue; }
3191                if(ch == ";") return src.substring(i);
3192              }
3193
3194              return "";
3195            }
3196
3197            function ASM_commentName(prefix,value,index)
3198            {
3199              prefix = String(prefix || "COMMENT").replace(/[^A-Za-z0-9_.-]+/g,"_");
3200              if(value != null && !isNaN(Number(value)))
3201                return prefix + "_" + ASM_symbolHex(value,2).replace(/^\$/,"") + "_" + index;
3202              return prefix + "_" + index;
3203            }
3204
3205            function ASM_collectComments()
3206            {
3207              var out = [];
3208              var rows = window.asmCompileResult && window.asmCompileResult.rows ? window.asmCompileResult.rows : [];
3209              var asm = window.oASM;
3210
3211              if(!asm) return out;
3212
3213              for(var i=0;i<rows.length;i++)
3214              {
3215                var row = rows[i] || {};
3216                var tag = row.tag || [];
3217                var sym = row.sym || [];
3218                var bytes = row.bytes || [];
3219                var comment = ASM_rowComment(row,asm);
3220                var commentText = ASM_cleanCommentText(comment);
3221
3222                if(!commentText) continue;
3223
3224                var lblIndex = asm.getTokenIndexByTag ? asm.getTokenIndexByTag(tag,"LBL") : -1;
3225                var pgmIndex = asm.getTokenIndexByTag ? asm.getTokenIndexByTag(tag,"PGM") : -1;
3226                var mneIndex = asm.getTokenIndexByTag ? asm.getTokenIndexByTag(tag,"MNE") : -1;
3227                var isEqu = false;
3228
3229                if(pgmIndex >= 0 && asm.pragmaInfo)
3230                {
3231                  var pinfo = asm.pragmaInfo(sym[pgmIndex]);
3232                  isEqu = !!(pinfo && pinfo.ref === "EQU");
3233                }
3234
3235                /*
3236                 * EQU comments are symbol comments: they belong to the EQU
3237                 * symbol value rather than to an instruction PC.
3238                 */
3239                if(isEqu && lblIndex >= 0 && sym[lblIndex])
3240                {
3241                  var equName = asm.getID ? asm.getID(sym[lblIndex]) : String(sym[lblIndex]).substring(0,8);
3242                  var equValue = typeof row.val == "number" ? row.val : (asm.symtab ? asm.symtab[equName] : undefined);
3243                  if(typeof equValue == "number" && !isNaN(equValue))
3244                  {
3245                    out.push({
3246                      name: "COMMENT_" + equName,
3247                      type: "comment",
3248                      targetType: "equ",
3249                      value: equValue & 0xFFFF,
3250                      comment: commentText
3251                    });
3252                  }
3253                  continue;
3254                }
3255
3256                /*
3257                 * Instruction/data comments must be matched to a generated
3258                 * address and opcode/data-byte sequence.  Pure comment rows
3259                 * do not have enough information for debugger-side matching.
3260                 */
3261                if(!bytes.length || typeof row.pc != "number") continue;
3262
3263                var value = row.pc & 0xFFFF;
3264
3265                out.push({
3266                  name: ASM_commentName("COMMENT",value,out.length),
3267                  type: "comment",
3268                  targetType: mneIndex >= 0 ? "instruction" : "data",
3269                  value: value,
3270                  comment: commentText,
3271                  opcode: bytes.slice ? bytes.slice() : []
3272                });
3273              }
3274
3275              return out;
3276            }
3277
3278            function ASM_collectSymbols()
3279            {
3280              var out = [];
3281              var seen = {};
3282              var rows = window.asmCompileResult && window.asmCompileResult.rows ? window.asmCompileResult.rows : [];
3283              var asm = window.oASM;
3284
3285              if(!asm) return out;
3286
3287              for(var i=0;i<rows.length;i++)
3288              {
3289                var row = rows[i] || {};
3290                var tag = row.tag || [];
3291                var sym = row.sym || [];
3292
3293                if(tag[0] !== "LBL" || !sym[0]) continue;
3294
3295                var name = asm.getID ? asm.getID(sym[0]) : String(sym[0]).substring(0,8);
3296                if(!name || seen[name]) continue;
3297
3298                var pgmIndex = asm.getTokenIndexByTag ? asm.getTokenIndexByTag(tag,"PGM") : -1;
3299                var type = "label";
3300                var value;
3301                var bytes = 2;
3302
3303                if(pgmIndex >= 0 && asm.pragmaInfo)
3304                {
3305                  var pinfo = asm.pragmaInfo(sym[pgmIndex]);
3306                  if(pinfo && pinfo.ref === "EQU")
3307                  {
3308                    type = "equ";
3309                    value = typeof row.val == "number" ? row.val : (asm.symtab ? asm.symtab[name] : undefined);
3310                    bytes = row.oby === 1 ? 1 : 2;
3311                  }
3312                }
3313
3314                if(type === "label")
3315                {
3316                  value = typeof row.pc == "number" ? row.pc : (asm.symtab ? asm.symtab[name] : undefined);
3317                  bytes = 2;
3318                }
3319
3320                if(typeof value != "number" || isNaN(value)) continue;
3321
3322                value = value & 0xFFFF;
3323                seen[name] = true;
3324
3325                out.push({
3326                  name: name,
3327                  type: type,
3328                  bytes: bytes,
3329                  value: value,
3330                  hex: ASM_symbolHex(value,bytes)
3331                });
3332              }
3333
3334              out = out.concat(ASM_collectComments());
3335
3336              return out;
3337            }
3338
3339            function ASM_buildSymbolExportObject()
3340            {
3341              var symbols = ASM_collectSymbols();
3342              var scope = ASM_symbolScopeFromBytes();
3343
3344              return {
3345                format: "RetroAppleJS-ASM-symbols",
3346                version: 2,
3347                source: ASM_currentFileName || "source.S",
3348
3349                /*
3350                * The table is valid only when the disassembled instruction address
3351                * is inside one of these ranges.
3352                */
3353                scopeMode: "instruction-pc",
3354                scope: scope,
3355                scopeHex: ASM_scopeHex(scope),
3356
3357                count: symbols.length,
3358                symbols: symbols
3359              };
3360            }
3361
3362            function ASM_debuggerOriginAddress()
3363            {
3364              /*
3365               * Prefer the first emitted byte from the assembler output.  This is
3366               * the source/listing origin in normal ORG-based programs and stays
3367               * stable even when the code contains later ORG blocks.
3368               */
3369              var compiled = window.asmCompileResult || {};
3370              var records = compiled.bytes || [];
3371
3372              for(var i = 0; i < records.length; i++)
3373              {
3374                if(records[i] && typeof records[i].pc == "number")
3375                  return records[i].pc & 0xFFFF;
3376              }
3377
3378              var scope = ASM_symbolScopeFromBytes();
3379              if(scope && scope.length && scope[0] && typeof scope[0][0] == "number")
3380                return scope[0][0] & 0xFFFF;
3381
3382              return null;
3383            }
3384
3385            function ASM_exportSymbols()
3386            {
3387              /*
3388              * Always assemble before export, otherwise the symbol table can be stale
3389              * after source edits.
3390              */
3391              if(!ASM_assembleCurrentSource()) return;
3392
3393              var exportObject = ASM_buildSymbolExportObject();
3394              var symbols = exportObject.symbols || [];
3395              var scope = exportObject.scope || [];
3396
3397              ASM_downloadText(
3398                ASM_getSymbolFileName(),
3399                JSON.stringify(exportObject,null,2),
3400                "application/json;charset=utf-8"
3401              );
3402
3403              ASM_setStatus(
3404                "Exported " + symbols.length + " symbol(s) / " + scope.length + " scope range(s).",
3405                "ok"
3406              );
3407            }            
3408
3409            function ASM_showEmulatorTools()
3410            {
3411              var settings = ASM_el("tab1.1");
3412              var tools    = ASM_el("tab1.2");
3413
3414              /*
3415              * Keep this helper in sync with the POPUP API.
3416              *
3417              * Do not force settings.style.display = "none" here. The Settings
3418              * button later calls oCOM.POPUP.toggle("tab1.1"), and that API only
3419              * flips the element's hidden property.
3420              */
3421              if(typeof oCOM != "undefined" && oCOM.POPUP && typeof oCOM.POPUP.off == "function")
3422                oCOM.POPUP.off("tab1.1");
3423
3424              if(tools)
3425              {
3426                if(typeof oCOM != "undefined" && oCOM.POPUP && typeof oCOM.POPUP.on == "function")
3427                  oCOM.POPUP.on("tab1.2");
3428                else
3429                {
3430                  tools.hidden = false;
3431                  tools.removeAttribute("hidden");
3432                }
3433              }
3434            }
3435
3436            function ASM_toEmulator()
3437            {
3438              ASM_syncByteCodeFromBoard();
3439              if(!ASM_currentByteCodeText)
3440                if(!ASM_assembleCurrentSource()) return;
3441
3442              if(typeof EMUboard != "undefined" && EMUboard.boardText)
3443              {
3444                var text = ASM_currentByteCodeText.replace(/\n/g,"\r\n");
3445
3446                /*
3447                 * Normal assembled byte code still needs the monitor prefix and
3448                 * a trailing return.  Source byte-code output is already a full
3449                 * paste script ending with a BSAVE command, deliberately without
3450                 * a trailing return after that final command.
3451                 */
3452                if(ASM_sourceByteCodeActive || /^\s*CALL-151(?:\r?\n|$)/i.test(ASM_currentByteCodeText))
3453                  EMUboard.boardText.value = text;
3454                else
3455                  EMUboard.boardText.value = "CALL-151\r\n" + text + "\r\n";
3456                ASM_showEmulatorTools();
3457                COM_switchTab("tab1");
3458                ASM_setStatus("Byte code copied to emulator paste board.","ok");
3459              }
3460              else ASM_setStatus("Emulator paste board is not available.","warn");
3461            }
3462
3463            function ASM_toDebugger()
3464            {
3465              /*
3466               * Always assemble first so byte code, labels, EQU values and
3467               * comments are generated from the same parser/compiler state.
3468               */
3469              if(!ASM_assembleCurrentSource()) return;
3470
3471              ASM_syncByteCodeFromBoard();
3472
3473              var symbolImported = false;
3474              var symbolObject = ASM_buildSymbolExportObject();
3475              var originPc = ASM_debuggerOriginAddress();
3476
3477              if(typeof DBG_receiveSymbolTable == "function")
3478              {
3479                symbolImported = DBG_receiveSymbolTable(
3480                  symbolObject,
3481                  ASM_currentFileName || "source.S",
3482                  {
3483                    overlap: "override",
3484                    disassemble: false,
3485                    status: false
3486                  }
3487                );
3488              }              
3489
3490              if(typeof DBG_receiveByteCode == "function")
3491              {
3492                DBG_receiveByteCode(ASM_currentByteCodeText,true,originPc);
3493                ASM_setStatus(
3494                  "Byte code" + (symbolImported ? " and symbol table" : "") + " imported into debugger tab.",
3495                  "ok"
3496                );                
3497                return;
3498              }
3499
3500              /*
3501              * Fallback for older debugger builds.
3502              */
3503              var debugfield = ASM_el("debugfield");
3504              if(debugfield)
3505              {
3506                debugfield.innerHTML = ASM_escapeHTML(ASM_currentByteCodeText).replace(/\n/g,"<br>");
3507                COM_switchTab("tab3");
3508                ASM_setStatus("Byte code copied to debugger tab.","ok");
3509              }
3510              else ASM_setStatus("Debugger output field is not available.","warn");
3511            }
3512
3513            function DBG_receiveByteCode(text,switchToTab,originPc)
3514            {
3515              DBG_currentByteCodeText = String(text || "");
3516
3517              var board = DBG_el("DBG_boardText");
3518              if(board) board.value = DBG_currentByteCodeText;
3519
3520              var ok = DBG_importBoardByteCode();
3521
3522              if(switchToTab !== false && typeof COM_switchTab == "function")
3523                COM_switchTab("tab3");
3524
3525              /*
3526               * Scroll after the tab switch has applied, otherwise the hidden
3527               * tab3 listing pane can report a zero clientHeight and the centering
3528               * calculation in DBG_scrollListingToPC() becomes unreliable.
3529               */
3530              if(originPc != null && !isNaN(Number(originPc)))
3531              {
3532                var pc = Number(originPc) & 0xFFFF;
3533                if(typeof DBG_prepareDebuggerStartPC == "function")
3534                  DBG_prepareDebuggerStartPC(pc,{clearTrace:true});
3535                if(typeof DBG_prepareDebuggerStartPC == "function")
3536                  DBG_prepareDebuggerStartPC(pc,{clearTrace:true});
3537                window.setTimeout(function(){
3538                  if(typeof DBG_scrollListingToPC != "function") return;
3539
3540                  var listed = DBG_scrollListingToPC(pc,{behavior:"auto"});
3541                  if(listed)
3542                    DBG_statusText(
3543                      "Imported byte code and positioned listing at $" + DBG_hexWord(pc) + "."
3544                    );
3545                },0);
3546              }
3547
3548              return ok;
3549            }
3550
3551            function ASM_clearBoard()
3552            {
3553              ASM_setByteCodeOutput("",0);
3554            }
3555
3556            function ASM_resizeTextAreaToContent(el)
3557            {
3558              if(!el) return;
3559              var horizontalScroll = el.scrollLeft || 0;
3560
3561              var wrap = el.closest ? el.closest(".com-pane-fr-wrap") : null;
3562
3563              el.style.height = "auto";
3564              el.style.overflowY = "hidden";
3565
3566              if(wrap)
3567              {
3568                wrap.style.height = "auto";
3569                wrap.style.overflow = "hidden";
3570              }
3571
3572              var h = Math.max(420, el.scrollHeight);
3573              el.style.height = h + "px";
3574              if(wrap) wrap.style.height = h + "px";
3575
3576              /*
3577               * These editors grow to their complete content height, so a
3578               * vertical textarea scroll offset is always stale.  An editable
3579               * source textarea can retain such an offset after caret/focus
3580               * activity; the find overlay copies it and shifts Source almost
3581               * one printer line above Listing.  Preserve horizontal scrolling,
3582               * but anchor both text origins at the top of their pane bodies.
3583               */
3584              el.scrollTop = 0;
3585              el.scrollLeft = horizontalScroll;
3586              if(wrap) {
3587                var overlay = wrap.querySelector(".com-pane-fr-overlay");
3588                if(overlay) {
3589                  overlay.scrollTop = 0;
3590                  overlay.scrollLeft = horizontalScroll;
3591                }
3592              }
3593
3594
3595            }
3596
3597            function ASM_resizeSourceListingPanes()
3598            {
3599              var sourcePane = ASM_el("ASM_sourcePane");
3600              var listingPane = ASM_el("ASM_listingPane");
3601              var includeMount = ASM_el("ASM_includeMount");
3602
3603              ASM_resizeTextAreaToContent(sourcePane);
3604              ASM_resizeTextAreaToContent(listingPane);
3605
3606              if(includeMount)
3607              {
3608                var sourceHeight  = sourcePane ? sourcePane.offsetHeight : 420;
3609                var listingHeight = listingPane ? listingPane.offsetHeight : 420;
3610                includeMount.style.height = Math.max(420, sourceHeight, listingHeight) + "px";
3611                includeMount.style.maxHeight = includeMount.style.height;
3612              }
3613
3614              ASM_equalizePaneHeaders();
3615            }
3616
3617            function ASM_normalizeLineEndingsInfo(text)
3618            {
3619              var m = String(text || "").match(/\r\n|\r|\n/);
3620              return {
3621                eol: m ? m[0] : "\n",
3622                hasFinalEol: /(\r\n|\r|\n)$/.test(String(text || ""))
3623              };
3624            }
3625
3626            function ASM_splitSourceLines(text)
3627            {
3628              return String(text || "").split(/\r\n|\r|\n/);
3629            }
3630
3631            function ASM_joinSourceLines(lines, info)
3632            {
3633              var out = lines.join(info.eol || "\n");
3634              if(info.hasFinalEol && lines.length && lines[lines.length - 1] !== "") out += info.eol || "\n";
3635              return out;
3636            }
3637
3638            function ASM_snapshotSourceEdit(label)
3639            {
3640              var sourcePane = ASM_el("ASM_sourcePane");
3641              if(!sourcePane) return null;
3642              return {
3643                label: label || "edit",
3644                value: sourcePane.value,
3645                selectionStart: sourcePane.selectionStart || 0,
3646                selectionEnd: sourcePane.selectionEnd || 0,
3647                scrollTop: sourcePane.scrollTop || 0
3648              };
3649            }
3650
3651            function ASM_restoreSourceSnapshot(snap)
3652            {
3653              var sourcePane = ASM_el("ASM_sourcePane");
3654              if(!snap || !sourcePane) return;
3655              sourcePane.value = snap.value;
3656              ASM_resizeSourceListingPanes();
3657              ASM_refreshFindReplaceOverlay();
3658              sourcePane.focus();
3659              try { sourcePane.setSelectionRange(snap.selectionStart, snap.selectionEnd); } catch(_ignore) {}
3660              sourcePane.scrollTop = snap.scrollTop || 0;
3661            }
3662
3663            function ASM_pushSourceUndo(label)
3664            {
3665              var snap = ASM_snapshotSourceEdit(label);
3666              if(!snap) return;
3667              ASM_sourceEditHistory.undo.push(snap);
3668              if(ASM_sourceEditHistory.undo.length > ASM_sourceEditHistory.max) ASM_sourceEditHistory.undo.shift();
3669              ASM_sourceEditHistory.redo.length = 0;
3670            }
3671
3672            function ASM_sourceUndo()
3673            {
3674              if(!ASM_sourceEditHistory.undo.length) return;
3675              ASM_sourceEditHistory.redo.push(ASM_snapshotSourceEdit("redo"));
3676              ASM_restoreSourceSnapshot(ASM_sourceEditHistory.undo.pop());
3677            }
3678
3679            function ASM_sourceRedo()
3680            {
3681              if(!ASM_sourceEditHistory.redo.length) return;
3682              ASM_sourceEditHistory.undo.push(ASM_snapshotSourceEdit("undo"));
3683              ASM_restoreSourceSnapshot(ASM_sourceEditHistory.redo.pop());
3684            }
3685
3686            function ASM_sourceOutdent()
3687            {
3688              var sourcePane = ASM_el("ASM_sourcePane");
3689              if(!sourcePane) return;
3690
3691              ASM_pushSourceUndo("outdent");
3692
3693              var text = sourcePane.value;
3694              var info = ASM_normalizeLineEndingsInfo(text);
3695              var lines = ASM_splitSourceLines(text);
3696              var removed = [];
3697
3698              for(var i=0;i<lines.length;i++)
3699              {
3700                if(lines[i].length)
3701                {
3702                  removed[i] = lines[i].charAt(0);
3703                  lines[i] = lines[i].slice(1);
3704                }
3705                else removed[i] = "";
3706              }
3707
3708              ASM_sourceEditHistory.outdent.push(removed);
3709              sourcePane.value = ASM_joinSourceLines(lines, info);
3710              ASM_resizeSourceListingPanes();
3711              ASM_refreshFindReplaceOverlay();
3712            }
3713
3714            function ASM_sourceIndent()
3715            {
3716              var sourcePane = ASM_el("ASM_sourcePane");
3717              if(!sourcePane) return;
3718
3719              ASM_pushSourceUndo("indent");
3720
3721              var text = sourcePane.value;
3722              var info = ASM_normalizeLineEndingsInfo(text);
3723              var lines = ASM_splitSourceLines(text);
3724              var restore = ASM_sourceEditHistory.outdent.length ? ASM_sourceEditHistory.outdent.pop() : null;
3725              var nbsp = " ";
3726
3727              for(var i=0;i<lines.length;i++)
3728                lines[i] = (restore ? (restore[i] || "") : nbsp) + lines[i];
3729
3730              sourcePane.value = ASM_joinSourceLines(lines, info);
3731              ASM_resizeSourceListingPanes();
3732              ASM_refreshFindReplaceOverlay();
3733            }
3734
3735            function ASM_syncSourceFindReplaceSkin()
3736            {
3737              var sourcePane = ASM_el("ASM_sourcePane");
3738              var wrap = sourcePane && sourcePane.closest ? sourcePane.closest(".com-pane-fr-wrap") : null;
3739
3740              /*
3741               * Source is wrapped by PANE.bindFindReplace().  Put the common
3742               * COM_MAIN.css dot-matrix skin on the wrapper instead of
3743               * duplicating the background-image in index.html.
3744               */
3745              if(wrap) wrap.classList.add("dotmatrix_bck");
3746            }
3747
3748            function ASM_syncSourceFileNameTheme()
3749            {
3750              var input = ASM_el("ASM_sourceFileNameInput");
3751              if(!input) return;
3752
3753              if(ASM_headerThemeDark)
3754              {
3755                input.style.background = "#0b0f14";
3756                input.style.borderColor = "#30363d";
3757                input.style.color = "#e6edf3";
3758              }
3759              else
3760              {
3761                input.style.background = "#ffffff";
3762                input.style.borderColor = "rgba(0,0,0,0.25)";
3763                input.style.color = "#000000";
3764              }
3765            }
3766
3767            function ASM_toggleHeaderTheme()
3768            {
3769              var root = ASM_el("ASM_root");
3770              if(!root) return;
3771              ASM_headerThemeDark = !ASM_headerThemeDark;
3772              root.classList.toggle("ASM_headerDark", ASM_headerThemeDark);
3773              root.classList.toggle("ASM_paneDark", ASM_headerThemeDark);
3774              ASM_syncSourceFileNameTheme();
3775              ASM_refreshFindReplaceOverlay();
3776            }
3777
3778            function defaultPaneHeaderWidgetHTML()
3779            {
3780                return ''
3781                    + '<span class="com-pane-header-default-widget com-pane-header-widget">'
3782                    + '<button type="button" class="appbut skinny" data-pane-default-act="undo" title="Undo"><i class="fa fa-undo"></i></button>'
3783                    + '<button type="button" class="appbut skinny" data-pane-default-act="redo" title="Redo"><i class="fa fa-redo"></i></button>'
3784                    + '<button type="button" class="appbut skinny" data-pane-default-act="indent" title="Indent"><i class="fa fa-indent"></i></button>'
3785                    + '<button type="button" class="appbut skinny" data-pane-default-act="outdent" title="Outdent"><i class="fa fa-outdent"></i></button>'
3786                    + '<button type="button" class="appbut skinny" data-pane-default-act="theme" title="Toggle dark/light mode"><i class="fa fa-adjust"></i></button>'
3787                    + '<button type="button" class="appbut skinny" data-pane-default-act="bottom" title="Jump to the bottom"><i class="fa fa-angle-double-down"></i></button>'
3788                    + '</span>';
3789            };
3790
3791            function ASM_sourceDefaultPaneHeaderHTML()
3792            {
3793              if(!ASM_oPANE || typeof defaultPaneHeaderWidgetHTML != "function")
3794                return '<span class="com-pane-header-default-widget com-pane-header-widget">'
3795                  + '<button type="button" class="appbut skinny" data-pane-default-act="assemble" title="Assemble"><i class="fa fa-play blink"></i></button>'
3796                  + '</span>';
3797
3798              return defaultPaneHeaderWidgetHTML().replace(
3799                '<span class="com-pane-header-default-widget com-pane-header-widget">',
3800                '<span class="com-pane-header-default-widget com-pane-header-widget">'
3801                + '<button type="button" class="appbut skinny" data-pane-default-act="assemble" title="Assemble"><i class="fa fa-play blink"></i></button>'
3802              );
3803            }
3804
3805            function ASM_bindSourceDefaultWidgetActions()
3806            {
3807              var host = ASM_el("ASM_sourceFindReplaceMount");
3808              if(!host) return;
3809
3810              host.addEventListener("click", function(ev){
3811                var btn = ev.target && ev.target.closest ? ev.target.closest("[data-pane-default-act]") : null;
3812                if(!btn) return;
3813
3814                var act = btn.getAttribute("data-pane-default-act");
3815                if(act == "assemble") ASM_assembleCurrentSource();
3816                else if(act == "undo") ASM_sourceUndo();
3817                else if(act == "redo") ASM_sourceRedo();
3818                else if(act == "indent") ASM_sourceIndent();
3819                else if(act == "outdent") ASM_sourceOutdent();
3820                else if(act == "theme") ASM_toggleHeaderTheme();
3821                else if(act == "bottom") window.scrollTo(0, document.body.scrollHeight);
3822                else if(act == "top") window.scrollTo({ top: 0 });
3823
3824                ev.preventDefault();
3825              });
3826            }
3827
3828            function ASM_equalizePaneHeaders()
3829            {
3830              if(ASM_oPANE && typeof ASM_oPANE.equalizePaneHeaderHeights == "function")
3831                ASM_oPANE.equalizePaneHeaderHeights("#ASM_root .ASM_paneTitle");
3832            }
3833
3834            function ASM_toggleIncludePane(forceShow)
3835            {
3836              var host = ASM_el("ASM_paneHost");
3837              if(!host) return false;
3838
3839              if(!ASM_includeFiles.length)
3840              {
3841                host.classList.remove("ASM_showInclude");
3842                ASM_syncIncludeToggleButton();
3843                ASM_equalizePaneHeaders();
3844                return false;
3845              }
3846
3847              if(!ASM_currentIncludeFile())
3848                ASM_selectedIncludeId = String(ASM_includeFiles[0].id);
3849
3850              var show = (typeof forceShow == "boolean") ? forceShow : !host.classList.contains("ASM_showInclude");
3851              host.classList.toggle("ASM_showInclude",show);
3852
3853              ASM_updateIncludePane();
3854              ASM_renderIncludeLabels();
3855              ASM_syncIncludeToggleButton();
3856              ASM_resizeSourceListingPanes();
3857              ASM_equalizePaneHeaders();
3858              return show;
3859            }
3860
3861            function ASM_refreshFindReplaceOverlay()
3862            {
3863              if(ASM_sourceFindReplace && typeof ASM_sourceFindReplace.render == "function")
3864                ASM_sourceFindReplace.render();
3865            }
3866
3867            function ASM_bindSourceFindReplace()
3868            {
3869              if(typeof PANE != "function")
3870              {
3871                ASM_setStatus("PANE() was not found. Search & replace is unavailable.","warn");
3872                return;
3873              }
3874
3875              ASM_oPANE = new PANE();
3876
3877              if(typeof ASM_oPANE.injectFindReplaceCSS == "function")
3878                ASM_oPANE.injectFindReplaceCSS();
3879
3880              ASM_applyFindReplaceScheme();
3881
3882              if(typeof ASM_oPANE.createPaneHeaderWidgetManager == "function"
3883                 && typeof defaultPaneHeaderWidgetHTML == "function")
3884              {
3885                ASM_sourceHeaderWidgetManager = ASM_oPANE.createPaneHeaderWidgetManager("ASM_sourceFindReplaceMount", {
3886                  defaultId:"default",
3887                  defaultHTML:ASM_sourceDefaultPaneHeaderHTML(),
3888                  onChange:function(){ ASM_equalizePaneHeaders(); }
3889                });
3890                ASM_bindSourceDefaultWidgetActions();
3891              }
3892
3893              if(typeof ASM_oPANE.bindFindReplace == "function")
3894              {
3895                ASM_sourceFindReplace = ASM_oPANE.bindFindReplace("ASM_sourcePane", {
3896                  id:"ASM_sourceFindReplace",
3897                  mount:"ASM_sourceFindReplaceMount",
3898                  inline:true,
3899                  overlayExpand:true,
3900                  floatingNav:true,
3901                  pageScrollOnMatch:true,
3902                  pageScrollOffset:72,
3903                  manager:ASM_sourceHeaderWidgetManager,
3904                  scheme:ASM_findReplaceScheme,
3905                  barWidth:"390px",
3906                  debounce_ms:500,
3907                  onChange:function(){
3908                    ASM_resizeSourceListingPanes();
3909                    ASM_equalizePaneHeaders();
3910                    ASM_setStatus("Source updated by search & replace. Click assemble to regenerate output.","warn");
3911                  }
3912                });
3913
3914                var sourceWrap = document.querySelector("#ASM_sourcePane") &&
3915                 document.querySelector("#ASM_sourcePane").closest(".com-pane-fr-wrap");
3916
3917                if(sourceWrap) sourceWrap.classList.add("dotmatrix_bck");
3918              }
3919              else ASM_setStatus("PANE.bindFindReplace() was not found. Search & replace is unavailable.","warn");
3920            }
3921
3922            function ASM_manualDocHTML(key)
3923            {
3924              if(typeof _DOCS == "undefined" || !_DOCS || !_DOCS[key]) return "";
3925
3926              return String(_DOCS[key])
3927                .replace(/https\:\/\/github\.com\/RetroAppleJS\/RetroAppleJS\.github\.io\/raw\/main\//g,"");
3928            }
3929
3930            function ASM_loadManualPane()
3931            {
3932              var manualEl = ASM_el("ASM_manual");
3933              if(!manualEl) return;
3934
3935              var assemblerDoc = ASM_manualDocHTML("ASSEMBLER");
3936              var cpuDoc       = ASM_manualDocHTML("6502");
3937
3938              if(!assemblerDoc && !cpuDoc)
3939              {
3940                manualEl.textContent = "Assembler manual is not available.";
3941                return;
3942              }
3943
3944              manualEl.innerHTML =
3945                assemblerDoc
3946                + (cpuDoc
3947                  ? [
3948                      "",
3949                      "<hr>",
3950                      "<h1>6502 CPU Reference</h1>",
3951                      cpuDoc
3952                    ].join("\n")
3953                  : "");              
3954
3955            }
3956
3957            function ASM_registerPopupRules()
3958            {
3959              /*
3960               * Keep tab2 popup handling consistent with tab1 and tab3:
3961               * buttons call oCOM.POPUP.runRule(), rules toggle one overlay
3962               * and close the other one.
3963               *
3964               * Rule names:
3965               *   tab2.1 = Settings
3966               *   tab2.2 = Tools
3967               */
3968              if(typeof oCOM == "undefined" || !oCOM || !oCOM.POPUP || typeof oCOM.POPUP.addRule != "function") return;
3969              oCOM.POPUP.addRule("tab2.1",function(o){ o.toggle("tab2.1"); o.off("tab2.2"); });
3970              oCOM.POPUP.addRule("tab2.2",function(o){ o.toggle("tab2.2"); o.off("tab2.1"); });
3971            }
3972
3973            function ASM_bindEvents()
3974            {
3975              var fileInput = ASM_el("ASM_fileInput");
3976              var includeFileInput = ASM_el("ASM_includeFileInput");
3977              var assembleButton = ASM_el("ASM_assembleButton");
3978              var toggleIncludeButton = ASM_el("ASM_toggleIncludeButton");
3979              var sourceFileNameInput = ASM_el("ASM_sourceFileNameInput");
3980              var symbolFileNameInput = ASM_el("ASM_symbolFileNameInput");
3981              var downloadSourceButton = ASM_el("ASM_downloadSourceButton");
3982              var copyByteCodeButton = ASM_el("ASM_copyByteCodeButton");
3983              var copyIncludeButton = ASM_el("ASM_copyIncludeButton");
3984              var closeIncludeButton = ASM_el("ASM_closeIncludeButton");
3985              var jsonPopupButton = ASM_el("ASM_jsonPopupButton");
3986              var diagnosticsButton = ASM_el("ASM_diagnosticsButton");
3987              var outputPopupBack = ASM_el("ASM_outputPopupBack");
3988              var outputPopupCopy = ASM_el("ASM_outputPopupCopy");
3989              var outputPopupClose = ASM_el("ASM_outputPopupClose");
3990              var encodeSourceButton = ASM_el("ASM_encodeSourceButton");
3991              var sourceByteCodeStartInput = ASM_el("ASM_sourceByteCodeStartInput");
3992              var toEmulatorButtons = document.querySelectorAll(".ASM_toEmulatorButton");
3993              var toDebuggerButtons = document.querySelectorAll(".ASM_toDebuggerButton");
3994              var sourcePane = ASM_el("ASM_sourcePane");
3995              var boardText = ASM_el("ASM_boardText");
3996              var listingColumnsInput = ASM_el("ASM_listingColumnsInput");
3997              var settingsListingColumnsInput = ASM_el("ASM_settingsListingColumnsInput");
3998
3999              var dQuoteLegacyInput = ASM_el("ASM_dQuoteLegacyInput");
4000              var listingPresetButtons = document.querySelectorAll(".ASM_listingPreset[data-listing-preset]");
4001              if(fileInput) fileInput.addEventListener("change",function(){ ASM_loadFile(fileInput.files && fileInput.files[0]); });
4002              if(includeFileInput) includeFileInput.addEventListener("change",async function()
4003              {
4004                try
4005                {
4006                  await ASM_loadIncludeFiles(includeFileInput.files);
4007                }
4008                catch(err)
4009                {
4010                  ASM_setStatus("Include load failed: " + (err && err.message ? err.message : err),"bad");
4011                }
4012                finally
4013                {
4014                  includeFileInput.value = "";
4015                }
4016              });
4017              if(assembleButton) assembleButton.addEventListener("click",function(ev){ ev.preventDefault(); ASM_assembleCurrentSource(); });
4018              if(toggleIncludeButton) toggleIncludeButton.addEventListener("click",function(){ ASM_toggleIncludePane(); });
4019              if(sourceFileNameInput) sourceFileNameInput.addEventListener("change",function(){ ASM_setCurrentFileName(sourceFileNameInput.value); });
4020              if(sourceFileNameInput) sourceFileNameInput.addEventListener("keydown",function(ev){
4021                if(ev.key == "Enter") { ev.preventDefault(); ASM_setCurrentFileName(sourceFileNameInput.value); sourceFileNameInput.blur(); }
4022              });
4023              if(symbolFileNameInput) symbolFileNameInput.addEventListener("change",function(){ ASM_getSymbolFileName(); });
4024              if(symbolFileNameInput) symbolFileNameInput.addEventListener("keydown",function(ev){
4025                if(ev.key == "Enter") { ev.preventDefault(); ASM_getSymbolFileName(); symbolFileNameInput.blur(); }
4026              });
4027              if(downloadSourceButton) downloadSourceButton.addEventListener("click",ASM_downloadSource);
4028              if(copyByteCodeButton) copyByteCodeButton.addEventListener("click",function(){ ASM_syncByteCodeFromBoard(); ASM_copyText(ASM_currentByteCodeText,"Byte code copied to clipboard."); });
4029              if(copyIncludeButton) copyIncludeButton.addEventListener("click",function(){
4030                var includeMount = ASM_el("ASM_includeMount");
4031                ASM_copyText(includeMount ? includeMount.value : "","Include file copied to clipboard.");
4032              });
4033              if(closeIncludeButton) closeIncludeButton.addEventListener("click",function(){ ASM_toggleIncludePane(false); });
4034              if(jsonPopupButton) jsonPopupButton.addEventListener("click",function(){ ASM_toggleOutputPopup("json"); });
4035              if(diagnosticsButton) diagnosticsButton.addEventListener("click",ASM_jumpToErrors);
4036              if(outputPopupCopy) outputPopupCopy.addEventListener("click",function(){
4037                var text = ASM_el("ASM_outputPopupText");
4038                ASM_copyText(
4039                  text ? text.value : "",
4040                  ASM_outputPopupKind == "diagnostics" ? "Diagnostics copied to clipboard." : "JSON copied to clipboard."
4041                );
4042              });
4043              if(outputPopupClose) outputPopupClose.addEventListener("click",ASM_closeOutputPopup);
4044              if(outputPopupBack) outputPopupBack.addEventListener("click",function(ev){
4045                if(ev.target === outputPopupBack) ASM_closeOutputPopup();
4046              });
4047              document.addEventListener("keydown",function(ev){
4048                if(ev.key == "Escape" && outputPopupBack && !outputPopupBack.hidden) ASM_closeOutputPopup();
4049              });
4050              if(boardText) boardText.addEventListener("input",function(){
4051                ASM_sourceByteCodeActive = false;
4052                ASM_setSourceByteCodeModeUI(false);
4053                ASM_syncByteCodeFromBoard();
4054                ASM_updateSourceByteCodeRangeInfo(ASM_sourceByteCodeOrigin(false),ASM_countByteCodeText(ASM_currentByteCodeText));
4055              });              
4056              if(encodeSourceButton)
4057              {
4058                encodeSourceButton.addEventListener("click",ASM_byteEncodeSource);
4059                encodeSourceButton.addEventListener("keydown",function(ev){
4060                  if(ev.key == "Enter" || ev.key == " ")
4061                  {
4062                    ev.preventDefault();
4063                    ASM_byteEncodeSource();
4064                  }
4065                });
4066              }   
4067              
4068              if(sourceByteCodeStartInput)
4069              {
4070                sourceByteCodeStartInput.addEventListener("input",ASM_refreshSourceByteCodeFromAddress);
4071                sourceByteCodeStartInput.addEventListener("change",function(){
4072                  ASM_normalizeSourceByteCodeStartInput();
4073                  ASM_refreshSourceByteCodeFromAddress();
4074                });
4075                sourceByteCodeStartInput.addEventListener("keydown",function(ev){
4076                  if(ev.key == "Enter")
4077                  {
4078                    ev.preventDefault();
4079                    ASM_normalizeSourceByteCodeStartInput();
4080                    if(!ASM_sourceByteCodeActive) ASM_byteEncodeSource();
4081                    else ASM_refreshSourceByteCodeFromAddress();
4082                    sourceByteCodeStartInput.blur();
4083                  }
4084                });
4085              }
4086
4087
4088              for(var i = 0; i < toEmulatorButtons.length; i++)
4089                toEmulatorButtons[i].addEventListener("click",ASM_toEmulator);
4090
4091              for(var j = 0; j < toDebuggerButtons.length; j++)
4092                toDebuggerButtons[j].addEventListener("click",ASM_toDebugger);
4093
4094              if(sourcePane)
4095              {
4096                /*
4097                 * Source is an auto-growing editor.  It must never retain a
4098                 * vertical internal scroll offset; page scrolling owns the
4099                 * vertical navigation for these side-by-side panes.
4100                 */
4101                sourcePane.addEventListener("scroll",function(){
4102                  if(sourcePane.scrollTop) sourcePane.scrollTop = 0;
4103                });
4104
4105                sourcePane.addEventListener("input",function(){
4106                  ASM_resizeSourceListingPanes();
4107                  ASM_refreshFindReplaceOverlay();
4108                  if(ASM_sourceByteCodeActive)
4109                    ASM_renderSourceByteCode({status:false,normalizeInput:false});
4110                });
4111                sourcePane.addEventListener("keydown",function(ev){
4112                  if((ev.ctrlKey || ev.metaKey) && ev.key == "Enter")
4113                  {
4114                    ev.preventDefault();
4115                    ASM_assembleCurrentSource();
4116                  }
4117                });
4118              }
4119
4120              if(listingColumnsInput) listingColumnsInput.addEventListener("change",function(){
4121                ASM_syncListingColumns(listingColumnsInput);
4122                ASM_clearOutputs("Listing columns changed. Click assemble to regenerate output.","warn");
4123              });
4124
4125              if(settingsListingColumnsInput) settingsListingColumnsInput.addEventListener("change",function(){
4126                ASM_syncListingColumns(settingsListingColumnsInput);
4127                ASM_clearOutputs("Listing columns changed. Click assemble to regenerate output.","warn");
4128              });
4129
4130              for(var p = 0; p < listingPresetButtons.length; p++)
4131              {
4132                listingPresetButtons[p].addEventListener("click",function(){
4133                  ASM_applyListingColumnPreset(this.getAttribute("data-listing-preset"));
4134                });
4135              }
4136
4137              if(dQuoteLegacyInput) dQuoteLegacyInput.addEventListener("change",function(){
4138                ASM_clearOutputs("dQuoteLegacy changed. Click assemble to regenerate output.","warn");
4139              });
4140
4141              ASM_syncSourceFindReplaceSkin();
4142            }
4143
4144
4145            ASM_registerPopupRules();
4146            ASM_bindEvents();
4147            ASM_updateSourceFileNameUI();
4148            ASM_updateSymbolFileNameFromSource();
4149            ASM_syncSourceFileNameTheme();
4150            ASM_syncListingColumns();
4151            ASM_bindSourceFindReplace();
4152            ASM_loadManualPane();
4153            ASM_toggleIncludePane(false);
4154            ASM_clearOutputs("Sample source loaded. Click assemble to generate output.","");
4155            ASM_updateSourceByteCodeRangeInfo(ASM_sourceByteCodeOrigin(false),0);
4156            ASM_resizeSourceListingPanes();
4157            ASM_refreshFindReplaceOverlay();
4158            ASM_equalizePaneHeaders();
4159            </script>
4159
4160
4161          </div>
4162
4163
4164          <!--
4165            ██████  ███████ ██████  ██    ██  ██████   ██████  ███████ ██████ 
4166            ██   ██ ██      ██   ██ ██    ██ ██       ██       ██      ██   ██
4167            ██   ██ █████   ██████  ██    ██ ██   ███ ██   ███ █████   ██████
4168            ██   ██ ██      ██   ██ ██    ██ ██    ██ ██    ██ ██      ██   ██
4169            ██████  ███████ ██████   ██████   ██████   ██████  ███████ ██   ██
4170          -->
4171
4172          <div id="tab3" style="width:1300px;min-height:720px;border-radius:10px;background-color:#76B9CE;padding: 0px 0px 14px 0px;box-sizing: border-box;user-select:none;-moz-user-select:none;-webkit-user-drag:none;-webkit-user-select:none;-ms-user-select:none;">
4173
4174
4175<style>
4176    /*
4177      Layout-first prototype for redesigned Debugger tab.
4178      This version intentionally uses mock content and lightweight UI toggles only.
4179    */
4180
4181    #DBG_root {
4182      position: relative;
4183    }
4184
4185    .DBG_paneHost {
4186      display: grid;
4187      grid-template-columns: repeat(3, minmax(0, 1fr));
4188      gap: 10px;
4189      width: calc(100% - 10px);
4190      max-width: calc(100% - 10px);
4191      align-items: stretch;
4192    }
4193
4194    .DBG_paneHost.DBG_hideWatch {
4195      grid-template-columns: repeat(2, minmax(0, 1fr));
4196    }
4197
4198    .DBG_paneHost.DBG_hideWatch #DBG_watchPane {
4199      display: none;
4200    }
4201
4202    .DBG_pane {
4203      min-width: 0;
4204      border-radius: 10px;
4205      overflow: visible;
4206    }
4207
4208    .DBG_paneTitle,
4209    .com-pane-title.DBG_paneTitle {
4210      display: flex;
4211      align-items: center;
4212      justify-content: space-between;
4213      gap: 10px;
4214      min-height: 34px;
4215      margin: 0px 0px 2px 0px;
4216      padding: 4px 8px;
4217      border-radius: 10px;
4218      border: 1px solid rgba(0,0,0,0.18);
4219      background: rgba(244,244,244,0.88);
4220      color: #000000;
4221      font-family: arial, sans-serif;
4222      font-size: 12px;
4223      line-height: 20px;
4224      box-sizing: border-box;
4225      overflow: visible;
4226      position: relative;
4227    }
4228
4229    .DBG_paneTitleMain {
4230      display: flex;
4231      align-items: center;
4232      gap: 8px;
4233      min-width: 0;
4234      white-space: nowrap;
4235    }
4236
4237    .DBG_paneTitleMain strong {
4238      color: #000000;
4239      font-size: 13px;
4240      font-weight: 700;
4241    }
4242
4243    .DBG_paneInfo {
4244      color: rgba(0,0,0,0.70);
4245      font-family: arial, sans-serif;
4246      font-size: 11px;
4247      white-space: nowrap;
4248    }
4249
4250    .DBG_headerActions,
4251    .com-pane-header-default-widget.DBG_headerActions {
4252      display: inline-flex;
4253      align-items: center;
4254      justify-content: flex-end;
4255      gap: 4px;
4256      flex-wrap: nowrap;
4257    }
4258
4259    .DBG_headerActions .appbut i {
4260      pointer-events: none;
4261    }
4262
4263    .DBG_headerActions.DBG_requestActive [data-dbg-action] {
4264      display: none !important;
4265    }
4266
4267    .DBG_traceRegisterInfo { font-family: courier, monospace; }
4268    .DBG_traceRegisterInfo sub { font-size: 8px; line-height: 0; }
4269
4270    .DBG_startPcRequester {
4271      display: inline-flex;
4272      align-items: center;
4273      justify-content: flex-end;
4274      gap: 4px;
4275      white-space: nowrap;
4276    }
4277
4278    .DBG_startPcRequester[hidden] {
4279      display: none !important;
4280    }
4281
4282    .DBG_startPcRequester span {
4283      color: rgba(0,0,0,0.72);
4284      font-family: arial, sans-serif;
4285      font-size: 11px;
4286      font-weight: 700;
4287    }
4288
4289    .DBG_startPcInput {
4290      width: 64px;
4291      min-height: 20px;
4292      font: 12px/1.2 courier, monospace;
4293      text-transform: uppercase;
4294      box-sizing: border-box;
4295    }
4296
4297    .DBG_paneBody {
4298      height: 595px;
4299      min-height: 595px;
4300      padding: 0px;
4301      position: relative;
4302      border-radius: 10px;
4303      border: 1px solid rgba(0,0,0,0.12);
4304      box-sizing: border-box;
4305      overflow: hidden;
4306    }
4307
4308    .DBG_textPane {
4309      display: block;
4310      width: 100%;
4311      height: 100%;
4312      margin: 0px;
4313      padding: 0px 30px;
4314      border: 0px;
4315      outline: none;
4316      resize: none;
4317      color: #000000;
4318      background: transparent;
4319      font-family: courier, monospace;
4320      font-size: 12px;
4321      font-weight: 500;
4322      line-height: 21px;
4323      box-sizing: border-box;
4324      tab-size: 4;
4325      position: relative;
4326      z-index: 1;
4327      white-space: pre;
4328      overflow: auto;
4329      user-select: text;
4330      -moz-user-select: text;
4331      -webkit-user-select: text;
4332      -ms-user-select: text;
4333      -webkit-user-drag: none;
4334      caret-color: transparent;
4335      font-variant-ligatures: none;
4336    }
4337
4338    .DBG_textPane::selection {
4339      background: rgba(80, 145, 230, 0.34);
4340    }
4341
4342    .DBG_listingPcOverlay 
4343    {
4344      position: absolute;
4345      left: 0px;
4346      right: 0px;
4347      height: 21px;
4348      background: rgba(255, 214, 64, 0.42);
4349      pointer-events: none;
4350      z-index: 0;
4351    }
4352
4353    .DBG_listingLineCurrent {
4354      display: block;
4355      margin: 0px -30px;
4356      padding: 0px 30px;
4357      background: rgba(255, 214, 64, 0.42);
4358    }
4359
4360    .DBG_breakpoint {
4361      color: #9a0000;
4362      font-weight: 700;
4363    }
4364
4365    .DBG_traceLineCurrent {
4366      display: block;
4367      margin: 0px -30px;
4368      padding: 0px 30px;
4369      background: rgba(140, 190, 220, 0.33);
4370    }
4371
4372
4373    /*
4374     * Debugger dark mode is applied dynamically by DBG_applyTheme().
4375     * Do not style #tab3 itself: the background around the panes must stay unchanged.
4376     */
4377  
4378    .DBG_symbolBox {
4379      width: 370px;
4380      max-width: 370px;
4381      height: auto;
4382      min-height: 63px;
4383      padding: 0px 6px;
4384      text-align: left;
4385      box-sizing: border-box;
4386      overflow: visible;
4387    }
4388
4389    .DBG_symbolLabelWrap {
4390      display: flex;
4391      flex-wrap: wrap;
4392      align-items: flex-start;
4393      gap: 2px 3px;
4394      width: 100%;
4395      min-width: 0;
4396      max-width: 100%;
4397      margin-top: 2px;
4398    }
4399
4400    .DBG_symbolLabelWrap .appbut.label {
4401      float: none !important;
4402      display: inline-flex;
4403      align-items: center;
4404      gap: 3px;
4405      width: auto !important;
4406      min-width: 0;
4407      height: auto;
4408      min-height: 16px;
4409      line-height: 14px;
4410      margin: 0px;
4411      border-radius: 3px;
4412      background: rgba(128,128,128,0.08);
4413      border: 1px solid black;
4414      color: #000000;
4415      font-family: arial;
4416      font-size: 11px;
4417      padding: 0px 2px;
4418      box-sizing: border-box;
4419      white-space: nowrap;
4420      cursor: default;
4421    }
4422
4423    .DBG_symbolLabelWrap .appbut.label:hover {
4424      background: rgba(255,255,255,0.75);
4425    }
4426
4427    .DBG_symbolLabelWrap .DBG_symbolRemove {
4428      float: none !important;
4429      display: inline-flex;
4430      align-items: center;
4431      justify-content: center;
4432      width: 14px !important;
4433      min-width: 14px;
4434      height: 14px;
4435      min-height: 14px;
4436      line-height: 12px;
4437      margin: 0px;
4438      padding: 0px;
4439      border-radius: 3px;
4440      font-size: 10px;
4441      cursor: pointer;
4442    }
4443
4444    .DBG_symbolOverlapBack {
4445      position: fixed;
4446      left: 0px;
4447      top: 0px;
4448      right: 0px;
4449      bottom: 0px;
4450      z-index: 1000;
4451      display: flex;
4452      align-items: center;
4453      justify-content: center;
4454      background: rgba(0,0,0,0.28);
4455    }
4456
4457    .DBG_symbolOverlapBack[hidden] {
4458      display: none !important;
4459    }
4460
4461    .DBG_symbolOverlapBox {
4462      width: 520px;
4463      max-width: calc(100vw - 40px);
4464      padding: 10px;
4465      border: 1px solid rgba(0,0,0,0.42);
4466      border-radius: 8px;
4467      background: #f4f4f4;
4468      color: #000000;
4469      box-shadow: 0px 4px 18px rgba(0,0,0,0.35);
4470      font-family: arial, sans-serif;
4471      font-size: 12px;
4472    }
4473
4474    .DBG_symbolOverlapBox strong {
4475      display: block;
4476      margin-bottom: 6px;
4477      font-size: 13px;
4478    }
4479
4480    .DBG_symbolOverlapBox pre {
4481      max-height: 220px;
4482      margin: 6px 0px;
4483      padding: 6px;
4484      overflow: auto;
4485      border: 1px solid rgba(0,0,0,0.20);
4486      border-radius: 4px;
4487      background: #ffffff;
4488      color: #000000;
4489      font-family: courier, monospace;
4490      font-size: 11px;
4491      white-space: pre-wrap;
4492    }
4493
4494    .DBG_symbolOverlapButtons {
4495      display: flex;
4496      justify-content: flex-end;
4497      gap: 4px;
4498      margin-top: 8px;
4499    }
4500
4501    .DBG_footerNote {
4502      margin: 8px 0px 0px 0px;
4503      padding: 6px 9px;
4504      border-radius: 8px;
4505      background: rgba(244,244,244,0.74);
4506      color: rgba(0,0,0,0.72);
4507      font-size: 11px;
4508    }
4509  </style>
4510
4511<script>
4512    var DBG_oPANE = null;
4513    var DBG_running = false;
4514    var DBG_RAM_SIZE = 0x10000;
4515    var DBG_RAM = new Uint8Array(DBG_RAM_SIZE);
4516    var DBG_currentByteCodeText = "";
4517    var DBG_debuggerStarted = false;
4518    var DBG_debuggerStartPC = 0x0000;
4519    var DBG_listingRows = [];
4520    var DBG_listingLines = [];
4521    var DBG_listingRowByPc = new Int32Array(DBG_RAM_SIZE);
4522    var DBG_listingRowByByteAddress = new Int32Array(DBG_RAM_SIZE);
4523    var DBG_listingCurrentPC = null;
4524    var DBG_listingScrollBehavior = "auto";
4525    var DBG_defaultListingColumns = {adr:0,code:6,lin:15,num:23,lbl:30,ins:40,opr:45,com:61};
4526    var DBG_listingColumnPresets = {
4527      "default":"{adr:0,code:6,lin:15,lbl:21,ins:30,opr:35,com:51}",
4528      "wide":"{adr:0,code:6,lin:17,lbl:24,ins:34,opr:40,com:60}",
4529      "compact":"{adr:0,code:6,lbl:15,ins:24,opr:29,com:45}"
4530    };
4531    var DBG_darkMode = false;
4532    var DBG_symbolTables = [];
4533    var DBG_symbolTablesById = {};
4534    var DBG_symbolAddressIndex = new Array(DBG_RAM_SIZE);
4535    var DBG_pendingSymbolImport = null;
4536    var DBG_nextSymbolTableId = 1;
4537    var DBG_traceCPU = null;
4538    var DBG_traceHW = null;
4539    var DBG_traceStarted = false;
4540    var DBG_traceStepCount = 0;
4541    var DBG_traceHistory = null;
4542    var DBG_traceLines = [];
4543    var DBG_traceCurrentWrites = null;
4544    var DBG_SELF_LOOP_THRESHOLD = 8;
4545    var DBG_runTimer = null;
4546    var DBG_runInstructionCount = 0;
4547    var DBG_runLastStatusStep = 0;
4548    var DBG_defaultTraceColumns = {stp:0,pc:5,byt:10,opc:19,rA:32,rX:35,rY:38,rSP:41,rP:44,cyc:47};
4549    var DBG_debugSessionFileName = "sample.DEB";
4550
4551    DBG_listingRowByPc.fill(-1);
4552    DBG_listingRowByByteAddress.fill(-1);
4553
4554
4555    function DBGUI()
4556    {
4557      /*
4558       * Debugger GUI helper container.
4559       *
4560       * Keep the debugger GUI local, but keep CPU-specific metadata owned by
4561       * the CPU implementation.  Opcode names, addressing modes, instruction
4562       * lengths and cycle counts come from Cpu6502.getConfig().
4563      */
4564
4565      this.getConfig = function()
4566      {
4567        var cpu =
4568          (typeof oEMU !== "undefined" &&
4569           oEMU &&
4570           oEMU.component &&
4571           oEMU.component.CPU) ? oEMU.component.CPU["6502"] : null;
4572
4573        if(cpu && typeof cpu.getConfig == "function")
4574          return cpu.getConfig();
4575
4576        if(typeof console !== "undefined" && console.warn)
4577          console.warn("DBGUI: Cpu6502.getConfig() is unavailable; opcode metadata cannot be resolved.");
4578
4579        return {opctab:[],instrlen:[],cycle_count:[]};
4580      };
4581
4582      this.getOpcodeTable = function()
4583      {
4584        return this.getConfig().opctab || [];
4585      };
4586
4587      /*
4588       * Compatibility property for older DBGUI call sites.
4589       * Source of truth remains Cpu6502.getConfig().opctab.
4590       */
4591      this.opctab = this.getOpcodeTable();
4592
4593      this.resetProcessor = function()
4594      {
4595        /*
4596         * Placeholder for future debugger GUI reset actions.
4597         * The current CPU trace reset is driven by DBG_startCpuTraceAtPC().
4598         */
4599        return true;
4600      };
4601
4602      this.getOpcodeEntry = function(opcode)
4603      {
4604        return this.getOpcodeTable()[opcode & 0xff] || ['???','imp'];
4605      };
4606
4607      this.instructionAddressMode = function(opcode)
4608      {
4609        var entry = this.getOpcodeEntry(opcode);
4610        return entry && entry[1] ? entry[1] : 'imp';
4611      };
4612
4613      this.instructionLength = function(opcode)
4614      {
4615        switch(this.instructionAddressMode(opcode))
4616        {
4617          case 'imm':
4618          case 'zpg':
4619          case 'zpx':
4620          case 'zpy':
4621          case 'inx':
4622          case 'iny':
4623          case 'rel':
4624            return 2;
4625
4626          case 'abs':
4627          case 'abx':
4628          case 'aby':
4629          case 'ind':
4630            return 3;
4631
4632          case 'acc':
4633          case 'imp':
4634          default:
4635            return 1;
4636        }
4637      };
4638
4639      this.disassemble = function(arg)
4640      {
4641        arg = arg || {};
4642
4643        if(typeof DASM !== 'function')
4644          return {adr_lst:'0000', opcode_lst:'', mnemonic:'???'};
4645
4646        var dasm = new DASM();
4647
4648        if(typeof arg.sym_search === 'function') dasm.sym_search = arg.sym_search;
4649        else dasm.sym_search = function(op, adm) { return op; };
4650
4651        if(typeof oCOM !== 'undefined' && oCOM && typeof oCOM.getHexByte === 'function')
4652          dasm.getHexByte = oCOM.getHexByte;
4653
4654        return dasm.disassemble({
4655          code_arr: arg.code_arr || [0,0,0],
4656          pc: typeof arg.pc === 'number' ? (arg.pc & 0xffff) : 0,
4657          opctab: this.getOpcodeTable()
4658        });
4659      };
4660    }
4661
4662    var oDBGUI = new DBGUI();
4663
4664
4665
4666
4667    function DBG_el(id) {
4668      return document.getElementById(id);
4669    }
4670
4671    function DBG_hexByte(v) {
4672      if (typeof oCOM !== "undefined" && oCOM && typeof oCOM.getHexByte === "function") return oCOM.getHexByte(v & 0xff);
4673      return ("0" + ((v & 0xff).toString(16).toUpperCase())).slice(-2);
4674    }
4675
4676    function DBG_hexWord(v) {
4677      if (typeof oCOM !== "undefined" && oCOM && typeof oCOM.getHexWord === "function") return oCOM.getHexWord(v & 0xffff);
4678      return ("000" + ((v & 0xffff).toString(16).toUpperCase())).slice(-4);
4679    }
4680
4681    function DBG_decodeDASMText(text) {
4682      return String(text || "")
4683        .replace(/<[^>]*>/g, "")
4684        .replace(/&nbsp;/g, " ")
4685        .replace(/&lt;/g, "<")
4686        .replace(/&gt;/g, ">")
4687        .replace(/&amp;/g, "&")
4688        .replace(/\s+/g, " ")
4689        .trim();
4690    }
4691
4692    function DBG_parseListingColumns(text) {
4693      var cfg = Object.assign({}, DBG_defaultListingColumns);
4694      String(text || "").replace(/([a-zA-Z_][\w]*)\s*:\s*(-?\d+)/g, function(_, key, val) {
4695        if (Object.prototype.hasOwnProperty.call(cfg, key)) cfg[key] = parseInt(val, 10) || 0;
4696        return "";
4697      });
4698      return cfg;
4699    }
4700
4701    function DBG_listingColumnsValue() {
4702      var input = DBG_el("DBG_listingColumnsInput");
4703      return input ? input.value : "{adr:0,code:6,lin:15,lbl:21,ins:30,opr:35,com:51}";
4704    }
4705
4706    function DBG_applyListingColumnPreset(name) {
4707      var input = DBG_el("DBG_listingColumnsInput");
4708      var spec = DBG_listingColumnPresets[name];
4709      if (!input || !spec) return false;
4710
4711      input.value = spec;
4712      DBG_disassembleRAM();
4713      return true;
4714    }
4715
4716    function DBG_cropListingField(text, width) {
4717      text = String(text == null ? "" : text);
4718      if (width == null || width <= 0 || text.length <= width) return text;
4719      if (width <= 1) return text.slice(0, width);
4720      return text.slice(0, width - 1) + "…";
4721    }
4722
4723    function DBG_formatListingParts(parts) {
4724      var columns = DBG_parseListingColumns(DBG_listingColumnsValue());
4725      var order = ["adr","code","lin","lbl","ins","opr","com"];
4726      if (typeof oCOM !== "undefined" && oCOM && typeof oCOM.renderTextTableRows === "function") {
4727        return oCOM.renderTextTableRows([parts], columns, {
4728          order: order,
4729          defaults: DBG_defaultListingColumns,
4730          trimRight: true,
4731          singleColumnRaw: true,
4732          ellipsis: "…"
4733        })[0];
4734      }
4735
4736      var line = "";
4737      for (var i = 0; i < order.length; i++) {
4738        var key = order[i];
4739        if (!parts[key]) continue;
4740        var col = columns[key] | 0;
4741        var width;
4742        for (var j = i + 1; j < order.length; j++) {
4743          if ((columns[order[j]] | 0) > col) { width = (columns[order[j]] | 0) - col; break; }
4744        }
4745        var text = DBG_cropListingField(parts[key], width);
4746        if (line.length < col) line = line.padEnd(col, " ");
4747        else if (line.length > col) line += " ";
4748        line += text;
4749      }
4750      return line.replace(/\s+$/, "");
4751    }
4752
4753    function DBG_countByteCodeText(text) {
4754      return DBG_parseByteCodeText(text).records.length;
4755    }
4756
4757    function DBG_parseByteCodeText(text) {
4758      var lines = String(text || "").split(/\r\n|\r|\n/);
4759      var records = [];
4760      var errors = [];
4761      var currentAddr = null;
4762
4763      for (var lineNo = 0; lineNo < lines.length; lineNo++) {
4764        var raw = lines[lineNo];
4765        var clean = raw.replace(/;.*/, "").replace(/\/\/.*$/, "").trim();
4766        if (!clean) continue;
4767
4768        var addrMatch = clean.match(/^\$?([0-9A-Fa-f]{4})\s*:?[\s,]*(.*)$/);
4769        var rest = clean;
4770
4771        if (addrMatch) {
4772          currentAddr = parseInt(addrMatch[1], 16) & 0xffff;
4773          rest = addrMatch[2] || "";
4774        }
4775
4776        var bytes = rest.match(/\b[0-9A-Fa-f]{2}\b/g);
4777        if (!bytes || !bytes.length) continue;
4778
4779        if (currentAddr === null) {
4780          errors.push("Line " + (lineNo + 1) + ": byte data has no address prefix.");
4781          continue;
4782        }
4783
4784        for (var i = 0; i < bytes.length; i++) {
4785          records.push({pc: currentAddr, val: parseInt(bytes[i], 16) & 0xff, line: lineNo + 1});
4786          currentAddr = (currentAddr + 1) & 0xffff;
4787        }
4788      }
4789
4790      return {records: records, errors: errors};
4791    }
4792
4793    function DBG_updateByteCodeInfo(byteCount, extraText) {
4794      var info = DBG_el("DBG_byteCodeInfo");
4795      if (info) info.textContent = (byteCount || 0) + " bytes" + (extraText ? " / " + extraText : "");
4796    }
4797
4798    function DBG_syncByteCodeFromBoard() {
4799      var board = DBG_el("DBG_boardText");
4800      DBG_currentByteCodeText = board ? board.value : "";
4801      var parsed = DBG_parseByteCodeText(DBG_currentByteCodeText);
4802      DBG_updateByteCodeInfo(parsed.records.length, parsed.errors.length ? parsed.errors.length + " parse warning(s)" : "");
4803      return parsed;
4804    }
4805
4806    function DBG_clearBoard() {
4807      DBG_currentByteCodeText = "";
4808      var board = DBG_el("DBG_boardText");
4809      if (board) board.value = "";
4810      DBG_updateByteCodeInfo(0);
4811    }
4812
4813
4814    function DBG_statusText(text) {
4815      var status = DBG_el("DBG_status");
4816      if (status) status.textContent = text;
4817    }
4818
4819    function DBG_parseHexAddress(text) {
4820      text = String(text == null ? "" : text).trim();
4821      text = text.replace(/^\$/,"").replace(/^0x/i,"");
4822      if (!/^[0-9A-Fa-f]{1,4}$/.test(text)) return null;
4823      var value = parseInt(text,16);
4824      if (isNaN(value) || value < 0 || value > 0xffff) return null;
4825      return value & 0xffff;
4826    }
4827
4828    function DBG_startPcHost(hostId) {
4829      return DBG_el(hostId || "DBG_traceHeaderActions");
4830    }
4831
4832    function DBG_showStartPcRequester(hostId) {
4833      var host = DBG_startPcHost(hostId);
4834      if (!host) return;
4835
4836      var requester = host.querySelector(".DBG_startPcRequester");
4837      var input = host.querySelector(".DBG_startPcInput");
4838      if (!requester || !input) return;
4839
4840      host.classList.add("DBG_requestActive");
4841      requester.hidden = false;
4842      input.value = "$" + DBG_hexWord(DBG_debuggerStartPC);
4843
4844      DBG_statusText("Enter hexadecimal start address for the debugger.");
4845
4846      window.setTimeout(function(){
4847        input.focus();
4848        input.select();
4849      },0);
4850
4851      DBG_equalizePaneHeaders();
4852    }
4853
4854    function DBG_hideStartPcRequester(hostId) {
4855      var host = DBG_startPcHost(hostId);
4856      if (!host) return;
4857
4858      var requester = host.querySelector(".DBG_startPcRequester");
4859      var input = host.querySelector(".DBG_startPcInput");
4860
4861      host.classList.remove("DBG_requestActive");
4862      if (requester) requester.hidden = true;
4863      if (input) input.value = "";
4864
4865      DBG_equalizePaneHeaders();
4866    }
4867
4868    function DBG_prepareDebuggerStartPC(pc,opts) {
4869      /*
4870       * Preset the debugger start PC without immediately starting execution.
4871       *
4872       * This is used by tab2's "to debugger" transfer.  After import, the
4873       * Listing is positioned at the assembler origin and the next click on
4874       * "Step one instruction forward" starts from that same address.
4875       */
4876      pc = Number(pc);
4877      if (isNaN(pc)) return false;
4878
4879      pc = pc & 0xffff;
4880      opts = opts || {};
4881
4882      DBG_debuggerStartPC = pc;
4883      DBG_debuggerStarted = false;
4884
4885      if (opts.clearTrace !== false) {
4886        DBG_traceCPU = null;
4887        DBG_traceHW = null;
4888        DBG_traceStarted = false;
4889        DBG_traceStepCount = 0;
4890        DBG_resetTraceHistory();
4891        DBG_traceLines = [];
4892        DBG_renderTraceView();
4893      }
4894
4895      var pcDisplay = DBG_el("dispPC");
4896      if (pcDisplay) pcDisplay.textContent = DBG_hexWord(pc);
4897
4898      var inputs = document.querySelectorAll(".DBG_startPcInput");
4899      for (var i = 0; i < inputs.length; i++)
4900        inputs[i].value = "$" + DBG_hexWord(pc);
4901
4902      return true;
4903    }
4904
4905    function DBG_prepareDebuggerStartPC(pc,opts) {
4906      /*
4907       * Preset the debugger start PC without immediately starting execution.
4908       *
4909       * This is used by tab2's "to debugger" transfer.  After import, the
4910       * Listing is positioned at the assembler origin and the next click on
4911       * "Step one instruction forward" starts from that same address.
4912       */
4913      pc = Number(pc);
4914      if (isNaN(pc)) return false;
4915
4916      pc = pc & 0xffff;
4917      opts = opts || {};
4918
4919      DBG_debuggerStartPC = pc;
4920      DBG_debuggerStarted = false;
4921
4922      if (opts.clearTrace !== false) {
4923        DBG_traceCPU = null;
4924        DBG_traceHW = null;
4925        DBG_traceStarted = false;
4926        DBG_traceStepCount = 0;
4927        DBG_traceLines = [];
4928        DBG_renderTraceView();
4929      }
4930
4931      var pcDisplay = DBG_el("dispPC");
4932      if (pcDisplay) pcDisplay.textContent = DBG_hexWord(pc);
4933
4934      var inputs = document.querySelectorAll(".DBG_startPcInput");
4935      for (var i = 0; i < inputs.length; i++)
4936        inputs[i].value = "$" + DBG_hexWord(pc);
4937
4938      return true;
4939    }
4940
4941    function DBG_startDebuggerAtPC(hostId) {
4942      var host = DBG_startPcHost(hostId);
4943      if (!host) return;
4944
4945      var input = host.querySelector(".DBG_startPcInput");
4946      var pc = DBG_parseHexAddress(input ? input.value : "");
4947
4948      if (pc === null) {
4949        DBG_statusText("Invalid start address. Use hexadecimal 0000-FFFF.");
4950        if (input) {
4951          input.focus();
4952          input.select();
4953        }
4954        return;
4955      }
4956
4957      DBG_debuggerStartPC = pc & 0xffff;
4958      DBG_debuggerStarted = true;
4959      DBG_startCpuTraceAtPC(DBG_debuggerStartPC);
4960
4961      var pcDisplay = DBG_el("dispPC");
4962      if (pcDisplay) pcDisplay.textContent = DBG_hexWord(DBG_debuggerStartPC);
4963
4964      var listed = DBG_scrollListingToPC(DBG_debuggerStartPC,{behavior:DBG_listingScrollBehavior});
4965
4966      DBG_hideStartPcRequester(hostId);
4967      DBG_statusText(
4968        "Debugger start address set to $" + DBG_hexWord(DBG_debuggerStartPC) + (listed ? " / listing positioned." : " / listing row not found.")
4969      );
4970    }
4971
4972    function DBG_startPcKeyDown(evt,hostId) {
4973      evt = evt || window.event;
4974      if (evt.key === "Enter") { evt.preventDefault(); DBG_startDebuggerAtPC(hostId); }
4975      else if (evt.key === "Escape") { evt.preventDefault(); DBG_hideStartPcRequester(hostId); }
4976    }
4977
4978
4979    function DBG_rangeListNormalize(scope) {
4980      var ranges = [];
4981      for (var i = 0; i < (scope || []).length; i++) {
4982        var r = scope[i] || [];
4983        if (r.length < 2) continue;
4984        var a = Number(r[0]);
4985        var b = Number(r[1]);
4986        if (isNaN(a) || isNaN(b)) continue;
4987        a = a & 0xffff;
4988        b = b & 0xffff;
4989        if (b < a) { var t = a; a = b; b = t; }
4990        ranges.push([a,b]);
4991      }
4992
4993      ranges.sort(function(x,y){ return x[0] - y[0] || x[1] - y[1]; });
4994
4995      var out = [];
4996      for (var j = 0; j < ranges.length; j++) {
4997        var cur = ranges[j];
4998        if (!out.length || cur[0] > out[out.length - 1][1] + 1) {
4999          out.push(cur.slice());
5000        } else if (cur[1] > out[out.length - 1][1]) {
5001          out[out.length - 1][1] = cur[1];
5002        }
5003      }
5004      return out;
5005    }
5006
5007    function DBG_rangeListSubtract(baseRanges, cutRanges) {
5008      var base = DBG_rangeListNormalize(baseRanges);
5009      var cuts = DBG_rangeListNormalize(cutRanges);
5010      var out = [];
5011
5012      for (var i = 0; i < base.length; i++) {
5013        var fragments = [base[i].slice()];
5014
5015        for (var j = 0; j < cuts.length && fragments.length; j++) {
5016          var cut = cuts[j];
5017          var next = [];
5018
5019          for (var k = 0; k < fragments.length; k++) {
5020            var f = fragments[k];
5021
5022            if (cut[1] < f[0] || cut[0] > f[1]) {
5023              next.push(f);
5024              continue;
5025            }
5026
5027            if (cut[0] > f[0]) next.push([f[0], cut[0] - 1]);
5028            if (cut[1] < f[1]) next.push([cut[1] + 1, f[1]]);
5029          }
5030
5031          fragments = next;
5032        }
5033
5034        for (var n = 0; n < fragments.length; n++) out.push(fragments[n]);
5035      }
5036
5037      return DBG_rangeListNormalize(out);
5038    }
5039
5040    function DBG_rangeListHex(ranges) {
5041      var out = [];
5042      for (var i = 0; i < (ranges || []).length; i++)
5043        out.push("$" + DBG_hexWord(ranges[i][0]) + "-$" + DBG_hexWord(ranges[i][1]));
5044      return out;
5045    }
5046
5047    function DBG_symbolSourceName(table) {
5048      var s = table && table.source ? String(table.source) : "";
5049      if (!s && table && table.filename) s = String(table.filename);
5050      return s || "symbol table";
5051    }
5052
5053    function DBG_normalizeSymbolTable(raw, fileName) {
5054      if (!raw || typeof raw !== "object") throw new Error("Invalid JSON symbol table.");
5055      if (raw.format && raw.format !== "RetroAppleJS-ASM-symbols")
5056        throw new Error("Unsupported symbol table format: " + raw.format);
5057
5058      var scope = DBG_rangeListNormalize(raw.scope || []);
5059      if (!scope.length) throw new Error("Symbol table has no usable scope.");
5060
5061      var table = {
5062        id: "symtab_" + (DBG_nextSymbolTableId++),
5063        source: DBG_symbolSourceName(raw) || fileName || "symbol table",
5064        filename: fileName || "",
5065        format: raw.format || "",
5066        version: raw.version || 0,
5067        scopeMode: raw.scopeMode || "instruction-pc",
5068        scope: scope,
5069        effectiveScope: scope.slice(),
5070        raw: raw,
5071        symbols: Array.isArray(raw.symbols) ? raw.symbols : [],
5072        byValue: {1:{}, 2:{}},
5073        labelsByAddress: {},
5074        commentsByAddress: {}
5075      };
5076
5077      for (var i = 0; i < table.symbols.length; i++) {
5078        var sym = table.symbols[i] || {};
5079        if (typeof sym.value !== "number") continue;
5080
5081        var value = sym.value & 0xffff;
5082
5083        /*
5084         * Comment entries use a reduced metadata set.  They must not enter
5085         * byValue, otherwise COMMENT_FD42 could be substituted as an operand
5086         * symbol.  Instruction/data comments are indexed by address and are
5087         * applied only after their opcode bytes match RAM at disassembly time.
5088         */
5089        if (sym.type === "comment") {
5090          if ((sym.targetType === "instruction" || sym.targetType === "data") &&
5091              typeof sym.comment === "string" &&
5092              sym.comment &&
5093              Array.isArray(sym.opcode) &&
5094              sym.opcode.length) {
5095            if (!table.commentsByAddress[value]) table.commentsByAddress[value] = [];
5096            table.commentsByAddress[value].push(sym);
5097          }
5098          continue;
5099        }
5100
5101        var bytes = sym.bytes === 1 ? 1 : 2;
5102
5103        if (!table.byValue[bytes][value]) table.byValue[bytes][value] = [];
5104        table.byValue[bytes][value].push(sym);
5105
5106        if (sym.type === "label") {
5107          if (!table.labelsByAddress[value]) table.labelsByAddress[value] = [];
5108          table.labelsByAddress[value].push(sym);
5109        }
5110      }
5111
5112      return table;
5113    }
5114
5115    function DBG_rebuildSymbolIndex() {
5116      DBG_symbolTablesById = {};
5117      DBG_symbolAddressIndex = new Array(DBG_RAM_SIZE);
5118
5119      for (var i = 0; i < DBG_symbolTables.length; i++) {
5120        var table = DBG_symbolTables[i];
5121        DBG_symbolTablesById[table.id] = table;
5122
5123        var ranges = table.effectiveScope || [];
5124        for (var r = 0; r < ranges.length; r++) {
5125          for (var a = ranges[r][0]; a <= ranges[r][1]; a++) {
5126            var pc = a & 0xffff;
5127            if (!DBG_symbolAddressIndex[pc]) DBG_symbolAddressIndex[pc] = [];
5128            if (DBG_symbolAddressIndex[pc].indexOf(table.id) < 0)
5129              DBG_symbolAddressIndex[pc].push(table.id);
5130          }
5131        }
5132      }
5133    }
5134
5135    function DBG_getSymbolTablesAt(pc) {
5136      var ids = DBG_symbolAddressIndex[pc & 0xffff] || [];
5137      var out = [];
5138      for (var i = 0; i < ids.length; i++) {
5139        var table = DBG_symbolTablesById[ids[i]];
5140        if (table) out.push(table);
5141      }
5142      return out;
5143    }
5144
5145    function DBG_findSymbolScopeOverlaps(scope) {
5146      var ranges = DBG_rangeListNormalize(scope);
5147      var overlaps = [];
5148      var active = null;
5149
5150      function flush() {
5151        if (active) {
5152          overlaps.push({
5153            from: active.from,
5154            to: active.to,
5155            tableIds: Object.keys(active.ids)
5156          });
5157          active = null;
5158        }
5159      }
5160
5161      for (var r = 0; r < ranges.length; r++) {
5162        for (var a = ranges[r][0]; a <= ranges[r][1]; a++) {
5163          var ids = DBG_symbolAddressIndex[a & 0xffff] || [];
5164          if (!ids.length) { flush(); continue; }
5165
5166          var key = ids.slice().sort().join("|");
5167          if (!active || active.key !== key || active.to + 1 !== a) {
5168            flush();
5169            active = {from:a, to:a, key:key, ids:{}};
5170            for (var i = 0; i < ids.length; i++) active.ids[ids[i]] = true;
5171          } else {
5172            active.to = a;
5173            for (var j = 0; j < ids.length; j++) active.ids[ids[j]] = true;
5174          }
5175        }
5176        flush();
5177      }
5178
5179      return overlaps;
5180    }
5181
5182    function DBG_formatSymbolOverlapReport(incoming, overlaps) {
5183      var lines = [];
5184      lines.push("New table: " + DBG_symbolSourceName(incoming));
5185      lines.push("New scope: " + DBG_rangeListHex(incoming.scope).join(", "));
5186      lines.push("");
5187      lines.push("Overlapping ranges:");
5188
5189      for (var i = 0; i < overlaps.length; i++) {
5190        var ov = overlaps[i];
5191        var names = [];
5192        for (var j = 0; j < ov.tableIds.length; j++) {
5193          var table = DBG_symbolTablesById[ov.tableIds[j]];
5194          if (table) names.push(DBG_symbolSourceName(table));
5195        }
5196        lines.push("  $" + DBG_hexWord(ov.from) + "-$" + DBG_hexWord(ov.to) + "  ->  " + names.join(", "));
5197      }
5198
5199      lines.push("");
5200      lines.push("Cancel: do not import this table.");
5201      lines.push("Override: new table replaces existing tables in the overlapping address ranges.");
5202      lines.push("Merge: import only the non-overlapping part of the new table.");
5203
5204      return lines.join("\n");
5205    }
5206
5207    function DBG_showSymbolOverlapPopup(incoming, overlaps) {
5208      DBG_pendingSymbolImport = {table: incoming, overlaps: overlaps};
5209
5210      var text = DBG_el("DBG_symbolOverlapText");
5211      if (text) text.textContent = DBG_formatSymbolOverlapReport(incoming, overlaps);
5212
5213      var popup = DBG_el("DBG_symbolOverlapPopup");
5214      if (popup) popup.hidden = false;
5215    }
5216
5217    function DBG_resolveSymbolOverlap(choice) {
5218      var pending = DBG_pendingSymbolImport;
5219      DBG_pendingSymbolImport = null;
5220
5221      var popup = DBG_el("DBG_symbolOverlapPopup");
5222      if (popup) popup.hidden = true;
5223
5224      if (!pending || choice === "cancel") {
5225        DBG_statusText("Symbol table import cancelled.");
5226        return;
5227      }
5228
5229      if (choice === "override") {
5230        for (var i = 0; i < DBG_symbolTables.length; i++) {
5231          DBG_symbolTables[i].effectiveScope =
5232            DBG_rangeListSubtract(DBG_symbolTables[i].effectiveScope, pending.table.scope);
5233        }
5234        DBG_symbolTables = DBG_symbolTables.filter(function(t){
5235          return t.effectiveScope && t.effectiveScope.length;
5236        });
5237        pending.table.effectiveScope = pending.table.scope.slice();
5238        DBG_addSymbolTable(pending.table);
5239        return;
5240      }
5241
5242      if (choice === "merge") {
5243        var occupied = [];
5244        for (var j = 0; j < DBG_symbolTables.length; j++)
5245          occupied = occupied.concat(DBG_symbolTables[j].effectiveScope || []);
5246
5247        pending.table.effectiveScope = DBG_rangeListSubtract(pending.table.scope, occupied);
5248
5249        if (!pending.table.effectiveScope.length) {
5250          DBG_statusText("Symbol table merge skipped: no non-overlapping scoped addresses.");
5251          return;
5252        }
5253
5254        DBG_addSymbolTable(pending.table);
5255      }
5256    }
5257
5258    function DBG_addSymbolTable(table, options) {
5259      options = options || {};
5260
5261      DBG_symbolTables.push(table);
5262      DBG_rebuildSymbolIndex();
5263      DBG_renderSymbolLabels();
5264      if(options.disassemble !== false)
5265        DBG_disassembleRAM();
5266
5267      if(options.status !== false)
5268      {
5269        DBG_statusText(
5270          "Loaded symbol table: " + DBG_symbolSourceName(table) +
5271          " / " + table.symbols.length + " symbol(s) / active scope " +
5272          DBG_rangeListHex(table.effectiveScope).join(", ")
5273        );
5274      }      
5275    }
5276
5277    function DBG_removeSymbolTable(id) {
5278      DBG_symbolTables = DBG_symbolTables.filter(function(t){ return t.id !== id; });
5279      DBG_rebuildSymbolIndex();
5280      DBG_renderSymbolLabels();
5281      DBG_disassembleRAM();
5282      DBG_statusText("Removed symbol table.");
5283    }
5284
5285    function DBG_renderSymbolLabels() {
5286      var wrap = DBG_el("DBG_symbolLabelWrap");
5287      if (!wrap) return;
5288
5289      wrap.replaceChildren();
5290
5291      for (var i = 0; i < DBG_symbolTables.length; i++) {
5292        var table = DBG_symbolTables[i];
5293
5294        var chip = document.createElement("div");
5295        chip.className = "appbut label";
5296        chip.title = DBG_rangeListHex(table.effectiveScope).join(", ");
5297        chip.appendChild(document.createTextNode(DBG_symbolSourceName(table) + " "));
5298
5299        var x = document.createElement("div");
5300        x.className = "appbut skinny DBG_symbolRemove";
5301        x.title = "Remove symbol table";
5302        x.textContent = "x";
5303        x.setAttribute("data-symbol-id", table.id);
5304        x.onclick = function(e) {
5305          e.stopPropagation();
5306          DBG_removeSymbolTable(this.getAttribute("data-symbol-id"));
5307        };
5308
5309        chip.appendChild(x);
5310        wrap.appendChild(chip);
5311      }
5312    }
5313
5314    async function DBG_onSymbolFileChange(evt) {
5315      var input = evt && evt.target ? evt.target : DBG_el("DBG_sym_fileInput");
5316      var file = input && input.files && input.files[0] ? input.files[0] : null;
5317      if (!file) return;
5318
5319      try {
5320        var raw = JSON.parse(await file.text());
5321        var table = DBG_normalizeSymbolTable(raw, file.name);
5322        var overlaps = DBG_findSymbolScopeOverlaps(table.scope);
5323        DBG_updateDebugSessionFileNameFromSource(file.name);
5324
5325        if (overlaps.length) DBG_showSymbolOverlapPopup(table, overlaps);
5326        else DBG_addSymbolTable(table);
5327      } catch (err) {
5328        DBG_statusText("Symbol table import failed: " + (err && err.message ? err.message : String(err)));
5329        if (typeof console !== "undefined" && console.error) console.error(err);
5330      } finally {
5331        if (input) input.value = "";
5332      }
5333    }
5334
5335    function DBG_receiveSymbolTable(raw, fileName, options) {
5336      /*
5337       * In-memory companion to DBG_onSymbolFileChange().
5338       *
5339       * Used by tab2's "to debugger" action so byte-code transfer can also
5340       * update the debugger with labels, EQU values and scoped comments from
5341       * the assembler parser without a manual export/import round-trip.
5342       */
5343      options = options || {};
5344
5345      try {
5346        var table = DBG_normalizeSymbolTable(raw, fileName || "");
5347        DBG_updateDebugSessionFileNameFromSource(fileName || table.source || "");
5348        var overlaps = DBG_findSymbolScopeOverlaps(table.scope);
5349        var mode = options.overlap || "ask";
5350
5351        if(overlaps.length && mode == "override")
5352        {
5353          for(var i = 0; i < DBG_symbolTables.length; i++)
5354          {
5355            DBG_symbolTables[i].effectiveScope =
5356              DBG_rangeListSubtract(DBG_symbolTables[i].effectiveScope, table.scope);
5357          }
5358
5359          DBG_symbolTables = DBG_symbolTables.filter(function(t){
5360            return t.effectiveScope && t.effectiveScope.length;
5361          });
5362
5363          table.effectiveScope = table.scope.slice();
5364          DBG_addSymbolTable(table, options);
5365          return true;
5366        }
5367
5368        if(overlaps.length && mode == "merge")
5369        {
5370          var occupied = [];
5371          for(var j = 0; j < DBG_symbolTables.length; j++)
5372            occupied = occupied.concat(DBG_symbolTables[j].effectiveScope || []);
5373
5374          table.effectiveScope = DBG_rangeListSubtract(table.scope, occupied);
5375
5376          if(!table.effectiveScope.length)
5377          {
5378            if(options.status !== false)
5379              DBG_statusText("Symbol table merge skipped: no non-overlapping scoped addresses.");
5380            return false;
5381          }
5382
5383          DBG_addSymbolTable(table, options);
5384          return true;
5385        }
5386
5387        if(overlaps.length)
5388        {
5389          DBG_showSymbolOverlapPopup(table, overlaps);
5390          return false;
5391        }
5392
5393        DBG_addSymbolTable(table, options);
5394        return true;
5395      } catch(err) {
5396        DBG_statusText("Symbol table import failed: " + (err && err.message ? err.message : String(err)));
5397        if(typeof console !== "undefined" && console.error) console.error(err);
5398        return false;
5399      }
5400    }
5401
5402    function DBG_symbolNameForValue(instrPc, value, bytes) {
5403      var tables = DBG_getSymbolTablesAt(instrPc);
5404      value = value & (bytes === 1 ? 0xff : 0xffff);
5405      bytes = bytes === 1 ? 1 : 2;
5406
5407      for (var i = 0; i < tables.length; i++) {
5408        var list = tables[i].byValue[bytes][value] || [];
5409        if (list.length) return list[0].name;
5410      }
5411
5412      return "";
5413    }
5414
5415    function DBG_labelForAddress(instrPc) {
5416      var tables = DBG_getSymbolTablesAt(instrPc);
5417      var value = instrPc & 0xffff;
5418
5419      for (var i = 0; i < tables.length; i++) {
5420        var list = tables[i].labelsByAddress[value] || [];
5421        if (list.length) return list[0].name;
5422      }
5423
5424      return "";
5425    }
5426
5427    function DBG_applySymbolsToOperand(operand, instrPc) {
5428      return String(operand || "").replace(/\$([0-9A-Fa-f]{2,4})\b/g, function(match, hex) {
5429        var bytes = hex.length <= 2 ? 1 : 2;
5430        var value = parseInt(hex,16);
5431        var name = DBG_symbolNameForValue(instrPc,value,bytes);
5432        return name || match;
5433      });
5434    }
5435
5436
5437    function DBG_opcodeMatchesAt(pcAddr, opcode) {
5438      if (!Array.isArray(opcode) || !opcode.length) return false;
5439
5440      pcAddr = pcAddr & 0xffff;
5441      for (var i = 0; i < opcode.length; i++) {
5442        var b = Number(opcode[i]);
5443        if (isNaN(b)) return false;
5444        if ((DBG_RAM[(pcAddr + i) & 0xffff] & 0xff) !== (b & 0xff)) return false;
5445      }
5446
5447      return true;
5448    }
5449
5450    function DBG_commentForInstruction(instrPc) {
5451      var tables = DBG_getSymbolTablesAt(instrPc);
5452      var value = instrPc & 0xffff;
5453
5454      for (var i = 0; i < tables.length; i++) {
5455        var table = tables[i] || {};
5456        var list = table.commentsByAddress && table.commentsByAddress[value] ? table.commentsByAddress[value] : [];
5457
5458        for (var j = 0; j < list.length; j++) {
5459          var sym = list[j] || {};
5460          if (sym.targetType !== "instruction" && sym.targetType !== "data") continue;
5461          if (!DBG_opcodeMatchesAt(value, sym.opcode)) continue;
5462          return String(sym.comment || "");
5463        }
5464      }
5465
5466      return "";
5467    }
5468
5469    function DBG_formatDisassemblyComment(comment) {
5470      comment = String(comment || "").trim();
5471      if (!comment) return "";
5472      return /^;
5472/.test(comment) ? comment : "; " + comment;
5473    }
5474
5475    function DBG_instructionAddressMode(opcode) {
5476      return oDBGUI.instructionAddressMode(opcode);
5477    }
5478
5479    function DBG_instructionLength(opcode) {
5480      return oDBGUI.instructionLength(opcode);
5481    }
5482
5483    function DBG_instructionTarget(pcAddr, mode) {
5484      pcAddr = pcAddr & 0xffff;
5485
5486      if (mode === "rel") {
5487        var off = DBG_RAM[(pcAddr + 1) & 0xffff] & 0xff;
5488        if (off & 0x80) off -= 0x100;
5489        return (pcAddr + 2 + off) & 0xffff;
5490      }
5491
5492      if (mode === "abs") {
5493        return (DBG_RAM[(pcAddr + 1) & 0xffff] | (DBG_RAM[(pcAddr + 2) & 0xffff] << 8)) & 0xffff;
5494      }
5495
5496      return null;
5497    }
5498
5499    function DBG_listingLineAssembler() {
5500      if (typeof ASM !== "function") return null;
5501
5502      var asm = new ASM({listingColumns: DBG_listingColumnsValue()});
5503      if (typeof asm.setListingColumns === "function")
5504        asm.setListingColumns(DBG_listingColumnsValue());
5505
5506      return asm;
5507    }
5508
5509    function DBG_applyDisassemblyJumpLines(rows) {
5510      var asm = DBG_listingLineAssembler();
5511      if (asm && typeof asm.applyListingLineColumn === "function")
5512        asm.applyListingLineColumn(rows || []);
5513      return rows || [];
5514    }
5515
5516    function DBG_formatDisassemblyRow(row) {
5517      row = row || {};
5518      return DBG_formatListingParts({
5519        adr: DBG_hexWord(row.pc).toLowerCase() + ":",
5520        code: (row.byteHex || []).join(" ").toLowerCase(),
5521        lin: row.lin || "",
5522        lbl: row.label || "",
5523        ins: row.ins || "",
5524        opr: row.opr || "",
5525        com: row.com || ""
5526      });
5527    }
5528
5529
5530    function DBG_disassembleInstruction(pcAddr) {
5531      var pcv = pcAddr & 0xffff;
5532      var opcode = DBG_RAM[pcv] & 0xff;
5533      var mode = DBG_instructionAddressMode(opcode);
5534      var len = DBG_instructionLength(opcode);
5535      var ret = oDBGUI.disassemble({
5536        code_arr: [DBG_RAM[pcv], DBG_RAM[(pcv + 1) & 0xffff], DBG_RAM[(pcv + 2) & 0xffff]],
5537        pc: pcv,
5538        sym_search: function(op, adm) { return op; }
5539      });
5540
5541      var mnemonicText = DBG_decodeDASMText(ret.mnemonic);
5542      var ins = mnemonicText;
5543      var opr = "";
5544      var split = mnemonicText.match(/^([^\s]+)(?:\s+(.*))?$/);
5545      if (split) {
5546        ins = split[1] || "";
5547        opr = split[2] || "";
5548      }
5549
5550      opr = DBG_applySymbolsToOperand(opr, pcv);
5551      var label = DBG_labelForAddress(pcv);
5552
5553      var bytes = [];
5554      var byteHex = [];
5555      for (var i = 0; i < len; i++) {
5556        var b = DBG_RAM[(pcv + i) & 0xffff] & 0xff;
5557        bytes.push(b);
5558        byteHex.push(DBG_hexByte(b));
5559      }
5560       var comment = DBG_commentForInstruction(pcv);
5561      var row = {
5562        pc: pcv,
5563        bytes: bytes,
5564        byteHex: byteHex,
5565        mnemonic: ins,
5566        addrMode: mode,
5567        val: DBG_instructionTarget(pcv, mode),
5568        lin: "",
5569        label: label,
5570        ins: ins,
5571        opr: opr,
5572        com: DBG_formatDisassemblyComment(comment)
5573      };      
5574
5575      return {
5576        len: len,
5577        row: row,
5578        line: DBG_formatDisassemblyRow(row)        
5579      };
5580    }
5581
5582    function DBG_resetListingIndex() {
5583      DBG_listingRows = [];
5584      DBG_listingLines = [];
5585      DBG_listingRowByPc.fill(-1);
5586      DBG_listingRowByByteAddress.fill(-1);
5587    }
5588
5589    function DBG_indexDisassemblyRows(rows,lines) {
5590      DBG_resetListingIndex();
5591      DBG_listingRows = rows || [];
5592      DBG_listingLines = lines || [];
5593
5594      for (var i = 0; i < DBG_listingRows.length; i++) {
5595        var row = DBG_listingRows[i] || {};
5596        if (typeof row.pc !== "number") continue;
5597
5598        var pc = row.pc & 0xffff;
5599        var len = row.bytes && row.bytes.length ? row.bytes.length : 1;
5600
5601        DBG_listingRowByPc[pc] = i;
5602        for (var b = 0; b < len; b++)
5603          DBG_listingRowByByteAddress[(pc + b) & 0xffff] = i;
5604      }
5605    }
5606
5607    function DBG_listingRowIndexForPC(pc) {
5608      pc = pc & 0xffff;
5609
5610      var rowIndex = DBG_listingRowByPc[pc];
5611      if (rowIndex >= 0) return rowIndex;
5612
5613      /*
5614       * Fallback: if the selected address is inside a disassembled instruction
5615       * byte sequence, scroll to that containing row.  This keeps navigation
5616       * useful even before the future virtual tracer can re-decode from any
5617       * arbitrary PC alignment.
5618       */
5619      rowIndex = DBG_listingRowByByteAddress[pc];
5620      return rowIndex >= 0 ? rowIndex : -1;
5621    }
5622
5623    function DBG_listingPaneBody() {
5624      return DBG_el("debugfield");
5625    }
5626
5627    function DBG_listingLineHeight() {
5628      var pre = DBG_listingPaneBody();
5629      if (!pre || !window.getComputedStyle) return 21;
5630
5631      var lh = parseFloat(window.getComputedStyle(pre).lineHeight);
5632      return isFinite(lh) && lh > 0 ? lh : 21;
5633    }
5634
5635    function DBG_positionListingPcOverlay(rowIndex) {
5636      var overlay = DBG_el("DBG_listingPcOverlay");
5637      if (!overlay || rowIndex == null || rowIndex < 0) return;
5638
5639      var lineH = DBG_listingLineHeight();
5640      var body = DBG_listingPaneBody();
5641      var scrollTop = body ? body.scrollTop : 0;
5642      overlay.style.top = Math.round((rowIndex * lineH) - scrollTop) + "px";
5643      overlay.style.height = Math.ceil(lineH) + "px";
5644      overlay.hidden = false;
5645    }
5646
5647    function DBG_hideListingPcOverlay() {
5648      var overlay = DBG_el("DBG_listingPcOverlay");
5649      if (overlay) overlay.hidden = true;
5650    }
5651
5652    function DBG_scrollListingToPC(pc,opts) {
5653      pc = pc & 0xffff;
5654
5655      /*
5656       * Full 64K disassembly is acceptable before tracing starts.  During
5657       * tracing, this function should only consume the existing row index;
5658       * the later virtual scroller/tracer will update a small visible window.
5659       */
5660      if (!DBG_listingRows.length) DBG_disassembleRAM();
5661
5662      var rowIndex = DBG_listingRowIndexForPC(pc);
5663      if (rowIndex < 0) {
5664        DBG_hideListingPcOverlay();
5665        return false;
5666      }
5667
5668      var body = DBG_listingPaneBody();
5669      if (!body) return false;
5670
5671      var lineH = DBG_listingLineHeight();
5672      var rowTop = Math.max(0, Math.round(rowIndex * lineH));
5673      var targetTop = Math.max(0, Math.round(rowTop - ((body.clientHeight - lineH) / 2)));
5674      var behavior = opts && opts.behavior ? opts.behavior : DBG_listingScrollBehavior;
5675      var doScroll = !opts || opts.scroll !== false;
5676
5677      if (doScroll) {
5678        if (typeof body.scrollTo === "function")
5679          body.scrollTo({top:targetTop,behavior:behavior});
5680        else
5681          body.scrollTop = targetTop;
5682      }
5683
5684      DBG_listingCurrentPC = pc;
5685      DBG_positionListingPcOverlay(rowIndex);
5686      return true;
5687    }
5688
5689    function DBG_updateListingPC(pc) {
5690      return DBG_scrollListingToPC(pc,{
5691        behavior: DBG_listingScrollBehavior,
5692        scroll: DBG_scrollListingEnabled()
5693      });
5694    }
5695
5696    function DBG_disassembleRAM() {
5697      var rows = [];
5698      var lines = [];
5699      var pcAddr = 0;
5700      while (pcAddr < DBG_RAM_SIZE) {
5701        var d = DBG_disassembleInstruction(pcAddr);
5702        if (d && d.row) rows.push(d.row);
5703        pcAddr += Math.max(1, d.len || 1);
5704      }
5705
5706      DBG_applyDisassemblyJumpLines(rows);
5707      for (var i = 0; i < rows.length; i++)
5708        lines.push(DBG_formatDisassemblyRow(rows[i]));
5709
5710      DBG_indexDisassemblyRows(rows,lines);
5711
5712      var debugfield = DBG_el("debugfield");
5713      if (debugfield) debugfield.value = lines.join("\n");
5714
5715      if (DBG_listingCurrentPC != null) DBG_scrollListingToPC(DBG_listingCurrentPC,{behavior:"auto"});
5716      else DBG_hideListingPcOverlay();
5717
5718      var info = DBG_el("DBG_listingInfo");
5719      if (info) info.textContent = lines.length + " lines / 64K";
5720
5721      return lines.length;
5722    }
5723
5724     function DBG_traceColumnsValue() {
5725       var input = DBG_el("DBG_traceColumnsInput");
5726       return input ? input.value : "{stp:0,pc:5,byt:10,opc:19,rA:32,rX:35,rY:38,rSP:41,rP:44,cyc:47}";
5727
5728      }
5729 
5730     function DBG_parseTraceColumns(text) {
5731       var cfg = Object.assign({}, DBG_defaultTraceColumns);
5732       String(text || "").replace(/([a-zA-Z_][\w]*)\s*:\s*(-?\d+)/g, function(_, key, val) {
5733         if (Object.prototype.hasOwnProperty.call(cfg, key)) cfg[key] = parseInt(val, 10) || 0;
5734         return "";
5735       });
5736       return cfg;
5737     }
5738 
5739     function DBG_formatTraceParts(parts) {
5740       var columns = DBG_parseTraceColumns(DBG_traceColumnsValue());
5741       var order = ["stp","pc","byt","opc","rA","rX","rY","rSP","rP","cyc"];
5742       var line = "";
5743 
5744       for (var i = 0; i < order.length; i++) {
5745         var key = order[i];
5746         if (parts[key] == null || parts[key] === "") continue;
5747         var col = columns[key] | 0;
5748         var width;
5749 
5750         for (var j = i + 1; j < order.length; j++) {
5751           if ((columns[order[j]] | 0) > col) { width = (columns[order[j]] | 0) - col; break; }
5752         }
5753 
5754         var text = DBG_cropListingField(String(parts[key]), width);
5755         if (line.length < col) line = line.padEnd(col, " ");
5756         else if (line.length > col) line += " ";
5757         line += text;
5758       }
5759 
5760       return line.replace(/\s+$/, "");
5761     }
5762 
5763     function DBG_escapeHTML(text) {
5764       return String(text == null ? "" : text)
5765         .replace(/&/g,"&amp;")
5766         .replace(/</g,"&lt;")
5767         .replace(/>/g,"&gt;")
5768         .replace(/\"/g,"&quot;")
5769         .replace(/'/g,"&#039;");
5770     }
5771 
5772     function DBG_createCpuTraceHw() {
5773       var hw = {
5774         nmi_signal: false,
5775         irq_signal: false,
5776         RD: [],
5777         WR: [],
5778         lineDecode: function(addr) { return ((addr & 0xffff) >> 12) & 0x0f; }
5779       };
5780 
5781       for (var i = 0; i < 16; i++) {
5782         hw.RD[i] = function(addr) { return DBG_RAM[addr & 0xffff] & 0xff; };
5783         hw.WR[i] = function(addr,d8) {
5784           var adr = addr & 0xffff;
5785           var oldVal = DBG_RAM[adr] & 0xff;
5786           var newVal = d8 & 0xff;
5787           if(DBG_traceCurrentWrites) DBG_traceCurrentWrites.push({addr:adr,oldVal:oldVal,newVal:newVal});
5788           DBG_RAM[adr] = newVal;
5789           return 0x00;
5790         };
5791       }
5792 
5793       return hw;
5794     }
5795 
5796     function DBG_armSelfLoopTrap()
5797     {
5798       if(DBG_traceCPU && typeof DBG_traceCPU.setSelfLoopTrap === "function")
5799         DBG_traceCPU.setSelfLoopTrap(true,DBG_SELF_LOOP_THRESHOLD);
5800     }
5801
5802     function DBG_getSelfLoopTrap()
5803     {
5804       return DBG_traceCPU && typeof DBG_traceCPU.getSelfLoopTrap === "function"
5805         ? DBG_traceCPU.getSelfLoopTrap()
5806         : null;
5807     }
5808
5809     function DBG_selfLoopTrapText(hit)
5810     {
5811       if(!hit) return "";
5812
5813       var decoded = DBG_traceInstructionTextAt(hit.pc & 0xffff);
5814       var msg = "SELF-LOOP TRAP at $" + DBG_hexWord(hit.pc)
5815         + " after " + hit.repeats + " consecutive instructions";
5816
5817       if(decoded && decoded.op) msg += " / " + decoded.op;
5818
5819       msg += " / A=$" + DBG_hexByte(hit.a)
5820         + " X=$" + DBG_hexByte(hit.x)
5821         + " Y=$" + DBG_hexByte(hit.y)
5822         + " P=$" + DBG_hexByte(hit.p)
5823         + " SP=$" + DBG_hexByte(hit.sp);
5824
5825       return msg;
5826     }
5827
5828      function DBG_settingChecked(id,defaultValue) {
5829        var el = DBG_el(id);
5830        if(!el) return !!defaultValue;
5831        return !!el.checked;
5832      }
5833
5834       function DBG_safeDebugBaseName(name) {
5835         name = String(name || "").trim();
5836         name = name.replace(/[\\/:*?\"<>|]+/g,"_");
5837         name = name.replace(/\.[^.]*$/,"");
5838         name = name.replace(/[^A-Za-z0-9_.-]+/g,"_");
5839         return name || "sample";
5840       }
5841
5842       function DBG_defaultDebugSessionFileName(sourceName) {
5843         return DBG_safeDebugBaseName(sourceName || "sample") + ".DEB";
5844       }
5845
5846       function DBG_safeDebugSessionFileName(name) {
5847         name = String(name || "").trim();
5848         name = name.replace(/[\\/:*?\"<>|]+/g,"_");
5849         name = name.replace(/\.[^.]*$/,"");
5850         name = name.replace(/[^A-Za-z0-9_.-]+/g,"_");
5851         return (name || DBG_safeDebugBaseName(DBG_debugSessionFileName)) + ".DEB";
5852       }
5853
5854       function DBG_updateDebugSessionFileNameUI() {
5855         var input = DBG_el("DBG_debugSessionFileNameInput");
5856         if(input && input.value !== DBG_debugSessionFileName)
5857           input.value = DBG_debugSessionFileName;
5858       }
5859
5860       function DBG_setDebugSessionFileName(name) {
5861         DBG_debugSessionFileName = DBG_safeDebugSessionFileName(name || DBG_debugSessionFileName);
5862         DBG_updateDebugSessionFileNameUI();
5863         return DBG_debugSessionFileName;
5864       }
5865
5866       function DBG_updateDebugSessionFileNameFromSource(sourceName) {
5867         if(!sourceName) return DBG_debugSessionFileName;
5868         DBG_debugSessionFileName = DBG_defaultDebugSessionFileName(sourceName);
5869         DBG_updateDebugSessionFileNameUI();
5870         return DBG_debugSessionFileName;
5871       }
5872
5873       function DBG_getDebugSessionFileName() {
5874         var input = DBG_el("DBG_debugSessionFileNameInput");
5875         return DBG_setDebugSessionFileName(input ? input.value : DBG_debugSessionFileName);
5876       }
5877
5878       function DBG_downloadText(filename,text,mime) {
5879         filename = filename || "debug.DEB";
5880         text = text || "";
5881         mime = mime || "application/json;charset=utf-8";
5882
5883         var blob = new Blob([text],{type:mime});
5884         var url = URL.createObjectURL(blob);
5885         var a = document.createElement("a");
5886
5887         a.href = url;
5888         a.download = filename;
5889         a.style.display = "none";
5890
5891         document.body.appendChild(a);
5892         a.click();
5893         document.body.removeChild(a);
5894
5895         URL.revokeObjectURL(url);
5896       }
5897
5898       function DBG_elementValue(id,defaultValue) {
5899         var el = DBG_el(id);
5900         return el ? el.value : defaultValue;
5901       }
5902
5903       function DBG_setElementValue(id,value) {
5904         var el = DBG_el(id);
5905         if(el && value != null) el.value = String(value);
5906       }
5907
5908       function DBG_setElementChecked(id,value) {
5909         var el = DBG_el(id);
5910         if(el && value != null) el.checked = !!value;
5911       }
5912
5913       function DBG_collectDebugSettings() {
5914         return {
5915           followPC: DBG_settingChecked("DBG_followPcInput",true),
5916           autoScrollTrace: DBG_settingChecked("DBG_autoScrollTraceInput",true),
5917           listingColumns: DBG_listingColumnsValue(),
5918           traceDepth: DBG_traceDepthLimit(),
5919           traceColumns: DBG_traceColumnsValue(),
5920           showCycles: DBG_showCyclesEnabled(),
5921           showPcHistogram: DBG_settingChecked("DBG_showPcHistogramInput",false),
5922           runSpeed: DBG_elementValue("DBG_runSpeedInput","1"),
5923           stopAfter: DBG_elementValue("DBG_stopAfterInput",""),
5924           defaultWatchSize: DBG_elementValue("DBG_defaultWatchSizeInput","byte"),
5925           defaultWatchFormat: DBG_elementValue("DBG_defaultWatchFormatInput","hex")
5926         };
5927       }
5928
5929       function DBG_applyDebugSettings(settings) {
5930         settings = settings || {};
5931
5932         DBG_setElementChecked("DBG_followPcInput",settings.followPC);
5933         DBG_setElementChecked("DBG_autoScrollTraceInput",settings.autoScrollTrace);
5934         DBG_setElementValue("DBG_listingColumnsInput",settings.listingColumns);
5935         DBG_setElementValue("DBG_traceDepthInput",settings.traceDepth);
5936         DBG_setElementValue("DBG_traceColumnsInput",settings.traceColumns);
5937         DBG_setElementChecked("DBG_showCyclesInput",settings.showCycles);
5938         DBG_setElementChecked("DBG_showPcHistogramInput",settings.showPcHistogram);
5939         DBG_setElementValue("DBG_runSpeedInput",settings.runSpeed);
5940         DBG_setElementValue("DBG_stopAfterInput",settings.stopAfter);
5941         DBG_setElementValue("DBG_defaultWatchSizeInput",settings.defaultWatchSize);
5942         DBG_setElementValue("DBG_defaultWatchFormatInput",settings.defaultWatchFormat);
5943
5944         DBG_stopAfterInstructionLimit();
5945         DBG_applyTraceDepthLimit();
5946         DBG_rebuildTraceLines();
5947         DBG_renderTraceView();
5948         DBG_updateRegisterDisplays();
5949       }
5950
5951       function DBG_symbolTableSessionObject(table) {
5952         table = table || {};
5953         return {
5954           source: table.source || "",
5955           filename: table.filename || "",
5956           effectiveScope: table.effectiveScope || table.scope || [],
5957           raw: table.raw || {
5958             format: "RetroAppleJS-ASM-symbols",
5959             version: table.version || 2,
5960             source: table.source || table.filename || "symbol table",
5961             scopeMode: table.scopeMode || "instruction-pc",
5962             scope: table.scope || [],
5963             symbols: table.symbols || []
5964           }
5965         };
5966       }
5967
5968       function DBG_collectDebugSession() {
5969         var board = DBG_el("DBG_boardText");
5970         if(board) DBG_currentByteCodeText = board.value || "";
5971
5972         return {
5973           format: "RetroAppleJS-DBG-session",
5974           version: 1,
5975           filename: DBG_getDebugSessionFileName(),
5976           startPC: DBG_debuggerStartPC & 0xffff,
5977           startPCHex: "$" + DBG_hexWord(DBG_debuggerStartPC),
5978           byteCode: DBG_currentByteCodeText || "",
5979           symbolTables: DBG_symbolTables.map(DBG_symbolTableSessionObject),
5980           settings: DBG_collectDebugSettings()
5981         };
5982       }
5983
5984       function DBG_exportDebugSession() {
5985         var session = DBG_collectDebugSession();
5986         DBG_downloadText(
5987           session.filename,
5988           JSON.stringify(session,null,2),
5989           "application/json;charset=utf-8"
5990         );
5991         DBG_statusText("Exported debugger session as " + session.filename + ".");
5992       }
5993
5994       function DBG_restoreDebugSymbolTables(list) {
5995         DBG_symbolTables = [];
5996         DBG_pendingSymbolImport = null;
5997
5998         list = Array.isArray(list) ? list : [];
5999         for(var i=0;i<list.length;i++) {
6000           var entry = list[i] || {};
6001           var raw = entry.raw || entry;
6002           try {
6003             var table = DBG_normalizeSymbolTable(raw,entry.filename || raw.source || "");
6004             var effective = DBG_rangeListNormalize(entry.effectiveScope || []);
6005             table.effectiveScope = effective.length ? effective : table.scope.slice();
6006             DBG_symbolTables.push(table);
6007           } catch(err) {
6008             if(typeof console !== "undefined" && console.warn)
6009               console.warn("Skipped symbol table from .DEB session:",err);
6010           }
6011         }
6012
6013         DBG_rebuildSymbolIndex();
6014         DBG_renderSymbolLabels();
6015         DBG_disassembleRAM();
6016       }
6017
6018       function DBG_applyDebugSession(session,fileName) {
6019         if(!session || typeof session !== "object") throw new Error("Invalid debugger session file.");
6020         if(session.format && session.format !== "RetroAppleJS-DBG-session")
6021           throw new Error("Unsupported debugger session format: " + session.format);
6022
6023         if(DBG_running) DBG_stopContinuousRun("Continuous run stopped before loading debugger session.");
6024
6025         DBG_applyDebugSettings(session.settings || {});
6026
6027         DBG_currentByteCodeText = String(session.byteCode || "");
6028         var board = DBG_el("DBG_boardText");
6029         if(board) board.value = DBG_currentByteCodeText;
6030
6031         DBG_importBoardByteCode();
6032         DBG_restoreDebugSymbolTables(session.symbolTables || session.symbols || []);
6033
6034         var startPC = Number(session.startPC);
6035         if(isNaN(startPC)) startPC = 0x0000;
6036         DBG_prepareDebuggerStartPC(startPC,{clearTrace:true});
6037         DBG_scrollListingToPC(startPC,{behavior:"auto"});
6038
6039         DBG_setDebugSessionFileName(fileName || session.filename || DBG_debugSessionFileName);
6040         DBG_statusText("Loaded debugger session " + DBG_debugSessionFileName + ".");
6041         return true;
6042       }
6043
6044       async function DBG_onDebugSessionFileChange(evt) {
6045         var input = evt && evt.target ? evt.target : DBG_el("DBG_debugSessionFileInput");
6046         var file = input && input.files && input.files[0] ? input.files[0] : null;
6047         if(!file) return;
6048
6049         try {
6050           var session = JSON.parse(await file.text());
6051           DBG_applyDebugSession(session,file.name);
6052         } catch(err) {
6053           DBG_statusText("Debugger session import failed: " + (err && err.message ? err.message : 
6053String(err)));
6054           if(typeof console !== "undefined" && console.error) console.error(err);
6055         } finally {
6056           if(input) input.value = "";
6057         }
6058       }
6059
6060      function DBG_showCyclesEnabled() {
6061        return DBG_settingChecked("DBG_showCyclesInput",true);
6062      }
6063
6064      function DBG_scrollListingEnabled() {
6065        return DBG_settingChecked("DBG_followPcInput",true);
6066      }
6067
6068      function DBG_scrollCpuTraceEnabled() {
6069        return DBG_settingChecked("DBG_autoScrollTraceInput",true);
6070      }
6071
6072      function DBG_traceDepthLimit() {
6073        var input = DBG_el("DBG_traceDepthInput");
6074        var n = input ? parseInt(input.value,10) : 2048;
6075        if(isNaN(n) || n < 1) n = 2048;
6076        if(input && String(input.value) !== String(n)) input.value = String(n);
6077        return n;
6078      }
6079
6080       function DBG_resetTraceHistory(records) 
6081       {
6082         DBG_traceHistory = (DBG_traceHistory || oCOM.buffer(DBG_traceDepthLimit()))
6083           .resize(DBG_traceDepthLimit())
6084           .fromArray(records || []);    
6085         return DBG_traceHistory;
6086       }
6087
6088
6089      function DBG_applyTraceDepthLimit() 
6090      {
6091          if(DBG_traceHistory) DBG_traceHistory.resize(DBG_traceDepthLimit());
6092          else DBG_resetTraceHistory();
6093         DBG_rebuildTraceLines();
6094      }
6095
6096      function DBG_cloneTraceState(state) {
6097        if(!state) return null;
6098        return {
6099          pc: state.pc & 0xffff,
6100          a:  state.a  & 0xff,
6101          x:  state.x  & 0xff,
6102          y:  state.y  & 0xff,
6103          p:  state.p  & 0xff,
6104          sp: state.sp & 0xff,
6105          cycle_delay: state.cycle_delay | 0
6106        };
6107      }
6108
6109      function DBG_traceStateLoadString(state) {
6110        state = state || {};
6111        return [
6112          DBG_hexByte(state.a || 0),
6113          DBG_hexByte(state.x || 0),
6114          DBG_hexByte(state.y || 0),
6115          DBG_hexByte(state.sp == null ? 0xff : state.sp),
6116          DBG_hexByte(state.p == null ? 0x24 : state.p),
6117          DBG_hexWord(state.pc || 0)
6118        ].join(",");
6119      }
6120
6121      function DBG_drawWatchField(grid,x,y,label,value,changed,width)
6122      {
6123        var text = String(label == null ? "" : label) + "·" + String(value == null ? "" : value);
6124        // 〉⭢ ⏵ ─ ╌ ⋯ ┈ · ┄
6125        var inner = Math.max(1,Number(width) || text.length || 1);
6126
6127        var outer = inner + 2;
6128        /*
6129         * Stable 3-line block:
6130         *   y     = optional top box line
6131         *   y + 1 = label/value
6132         *   y + 2 = optional bottom box line
6133         *
6134         * Unchanged values still reserve the same grid area, but no box is drawn.
6135         */
6136        if(changed) grid.box(x,y,x+outer-1,y+2,{kind:grid.BOX_SINGLE});
6137        grid.cell(x + 1,y + 1,text);
6138        return outer;
6139      }
6140
6141      function DBG_renderWatchView(state,compareState) 
6142      {
6143        var watch = DBG_el("DBG_watchView");
6144        if(!watch) return;
6145
6146        state = state || {};
6147        compareState = compareState || null;
6148
6149        var highlight = !!compareState;
6150        var cpc = compareState ? (compareState.pc & 0xffff) : (state.pc & 0xffff);
6151        var ca  = compareState ? (compareState.a  & 0xff)   : (state.a  & 0xff);
6152        var cx  = compareState ? (compareState.x  & 0xff)   : (state.x  & 0xff);
6153        var cy  = compareState ? (compareState.y  & 0xff)   : (state.y  & 0xff);
6154        var cp  = compareState ? (compareState.p  & 0xff)   : (state.p  & 0xff);
6155        var csp = compareState ? (compareState.sp & 0xff)   : (state.sp & 0xff);
6156
6157        var grid = oCOM.textGrid(80,6);
6158        var x = 0;
6159
6160        x += DBG_drawWatchField(grid,x,0,"PC",DBG_hexWord(state.pc),highlight && ((state.pc & 0xffff) !== cpc),7);
6161        x += DBG_drawWatchField(grid,x,0,"a", DBG_hexByte(state.a), highlight && ((state.a  & 0xff) !== ca),4);
6162        x += DBG_drawWatchField(grid,x,0,"x", DBG_hexByte(state.x), highlight && ((state.x  & 0xff) !== cx),4);
6163        x += DBG_drawWatchField(grid,x,0,"y", DBG_hexByte(state.y), highlight && ((state.y  & 0xff) !== cy),4);
6164        x += DBG_drawWatchField(grid,x,0,"SP",DBG_hexByte(state.sp),highlight && ((state.sp & 0xff) !== csp),5);
6165
6166        var names = ["N","V","B","D","I","Z","C"];
6167        var bits  = [ 7,  6,  4,  3,  2,  1,  0];
6168        x = 0;
6169
6170        for(var i=0;i<bits.length;i++)
6171        {
6172          var bit = (state.p >> bits[i]) & 1;
6173          var cbit = (cp >> bits[i]) & 1;
6174          x += DBG_drawWatchField(grid,x,2,names[i],String(bit),highlight && bit !== cbit,3);
6175        }
6176
6177        watch.value = grid.toString();
6178      }
6179
6180      function DBG_traceLineFromRecord(record) {
6181        var before = record.before || {};
6182        var decoded = record.decoded || {};
6183        var parts = {
6184          stp: ("0000" + record.step).slice(-4),
6185          pc:  DBG_hexWord(record.pc),
6186          byt: decoded.bytes || "",
6187          opc: decoded.op || "",
6188          rA:  DBG_hexByte(before.a || 0),
6189          rX:  DBG_hexByte(before.x || 0),
6190          rY:  DBG_hexByte(before.y || 0),
6191          rSP: DBG_hexByte(before.sp == null ? 0xff : before.sp),
6192          rP:  DBG_hexByte(before.p || 0)
6193        };
6194
6195        if(record.selfLoopTrap)
6196          parts.opc = "[LOOP] " + parts.opc;
6197
6198        if(DBG_showCyclesEnabled()) parts.cyc = String(record.cycles || 0);
6199        return DBG_formatTraceParts(parts);
6200      }
6201
6202      function DBG_rebuildTraceLines() {
6203        DBG_traceLines = [];
6204         if(DBG_traceHistory)
6205           DBG_traceHistory.forEach(function(record){ DBG_traceLines.push(DBG_traceLineFromRecord(record)); });
6206
6207      }
6208
6209      function DBG_restoreTraceBefore(record) {
6210        if(!record || !DBG_traceCPU) return false;
6211
6212        var writes = record.writes || [];
6213        for(var i=writes.length-1;i>=0;i--)
6214          DBG_RAM[writes[i].addr & 0xffff] = writes[i].oldVal & 0xff;
6215
6216        DBG_traceCPU.load(DBG_traceStateLoadString(record.before));
6217        return true;
6218      }
6219 
6220     function DBG_updateRegisterDisplays(state,compareState) {
6221       state = state || (DBG_traceCPU && DBG_traceCPU.watch ? DBG_traceCPU.watch() : {
6222         pc:DBG_debuggerStartPC || 0x0000,
6223         a:0,x:0,y:0,p:0x24,sp:0xff,
6224         cycle_delay:0
6225       });
6226 
6227       var pc = state.pc & 0xffff;
6228       var a  = state.a  & 0xff;
6229       var x  = state.x  & 0xff;
6230       var y  = state.y  & 0xff;
6231       var p  = state.p  & 0xff;
6232       var sp = state.sp & 0xff;
6233 
6234       DBG_renderWatchView({pc:pc,a:a,x:x,y:y,p:p,sp:sp},compareState);
6235 
6236       var info = DBG_el("DBG_traceRegisterInfo");
6237       if (info) {
6238         info.innerHTML = oCOM.formatCpuRegistersHTML(
6239           {pc:pc,a:a,x:x,y:y,sp:sp,p:p},
6240           {includePC:false}
6241         );
6242       }
6243     }
6244 
6245     function DBG_setTraceActive(active) {
6246        /*
6247         * Trace mode does not hide Listing header action buttons.
6248         * DBG_requestActive is the only header action/requester switch.
6249         */
6250     }
6251 
6252     function DBG_renderTraceView() {
6253       var trace = DBG_el("DBG_traceView");
6254       if (!trace) return;
6255
6256       trace.value = DBG_traceLines.join("\n") + (DBG_traceLines.length ? "\n" : "");
6257      if (DBG_scrollCpuTraceEnabled())
6258        trace.scrollTop = trace.scrollHeight;
6259     }
6260 
6261     function DBG_startCpuTraceAtPC(startPC) {
6262       if (typeof Cpu6502 !== "function") {
6263         DBG_statusText("Cpu6502() was not found. Check res/EMU_cpu6502.js.");
6264         return false;
6265       }
6266 
6267       startPC = startPC & 0xffff;
6268       DBG_traceHW = DBG_createCpuTraceHw();
6269       DBG_traceCPU = new Cpu6502(DBG_traceHW);
6270       DBG_traceCPU.load("00,00,00,ff,24," + DBG_hexWord(startPC));
6271       DBG_armSelfLoopTrap();
6272       DBG_traceStarted = true;
6273       DBG_traceStepCount = 0;
6274       DBG_resetTraceHistory();
6275       DBG_traceLines = [];
6276       DBG_traceCurrentWrites = null;
6277
6278       DBG_renderTraceView();
6279       DBG_updateRegisterDisplays(DBG_traceCPU.watch());
6280       DBG_updateListingPC(startPC);
6281       return true;
6282     }
6283 
6284
6285     /*
6286      * TEST BENCH CPU bridge.
6287      *
6288      * Uses the same isolated CPU and DBG_RAM as the normal debugger without
6289      * rendering one CPU TRACE row per instruction.  Bulk algorithm tests can
6290      * therefore run headlessly and repaint the debugger only when finished.
6291      */
6292     function DBG_testCpuState()
6293     {
6294       if(!DBG_traceCPU || typeof DBG_traceCPU.watch !== "function")
6295         return null;
6296
6297       var s = DBG_traceCPU.watch();
6298       return {
6299         pc:          s.pc & 0xffff,
6300         a:           s.a  & 0xff,
6301         x:           s.x  & 0xff,
6302         y:           s.y  & 0xff,
6303         sp:          s.sp & 0xff,
6304         p:           s.p  & 0xff,
6305         cycle_delay: s.cycle_delay | 0,
6306         ic:          Number(s.ic) || 0
6307       };
6308     }
6309
6310     function DBG_testNowMs()
6311     {
6312       return window.performance && typeof window.performance.now === "function"
6313         ? window.performance.now()
6314         : Date.now();
6315     }
6316
6317     function DBG_testCpuStateText(state)
6318     {
6319       if(!state) return "PC=?";
6320       return "PC=$" + DBG_hexWord(state.pc)
6321         + " A=$" + DBG_hexByte(state.a)
6322         + " X=$" + DBG_hexByte(state.x)
6323         + " Y=$" + DBG_hexByte(state.y)
6324         + " SP=$" + DBG_hexByte(state.sp)
6325         + " P=$" + DBG_hexByte(state.p);
6326     }
6327
6328     function DBG_testCpuReadWord(address)
6329     {
6330       if(address == null) return null;
6331       address = Number(address) & 0xffff;
6332       return (DBG_RAM[address] | (DBG_RAM[(address + 1) & 0xffff] << 8)) & 0xffff;
6333     }
6334
6335     /*
6336      * Capture a compact memory window around a pointer.  Seven bytes before
6337      * the current address plus the current byte and eight following bytes
6338      * gives a 16-byte diagnostic line without turning failures into a dump.
6339      */
6340     function DBG_testCpuHexWindow(address)
6341     {
6342       if(address == null) return null;
6343
6344       address = Number(address) & 0xffff;
6345       var before = 7;
6346       var length = 16;
6347       var start = (address - before) & 0xffff;
6348       var bytes = [];
6349
6350       for(var i=0;i<length;i++)
6351         bytes.push(DBG_RAM[(start + i) & 0xffff] & 0xff);
6352
6353       return {
6354         address:address,
6355         start:start,
6356         currentIndex:before,
6357         bytes:bytes
6358       };
6359     }
6360
6361     function DBG_testCpuHexWindowText(window)
6362     {
6363       if(!window || !Array.isArray(window.bytes)) return "";
6364
6365       var bytes = window.bytes.map(function(value,index){
6366         var text = DBG_hexByte(value);
6367         return index === window.currentIndex ? "[" + text + "]" : text;
6368       });
6369
6370       return "$" + DBG_hexWord(window.start) + ": " + bytes.join(" ");
6371     }
6372
6373
6374     function DBG_testCpuMemoryContext(inputPointerAddress,outputPointerAddress)
6375     {
6376       var inputPointer = DBG_testCpuReadWord(inputPointerAddress);
6377       var outputPointer = DBG_testCpuReadWord(outputPointerAddress);
6378
6379       return {
6380         inputPointerAddress:inputPointerAddress == null ? null : (Number(inputPointerAddress) & 0xffff),
6381         outputPointerAddress:outputPointerAddress == null ? null : (Number(outputPointerAddress) & 0xffff),
6382         inputPointer:inputPointer,
6383         outputPointer:outputPointer,
6384         inputWindow:DBG_testCpuHexWindow(inputPointer),
6385         outputWindow:DBG_testCpuHexWindow(outputPointer)
6386       };
6387     }
6388
6389     function DBG_testCpuContextText(result)
6390     {
6391       result = result || {};
6392       var parts = [];
6393
6394       if(result.state)
6395         parts.push(DBG_testCpuStateText(result.state));
6396
6397       var memory = result.memory || {};
6398       if(memory.inputPointer != null || memory.outputPointer != null)
6399       {
6400         var ptr = "PTR";
6401         if(memory.inputPointer != null)
6402           ptr += " in"
6403             + (memory.inputPointerAddress != null ? "[$" + DBG_hexWord(memory.inputPointerAddress) + "]" : "")
6404             + "=$" + DBG_hexWord(memory.inputPointer);
6405         if(memory.outputPointer != null)
6406           ptr += " out"
6407             + (memory.outputPointerAddress != null ? "[$" + DBG_hexWord(memory.outputPointerAddress) + "]" : "")
6408             + "=$" + DBG_hexWord(memory.outputPointer);
6409         parts.push(ptr);
6410       }
6411
6412       var pcs = Array.isArray(result.recentPCs) ? result.recentPCs.slice(-6) : [];
6413       if(pcs.length)
6414         parts.push("PCs=" + pcs.map(function(pc){ return "$" + DBG_hexWord(pc); }).join("→"));
6415
6416       var dump = [];
6417       if(memory.inputWindow)
6418         dump.push("IN  " + DBG_testCpuHexWindowText(memory.inputWindow));
6419       if(memory.outputWindow)
6420         dump.push("OUT " + DBG_testCpuHexWindowText(memory.outputWindow));
6421
6422       var text = parts.length ? " / " + parts.join(" / ") : "";
6423       if(dump.length) text += "\n    " + dump.join("\n    ");
6424       return text;
6425     }
6426
6427     function DBG_testCpuDiagnostic(result)
6428     {
6429       result = result || {};
6430       var elapsed = Number(result.elapsedMs) || 0;
6431       var suffix = result.reason === "stop-pc" ? "" : DBG_testCpuContextText(result);
6432
6433       switch(result.reason)
6434       {
6435         case "stop-pc":
6436           return "returned to $" + DBG_hexWord(result.stopPC)
6437             + " / " + result.instructions + " ins"
6438             + " / " + result.cycles + " cyc"
6439             + " / " + elapsed.toFixed(1) + " ms";
6440
6441         case "timeout":
6442           return "timeout after " + elapsed.toFixed(1) + " ms"
6443             + " / " + result.instructions + " ins" + suffix;
6444
6445         case "instruction-limit":
6446           return "instruction limit " + result.maxInstructions
6447             + " reached / " + result.instructions + " ins" + suffix;
6448
6449         case "self-loop":
6450           return "self-loop at $" + DBG_hexWord(
6451               result.loopTrap && result.loopTrap.pc != null
6452                 ? result.loopTrap.pc
6453                 : (result.state ? result.state.pc : 0)
6454             )
6455             + (result.loopTrap && result.loopTrap.repeats
6456                 ? " x" + result.loopTrap.repeats
6457                 : "")
6458             + " / " + result.instructions + " ins" + suffix;
6459
6460         case "pc-loop":
6461           var pcs = result.pcLoop && Array.isArray(result.pcLoop.pcs)
6462             ? result.pcLoop.pcs.map(function(pc){ return "$" + DBG_hexWord(pc); }).join("→")
6463             : "";
6464           return "PC loop period " + (result.pcLoop ? result.pcLoop.period : "?")
6465             + " x" + (result.pcLoop ? result.pcLoop.repeats : "?")
6466             + (pcs ? " [" + pcs + "]" : "")
6467             + " / " + result.instructions + " ins" + suffix;
6468
6469         case "cpu-error":
6470           return "CPU error" + (result.error ? ": " + result.error : "") + suffix;
6471
6472         case "instruction-did-not-complete":
6473           return "instruction did not complete" + suffix;
6474
6475         case "cpu-state-unavailable":
6476           return "CPU state unavailable";
6477
6478         case "not-started":
6479           return "CPU not started";
6480       }
6481
6482       return String(result.reason || "unknown failure") + suffix;
6483     }
6484
6485     function DBG_testCpuStart(startPC)
6486     {
6487       startPC = Number(startPC) & 0xffff;
6488
6489       if(DBG_running)
6490         DBG_stopContinuousRun("Continuous run stopped for TEST BENCH.");
6491
6492       if(!DBG_startCpuTraceAtPC(startPC))
6493         return null;
6494
6495       return DBG_testCpuState();
6496     }
6497
6498     /* Execute one complete 6502 instruction, without rendering a trace row. */
6499     function DBG_testCpuStepInstruction(options)
6500     {
6501       options = options || {};
6502
6503       if(!DBG_traceCPU)
6504         return {ok:false,reason:"not-started"};
6505
6506       var before = DBG_testCpuState();
6507       var after = before;
6508       var cycles = 0;
6509       var captureWrites = !!options.captureWrites;
6510
6511       DBG_traceCurrentWrites = captureWrites ? [] : null;
6512
6513       try
6514       {
6515         do
6516         {
6517           DBG_traceCPU.cycle();
6518           cycles++;
6519           after = DBG_testCpuState();
6520         }
6521         while(after && after.cycle_delay > 0 && cycles < 64);
6522       }
6523       catch(err)
6524       {
6525         var failedWrites = DBG_traceCurrentWrites
6526           ? DBG_traceCurrentWrites.slice()
6527           : [];
6528
6529         DBG_traceCurrentWrites = null;
6530
6531         return {
6532           ok:false,
6533           reason:"cpu-error",
6534           error:String(err),
6535           before:before,
6536           after:DBG_testCpuState(),
6537           cycles:cycles,
6538           writes:failedWrites
6539         };
6540       }
6541
6542       var writes = DBG_traceCurrentWrites
6543         ? DBG_traceCurrentWrites.slice()
6544         : [];
6545
6546       DBG_traceCurrentWrites = null;
6547
6548       if(!after || after.cycle_delay > 0)
6549       {
6550         return {
6551           ok:false,
6552           reason:"instruction-did-not-complete",
6553           before:before,
6554           after:after,
6555           cycles:cycles,
6556           writes:writes
6557         };
6558       }
6559
6560       return {
6561         ok:true,
6562         reason:"step",
6563         before:before,
6564         after:after,
6565         cycles:cycles,
6566         writes:writes
6567       };
6568     }
6569
6570     /*
6571      * Detect a short repeating PC cycle such as A→B→A→B. Same-PC loops are
6572      * already handled by Cpu6502's self-loop trap. The deliberately high
6573      * repeat limit avoids mistaking normal decode/copy loops for hangs.
6574      */
6575     function DBG_testPcLoopDetector(maxPeriod,repeatLimit)
6576     {
6577       maxPeriod = Math.max(2,Math.min(32,Number(maxPeriod) | 0 || 8));
6578       repeatLimit = Math.max(2,Number(repeatLimit) | 0 || 1024);
6579
6580       var recent = [];
6581       var streak = new Array(maxPeriod + 1).fill(0);
6582
6583       return function(pc)
6584       {
6585         recent.push(pc & 0xffff);
6586         if(recent.length > maxPeriod * 2) recent.shift();
6587
6588         for(var period=2;period<=maxPeriod;period++)
6589         {
6590           if(recent.length < period * 2) continue;
6591
6592           var n = recent.length;
6593           var same = true;
6594           for(var i=0;i<period;i++)
6595           {
6596             if(recent[n-1-i] !== recent[n-1-period-i])
6597             {
6598               same = false;
6599               break;
6600             }
6601           }
6602
6603           streak[period] = same ? streak[period] + 1 : 0;
6604
6605           if(streak[period] >= period * (repeatLimit - 1))
6606           {
6607             return {
6608               period:period,
6609               repeats:Math.floor(streak[period] / period) + 1,
6610               pcs:recent.slice(n-period)
6611             };
6612           }
6613         }
6614
6615         return null;
6616       };
6617     }
6618
6619     /*
6620      * Run until stopPC is reached at a clean instruction boundary.
6621      * The instruction at stopPC is NOT executed.
6622      */
6623     function DBG_testCpuRunUntil(stopPC,maxInstructions,options)
6624     {
6625       options = options || {};
6626       stopPC = Number(stopPC) & 0xffff;
6627
6628       maxInstructions = Number(maxInstructions);
6629       if(!isFinite(maxInstructions) || maxInstructions < 1)
6630         maxInstructions = 1000000;
6631       maxInstructions = Math.floor(maxInstructions);
6632
6633       var timeoutMs = options.timeoutMs == null ? 5000 : Number(options.timeoutMs);
6634       if(!isFinite(timeoutMs) || timeoutMs < 0) timeoutMs = 5000;
6635
6636       var inputPointerAddress = options.inputPointerAddress == null
6637         ? null
6638         : (Number(options.inputPointerAddress) & 0xffff);
6639       var outputPointerAddress = options.outputPointerAddress == null
6640         ? null
6641         : (Number(options.outputPointerAddress) & 0xffff);
6642
6643       var detectPcLoops = options.detectPcLoops !== false;
6644       var pcLoopMaxPeriod = options.pcLoopMaxPeriod == null ? 8 : options.pcLoopMaxPeriod;
6645       var pcLoopRepeatLimit = options.pcLoopRepeatLimit == null ? 1024 : options.pcLoopRepeatLimit;
6646       var detectPcLoop = detectPcLoops
6647         ? DBG_testPcLoopDetector(pcLoopMaxPeriod,pcLoopRepeatLimit)
6648         : null;
6649
6650       var captureWrites = !!options.captureWrites;
6651       var allWrites = [];
6652       var instructions = 0;
6653       var cycles = 0;
6654       var reason = "not-started";
6655       var error = null;
6656       var loopTrap = null;
6657       var pcLoop = null;
6658       var recentPCs = [];
6659       var startedAt = DBG_testNowMs();
6660
6661       if(!DBG_traceCPU)
6662       {
6663          var notStarted = {
6664           ok:false,
6665           reason:reason,
6666           stopPC:stopPC,
6667           maxInstructions:maxInstructions,
6668           timeoutMs:timeoutMs,
6669           instructions:0,
6670           cycles:0,
6671           elapsedMs:0,
6672           state:null,
6673           loopTrap:null,
6674           pcLoop:null,
6675           recentPCs:[],
6676           memory:DBG_testCpuMemoryContext(inputPointerAddress,outputPointerAddress),
6677           writes:[]
6678         };
6679         notStarted.diagnostic = DBG_testCpuDiagnostic(notStarted);
6680         return notStarted;
6681       }
6682
6683       while(true)
6684       {
6685         var state = DBG_testCpuState();
6686
6687         if(!state)
6688         {
6689           reason = "cpu-state-unavailable";
6690           break;
6691         }
6692
6693         if(state.cycle_delay === 0 && state.pc === stopPC)
6694         {
6695           reason = "stop-pc";
6696           break;
6697         }
6698
6699         if(instructions >= maxInstructions)
6700         {
6701           reason = "instruction-limit";
6702           break;
6703         }
6704
6705         /* Amortize wall-clock checks so long suites do not pay for Date.now()
6706          * on every single instruction.  timeoutMs=0 explicitly disables it. */
6707         if(timeoutMs > 0 && (instructions & 0xff) === 0 &&
6708            DBG_testNowMs() - startedAt >= timeoutMs)
6709         {
6710           reason = "timeout";
6711           break;
6712         }
6713
6714         var step = DBG_testCpuStepInstruction({captureWrites:captureWrites});
6715
6716         /* Keep the actual instruction addresses, not merely the following PCs. */
6717         if(step.before)
6718         {
6719           recentPCs.push(step.before.pc & 0xffff);
6720           if(recentPCs.length > 8) recentPCs.shift();
6721         }
6722
6723         if(!step.ok)
6724         {
6725           reason = step.reason;
6726           error = step.error || null;
6727           if(captureWrites && step.writes)
6728             Array.prototype.push.apply(allWrites,step.writes);
6729           break;
6730         }
6731
6732         instructions++;
6733         cycles += step.cycles;
6734
6735         if(captureWrites && step.writes.length)
6736           Array.prototype.push.apply(allWrites,step.writes);
6737
6738         loopTrap = DBG_getSelfLoopTrap();
6739         if(loopTrap)
6740         {
6741           reason = "self-loop";
6742           break;
6743         }
6744
6745
6746         if(detectPcLoop && step.after && step.after.pc !== stopPC)
6747         {
6748           pcLoop = detectPcLoop(step.after.pc);
6749           if(pcLoop)
6750           {
6751             reason = "pc-loop";
6752             break;
6753           }
6754         }
6755
6756       }
6757
6758       var finalState = DBG_testCpuState();
6759       var elapsedMs = Math.max(0,DBG_testNowMs() - startedAt);
6760       var ok = reason === "stop-pc";
6761
6762       /* Headless while running; repaint once at the end. */
6763       if(finalState)
6764       {
6765         DBG_updateRegisterDisplays(finalState);
6766         DBG_updateListingPC(finalState.pc & 0xffff);
6767       }
6768
6769       var result = {
6770         ok:ok,
6771         reason:reason,
6772         stopPC:stopPC,
6773         maxInstructions:maxInstructions,
6774         timeoutMs:timeoutMs,
6775         instructions:instructions,
6776         cycles:cycles,
6777         elapsedMs:elapsedMs,
6778         state:finalState,
6779         error:error,
6780         loopTrap:loopTrap,
6781         pcLoop:pcLoop,
6782         recentPCs:recentPCs.slice(),
6783         memory:DBG_testCpuMemoryContext(inputPointerAddress,outputPointerAddress),
6784         writes:captureWrites ? allWrites : []
6785       };
6786       result.diagnostic = DBG_testCpuDiagnostic(result);
6787       /*
6788        * If CPU execution returned normally but an algorithm assertion fails,
6789        * TB.report() still needs the same register/pointer/memory context.
6790        */
6791       result.failureDiagnostic = result.ok
6792         ? result.diagnostic + DBG_testCpuContextText(result)
6793         : result.diagnostic;
6794       DBG_statusText("TEST BENCH: " + result.diagnostic);
6795       return result;
6796     }
6797
6798     /* Narrow public surface consumed by res/DBG_testbench.js. */
6799     window.DBG_TEST_CPU = {
6800       start:DBG_testCpuStart,
6801       state:DBG_testCpuState,
6802       step:DBG_testCpuStepInstruction,
6803       runUntil:DBG_testCpuRunUntil,
6804       diagnostic:DBG_testCpuDiagnostic
6805     };
6806
6807     function DBG_traceInstructionTextAt(pc) {
6808       var d = DBG_disassembleInstruction(pc);
6809       var row = d && d.row ? d.row : null;
6810       if (!row) return {bytes:"", op:""};
6811 
6812       return {
6813         bytes: (row.byteHex || []).join(" "),
6814         op: String(row.ins || "") + (row.opr ? " " + row.opr : "")
6815       };
6816     }
6817 
6818     function DBG_traceStepForward() {
6819       if (!DBG_traceStarted || !DBG_traceCPU) {
6820         DBG_startCpuTraceAtPC(DBG_debuggerStartPC || 0x0000);
6821         if (!DBG_traceCPU) return false;
6822       }
6823 
6824       var before = DBG_cloneTraceState(DBG_traceCPU.watch());
6825       var pc = before.pc & 0xffff;
6826       var decoded = DBG_traceInstructionTextAt(pc);
6827       var cycles = 0;
6828       var after;
6829       DBG_traceCurrentWrites = [];
6830 
6831       do {
6832         DBG_traceCPU.cycle();
6833         cycles++;
6834         after = DBG_cloneTraceState(DBG_traceCPU.watch());
6835       } while (after && after.cycle_delay > 0 && cycles < 64);
6836 
6837       var selfLoopTrap = DBG_getSelfLoopTrap();
6838
6839       var writes = DBG_traceCurrentWrites || [];
6840       DBG_traceCurrentWrites = null;
6841
6842       DBG_traceStepCount++;
6843
6844       var record = {
6845         step: DBG_traceStepCount,
6846         before: before,
6847         after: after,
6848         pc: pc,
6849         decoded: decoded,
6850         cycles: cycles,
6851         writes: writes.slice(),
6852         selfLoopTrap: selfLoopTrap
6853       };
6854
6855       if(!DBG_traceHistory) DBG_resetTraceHistory();
6856       if(DBG_traceHistory.full()) DBG_traceLines.shift();
6857       DBG_traceHistory.push(record);
6858       DBG_traceLines.push(DBG_traceLineFromRecord(record));
6859       while(DBG_traceLines.length > DBG_traceHistory.length)
6860         DBG_traceLines.shift();    
6861 
6862       DBG_renderTraceView();
6863       DBG_updateRegisterDisplays(after,before);
6864       if (after) DBG_updateListingPC(after.pc & 0xffff);
6865
6866       if(selfLoopTrap && !DBG_running)
6867         DBG_statusText(DBG_selfLoopTrapText(selfLoopTrap));
6868
6869       return true;
6870     }
6871 
6872       function DBG_clearTrace() {
6873         /*
6874          * Clear only the trace log/ring buffer and restart trace numbering.
6875          * Do not reset the CPU and do not stop an active continuous run.
6876          */
6877         DBG_traceStepCount = 0;
6878         DBG_resetTraceHistory();
6879         DBG_traceLines = [];
6880         DBG_armSelfLoopTrap();
6881         DBG_renderTraceView();
6882         DBG_statusText("Trace cleared; next trace entry will be step 0001.");
6883         return true;
6884       }     
6885
6886      function DBG_traceStepBackward() 
6887      {
6888         if(DBG_running)
6889           DBG_stopContinuousRun("Continuous run stopped before stepping backward.");
6890
6891        if(!DBG_traceHistory || !DBG_traceHistory.length) {
6892          DBG_statusText("Trace history is empty; cannot step backward.");
6893          return false;
6894        }
6895
6896        if(!DBG_traceCPU) {
6897          DBG_statusText("Trace CPU is not active; cannot step backward.");
6898          return false;
6899        }
6900
6901        var record = DBG_traceHistory.pop();
6902        if(!DBG_restoreTraceBefore(record)) return false;
6903
6904        DBG_traceStepCount = Math.max(0,DBG_traceStepCount - 1);
6905        DBG_rebuildTraceLines();
6906        DBG_renderTraceView();
6907        DBG_updateRegisterDisplays(record.before,record.after);
6908        DBG_updateListingPC(record.before.pc & 0xffff);
6909        DBG_statusText("Stepped backward to $" + DBG_hexWord(record.before.pc) + ".");
6910        return true;
6911      }      
6912
6913      function DBG_runSpeedValue() {
6914        var input = DBG_el("DBG_runSpeedInput");
6915        var value = input ? String(input.value || "").toLowerCase() : "1";
6916
6917        if(value === "max") return {mode:"max",ips:0,stepsPerTick:250,delay:0,label:"Max"};
6918
6919        var ips = parseInt(value,10);
6920        if(isNaN(ips) || ips < 1) ips = 1;
6921
6922        if(ips >= 1000) return {mode:"fixed",ips:1000,stepsPerTick:50,delay:50,label:"1000 IPS"};
6923        if(ips >= 100)  return {mode:"fixed",ips:100, stepsPerTick:5, delay:50,label:"100 IPS"};
6924        if(ips >= 10)   return {mode:"fixed",ips:10,  stepsPerTick:1, delay:100,label:"10 IPS"};
6925
6926        return {mode:"fixed",ips:1,stepsPerTick:1,delay:1000,label:"1 IPS"};
6927      }
6928
6929      function DBG_stopAfterInstructionLimit() {
6930        var input = DBG_el("DBG_stopAfterInput");
6931         if(!input) return 0;
6932
6933         var raw = String(input.value || "").trim();
6934         if(raw === "") return 0;          // empty means no run limit / infinity
6935
6936         var n = parseInt(raw,10);
6937         if(isNaN(n) || n < 1) {
6938           input.value = "";
6939           return 0;
6940         }
6941
6942         if(String(input.value) !== String(n)) input.value = String(n);
6943         return n;
6944      }
6945
6946      function DBG_updateRunButton() {
6947        var btn = DBG_el("DBG_runToggle");
6948        if(!btn) return;
6949
6950        btn.innerHTML = DBG_running ? '<i class="fa fa-pause"></i>' : '<i class="fa fa-running"></i>';
6951        btn.title = DBG_running ? "Stop continuous run" : "Continuous run";
6952        btn.classList.remove("DBG_changed");
6953      }
6954
6955      function DBG_stopContinuousRun(reason) {
6956        DBG_running = false;
6957
6958        if(DBG_runTimer !== null) {
6959          window.clearTimeout(DBG_runTimer);
6960          DBG_runTimer = null;
6961        }
6962
6963        DBG_updateRunButton();
6964        if(reason) DBG_statusText(reason);
6965      }
6966
6967      function DBG_scheduleRunLoop(delay) {
6968        if(DBG_runTimer !== null) window.clearTimeout(DBG_runTimer);
6969        DBG_runTimer = window.setTimeout(DBG_runLoop,Math.max(0,delay | 0));
6970      }
6971
6972      function DBG_runLoop() {
6973        if(!DBG_running) return;
6974
6975        var speed = DBG_runSpeedValue();
6976        var stopAfter = DBG_stopAfterInstructionLimit();
6977        var stepsThisTick = Math.max(1,speed.stepsPerTick | 0);
6978
6979        for(var i=0;i<stepsThisTick;i++) {
6980          if(stopAfter > 0 && DBG_runInstructionCount >= stopAfter) {
6981            DBG_stopContinuousRun(
6982              "Continuous run stopped after " + DBG_runInstructionCount + " instruction(s)."
6983            );
6984            return;
6985          }
6986
6987          if(!DBG_traceStepForward()) {
6988            DBG_stopContinuousRun("Continuous run stopped: step failed.");
6989            return;
6990          }
6991
6992          DBG_runInstructionCount++;
6993
6994          var selfLoopTrap = DBG_getSelfLoopTrap();
6995          if(selfLoopTrap) {
6996            DBG_stopContinuousRun(DBG_selfLoopTrapText(selfLoopTrap));
6997            return;
6998          }
6999
7000          if(stopAfter > 0 && DBG_runInstructionCount >= stopAfter) {
7001            DBG_stopContinuousRun(
7002              "Continuous run stopped after " + DBG_runInstructionCount + " instruction(s)."
7003            );
7004            return;
7005          }
7006        }
7007
7008        if(DBG_traceStepCount !== DBG_runLastStatusStep) {
7009          DBG_armSelfLoopTrap();
7010          DBG_runLastStatusStep = DBG_traceStepCount;
7011          DBG_statusText(
7012            "Continuous run: " + DBG_runInstructionCount + " instruction(s) / " + speed.label +
7013            (stopAfter > 0 ? " / stop at " + stopAfter : "")
7014          );
7015        }
7016
7017        DBG_scheduleRunLoop(speed.delay);
7018      }
7019
7020      function DBG_startContinuousRun() {
7021        if(DBG_running) return;
7022
7023        if(!DBG_traceStarted || !DBG_traceCPU) {
7024          DBG_startCpuTraceAtPC(DBG_debuggerStartPC || 0x0000);
7025          if(!DBG_traceCPU) return;
7026        }
7027
7028        DBG_runInstructionCount = 0;
7029        DBG_runLastStatusStep = DBG_traceStepCount;
7030        DBG_running = true;
7031        DBG_updateRunButton();
7032
7033        var speed = DBG_runSpeedValue();
7034        var stopAfter = DBG_stopAfterInstructionLimit();
7035        DBG_statusText(
7036          "Continuous run started at " + speed.label +
7037          (stopAfter > 0 ? " / stop after " + stopAfter + " instruction(s)." : ".")
7038        );
7039
7040        DBG_scheduleRunLoop(0);
7041      }
7042
7043      function DBG_toggleContinuousRun() {
7044        if(DBG_running) DBG_stopContinuousRun("Continuous run stopped.");
7045        else DBG_startContinuousRun();
7046      }
7047
7048
7049    function DBG_importBoardByteCode() {
7050      var parsed = DBG_syncByteCodeFromBoard();
7051      if (!parsed.records.length) {
7052        DBG_stub("No byte code to import");
7053        return false;
7054      }
7055
7056      for (var i = 0; i < parsed.records.length; i++) {
7057        DBG_RAM[parsed.records[i].pc & 0xffff] = parsed.records[i].val & 0xff;
7058      }
7059
7060      var lineCount = DBG_disassembleRAM();
7061      DBG_updateByteCodeInfo(parsed.records.length, lineCount + " listing lines");
7062      DBG_statusText("Imported " + parsed.records.length + " byte(s) and disassembled RAM.");
7063
7064      if (parsed.errors.length && typeof console !== "undefined" && console.warn) console.warn(parsed.errors.join("\n"));
7065      return true;
7066    }
7067
7068    function DBG_togglePanel(id) {
7069      var target = DBG_el(id);
7070      if (!target) return;
7071
7072      var other = id === "tab3.1" ? DBG_el("tab3.2") : DBG_el("tab3.1");
7073      if (other) other.hidden = true;
7074      target.hidden = !target.hidden;
7075      DBG_equalizePaneHeaders();
7076    }
7077
7078    function DBG_closePanels() {
7079      if (DBG_el("tab3.1")) DBG_el("tab3.1").hidden = true;
7080      if (DBG_el("tab3.2")) DBG_el("tab3.2").hidden = true;
7081    }
7082
7083    function DBG_registerPopupRules() {
7084      /*
7085       * Keep the debugger tab buttons consistent with tab1:
7086       * buttons call oCOM.POPUP.runRule(), rules toggle one overlay
7087       * and close the other one.
7088       *
7089       * Rule names follow the tab1 convention:
7090       *   tab3.1 = Settings
7091       *   tab3.2 = Tools
7092       *
7093       * The actual popup IDs remain tab3.1 / tab3.2,
7094       * because the v22 debugger code already uses those IDs internally.
7095       */
7096      if (typeof oCOM === "undefined" || !oCOM || !oCOM.POPUP || typeof oCOM.POPUP.addRule !== "function") return;
7097      oCOM.POPUP.addRule("tab3.1", function(o) { o.toggle("tab3.1"); o.off("tab3.2"); DBG_equalizePaneHeaders(); });
7098      oCOM.POPUP.addRule("tab3.2", function(o) { o.toggle("tab3.2"); o.off("tab3.1"); DBG_equalizePaneHeaders(); });
7099    }
7100
7101    function DBG_applyPaneHeaderButtonTheme() {
7102      /*
7103       * Keep pane-header buttons readable in dark mode without adding more
7104       * dark-mode CSS override chains.  These colors are UI state, so apply
7105       * and clear them when the theme changes.
7106       */
7107      var buttons = document.querySelectorAll("#DBG_root .DBG_paneTitle .DBG_headerActions .appbut");
7108
7109      for (var i = 0; i < buttons.length; i++) {
7110        var btn = buttons[i];
7111
7112        if (DBG_darkMode) {
7113          btn.style.backgroundColor = "#2b3139";
7114          btn.style.borderColor = "#59636f";
7115          btn.style.color = "#e6edf3";
7116        } else {
7117          btn.style.backgroundColor = "";
7118          btn.style.borderColor = "";
7119          btn.style.color = "";
7120        }
7121      }
7122    }
7123
7124    function DBG_setStyles(selector, styles)
7125    {
7126      var nodes = document.querySelectorAll(selector);
7127      for(var i = 0; i < nodes.length; i++)
7128        for(var k in styles)
7129          nodes[i].style[k] = styles[k];
7130    }
7131
7132    function DBG_clearStyles(selector, props)
7133    {
7134      var nodes = document.querySelectorAll(selector);
7135      for(var i = 0; i < nodes.length; i++)
7136        for(var p = 0; p < props.length; p++)
7137          nodes[i].style[props[p]] = "";
7138    }
7139
7140    function DBG_applyTheme()
7141    {
7142      /*
7143       * Dynamic-only debugger theme.
7144       */
7145      var tab = DBG_el("tab3");
7146      if(tab)
7147      {
7148        tab.classList.remove("DBG_dark");
7149        tab.style.backgroundColor = "";
7150        tab.style.color = "";
7151      }
7152
7153      var basicProps = ["backgroundColor","backgroundImage","background","borderColor","color","caretColor"];
7154
7155      if(DBG_darkMode)
7156      {
7157        DBG_setStyles("#DBG_root .DBG_paneTitle", {
7158          background:"#111820",
7159          borderColor:"#30363d",
7160          color:"#e6edf3"
7161        });
7162        DBG_setStyles("#DBG_root .DBG_paneTitleMain strong, #DBG_root .DBG_paneInfo", {
7163          color:"#e6edf3"
7164        });
7165        DBG_setStyles("#DBG_root .DBG_paneBody", {
7166          backgroundColor:"#000000",
7167          backgroundImage:"none",
7168          borderColor:"#30363d",
7169          color:"#e6edf3",
7170          caretColor:"#e6edf3"
7171        });
7172        DBG_setStyles("#DBG_root .DBG_textPane", {
7173          color:"#e6edf3"
7174        });
7175
7176        DBG_setStyles("#DBG_root .DBG_listingLineCurrent, #DBG_root .DBG_traceLineCurrent", {
7177          color:"#000000"
7178        });
7179        DBG_setStyles("#DBG_root .DBG_listingPcOverlay", {
7180          background:"rgba(255, 214, 64, 0.34)"
7181        });
7182        DBG_setStyles("#DBG_root .DBG_breakpoint", {
7183          color:"#ff7a7a"
7184        });
7185      }
7186      else
7187      {
7188        DBG_clearStyles("#DBG_root .DBG_paneTitle, #DBG_root .DBG_paneTitleMain strong, #DBG_root .DBG_paneInfo, #DBG_root .DBG_paneBody, #DBG_root .DBG_textPane, #DBG_root .DBG_listingLineCurrent, #DBG_root .DBG_traceLineCurrent, #DBG_root .DBG_listingPcOverlay, #DBG_root .DBG_breakpoint", basicProps);
7189      }
7190    }
7191
7192    function DBG_toggleRun(btn) {
7193      DBG_running = !DBG_running;
7194      if (!btn) btn = DBG_el("DBG_runToggle");
7195      if (!btn) return;
7196
7197      if (DBG_running) {
7198        btn.innerHTML = '<i class="fa fa-snowflake"></i>';
7199        btn.title = "Stop continuous run";
7200        btn.setAttribute("aria-label", "Stop continuous run");
7201      } else {
7202        btn.innerHTML = '<i class="fa fa-running"></i>';
7203        btn.title = "Continuous run";
7204        btn.setAttribute("aria-label", "Continuous run");
7205      }
7206    }
7207
7208    function DBG_togglePane3(forceVisible) {
7209      var host = DBG_el("DBG_paneHost");
7210      var btn = DBG_el("DBG_toggleWatchPaneButton");
7211      if (!host) return;
7212
7213      var hide = host.classList.contains("DBG_hideWatch");
7214      if (typeof forceVisible === "boolean") hide = forceVisible ? true : false;
7215
7216      if (hide) host.classList.remove("DBG_hideWatch");
7217      else host.classList.add("DBG_hideWatch");
7218
7219      if (btn) {
7220        var visible = !host.classList.contains("DBG_hideWatch");
7221        btn.innerHTML = visible ? '<i class="fa fa-eye-slash"></i>' : '<i class="fa fa-eye"></i>';
7222        btn.title = visible ? "Hide Memory Watch pane" : "Show Memory Watch pane";
7223      }
7224      DBG_equalizePaneHeaders();
7225    }
7226
7227    function DBG_toggleTheme() {
7228      DBG_darkMode = !DBG_darkMode;
7229      DBG_applyTheme();
7230      DBG_applyPaneHeaderButtonTheme();
7231      DBG_equalizePaneHeaders();
7232    }
7233
7234    function DBG_stub(action) {
7235      var status = DBG_el("DBG_status");
7236      if (status) status.textContent = "Layout prototype: " + action + " is not wired yet.";
7237    }
7238
7239    function DBG_equalizePaneHeaders() {
7240      if (DBG_oPANE && typeof DBG_oPANE.equalizePaneHeaderHeights === "function") {
7241        DBG_oPANE.equalizePaneHeaderHeights("#DBG_root .com-pane-title");
7242      }
7243    }
7244
7245    function DBG_bindTextPaneEvents() {
7246      var listing = DBG_el("debugfield");
7247      if(listing)
7248        listing.addEventListener("scroll",function(){
7249          if(DBG_listingCurrentPC != null) DBG_positionListingPcOverlay(DBG_listingRowIndexForPC(DBG_listingCurrentPC));
7250        });
7251    }
7252
7253    function DBG_initPaneContainer() {
7254      if (typeof PANE === "function") {
7255        DBG_oPANE = new PANE();
7256      }
7257
7258      var board = DBG_el("DBG_boardText");
7259      if (board) DBG_syncByteCodeFromBoard();
7260
7261      var symFile = DBG_el("DBG_sym_fileInput");
7262      if (symFile) symFile.addEventListener("change", DBG_onSymbolFileChange);
7263
7264      var debFile = DBG_el("DBG_debugSessionFileInput");
7265      if (debFile) debFile.addEventListener("change", DBG_onDebugSessionFileChange);
7266      DBG_bindTextPaneEvents();
7267
7268      DBG_renderSymbolLabels();
7269      DBG_updateDebugSessionFileNameUI();
7270      DBG_applyTheme();
7271      DBG_applyPaneHeaderButtonTheme();
7272      DBG_equalizePaneHeaders();
7273      DBG_updateRegisterDisplays();
7274
7275      DBG_loadManualPane();
7276    }
7277
7278    function DBG_loadManualPane() {
7279      /*
7280      * Keep debugger manual loading explicit, mirroring the tab2 manual pane
7281      * while using the debugger documentation block.
7282      */
7283      var manual = DBG_el("DBG_manual");
7284      if (!manual) return false;
7285
7286      var html = "";
7287      if (typeof _DOCS !== "undefined" && _DOCS && _DOCS["DEBUGGER"]) {
7288        html = String(_DOCS["DEBUGGER"])
7289          .replace(/https:\/\/github\.com\/RetroAppleJS\/RetroAppleJS\.github\.io\/raw\/main\//g, "");
7290      }
7291
7292      manual.innerHTML = html;
7293      return true;
7294    }
7295
7296    if (typeof oCOM !== "undefined" && oCOM && typeof oCOM.addToEventStack === "function") {
7297      oCOM.addToEventStack("onload", DBG_initPaneContainer);
7298    } else {
7299      window.addEventListener("load", DBG_initPaneContainer);
7300    }
7301
7302     DBG_registerPopupRules();
7303  </script>
7303
7304
7305        <div id="DBG_root">
7306
7307          <!-- DEBUGGER TAB PANE -->
7308          <div style="height:30px;padding:0px 90px 0px 0px;">
7309            <button class=appbut value="Settings" onclick="javascript:oCOM.POPUP.runRule('tab3.1');" title="Settings"><i class="fa fa fa-sliders-h"></i></button>
7310            <button class=appbut value="Tools"    onclick="javascript:oCOM.POPUP.runRule('tab3.2');" title="Tools"><i class="fa fa-toolbox"></i></button>
7311          </div>
7312
7313          <!-- DEBUGGER SETTINGS -->
7314          <div class="popupHost" id="tab3.1" hidden>
7315            <div class="toolbox">
7316              <div class="appbox" title="Debugger display">
7317                LISTING<br>
7318                <label>
7319                  Scroll Listing <input id="DBG_followPcInput" type="checkbox" checked>
7320                  &nbsp;&nbsp;
7321                  Scroll CPU Trace <input id="DBG_autoScrollTraceInput" type="checkbox" checked>
7322                </label>
7323                <label>
7324                  Columns 
7325                  <input id="DBG_listingColumnsInput" type="text" value="{adr:0,code:6,lin:15,lbl:21,ins:30,opr:35,com:51}" title="Listing columns: address, code bytes, jump lines, label, instruction, operand, comment" spellcheck="false" onchange="DBG_disassembleRAM()">
7326                </label>
7327                <div class="DBG_listingPresetList" aria-label="Listing column presets">
7328                  <button type="button" class="DBG_listingPreset" onclick="DBG_applyListingColumnPreset('default')" title="Use default debugger listing columns">
7329                    <span>default</span>
7330                    <i class="fa fa-table"></i>
7331                  </button>
7332                  <button type="button" class="DBG_listingPreset" onclick="DBG_applyListingColumnPreset('wide')" title="Use wider debugger listing columns">
7333                    <span>wide</span>
7334                    <i class="fa fa-table"></i>
7335                  </button>
7336                  <button type="button" class="DBG_listingPreset" onclick="DBG_applyListingColumnPreset('compact')" title="Use compact debugger listing columns">
7337                    <span>compact</span>
7338                    <i class="fa fa-table"></i>
7339                  </button>
7340                </div>
7341              </div>
7342            </div>
7343
7344            <div class="toolbox">
7345              <div class="appbox" title="Trace configuration">
7346                CPU TRACE<br>
7347                <label>Buffer 
7348                  <input id="DBG_traceDepthInput" type="number" value="2048" min="1" step="128" title="Trace depth / ring buffer" onchange="DBG_applyTraceDepthLimit();DBG_renderTraceView();">
7349                </label>
7350                 <label>
7351                   Columns 
7352                   <input id="DBG_traceColumnsInput" type="text" value="{stp:0,pc:5,byt:10,opc:19,rA:32,rX:35,rY:38,rSP:41,rP:44,cyc:47}" title="Trace columns: step, PC, bytes, opcode, A, X, Y, P, SP, cycles" spellcheck="false" onchange="DBG_rebuildTraceLines();DBG_renderTraceView()">
7353                 </label>
7354                <label>Show cycles <input id="DBG_showCyclesInput" type="checkbox" checked onchange="DBG_rebuildTraceLines();DBG_renderTraceView()"></label>
7355                <label>Show PC histogram <input id="DBG_showPcHistogramInput" type="checkbox" onchange="DBG_stub('Show PC histogram')"></label>
7356              </div>
7357            </div>
7358
7359            <div class="toolbox">
7360              <div class="appbox" title="Run limits">
7361                RUN LIMITS<br>
7362                <label>
7363                  Continuous run speed<br>
7364                  <select id="DBG_runSpeedInput" onchange="DBG_statusText('Continuous run speed set to ' + this.options[this.selectedIndex].text + '.')">
7365                    <option value="1">1 IPS</option>
7366                    <option value="10">10 IPS</option>
7367                    <option value="100">100 IPS</option>
7368                    <option value="1000">1000 IPS</option>
7369                    <option value="max">Max</option>
7370                  </select>
7371                </label>
7372                <label>Stop after N instructions<br>
7373                  <input id="DBG_stopAfterInput" type="number" value="" placeholder="∞" min="1" step="100" onchange="DBG_stopAfterInstru
7373ctionLimit()">
7374                </label>
7375              </div>
7376            </div>
7377
7378            <div class="toolbox">
7379              <div class="appbox" title="Watch defaults">
7380                WATCH DEFAULTS<br>
7381                <label>
7382                  Default watch size<br>
7383                  <select id="DBG_defaultWatchSizeInput" onchange="DBG_stub('Default watch size')">
7384                    <option>byte</option>
7385                    <option>word</option>
7386                    <option>bit</option>
7387                  </select>
7388                </label>
7389                <label>
7390                  Default watch format<br>
7391                  <select id="DBG_defaultWatchFormatInput" onchange="DBG_stub('Default watch format')">
7392                    <option>hex</option>
7393                    <option>decimal</option>
7394                    <option>binary</option>
7395                    <option>ASCII</option>
7396                  </select>
7397                </label>
7398              </div>
7399            </div>
7400          </div>
7401
7402          <!-- DEBUGGER TOOLS -->
7403          <div class="popupHost" id="tab3.2" hidden>
7404    
7405          <div class=toolbox>
7406            <form name="DBGboard" onsubmit="return false">
7407              <textarea id="DBG_boardText" cols=40 rows=4 style="float:left" placeholder="Byte-code input" name="boardText" oninput="DBG_syncByteCodeFromBoard()"></textarea><br>
7408              <input title="Send byte code to debugger" id="DBG_ByteCodeImportButton" type="button" value="to debugger" onclick="DBG_importBoardByteCode()">
7409              <input title="Clear byte code" type="button" value="Clear" onclick="javascript:DBG_clearBoard()">
7410              <span id="DBG_byteCodeInfo" style="color:#000000;font-family:arial;font-size:11px;white-space:nowrap;">0 bytes</span>
7411              <input type="button" value="X" onclick="javascript:oCOM.POPUP.off('tab3.2')">
7412            </form>
7413          </div>
7414
7415            <!-- DEBUGGER ACTION BUTTONS -->
7416            <div class="toolbox">
7417              <button id="DBG_toggleWatchPaneButton" class="appbut" onclick="DBG_togglePane3();" title="Show/hide Memory Watch pane">
7418                <i class="fa fa-eye-slash"></i>
7419              </button>
7420              <br>
7421              <button class="appbut" onclick="DBG_stub('Show memory map')" title="Show/hide Memory Map">
7422                <i class="fa fa-th"></i>
7423              </button>            
7424            </div>
7425
7426            <!--  SYMBOL FILE TOOLBOX -->
7427            <div class="toolbox">
7428              <div class="appbox DBG_symbolBox">
7429                <div class="appbut" style="color:black">add symbol table<input id="DBG_sym_fileInput" type="file" accept=".JSON,.json"></div>
7430                <div class="appbut" style="color:black">load session<input id="DBG_debugSessionFileInput" type="file" accept=".DEB,.deb,.JSON,.json"></div>
7431                <div id="DBG_symbolLabelWrap" class="DBG_symbolLabelWrap"></div>
7432              </div>
7433            </div>
7434
7435  
7436            <!-- ALGORITHM TEST BENCH -->
7437            <div class="toolbox DBG_testbenchToolbox">
7438              <div id="DBG_testbenchBox" class="appbox DBG_testbenchBox" title="Algorithm test bench">
7439                <div class="DBG_testbenchHeader">
7440                  <span class="DBG_testbenchTitle">TEST BENCH</span>
7441                  <span class="DBG_testbenchHeaderButtons">
7442                    <button id="DBG_testExampleButton" class="appbut skinny" type="button" title="Load example JavaScript"><i class="fa fa-lightbulb"></i> example</button>
7443                    <button id="DBG_testRunButton" class="appbut skinny" type="button" title="Run script (Ctrl/Cmd+Enter)"><i class="fa fa-play"></i> run</button>
7444                  </span>
7445                </div>
7446
7447                <div class="DBG_testbenchHarnessBar">
7448                  test harness <code>TB</code>
7449                  &nbsp;·&nbsp; RAM <code>DBG_RAM</code>
7450                  &nbsp;·&nbsp; helpers <code>TB.ram.*</code>
7451                  <span id="DBG_testbenchStatus">initializing</span>
7452                </div>
7453
7454                <div class="DBG_testbenchGrid">
7455                  <section class="DBG_testbenchSection">
7456                    <div class="DBG_testbenchSectionHeader">
7457                      <span>JAVASCRIPT</span>
7458                      <span>Ctrl/Cmd + Enter = run</span>
7459                    </div>
7460                    <textarea id="DBG_testScript" spellcheck="false" wrap="off">// Test-bench JavaScript
7461print("DBG_RAM:", RAM.length, "bytes");
7462print(TB.ram.dump(0x0000, 32));</textarea>
7463                  </section>
7464
7465                  <section class="DBG_testbenchSection">
7466                    <div class="DBG_testbenchSectionHeader">
7467                      <span>CONSOLE / REPL</span>
7468                      <span class="DBG_testbenchConsoleHeaderRight">
7469                        <span>TB · RAM · ram · print()</span>
7470                        <button id="DBG_testClearConsoleButton" class="appbut skinny DBG_testConsoleClear" type="button" title="Clear console"><i class="fa fa-trash-alt"></i> clear</button>
7471                      </span>                      
7472                    </div>
7473                    <div id="DBG_testConsole" class="DBG_testConsole"></div>
7474                  </section>
7475                </div>
7476
7477                <div class="DBG_testbenchRam">
7478                  <div class="DBG_testbenchRamRow">
7479                    <span class="DBG_testbenchRamTitle">RAM I/O</span>
7480                    <label>address <input id="DBG_testRamAddress" type="text" value="$3000" spellcheck="false"></label>
7481                    <label>length <input id="DBG_testRamLength" type="number" value="16" min="1" max="65536"></label>
7482                    <input id="DBG_testRamData" type="text" value="00 01 02 03" spellcheck="false" title="Hex bytes; prefix with = for a trusted JavaScript array/expression">
7483                    <button id="DBG_testRamInjectButton" class="appbut skinny" type="button" title="Inject data into debugger RAM"><i class="fa fa-sign-in-alt"></i> inject</button>
7484                    <button id="DBG_testRamReadButton" class="appbut skinny" type="button" title="Read debugger RAM"><i class="fa fa-search"></i> read</button>
7485                  </div>
7486                </div>
7487              </div>
7488            </div>  
7489
7490
7491
7492          </div>
7493
7494          <div id="DBG_symbolOverlapPopup" class="DBG_symbolOverlapBack" hidden>
7495            <div class="DBG_symbolOverlapBox">
7496              <strong>Symbol table scope overlap</strong>
7497              <div>The new symbol table overlaps with already loaded scoped addresses.</div>
7498              <pre id="DBG_symbolOverlapText"></pre>
7499              <div class="DBG_symbolOverlapButtons">
7500                <button class="appbut skinny" type="button" onclick="DBG_resolveSymbolOverlap('cancel')">Cancel</button>
7501                <button class="appbut skinny" type="button" onclick="DBG_resolveSymbolOverlap('override')">Override</button>
7502                <button class="appbut skinny" type="button" onclick="DBG_resolveSymbolOverlap('merge')">Merge</button>
7503              </div>
7504            </div>
7505          </div>
7506
7507          <div id="DBG_paneHost" class="DBG_paneHost">
7508            <section id="DBG_listingPane" class="DBG_pane">
7509              <div class="com-pane-title DBG_paneTitle">
7510                <div class="DBG_paneTitleMain">
7511                  <strong>Listing</strong>
7512                  <input id="DBG_debugSessionFileNameInput"
7513                         type="text"
7514                         value="sample.DEB"
7515                         title="Debugger session filename"
7516                         spellcheck="false"
7517                         style="width:150px;max-width:22vw;border:1px solid rgba(0,0,0,0.25);border-radius:4px;background:#ffffff;color:#000000;padding:2px 5px;font:11px/1.2 courier,monospace;">
7518                  <button id="DBG_exportDebugSessionButton"
7519                          class="appbut skinny"
7520                          type="button"
7521                          title="Download debugger session"
7522                          onclick="DBG_exportDebugSession()"><i class="fa fa-cloud-download-alt"></i></button>
7523                  <span id="DBG_listingInfo" class="DBG_paneInfo"></span>
7524                </div>
7525                <div id="DBG_listingHeaderActions" class="com-pane-header-default-widget DBG_headerActions">
7526
7527                  <button class="appbut skinny" data-dbg-action="step-back" onclick="DBG_traceStepBackward()" title="Step one instruction backward"><i class="fa fa-step-backward"></i></button>
7528                  <button class="appbut skinny" data-dbg-action="step-forward" onclick="DBG_traceStepForward()" title="Step one instruction forward"><i class="fa fa-step-forward"></i></button>
7529                  <button id="DBG_runToggle" class="appbut skinny" data-dbg-action="run" onclick="DBG_toggleContinuousRun()" title="Continuous run"><i class="fa fa-running"></i></button>
7530                  <button class="appbut skinny" data-dbg-action="goto-pc" onclick="DBG_showStartPcRequester('DBG_listingHeaderActions')" title="Go to PC"><i class="fa fa-crosshairs"></i></button>
7531                  <button class="appbut skinny" data-dbg-action="theme" onclick="DBG_toggleTheme()" title="Toggle dark/light mode"><i class="fa fa-adjust"></i></button>
7532
7533                  <span class="DBG_startPcRequester" hidden>
7534                    <span>PC</span>
7535                    <input class="DBG_startPcInput" type="text" value="$0000" maxlength="6" spellcheck="false" onkeydown="DBG_startPcKeyDown(event,'DBG_listingHeaderActions')">
7536                    <button class="appbut skinny" type="button" onclick="DBG_startDebuggerAtPC('DBG_listingHeaderActions')" title="Start debugger at this PC"><i class="fa fa-play"></i></button>
7537                  </span>
7538
7539                  <!--button class="appbut skinny" data-dbg-action="find" onclick="DBG_stub('Find address / label / opcode')" title="Find address / label / opcode"><i class="fa fa-search"></i></button-->
7540                  <!-- TODO: use the find & replace function here (triggered by ctrl/command + F), same pop-up behavior as in tab2 -->
7541                </div>
7542              </div>
7543              <div class="DBG_paneBody dotmatrix_bck">
7544              <div id="DBG_listingPcOverlay" class="DBG_listingPcOverlay" hidden></div>
7545              <textarea id="debugfield" class="DBG_textPane" readonly spellcheck="false" wrap="off"></textarea>
7546              </div>
7547            </section>
7548
7549            <section id="DBG_tracePane" class="DBG_pane">
7550              <div class="com-pane-title DBG_paneTitle">
7551                <div class="DBG_paneTitleMain">
7552                  <strong>CPU Trace</strong>
7553                  <span id="DBG_traceRegisterInfo" class="DBG_paneInfo DBG_traceRegisterInfo">A=00 X=00 Y=00 SP=FF SR=<sub>n</sub>0<sub>v</sub>0<sub>-</sub>1<sub>b</sub>0<sub>d</sub>0<sub>i</sub>1<sub>z</sub>0<sub>c</sub>0</span>
7554                </div>
7555                <div id="DBG_traceHeaderActions" class="com-pane-header-default-widget DBG_headerActions">
7556                  <button class="appbut skinny" data-dbg-action="clear-trace" onclick="DBG_clearTrace()" title="Clear trace"><i class="fa fa-trash-alt"></i></button>
7557                  <!--button class="appbut skinny" data-dbg-action="autoscroll" onclick="DBG_stub('Auto-scroll trace')" title="Auto-scroll trace on/off"><i class="fa fa-arrow-down"></i></button-->
7558                  <!-- TODO: always auto-scroll -->
7559                  <!--button class="appbut skinny" data-dbg-action="filter-trace" onclick="DBG_stub('Filter trace')" title="Filter trace"><i class="fa fa-filter"></i></button-->
7560                 
7561                  <button class="appbut skinny" data-dbg-action="break-on-change" onclick="DBG_stub('Break on change')" title="Add break rule"><i class="fa fa-bell"></i></button>
7562                  <button class="appbut skinny" data-dbg-action="export-trace" onclick="DBG_stub('Export trace')" title="Export trace"><i class="fa fa-cloud-download-alt"></i></button>
7563                  
7564                  <span class="DBG_startPcRequester" hidden>
7565                    <span>PC</span>
7566                    <input class="DBG_startPcInput" type="text" value="$0000" maxlength="6" spellcheck="false" onkeydown="DBG_startPcKeyDown(event,'DBG_traceHeaderActions')">
7567                    <button class="appbut skinny" type="button" onclick="DBG_startDebuggerAtPC('DBG_traceHeaderActions')" title="Start debugger at this PC"><i class="fa fa-play"></i></button>
7568                  </span>
7569
7570                
7571                </div>
7572              </div>
7573              <div class="DBG_paneBody dotmatrix_bck">
7574              <textarea id="DBG_traceView" class="DBG_textPane" readonly spellcheck="false" wrap="off"></textarea>
7575              </div>
7576            </section>
7577
7578            <section id="DBG_watchPane" class="DBG_pane">
7579              <div class="com-pane-title DBG_paneTitle">
7580                <div class="DBG_paneTitleMain">
7581                  <strong>CPU Watch</strong>
7582                  <span id="DBG_status" class="DBG_paneInfo"></span>
7583                </div>
7584                <div class="com-pane-header-default-widget DBG_headerActions">
7585                  <button class="appbut skinny" data-dbg-action="add-watch" onclick="DBG_stub('Add watch')" title="Add watch"><i class="fa fa-stethoscope"></i></button>
7586                  <!--button class="appbut skinny" data-dbg-action="remove-watch" onclick="DBG_stub('Remove selected watcher')" title="Remove selected watcher"><i class="fa fa-minus-circle"></i></button-->
7587                  <!--  TODO: WE WILL PUT A DELETE PICTOGRAM NEXT TO EACH MEMORY WATCH ITEM like <i class="fa fa-times-circle"></i> -->
7588                  <!--button class="appbut skinny" data-dbg-action="load-save-watch-and-break" onclick="DBG_stub('Load/Save watch & break ')" title="Load/Save"><i class="fa fa-save"></i></button-->
7589
7590                </div>
7591              </div>
7592              <div class="DBG_paneBody dotmatrix_bck">
7593                <textarea id="DBG_watchView" class="DBG_textPane" readonly spellcheck="false" wrap="off"></textarea>             
7594              </div>
7595            </section>
7596
7597         </div>
7598
7599            <section id="DBG_manualPane" class="DBG_manualPane CRT_TV DOUBL_TV">
7600              <div style="height:120px"></div>
7601              <div name="codefield" id="DBG_manual" class="DBG_manualField markdown_mon" style="border:1px solid black;width:625px;height:420px"></div>
7602              <div style="height:300px"></div>
7603            </section>
7604
7605        </div>
7606      </div>
7607          
7608        </div>
7609      </td>
7610  </tr>
7611</table>
7612
7613</body>
7614<footer><small><a target=_blank href=https://github.com/RetroAppleJS/RetroAppleJS.github.io>GitHub</a>&nbsp;&nbsp;<a target=_blank href="tools/TOOLS_CATALOG.html">Tools</a>&nbsp;&nbsp;<a target=_blank href=https://raw.githubusercontent.com/RetroAppleJS/RetroAppleJS.github.io/main/LICENSE.md>
7614Copyright (c) 2022, Freddy Vandriessche</a> <a href="JavaScript:document.body.scrollTop=0;document.documentElement.scrollTop = 0;">Top</a>  </small> <div id="footer" style="display: inline"></div></footer>
7615</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.