easycoder 251105.1__py2.py3-none-any.whl → 260111.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. easycoder/__init__.py +6 -3
  2. easycoder/debugger/__init__.py +5 -0
  3. easycoder/debugger/ec_dbg_value_display copy.py +195 -0
  4. easycoder/debugger/ec_dbg_value_display.py +24 -0
  5. easycoder/debugger/ec_dbg_watch_list copy.py +219 -0
  6. easycoder/debugger/ec_dbg_watchlist.py +293 -0
  7. easycoder/debugger/ec_debug.py +1025 -0
  8. easycoder/ec_classes.py +487 -11
  9. easycoder/ec_compiler.py +81 -44
  10. easycoder/ec_condition.py +1 -1
  11. easycoder/ec_core.py +1042 -1081
  12. easycoder/ec_gclasses.py +236 -0
  13. easycoder/ec_graphics.py +1683 -0
  14. easycoder/ec_handler.py +18 -14
  15. easycoder/ec_mqtt.py +248 -0
  16. easycoder/ec_program.py +297 -168
  17. easycoder/ec_psutil.py +48 -0
  18. easycoder/ec_value.py +65 -47
  19. easycoder/pre/README.md +3 -0
  20. easycoder/pre/__init__.py +17 -0
  21. easycoder/pre/debugger/__init__.py +5 -0
  22. easycoder/pre/debugger/ec_dbg_value_display copy.py +195 -0
  23. easycoder/pre/debugger/ec_dbg_value_display.py +24 -0
  24. easycoder/pre/debugger/ec_dbg_watch_list copy.py +219 -0
  25. easycoder/pre/debugger/ec_dbg_watchlist.py +293 -0
  26. easycoder/{ec_debug.py → pre/debugger/ec_debug.py} +418 -185
  27. easycoder/pre/ec_border.py +67 -0
  28. easycoder/pre/ec_classes.py +470 -0
  29. easycoder/pre/ec_compiler.py +291 -0
  30. easycoder/pre/ec_condition.py +27 -0
  31. easycoder/pre/ec_core.py +2772 -0
  32. easycoder/pre/ec_gclasses.py +230 -0
  33. easycoder/{ec_pyside.py → pre/ec_graphics.py} +583 -433
  34. easycoder/pre/ec_handler.py +79 -0
  35. easycoder/pre/ec_keyboard.py +439 -0
  36. easycoder/pre/ec_program.py +557 -0
  37. easycoder/pre/ec_psutil.py +48 -0
  38. easycoder/pre/ec_timestamp.py +11 -0
  39. easycoder/pre/ec_value.py +124 -0
  40. easycoder/pre/icons/close.png +0 -0
  41. easycoder/pre/icons/exit.png +0 -0
  42. easycoder/pre/icons/run.png +0 -0
  43. easycoder/pre/icons/step.png +0 -0
  44. easycoder/pre/icons/stop.png +0 -0
  45. easycoder/pre/icons/tick.png +0 -0
  46. {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/METADATA +11 -1
  47. easycoder-260111.1.dist-info/RECORD +59 -0
  48. easycoder-251105.1.dist-info/RECORD +0 -24
  49. {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/WHEEL +0 -0
  50. {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/entry_points.txt +0 -0
  51. {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/licenses/LICENSE +0 -0
@@ -4,20 +4,24 @@ from PySide6.QtWidgets import (
4
4
  QWidget,
5
5
  QFrame,
6
6
  QHBoxLayout,
7
+ QGridLayout,
7
8
  QVBoxLayout,
8
9
  QLabel,
9
10
  QSplitter,
10
11
  QMessageBox,
11
12
  QScrollArea,
13
+ QScrollBar,
12
14
  QSizePolicy,
13
15
  QToolBar,
14
16
  QPushButton,
15
- QInputDialog
17
+ QInputDialog,
18
+ QTabWidget
16
19
  )
17
20
  from PySide6.QtGui import QTextCursor, QIcon
18
21
  from PySide6.QtCore import Qt, QTimer
19
- from typing import Any
20
22
  from typing import Any, Optional
23
+ from .ec_dbg_value_display import ValueDisplay
24
+ from .ec_dbg_watchlist import WatchListWidget
21
25
 
22
26
  class Object():
23
27
  def __setattr__(self, name: str, value: Any) -> None:
@@ -38,6 +42,24 @@ class Debugger(QMainWindow):
38
42
  def write(self, text: str):
39
43
  if not text:
40
44
  return
45
+
46
+ # Echo all output to original stdout with proper line breaks
47
+ try:
48
+ if self.debugger._orig_stdout:
49
+ self.debugger._orig_stdout.write(text)
50
+ self.debugger._orig_stdout.flush()
51
+ except Exception:
52
+ pass
53
+
54
+ # Check if this looks like an error message - if so, also write to original stderr
55
+ if any(err_marker in text for err_marker in ['Error', 'Traceback', 'Exception']):
56
+ try:
57
+ if self.debugger._orig_stderr:
58
+ self.debugger._orig_stderr.write(text)
59
+ self.debugger._orig_stderr.flush()
60
+ except Exception:
61
+ pass
62
+
41
63
  # Buffer text and request a flush on the GUI timer
42
64
  self._buf.append(text)
43
65
  if self.debugger._flush_timer and not self.debugger._flush_timer.isActive():
@@ -54,100 +76,40 @@ class Debugger(QMainWindow):
54
76
  super().__init__(parent)
55
77
  self.debugger = parent
56
78
  layout = QVBoxLayout(self)
57
-
58
- # Create toolbar with icon buttons
59
- toolbar = QToolBar()
60
- toolbar.setMovable(False)
61
-
62
- # Get the icons directory path
63
- icons_dir = os.path.join(os.path.dirname(__file__), 'icons')
64
-
65
- # Run button
66
- run_btn = QPushButton()
67
- run_icon_path = os.path.join(icons_dir, 'run.png')
68
- run_btn.setIcon(QIcon(run_icon_path))
69
- run_btn.setToolTip("Run")
70
- run_btn.clicked.connect(self.on_run_clicked)
71
- toolbar.addWidget(run_btn)
72
-
73
- # Step button
74
- step_btn = QPushButton()
75
- step_icon_path = os.path.join(icons_dir, 'step.png')
76
- step_btn.setIcon(QIcon(step_icon_path))
77
- step_btn.setToolTip("Step")
78
- step_btn.clicked.connect(self.on_step_clicked)
79
- toolbar.addWidget(step_btn)
80
-
81
- # Stop button
82
- stop_btn = QPushButton()
83
- stop_icon_path = os.path.join(icons_dir, 'stop.png')
84
- stop_btn.setIcon(QIcon(stop_icon_path))
85
- stop_btn.setToolTip("Stop")
86
- stop_btn.clicked.connect(self.on_stop_clicked)
87
- toolbar.addWidget(stop_btn)
88
-
89
- # Exit button
90
- exit_btn = QPushButton()
91
- exit_icon_path = os.path.join(icons_dir, 'exit.png')
92
- exit_btn.setIcon(QIcon(exit_icon_path))
93
- exit_btn.setToolTip("Exit")
94
- exit_btn.clicked.connect(self.on_exit_clicked)
95
- toolbar.addWidget(exit_btn)
96
-
97
79
 
98
- layout.addWidget(toolbar)
99
-
100
- # --- Watch panel (like VS Code) ---
101
- watch_panel = QFrame()
102
- watch_panel.setFrameShape(QFrame.Shape.StyledPanel)
103
- # Ensure the VARIABLES bar stretches to full available width
104
- watch_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
105
- watch_layout = QHBoxLayout(watch_panel)
80
+ # Header panel
81
+ variable_panel = QFrame()
82
+ variable_panel.setFrameShape(QFrame.Shape.StyledPanel)
83
+ variable_panel.setStyleSheet("background-color: white;")
84
+ variable_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
85
+ watch_layout = QHBoxLayout(variable_panel)
106
86
  watch_layout.setContentsMargins(4, 4, 4, 4)
107
87
  watch_layout.setSpacing(4)
108
88
 
109
-
110
- # Title label
111
89
  title_label = QLabel("VARIABLES")
112
90
  title_label.setStyleSheet("font-weight: bold; letter-spacing: 1px;")
113
91
  watch_layout.addWidget(title_label)
114
-
115
- # Stretch to push buttons right
116
92
  watch_layout.addStretch()
117
93
 
118
- # Placeholder add/remove icons (replace with real icons later)
119
- add_btn = QPushButton()
94
+ add_btn = QPushButton("+")
120
95
  add_btn.setToolTip("Add variable to watch")
121
- # TODO: set add_btn.setIcon(QIcon(path)) when icon is available
122
- add_btn.setText("+")
123
96
  add_btn.setFixedSize(24, 24)
124
97
  add_btn.clicked.connect(self.on_add_clicked)
125
98
  watch_layout.addWidget(add_btn)
126
99
 
127
- layout.addWidget(watch_panel)
128
-
129
- # Watch list area (renders selected variables beneath the toolbar)
130
- self.watch_list_widget = QWidget()
131
- self.watch_list_layout = QVBoxLayout(self.watch_list_widget)
132
- self.watch_list_layout.setContentsMargins(6, 2, 6, 2)
133
- self.watch_list_layout.setSpacing(2)
134
- layout.addWidget(self.watch_list_widget)
135
-
136
- # Keep a simple set to prevent duplicate labels
137
- self._watch_set = set()
100
+ layout.addWidget(variable_panel)
138
101
 
139
- layout.addStretch()
102
+ # Watch list widget
103
+ self.watch_list = WatchListWidget(self.debugger)
104
+ layout.addWidget(self.watch_list, 1)
140
105
 
141
106
  def on_add_clicked(self):
142
- # Build the variable list from the program. Prefer Program.symbols mapping.
143
107
  try:
144
108
  program = self.debugger.program # type: ignore[attr-defined]
145
- # Fallback to scanning code if symbols is empty
146
109
  items = []
147
110
  if hasattr(program, 'symbols') and isinstance(program.symbols, dict) and program.symbols:
148
111
  items = sorted([name for name in program.symbols.keys() if name and not name.endswith(':')])
149
112
  else:
150
- # Fallback heuristic: look for commands whose 'type' == 'symbol' (as per requirement)
151
113
  for cmd in getattr(program, 'code', []):
152
114
  try:
153
115
  if cmd.get('type') == 'symbol' and 'name' in cmd:
@@ -158,80 +120,60 @@ class Debugger(QMainWindow):
158
120
  if not items:
159
121
  QMessageBox.information(self, "Add Watch", "No variables found in this program.")
160
122
  return
161
- choice, ok = QInputDialog.getItem(self, "Add Watch", "Select a variable:", items, 0, False)
162
- if ok and choice:
163
- # Record the choice for future use (UI for list will be added later)
164
- if not hasattr(self.debugger, 'watched'):
165
- self.debugger.watched = [] # type: ignore[attr-defined]
166
- if choice not in self.debugger.watched: # type: ignore[attr-defined]
167
- self.debugger.watched.append(choice) # type: ignore[attr-defined]
168
- # Render as a plain label beneath the toolbar if not already present
169
- if choice not in self._watch_set:
170
- self._add_watch_row(choice)
171
- self._watch_set.add(choice)
172
- # Optionally echo to console for now
173
- try:
174
- self.debugger.console.append(f"Watching: {choice}") # type: ignore[attr-defined]
175
- except Exception:
176
- pass
123
+
124
+ # Use a custom chooser dialog with a visible list (already open)
125
+ from PySide6.QtWidgets import QDialog, QVBoxLayout, QListWidget, QDialogButtonBox
126
+ dlg = QDialog(self)
127
+ dlg.setWindowTitle("Add Watch")
128
+ v = QVBoxLayout(dlg)
129
+ lst = QListWidget(dlg)
130
+ lst.addItems(items)
131
+ lst.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
132
+ if items:
133
+ lst.setCurrentRow(0)
134
+ # Allow double-click to accept immediately
135
+ def accept_double(item):
136
+ if item:
137
+ lst.setCurrentItem(item)
138
+ dlg.accept()
139
+ lst.itemDoubleClicked.connect(accept_double)
140
+ v.addWidget(lst)
141
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=dlg)
142
+ v.addWidget(buttons)
143
+ buttons.accepted.connect(dlg.accept)
144
+ buttons.rejected.connect(dlg.reject)
145
+ if dlg.exec() == QDialog.DialogCode.Accepted:
146
+ cur = lst.currentItem()
147
+ choice = cur.text() if cur else None
148
+ if choice:
149
+ self.watch_list.addVariable(choice)
150
+ try:
151
+ self.debugger.console.append(f"Watching: {choice}") # type: ignore[attr-defined]
152
+ except Exception:
153
+ pass
177
154
  except Exception as exc:
178
155
  QMessageBox.warning(self, "Add Watch", f"Could not list variables: {exc}")
179
156
 
180
- def _add_watch_row(self, name: str):
181
- row = QWidget()
182
- h = QHBoxLayout(row)
183
- h.setContentsMargins(0, 0, 0, 0)
184
- h.setSpacing(4)
185
- lbl = QLabel(name)
186
- lbl.setStyleSheet("font-family: mono; padding: 1px 2px;")
187
- h.addWidget(lbl)
188
- h.addStretch()
189
- btn = QPushButton()
190
- btn.setText("–") # placeholder until icon provided
191
- btn.setToolTip(f"Remove '{name}' from watch")
192
- btn.setFixedSize(20, 20)
193
-
194
- def on_remove():
195
- try:
196
- # update internal structures
197
- if hasattr(self.debugger, 'watched') and name in self.debugger.watched: # type: ignore[attr-defined]
198
- self.debugger.watched.remove(name) # type: ignore[attr-defined]
199
- if name in self._watch_set:
200
- self._watch_set.remove(name)
201
- # remove row from layout/UI
202
- row.setParent(None)
203
- row.deleteLater()
204
- except Exception:
205
- pass
206
-
207
- btn.clicked.connect(on_remove)
208
- h.addWidget(btn)
209
- self.watch_list_layout.addWidget(row)
210
-
211
- def on_run_clicked(self):
212
- self.debugger.doRun() # type: ignore[attr-defined]
213
-
214
- def on_step_clicked(self):
215
- self.debugger.doStep() # type: ignore[attr-defined]
216
-
217
- def on_stop_clicked(self):
218
- self.debugger.doStop() # type: ignore[attr-defined]
219
-
220
- def on_exit_clicked(self):
221
- self.debugger.doClose() # type: ignore[attr-defined]
222
-
223
157
  ###########################################################################
224
- # The right-hand column of the main window
225
- class MainRightColumn(QWidget):
158
+ # A single script panel that displays one script's lines
159
+ class ScriptPanel(QWidget):
226
160
  scroll: QScrollArea
227
161
  layout: QHBoxLayout # type: ignore[assignment]
228
- blob: QLabel
229
162
 
230
163
  def __init__(self, parent=None):
231
164
  super().__init__(parent)
165
+
166
+ # Set white background
167
+ self.setStyleSheet("background-color: white;")
168
+
169
+ # Main layout
170
+ panel_layout = QVBoxLayout(self)
171
+ panel_layout.setContentsMargins(0, 0, 0, 0)
172
+ panel_layout.setSpacing(0)
232
173
 
233
174
  # Create a scroll area - its content widget holds the lines
234
- self.scroll = QScrollArea(self)
175
+ self.scroll = QScrollArea()
176
+ self.scroll.setStyleSheet("background-color: white;")
235
177
  self.scroll.setWidgetResizable(True)
236
178
 
237
179
  # Ensure this widget and the scroll area expand to fill available space
@@ -249,21 +191,19 @@ class Debugger(QMainWindow):
249
191
 
250
192
  self.scroll.setWidget(self.content)
251
193
 
252
- # outer layout for this widget contains only the scroll area
253
- main_layout = QVBoxLayout(self)
254
- main_layout.setContentsMargins(0, 0, 0, 0)
255
- main_layout.addWidget(self.scroll)
256
- # ensure the scroll area gets the stretch so it fills the parent
257
- main_layout.setStretch(0, 1)
194
+ # Add scroll area to the panel layout
195
+ panel_layout.addWidget(self.scroll)
196
+
197
+ # Store script lines for this panel
198
+ self.scriptLines = []
258
199
 
259
200
  #######################################################################
260
- # Add a line to the right-hand column
201
+ # Add a line to this script panel
261
202
  def addLine(self, spec):
262
203
 
263
- # Determine if this line is a command (not empty, not a comment), using the original script line
264
- orig_line = getattr(spec, 'orig_line', spec.line) if hasattr(spec, 'orig_line') or 'orig_line' in spec.__dict__ else spec.line
265
- line_lstripped = orig_line.lstrip()
266
- is_command = bool(line_lstripped and not line_lstripped.startswith('!'))
204
+ # is_command will be set later by enableBreakpoints() after compilation
205
+ # Initialize to False for now
206
+ spec.is_command = False
267
207
 
268
208
  class Label(QLabel):
269
209
  def __init__(self, text, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft, on_click=None):
@@ -277,7 +217,7 @@ class Debugger(QMainWindow):
277
217
  if fixed_width is not None:
278
218
  self.setFixedWidth(fixed_width)
279
219
  self.setAlignment(align | Qt.AlignmentFlag.AlignVCenter)
280
- self._on_click = on_click if is_command else None
220
+ self._on_click = on_click
281
221
 
282
222
  def mousePressEvent(self, event):
283
223
  if self._on_click:
@@ -313,7 +253,7 @@ class Debugger(QMainWindow):
313
253
  class ClickableBlob(QLabel):
314
254
  def __init__(self, on_click=None):
315
255
  super().__init__()
316
- self._on_click = on_click if is_command else None
256
+ self._on_click = on_click
317
257
  def mousePressEvent(self, event):
318
258
  if self._on_click:
319
259
  try:
@@ -323,7 +263,7 @@ class Debugger(QMainWindow):
323
263
  super().mousePressEvent(event)
324
264
 
325
265
  blob_size = 10
326
- blob = ClickableBlob(on_click=(lambda: spec.onClick(spec.lino)) if is_command else None)
266
+ blob = ClickableBlob(on_click=lambda: spec.onClick(spec.lino))
327
267
  blob.setFixedSize(blob_size, blob_size)
328
268
 
329
269
  def set_blob_visible(widget, visible):
@@ -348,7 +288,7 @@ class Debugger(QMainWindow):
348
288
 
349
289
  # create the line-number label; clicking it reports back to the caller
350
290
  lino_label = Label(str(spec.lino+1), fixed_width=width_4, align=Qt.AlignmentFlag.AlignRight,
351
- on_click=(lambda: spec.onClick(spec.lino)) if is_command else None)
291
+ on_click=lambda: spec.onClick(spec.lino))
352
292
  lino_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
353
293
  # create the text label for the line itself
354
294
  text_label = Label(spec.line, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft)
@@ -361,14 +301,119 @@ class Debugger(QMainWindow):
361
301
  self.inner_layout.addWidget(panel)
362
302
  return panel
363
303
 
364
- def showBlob(self):
365
- self.blob.setStyleSheet("background-color: red; border-radius: 5px; margin:0px; padding:0px;")
304
+ def addStretch(self):
305
+ self.inner_layout.addStretch()
306
+
307
+ ###########################################################################
308
+ # The right-hand column of the main window
309
+ class MainRightColumn(QWidget):
366
310
 
367
- def hideBlob(self):
368
- self.blob.setStyleSheet("background-color: none; border-radius: 5px; margin:0px; padding:0px;")
311
+ def __init__(self, parent=None):
312
+ super().__init__(parent)
313
+
314
+ # Set white background for the entire column
315
+ self.setStyleSheet("background-color: white;")
316
+
317
+ # Main layout for this column
318
+ column_layout = QVBoxLayout(self)
319
+ column_layout.setContentsMargins(0, 0, 0, 0)
320
+ column_layout.setSpacing(0)
321
+
322
+ # Create toolbar with icon buttons
323
+ toolbar = QToolBar()
324
+ toolbar.setMovable(False)
325
+
326
+ # Get the icons directory path
327
+ icons_dir = os.path.join(os.path.dirname(__file__), '../icons')
328
+
329
+ # Get the parent debugger for callbacks
330
+ debugger = parent
331
+
332
+ # Run button
333
+ run_btn = QPushButton()
334
+ run_icon_path = os.path.join(icons_dir, 'run.png')
335
+ run_btn.setIcon(QIcon(run_icon_path))
336
+ run_btn.setToolTip("Run")
337
+ run_btn.clicked.connect(lambda: debugger.doRun() if debugger else None) # type: ignore[attr-defined]
338
+ toolbar.addWidget(run_btn)
339
+
340
+ # Step button
341
+ step_btn = QPushButton()
342
+ step_icon_path = os.path.join(icons_dir, 'step.png')
343
+ step_btn.setIcon(QIcon(step_icon_path))
344
+ step_btn.setToolTip("Step")
345
+ step_btn.clicked.connect(lambda: debugger.doStep() if debugger else None) # type: ignore[attr-defined]
346
+ toolbar.addWidget(step_btn)
347
+
348
+ # Stop button
349
+ stop_btn = QPushButton()
350
+ stop_icon_path = os.path.join(icons_dir, 'stop.png')
351
+ stop_btn.setIcon(QIcon(stop_icon_path))
352
+ stop_btn.setToolTip("Stop")
353
+ stop_btn.clicked.connect(lambda: debugger.doStop() if debugger else None) # type: ignore[attr-defined]
354
+ toolbar.addWidget(stop_btn)
355
+
356
+ # Exit button
357
+ exit_btn = QPushButton()
358
+ exit_icon_path = os.path.join(icons_dir, 'exit.png')
359
+ exit_btn.setIcon(QIcon(exit_icon_path))
360
+ exit_btn.setToolTip("Exit")
361
+ exit_btn.clicked.connect(lambda: debugger.doClose() if debugger else None) # type: ignore[attr-defined]
362
+ toolbar.addWidget(exit_btn)
363
+
364
+ column_layout.addWidget(toolbar)
365
+
366
+ # Create a tab widget to hold multiple script panels
367
+ self.tabWidget = QTabWidget()
368
+ self.tabWidget.setTabsClosable(False) # Don't allow closing tabs for now
369
+
370
+ # Ensure tab widget expands
371
+ self.tabWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
372
+
373
+ column_layout.addWidget(self.tabWidget)
374
+
375
+ # Dictionary to map program -> script panel
376
+ self.programPanels = {}
377
+
378
+ #######################################################################
379
+ # Add a new script tab
380
+ def addScriptTab(self, program, filename):
381
+ """Add a new tab for a script"""
382
+ panel = Debugger.ScriptPanel(self)
383
+ # Extract just the filename from the full path
384
+ tab_label = os.path.basename(filename)
385
+ self.tabWidget.addTab(panel, tab_label)
386
+ self.programPanels[id(program)] = panel
387
+ return panel
388
+
389
+ #######################################################################
390
+ # Get the current active script panel
391
+ def getCurrentPanel(self):
392
+ """Get the currently active script panel"""
393
+ return self.tabWidget.currentWidget()
394
+
395
+ #######################################################################
396
+ # Get the panel for a specific program
397
+ def getPanelForProgram(self, program):
398
+ """Get the panel associated with a program"""
399
+ return self.programPanels.get(id(program))
400
+
401
+ #######################################################################
402
+ # Legacy method - add a line to the current panel
403
+ def addLine(self, spec):
404
+ """Delegate to the current panel's addLine method"""
405
+ panel = self.getCurrentPanel()
406
+ if panel and isinstance(panel, Debugger.ScriptPanel):
407
+ return panel.addLine(spec)
408
+ return None
369
409
 
410
+ #######################################################################
411
+ # Add stretch to current panel
370
412
  def addStretch(self):
371
- self.layout.addStretch()
413
+ """Delegate to the current panel's addStretch method"""
414
+ panel = self.getCurrentPanel()
415
+ if panel and isinstance(panel, Debugger.ScriptPanel):
416
+ panel.addStretch()
372
417
 
373
418
  ###########################################################################
374
419
  # Main debugger class initializer
@@ -380,6 +425,9 @@ class Debugger(QMainWindow):
380
425
  # Disable the window close button
381
426
  self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
382
427
  self.stopped = True
428
+ self.skip_next_breakpoint = False # Flag to skip breakpoint check on resume
429
+ self.saved_queue = [] # Save queue state when stopped to preserve forked threads
430
+ self._highlighted: set[int] = set()
383
431
 
384
432
  # try to load saved geometry from ~/.ecdebug.conf
385
433
  cfg_path = os.path.join(os.path.expanduser("~"), ".ecdebug.conf")
@@ -429,9 +477,10 @@ class Debugger(QMainWindow):
429
477
  left.setFrameShape(QFrame.Shape.StyledPanel)
430
478
  left_layout = QVBoxLayout(left)
431
479
  left_layout.setContentsMargins(8, 8, 8, 8)
480
+ left_layout.setSpacing(0)
432
481
  self.leftColumn = self.MainLeftColumn(self)
433
- left_layout.addWidget(self.leftColumn)
434
- left_layout.addStretch()
482
+ self.leftColumn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
483
+ left_layout.addWidget(self.leftColumn, 1)
435
484
 
436
485
  # Right pane
437
486
  right = QFrame()
@@ -489,7 +538,7 @@ class Debugger(QMainWindow):
489
538
 
490
539
  # Use the vertical splitter as the central widget
491
540
  self.setCentralWidget(self.vsplitter)
492
- self.parse(program.script.lines)
541
+ self.parse(program.script.lines, program, program.scriptName)
493
542
  self.show()
494
543
 
495
544
  def _flush_console_buffer(self):
@@ -523,15 +572,41 @@ class Debugger(QMainWindow):
523
572
 
524
573
  ###########################################################################
525
574
  # Parse a script into the right-hand column
526
- def parse(self, script):
575
+ def parse(self, script, program=None, filename=None):
576
+ """Parse a script and add it as a tab
577
+
578
+ Args:
579
+ script: List of script lines
580
+ program: The Program instance this script belongs to
581
+ filename: The filename to use as the tab label
582
+ """
527
583
  self.scriptLines = []
528
- # Clear existing lines from the right column layout
529
- layout = self.rightColumn.inner_layout
530
- while layout.count():
531
- item = layout.takeAt(0)
532
- widget = item.widget()
533
- if widget:
534
- widget.deleteLater()
584
+
585
+ # Get or create the panel for this program
586
+ panel = None
587
+ if program:
588
+ panel = self.rightColumn.getPanelForProgram(program)
589
+ if not panel:
590
+ # Create a new tab for this program
591
+ if not filename:
592
+ filename = getattr(program, 'path', 'Untitled')
593
+ panel = self.rightColumn.addScriptTab(program, filename)
594
+
595
+ # If no program specified or panel creation failed, use current panel
596
+ if not panel:
597
+ panel = self.rightColumn.getCurrentPanel()
598
+ if not panel:
599
+ # Create a default tab
600
+ panel = self.rightColumn.addScriptTab(None, filename or 'Untitled')
601
+
602
+ # Clear existing lines from the panel
603
+ if panel and isinstance(panel, Debugger.ScriptPanel):
604
+ layout = panel.inner_layout
605
+ while layout.count():
606
+ item = layout.takeAt(0)
607
+ widget = item.widget()
608
+ if widget:
609
+ widget.deleteLater()
535
610
 
536
611
  # Parse and add new lines
537
612
  lino = 0
@@ -631,11 +706,49 @@ class Debugger(QMainWindow):
631
706
 
632
707
  return output
633
708
 
709
+ ###########################################################################
710
+ # Enable breakpoints to be set on any line that is classed as a command
711
+ def enableBreakpoints(self):
712
+ """
713
+ Examine each line and set is_command flag based on compiled code.
714
+ A line is a command if:
715
+ - It's not empty or a comment
716
+ - It's not a label (single word ending with colon)
717
+ - It corresponds to a command in program.code with type != 'symbol'
718
+ """
719
+ # First, mark all lines as non-commands by default
720
+ for lineSpec in self.scriptLines:
721
+ lineSpec.is_command = False
722
+
723
+ # Now iterate through compiled commands and mark those that are executable
724
+ for command in self.program.code:
725
+ if 'lino' not in command:
726
+ continue
727
+
728
+ lino = command['lino']
729
+ if lino < 0 or lino >= len(self.scriptLines):
730
+ continue
731
+
732
+ # Check if this is a symbol declaration (variable/constant definition)
733
+ if command.get('type') == 'symbol':
734
+ continue
735
+
736
+ # Check if this is a structural keyword that shouldn't have breakpoints
737
+ if command.get('keyword') in ['begin', 'end']:
738
+ continue
739
+
740
+ # This is an executable command
741
+ self.scriptLines[lino].is_command = True
742
+
634
743
  ###########################################################################
635
744
  # Here when the user clicks a line number
636
745
  def onClickLino(self, lino):
637
- # Show or hide the red blob next to this line
746
+ # Check if this line is a command - if not, take no action
638
747
  lineSpec = self.scriptLines[lino]
748
+ if not getattr(lineSpec, 'is_command', True):
749
+ return
750
+
751
+ # Show or hide the red blob next to this line
639
752
  lineSpec.bp = not lineSpec.bp
640
753
  if lineSpec.bp: lineSpec.label.showBlob()
641
754
  else: lineSpec.label.hideBlob()
@@ -659,8 +772,13 @@ class Debugger(QMainWindow):
659
772
  if not panel:
660
773
  return
661
774
 
662
- # Get the scroll area from the right column
663
- scroll_area = self.rightColumn.scroll
775
+ # Get the current script panel
776
+ script_panel = self.rightColumn.getCurrentPanel()
777
+ if not script_panel or not isinstance(script_panel, Debugger.ScriptPanel):
778
+ return
779
+
780
+ # Get the scroll area from the script panel
781
+ scroll_area = script_panel.scroll
664
782
 
665
783
  # Get the vertical position of the panel relative to the content widget
666
784
  panel_y = panel.y()
@@ -687,7 +805,7 @@ class Debugger(QMainWindow):
687
805
  ###########################################################################
688
806
  # Set the background color of one line of the script
689
807
  def setBackground(self, lino, color):
690
- # Set the background color of the given line
808
+ # Set the background color of the given line and track highlighted lines
691
809
  if lino < 0 or lino >= len(self.scriptLines):
692
810
  return
693
811
  lineSpec = self.scriptLines[lino]
@@ -696,38 +814,153 @@ class Debugger(QMainWindow):
696
814
  return
697
815
  if color == 'none':
698
816
  panel.setStyleSheet("")
817
+ self._highlighted.discard(lino)
699
818
  else:
700
819
  panel.setStyleSheet(f"background-color: {color};")
820
+ self._highlighted.add(lino)
821
+
822
+ def _clearHighlights(self):
823
+ # Remove highlighting from all previously highlighted lines
824
+ for lino in list(self._highlighted):
825
+ self.setBackground(lino, 'none')
826
+ self._highlighted.clear()
701
827
 
702
828
  ###########################################################################
703
- # Here after each instruction has run
704
- def continueExecution(self):
705
- result = True
829
+ # Here before each instruction is run
830
+ # Returns True if the program should halt and wait for user interaction
831
+ def checkIfHalt(self, is_first_command=False):
706
832
  self.pc = self.program.pc
707
833
  command = self.program.code[self.pc]
708
- lino = command['lino'] + 1
709
- if self.stopped: result = False
710
- elif command['bp']:
711
- print(f"Hit breakpoint at line {lino}")
834
+ lino = command['lino'] if 'lino' in command else 0
835
+ bp = command.get('bp', False)
836
+
837
+ # Check if we should skip this breakpoint check (resuming from same location)
838
+ if self.skip_next_breakpoint:
839
+ self.skip_next_breakpoint = False
840
+ return False
841
+
842
+ # Labels should never halt execution - they're just markers
843
+ # A label is a symbol whose name ends with ':'
844
+ if command.get('type') == 'symbol' and command.get('name', '').endswith(':'):
845
+ return False
846
+
847
+ # Determine if we should halt
848
+ should_halt = False
849
+
850
+ # If this is the first real command (pc==1), always halt to initialize display
851
+ if is_first_command:
852
+ should_halt = True
853
+ self.stopped = True
854
+ print(f"Program ready at line {lino + 1}")
855
+ # If we're in stopped (step) mode, halt after each command
856
+ elif self.stopped:
857
+ should_halt = True
858
+ # If there's a breakpoint on this line, halt
859
+ elif bp:
860
+ print(f"Hit breakpoint at line {lino + 1}")
712
861
  self.stopped = True
713
- result = False
714
- if not result:
862
+ should_halt = True
863
+
864
+ # If halting, update the UI and save queue state
865
+ if should_halt:
715
866
  self.scrollTo(lino)
716
- self.setBackground(command['lino'], 'LightYellow')
717
- return result
867
+ self._clearHighlights()
868
+ self.setBackground(lino, 'Yellow')
869
+ # Refresh variable values when halted
870
+ self.refreshVariables()
871
+ # Save the current queue state to preserve forked threads
872
+ self._saveQueueState()
873
+
874
+ return should_halt
875
+
876
+ def refreshVariables(self):
877
+ """Update all watched variable values"""
878
+ try:
879
+ if hasattr(self, 'leftColumn') and hasattr(self.leftColumn, 'watch_list'):
880
+ self.leftColumn.watch_list.refreshVariables(self.program)
881
+ except Exception as ex:
882
+ print(f"Error refreshing variables: {ex}")
883
+
884
+ def _saveQueueState(self):
885
+ """Save the current global queue state (preserves forked threads)"""
886
+ try:
887
+ # Import the module to access the global queue
888
+ from easycoder import ec_program
889
+ # Save a copy of the queue
890
+ self.saved_queue = list(ec_program.queue)
891
+ except Exception as ex:
892
+ print(f"Error saving queue state: {ex}")
893
+
894
+ def _restoreQueueState(self):
895
+ """Restore the saved queue state (resume all forked threads)"""
896
+ try:
897
+ # Import here to avoid circular dependency
898
+ from easycoder import ec_program
899
+ # Restore the queue from saved state
900
+ if self.saved_queue:
901
+ ec_program.queue.clear()
902
+ ec_program.queue.extend(self.saved_queue)
903
+ except Exception as ex:
904
+ print(f"Error restoring queue state: {ex}")
718
905
 
719
906
  def doRun(self):
907
+ """Resume free-running execution from current PC"""
908
+ command = self.program.code[self.pc]
909
+ lino = command.get('lino', 0)
910
+ print(f"Continuing execution at line {lino + 1}")
911
+
912
+ # Clear the highlight on the current line
913
+ self.setBackground(lino, 'none')
914
+
915
+ # Switch to free-running mode
720
916
  self.stopped = False
721
- print("Continuing execution at line", self.program.pc + 1)
917
+
918
+ # Skip the breakpoint check for the current instruction (the one we're resuming from)
919
+ self.skip_next_breakpoint = True
920
+
921
+ # Restore the saved queue state to resume all forked threads
922
+ self._restoreQueueState()
923
+
924
+ # Enqueue the current thread, then flush immediately
722
925
  self.program.run(self.pc)
926
+ from easycoder.ec_program import flush
927
+ flush()
723
928
 
724
929
  def doStep(self):
930
+ """Execute one instruction and halt again"""
725
931
  command = self.program.code[self.pc]
726
- # print("Stepping at line", command['lino'] + 1)
727
- self.setBackground(command['lino'], 'none')
932
+ lino = command.get('lino', 0)
933
+
934
+ # Clear the highlight on the current line
935
+ self.setBackground(lino, 'none')
936
+
937
+ # Stay in stopped mode (will halt after next instruction)
938
+ self.stopped = True
939
+
940
+ # Skip the breakpoint check for the current instruction (the one we're stepping from)
941
+ self.skip_next_breakpoint = True
942
+
943
+ # Restore the saved queue state to resume all forked threads
944
+ self._restoreQueueState()
945
+
946
+ # Enqueue the current thread, then flush a single cycle
728
947
  self.program.run(self.pc)
948
+ from easycoder.ec_program import flush
949
+ flush()
729
950
 
730
951
  def doStop(self):
952
+ try:
953
+ lino = self.program.code[self.pc].get('lino', 0) + 1
954
+ print(f"Stopped by user at line {lino}")
955
+ except Exception:
956
+ print("Stopped by user")
957
+ # Clear all previous highlights and mark the current line
958
+ try:
959
+ self._clearHighlights()
960
+ current_lino = self.program.code[self.pc].get('lino', 0)
961
+ self.setBackground(current_lino, 'LightYellow')
962
+ except Exception:
963
+ pass
731
964
  self.stopped = True
732
965
 
733
966
  def doClose(self):